diff --git a/R4.01_R4.A.10/README.md b/R4.01_R4.A.10/README.md index a3b6112..36d60c2 100644 --- a/R4.01_R4.A.10/README.md +++ b/R4.01_R4.A.10/README.md @@ -13,7 +13,10 @@ [Promesses, Ajax, API de données](cours/ajax.pdf), [tp4](./td_tp/tp4) #### Semaine 5 -[Programmation déclarative, RIOT.js](cours/riot.pdf), [tp4](./td_tp/tp5) +[Programmation déclarative, RIOT.js](cours/riot.pdf), [tp5](./td_tp/tp5) +#### Semaine 6 +[API REST](cours/api.pdf), service firebase [tp6](./td_tp/tp6) + diff --git a/R4.01_R4.A.10/td_tp/tp6/README.md b/R4.01_R4.A.10/td_tp/tp6/README.md new file mode 100644 index 0000000..5ad2eba --- /dev/null +++ b/R4.01_R4.A.10/td_tp/tp6/README.md @@ -0,0 +1,110 @@ +# TP : Une api rest pour la todo list. + +Le but de l'exercice est d'écrire une api de données restful pour l'application todolist du +[tp2](../tp2mvc) ou [tp5](../tp5). + + +Les routes de notre api : + +``` +get /todo/(:id) +post /todo +delete /todo/:id +put /todo/:id +``` + +- Pour routage et les entrées/sorties http, on utilise [flight php](https://flightphp.com/). +- Pour les entrées/sorties avec la base données, je vous donne un son utilise l'orm [readbean php](https://www.redbeanphp.com/index.php). + +Copiez le fichier `.htaccess` à la racine de vos sources pour activer la réécriture d'urls. + +```apache +Require method GET POST PUT DELETE OPTIONS +RewriteEngine On +RewriteCond %{REQUEST_FILENAME} !-f +RewriteCond %{REQUEST_FILENAME} !-d +RewriteRule ^(.*)$ index.php [QSA,L] +``` + +Votre api pour l'instant + +```php + Flight::request()->data->title , + "done" => Flight::request()->data->done + ]; + + $id = Todo::create($todo); + Flight::response()->header("Location",Flight::request()->url.$id); + $todo['id'] = $id; + Flight::json($todo,201); + +} + +function getTodos($id = null) +{ + $filter = Flight::request()->query->filter ?? "all"; + + if ($id === null){ + switch($filter){ + case "done": + $todos = Todo::findCompleted(); + break; + case "active": + $todos = Todo::findUnCompleted(); + break; + default: + $todos = Todo::findAll(); + } + Flight::json( + [ + "results" => $todos + ] + ); + + } else { + $todo = Todo::find($id); + if ($todo) + Flight::json($todo); + else + Flight::halt(404); + } +} + + + +Flight::start(); +``` + +Le modèle utilise PDO, en php. Il vous faudra créér une table avec les attributs nécessaires. + + + + +1. Complétez le fichier index.php +2. Testez votre api à la ligne de commandes en utilisant `curl`. +3. Connectez votre application todolist avec l'api. + +Écrivez un module "abstrait" en javascript pour l'interaction +avec l'api. Ce module devra pouvoir être changer pour utiliser firebase **sans que l'application cliente ne change**. diff --git a/R4.01_R4.A.10/td_tp/tp6/src/api_php/flight/Engine.php b/R4.01_R4.A.10/td_tp/tp6/src/api_php/flight/Engine.php new file mode 100644 index 0000000..2559810 --- /dev/null +++ b/R4.01_R4.A.10/td_tp/tp6/src/api_php/flight/Engine.php @@ -0,0 +1,911 @@ + + * + * # Core methods + * @method void start() Starts engine + * @method void stop() Stops framework and outputs current response + * @method void halt(int $code = 200, string $message = '', bool $actuallyExit = true) Stops processing and returns a given response. + * + * # Routing + * @method Route route(string $pattern, callable|string $callback, bool $pass_route = false, string $alias = '') + * Routes a URL to a callback function with all applicable methods + * @method void group(string $pattern, callable $callback, array $group_middlewares = []) + * Groups a set of routes together under a common prefix. + * @method Route post(string $pattern, callable|string $callback, bool $pass_route = false, string $alias = '') + * Routes a POST URL to a callback function. + * @method Route put(string $pattern, callable|string $callback, bool $pass_route = false, string $alias = '') + * Routes a PUT URL to a callback function. + * @method Route patch(string $pattern, callable|string $callback, bool $pass_route = false, string $alias = '') + * Routes a PATCH URL to a callback function. + * @method Route delete(string $pattern, callable|string $callback, bool $pass_route = false, string $alias = '') + * Routes a DELETE URL to a callback function. + * @method Router router() Gets router + * @method string getUrl(string $alias) Gets a url from an alias + * + * # Views + * @method void render(string $file, ?array $data = null, ?string $key = null) Renders template + * @method View view() Gets current view + * + * # Request-Response + * @method Request request() Gets current request + * @method Response response() Gets current response + * @method void error(Throwable $e) Sends an HTTP 500 response for any errors. + * @method void notFound() Sends an HTTP 404 response when a URL is not found. + * @method void redirect(string $url, int $code = 303) Redirects the current request to another URL. + * @method void json(mixed $data, int $code = 200, bool $encode = true, string $charset = 'utf-8', int $option = 0) + * Sends a JSON response. + * @method void jsonp(mixed $data, string $param = 'jsonp', int $code = 200, bool $encode = true, string $charset = 'utf-8', int $option = 0) + * Sends a JSONP response. + * + * # HTTP caching + * @method void etag(string $id, ('strong'|'weak') $type = 'strong') Handles ETag HTTP caching. + * @method void lastModified(int $time) Handles last modified HTTP caching. + * + * phpcs:disable PSR2.Methods.MethodDeclaration.Underscore + */ +class Engine +{ + /** + * @var array List of methods that can be extended in the Engine class. + */ + private const MAPPABLE_METHODS = [ + 'start', 'stop', 'route', 'halt', 'error', 'notFound', + 'render', 'redirect', 'etag', 'lastModified', 'json', 'jsonp', + 'post', 'put', 'patch', 'delete', 'group', 'getUrl' + ]; + + /** @var array Stored variables. */ + protected array $vars = []; + + /** Class loader. */ + protected Loader $loader; + + /** Event dispatcher. */ + protected Dispatcher $dispatcher; + + /** If the framework has been initialized or not. */ + protected bool $initialized = false; + + public function __construct() + { + $this->loader = new Loader(); + $this->dispatcher = new Dispatcher(); + $this->init(); + } + + /** + * Handles calls to class methods. + * + * @param string $name Method name + * @param array $params Method parameters + * + * @throws Exception + * @return mixed Callback results + */ + public function __call(string $name, array $params) + { + $callback = $this->dispatcher->get($name); + + if (\is_callable($callback)) { + return $this->dispatcher->run($name, $params); + } + + if (!$this->loader->get($name)) { + throw new Exception("$name must be a mapped method."); + } + + $shared = empty($params) || $params[0]; + + return $this->loader->load($name, $shared); + } + + ////////////////// + // Core Methods // + ////////////////// + + /** Initializes the framework. */ + public function init(): void + { + $initialized = $this->initialized; + $self = $this; + + if ($initialized) { + $this->vars = []; + $this->loader->reset(); + $this->dispatcher->reset(); + } + + // Add this class to Dispatcher + $this->dispatcher->setEngine($this); + + // Register default components + $this->loader->register('request', Request::class); + $this->loader->register('response', Response::class); + $this->loader->register('router', Router::class); + + $this->loader->register('view', View::class, [], function (View $view) use ($self) { + $view->path = $self->get('flight.views.path'); + $view->extension = $self->get('flight.views.extension'); + }); + + foreach (self::MAPPABLE_METHODS as $name) { + $this->dispatcher->set($name, [$this, "_$name"]); + } + + // Default configuration settings + $this->set('flight.base_url'); + $this->set('flight.case_sensitive', false); + $this->set('flight.handle_errors', true); + $this->set('flight.log_errors', false); + $this->set('flight.views.path', './views'); + $this->set('flight.views.extension', '.php'); + $this->set('flight.content_length', true); + $this->set('flight.v2.output_buffering', false); + + // Startup configuration + $this->before('start', function () use ($self) { + // Enable error handling + if ($self->get('flight.handle_errors')) { + set_error_handler([$self, 'handleError']); + set_exception_handler([$self, 'handleException']); + } + + // Set case-sensitivity + $self->router()->case_sensitive = $self->get('flight.case_sensitive'); + // Set Content-Length + $self->response()->content_length = $self->get('flight.content_length'); + // This is to maintain legacy handling of output buffering + // which causes a lot of problems. This will be removed + // in v4 + $self->response()->v2_output_buffering = $this->get('flight.v2.output_buffering'); + }); + + $this->initialized = true; + } + + /** + * Custom error handler. Converts errors into exceptions. + * + * @param int $errno Error number + * @param string $errstr Error string + * @param string $errfile Error file name + * @param int $errline Error file line number + * + * @return false + * @throws ErrorException + */ + public function handleError(int $errno, string $errstr, string $errfile, int $errline): bool + { + if ($errno & error_reporting()) { + throw new ErrorException($errstr, $errno, 0, $errfile, $errline); + } + + return false; + } + + /** + * Custom exception handler. Logs exceptions. + * + * @param Throwable $e Thrown exception + */ + public function handleException(Throwable $e): void + { + if ($this->get('flight.log_errors')) { + error_log($e->getMessage()); // @codeCoverageIgnore + } + + $this->error($e); + } + + /** + * Registers the container handler + * + * @param callable|object $containerHandler Callback function or PSR-11 Container object that sets the container and how it will inject classes + * + * @return void + */ + public function registerContainerHandler($containerHandler): void + { + $this->dispatcher->setContainerHandler($containerHandler); + } + + /** + * Maps a callback to a framework method. + * + * @param string $name Method name + * @param callable $callback Callback function + * + * @throws Exception If trying to map over a framework method + */ + public function map(string $name, callable $callback): void + { + if (method_exists($this, $name)) { + throw new Exception('Cannot override an existing framework method.'); + } + + $this->dispatcher->set($name, $callback); + } + + /** + * Registers a class to a framework method. + * + * # Usage example: + * ``` + * $app = new Engine; + * $app->register('user', User::class); + * + * $app->user(); # <- Return a User instance + * ``` + * + * @param string $name Method name + * @param class-string $class Class name + * @param array $params Class initialization parameters + * @param ?Closure(T $instance): void $callback Function to call after object instantiation + * + * @template T of object + * @throws Exception If trying to map over a framework method + */ + public function register(string $name, string $class, array $params = [], ?callable $callback = null): void + { + if (method_exists($this, $name)) { + throw new Exception('Cannot override an existing framework method.'); + } + + $this->loader->register($name, $class, $params, $callback); + } + + /** Unregisters a class to a framework method. */ + public function unregister(string $methodName): void + { + $this->loader->unregister($methodName); + } + + /** + * Adds a pre-filter to a method. + * + * @param string $name Method name + * @param Closure(array &$params, string &$output): (void|false) $callback + */ + public function before(string $name, callable $callback): void + { + $this->dispatcher->hook($name, 'before', $callback); + } + + /** + * Adds a post-filter to a method. + * + * @param string $name Method name + * @param Closure(array &$params, string &$output): (void|false) $callback + */ + public function after(string $name, callable $callback): void + { + $this->dispatcher->hook($name, 'after', $callback); + } + + /** + * Gets a variable. + * + * @param ?string $key Variable name + * + * @return mixed Variable value or `null` if `$key` doesn't exists. + */ + public function get(?string $key = null) + { + if ($key === null) { + return $this->vars; + } + + return $this->vars[$key] ?? null; + } + + /** + * Sets a variable. + * + * @param string|iterable $key + * Variable name as `string` or an iterable of `'varName' => $varValue` + * @param mixed $value Ignored if `$key` is an `iterable` + */ + public function set($key, $value = null): void + { + if (\is_iterable($key)) { + foreach ($key as $k => $v) { + $this->vars[$k] = $v; + } + + return; + } + + $this->vars[$key] = $value; + } + + /** + * Checks if a variable has been set. + * + * @param string $key Variable name + * + * @return bool Variable status + */ + public function has(string $key): bool + { + return isset($this->vars[$key]); + } + + /** + * Unsets a variable. If no key is passed in, clear all variables. + * + * @param ?string $key Variable name, if `$key` isn't provided, it clear all variables. + */ + public function clear(?string $key = null): void + { + if ($key === null) { + $this->vars = []; + return; + } + + unset($this->vars[$key]); + } + + /** + * Adds a path for class autoloading. + * + * @param string $dir Directory path + */ + public function path(string $dir): void + { + $this->loader->addDirectory($dir); + } + + /** + * Processes each routes middleware. + * + * @param Route $route The route to process the middleware for. + * @param string $eventName If this is the before or after method. + */ + protected function processMiddleware(Route $route, string $eventName): bool + { + $atLeastOneMiddlewareFailed = false; + + // Process things normally for before, and then in reverse order for after. + $middlewares = $eventName === Dispatcher::FILTER_BEFORE + ? $route->middleware + : array_reverse($route->middleware); + $params = $route->params; + + foreach ($middlewares as $middleware) { + // Assume that nothing is going to be executed for the middleware. + $middlewareObject = false; + + // Closure functions can only run on the before event + if ($eventName === Dispatcher::FILTER_BEFORE && is_object($middleware) === true && ($middleware instanceof Closure)) { + $middlewareObject = $middleware; + + // If the object has already been created, we can just use it if the event name exists. + } elseif (is_object($middleware) === true) { + $middlewareObject = method_exists($middleware, $eventName) === true ? [ $middleware, $eventName ] : false; + + // If the middleware is a string, we need to create the object and then call the event. + } elseif (is_string($middleware) === true && method_exists($middleware, $eventName) === true) { + $resolvedClass = null; + + // if there's a container assigned, we should use it to create the object + if ($this->dispatcher->mustUseContainer($middleware) === true) { + $resolvedClass = $this->dispatcher->resolveContainerClass($middleware, $params); + // otherwise just assume it's a plain jane class, so inject the engine + // just like in Dispatcher::invokeCallable() + } elseif (class_exists($middleware) === true) { + $resolvedClass = new $middleware($this); + } + + // If something was resolved, create an array callable that will be passed in later. + if ($resolvedClass !== null) { + $middlewareObject = [ $resolvedClass, $eventName ]; + } + } + + // If nothing was resolved, go to the next thing + if ($middlewareObject === false) { + continue; + } + + // This is the way that v3 handles output buffering (which captures output correctly) + $useV3OutputBuffering = + $this->response()->v2_output_buffering === false && + $route->is_streamed === false; + + if ($useV3OutputBuffering === true) { + ob_start(); + } + + // Here is the array callable $middlewareObject that we created earlier. + // It looks bizarre but it's really calling [ $class, $method ]($params) + // Which loosely translates to $class->$method($params) + $middlewareResult = $middlewareObject($params); + + if ($useV3OutputBuffering === true) { + $this->response()->write(ob_get_clean()); + } + + // If you return false in your middleware, it will halt the request + // and throw a 403 forbidden error by default. + if ($middlewareResult === false) { + $atLeastOneMiddlewareFailed = true; + break; + } + } + + return $atLeastOneMiddlewareFailed; + } + + //////////////////////// + // Extensible Methods // + //////////////////////// + /** + * Starts the framework. + * + * @throws Exception + */ + public function _start(): void + { + $dispatched = false; + $self = $this; + $request = $this->request(); + $response = $this->response(); + $router = $this->router(); + + // Allow filters to run + $this->after('start', function () use ($self) { + $self->stop(); + }); + + if ($response->v2_output_buffering === true) { + // Flush any existing output + if (ob_get_length() > 0) { + $response->write(ob_get_clean()); // @codeCoverageIgnore + } + + // Enable output buffering + // This is closed in the Engine->_stop() method + ob_start(); + } + + // Route the request + $failedMiddlewareCheck = false; + + while ($route = $router->route($request)) { + $params = array_values($route->params); + + // Add route info to the parameter list + if ($route->pass) { + $params[] = $route; + } + + // If this route is to be streamed, we need to output the headers now + if ($route->is_streamed === true) { + if (count($route->streamed_headers) > 0) { + $response->status($route->streamed_headers['status'] ?? 200); + unset($route->streamed_headers['status']); + foreach ($route->streamed_headers as $header => $value) { + $response->header($header, $value); + } + } + + $response->header('X-Accel-Buffering', 'no'); + $response->header('Connection', 'close'); + + // We obviously don't know the content length right now. This must be false. + $response->content_length = false; + $response->sendHeaders(); + $response->markAsSent(); + } + + // Run any before middlewares + if (count($route->middleware) > 0) { + $atLeastOneMiddlewareFailed = $this->processMiddleware($route, 'before'); + if ($atLeastOneMiddlewareFailed === true) { + $failedMiddlewareCheck = true; + break; + } + } + + $useV3OutputBuffering = + $this->response()->v2_output_buffering === false && + $route->is_streamed === false; + + if ($useV3OutputBuffering === true) { + ob_start(); + } + + // Call route handler + $continue = $this->dispatcher->execute( + $route->callback, + $params + ); + + if ($useV3OutputBuffering === true) { + $response->write(ob_get_clean()); + } + + // Run any before middlewares + if (count($route->middleware) > 0) { + // process the middleware in reverse order now + $atLeastOneMiddlewareFailed = $this->processMiddleware($route, 'after'); + + if ($atLeastOneMiddlewareFailed === true) { + $failedMiddlewareCheck = true; + break; + } + } + + $dispatched = true; + + if (!$continue) { + break; + } + + $router->next(); + + $dispatched = false; + } + + // HEAD requests should be identical to GET requests but have no body + if ($request->method === 'HEAD') { + $response->clearBody(); + } + + if ($failedMiddlewareCheck === true) { + $this->halt(403, 'Forbidden', empty(getenv('PHPUNIT_TEST'))); + } elseif ($dispatched === false) { + // Get the previous route and check if the method failed, but the URL was good. + $lastRouteExecuted = $router->executedRoute; + if ($lastRouteExecuted !== null && $lastRouteExecuted->matchUrl($request->url) === true && $lastRouteExecuted->matchMethod($request->method) === false) { + $this->halt(405, 'Method Not Allowed', empty(getenv('PHPUNIT_TEST'))); + } else { + $this->notFound(); + } + } + } + + /** + * Sends an HTTP 500 response for any errors. + * + * @param Throwable $e Thrown exception + */ + public function _error(Throwable $e): void + { + $msg = sprintf( + <<500 Internal Server Error +

%s (%s)

+
%s
+ HTML, + $e->getMessage(), + $e->getCode(), + $e->getTraceAsString() + ); + + try { + $this->response() + ->clear() + ->status(500) + ->write($msg) + ->send(); + // @codeCoverageIgnoreStart + } catch (Throwable $t) { + exit($msg); + } + // @codeCoverageIgnoreEnd + } + + /** + * Stops the framework and outputs the current response. + * + * @param ?int $code HTTP status code + * + * @throws Exception + * @deprecated 3.5.3 This method will be removed in v4 + */ + public function _stop(?int $code = null): void + { + $response = $this->response(); + + if ($response->sent() === false) { + if ($code !== null) { + $response->status($code); + } + + if ($response->v2_output_buffering === true && ob_get_length() > 0) { + $response->write(ob_get_clean()); + } + + $response->send(); + } + } + + /** + * Routes a URL to a callback function. + * + * @param string $pattern URL pattern to match + * @param callable|string $callback Callback function + * @param bool $pass_route Pass the matching route object to the callback + * @param string $alias The alias for the route + */ + public function _route(string $pattern, $callback, bool $pass_route = false, string $alias = ''): Route + { + return $this->router()->map($pattern, $callback, $pass_route, $alias); + } + + /** + * Routes a URL to a callback function. + * + * @param string $pattern URL pattern to match + * @param callable $callback Callback function that includes the Router class as first parameter + * @param array $group_middlewares The middleware to be applied to the route + */ + public function _group(string $pattern, callable $callback, array $group_middlewares = []): void + { + $this->router()->group($pattern, $callback, $group_middlewares); + } + + /** + * Routes a URL to a callback function. + * + * @param string $pattern URL pattern to match + * @param callable|string $callback Callback function or string class->method + * @param bool $pass_route Pass the matching route object to the callback + * + * @return Route + */ + public function _post(string $pattern, $callback, bool $pass_route = false, string $route_alias = ''): Route + { + return $this->router()->map('POST ' . $pattern, $callback, $pass_route, $route_alias); + } + + /** + * Routes a URL to a callback function. + * + * @param string $pattern URL pattern to match + * @param callable|string $callback Callback function or string class->method + * @param bool $pass_route Pass the matching route object to the callback + * + * @return Route + */ + public function _put(string $pattern, $callback, bool $pass_route = false, string $route_alias = ''): Route + { + return $this->router()->map('PUT ' . $pattern, $callback, $pass_route, $route_alias); + } + + /** + * Routes a URL to a callback function. + * + * @param string $pattern URL pattern to match + * @param callable|string $callback Callback function or string class->method + * @param bool $pass_route Pass the matching route object to the callback + * + * @return Route + */ + public function _patch(string $pattern, $callback, bool $pass_route = false, string $route_alias = ''): Route + { + return $this->router()->map('PATCH ' . $pattern, $callback, $pass_route, $route_alias); + } + + /** + * Routes a URL to a callback function. + * + * @param string $pattern URL pattern to match + * @param callable|string $callback Callback function or string class->method + * @param bool $pass_route Pass the matching route object to the callback + * + * @return Route + */ + public function _delete(string $pattern, $callback, bool $pass_route = false, string $route_alias = ''): Route + { + return $this->router()->map('DELETE ' . $pattern, $callback, $pass_route, $route_alias); + } + + /** + * Stops processing and returns a given response. + * + * @param int $code HTTP status code + * @param string $message Response message + * @param bool $actuallyExit Whether to actually exit the script or just send response + */ + public function _halt(int $code = 200, string $message = '', bool $actuallyExit = true): void + { + $this->response() + ->clear() + ->status($code) + ->write($message) + ->send(); + if ($actuallyExit === true) { + exit(); // @codeCoverageIgnore + } + } + + /** Sends an HTTP 404 response when a URL is not found. */ + public function _notFound(): void + { + $output = '

404 Not Found

The page you have requested could not be found.

'; + + $this->response() + ->clear() + ->status(404) + ->write($output) + ->send(); + } + + /** + * Redirects the current request to another URL. + * + * @param int $code HTTP status code + */ + public function _redirect(string $url, int $code = 303): void + { + $base = $this->get('flight.base_url'); + + if ($base === null) { + $base = $this->request()->base; + } + + // Append base url to redirect url + if ($base !== '/' && strpos($url, '://') === false) { + $url = $base . preg_replace('#/+#', '/', '/' . $url); + } + + $this->response() + ->clear() + ->status($code) + ->header('Location', $url) + ->send(); + } + + /** + * Renders a template. + * + * @param string $file Template file + * @param ?array $data Template data + * @param ?string $key View variable name + * + * @throws Exception If template file wasn't found + */ + public function _render(string $file, ?array $data = null, ?string $key = null): void + { + if ($key !== null) { + $this->view()->set($key, $this->view()->fetch($file, $data)); + return; + } + + $this->view()->render($file, $data); + } + + /** + * Sends a JSON response. + * + * @param mixed $data JSON data + * @param int $code HTTP status code + * @param bool $encode Whether to perform JSON encoding + * @param string $charset Charset + * @param int $option Bitmask Json constant such as JSON_HEX_QUOT + * + * @throws Exception + */ + public function _json( + $data, + int $code = 200, + bool $encode = true, + string $charset = 'utf-8', + int $option = 0 + ): void { + $json = $encode ? json_encode($data, $option) : $data; + + $this->response() + ->status($code) + ->header('Content-Type', 'application/json; charset=' . $charset) + ->write($json); + if ($this->response()->v2_output_buffering === true) { + $this->response()->send(); + } + } + + /** + * Sends a JSONP response. + * + * @param mixed $data JSON data + * @param string $param Query parameter that specifies the callback name. + * @param int $code HTTP status code + * @param bool $encode Whether to perform JSON encoding + * @param string $charset Charset + * @param int $option Bitmask Json constant such as JSON_HEX_QUOT + * + * @throws Exception + */ + public function _jsonp( + $data, + string $param = 'jsonp', + int $code = 200, + bool $encode = true, + string $charset = 'utf-8', + int $option = 0 + ): void { + $json = $encode ? json_encode($data, $option) : $data; + $callback = $this->request()->query[$param]; + + $this->response() + ->status($code) + ->header('Content-Type', 'application/javascript; charset=' . $charset) + ->write($callback . '(' . $json . ');'); + if ($this->response()->v2_output_buffering === true) { + $this->response()->send(); + } + } + + /** + * Handles ETag HTTP caching. + * + * @param string $id ETag identifier + * @param 'strong'|'weak' $type ETag type + */ + public function _etag(string $id, string $type = 'strong'): void + { + $id = (($type === 'weak') ? 'W/' : '') . $id; + + $this->response()->header('ETag', '"' . str_replace('"', '\"', $id) . '"'); + + if ( + isset($_SERVER['HTTP_IF_NONE_MATCH']) && + $_SERVER['HTTP_IF_NONE_MATCH'] === $id + ) { + $this->halt(304, '', empty(getenv('PHPUNIT_TEST'))); + } + } + + /** + * Handles last modified HTTP caching. + * + * @param int $time Unix timestamp + */ + public function _lastModified(int $time): void + { + $this->response()->header('Last-Modified', gmdate('D, d M Y H:i:s \G\M\T', $time)); + + if ( + isset($_SERVER['HTTP_IF_MODIFIED_SINCE']) && + strtotime($_SERVER['HTTP_IF_MODIFIED_SINCE']) === $time + ) { + $this->halt(304, '', empty(getenv('PHPUNIT_TEST'))); + } + } + + /** + * Gets a url from an alias that's supplied. + * + * @param string $alias the route alias. + * @param array $params The params for the route if applicable. + */ + public function _getUrl(string $alias, array $params = []): string + { + return $this->router()->getUrlByAlias($alias, $params); + } +} diff --git a/R4.01_R4.A.10/td_tp/tp6/src/api_php/flight/Flight.php b/R4.01_R4.A.10/td_tp/tp6/src/api_php/flight/Flight.php new file mode 100644 index 0000000..a04ad80 --- /dev/null +++ b/R4.01_R4.A.10/td_tp/tp6/src/api_php/flight/Flight.php @@ -0,0 +1,144 @@ + + * + * # Core methods + * @method static void start() Starts the framework. + * @method static void path(string $path) Adds a path for autoloading classes. + * @method static void stop(?int $code = null) Stops the framework and sends a response. + * @method static void halt(int $code = 200, string $message = '', bool $actuallyExit = true) + * Stop the framework with an optional status code and message. + * @method static void register(string $name, string $class, array $params = [], ?callable $callback = null) + * Registers a class to a framework method. + * @method static void unregister(string $methodName) + * Unregisters a class to a framework method. + * @method static void registerContainerHandler(callable|object $containerHandler) Registers a container handler. + * + * # Routing + * @method static Route route(string $pattern, callable|string $callback, bool $pass_route = false, string $alias = '') + * Maps a URL pattern to a callback with all applicable methods. + * @method static void group(string $pattern, callable $callback, callable[] $group_middlewares = []) + * Groups a set of routes together under a common prefix. + * @method static Route post(string $pattern, callable|string $callback, bool $pass_route = false, string $alias = '') + * Routes a POST URL to a callback function. + * @method static Route put(string $pattern, callable|string $callback, bool $pass_route = false, string $alias = '') + * Routes a PUT URL to a callback function. + * @method static Route patch(string $pattern, callable|string $callback, bool $pass_route = false, string $alias = '') + * Routes a PATCH URL to a callback function. + * @method static Route delete(string $pattern, callable|string $callback, bool $pass_route = false, string $alias = '') + * Routes a DELETE URL to a callback function. + * @method static Router router() Returns Router instance. + * @method static string getUrl(string $alias, array $params = []) Gets a url from an alias + * + * @method static void map(string $name, callable $callback) Creates a custom framework method. + * + * @method static void before(string $name, Closure(array &$params, string &$output): (void|false) $callback) + * Adds a filter before a framework method. + * @method static void after(string $name, Closure(array &$params, string &$output): (void|false) $callback) + * Adds a filter after a framework method. + * + * @method static void set(string|iterable $key, mixed $value) Sets a variable. + * @method static mixed get(?string $key) Gets a variable. + * @method static bool has(string $key) Checks if a variable is set. + * @method static void clear(?string $key = null) Clears a variable. + * + * # Views + * @method static void render(string $file, ?array $data = null, ?string $key = null) + * Renders a template file. + * @method static View view() Returns View instance. + * + * # Request-Response + * @method static Request request() Returns Request instance. + * @method static Response response() Returns Response instance. + * @method static void redirect(string $url, int $code = 303) Redirects to another URL. + * @method static void json(mixed $data, int $code = 200, bool $encode = true, string $charset = "utf8", int $encodeOption = 0, int $encodeDepth = 512) + * Sends a JSON response. + * @method static void jsonp(mixed $data, string $param = 'jsonp', int $code = 200, bool $encode = true, string $charset = "utf8", int $encodeOption = 0, int $encodeDepth = 512) + * Sends a JSONP response. + * @method static void error(Throwable $exception) Sends an HTTP 500 response. + * @method static void notFound() Sends an HTTP 404 response. + * + * # HTTP caching + * @method static void etag(string $id, ('strong'|'weak') $type = 'strong') Performs ETag HTTP caching. + * @method static void lastModified(int $time) Performs last modified HTTP caching. + */ +class Flight +{ + /** Framework engine. */ + private static Engine $engine; + + /** Whether or not the app has been initialized. */ + private static bool $initialized = false; + + /** + * Don't allow object instantiation + * + * @codeCoverageIgnore + * @return void + */ + private function __construct() + { + } + + /** + * Forbid cloning the class + * + * @codeCoverageIgnore + * @return void + */ + private function __clone() + { + } + + /** + * Handles calls to static methods. + * + * @param string $name Method name + * @param array $params Method parameters + * + * @return mixed Callback results + * @throws Exception + */ + public static function __callStatic(string $name, array $params) + { + return self::app()->{$name}(...$params); + } + + /** @return Engine Application instance */ + public static function app(): Engine + { + if (!self::$initialized) { + require_once __DIR__ . '/autoload.php'; + + self::setEngine(new Engine()); + self::$initialized = true; + } + + return self::$engine; + } + + /** + * Set the engine instance + * + * @param Engine $engine Vroom vroom! + */ + public static function setEngine(Engine $engine): void + { + self::$engine = $engine; + } +} diff --git a/R4.01_R4.A.10/td_tp/tp6/src/api_php/flight/autoload.php b/R4.01_R4.A.10/td_tp/tp6/src/api_php/flight/autoload.php new file mode 100644 index 0000000..0a31c86 --- /dev/null +++ b/R4.01_R4.A.10/td_tp/tp6/src/api_php/flight/autoload.php @@ -0,0 +1,10 @@ + $config JSON config from .runway-config.json + */ + public function __construct(array $config) + { + parent::__construct('make:controller', 'Create a controller', $config); + $this->argument('', 'The name of the controller to create (with or without the Controller suffix)'); + } + + /** + * Executes the function + * + * @return void + */ + public function execute(string $controller) + { + $io = $this->app()->io(); + if (isset($this->config['app_root']) === false) { + $io->error('app_root not set in .runway-config.json', true); + return; + } + + if (!preg_match('/Controller$/', $controller)) { + $controller .= 'Controller'; + } + + $controllerPath = getcwd() . DIRECTORY_SEPARATOR . $this->config['app_root'] . 'controllers' . DIRECTORY_SEPARATOR . $controller . '.php'; + if (file_exists($controllerPath) === true) { + $io->error($controller . ' already exists.', true); + return; + } + + if (is_dir(dirname($controllerPath)) === false) { + $io->info('Creating directory ' . dirname($controllerPath), true); + mkdir(dirname($controllerPath), 0755, true); + } + + $file = new PhpFile(); + $file->setStrictTypes(); + + $namespace = new PhpNamespace('app\\controllers'); + $namespace->addUse('flight\\Engine'); + + $class = new ClassType($controller); + $class->addProperty('app') + ->setVisibility('protected') + ->setType('flight\\Engine') + ->addComment('@var Engine'); + $method = $class->addMethod('__construct') + ->addComment('Constructor') + ->setVisibility('public') + ->setBody('$this->app = $app;'); + $method->addParameter('app') + ->setType('flight\\Engine'); + + $namespace->add($class); + $file->addNamespace($namespace); + + $this->persistClass($controller, $file); + + $io->ok('Controller successfully created at ' . $controllerPath, true); + } + + /** + * Saves the class name to a file + * + * @param string $controllerName Name of the Controller + * @param PhpFile $file Class Object from Nette\PhpGenerator + * + * @return void + */ + protected function persistClass(string $controllerName, PhpFile $file) + { + $printer = new \Nette\PhpGenerator\PsrPrinter(); + file_put_contents(getcwd() . DIRECTORY_SEPARATOR . $this->config['app_root'] . 'controllers' . DIRECTORY_SEPARATOR . $controllerName . '.php', $printer->printFile($file)); + } +} diff --git a/R4.01_R4.A.10/td_tp/tp6/src/api_php/flight/commands/RouteCommand.php b/R4.01_R4.A.10/td_tp/tp6/src/api_php/flight/commands/RouteCommand.php new file mode 100644 index 0000000..a34b821 --- /dev/null +++ b/R4.01_R4.A.10/td_tp/tp6/src/api_php/flight/commands/RouteCommand.php @@ -0,0 +1,126 @@ + $config JSON config from .runway-config.json + */ + public function __construct(array $config) + { + parent::__construct('routes', 'Gets all routes for an application', $config); + + $this->option('--get', 'Only return GET requests'); + $this->option('--post', 'Only return POST requests'); + $this->option('--delete', 'Only return DELETE requests'); + $this->option('--put', 'Only return PUT requests'); + $this->option('--patch', 'Only return PATCH requests'); + } + + /** + * Executes the function + * + * @return void + */ + public function execute() + { + $io = $this->app()->io(); + + if (isset($this->config['index_root']) === false) { + $io->error('index_root not set in .runway-config.json', true); + return; + } + + $io->bold('Routes', true); + + $cwd = getcwd(); + + $index_root = $cwd . '/' . $this->config['index_root']; + + // This makes it so the framework doesn't actually execute + Flight::map('start', function () { + return; + }); + include($index_root); + $routes = Flight::router()->getRoutes(); + $arrayOfRoutes = []; + foreach ($routes as $route) { + if ($this->shouldAddRoute($route) === true) { + $middlewares = []; + if (!empty($route->middleware)) { + try { + $middlewares = array_map(function ($middleware) { + $middleware_class_name = explode("\\", get_class($middleware)); + return preg_match("/^class@anonymous/", end($middleware_class_name)) ? 'Anonymous' : end($middleware_class_name); + }, $route->middleware); + } catch (\TypeError $e) { + $middlewares[] = 'Bad Middleware'; + } finally { + if (is_string($route->middleware) === true) { + $middlewares[] = $route->middleware; + } + } + } + + $arrayOfRoutes[] = [ + 'Pattern' => $route->pattern, + 'Methods' => implode(', ', $route->methods), + 'Alias' => $route->alias ?? '', + 'Streamed' => $route->is_streamed ? 'Yes' : 'No', + 'Middleware' => !empty($middlewares) ? implode(",", $middlewares) : '-' + ]; + } + } + $io->table($arrayOfRoutes, [ + 'head' => 'boldGreen' + ]); + } + + /** + * Whether or not to add the route based on the request + * + * @param Route $route Flight Route object + * + * @return boolean + */ + public function shouldAddRoute(Route $route) + { + $boolval = false; + + $showAll = !$this->get && !$this->post && !$this->put && !$this->delete && !$this->patch; + if ($showAll === true) { + $boolval = true; + } else { + $methods = [ 'GET', 'POST', 'PUT', 'DELETE', 'PATCH' ]; + foreach ($methods as $method) { + $lowercaseMethod = strtolower($method); + if ( + $this->{$lowercaseMethod} === true && + ( + $route->methods[0] === '*' || + in_array($method, $route->methods, true) === true + ) + ) { + $boolval = true; + break; + } + } + } + return $boolval; + } +} diff --git a/R4.01_R4.A.10/td_tp/tp6/src/api_php/flight/core/Dispatcher.php b/R4.01_R4.A.10/td_tp/tp6/src/api_php/flight/core/Dispatcher.php new file mode 100644 index 0000000..20e27b8 --- /dev/null +++ b/R4.01_R4.A.10/td_tp/tp6/src/api_php/flight/core/Dispatcher.php @@ -0,0 +1,504 @@ + + */ +class Dispatcher +{ + public const FILTER_BEFORE = 'before'; + public const FILTER_AFTER = 'after'; + + /** Exception message if thrown by setting the container as a callable method. */ + protected ?Throwable $containerException = null; + + /** @var ?Engine $engine Engine instance. */ + protected ?Engine $engine = null; + + /** @var array Mapped events. */ + protected array $events = []; + + /** + * Method filters. + * + * @var array &$params, mixed &$output): (void|false)>>> + */ + protected array $filters = []; + + /** + * This is a container for the dependency injection. + * + * @var null|ContainerInterface|(callable(string $classString, array $params): (null|object)) + */ + protected $containerHandler = null; + + /** + * Sets the dependency injection container handler. + * + * @param ContainerInterface|(callable(string $classString, array $params): (null|object)) $containerHandler + * Dependency injection container. + * + * @throws InvalidArgumentException If $containerHandler is not a `callable` or instance of `Psr\Container\ContainerInterface`. + */ + public function setContainerHandler($containerHandler): void + { + $containerInterfaceNS = '\Psr\Container\ContainerInterface'; + + if ( + is_a($containerHandler, $containerInterfaceNS) + || is_callable($containerHandler) + ) { + $this->containerHandler = $containerHandler; + + return; + } + + throw new InvalidArgumentException( + "\$containerHandler must be of type callable or instance $containerInterfaceNS" + ); + } + + public function setEngine(Engine $engine): void + { + $this->engine = $engine; + } + + /** + * Dispatches an event. + * + * @param string $name Event name. + * @param array $params Callback parameters. + * + * @return mixed Output of callback + * @throws Exception If event name isn't found or if event throws an `Exception`. + */ + public function run(string $name, array $params = []) + { + $this->runPreFilters($name, $params); + $output = $this->runEvent($name, $params); + + return $this->runPostFilters($name, $output); + } + + /** + * @param array &$params + * + * @return $this + * @throws Exception + */ + protected function runPreFilters(string $eventName, array &$params): self + { + $thereAreBeforeFilters = !empty($this->filters[$eventName][self::FILTER_BEFORE]); + + if ($thereAreBeforeFilters) { + $this->filter($this->filters[$eventName][self::FILTER_BEFORE], $params, $output); + } + + return $this; + } + + /** + * @param array &$params + * + * @return void|mixed + * @throws Exception + */ + protected function runEvent(string $eventName, array &$params) + { + $requestedMethod = $this->get($eventName); + + if ($requestedMethod === null) { + throw new Exception("Event '$eventName' isn't found."); + } + + return $this->execute($requestedMethod, $params); + } + + /** + * @param mixed &$output + * + * @return mixed + * @throws Exception + */ + protected function runPostFilters(string $eventName, &$output) + { + static $params = []; + + $thereAreAfterFilters = !empty($this->filters[$eventName][self::FILTER_AFTER]); + + if ($thereAreAfterFilters) { + $this->filter($this->filters[$eventName][self::FILTER_AFTER], $params, $output); + } + + return $output; + } + + /** + * Assigns a callback to an event. + * + * @param string $name Event name. + * @param callable(): (void|mixed) $callback Callback function. + * + * @return $this + */ + public function set(string $name, callable $callback): self + { + $this->events[$name] = $callback; + + return $this; + } + + /** + * Gets an assigned callback. + * + * @param string $name Event name. + * + * @return null|(callable(): (void|mixed)) $callback Callback function. + */ + public function get(string $name): ?callable + { + return $this->events[$name] ?? null; + } + + /** + * Checks if an event has been set. + * + * @param string $name Event name. + * + * @return bool If event exists or doesn't exists. + */ + public function has(string $name): bool + { + return isset($this->events[$name]); + } + + /** + * Clears an event. If no name is given, all events will be removed. + * + * @param ?string $name Event name. + */ + public function clear(?string $name = null): void + { + if ($name !== null) { + unset($this->events[$name]); + unset($this->filters[$name]); + + return; + } + + $this->reset(); + } + + /** + * Hooks a callback to an event. + * + * @param string $name Event name + * @param 'before'|'after' $type Filter type. + * @param callable(array &$params, mixed &$output): (void|false)|callable(mixed &$output): (void|false) $callback + * + * @return $this + */ + public function hook(string $name, string $type, callable $callback): self + { + static $filterTypes = [self::FILTER_BEFORE, self::FILTER_AFTER]; + + if (!in_array($type, $filterTypes, true)) { + $noticeMessage = "Invalid filter type '$type', use " . join('|', $filterTypes); + + trigger_error($noticeMessage, E_USER_NOTICE); + } + + if ($type === self::FILTER_AFTER) { + $callbackInfo = new ReflectionFunction($callback); + $parametersNumber = $callbackInfo->getNumberOfParameters(); + + if ($parametersNumber === 1) { + /** @disregard &$params in after filters are deprecated. */ + $callback = fn (array &$params, &$output) => $callback($output); + } + } + + $this->filters[$name][$type][] = $callback; + + return $this; + } + + /** + * Executes a chain of method filters. + * + * @param array &$params, mixed &$output): (void|false)> $filters + * Chain of filters. + * @param array $params Method parameters. + * @param mixed $output Method output. + * + * @throws Exception If an event throws an `Exception` or if `$filters` contains an invalid filter. + */ + public function filter(array $filters, array &$params, &$output): void + { + foreach ($filters as $key => $callback) { + if (!is_callable($callback)) { + throw new InvalidArgumentException("Invalid callable \$filters[$key]."); + } + + $continue = $callback($params, $output); + + if ($continue === false) { + break; + } + } + } + + /** + * Executes a callback function. + * + * @param callable-string|(callable(): mixed)|array{class-string|object, string} $callback + * Callback function. + * @param array $params Function parameters. + * + * @return mixed Function results. + * @throws Exception If `$callback` also throws an `Exception`. + */ + public function execute($callback, array &$params = []) + { + if ( + is_string($callback) === true + && (strpos($callback, '->') !== false || strpos($callback, '::') !== false) + ) { + $callback = $this->parseStringClassAndMethod($callback); + } + + return $this->invokeCallable($callback, $params); + } + + /** + * Parses a string into a class and method. + * + * @param string $classAndMethod Class and method + * + * @return array{0: class-string|object, 1: string} Class and method + */ + public function parseStringClassAndMethod(string $classAndMethod): array + { + $classParts = explode('->', $classAndMethod); + + if (count($classParts) === 1) { + $classParts = explode('::', $classParts[0]); + } + + return $classParts; + } + + /** + * Calls a function. + * + * @param callable $func Name of function to call. + * @param array &$params Function parameters. + * + * @return mixed Function results. + * @deprecated 3.7.0 Use invokeCallable instead + */ + public function callFunction(callable $func, array &$params = []) + { + return $this->invokeCallable($func, $params); + } + + /** + * Invokes a method. + * + * @param array{0: class-string|object, 1: string} $func Class method. + * @param array &$params Class method parameters. + * + * @return mixed Function results. + * @throws TypeError For nonexistent class name. + * @deprecated 3.7.0 Use invokeCallable instead. + */ + public function invokeMethod(array $func, array &$params = []) + { + return $this->invokeCallable($func, $params); + } + + /** + * Invokes a callable (anonymous function or Class->method). + * + * @param array{0: class-string|object, 1: string}|callable $func Class method. + * @param array &$params Class method parameters. + * + * @return mixed Function results. + * @throws TypeError For nonexistent class name. + * @throws InvalidArgumentException If the constructor requires parameters. + * @version 3.7.0 + */ + public function invokeCallable($func, array &$params = []) + { + // If this is a directly callable function, call it + if (is_array($func) === false) { + $this->verifyValidFunction($func); + + return call_user_func_array($func, $params); + } + + [$class, $method] = $func; + + $mustUseTheContainer = $this->mustUseContainer($class); + + if ($mustUseTheContainer === true) { + $resolvedClass = $this->resolveContainerClass($class, $params); + + if ($resolvedClass) { + $class = $resolvedClass; + } + } + + $this->verifyValidClassCallable($class, $method, $resolvedClass ?? null); + + // Class is a string, and method exists, create the object by hand and inject only the Engine + if (is_string($class)) { + $class = new $class($this->engine); + } + + return call_user_func_array([$class, $method], $params); + } + + /** + * Handles invalid callback types. + * + * @param callable-string|(callable(): mixed)|array{0: class-string|object, 1: string} $callback + * Callback function. + * + * @throws InvalidArgumentException If `$callback` is an invalid type. + */ + protected function verifyValidFunction($callback): void + { + if (is_string($callback) && !function_exists($callback)) { + throw new InvalidArgumentException('Invalid callback specified.'); + } + } + + + /** + * Verifies if the provided class and method are valid callable. + * + * @param class-string|object $class The class name. + * @param string $method The method name. + * @param object|null $resolvedClass The resolved class. + * + * @throws Exception If the class or method is not found. + */ + protected function verifyValidClassCallable($class, $method, $resolvedClass): void + { + $exception = null; + + // Final check to make sure it's actually a class and a method, or throw an error + if (is_object($class) === false && class_exists($class) === false) { + $exception = new Exception("Class '$class' not found. Is it being correctly autoloaded with Flight::path()?"); + + // If this tried to resolve a class in a container and failed somehow, throw the exception + } elseif (!$resolvedClass && $this->containerException !== null) { + $exception = $this->containerException; + + // Class is there, but no method + } elseif (is_object($class) === true && method_exists($class, $method) === false) { + $classNamespace = get_class($class); + $exception = new Exception("Class found, but method '$classNamespace::$method' not found."); + } + + if ($exception !== null) { + $this->fixOutputBuffering(); + + throw $exception; + } + } + + /** + * Resolves the container class. + * + * @param class-string $class Class name. + * @param array &$params Class constructor parameters. + * + * @return ?object Class object. + */ + public function resolveContainerClass(string $class, array &$params) + { + // PSR-11 + if ( + is_a($this->containerHandler, '\Psr\Container\ContainerInterface') + && $this->containerHandler->has($class) + ) { + return $this->containerHandler->get($class); + } + + // Just a callable where you configure the behavior (Dice, PHP-DI, etc.) + if (is_callable($this->containerHandler)) { + /* This is to catch all the error that could be thrown by whatever + container you are using */ + try { + return ($this->containerHandler)($class, $params); + + // could not resolve a class for some reason + } catch (Exception $exception) { + // If the container throws an exception, we need to catch it + // and store it somewhere. If we just let it throw itself, it + // doesn't properly close the output buffers and can cause other + // issues. + // This is thrown in the verifyValidClassCallable method. + $this->containerException = $exception; + } + } + + return null; + } + + /** + * Checks to see if a container should be used or not. + * + * @param string|object $class the class to verify + * + * @return boolean + */ + public function mustUseContainer($class): bool + { + return $this->containerHandler !== null && ( + (is_object($class) === true && strpos(get_class($class), 'flight\\') === false) + || is_string($class) + ); + } + + /** Because this could throw an exception in the middle of an output buffer, */ + protected function fixOutputBuffering(): void + { + // Cause PHPUnit has 1 level of output buffering by default + if (ob_get_level() > (getenv('PHPUNIT_TEST') ? 1 : 0)) { + ob_end_clean(); + } + } + + /** + * Resets the object to the initial state. + * + * @return $this + */ + public function reset(): self + { + $this->events = []; + $this->filters = []; + + return $this; + } +} diff --git a/R4.01_R4.A.10/td_tp/tp6/src/api_php/flight/core/Loader.php b/R4.01_R4.A.10/td_tp/tp6/src/api_php/flight/core/Loader.php new file mode 100644 index 0000000..1824b9c --- /dev/null +++ b/R4.01_R4.A.10/td_tp/tp6/src/api_php/flight/core/Loader.php @@ -0,0 +1,241 @@ + + */ +class Loader +{ + /** + * Registered classes. + * + * @var array, ?callable}> $classes + */ + protected array $classes = []; + + /** + * If this is disabled, classes can load with underscores + */ + protected static bool $v2ClassLoading = true; + + /** + * Class instances. + * + * @var array + */ + protected array $instances = []; + + /** + * Autoload directories. + * + * @var array + */ + protected static array $dirs = []; + + /** + * Registers a class. + * + * @param string $name Registry name + * @param class-string|Closure(): T $class Class name or function to instantiate class + * @param array $params Class initialization parameters + * @param ?Closure(T $instance): void $callback $callback Function to call after object instantiation + * + * @template T of object + */ + public function register(string $name, $class, array $params = [], ?callable $callback = null): void + { + unset($this->instances[$name]); + + $this->classes[$name] = [$class, $params, $callback]; + } + + /** + * Unregisters a class. + * + * @param string $name Registry name + */ + public function unregister(string $name): void + { + unset($this->classes[$name]); + } + + /** + * Loads a registered class. + * + * @param string $name Method name + * @param bool $shared Shared instance + * + * @throws Exception + * + * @return ?object Class instance + */ + public function load(string $name, bool $shared = true): ?object + { + $obj = null; + + if (isset($this->classes[$name])) { + [0 => $class, 1 => $params, 2 => $callback] = $this->classes[$name]; + + $exists = isset($this->instances[$name]); + + if ($shared) { + $obj = ($exists) ? + $this->getInstance($name) : + $this->newInstance($class, $params); + + if (!$exists) { + $this->instances[$name] = $obj; + } + } else { + $obj = $this->newInstance($class, $params); + } + + if ($callback && (!$shared || !$exists)) { + $ref = [&$obj]; + \call_user_func_array($callback, $ref); + } + } + + return $obj; + } + + /** + * Gets a single instance of a class. + * + * @param string $name Instance name + * + * @return ?object Class instance + */ + public function getInstance(string $name): ?object + { + return $this->instances[$name] ?? null; + } + + /** + * Gets a new instance of a class. + * + * @param class-string|Closure(): class-string $class Class name or callback function to instantiate class + * @param array $params Class initialization parameters + * + * @template T of object + * + * @throws Exception + * + * @return T Class instance + */ + public function newInstance($class, array $params = []) + { + if (\is_callable($class)) { + return \call_user_func_array($class, $params); + } + + return new $class(...$params); + } + + /** + * Gets a registered callable + * + * @param string $name Registry name + * + * @return mixed Class information or null if not registered + */ + public function get(string $name) + { + return $this->classes[$name] ?? null; + } + + /** + * Resets the object to the initial state. + */ + public function reset(): void + { + $this->classes = []; + $this->instances = []; + } + + // Autoloading Functions + + /** + * Starts/stops autoloader. + * + * @param bool $enabled Enable/disable autoloading + * @param string|iterable $dirs Autoload directories + */ + public static function autoload(bool $enabled = true, $dirs = []): void + { + if ($enabled) { + spl_autoload_register([__CLASS__, 'loadClass']); + } else { + spl_autoload_unregister([__CLASS__, 'loadClass']); // @codeCoverageIgnore + } + + if (!empty($dirs)) { + self::addDirectory($dirs); + } + } + + /** + * Autoloads classes. + * + * Classes are not allowed to have underscores in their names. + * + * @param string $class Class name + */ + public static function loadClass(string $class): void + { + $replace_chars = self::$v2ClassLoading === true ? ['\\', '_'] : ['\\']; + $classFile = str_replace($replace_chars, '/', $class) . '.php'; + + foreach (self::$dirs as $dir) { + $filePath = "$dir/$classFile"; + + if (file_exists($filePath)) { + require_once $filePath; + return; + } + } + } + + /** + * Adds a directory for autoloading classes. + * + * @param string|iterable $dir Directory path + */ + public static function addDirectory($dir): void + { + if (\is_array($dir) || \is_object($dir)) { + foreach ($dir as $value) { + self::addDirectory($value); + } + } elseif (\is_string($dir)) { + if (!\in_array($dir, self::$dirs, true)) { + self::$dirs[] = $dir; + } + } + } + + + /** + * Sets the value for V2 class loading. + * + * @param bool $value The value to set for V2 class loading. + * + * @return void + */ + public static function setV2ClassLoading(bool $value): void + { + self::$v2ClassLoading = $value; + } +} diff --git a/R4.01_R4.A.10/td_tp/tp6/src/api_php/flight/database/PdoWrapper.php b/R4.01_R4.A.10/td_tp/tp6/src/api_php/flight/database/PdoWrapper.php new file mode 100644 index 0000000..297121a --- /dev/null +++ b/R4.01_R4.A.10/td_tp/tp6/src/api_php/flight/database/PdoWrapper.php @@ -0,0 +1,150 @@ +runQuery("SELECT * FROM table WHERE something = ?", [ $something ]); + * while($row = $statement->fetch()) { + * // ... + * } + * + * $db->runQuery("INSERT INTO table (name) VALUES (?)", [ $name ]); + * $db->runQuery("UPDATE table SET name = ? WHERE id = ?", [ $name, $id ]); + * + * @param string $sql - Ex: "SELECT * FROM table WHERE something = ?" + * @param array $params - Ex: [ $something ] + * + * @return PDOStatement + */ + public function runQuery(string $sql, array $params = []): PDOStatement + { + $processed_sql_data = $this->processInStatementSql($sql, $params); + $sql = $processed_sql_data['sql']; + $params = $processed_sql_data['params']; + $statement = $this->prepare($sql); + $statement->execute($params); + return $statement; + } + + /** + * Pulls one field from the query + * + * Ex: $id = $db->fetchField("SELECT id FROM table WHERE something = ?", [ $something ]); + * + * @param string $sql - Ex: "SELECT id FROM table WHERE something = ?" + * @param array $params - Ex: [ $something ] + * + * @return mixed + */ + public function fetchField(string $sql, array $params = []) + { + $result = $this->fetchRow($sql, $params); + $data = $result->getData(); + return reset($data); + } + + /** + * Pulls one row from the query + * + * Ex: $row = $db->fetchRow("SELECT * FROM table WHERE something = ?", [ $something ]); + * + * @param string $sql - Ex: "SELECT * FROM table WHERE something = ?" + * @param array $params - Ex: [ $something ] + * + * @return Collection + */ + public function fetchRow(string $sql, array $params = []): Collection + { + $sql .= stripos($sql, 'LIMIT') === false ? ' LIMIT 1' : ''; + $result = $this->fetchAll($sql, $params); + return count($result) > 0 ? $result[0] : new Collection(); + } + + /** + * Pulls all rows from the query + * + * Ex: $rows = $db->fetchAll("SELECT * FROM table WHERE something = ?", [ $something ]); + * foreach($rows as $row) { + * // ... + * } + * + * @param string $sql - Ex: "SELECT * FROM table WHERE something = ?" + * @param array $params - Ex: [ $something ] + * + * @return array + */ + public function fetchAll(string $sql, array $params = []) + { + $processed_sql_data = $this->processInStatementSql($sql, $params); + $sql = $processed_sql_data['sql']; + $params = $processed_sql_data['params']; + $statement = $this->prepare($sql); + $statement->execute($params); + $results = $statement->fetchAll(); + if (is_array($results) === true && count($results) > 0) { + foreach ($results as &$result) { + $result = new Collection($result); + } + } else { + $results = []; + } + return $results; + } + + /** + * Don't worry about this guy. Converts stuff for IN statements + * + * Ex: $row = $db->fetchAll("SELECT * FROM table WHERE id = ? AND something IN(?), [ $id, [1,2,3] ]); + * Converts this to "SELECT * FROM table WHERE id = ? AND something IN(?,?,?)" + * + * @param string $sql the sql statement + * @param array $params the params for the sql statement + * + * @return array> + */ + protected function processInStatementSql(string $sql, array $params = []): array + { + // Replace "IN(?)" with "IN(?,?,?)" + $sql = preg_replace('/IN\s*\(\s*\?\s*\)/i', 'IN(?)', $sql); + + $current_index = 0; + while (($current_index = strpos($sql, 'IN(?)', $current_index)) !== false) { + $preceeding_count = substr_count($sql, '?', 0, $current_index - 1); + + $param = $params[$preceeding_count]; + $question_marks = '?'; + + if (is_string($param) || is_array($param)) { + $params_to_use = $param; + if (is_string($param)) { + $params_to_use = explode(',', $param); + } + + foreach ($params_to_use as $key => $value) { + if (is_string($value)) { + $params_to_use[$key] = trim($value); + } + } + + $question_marks = join(',', array_fill(0, count($params_to_use), '?')); + $sql = substr_replace($sql, $question_marks, $current_index + 3, 1); + + array_splice($params, $preceeding_count, 1, $params_to_use); + } + + $current_index += strlen($question_marks) + 4; + } + + return ['sql' => $sql, 'params' => $params]; + } +} diff --git a/R4.01_R4.A.10/td_tp/tp6/src/api_php/flight/net/Request.php b/R4.01_R4.A.10/td_tp/tp6/src/api_php/flight/net/Request.php new file mode 100644 index 0000000..fd9194b --- /dev/null +++ b/R4.01_R4.A.10/td_tp/tp6/src/api_php/flight/net/Request.php @@ -0,0 +1,417 @@ + + * + * The default request properties are: + * + * - **url** - The URL being requested + * - **base** - The parent subdirectory of the URL + * - **method** - The request method (GET, POST, PUT, DELETE) + * - **referrer** - The referrer URL + * - **ip** - IP address of the client + * - **ajax** - Whether the request is an AJAX request + * - **scheme** - The server protocol (http, https) + * - **user_agent** - Browser information + * - **type** - The content type + * - **length** - The content length + * - **query** - Query string parameters + * - **data** - Post parameters + * - **cookies** - Cookie parameters + * - **files** - Uploaded files + * - **secure** - Connection is secure + * - **accept** - HTTP accept parameters + * - **proxy_ip** - Proxy IP address of the client + */ +class Request +{ + /** + * URL being requested + */ + public string $url; + + /** + * Parent subdirectory of the URL + */ + public string $base; + + /** + * Request method (GET, POST, PUT, DELETE) + */ + public string $method; + + /** + * Referrer URL + */ + public string $referrer; + + /** + * IP address of the client + */ + public string $ip; + + /** + * Whether the request is an AJAX request + */ + public bool $ajax; + + /** + * Server protocol (http, https) + */ + public string $scheme; + + /** + * Browser information + */ + public string $user_agent; + + /** + * Content type + */ + public string $type; + + /** + * Content length + */ + public int $length; + + /** + * Query string parameters + */ + public Collection $query; + + /** + * Post parameters + */ + public Collection $data; + + /** + * Cookie parameters + */ + public Collection $cookies; + + /** + * Uploaded files + */ + public Collection $files; + + /** + * Whether the connection is secure + */ + public bool $secure; + + /** + * HTTP accept parameters + */ + public string $accept; + + /** + * Proxy IP address of the client + */ + public string $proxy_ip; + + /** + * HTTP host name + */ + public string $host; + + /** + * Stream path for where to pull the request body from + */ + private string $stream_path = 'php://input'; + + /** + * Raw HTTP request body + */ + public string $body = ''; + + /** + * Constructor. + * + * @param array $config Request configuration + */ + public function __construct(array $config = []) + { + // Default properties + if (empty($config)) { + $config = [ + 'url' => str_replace('@', '%40', self::getVar('REQUEST_URI', '/')), + 'base' => str_replace(['\\', ' '], ['/', '%20'], \dirname(self::getVar('SCRIPT_NAME'))), + 'method' => self::getMethod(), + 'referrer' => self::getVar('HTTP_REFERER'), + 'ip' => self::getVar('REMOTE_ADDR'), + 'ajax' => self::getVar('HTTP_X_REQUESTED_WITH') === 'XMLHttpRequest', + 'scheme' => self::getScheme(), + 'user_agent' => self::getVar('HTTP_USER_AGENT'), + 'type' => self::getVar('CONTENT_TYPE'), + 'length' => intval(self::getVar('CONTENT_LENGTH', 0)), + 'query' => new Collection($_GET), + 'data' => new Collection($_POST), + 'cookies' => new Collection($_COOKIE), + 'files' => new Collection($_FILES), + 'secure' => self::getScheme() === 'https', + 'accept' => self::getVar('HTTP_ACCEPT'), + 'proxy_ip' => self::getProxyIpAddress(), + 'host' => self::getVar('HTTP_HOST'), + ]; + } + + $this->init($config); + } + + /** + * Initialize request properties. + * + * @param array $properties Array of request properties + * + * @return self + */ + public function init(array $properties = []): self + { + // Set all the defined properties + foreach ($properties as $name => $value) { + $this->{$name} = $value; + } + + // Get the requested URL without the base directory + // This rewrites the url in case the public url and base directories match + // (such as installing on a subdirectory in a web server) + // @see testInitUrlSameAsBaseDirectory + if ($this->base !== '/' && $this->base !== '' && strpos($this->url, $this->base) === 0) { + $this->url = substr($this->url, \strlen($this->base)); + } + + // Default url + if (empty($this->url) === true) { + $this->url = '/'; + } else { + // Merge URL query parameters with $_GET + $_GET = array_merge($_GET, self::parseQuery($this->url)); + + $this->query->setData($_GET); + } + + // Check for JSON input + if (strpos($this->type, 'application/json') === 0) { + $body = $this->getBody(); + if ($body !== '') { + $data = json_decode($body, true); + if (is_array($data) === true) { + $this->data->setData($data); + } + } + } + + return $this; + } + + /** + * Gets the body of the request. + * + * @return string Raw HTTP request body + */ + public function getBody(): string + { + $body = $this->body; + + if ($body !== '') { + return $body; + } + + $method = $this->method ?? self::getMethod(); + + if ($method === 'POST' || $method === 'PUT' || $method === 'DELETE' || $method === 'PATCH') { + $body = file_get_contents($this->stream_path); + } + + $this->body = $body; + + return $body; + } + + /** + * Gets the request method. + */ + public static function getMethod(): string + { + $method = self::getVar('REQUEST_METHOD', 'GET'); + + if (isset($_SERVER['HTTP_X_HTTP_METHOD_OVERRIDE']) === true) { + $method = $_SERVER['HTTP_X_HTTP_METHOD_OVERRIDE']; + } elseif (isset($_REQUEST['_method']) === true) { + $method = $_REQUEST['_method']; + } + + return strtoupper($method); + } + + /** + * Gets the real remote IP address. + * + * @return string IP address + */ + public static function getProxyIpAddress(): string + { + $forwarded = [ + 'HTTP_CLIENT_IP', + 'HTTP_X_FORWARDED_FOR', + 'HTTP_X_FORWARDED', + 'HTTP_X_CLUSTER_CLIENT_IP', + 'HTTP_FORWARDED_FOR', + 'HTTP_FORWARDED', + ]; + + $flags = \FILTER_FLAG_NO_PRIV_RANGE | \FILTER_FLAG_NO_RES_RANGE; + + foreach ($forwarded as $key) { + if (\array_key_exists($key, $_SERVER) === true) { + sscanf($_SERVER[$key], '%[^,]', $ip); + if (filter_var($ip, \FILTER_VALIDATE_IP, $flags) !== false) { + return $ip; + } + } + } + + return ''; + } + + /** + * Gets a variable from $_SERVER using $default if not provided. + * + * @param string $var Variable name + * @param mixed $default Default value to substitute + * + * @return mixed Server variable value + */ + public static function getVar(string $var, $default = '') + { + return $_SERVER[$var] ?? $default; + } + + /** + * This will pull a header from the request. + * + * @param string $header Header name. Can be caps, lowercase, or mixed. + * @param string $default Default value if the header does not exist + * + * @return string + */ + public static function getHeader(string $header, $default = ''): string + { + $header = 'HTTP_' . strtoupper(str_replace('-', '_', $header)); + return self::getVar($header, $default); + } + + /** + * Gets all the request headers + * + * @return array + */ + public static function getHeaders(): array + { + $headers = []; + foreach ($_SERVER as $key => $value) { + if (strpos($key, 'HTTP_') === 0) { + // converts headers like HTTP_CUSTOM_HEADER to Custom-Header + $key = str_replace(' ', '-', ucwords(str_replace('_', ' ', strtolower(substr($key, 5))))); + $headers[$key] = $value; + } + } + return $headers; + } + + /** + * Alias of Request->getHeader(). Gets a single header. + * + * @param string $header Header name. Can be caps, lowercase, or mixed. + * @param string $default Default value if the header does not exist + * + * @return string + */ + public static function header(string $header, $default = '') + { + return self::getHeader($header, $default); + } + + /** + * Alias of Request->getHeaders(). Gets all the request headers + * + * @return array + */ + public static function headers(): array + { + return self::getHeaders(); + } + + /** + * Gets the full request URL. + * + * @return string URL + */ + public function getFullUrl(): string + { + return $this->scheme . '://' . $this->host . $this->url; + } + + /** + * Grabs the scheme and host. Does not end with a / + * + * @return string + */ + public function getBaseUrl(): string + { + return $this->scheme . '://' . $this->host; + } + + /** + * Parse query parameters from a URL. + * + * @param string $url URL string + * + * @return array> + */ + public static function parseQuery(string $url): array + { + $params = []; + + $args = parse_url($url); + if (isset($args['query']) === true) { + parse_str($args['query'], $params); + } + + return $params; + } + + /** + * Gets the URL Scheme + * + * @return string 'http'|'https' + */ + public static function getScheme(): string + { + if ( + (isset($_SERVER['HTTPS']) === true && strtolower($_SERVER['HTTPS']) === 'on') + || + (isset($_SERVER['HTTP_X_FORWARDED_PROTO']) === true && $_SERVER['HTTP_X_FORWARDED_PROTO'] === 'https') + || + (isset($_SERVER['HTTP_FRONT_END_HTTPS']) === true && $_SERVER['HTTP_FRONT_END_HTTPS'] === 'on') + || + (isset($_SERVER['REQUEST_SCHEME']) === true && $_SERVER['REQUEST_SCHEME'] === 'https') + ) { + return 'https'; + } + + return 'http'; + } +} diff --git a/R4.01_R4.A.10/td_tp/tp6/src/api_php/flight/net/Response.php b/R4.01_R4.A.10/td_tp/tp6/src/api_php/flight/net/Response.php new file mode 100644 index 0000000..1798de5 --- /dev/null +++ b/R4.01_R4.A.10/td_tp/tp6/src/api_php/flight/net/Response.php @@ -0,0 +1,473 @@ + + */ +class Response +{ + /** + * Content-Length header. + */ + public bool $content_length = true; + + /** + * This is to maintain legacy handling of output buffering + * which causes a lot of problems. This will be removed + * in v4 + * + * @var boolean + */ + public bool $v2_output_buffering = false; + + /** + * HTTP status codes + * + * @var array $codes + */ + public static array $codes = [ + 100 => 'Continue', + 101 => 'Switching Protocols', + 102 => 'Processing', + + 200 => 'OK', + 201 => 'Created', + 202 => 'Accepted', + 203 => 'Non-Authoritative Information', + 204 => 'No Content', + 205 => 'Reset Content', + 206 => 'Partial Content', + 207 => 'Multi-Status', + 208 => 'Already Reported', + + 226 => 'IM Used', + + 300 => 'Multiple Choices', + 301 => 'Moved Permanently', + 302 => 'Found', + 303 => 'See Other', + 304 => 'Not Modified', + 305 => 'Use Proxy', + 306 => '(Unused)', + 307 => 'Temporary Redirect', + 308 => 'Permanent Redirect', + + 400 => 'Bad Request', + 401 => 'Unauthorized', + 402 => 'Payment Required', + 403 => 'Forbidden', + 404 => 'Not Found', + 405 => 'Method Not Allowed', + 406 => 'Not Acceptable', + 407 => 'Proxy Authentication Required', + 408 => 'Request Timeout', + 409 => 'Conflict', + 410 => 'Gone', + 411 => 'Length Required', + 412 => 'Precondition Failed', + 413 => 'Payload Too Large', + 414 => 'URI Too Long', + 415 => 'Unsupported Media Type', + 416 => 'Range Not Satisfiable', + 417 => 'Expectation Failed', + + 422 => 'Unprocessable Entity', + 423 => 'Locked', + 424 => 'Failed Dependency', + + 426 => 'Upgrade Required', + + 428 => 'Precondition Required', + 429 => 'Too Many Requests', + + 431 => 'Request Header Fields Too Large', + + 500 => 'Internal Server Error', + 501 => 'Not Implemented', + 502 => 'Bad Gateway', + 503 => 'Service Unavailable', + 504 => 'Gateway Timeout', + 505 => 'HTTP Version Not Supported', + 506 => 'Variant Also Negotiates', + 507 => 'Insufficient Storage', + 508 => 'Loop Detected', + + 510 => 'Not Extended', + 511 => 'Network Authentication Required', + ]; + + /** + * HTTP status + */ + protected int $status = 200; + + /** + * HTTP response headers + * + * @var array> $headers + */ + protected array $headers = []; + + /** + * HTTP response body + */ + protected string $body = ''; + + /** + * HTTP response sent + */ + protected bool $sent = false; + + /** + * These are callbacks that can process the response body before it's sent + * + * @var array $responseBodyCallbacks + */ + protected array $responseBodyCallbacks = []; + + /** + * Sets the HTTP status of the response. + * + * @param ?int $code HTTP status code. + * + * @throws Exception If invalid status code + * + * @return int|$this Self reference + */ + public function status(?int $code = null) + { + if ($code === null) { + return $this->status; + } + + if (\array_key_exists($code, self::$codes)) { + $this->status = $code; + } else { + throw new Exception('Invalid status code.'); + } + + return $this; + } + + /** + * Adds a header to the response. + * + * @param array|string $name Header name or array of names and values + * @param ?string $value Header value + * + * @return $this + */ + public function header($name, ?string $value = null): self + { + if (\is_array($name)) { + foreach ($name as $k => $v) { + $this->headers[$k] = $v; + } + } else { + $this->headers[$name] = $value; + } + + return $this; + } + + /** + * Gets a single header from the response. + * + * @param string $name the name of the header + * + * @return string|null + */ + public function getHeader(string $name): ?string + { + $headers = $this->headers; + // lowercase all the header keys + $headers = array_change_key_case($headers, CASE_LOWER); + return $headers[strtolower($name)] ?? null; + } + + /** + * Alias of Response->header(). Adds a header to the response. + * + * @param array|string $name Header name or array of names and values + * @param ?string $value Header value + * + * @return $this + */ + public function setHeader($name, ?string $value): self + { + return $this->header($name, $value); + } + + /** + * Returns the headers from the response. + * + * @return array> + */ + public function headers(): array + { + return $this->headers; + } + + /** + * Alias for Response->headers(). Returns the headers from the response. + * + * @return array> + */ + public function getHeaders(): array + { + return $this->headers(); + } + + /** + * Writes content to the response body. + * + * @param string $str Response content + * @param bool $overwrite Overwrite the response body + * + * @return $this Self reference + */ + public function write(string $str, bool $overwrite = false): self + { + if ($overwrite === true) { + $this->clearBody(); + } + + $this->body .= $str; + + return $this; + } + + /** + * Clears the response body. + * + * @return $this Self reference + */ + public function clearBody(): self + { + $this->body = ''; + return $this; + } + + /** + * Clears the response. + * + * @return $this Self reference + */ + public function clear(): self + { + $this->status = 200; + $this->headers = []; + $this->clearBody(); + + // This needs to clear the output buffer if it's on + if ($this->v2_output_buffering === false && ob_get_length() > 0) { + ob_clean(); + } + + return $this; + } + + /** + * Sets caching headers for the response. + * + * @param int|string|false $expires Expiration time as time() or as strtotime() string value + * + * @return $this Self reference + */ + public function cache($expires): self + { + if ($expires === false) { + $this->headers['Expires'] = 'Mon, 26 Jul 1997 05:00:00 GMT'; + + $this->headers['Cache-Control'] = [ + 'no-store, no-cache, must-revalidate', + 'post-check=0, pre-check=0', + 'max-age=0', + ]; + + $this->headers['Pragma'] = 'no-cache'; + } else { + $expires = \is_int($expires) ? $expires : strtotime($expires); + $this->headers['Expires'] = gmdate('D, d M Y H:i:s', $expires) . ' GMT'; + $this->headers['Cache-Control'] = 'max-age=' . ($expires - time()); + + if (isset($this->headers['Pragma']) && $this->headers['Pragma'] === 'no-cache') { + unset($this->headers['Pragma']); + } + } + + return $this; + } + + /** + * Sends HTTP headers. + * + * @return $this Self reference + */ + public function sendHeaders(): self + { + // Send status code header + if (strpos(\PHP_SAPI, 'cgi') !== false) { + // @codeCoverageIgnoreStart + $this->setRealHeader( + sprintf( + 'Status: %d %s', + $this->status, + self::$codes[$this->status] + ), + true + ); + // @codeCoverageIgnoreEnd + } else { + $this->setRealHeader( + sprintf( + '%s %d %s', + $_SERVER['SERVER_PROTOCOL'] ?? 'HTTP/1.1', + $this->status, + self::$codes[$this->status] + ), + true, + $this->status + ); + } + + if ($this->content_length === true) { + // Send content length + $length = $this->getContentLength(); + + if ($length > 0) { + $this->setHeader('Content-Length', (string) $length); + } + } + + // Send other headers + foreach ($this->headers as $field => $value) { + if (\is_array($value)) { + foreach ($value as $v) { + $this->setRealHeader($field . ': ' . $v, false); + } + } else { + $this->setRealHeader($field . ': ' . $value); + } + } + + return $this; + } + + /** + * Sets a real header. Mostly used for test mocking. + * + * @param string $header_string The header string you would pass to header() + * @param bool $replace The optional replace parameter indicates whether the + * header should replace a previous similar header, or add a second header of + * the same type. By default it will replace, but if you pass in false as the + * second argument you can force multiple headers of the same type. + * @param int $response_code The response code to send + * + * @return self + * + * @codeCoverageIgnore + */ + public function setRealHeader(string $header_string, bool $replace = true, int $response_code = 0): self + { + header($header_string, $replace, $response_code); + return $this; + } + + /** + * Gets the content length. + */ + public function getContentLength(): int + { + return \extension_loaded('mbstring') ? + mb_strlen($this->body, 'latin1') : + \strlen($this->body); + } + + /** + * Gets the response body + * + * @return string + */ + public function getBody(): string + { + return $this->body; + } + + /** + * Gets whether response body was sent. + */ + public function sent(): bool + { + return $this->sent; + } + + /** + * Marks the response as sent. + */ + public function markAsSent(): void + { + $this->sent = true; + } + + /** + * Sends a HTTP response. + */ + public function send(): void + { + // legacy way of handling this + if ($this->v2_output_buffering === true) { + if (ob_get_length() > 0) { + ob_end_clean(); // @codeCoverageIgnore + } + } + + // Only for the v3 output buffering. + if ($this->v2_output_buffering === false) { + $this->processResponseCallbacks(); + } + + if (headers_sent() === false) { + $this->sendHeaders(); // @codeCoverageIgnore + } + + echo $this->body; + + $this->sent = true; + } + + /** + * Adds a callback to process the response body before it's sent. These are processed in the order + * they are added + * + * @param callable $callback The callback to process the response body + * + * @return void + */ + public function addResponseBodyCallback(callable $callback): void + { + $this->responseBodyCallbacks[] = $callback; + } + + /** + * Cycles through the response body callbacks and processes them in order + * + * @return void + */ + protected function processResponseCallbacks(): void + { + foreach ($this->responseBodyCallbacks as $callback) { + $this->body = $callback($this->body); + } + } +} diff --git a/R4.01_R4.A.10/td_tp/tp6/src/api_php/flight/net/Route.php b/R4.01_R4.A.10/td_tp/tp6/src/api_php/flight/net/Route.php new file mode 100644 index 0000000..4e6e83c --- /dev/null +++ b/R4.01_R4.A.10/td_tp/tp6/src/api_php/flight/net/Route.php @@ -0,0 +1,266 @@ + + */ +class Route +{ + /** + * URL pattern + */ + public string $pattern; + + /** + * Callback function + * + * @var mixed + */ + public $callback; + + /** + * HTTP methods + * + * @var array + */ + public array $methods = []; + + /** + * Route parameters + * + * @var array + */ + public array $params = []; + + /** + * Matching regular expression + */ + public ?string $regex = null; + + /** + * URL splat content + */ + public string $splat = ''; + + /** + * Pass self in callback parameters + */ + public bool $pass = false; + + /** + * The alias is a way to identify the route using a simple name ex: 'login' instead of /admin/login + */ + public string $alias = ''; + + /** + * The middleware to be applied to the route + * + * @var array + */ + public array $middleware = []; + + /** Whether the response for this route should be streamed. */ + public bool $is_streamed = false; + + /** + * If this route is streamed, the headers to be sent before the response. + * + * @var array + */ + public array $streamed_headers = []; + + /** + * Constructor. + * + * @param string $pattern URL pattern + * @param callable|string $callback Callback function + * @param array $methods HTTP methods + * @param bool $pass Pass self in callback parameters + */ + public function __construct(string $pattern, $callback, array $methods, bool $pass, string $alias = '') + { + $this->pattern = $pattern; + $this->callback = $callback; + $this->methods = $methods; + $this->pass = $pass; + $this->alias = $alias; + } + + /** + * Checks if a URL matches the route pattern. Also parses named parameters in the URL. + * + * @param string $url Requested URL (original format, not URL decoded) + * @param bool $case_sensitive Case sensitive matching + * + * @return bool Match status + */ + public function matchUrl(string $url, bool $case_sensitive = false): bool + { + // Wildcard or exact match + if ($this->pattern === '*' || $this->pattern === $url) { + return true; + } + + $ids = []; + $last_char = substr($this->pattern, -1); + + // Get splat + if ($last_char === '*') { + $n = 0; + $len = \strlen($url); + $count = substr_count($this->pattern, '/'); + + for ($i = 0; $i < $len; $i++) { + if ($url[$i] === '/') { + ++$n; + } + + if ($n === $count) { + break; + } + } + + $this->splat = urldecode(strval(substr($url, $i + 1))); + } + + // Build the regex for matching + $pattern_utf_chars_encoded = preg_replace_callback( + '#(\\p{L}+)#u', + static function ($matches) { + return urlencode($matches[0]); + }, + $this->pattern + ); + $regex = str_replace([')', '/*'], [')?', '(/?|/.*?)'], $pattern_utf_chars_encoded); + + $regex = preg_replace_callback( + '#@([\w]+)(:([^/\(\)]*))?#', + static function ($matches) use (&$ids) { + $ids[$matches[1]] = null; + if (isset($matches[3])) { + return '(?P<' . $matches[1] . '>' . $matches[3] . ')'; + } + + return '(?P<' . $matches[1] . '>[^/\?]+)'; + }, + $regex + ); + + $regex .= $last_char === '/' ? '?' : '/?'; + + // Attempt to match route and named parameters + if (!preg_match('#^' . $regex . '(?:\?[\s\S]*)?$#' . (($case_sensitive) ? '' : 'i'), $url, $matches)) { + return false; + } + + foreach (array_keys($ids) as $k) { + $this->params[$k] = (\array_key_exists($k, $matches)) ? urldecode($matches[$k]) : null; + } + + $this->regex = $regex; + + return true; + } + + /** + * Checks if an HTTP method matches the route methods. + * + * @param string $method HTTP method + * + * @return bool Match status + */ + public function matchMethod(string $method): bool + { + return \count(array_intersect([$method, '*'], $this->methods)) > 0; + } + + /** + * Checks if an alias matches the route alias. + */ + public function matchAlias(string $alias): bool + { + return $this->alias === $alias; + } + + /** + * Hydrates the route url with the given parameters + * + * @param array $params the parameters to pass to the route + */ + public function hydrateUrl(array $params = []): string + { + $url = preg_replace_callback("/(?:@([\w]+)(?:\:([^\/]+))?\)*)/i", function ($match) use ($params) { + if (isset($match[1]) && isset($params[$match[1]])) { + return $params[$match[1]]; + } + }, $this->pattern); + + // catches potential optional parameter + $url = str_replace('(/', '/', $url); + // trim any trailing slashes + if ($url !== '/') { + $url = rtrim($url, '/'); + } + return $url; + } + + /** + * Sets the route alias + * + * @return $this + */ + public function setAlias(string $alias): self + { + $this->alias = $alias; + return $this; + } + + /** + * Sets the route middleware + * + * @param array|callable|string $middleware + */ + public function addMiddleware($middleware): self + { + if (is_array($middleware) === true) { + $this->middleware = array_merge($this->middleware, $middleware); + } else { + $this->middleware[] = $middleware; + } + return $this; + } + + /** + * If the response should be streamed + * + * @return self + */ + public function stream(): self + { + $this->is_streamed = true; + return $this; + } + + /** + * This will allow the response for this route to be streamed. + * + * @param array $headers a key value of headers to set before the stream starts. + * + * @return $this + */ + public function streamWithHeaders(array $headers): self + { + $this->is_streamed = true; + $this->streamed_headers = $headers; + + return $this; + } +} diff --git a/R4.01_R4.A.10/td_tp/tp6/src/api_php/flight/net/Router.php b/R4.01_R4.A.10/td_tp/tp6/src/api_php/flight/net/Router.php new file mode 100644 index 0000000..a43b5ba --- /dev/null +++ b/R4.01_R4.A.10/td_tp/tp6/src/api_php/flight/net/Router.php @@ -0,0 +1,330 @@ + + */ +class Router +{ + /** + * Case sensitive matching. + */ + public bool $case_sensitive = false; + + /** + * Mapped routes. + * + * @var array $routes + */ + protected array $routes = []; + + /** + * The current route that is has been found and executed. + */ + public ?Route $executedRoute = null; + + /** + * Pointer to current route. + */ + protected int $index = 0; + + /** + * When groups are used, this is mapped against all the routes + */ + protected string $groupPrefix = ''; + + /** + * Group Middleware + * + * @var array + */ + protected array $groupMiddlewares = []; + + /** + * Allowed HTTP methods + * + * @var array + */ + protected array $allowedMethods = ['GET', 'POST', 'PUT', 'PATCH', 'DELETE', 'HEAD', 'OPTIONS']; + + /** + * Gets mapped routes. + * + * @return array Array of routes + */ + public function getRoutes(): array + { + return $this->routes; + } + + /** + * Clears all routes in the router. + */ + public function clear(): void + { + $this->routes = []; + } + + /** + * Maps a URL pattern to a callback function. + * + * @param string $pattern URL pattern to match. + * @param callable|string $callback Callback function or string class->method + * @param bool $pass_route Pass the matching route object to the callback. + * @param string $route_alias Alias for the route. + */ + public function map(string $pattern, $callback, bool $pass_route = false, string $route_alias = ''): Route + { + + // This means that the route ies defined in a group, but the defined route is the base + // url path. Note the '' in route() + // Ex: Flight::group('/api', function() { + // Flight::route('', function() {}); + // } + // Keep the space so that it can execute the below code normally + if ($this->groupPrefix !== '') { + $url = ltrim($pattern); + } else { + $url = trim($pattern); + } + + $methods = ['*']; + + if (strpos($url, ' ') !== false) { + [$method, $url] = explode(' ', $url, 2); + $url = trim($url); + $methods = explode('|', $method); + + // Add head requests to get methods, should they come in as a get request + if (in_array('GET', $methods, true) === true && in_array('HEAD', $methods, true) === false) { + $methods[] = 'HEAD'; + } + } + + // And this finishes it off. + if ($this->groupPrefix !== '') { + $url = rtrim($this->groupPrefix . $url); + } + + $route = new Route($url, $callback, $methods, $pass_route, $route_alias); + + // to handle group middleware + foreach ($this->groupMiddlewares as $gm) { + $route->addMiddleware($gm); + } + + $this->routes[] = $route; + + return $route; + } + + /** + * Creates a GET based route + * + * @param string $pattern URL pattern to match + * @param callable|string $callback Callback function or string class->method + * @param bool $pass_route Pass the matching route object to the callback + * @param string $alias Alias for the route + */ + public function get(string $pattern, $callback, bool $pass_route = false, string $alias = ''): Route + { + return $this->map('GET ' . $pattern, $callback, $pass_route, $alias); + } + + /** + * Creates a POST based route + * + * @param string $pattern URL pattern to match + * @param callable|string $callback Callback function or string class->method + * @param bool $pass_route Pass the matching route object to the callback + * @param string $alias Alias for the route + */ + public function post(string $pattern, $callback, bool $pass_route = false, string $alias = ''): Route + { + return $this->map('POST ' . $pattern, $callback, $pass_route, $alias); + } + + /** + * Creates a PUT based route + * + * @param string $pattern URL pattern to match + * @param callable|string $callback Callback function or string class->method + * @param bool $pass_route Pass the matching route object to the callback + * @param string $alias Alias for the route + */ + public function put(string $pattern, $callback, bool $pass_route = false, string $alias = ''): Route + { + return $this->map('PUT ' . $pattern, $callback, $pass_route, $alias); + } + + /** + * Creates a PATCH based route + * + * @param string $pattern URL pattern to match + * @param callable|string $callback Callback function or string class->method + * @param bool $pass_route Pass the matching route object to the callback + * @param string $alias Alias for the route + */ + public function patch(string $pattern, $callback, bool $pass_route = false, string $alias = ''): Route + { + return $this->map('PATCH ' . $pattern, $callback, $pass_route, $alias); + } + + /** + * Creates a DELETE based route + * + * @param string $pattern URL pattern to match + * @param callable|string $callback Callback function or string class->method + * @param bool $pass_route Pass the matching route object to the callback + * @param string $alias Alias for the route + */ + public function delete(string $pattern, $callback, bool $pass_route = false, string $alias = ''): Route + { + return $this->map('DELETE ' . $pattern, $callback, $pass_route, $alias); + } + + /** + * Group together a set of routes + * + * @param string $groupPrefix group URL prefix (such as /api/v1) + * @param callable $callback The necessary calling that holds the Router class + * @param array $groupMiddlewares + * The middlewares to be applied to the group. Example: `[$middleware1, $middleware2]` + */ + public function group(string $groupPrefix, callable $callback, array $groupMiddlewares = []): void + { + $oldGroupPrefix = $this->groupPrefix; + $oldGroupMiddlewares = $this->groupMiddlewares; + $this->groupPrefix .= $groupPrefix; + $this->groupMiddlewares = array_merge($this->groupMiddlewares, $groupMiddlewares); + $callback($this); + $this->groupPrefix = $oldGroupPrefix; + $this->groupMiddlewares = $oldGroupMiddlewares; + } + + /** + * Routes the current request. + * + * @return false|Route Matching route or false if no match + */ + public function route(Request $request) + { + while ($route = $this->current()) { + $urlMatches = $route->matchUrl($request->url, $this->case_sensitive); + $methodMatches = $route->matchMethod($request->method); + if ($urlMatches === true && $methodMatches === true) { + $this->executedRoute = $route; + return $route; + // capture the route but don't execute it. We'll use this in Engine->start() to throw a 405 + } elseif ($urlMatches === true && $methodMatches === false) { + $this->executedRoute = $route; + } + $this->next(); + } + + return false; + } + + /** + * Gets the URL for a given route alias + * + * @param string $alias the alias to match + * @param array $params the parameters to pass to the route + */ + public function getUrlByAlias(string $alias, array $params = []): string + { + $potential_aliases = []; + foreach ($this->routes as $route) { + $potential_aliases[] = $route->alias; + if ($route->matchAlias($alias)) { + // This will make it so the params that already + // exist in the url will be passed in. + if (!empty($this->executedRoute->params)) { + $params = $params + $this->executedRoute->params; + } + return $route->hydrateUrl($params); + } + } + + // use a levenshtein to find the closest match and make a recommendation + $closest_match = ''; + $closest_match_distance = 0; + foreach ($potential_aliases as $potential_alias) { + $levenshtein_distance = levenshtein($alias, $potential_alias); + if ($levenshtein_distance > $closest_match_distance) { + $closest_match = $potential_alias; + $closest_match_distance = $levenshtein_distance; + } + } + + $exception_message = 'No route found with alias: \'' . $alias . '\'.'; + if ($closest_match !== '') { + $exception_message .= ' Did you mean \'' . $closest_match . '\'?'; + } + + throw new Exception($exception_message); + } + + /** + * Rewinds the current route index. + */ + public function rewind(): void + { + $this->index = 0; + } + + /** + * Checks if more routes can be iterated. + * + * @return bool More routes + */ + public function valid(): bool + { + return isset($this->routes[$this->index]); + } + + /** + * Gets the current route. + * + * @return false|Route + */ + public function current() + { + return $this->routes[$this->index] ?? false; + } + + /** + * Gets the previous route. + */ + public function previous(): void + { + --$this->index; + } + + /** + * Gets the next route. + */ + public function next(): void + { + ++$this->index; + } + + /** + * Reset to the first route. + */ + public function reset(): void + { + $this->rewind(); + } +} diff --git a/R4.01_R4.A.10/td_tp/tp6/src/api_php/flight/template/View.php b/R4.01_R4.A.10/td_tp/tp6/src/api_php/flight/template/View.php new file mode 100644 index 0000000..c43a35f --- /dev/null +++ b/R4.01_R4.A.10/td_tp/tp6/src/api_php/flight/template/View.php @@ -0,0 +1,197 @@ + + */ +class View +{ + /** Location of view templates. */ + public string $path; + + /** File extension. */ + public string $extension = '.php'; + + /** + * View variables. + * + * @var array $vars + */ + protected array $vars = []; + + /** Template file. */ + private string $template; + + /** + * Constructor. + * + * @param string $path Path to templates directory + */ + public function __construct(string $path = '.') + { + $this->path = $path; + } + + /** + * Gets a template variable. + * + * @return mixed Variable value or `null` if doesn't exists + */ + public function get(string $key) + { + return $this->vars[$key] ?? null; + } + + /** + * Sets a template variable. + * + * @param string|iterable $key + * @param mixed $value Value + * + * @return self + */ + public function set($key, $value = null): self + { + if (\is_iterable($key)) { + foreach ($key as $k => $v) { + $this->vars[$k] = $v; + } + } else { + $this->vars[$key] = $value; + } + + return $this; + } + + /** + * Checks if a template variable is set. + * + * @return bool If key exists + */ + public function has(string $key): bool + { + return isset($this->vars[$key]); + } + + /** + * Unsets a template variable. If no key is passed in, clear all variables. + * + * @return $this + */ + public function clear(?string $key = null): self + { + if ($key === null) { + $this->vars = []; + } else { + unset($this->vars[$key]); + } + + return $this; + } + + /** + * Renders a template. + * + * @param string $file Template file + * @param ?array $data Template data + * + * @throws \Exception If template not found + */ + public function render(string $file, ?array $data = null): void + { + $this->template = $this->getTemplate($file); + + if (!\file_exists($this->template)) { + $normalized_path = self::normalizePath($this->template); + throw new \Exception("Template file not found: {$normalized_path}."); + } + + if (\is_array($data)) { + $this->vars = \array_merge($this->vars, $data); + } + + \extract($this->vars); + + include $this->template; + } + + /** + * Gets the output of a template. + * + * @param string $file Template file + * @param ?array $data Template data + * + * @return string Output of template + */ + public function fetch(string $file, ?array $data = null): string + { + \ob_start(); + + $this->render($file, $data); + + return \ob_get_clean(); + } + + /** + * Checks if a template file exists. + * + * @param string $file Template file + * + * @return bool Template file exists + */ + public function exists(string $file): bool + { + return \file_exists($this->getTemplate($file)); + } + + /** + * Gets the full path to a template file. + * + * @param string $file Template file + * + * @return string Template file location + */ + public function getTemplate(string $file): string + { + $ext = $this->extension; + + if (!empty($ext) && (\substr($file, -1 * \strlen($ext)) != $ext)) { + $file .= $ext; + } + + $is_windows = \strtoupper(\substr(PHP_OS, 0, 3)) === 'WIN'; + + if ((\substr($file, 0, 1) === '/') || ($is_windows && \substr($file, 1, 1) === ':')) { + return $file; + } + + return $this->path . DIRECTORY_SEPARATOR . $file; + } + + /** + * Displays escaped output. + * + * @param string $str String to escape + * + * @return string Escaped string + */ + public function e(string $str): string + { + $value = \htmlentities($str); + echo $value; + return $value; + } + + protected static function normalizePath(string $path, string $separator = DIRECTORY_SEPARATOR): string + { + return \str_replace(['\\', '/'], $separator, $path); + } +} diff --git a/R4.01_R4.A.10/td_tp/tp6/src/api_php/flight/util/Collection.php b/R4.01_R4.A.10/td_tp/tp6/src/api_php/flight/util/Collection.php new file mode 100644 index 0000000..e17ed37 --- /dev/null +++ b/R4.01_R4.A.10/td_tp/tp6/src/api_php/flight/util/Collection.php @@ -0,0 +1,223 @@ + + * @implements ArrayAccess + * @implements Iterator + */ +class Collection implements ArrayAccess, Iterator, Countable, JsonSerializable +{ + /** + * Collection data. + * + * @var array + */ + private array $data; + + /** + * Constructor. + * + * @param array $data Initial data + */ + public function __construct(array $data = []) + { + $this->data = $data; + } + + /** + * Gets an item. + * + * @return mixed Value if `$key` exists in collection data, otherwise returns `NULL` + */ + public function __get(string $key) + { + return $this->data[$key] ?? null; + } + + /** + * Set an item. + * + * @param mixed $value Value + */ + public function __set(string $key, $value): void + { + $this->data[$key] = $value; + } + + /** + * Checks if an item exists. + */ + public function __isset(string $key): bool + { + return isset($this->data[$key]); + } + + /** + * Removes an item. + */ + public function __unset(string $key): void + { + unset($this->data[$key]); + } + + /** + * Gets an item at the offset. + * + * @param string $offset Offset + * + * @return mixed Value + */ + #[\ReturnTypeWillChange] + public function offsetGet($offset) + { + return $this->data[$offset] ?? null; + } + + /** + * Sets an item at the offset. + * + * @param ?string $offset Offset + * @param mixed $value Value + */ + #[\ReturnTypeWillChange] + public function offsetSet($offset, $value): void + { + if ($offset === null) { + $this->data[] = $value; + } else { + $this->data[$offset] = $value; + } + } + + /** + * Checks if an item exists at the offset. + * + * @param string $offset + */ + public function offsetExists($offset): bool + { + return isset($this->data[$offset]); + } + + /** + * Removes an item at the offset. + * + * @param string $offset + */ + public function offsetUnset($offset): void + { + unset($this->data[$offset]); + } + + /** + * Resets the collection. + */ + public function rewind(): void + { + reset($this->data); + } + + /** + * Gets current collection item. + * + * @return mixed Value + */ + #[\ReturnTypeWillChange] + public function current() + { + return current($this->data); + } + + /** + * Gets current collection key. + * + * @return mixed Value + */ + #[\ReturnTypeWillChange] + public function key() + { + return key($this->data); + } + + /** + * Gets the next collection value. + */ + #[\ReturnTypeWillChange] + public function next(): void + { + next($this->data); + } + + /** + * Checks if the current collection key is valid. + */ + public function valid(): bool + { + return key($this->data) !== null; + } + + /** + * Gets the size of the collection. + */ + public function count(): int + { + return \count($this->data); + } + + /** + * Gets the item keys. + * + * @return array Collection keys + */ + public function keys(): array + { + return array_keys($this->data); + } + + /** + * Gets the collection data. + * + * @return array Collection data + */ + public function getData(): array + { + return $this->data; + } + + /** + * Sets the collection data. + * + * @param array $data New collection data + */ + public function setData(array $data): void + { + $this->data = $data; + } + + #[\ReturnTypeWillChange] + public function jsonSerialize() + { + return $this->data; + } + + /** + * Removes all items from the collection. + */ + public function clear(): void + { + $this->data = []; + } +} diff --git a/R4.01_R4.A.10/td_tp/tp6/src/api_php/flight/util/ReturnTypeWillChange.php b/R4.01_R4.A.10/td_tp/tp6/src/api_php/flight/util/ReturnTypeWillChange.php new file mode 100644 index 0000000..1eba39e --- /dev/null +++ b/R4.01_R4.A.10/td_tp/tp6/src/api_php/flight/util/ReturnTypeWillChange.php @@ -0,0 +1,8 @@ + Flight::request()->data->title , + "done" => Flight::request()->data->done + ]; + + $id = Todo::create($todo); + Flight::response()->header("Location",Flight::request()->url."/".$id); + $todo['id'] = $id; + Flight::json($todo,201); + +} + +function deleteTodo($id) +{ + // TODO +} + +function updateTodo($id) +{ + // TODO +} + + +function getTodos($id = null) +{ + $filter = Flight::request()->query->filter ?? "all"; + + if ($id === null){ + switch($filter){ + case "done": + $todos = Todo::findCompleted(); + break; + case "active": + $todos = Todo::findUnCompleted(); + break; + default: + $todos = Todo::findAll(); + } + Flight::json( + [ + "results" => $todos + ] + ); + + } else { + $todo = Todo::find($id); + if ($todo) + Flight::json($todo); + else + Flight::halt(404); + } +} + +Flight::start(); diff --git a/R4.01_R4.A.10/td_tp/tp6/src/api_php/model/model.php b/R4.01_R4.A.10/td_tp/tp6/src/api_php/model/model.php new file mode 100644 index 0000000..8655be6 --- /dev/null +++ b/R4.01_R4.A.10/td_tp/tp6/src/api_php/model/model.php @@ -0,0 +1,114 @@ +host = $host; + $this->user = $user; + $this->pass = $pass; + $this->dbname = $dbname; + } + public function connect() { + $driver_options = [ + PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION, + PDO::ATTR_DEFAULT_FETCH_MODE => PDO::FETCH_ASSOC, + ]; + $dsn = "mysql:host=$this->host;dbname=$this->dbname"; + $this->pdo = new PDO($dsn, $this->user, $this->pass,$driver_options); + } + + public function query($sql, $params = []) { + $stmt = $this->pdo->prepare($sql); + if ($stmt->execute($params) === true) + return $stmt; + return false; + } + + public function lastInsertId(){ + return $this->pdo->lastInsertId(); + } +} + +class res { + public $id; + public $title; + public $done; + public function __construct(){ + $this->done = (bool)$this->done;; + } +} + +class Todo { + static private $db = NULL; + + public static function todo($id,$title,$done){ + return ["id"=>$id,"title"=>$title,"done"=>(bool)$done]; + + } + public static function init(){ + //TODO + self::$db = new Database("localhost","test","test","test"); // vos parametres !!!! + self::$db->connect(); + } + + public static function find($id) { + $sql = "SELECT * FROM todo WHERE id = ?"; + $stmt = self::$db->query($sql,[$id]); + + $stmt->setFetchMode(PDO::FETCH_CLASS, 'res'); + $result = $stmt->fetch(); + + return $result; + } + + public static function findCompleted(){ + $sql = "SELECT * FROM todo WHERE done = '1'"; + $stmt = self::$db->query($sql); + + $stmt->setFetchMode(PDO::FETCH_CLASS, 'res'); + $result = $stmt->fetchAll(); + return $result; + } + + public static function findUnCompleted(){ + $sql = "SELECT * FROM todo WHERE done = '0'"; + $stmt = self::$db->query($sql); + + $stmt->setFetchMode(PDO::FETCH_CLASS, 'res'); + $result = $stmt->fetchAll(); + return $result; + } + + public static function findAll() { + $sql = "SELECT id,title, done FROM todo"; + $stmt = self::$db->query($sql); + // $result = $stmt->fetchAll(PDO::FETCH_FUNC, "Todo::todo"); + $stmt->setFetchMode(PDO::FETCH_CLASS, 'res'); + + $result = $stmt->fetchAll(); + return $result; + } + + public static function update($todo){ + + $sql = "UPDATE todo SET title = ?, done = ? WHERE id = ?"; + $stmt = self::$db->query($sql,[$todo['title'],(int)$todo['done'],$todo['id']]); + } + + public static function create($todo){ + + $sql = "INSERT INTO todo (title, done) VALUES (?, ?)"; + $stmt = self::$db->query($sql,[$todo['title'],(int)$todo['done']]); + return self::$db->lastInsertId(); + } + + public static function delete($id){ + $sql = "DELETE FROM todo WHERE id=?"; + $stmt = self::$db->query($sql,[$id]); + } +} +