From 097dd968b18544bfd7863e0def0d787856e6e254 Mon Sep 17 00:00:00 2001 From: fadrian06 Date: Thu, 1 Feb 2024 14:08:55 -0400 Subject: [PATCH 01/13] Fixed root files code format --- .editorconfig | 3 +++ flight.sublime-project | 4 ++-- phpcs.xml | 11 +++++------ 3 files changed, 10 insertions(+), 8 deletions(-) diff --git a/.editorconfig b/.editorconfig index 803737f..dcc5687 100644 --- a/.editorconfig +++ b/.editorconfig @@ -5,3 +5,6 @@ indent_style = space indent_size = 4 end_of_line = lf charset = utf-8 + +[*.md] +indent_size = 2 diff --git a/flight.sublime-project b/flight.sublime-project index ccca03f..2040dbf 100644 --- a/flight.sublime-project +++ b/flight.sublime-project @@ -40,12 +40,12 @@ }, { "name": "Linter - Default", - "quiet": true, + "quiet": false, "shell_cmd": "composer lint -- --no-ansi & composer phpcs -- --no-colors", }, { "name": "PHPCS", - "quiet": true, + "quiet": false, "shell_cmd": "composer phpcs -- --no-colors" }, { diff --git a/phpcs.xml b/phpcs.xml index 4cdfb52..22b3fdd 100644 --- a/phpcs.xml +++ b/phpcs.xml @@ -29,14 +29,13 @@ - + - - - + + + flight/ tests/ - tests/views/* - tests/views/ + tests/views/* From aeb89110386c77144cd0ea48e38dd5fe97fe6994 Mon Sep 17 00:00:00 2001 From: fadrian06 Date: Thu, 1 Feb 2024 15:20:01 -0400 Subject: [PATCH 02/13] Improved docblocks --- flight.sublime-project | 1 + flight/Engine.php | 113 ++++++++++++++++++++----------------- flight/Flight.php | 112 +++++++++++++++++------------------- flight/core/Dispatcher.php | 7 ++- flight/core/Loader.php | 10 ++-- phpcs.xml | 1 + tests/FlightTest.php | 1 - 7 files changed, 124 insertions(+), 121 deletions(-) diff --git a/flight.sublime-project b/flight.sublime-project index 2040dbf..536f644 100644 --- a/flight.sublime-project +++ b/flight.sublime-project @@ -6,6 +6,7 @@ } ], "settings": { + "SublimeLinter.linters.phpstan.executable": "${project_path}/vendor/bin/phpstan.bat", "LSP": { "LSP-intelephense": { "settings": { diff --git a/flight/Engine.php b/flight/Engine.php index 32a1a70..543dbb6 100644 --- a/flight/Engine.php +++ b/flight/Engine.php @@ -63,7 +63,6 @@ use flight\net\Route; * @method void etag($id, string $type = 'strong') Handles ETag HTTP caching. * @method void lastModified(int $time) Handles last modified HTTP caching. */ -// phpcs:ignoreFile Generic.Files.LineLength.TooLong, PSR2.Methods.MethodDeclaration.Underscore class Engine { /** @@ -243,13 +242,21 @@ class Engine /** * Registers a class to a framework method. - * @template T of object * - * @param string $name Method name - * @param class-string $class Class name - * @param array $params Class initialization parameters - * @param ?callable(T $instance): void $callback Function to call after object instantiation + * # 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 @@ -270,8 +277,8 @@ class Engine /** * Adds a pre-filter to a method. * - * @param string $name Method name - * @param callable $callback Callback function + * @param string $name Method name + * @param Closure(array &$params, string &$output): (void|false) $callback */ public function before(string $name, callable $callback): void { @@ -281,8 +288,8 @@ class Engine /** * Adds a post-filter to a method. * - * @param string $name Method name - * @param callable $callback Callback function + * @param string $name Method name + * @param Closure(array &$params, string &$output): (void|false) $callback */ public function after(string $name, callable $callback): void { @@ -292,9 +299,9 @@ class Engine /** * Gets a variable. * - * @param string|null $key Key + * @param ?string $key Variable name * - * @return array|mixed|null + * @return mixed Variable value or `null` if `$key` doesn't exists. */ public function get(?string $key = null) { @@ -308,24 +315,27 @@ class Engine /** * Sets a variable. * - * @param mixed $key Key - * @param mixed|null $value Value + * @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_array($key) || \is_object($key)) { + if (\is_iterable($key)) { foreach ($key as $k => $v) { $this->vars[$k] = $v; } - } else { - $this->vars[$key] = $value; + + return; } + + $this->vars[$key] = $value; } /** * Checks if a variable has been set. * - * @param string $key Key + * @param string $key Variable name * * @return bool Variable status */ @@ -337,15 +347,16 @@ class Engine /** * Unsets a variable. If no key is passed in, clear all variables. * - * @param string|null $key Key + * @param ?string $key Variable name, if `$key` isn't provided, it clear all variables. */ public function clear(?string $key = null): void { if (null === $key) { $this->vars = []; - } else { - unset($this->vars[$key]); + return; } + + unset($this->vars[$key]); } /** @@ -478,7 +489,7 @@ class Engine * * @param Throwable $e Thrown exception */ - public function _error($e): void + public function _error(Throwable $e): void { $msg = sprintf( '

500 Internal Server Error

' . @@ -505,7 +516,7 @@ class Engine /** * Stops the framework and outputs the current response. * - * @param int|null $code HTTP status code + * @param ?int $code HTTP status code * * @throws Exception */ @@ -558,9 +569,9 @@ class Engine * @param callable $callback Callback function * @param bool $pass_route Pass the matching route object to the callback */ - public function _post(string $pattern, callable $callback, bool $pass_route = false): void + public function _post(string $pattern, callable $callback, bool $pass_route = false, string $route_alias = ''): void { - $this->router()->map('POST ' . $pattern, $callback, $pass_route); + $this->router()->map('POST ' . $pattern, $callback, $pass_route, $route_alias); } /** @@ -570,9 +581,9 @@ class Engine * @param callable $callback Callback function * @param bool $pass_route Pass the matching route object to the callback */ - public function _put(string $pattern, callable $callback, bool $pass_route = false): void + public function _put(string $pattern, callable $callback, bool $pass_route = false, string $route_alias = ''): void { - $this->router()->map('PUT ' . $pattern, $callback, $pass_route); + $this->router()->map('PUT ' . $pattern, $callback, $pass_route, $route_alias); } /** @@ -582,9 +593,9 @@ class Engine * @param callable $callback Callback function * @param bool $pass_route Pass the matching route object to the callback */ - public function _patch(string $pattern, callable $callback, bool $pass_route = false): void + public function _patch(string $pattern, callable $callback, bool $pass_route = false, string $route_alias = ''): void { - $this->router()->map('PATCH ' . $pattern, $callback, $pass_route); + $this->router()->map('PATCH ' . $pattern, $callback, $pass_route, $route_alias); } /** @@ -594,9 +605,9 @@ class Engine * @param callable $callback Callback function * @param bool $pass_route Pass the matching route object to the callback */ - public function _delete(string $pattern, callable $callback, bool $pass_route = false): void + public function _delete(string $pattern, callable $callback, bool $pass_route = false, string $route_alias = ''): void { - $this->router()->map('DELETE ' . $pattern, $callback, $pass_route); + $this->router()->map('DELETE ' . $pattern, $callback, $pass_route, $route_alias); } /** @@ -618,9 +629,7 @@ class Engine } } - /** - * Sends an HTTP 404 response when a URL is not found. - */ + /** Sends an HTTP 404 response when a URL is not found. */ public function _notFound(): void { $this->response() @@ -637,7 +646,7 @@ class Engine * Redirects the current request to another URL. * * @param string $url URL - * @param int $code HTTP status code + * @param int $code HTTP status code */ public function _redirect(string $url, int $code = 303): void { @@ -662,29 +671,30 @@ class Engine /** * Renders a template. * - * @param string $file Template file + * @param string $file Template file * @param ?array $data Template data - * @param string|null $key View variable name + * @param ?string $key View variable name * - * @throws Exception + * @throws Exception If template file wasn't found */ public function _render(string $file, ?array $data = null, ?string $key = null): void { if (null !== $key) { $this->view()->set($key, $this->view()->fetch($file, $data)); - } else { - $this->view()->render($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 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 + * @param int $option Bitmask Json constant such as JSON_HEX_QUOT * * @throws Exception */ @@ -707,12 +717,12 @@ class Engine /** * 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 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 + * @param int $option Bitmask Json constant such as JSON_HEX_QUOT * * @throws Exception */ @@ -725,7 +735,6 @@ class Engine int $option = 0 ): void { $json = $encode ? json_encode($data, $option) : $data; - $callback = $this->request()->query[$param]; $this->response() @@ -738,8 +747,8 @@ class Engine /** * Handles ETag HTTP caching. * - * @param string $id ETag identifier - * @param string $type ETag type + * @param string $id ETag identifier + * @param 'strong'|'weak' $type ETag type */ public function _etag(string $id, string $type = 'strong'): void { diff --git a/flight/Flight.php b/flight/Flight.php index 1f1d2ae..c2bcf28 100644 --- a/flight/Flight.php +++ b/flight/Flight.php @@ -12,74 +12,71 @@ use flight\net\Route; /** * The Flight class is a static representation of the framework. + * * @license MIT, http://flightphp.com/license * @copyright Copyright (c) 2011, Mike Cao * * # 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() Stops the framework and sends a response. + * @method static void stop(?int $code = null) Stops the framework and sends a response. * @method static void halt(int $code = 200, string $message = '') * Stop the framework with an optional status code and message. * * # Routing - * @method static Route route(string $pattern, callable $callback, bool $pass_route = false, string $alias = '') + * @method static Route route(string $pattern, callable $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, array $group_middlewares = []) + * @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 $callback, bool $pass_route = false, string $alias = '') + * @method static Route post(string $pattern, callable $callback, bool $pass_route = false, string $alias = '') * Routes a POST URL to a callback function. - * @method static Route put(string $pattern, callable $callback, bool $pass_route = false, string $alias = '') + * @method static Route put(string $pattern, callable $callback, bool $pass_route = false, string $alias = '') * Routes a PUT URL to a callback function. - * @method static Route patch(string $pattern, callable $callback, bool $pass_route = false, string $alias = '') + * @method static Route patch(string $pattern, callable $callback, bool $pass_route = false, string $alias = '') * Routes a PATCH URL to a callback function. - * @method static Route delete(string $pattern, callable $callback, bool $pass_route = false, string $alias = '') + * @method static Route delete(string $pattern, callable $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) Gets a url from an alias + * @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 map(string $name, callable $callback) Creates a custom framework method. * - * @method static void before($name, $callback) Adds a filter before a framework method. - * @method static void after($name, $callback) Adds a filter after a 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($key, $value) Sets a variable. - * @method static mixed get($key) Gets a variable. - * @method static bool has($key) Checks if a variable is set. - * @method static void clear($key = null) Clears a variable. + * @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($file, array $data = null, $key = null) Renders a template file. - * @method static View view() Returns View instance. + * @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($url, $code = 303) Redirects to another URL. - * @method static void json($data, $code = 200, $encode = true, $charset = "utf8", $encodeOption = 0, $encodeDepth = 512) Sends a JSON response. - * @method static void jsonp($data, $param = 'jsonp', $code = 200, $encode = true, $charset = "utf8", $encodeOption = 0, $encodeDepth = 512) Sends a JSONP response. - * @method static void error($exception) Sends an HTTP 500 response. - * @method static void notFound() Sends an HTTP 404 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($id, $type = 'strong') Performs ETag HTTP caching. - * @method static void lastModified($time) Performs last modified 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. */ -// phpcs:ignoreFile Generic.Files.LineLength.TooLong, PSR1.Classes.ClassDeclaration.MissingNamespace -class Flight +class Flight { - /** - * Framework engine. - * - * @var Engine $engine - */ + /** Framework engine. */ private static Engine $engine; - /** - * Whether or not the app has been initialized - * - * @var boolean - */ + /** Whether or not the app has been initialized. */ private static bool $initialized = false; /** @@ -104,56 +101,53 @@ class Flight /** * Registers a class to a framework method. - * @template T of object - * @param string $name Static method name + * + * # Usage example: * ``` * Flight::register('user', User::class); * * Flight::user(); # <- Return a User instance * ``` - * @param class-string $class Fully Qualified Class Name - * @param array $params Class constructor params - * @param ?Closure(T $instance): void $callback Perform actions with the instance - * @return void + * + * @param string $name Static method name + * @param class-string $class Fully Qualified Class Name + * @param array $params Class constructor params + * @param ?Closure(T $instance): void $callback Perform actions with the instance + * + * @template T of object */ - public static function register($name, $class, $params = [], $callback = null) + public static function register($name, $class, $params = [], $callback = null): void { - static::__callStatic('register', func_get_args()); + static::__callStatic('register', [$name, $class, $params, $callback]); } /** Unregisters a class. */ public static function unregister(string $methodName): void { - static::__callStatic('unregister', func_get_args()); + static::__callStatic('unregister', [$methodName]); } /** * Handles calls to static methods. * - * @param string $name Method name - * @param array $params Method parameters - * - * @throws Exception + * @param string $name Method name + * @param array $params Method parameters * * @return mixed Callback results + * @throws Exception */ public static function __callStatic(string $name, array $params) { - $app = self::app(); - - return Dispatcher::invokeMethod([$app, $name], $params); + return Dispatcher::invokeMethod([self::app(), $name], $params); } - /** - * @return Engine Application instance - */ + /** @return Engine Application instance */ public static function app(): Engine { if (!self::$initialized) { require_once __DIR__ . '/autoload.php'; self::setEngine(new Engine()); - self::$initialized = true; } diff --git a/flight/core/Dispatcher.php b/flight/core/Dispatcher.php index ff9c9ec..fe7ca2f 100644 --- a/flight/core/Dispatcher.php +++ b/flight/core/Dispatcher.php @@ -4,6 +4,7 @@ declare(strict_types=1); namespace flight\core; +use Closure; use Exception; use InvalidArgumentException; @@ -117,9 +118,9 @@ class Dispatcher /** * Hooks a callback to an event. * - * @param string $name Event name - * @param string $type Filter type - * @param callable $callback Callback function + * @param string $name Event name + * @param 'before'|'after' $type Filter type + * @param Closure(array &$params, string &$output): (void|false) $callback */ public function hook(string $name, string $type, callable $callback): void { diff --git a/flight/core/Loader.php b/flight/core/Loader.php index bf83e3f..3bd3696 100644 --- a/flight/core/Loader.php +++ b/flight/core/Loader.php @@ -42,14 +42,12 @@ class Loader /** * 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 ?callable(T $instance): void $callback $callback Function to call after object instantiation + * @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 - * - * @return void */ public function register(string $name, $class, array $params = [], ?callable $callback = null): void { diff --git a/phpcs.xml b/phpcs.xml index 22b3fdd..ad8cc88 100644 --- a/phpcs.xml +++ b/phpcs.xml @@ -34,6 +34,7 @@ + flight/ tests/ diff --git a/tests/FlightTest.php b/tests/FlightTest.php index 2018d53..c37a8fa 100644 --- a/tests/FlightTest.php +++ b/tests/FlightTest.php @@ -164,7 +164,6 @@ class FlightTest extends TestCase public function testStaticRoutePut() { - Flight::put('/test', function () { echo 'test put'; }); From 28b6c995615b4ffc0c4f57d014485ad5e60b6a69 Mon Sep 17 00:00:00 2001 From: fadrian06 Date: Thu, 1 Feb 2024 16:10:51 -0400 Subject: [PATCH 03/13] Improved docblocks in Engine class --- flight/Engine.php | 118 ++++++++++++++++++------------------------ flight/Flight.php | 2 +- flight/net/Router.php | 16 +++--- phpcs.xml | 1 + 4 files changed, 60 insertions(+), 77 deletions(-) diff --git a/flight/Engine.php b/flight/Engine.php index 543dbb6..c62df84 100644 --- a/flight/Engine.php +++ b/flight/Engine.php @@ -32,7 +32,7 @@ use flight\net\Route; * # Routing * @method Route route(string $pattern, callable $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 = []) + * @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 $callback, bool $pass_route = false, string $alias = '') * Routes a POST URL to a callback function. @@ -46,7 +46,7 @@ use flight\net\Route; * @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 void render(string $file, ?array $data = null, ?string $key = null) Renders template * @method View view() Gets current view * * # Request-Response @@ -57,58 +57,41 @@ use flight\net\Route; * @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. + * @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($id, string $type = 'strong') Handles ETag 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. */ class Engine { - /** - * Stored variables. - * @var array - */ - protected array $vars; + /** @var array Stored variables. */ + protected array $vars = []; - /** - * Class loader. - */ + /** Class loader. */ protected Loader $loader; - /** - * Event dispatcher. - */ + /** Event dispatcher. */ protected Dispatcher $dispatcher; - /** - * If the framework has been initialized or not - * - * @var boolean - */ + /** If the framework has been initialized or not. */ protected bool $initialized = false; - /** - * Constructor. - */ public function __construct() { - $this->vars = []; - $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 + * @param string $name Method name + * @param array $params Method parameters * * @throws Exception - * * @return mixed Callback results */ public function __call(string $name, array $params) @@ -120,7 +103,7 @@ class Engine } if (!$this->loader->get($name)) { - throw new Exception("{$name} must be a mapped method."); + throw new Exception("$name must be a mapped method."); } $shared = empty($params) || $params[0]; @@ -128,11 +111,11 @@ class Engine return $this->loader->load($name, $shared); } - // Core Methods + ////////////////// + // Core Methods // + ////////////////// - /** - * Initializes the framework. - */ + /** Initializes the framework. */ public function init(): void { $initialized = $this->initialized; @@ -148,7 +131,8 @@ class Engine $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) use ($self) { + + $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'); }); @@ -159,6 +143,7 @@ class Engine 'render', 'redirect', 'etag', 'lastModified', 'json', 'jsonp', 'post', 'put', 'patch', 'delete', 'group', 'getUrl', ]; + foreach ($methods as $name) { $this->dispatcher->set($name, [$this, '_' . $name]); } @@ -192,15 +177,15 @@ class Engine /** * Custom error handler. Converts errors into exceptions. * - * @param int $errno Error number - * @param string $errstr Error string + * @param int $errno Error number + * @param string $errstr Error string * @param string $errfile Error file name - * @param int $errline Error file line number + * @param int $errline Error file line number * + * @return false * @throws ErrorException - * @return bool */ - public function handleError(int $errno, string $errstr, string $errfile, int $errline) + public function handleError(int $errno, string $errstr, string $errfile, int $errline): bool { if ($errno & error_reporting()) { throw new ErrorException($errstr, $errno, 0, $errfile, $errline); @@ -214,7 +199,7 @@ class Engine * * @param Throwable $e Thrown exception */ - public function handleException($e): void + public function handleException(Throwable $e): void { if ($this->get('flight.log_errors')) { error_log($e->getMessage()); // @codeCoverageIgnore @@ -226,7 +211,7 @@ class Engine /** * Maps a callback to a framework method. * - * @param string $name Method name + * @param string $name Method name * @param callable $callback Callback function * * @throws Exception If trying to map over a framework method @@ -539,11 +524,10 @@ class Engine /** * Routes a URL to a callback function. * - * @param string $pattern URL pattern to match - * @param callable $callback Callback function - * @param bool $pass_route Pass the matching route object to the callback - * @param string $alias the alias for the route - * @return Route + * @param string $pattern URL pattern to match + * @param callable $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, callable $callback, bool $pass_route = false, string $alias = ''): Route { @@ -553,9 +537,9 @@ class Engine /** * 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 + * @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 { @@ -565,9 +549,9 @@ class Engine /** * Routes a URL to a callback function. * - * @param string $pattern URL pattern to match - * @param callable $callback Callback function - * @param bool $pass_route Pass the matching route object to the callback + * @param string $pattern URL pattern to match + * @param callable $callback Callback function + * @param bool $pass_route Pass the matching route object to the callback */ public function _post(string $pattern, callable $callback, bool $pass_route = false, string $route_alias = ''): void { @@ -577,9 +561,9 @@ class Engine /** * Routes a URL to a callback function. * - * @param string $pattern URL pattern to match - * @param callable $callback Callback function - * @param bool $pass_route Pass the matching route object to the callback + * @param string $pattern URL pattern to match + * @param callable $callback Callback function + * @param bool $pass_route Pass the matching route object to the callback */ public function _put(string $pattern, callable $callback, bool $pass_route = false, string $route_alias = ''): void { @@ -589,9 +573,9 @@ class Engine /** * Routes a URL to a callback function. * - * @param string $pattern URL pattern to match - * @param callable $callback Callback function - * @param bool $pass_route Pass the matching route object to the callback + * @param string $pattern URL pattern to match + * @param callable $callback Callback function + * @param bool $pass_route Pass the matching route object to the callback */ public function _patch(string $pattern, callable $callback, bool $pass_route = false, string $route_alias = ''): void { @@ -601,9 +585,9 @@ class Engine /** * Routes a URL to a callback function. * - * @param string $pattern URL pattern to match - * @param callable $callback Callback function - * @param bool $pass_route Pass the matching route object to the callback + * @param string $pattern URL pattern to match + * @param callable $callback Callback function + * @param bool $pass_route Pass the matching route object to the callback */ public function _delete(string $pattern, callable $callback, bool $pass_route = false, string $route_alias = ''): void { @@ -613,7 +597,7 @@ class Engine /** * Stops processing and returns a given response. * - * @param int $code HTTP status code + * @param int $code HTTP status code * @param string $message Response message */ public function _halt(int $code = 200, string $message = ''): void @@ -632,20 +616,18 @@ class Engine /** 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( - '

404 Not Found

' . - '

The page you have requested could not be found.

' - ) + ->write($output) ->send(); } /** * Redirects the current request to another URL. * - * @param string $url URL * @param int $code HTTP status code */ public function _redirect(string $url, int $code = 303): void diff --git a/flight/Flight.php b/flight/Flight.php index c2bcf28..837d0bc 100644 --- a/flight/Flight.php +++ b/flight/Flight.php @@ -68,7 +68,7 @@ use flight\net\Route; * @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 etag(string $id, ('strong'|'weak') $type = 'strong') Performs ETag HTTP caching. * @method static void lastModified(int $time) Performs last modified HTTP caching. */ class Flight diff --git a/flight/net/Router.php b/flight/net/Router.php index 485faa2..5d067ad 100644 --- a/flight/net/Router.php +++ b/flight/net/Router.php @@ -67,10 +67,10 @@ class Router /** * Maps a URL pattern to a callback function. * - * @param string $pattern URL pattern to match - * @param callable $callback Callback function - * @param bool $pass_route Pass the matching route object to the callback - * @param string $route_alias Alias for the route + * @param string $pattern URL pattern to match. + * @param callable $callback Callback function. + * @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, callable $callback, bool $pass_route = false, string $route_alias = ''): Route { @@ -163,10 +163,10 @@ class Router /** * Group together a set of routes * - * @param string $group_prefix group URL prefix (such as /api/v1) - * @param callable $callback The necessary calling that holds the Router class - * @param array $group_middlewares The middlewares to be - * applied to the group Ex: [ $middleware1, $middleware2 ] + * @param string $group_prefix group URL prefix (such as /api/v1) + * @param callable $callback The necessary calling that holds the Router class + * @param array $group_middlewares + * The middlewares to be applied to the group. Example: `[$middleware1, $middleware2]` */ public function group(string $group_prefix, callable $callback, array $group_middlewares = []): void { diff --git a/phpcs.xml b/phpcs.xml index ad8cc88..60c0d7f 100644 --- a/phpcs.xml +++ b/phpcs.xml @@ -35,6 +35,7 @@ + flight/ tests/ From e35fc32a97a3bad18d49108208418064310b80d6 Mon Sep 17 00:00:00 2001 From: fadrian06 Date: Thu, 1 Feb 2024 17:38:47 -0400 Subject: [PATCH 04/13] Chaining method definitions in Dispatcher::set() --- flight/core/Dispatcher.php | 6 +++++- tests/DispatcherTest.php | 30 +++++++++++++----------------- 2 files changed, 18 insertions(+), 18 deletions(-) diff --git a/flight/core/Dispatcher.php b/flight/core/Dispatcher.php index fe7ca2f..7659446 100644 --- a/flight/core/Dispatcher.php +++ b/flight/core/Dispatcher.php @@ -69,10 +69,14 @@ class Dispatcher * * @param string $name Event name * @param callable $callback Callback function + * + * @return $this */ - public function set(string $name, callable $callback): void + public function set(string $name, callable $callback): self { $this->events[$name] = $callback; + + return $this; } /** diff --git a/tests/DispatcherTest.php b/tests/DispatcherTest.php index 6a9ff3d..df3880f 100644 --- a/tests/DispatcherTest.php +++ b/tests/DispatcherTest.php @@ -4,6 +4,7 @@ declare(strict_types=1); namespace tests; +use Closure; use Exception; use flight\core\Dispatcher; use tests\classes\Hello; @@ -21,54 +22,49 @@ class DispatcherTest extends TestCase // Map a closure public function testClosureMapping() { - $this->dispatcher->set('map1', function () { + $closure = Closure::fromCallable(function (): string { return 'hello'; }); - $result = $this->dispatcher->run('map1'); + $this->dispatcher->set('map1', $closure); + $result = $this->dispatcher->run('map1'); self::assertEquals('hello', $result); } // Map a function public function testFunctionMapping() { - $this->dispatcher->set('map2', function () { + $this->dispatcher->set('map2', function (): string { return 'hello'; }); $result = $this->dispatcher->run('map2'); - self::assertEquals('hello', $result); } public function testHasEvent() { - $this->dispatcher->set('map-event', function () { - return 'hello'; + $this->dispatcher->set('map-event', function (): void { }); $result = $this->dispatcher->has('map-event'); - $this->assertTrue($result); } public function testClearAllRegisteredEvents() { - $this->dispatcher->set('map-event', function () { - return 'hello'; - }); + $customFunction = $anotherFunction = function (): void { + }; - $this->dispatcher->set('map-event-2', function () { - return 'there'; - }); + $this->dispatcher + ->set('map-event', $customFunction) + ->set('map-event-2', $anotherFunction); $this->dispatcher->clear(); - $result = $this->dispatcher->has('map-event'); - $this->assertFalse($result); - $result = $this->dispatcher->has('map-event-2'); - $this->assertFalse($result); + $this->assertFalse($this->dispatcher->has('map-event')); + $this->assertFalse($this->dispatcher->has('map-event-2')); } public function testClearDeclaredRegisteredEvent() From 631d4a26c2e5799ce2938e493b50db3d0cd5c0d4 Mon Sep 17 00:00:00 2001 From: fadrian06 Date: Thu, 1 Feb 2024 17:55:52 -0400 Subject: [PATCH 05/13] Added method chaining Dispatcher::hook and FILTER_TYPES consts --- flight/core/Dispatcher.php | 9 +++- tests/DispatcherTest.php | 98 +++++++++++++++----------------------- 2 files changed, 46 insertions(+), 61 deletions(-) diff --git a/flight/core/Dispatcher.php b/flight/core/Dispatcher.php index 7659446..4d4b48d 100644 --- a/flight/core/Dispatcher.php +++ b/flight/core/Dispatcher.php @@ -19,6 +19,9 @@ use InvalidArgumentException; */ class Dispatcher { + public const FILTER_BEFORE = 'before'; + public const FILTER_AFTER = 'after'; + /** * Mapped events. * @@ -125,10 +128,14 @@ class Dispatcher * @param string $name Event name * @param 'before'|'after' $type Filter type * @param Closure(array &$params, string &$output): (void|false) $callback + * + * @return $this */ - public function hook(string $name, string $type, callable $callback): void + public function hook(string $name, string $type, callable $callback): self { $this->filters[$name][$type][] = $callback; + + return $this; } /** diff --git a/tests/DispatcherTest.php b/tests/DispatcherTest.php index df3880f..7055a5d 100644 --- a/tests/DispatcherTest.php +++ b/tests/DispatcherTest.php @@ -19,40 +19,33 @@ class DispatcherTest extends TestCase $this->dispatcher = new Dispatcher(); } - // Map a closure - public function testClosureMapping() + public function testClosureMapping(): void { - $closure = Closure::fromCallable(function (): string { + $this->dispatcher->set('map1', Closure::fromCallable(function (): string { return 'hello'; - }); - - $this->dispatcher->set('map1', $closure); + })); - $result = $this->dispatcher->run('map1'); - self::assertEquals('hello', $result); + $this->assertSame('hello', $this->dispatcher->run('map1')); } - // Map a function - public function testFunctionMapping() + public function testFunctionMapping(): void { $this->dispatcher->set('map2', function (): string { return 'hello'; }); - $result = $this->dispatcher->run('map2'); - self::assertEquals('hello', $result); + $this->assertSame('hello', $this->dispatcher->run('map2')); } - public function testHasEvent() + public function testHasEvent(): void { $this->dispatcher->set('map-event', function (): void { }); - $result = $this->dispatcher->has('map-event'); - $this->assertTrue($result); + $this->assertTrue($this->dispatcher->has('map-event')); } - public function testClearAllRegisteredEvents() + public function testClearAllRegisteredEvents(): void { $customFunction = $anotherFunction = function (): void { }; @@ -67,76 +60,61 @@ class DispatcherTest extends TestCase $this->assertFalse($this->dispatcher->has('map-event-2')); } - public function testClearDeclaredRegisteredEvent() + public function testClearDeclaredRegisteredEvent(): void { - $this->dispatcher->set('map-event', function () { - return 'hello'; - }); + $customFunction = $anotherFunction = function (): void { + }; - $this->dispatcher->set('map-event-2', function () { - return 'there'; - }); + $this->dispatcher + ->set('map-event', $customFunction) + ->set('map-event-2', $anotherFunction); $this->dispatcher->clear('map-event'); - $result = $this->dispatcher->has('map-event'); - $this->assertFalse($result); - $result = $this->dispatcher->has('map-event-2'); - $this->assertTrue($result); + $this->assertFalse($this->dispatcher->has('map-event')); + $this->assertTrue($this->dispatcher->has('map-event-2')); } - // Map a static function - public function testStaticFunctionMapping() + public function testStaticFunctionMapping(): void { - $this->dispatcher->set('map2', 'tests\classes\Hello::sayBye'); - - $result = $this->dispatcher->run('map2'); + $this->dispatcher->set('map2', Hello::class . '::sayBye'); - self::assertEquals('goodbye', $result); + $this->assertSame('goodbye', $this->dispatcher->run('map2')); } - // Map a class method - public function testClassMethodMapping() + public function testClassMethodMapping(): void { - $h = new Hello(); - - $this->dispatcher->set('map3', [$h, 'sayHi']); + $this->dispatcher->set('map3', [new Hello(), 'sayHi']); - $result = $this->dispatcher->run('map3'); - - self::assertEquals('hello', $result); + $this->assertSame('hello', $this->dispatcher->run('map3')); } - // Map a static class method - public function testStaticClassMethodMapping() + public function testStaticClassMethodMapping(): void { - $this->dispatcher->set('map4', ['\tests\classes\Hello', 'sayBye']); - - $result = $this->dispatcher->run('map4'); + $this->dispatcher->set('map4', [Hello::class, 'sayBye']); - self::assertEquals('goodbye', $result); + $this->assertSame('goodbye', $this->dispatcher->run('map4')); } - // Run before and after filters - public function testBeforeAndAfter() + public function testBeforeAndAfter(): void { - $this->dispatcher->set('hello', function ($name) { + $this->dispatcher->set('hello', function (string $name): string { return "Hello, $name!"; }); - $this->dispatcher->hook('hello', 'before', function (&$params) { - // Manipulate the parameter - $params[0] = 'Fred'; - }); - - $this->dispatcher->hook('hello', 'after', function (&$params, &$output) { - // Manipulate the output - $output .= ' Have a nice day!'; - }); + $this->dispatcher + ->hook('hello', $this->dispatcher::FILTER_BEFORE, function (array &$params): void { + // Manipulate the parameter + $params[0] = 'Fred'; + }) + ->hook('hello', $this->dispatcher::FILTER_AFTER, function (array &$params, string &$output): void { + // Manipulate the output + $output .= ' Have a nice day!'; + }); $result = $this->dispatcher->run('hello', ['Bob']); - self::assertEquals('Hello, Fred! Have a nice day!', $result); + $this->assertSame('Hello, Fred! Have a nice day!', $result); } // Test an invalid callback From 71a01c3711145d6abe73d4cb82bdf7b2e20ebe09 Mon Sep 17 00:00:00 2001 From: fadrian06 Date: Thu, 1 Feb 2024 19:17:45 -0400 Subject: [PATCH 06/13] Improved error reporting in Dispatcher --- flight/core/Dispatcher.php | 73 +++++++++++++++++------------- tests/DispatcherTest.php | 92 ++++++++++++++++++++++++++++++-------- 2 files changed, 115 insertions(+), 50 deletions(-) diff --git a/flight/core/Dispatcher.php b/flight/core/Dispatcher.php index 4d4b48d..3be1de0 100644 --- a/flight/core/Dispatcher.php +++ b/flight/core/Dispatcher.php @@ -22,45 +22,49 @@ class Dispatcher public const FILTER_BEFORE = 'before'; public const FILTER_AFTER = 'after'; - /** - * Mapped events. - * - * @var array - */ + /** @var array Mapped events. */ protected array $events = []; /** * Method filters. * - * @var array>> + * @var array &$params, mixed &$output): (void|false)>>> */ protected array $filters = []; /** * Dispatches an event. * - * @param string $name Event name - * @param array $params Callback parameters - * - * @throws Exception + * @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 = []) { $output = ''; // Run pre-filters - if (!empty($this->filters[$name]['before'])) { + $thereAreBeforeFilters = !empty($this->filters[$name]['before']); + + if ($thereAreBeforeFilters) { $this->filter($this->filters[$name]['before'], $params, $output); } // Run requested method $callback = $this->get($name); + + if ($callback === null) { + throw new Exception("Event '$name' isn't found."); + } + $output = $callback(...$params); // Run post-filters - if (!empty($this->filters[$name]['after'])) { + $thereAreAfterFilters = !empty($this->filters[$name]['after']); + + if ($thereAreAfterFilters) { $this->filter($this->filters[$name]['after'], $params, $output); } @@ -77,6 +81,10 @@ class Dispatcher */ public function set(string $name, callable $callback): self { + if ($this->get($name) !== null) { + trigger_error("Event '$name' has been overriden!", E_USER_NOTICE); + } + $this->events[$name] = $callback; return $this; @@ -87,7 +95,7 @@ class Dispatcher * * @param string $name Event name * - * @return ?callable $callback Callback function + * @return null|(Closure(): (void|mixed)) $callback Callback function */ public function get(string $name): ?callable { @@ -141,18 +149,19 @@ class Dispatcher /** * Executes a chain of method filters. * - * @param array $filters Chain of filters - * @param array $params Method parameters - * @param mixed $output Method output + * @param array &$params, mixed &$output): (void|false)> $filters + * Chain of filters- + * @param array $params Method parameters + * @param mixed $output Method output * - * @throws Exception + * @throws Exception If an event throws an `Exception`. */ public function filter(array $filters, array &$params, &$output): void { - $args = [&$params, &$output]; foreach ($filters as $callback) { - $continue = $callback(...$args); - if (false === $continue) { + $continue = $callback($params, $output); + + if ($continue === false) { break; } } @@ -161,33 +170,33 @@ class Dispatcher /** * Executes a callback function. * - * @param callable|array $callback Callback function - * @param array $params Function parameters - * - * @throws Exception + * @param callable-string|(Closure(): mixed)|array{class-string|object, string} $callback + * Callback function + * @param array $params Function parameters * * @return mixed Function results + * @throws Exception */ public static function execute($callback, array &$params = []) { - if (\is_callable($callback)) { - return \is_array($callback) ? - self::invokeMethod($callback, $params) : - self::callFunction($callback, $params); + if (!\is_callable($callback)) { + throw new InvalidArgumentException('Invalid callback specified.'); } - throw new InvalidArgumentException('Invalid callback specified.'); + return \is_array($callback) + ? self::invokeMethod($callback, $params) + : self::callFunction($callback, $params); } /** * Calls a function. * - * @param callable|string $func Name of function to call - * @param array $params Function parameters + * @param callable $func Name of function to call + * @param array &$params Function parameters * * @return mixed Function results */ - public static function callFunction($func, array &$params = []) + public static function callFunction(callable $func, array &$params = []) { return call_user_func_array($func, $params); } diff --git a/tests/DispatcherTest.php b/tests/DispatcherTest.php index 7055a5d..1d754bf 100644 --- a/tests/DispatcherTest.php +++ b/tests/DispatcherTest.php @@ -7,6 +7,7 @@ namespace tests; use Closure; use Exception; use flight\core\Dispatcher; +use PharIo\Manifest\InvalidEmailException; use tests\classes\Hello; use PHPUnit\Framework\TestCase; @@ -117,41 +118,96 @@ class DispatcherTest extends TestCase $this->assertSame('Hello, Fred! Have a nice day!', $result); } - // Test an invalid callback - public function testInvalidCallback() + public function testInvalidCallback(): void { $this->expectException(Exception::class); - $this->dispatcher->execute(['NonExistentClass', 'nonExistentMethod']); + Dispatcher::execute(['NonExistentClass', 'nonExistentMethod']); } - public function testCallFunction4Params() + public function testItThrowsAnExceptionWhenRunAnUnregistedEventName(): void { - $closure = function ($param1, $params2, $params3, $param4) { - return 'hello' . $param1 . $params2 . $params3 . $param4; + $this->expectException(Exception::class); + + $this->dispatcher->run('nonExistentEvent'); + } + + public function testWhenAnEventThrowsAnExceptionItPersistUntilNextCatchBlock(): void + { + $this->dispatcher->set('myMethod', function (): void { + throw new Exception('myMethod Exception'); + }); + + $this->expectException(Exception::class); + $this->expectExceptionMessage('myMethod Exception'); + + $this->dispatcher->run('myMethod'); + } + + public function testWhenAnEventThrowsCustomExceptionItPersistUntilNextCatchBlock(): void + { + $this->dispatcher->set('checkEmail', function (string $email): void { + throw new InvalidEmailException("Invalid email $email"); + }); + + $this->expectException(InvalidEmailException::class); + $this->expectExceptionMessage('Invalid email mail@mail,com'); + + $this->dispatcher->run('checkEmail', ['mail@mail,com']); + } + + public function testItThrowsNoticeForOverrideRegisteredEvents(): void + { + set_error_handler(function (int $errno, string $errstr): void { + $this->assertSame(E_USER_NOTICE, $errno); + $this->assertSame("Event 'myMethod' has been overriden!", $errstr); + }); + + $this->dispatcher->set('myMethod', function (): string { + return 'Original'; + }); + + $this->dispatcher->set('myMethod', function (): string { + return 'Overriden'; + }); + + $this->assertSame('Overriden', $this->dispatcher->run('myMethod')); + restore_error_handler(); + } + + public function testCallFunction4Params(): void + { + $myFunction = function ($param1, $param2, $param3, $param4) { + return "hello{$param1}{$param2}{$param3}{$param4}"; }; + $params = ['param1', 'param2', 'param3', 'param4']; - $result = $this->dispatcher->callFunction($closure, $params); - $this->assertEquals('helloparam1param2param3param4', $result); + $result = Dispatcher::callFunction($myFunction, $params); + + $this->assertSame('helloparam1param2param3param4', $result); } - public function testCallFunction5Params() + public function testCallFunction5Params(): void { - $closure = function ($param1, $params2, $params3, $param4, $param5) { - return 'hello' . $param1 . $params2 . $params3 . $param4 . $param5; + $myFunction = function ($param1, $param2, $param3, $param4, $param5) { + return "hello{$param1}{$param2}{$param3}{$param4}{$param5}"; }; + $params = ['param1', 'param2', 'param3', 'param4', 'param5']; - $result = $this->dispatcher->callFunction($closure, $params); - $this->assertEquals('helloparam1param2param3param4param5', $result); + $result = Dispatcher::callFunction($myFunction, $params); + + $this->assertSame('helloparam1param2param3param4param5', $result); } - public function testCallFunction6Params() + public function testCallFunction6Params(): void { - $closure = function ($param1, $params2, $params3, $param4, $param5, $param6) { - return 'hello' . $param1 . $params2 . $params3 . $param4 . $param5 . $param6; + $func = function ($param1, $param2, $param3, $param4, $param5, $param6) { + return "hello{$param1}{$param2}{$param3}{$param4}{$param5}{$param6}"; }; + $params = ['param1', 'param2', 'param3', 'param4', 'param5', 'param6']; - $result = $this->dispatcher->callFunction($closure, $params); - $this->assertEquals('helloparam1param2param3param4param5param6', $result); + $result = Dispatcher::callFunction($func, $params); + + $this->assertSame('helloparam1param2param3param4param5param6', $result); } } From 72c50d4cb5d7418f75bac4a6c521885f97d21717 Mon Sep 17 00:00:00 2001 From: fadrian06 Date: Thu, 1 Feb 2024 19:34:27 -0400 Subject: [PATCH 07/13] Throws an E_USER_NOTICE for invalid filter type --- flight/core/Dispatcher.php | 17 +++++++++++++---- tests/DispatcherTest.php | 19 +++++++++++++++++++ 2 files changed, 32 insertions(+), 4 deletions(-) diff --git a/flight/core/Dispatcher.php b/flight/core/Dispatcher.php index 3be1de0..f7d2a8a 100644 --- a/flight/core/Dispatcher.php +++ b/flight/core/Dispatcher.php @@ -21,6 +21,7 @@ class Dispatcher { public const FILTER_BEFORE = 'before'; public const FILTER_AFTER = 'after'; + private const FILTER_TYPES = [self::FILTER_BEFORE, self::FILTER_AFTER]; /** @var array Mapped events. */ protected array $events = []; @@ -121,13 +122,15 @@ class Dispatcher */ public function clear(?string $name = null): void { - if (null !== $name) { + if ($name !== null) { unset($this->events[$name]); unset($this->filters[$name]); - } else { - $this->events = []; - $this->filters = []; + + return; } + + $this->events = []; + $this->filters = []; } /** @@ -141,6 +144,12 @@ class Dispatcher */ public function hook(string $name, string $type, callable $callback): self { + if (!in_array($type, self::FILTER_TYPES, true)) { + $noticeMessage = "Invalid filter type '$type', use " . join('|', self::FILTER_TYPES); + + trigger_error($noticeMessage, E_USER_NOTICE); + } + $this->filters[$name][$type][] = $callback; return $this; diff --git a/tests/DispatcherTest.php b/tests/DispatcherTest.php index 1d754bf..0be7235 100644 --- a/tests/DispatcherTest.php +++ b/tests/DispatcherTest.php @@ -175,6 +175,25 @@ class DispatcherTest extends TestCase restore_error_handler(); } + public function testItThrowsNoticeForInvalidFilterTypes(): void + { + set_error_handler(function (int $errno, string $errstr): void { + $this->assertSame(E_USER_NOTICE, $errno); + $this->assertStringStartsWith("Invalid filter type 'invalid', use ", $errstr); + }); + + $this->dispatcher + ->set('myMethod', function (): string { + return 'Original'; + }) + ->hook('myMethod', 'invalid', function (array &$params, $output): void { + $output = 'Overriden'; + }); + + $this->assertSame('Original', $this->dispatcher->run('myMethod')); + restore_error_handler(); + } + public function testCallFunction4Params(): void { $myFunction = function ($param1, $param2, $param3, $param4) { From a15a82a209fc1d8195ea8de68031af1ae3c485e6 Mon Sep 17 00:00:00 2001 From: fadrian06 Date: Thu, 1 Feb 2024 19:42:31 -0400 Subject: [PATCH 08/13] It throws an Exception for invalid Filters array --- flight/core/Dispatcher.php | 8 ++++++-- tests/DispatcherTest.php | 14 ++++++++++++++ 2 files changed, 20 insertions(+), 2 deletions(-) diff --git a/flight/core/Dispatcher.php b/flight/core/Dispatcher.php index f7d2a8a..0fcc39a 100644 --- a/flight/core/Dispatcher.php +++ b/flight/core/Dispatcher.php @@ -165,9 +165,13 @@ class Dispatcher * * @throws Exception If an event throws an `Exception`. */ - public function filter(array $filters, array &$params, &$output): void + public static function filter(array $filters, array &$params, &$output): void { - foreach ($filters as $callback) { + foreach ($filters as $key => $callback) { + if (!is_callable($callback)) { + throw new InvalidArgumentException("Invalid callable \$filters[$key]."); + } + $continue = $callback($params, $output); if ($continue === false) { diff --git a/tests/DispatcherTest.php b/tests/DispatcherTest.php index 0be7235..795d01d 100644 --- a/tests/DispatcherTest.php +++ b/tests/DispatcherTest.php @@ -194,6 +194,20 @@ class DispatcherTest extends TestCase restore_error_handler(); } + public function testItThrowsAnExceptionForInvalidFilters(): void + { + $this->expectException(Exception::class); + $this->expectExceptionMessage('Invalid callable $filters[1]'); + + $params = []; + $output = ''; + $validCallable = function (): void { + }; + $invalidCallable = 'invalidGlobalFunction'; + + Dispatcher::filter([$validCallable, $invalidCallable], $params, $output); + } + public function testCallFunction4Params(): void { $myFunction = function ($param1, $param2, $param3, $param4) { From cd79dc92b6c828f812f7fce05b0744ad464e3a5e Mon Sep 17 00:00:00 2001 From: fadrian06 Date: Thu, 1 Feb 2024 20:04:31 -0400 Subject: [PATCH 09/13] Can execute instance methods statically (useful for Controllers) --- flight/core/Dispatcher.php | 26 +++++++++++++++----------- tests/DispatcherTest.php | 9 ++++++++- 2 files changed, 23 insertions(+), 12 deletions(-) diff --git a/flight/core/Dispatcher.php b/flight/core/Dispatcher.php index 0fcc39a..b1e6d73 100644 --- a/flight/core/Dispatcher.php +++ b/flight/core/Dispatcher.php @@ -7,6 +7,7 @@ namespace flight\core; use Closure; use Exception; use InvalidArgumentException; +use TypeError; /** * The Dispatcher class is responsible for dispatching events. Events @@ -192,13 +193,15 @@ class Dispatcher */ public static function execute($callback, array &$params = []) { - if (!\is_callable($callback)) { + if (is_string($callback) && !function_exists($callback)) { throw new InvalidArgumentException('Invalid callback specified.'); } - return \is_array($callback) - ? self::invokeMethod($callback, $params) - : self::callFunction($callback, $params); + if (is_array($callback)) { + return self::invokeMethod($callback, $params); + } + + return self::callFunction($callback, $params); } /** @@ -217,20 +220,21 @@ class Dispatcher /** * Invokes a method. * - * @param mixed $func Class method - * @param array $params Class method parameters + * @param array{class-string|object, string} $func Class method + * @param array &$params Class method parameters * * @return mixed Function results + * @throws TypeError For unexistent class name. */ - public static function invokeMethod($func, array &$params = []) + public static function invokeMethod(array $func, array &$params = []) { [$class, $method] = $func; - $instance = \is_object($class); + if (is_string($class) && class_exists($class)) { + $class = new $class(); + } - return ($instance) ? - $class->$method(...$params) : - $class::$method(); + return call_user_func_array([$class, $method], $params); } /** diff --git a/tests/DispatcherTest.php b/tests/DispatcherTest.php index 795d01d..97401ad 100644 --- a/tests/DispatcherTest.php +++ b/tests/DispatcherTest.php @@ -10,6 +10,7 @@ use flight\core\Dispatcher; use PharIo\Manifest\InvalidEmailException; use tests\classes\Hello; use PHPUnit\Framework\TestCase; +use TypeError; class DispatcherTest extends TestCase { @@ -120,11 +121,17 @@ class DispatcherTest extends TestCase public function testInvalidCallback(): void { - $this->expectException(Exception::class); + $this->expectException(TypeError::class); Dispatcher::execute(['NonExistentClass', 'nonExistentMethod']); } + // It will be useful for executing instance Controller methods statically + public function testCanExecuteAnNonStaticMethodStatically(): void + { + $this->assertSame('hello', Dispatcher::execute([Hello::class, 'sayHi'])); + } + public function testItThrowsAnExceptionWhenRunAnUnregistedEventName(): void { $this->expectException(Exception::class); From 31c690d17b7ebf84982ecb659619209b11bbd078 Mon Sep 17 00:00:00 2001 From: fadrian06 Date: Thu, 1 Feb 2024 20:28:33 -0400 Subject: [PATCH 10/13] Dispatcher::reset now is also chaineable --- flight/core/Dispatcher.php | 6 +++++- phpstan.neon | 1 + 2 files changed, 6 insertions(+), 1 deletion(-) diff --git a/flight/core/Dispatcher.php b/flight/core/Dispatcher.php index b1e6d73..7be4035 100644 --- a/flight/core/Dispatcher.php +++ b/flight/core/Dispatcher.php @@ -239,10 +239,14 @@ class Dispatcher /** * Resets the object to the initial state. + * + * @return $this */ - public function reset(): void + public function reset(): self { $this->events = []; $this->filters = []; + + return $this; } } diff --git a/phpstan.neon b/phpstan.neon index 0b21e61..95afff0 100644 --- a/phpstan.neon +++ b/phpstan.neon @@ -8,3 +8,4 @@ parameters: paths: - flight - index.php + treatPhpDocTypesAsCertain: false From db02086d216c35f05fdf88103dcc7a874e8a501c Mon Sep 17 00:00:00 2001 From: fadrian06 Date: Thu, 1 Feb 2024 20:31:09 -0400 Subject: [PATCH 11/13] Improved Dispatcher::set docblock --- flight/core/Dispatcher.php | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/flight/core/Dispatcher.php b/flight/core/Dispatcher.php index 7be4035..a0f24aa 100644 --- a/flight/core/Dispatcher.php +++ b/flight/core/Dispatcher.php @@ -76,8 +76,8 @@ class Dispatcher /** * Assigns a callback to an event. * - * @param string $name Event name - * @param callable $callback Callback function + * @param string $name Event name + * @param Closure(): (void|mixed) $callback Callback function * * @return $this */ From afba9c16a0372fc98f657ba64f110e45298e2eea Mon Sep 17 00:00:00 2001 From: fadrian06 Date: Sun, 4 Feb 2024 18:34:58 -0400 Subject: [PATCH 12/13] Fixed https://github.com/flightphp/core/pull/538#pullrequestreview-1861099821 --- composer.json | 3 +-- flight/Flight.php | 2 ++ flight/autoload.php | 1 + flight/core/Loader.php | 9 +++++---- index.php | 3 ++- phpcs.xml | 2 ++ 6 files changed, 13 insertions(+), 7 deletions(-) diff --git a/composer.json b/composer.json index e6fd63a..d36788d 100644 --- a/composer.json +++ b/composer.json @@ -28,8 +28,7 @@ }, "autoload": { "files": [ - "flight/autoload.php", - "flight/Flight.php" + "flight/autoload.php" ] }, "autoload-dev": { diff --git a/flight/Flight.php b/flight/Flight.php index 837d0bc..c53b979 100644 --- a/flight/Flight.php +++ b/flight/Flight.php @@ -10,6 +10,8 @@ use flight\net\Router; use flight\template\View; use flight\net\Route; +require_once __DIR__ . '/autoload.php'; + /** * The Flight class is a static representation of the framework. * diff --git a/flight/autoload.php b/flight/autoload.php index 3a7ea5a..0a31c86 100644 --- a/flight/autoload.php +++ b/flight/autoload.php @@ -4,6 +4,7 @@ declare(strict_types=1); use flight\core\Loader; +require_once __DIR__ . '/Flight.php'; require_once __DIR__ . '/core/Loader.php'; Loader::autoload(true, [dirname(__DIR__)]); diff --git a/flight/core/Loader.php b/flight/core/Loader.php index 3bd3696..9792949 100644 --- a/flight/core/Loader.php +++ b/flight/core/Loader.php @@ -190,12 +190,13 @@ class Loader */ public static function loadClass(string $class): void { - $class_file = str_replace(['\\', '_'], '/', $class) . '.php'; + $classFile = str_replace(['\\', '_'], '/', $class) . '.php'; foreach (self::$dirs as $dir) { - $file = $dir . '/' . $class_file; - if (file_exists($file)) { - require $file; + $filePath = "$dir/$classFile"; + + if (file_exists($filePath)) { + require_once $filePath; return; } diff --git a/index.php b/index.php index 65cdd7e..5a21ed6 100644 --- a/index.php +++ b/index.php @@ -1,6 +1,7 @@ + + flight/ tests/ From 6e29e663ee70bb0830b16cfd958bd84473ee9eef Mon Sep 17 00:00:00 2001 From: fadrian06 Date: Sun, 4 Feb 2024 19:12:01 -0400 Subject: [PATCH 13/13] Added Exception for call instance methods when a Class constructor require parameters --- flight/Engine.php | 2 +- flight/core/Dispatcher.php | 25 ++++++++++++++++++++++++- 2 files changed, 25 insertions(+), 2 deletions(-) diff --git a/flight/Engine.php b/flight/Engine.php index c62df84..58b0859 100644 --- a/flight/Engine.php +++ b/flight/Engine.php @@ -145,7 +145,7 @@ class Engine ]; foreach ($methods as $name) { - $this->dispatcher->set($name, [$this, '_' . $name]); + $this->dispatcher->set($name, [$this, "_$name"]); } // Default configuration settings diff --git a/flight/core/Dispatcher.php b/flight/core/Dispatcher.php index a0f24aa..d91b4b2 100644 --- a/flight/core/Dispatcher.php +++ b/flight/core/Dispatcher.php @@ -7,6 +7,7 @@ namespace flight\core; use Closure; use Exception; use InvalidArgumentException; +use ReflectionClass; use TypeError; /** @@ -193,7 +194,12 @@ class Dispatcher */ public static function execute($callback, array &$params = []) { - if (is_string($callback) && !function_exists($callback)) { + $isInvalidFunctionName = ( + is_string($callback) + && !function_exists($callback) + ); + + if ($isInvalidFunctionName) { throw new InvalidArgumentException('Invalid callback specified.'); } @@ -231,6 +237,23 @@ class Dispatcher [$class, $method] = $func; if (is_string($class) && class_exists($class)) { + $constructor = (new ReflectionClass($class))->getConstructor(); + $constructorParamsNumber = 0; + + if ($constructor !== null) { + $constructorParamsNumber = count($constructor->getParameters()); + } + + if ($constructorParamsNumber > 0) { + $exceptionMessage = "Method '$class::$method' cannot be called statically. "; + $exceptionMessage .= sprintf( + "$class::__construct require $constructorParamsNumber parameter%s", + $constructorParamsNumber > 1 ? 's' : '' + ); + + throw new InvalidArgumentException($exceptionMessage, E_ERROR); + } + $class = new $class(); }