diff --git a/.editorconfig b/.editorconfig index 803737fe..dcc56878 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/composer.json b/composer.json index e6fd63a3..d36788db 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.sublime-project b/flight.sublime-project index ccca03f7..536f6441 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": { @@ -40,12 +41,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/flight/Engine.php b/flight/Engine.php index 32a1a705..58b08599 100644 --- a/flight/Engine.php +++ b/flight/Engine.php @@ -32,7 +32,7 @@ * # 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 @@ * @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,59 +57,41 @@ * @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. */ -// phpcs:ignoreFile Generic.Files.LineLength.TooLong, PSR2.Methods.MethodDeclaration.Underscore 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) @@ -121,7 +103,7 @@ public function __call(string $name, array $params) } 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]; @@ -129,11 +111,11 @@ public function __call(string $name, array $params) return $this->loader->load($name, $shared); } - // Core Methods + ////////////////// + // Core Methods // + ////////////////// - /** - * Initializes the framework. - */ + /** Initializes the framework. */ public function init(): void { $initialized = $this->initialized; @@ -149,7 +131,8 @@ public function init(): void $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'); }); @@ -160,8 +143,9 @@ public function init(): void 'render', 'redirect', 'etag', 'lastModified', 'json', 'jsonp', 'post', 'put', 'patch', 'delete', 'group', 'getUrl', ]; + foreach ($methods as $name) { - $this->dispatcher->set($name, [$this, '_' . $name]); + $this->dispatcher->set($name, [$this, "_$name"]); } // Default configuration settings @@ -193,15 +177,15 @@ public function init(): void /** * 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); @@ -215,7 +199,7 @@ public function handleError(int $errno, string $errstr, string $errfile, int $er * * @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 @@ -227,7 +211,7 @@ public function handleException($e): void /** * 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 @@ -243,13 +227,21 @@ public function map(string $name, callable $callback): void /** * 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 +262,8 @@ public function unregister(string $methodName): void /** * 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 +273,8 @@ public function before(string $name, callable $callback): void /** * 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 +284,9 @@ public function after(string $name, callable $callback): void /** * 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 +300,27 @@ public function get(?string $key = null) /** * 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 +332,16 @@ public function has(string $key): bool /** * 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 +474,7 @@ public function _start(): void * * @param Throwable $e Thrown exception */ - public function _error($e): void + public function _error(Throwable $e): void { $msg = sprintf( '

500 Internal Server Error

' . @@ -505,7 +501,7 @@ public function _error($e): void /** * Stops the framework and outputs the current response. * - * @param int|null $code HTTP status code + * @param ?int $code HTTP status code * * @throws Exception */ @@ -528,11 +524,10 @@ public function _stop(?int $code = null): void /** * 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 { @@ -542,9 +537,9 @@ public function _route(string $pattern, callable $callback, bool $pass_route = f /** * 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 { @@ -554,55 +549,55 @@ public function _group(string $pattern, callable $callback, array $group_middlew /** * 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): 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); } /** * 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): 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); } /** * 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): 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); } /** * 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): 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); } /** * 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 @@ -618,26 +613,22 @@ public function _halt(int $code = 200, string $message = ''): void } } - /** - * 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 { + $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 + * @param int $code HTTP status code */ public function _redirect(string $url, int $code = 303): void { @@ -662,29 +653,30 @@ public function _redirect(string $url, int $code = 303): void /** * 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 +699,12 @@ public function _json( /** * 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 +717,6 @@ public function _jsonp( int $option = 0 ): void { $json = $encode ? json_encode($data, $option) : $data; - $callback = $this->request()->query[$param]; $this->response() @@ -738,8 +729,8 @@ public function _jsonp( /** * 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 1f1d2ae1..c53b9799 100644 --- a/flight/Flight.php +++ b/flight/Flight.php @@ -10,76 +10,75 @@ use flight\template\View; use flight\net\Route; +require_once __DIR__ . '/autoload.php'; + /** * 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 +103,53 @@ private function __clone() /** * 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/autoload.php b/flight/autoload.php index 3a7ea5a0..0a31c864 100644 --- a/flight/autoload.php +++ b/flight/autoload.php @@ -4,6 +4,7 @@ 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/Dispatcher.php b/flight/core/Dispatcher.php index ff9c9ecd..d91b4b2d 100644 --- a/flight/core/Dispatcher.php +++ b/flight/core/Dispatcher.php @@ -4,8 +4,11 @@ namespace flight\core; +use Closure; use Exception; use InvalidArgumentException; +use ReflectionClass; +use TypeError; /** * The Dispatcher class is responsible for dispatching events. Events @@ -18,45 +21,53 @@ */ class Dispatcher { - /** - * Mapped events. - * - * @var array - */ + 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 = []; /** * 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); } @@ -66,12 +77,20 @@ public function run(string $name, array $params = []) /** * 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 */ - public function set(string $name, callable $callback): void + 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; } /** @@ -79,7 +98,7 @@ public function set(string $name, callable $callback): void * * @param string $name Event name * - * @return ?callable $callback Callback function + * @return null|(Closure(): (void|mixed)) $callback Callback function */ public function get(string $name): ?callable { @@ -105,42 +124,59 @@ public function has(string $name): bool */ 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 = []; } /** * 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 + * + * @return $this */ - public function hook(string $name, string $type, callable $callback): void + 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; } /** * 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 + public static function filter(array $filters, array &$params, &$output): void { - $args = [&$params, &$output]; - foreach ($filters as $callback) { - $continue = $callback(...$args); - if (false === $continue) { + foreach ($filters as $key => $callback) { + if (!is_callable($callback)) { + throw new InvalidArgumentException("Invalid callable \$filters[$key]."); + } + + $continue = $callback($params, $output); + + if ($continue === false) { break; } } @@ -149,33 +185,40 @@ public function filter(array $filters, array &$params, &$output): void /** * 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); + $isInvalidFunctionName = ( + is_string($callback) + && !function_exists($callback) + ); + + if ($isInvalidFunctionName) { + throw new InvalidArgumentException('Invalid callback specified.'); } - throw new InvalidArgumentException('Invalid callback specified.'); + if (is_array($callback)) { + return self::invokeMethod($callback, $params); + } + + return 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); } @@ -183,28 +226,50 @@ public static function callFunction($func, array &$params = []) /** * 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)) { + $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' : '' + ); - return ($instance) ? - $class->$method(...$params) : - $class::$method(); + throw new InvalidArgumentException($exceptionMessage, E_ERROR); + } + + $class = new $class(); + } + + return call_user_func_array([$class, $method], $params); } /** * 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/flight/core/Loader.php b/flight/core/Loader.php index bf83e3f8..9792949a 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 { @@ -192,12 +190,13 @@ public static function autoload(bool $enabled = true, $dirs = []): void */ 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/flight/net/Router.php b/flight/net/Router.php index 485faa29..5d067adf 100644 --- a/flight/net/Router.php +++ b/flight/net/Router.php @@ -67,10 +67,10 @@ public function clear(): void /** * 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 @@ public function delete(string $pattern, callable $callback, bool $pass_route = f /** * 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/index.php b/index.php index 65cdd7e8..5a21ed6d 100644 --- a/index.php +++ b/index.php @@ -1,6 +1,7 @@ - + - - - + + + + + + + flight/ tests/ - tests/views/* - tests/views/ + tests/views/* diff --git a/phpstan.neon b/phpstan.neon index 0b21e618..95afff0b 100644 --- a/phpstan.neon +++ b/phpstan.neon @@ -8,3 +8,4 @@ parameters: paths: - flight - index.php + treatPhpDocTypesAsCertain: false diff --git a/tests/DispatcherTest.php b/tests/DispatcherTest.php index 6a9ff3d7..97401ad5 100644 --- a/tests/DispatcherTest.php +++ b/tests/DispatcherTest.php @@ -4,10 +4,13 @@ namespace tests; +use Closure; use Exception; use flight\core\Dispatcher; +use PharIo\Manifest\InvalidEmailException; use tests\classes\Hello; use PHPUnit\Framework\TestCase; +use TypeError; class DispatcherTest extends TestCase { @@ -18,166 +21,233 @@ protected function setUp(): void $this->dispatcher = new Dispatcher(); } - // Map a closure - public function testClosureMapping() + public function testClosureMapping(): void { - $this->dispatcher->set('map1', function () { + $this->dispatcher->set('map1', Closure::fromCallable(function (): string { return 'hello'; - }); - - $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 () { + $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 () { - return 'hello'; + $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 { - $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() + 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'); + $this->dispatcher->set('map2', Hello::class . '::sayBye'); - $result = $this->dispatcher->run('map2'); + $this->assertSame('goodbye', $this->dispatcher->run('map2')); + } - self::assertEquals('goodbye', $result); + public function testClassMethodMapping(): void + { + $this->dispatcher->set('map3', [new Hello(), 'sayHi']); + + $this->assertSame('hello', $this->dispatcher->run('map3')); } - // Map a class method - public function testClassMethodMapping() + public function testStaticClassMethodMapping(): void { - $h = new Hello(); + $this->dispatcher->set('map4', [Hello::class, 'sayBye']); + + $this->assertSame('goodbye', $this->dispatcher->run('map4')); + } + + public function testBeforeAndAfter(): void + { + $this->dispatcher->set('hello', function (string $name): string { + return "Hello, $name!"; + }); - $this->dispatcher->set('map3', [$h, 'sayHi']); + $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('map3'); + $result = $this->dispatcher->run('hello', ['Bob']); - self::assertEquals('hello', $result); + $this->assertSame('Hello, Fred! Have a nice day!', $result); } - // Map a static class method - public function testStaticClassMethodMapping() + public function testInvalidCallback(): void { - $this->dispatcher->set('map4', ['\tests\classes\Hello', 'sayBye']); + $this->expectException(TypeError::class); - $result = $this->dispatcher->run('map4'); + Dispatcher::execute(['NonExistentClass', 'nonExistentMethod']); + } - self::assertEquals('goodbye', $result); + // It will be useful for executing instance Controller methods statically + public function testCanExecuteAnNonStaticMethodStatically(): void + { + $this->assertSame('hello', Dispatcher::execute([Hello::class, 'sayHi'])); } - // Run before and after filters - public function testBeforeAndAfter() + public function testItThrowsAnExceptionWhenRunAnUnregistedEventName(): void { - $this->dispatcher->set('hello', function ($name) { - return "Hello, $name!"; + $this->expectException(Exception::class); + + $this->dispatcher->run('nonExistentEvent'); + } + + public function testWhenAnEventThrowsAnExceptionItPersistUntilNextCatchBlock(): void + { + $this->dispatcher->set('myMethod', function (): void { + throw new Exception('myMethod Exception'); }); - $this->dispatcher->hook('hello', 'before', function (&$params) { - // Manipulate the parameter - $params[0] = 'Fred'; + $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->dispatcher->hook('hello', 'after', function (&$params, &$output) { - // Manipulate the output - $output .= ' Have a nice day!'; + $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); }); - $result = $this->dispatcher->run('hello', ['Bob']); + $this->dispatcher->set('myMethod', function (): string { + return 'Original'; + }); + + $this->dispatcher->set('myMethod', function (): string { + return 'Overriden'; + }); - self::assertEquals('Hello, Fred! Have a nice day!', $result); + $this->assertSame('Overriden', $this->dispatcher->run('myMethod')); + restore_error_handler(); } - // Test an invalid callback - public function testInvalidCallback() + 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 testItThrowsAnExceptionForInvalidFilters(): void { $this->expectException(Exception::class); + $this->expectExceptionMessage('Invalid callable $filters[1]'); + + $params = []; + $output = ''; + $validCallable = function (): void { + }; + $invalidCallable = 'invalidGlobalFunction'; - $this->dispatcher->execute(['NonExistentClass', 'nonExistentMethod']); + Dispatcher::filter([$validCallable, $invalidCallable], $params, $output); } - public function testCallFunction4Params() + public function testCallFunction4Params(): void { - $closure = function ($param1, $params2, $params3, $param4) { - return 'hello' . $param1 . $params2 . $params3 . $param4; + $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); } } diff --git a/tests/FlightTest.php b/tests/FlightTest.php index 2018d535..c37a8fa6 100644 --- a/tests/FlightTest.php +++ b/tests/FlightTest.php @@ -164,7 +164,6 @@ public function testStaticRoutePost() public function testStaticRoutePut() { - Flight::put('/test', function () { echo 'test put'; });