Merge pull request #538 from flightphp/dev

Dispatcher improvements.
pull/539/head v3.4.0
n0nag0n 12 months ago committed by GitHub
commit 6e40f791c2
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194

@ -5,3 +5,6 @@ indent_style = space
indent_size = 4
end_of_line = lf
charset = utf-8
[*.md]
indent_size = 2

@ -28,8 +28,7 @@
},
"autoload": {
"files": [
"flight/autoload.php",
"flight/Flight.php"
"flight/autoload.php"
]
},
"autoload-dev": {

@ -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"
},
{

@ -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<int, callable|object> $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,59 +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.
*/
// phpcs:ignoreFile Generic.Files.LineLength.TooLong, PSR2.Methods.MethodDeclaration.Underscore
class Engine
{
/**
* Stored variables.
* @var array<string, mixed>
*/
protected array $vars;
/** @var array<string, mixed> 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<int, mixed> $params Method parameters
* @param string $name Method name
* @param array<int, mixed> $params Method parameters
*
* @throws Exception
*
* @return mixed Callback results
*/
public function __call(string $name, array $params)
@ -121,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];
@ -129,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;
@ -149,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');
});
@ -160,8 +143,9 @@ class Engine
'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 @@ 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);
@ -215,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
@ -227,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
@ -243,13 +227,21 @@ class Engine
/**
* Registers a class to a framework method.
* @template T of object
*
* @param string $name Method name
* @param class-string<T> $class Class name
* @param array<int, mixed> $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<T> $class Class name
* @param array<int, mixed> $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 @@ 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<int, mixed> &$params, string &$output): (void|false) $callback
*/
public function before(string $name, callable $callback): void
{
@ -281,8 +273,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<int, mixed> &$params, string &$output): (void|false) $callback
*/
public function after(string $name, callable $callback): void
{
@ -292,9 +284,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 +300,27 @@ class Engine
/**
* Sets a variable.
*
* @param mixed $key Key
* @param mixed|null $value Value
* @param string|iterable<string, mixed> $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 @@ 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 +474,7 @@ class Engine
*
* @param Throwable $e Thrown exception
*/
public function _error($e): void
public function _error(Throwable $e): void
{
$msg = sprintf(
'<h1>500 Internal Server Error</h1>' .
@ -505,7 +501,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
*/
@ -528,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
{
@ -542,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<callable> $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<int, callable|object> $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 @@ 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): 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 @@ 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
{
$output = '<h1>404 Not Found</h1><h3>The page you have requested could not be found.</h3>';
$this->response()
->clear()
->status(404)
->write(
'<h1>404 Not Found</h1>' .
'<h3>The page you have requested could not be found.</h3>'
)
->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 @@ class Engine
/**
* Renders a template.
*
* @param string $file Template file
* @param string $file Template file
* @param ?array<string, mixed> $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 @@ 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 +717,6 @@ class Engine
int $option = 0
): void {
$json = $encode ? json_encode($data, $option) : $data;
$callback = $this->request()->query[$param];
$this->response()
@ -738,8 +729,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
{

@ -10,76 +10,75 @@ 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.
*
* @license MIT, http://flightphp.com/license
* @copyright Copyright (c) 2011, Mike Cao <mike@mikecao.com>
*
* # 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<string, mixed> $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<int, mixed> &$params, string &$output): (void|false) $callback)
* Adds a filter before a framework method.
* @method static void after(string $name, Closure(array<int, mixed> &$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<string, mixed> $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<string, mixed> $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 @@ 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<T> $class Fully Qualified Class Name
* @param array<int, mixed> $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<T> $class Fully Qualified Class Name
* @param array<int, mixed> $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<int, mixed> $params Method parameters
*
* @throws Exception
* @param string $name Method name
* @param array<int, mixed> $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;
}

@ -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__)]);

@ -4,8 +4,11 @@ declare(strict_types=1);
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 @@ use InvalidArgumentException;
*/
class Dispatcher
{
/**
* Mapped events.
*
* @var array<string, callable>
*/
public const FILTER_BEFORE = 'before';
public const FILTER_AFTER = 'after';
private const FILTER_TYPES = [self::FILTER_BEFORE, self::FILTER_AFTER];
/** @var array<string, Closure(): (void|mixed)> Mapped events. */
protected array $events = [];
/**
* Method filters.
*
* @var array<string, array<'before'|'after', array<int, callable>>>
* @var array<string, array<'before'|'after', array<int, Closure(array<int, mixed> &$params, mixed &$output): (void|false)>>>
*/
protected array $filters = [];
/**
* Dispatches an event.
*
* @param string $name Event name
* @param array<int, mixed> $params Callback parameters
*
* @throws Exception
* @param string $name Event name
* @param array<int, mixed> $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 @@ 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
*/
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 @@ 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
{
@ -105,42 +124,59 @@ 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 = [];
}
/**
* 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<int, mixed> &$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<int, callable> $filters Chain of filters
* @param array<int, mixed> $params Method parameters
* @param mixed $output Method output
* @param array<int, Closure(array<int, mixed> &$params, mixed &$output): (void|false)> $filters
* Chain of filters-
* @param array<int, mixed> $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 @@ class Dispatcher
/**
* Executes a callback function.
*
* @param callable|array<class-string|object, string> $callback Callback function
* @param array<int, mixed> $params Function parameters
*
* @throws Exception
* @param callable-string|(Closure(): mixed)|array{class-string|object, string} $callback
* Callback function
* @param array<int, mixed> $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<int, mixed> $params Function parameters
* @param callable $func Name of function to call
* @param array<int, mixed> &$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 @@ class Dispatcher
/**
* Invokes a method.
*
* @param mixed $func Class method
* @param array<int, mixed> $params Class method parameters
* @param array{class-string|object, string} $func Class method
* @param array<int, mixed> &$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;
}
}

@ -42,14 +42,12 @@ class Loader
/**
* Registers a class.
*
* @param string $name Registry name
* @param class-string<T>|Closure(): T $class Class name or function to instantiate class
* @param array<int, mixed> $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<T>|Closure(): T $class Class name or function to instantiate class
* @param array<int, mixed> $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 @@ 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;
}

@ -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<int, callable|object> $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<int, callable|object> $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
{

@ -1,6 +1,7 @@
<?php
require 'flight/Flight.php';
// require 'flight/Flight.php';
require 'flight/autoload.php';
Flight::route('/', function () {
echo 'hello world!';

@ -29,14 +29,17 @@
<exclude name="Generic.Commenting.DocComment.SpacingBeforeShort" />
<exclude name="Generic.Commenting.DocComment.ContentAfterOpen" />
<exclude name="Generic.Functions.OpeningFunctionBraceBsdAllman.BraceOnSameLine" />
<exclude name="Generic.PHP.DisallowRequestSuperglobal.Found" />
<exclude name="Generic.PHP.DisallowRequestSuperglobal.Found" />
<exclude name="Generic.Commenting.DocComment.ContentBeforeClose" />
<exclude name="Generic.ControlStructures.DisallowYodaConditions.Found" />
<exclude name="Generic.Strings.UnnecessaryStringConcat.Found" />
<exclude name="Generic.CodeAnalysis.AssignmentInCondition.FoundInWhileCondition" />
<exclude name="Generic.ControlStructures.DisallowYodaConditions.Found" />
<exclude name="Generic.Strings.UnnecessaryStringConcat.Found" />
<exclude name="Generic.CodeAnalysis.AssignmentInCondition.FoundInWhileCondition" />
<exclude name="Generic.Commenting.DocComment.MissingShort" />
<exclude name="Generic.Commenting.DocComment.SpacingBeforeTags" />
<exclude name="Generic.WhiteSpace.ArbitraryParenthesesSpacing.SpaceAfterOpen" />
<exclude name="Generic.WhiteSpace.ArbitraryParenthesesSpacing.SpaceBeforeClose" />
</rule>
<file>flight/</file>
<file>tests/</file>
<exclude-pattern>tests/views/*</exclude-pattern>
<ignore>tests/views/</ignore>
<exclude-pattern>tests/views/*</exclude-pattern>
</ruleset>

@ -8,3 +8,4 @@ parameters:
paths:
- flight
- index.php
treatPhpDocTypesAsCertain: false

@ -4,10 +4,13 @@ declare(strict_types=1);
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 @@ class DispatcherTest extends TestCase
$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);
}
}

@ -164,7 +164,6 @@ class FlightTest extends TestCase
public function testStaticRoutePut()
{
Flight::put('/test', function () {
echo 'test put';
});

Loading…
Cancel
Save