Merge pull request #567 from flightphp/dispatcher-refactor

Improvements to Dispatcher class
pull/571/head
n0nag0n 10 months ago committed by GitHub
commit d1584a8aeb
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194

1
.gitignore vendored

@ -6,3 +6,4 @@ composer.lock
coverage/ coverage/
*.sublime* *.sublime*
clover.xml clover.xml
phpcs.xml

@ -64,7 +64,10 @@
"test-coverage:win": "del clover.xml && phpunit --coverage-html=coverage --coverage-clover=clover.xml && coverage-check clover.xml 100", "test-coverage:win": "del clover.xml && phpunit --coverage-html=coverage --coverage-clover=clover.xml && coverage-check clover.xml 100",
"lint": "phpstan --no-progress -cphpstan.neon", "lint": "phpstan --no-progress -cphpstan.neon",
"beautify": "phpcbf --standard=phpcs.xml", "beautify": "phpcbf --standard=phpcs.xml",
"phpcs": "phpcs --standard=phpcs.xml -n" "phpcs": "phpcs --standard=phpcs.xml -n",
"post-install-cmd": [
"php -r \"if (!file_exists('phpcs.xml')) copy('phpcs.xml.dist', 'phpcs.xml');\""
]
}, },
"suggest": { "suggest": {
"latte/latte": "Latte template engine", "latte/latte": "Latte template engine",

@ -4,10 +4,12 @@ declare(strict_types=1);
namespace flight\core; namespace flight\core;
use Closure;
use Exception; use Exception;
use flight\Engine; use flight\Engine;
use InvalidArgumentException; use InvalidArgumentException;
use Psr\Container\ContainerInterface;
use ReflectionFunction;
use Throwable;
use TypeError; use TypeError;
/** /**
@ -23,41 +25,54 @@ class Dispatcher
{ {
public const FILTER_BEFORE = 'before'; public const FILTER_BEFORE = 'before';
public const FILTER_AFTER = 'after'; public const FILTER_AFTER = 'after';
private const FILTER_TYPES = [self::FILTER_BEFORE, self::FILTER_AFTER];
/** @var mixed $containerException Exception message if thrown by setting the container as a callable method */ /** Exception message if thrown by setting the container as a callable method. */
protected $containerException = null; protected ?Throwable $containerException = null;
/** @var ?Engine $engine Engine instance */ /** @var ?Engine $engine Engine instance. */
protected ?Engine $engine = null; protected ?Engine $engine = null;
/** @var array<string, Closure(): (void|mixed)> Mapped events. */ /** @var array<string, callable(): (void|mixed)> Mapped events. */
protected array $events = []; protected array $events = [];
/** /**
* Method filters. * Method filters.
* *
* @var array<string, array<'before'|'after', array<int, Closure(array<int, mixed> &$params, mixed &$output): (void|false)>>> * @var array<string, array<'before'|'after', array<int, callable(array<int, mixed> &$params, mixed &$output): (void|false)>>>
*/ */
protected array $filters = []; protected array $filters = [];
/** /**
* This is a container for the dependency injection. * This is a container for the dependency injection.
* *
* @var callable|object|null * @var null|ContainerInterface|(callable(string $classString, array<int, mixed> $params): (null|object))
*/ */
protected $containerHandler = null; protected $containerHandler = null;
/** /**
* Sets the dependency injection container handler. * Sets the dependency injection container handler.
* *
* @param callable|object $containerHandler Dependency injection container * @param ContainerInterface|(callable(string $classString, array<int, mixed> $params): (null|object)) $containerHandler
* Dependency injection container.
* *
* @return void * @throws InvalidArgumentException If $containerHandler is not a `callable` or instance of `Psr\Container\ContainerInterface`.
*/ */
public function setContainerHandler($containerHandler): void public function setContainerHandler($containerHandler): void
{ {
$this->containerHandler = $containerHandler; $containerInterfaceNS = '\Psr\Container\ContainerInterface';
if (
is_a($containerHandler, $containerInterfaceNS)
|| is_callable($containerHandler)
) {
$this->containerHandler = $containerHandler;
return;
}
throw new InvalidArgumentException(
"\$containerHandler must be of type callable or instance $containerInterfaceNS"
);
} }
public function setEngine(Engine $engine): void public function setEngine(Engine $engine): void
@ -68,11 +83,11 @@ class Dispatcher
/** /**
* Dispatches an event. * Dispatches an event.
* *
* @param string $name Event name * @param string $name Event name.
* @param array<int, mixed> $params Callback parameters. * @param array<int, mixed> $params Callback parameters.
* *
* @return mixed Output of callback * @return mixed Output of callback
* @throws Exception If event name isn't found or if event throws an `Exception` * @throws Exception If event name isn't found or if event throws an `Exception`.
*/ */
public function run(string $name, array $params = []) public function run(string $name, array $params = [])
{ {
@ -110,7 +125,7 @@ class Dispatcher
$requestedMethod = $this->get($eventName); $requestedMethod = $this->get($eventName);
if ($requestedMethod === null) { if ($requestedMethod === null) {
throw new Exception("Event '{$eventName}' isn't found."); throw new Exception("Event '$eventName' isn't found.");
} }
return $this->execute($requestedMethod, $params); return $this->execute($requestedMethod, $params);
@ -138,8 +153,8 @@ class Dispatcher
/** /**
* Assigns a callback to an event. * Assigns a callback to an event.
* *
* @param string $name Event name * @param string $name Event name.
* @param Closure(): (void|mixed) $callback Callback function * @param callable(): (void|mixed) $callback Callback function.
* *
* @return $this * @return $this
*/ */
@ -153,9 +168,9 @@ class Dispatcher
/** /**
* Gets an assigned callback. * Gets an assigned callback.
* *
* @param string $name Event name * @param string $name Event name.
* *
* @return null|(Closure(): (void|mixed)) $callback Callback function * @return null|(callable(): (void|mixed)) $callback Callback function.
*/ */
public function get(string $name): ?callable public function get(string $name): ?callable
{ {
@ -165,9 +180,9 @@ class Dispatcher
/** /**
* Checks if an event has been set. * Checks if an event has been set.
* *
* @param string $name Event name * @param string $name Event name.
* *
* @return bool Event status * @return bool If event exists or doesn't exists.
*/ */
public function has(string $name): bool public function has(string $name): bool
{ {
@ -177,7 +192,7 @@ class Dispatcher
/** /**
* Clears an event. If no name is given, all events will be removed. * Clears an event. If no name is given, all events will be removed.
* *
* @param ?string $name Event name * @param ?string $name Event name.
*/ */
public function clear(?string $name = null): void public function clear(?string $name = null): void
{ {
@ -188,27 +203,38 @@ class Dispatcher
return; return;
} }
$this->events = []; $this->reset();
$this->filters = [];
} }
/** /**
* Hooks a callback to an event. * Hooks a callback to an event.
* *
* @param string $name Event name * @param string $name Event name
* @param 'before'|'after' $type Filter type * @param 'before'|'after' $type Filter type.
* @param Closure(array<int, mixed> &$params, string &$output): (void|false) $callback * @param callable(array<int, mixed> &$params, mixed &$output): (void|false)|callable(mixed &$output): (void|false) $callback
* *
* @return $this * @return $this
*/ */
public function hook(string $name, string $type, callable $callback): self public function hook(string $name, string $type, callable $callback): self
{ {
if (!in_array($type, self::FILTER_TYPES, true)) { static $filterTypes = [self::FILTER_BEFORE, self::FILTER_AFTER];
$noticeMessage = "Invalid filter type '$type', use " . join('|', self::FILTER_TYPES);
if (!in_array($type, $filterTypes, true)) {
$noticeMessage = "Invalid filter type '$type', use " . join('|', $filterTypes);
trigger_error($noticeMessage, E_USER_NOTICE); trigger_error($noticeMessage, E_USER_NOTICE);
} }
if ($type === self::FILTER_AFTER) {
$callbackInfo = new ReflectionFunction($callback);
$parametersNumber = $callbackInfo->getNumberOfParameters();
if ($parametersNumber === 1) {
/** @disregard &$params in after filters are deprecated. */
$callback = fn (array &$params, &$output) => $callback($output);
}
}
$this->filters[$name][$type][] = $callback; $this->filters[$name][$type][] = $callback;
return $this; return $this;
@ -217,10 +243,10 @@ class Dispatcher
/** /**
* Executes a chain of method filters. * Executes a chain of method filters.
* *
* @param array<int, Closure(array<int, mixed> &$params, mixed &$output): (void|false)> $filters * @param array<int, callable(array<int, mixed> &$params, mixed &$output): (void|false)> $filters
* Chain of filters- * Chain of filters.
* @param array<int, mixed> $params Method parameters * @param array<int, mixed> $params Method parameters.
* @param mixed $output Method output * @param mixed $output Method output.
* *
* @throws Exception If an event throws an `Exception` or if `$filters` contains an invalid filter. * @throws Exception If an event throws an `Exception` or if `$filters` contains an invalid filter.
*/ */
@ -242,16 +268,19 @@ class Dispatcher
/** /**
* Executes a callback function. * Executes a callback function.
* *
* @param callable-string|(Closure(): mixed)|array{class-string|object, string} $callback * @param callable-string|(callable(): mixed)|array{class-string|object, string} $callback
* Callback function * Callback function.
* @param array<int, mixed> $params Function parameters * @param array<int, mixed> $params Function parameters.
* *
* @return mixed Function results * @return mixed Function results.
* @throws Exception If `$callback` also throws an `Exception`. * @throws Exception If `$callback` also throws an `Exception`.
*/ */
public function execute($callback, array &$params = []) public function execute($callback, array &$params = [])
{ {
if (is_string($callback) === true && (strpos($callback, '->') !== false || strpos($callback, '::') !== false)) { if (
is_string($callback) === true
&& (strpos($callback, '->') !== false || strpos($callback, '::') !== false)
) {
$callback = $this->parseStringClassAndMethod($callback); $callback = $this->parseStringClassAndMethod($callback);
} }
@ -263,28 +292,26 @@ class Dispatcher
* *
* @param string $classAndMethod Class and method * @param string $classAndMethod Class and method
* *
* @return array{class-string|object, string} Class and method * @return array{0: class-string|object, 1: string} Class and method
*/ */
public function parseStringClassAndMethod(string $classAndMethod): array public function parseStringClassAndMethod(string $classAndMethod): array
{ {
$class_parts = explode('->', $classAndMethod); $classParts = explode('->', $classAndMethod);
if (count($class_parts) === 1) {
$class_parts = explode('::', $class_parts[0]);
}
$class = $class_parts[0]; if (count($classParts) === 1) {
$method = $class_parts[1]; $classParts = explode('::', $classParts[0]);
}
return [ $class, $method ]; return $classParts;
} }
/** /**
* Calls a function. * Calls a function.
* *
* @param callable $func Name of function to call * @param callable $func Name of function to call.
* @param array<int, mixed> &$params Function parameters * @param array<int, mixed> &$params Function parameters.
* *
* @return mixed Function results * @return mixed Function results.
* @deprecated 3.7.0 Use invokeCallable instead * @deprecated 3.7.0 Use invokeCallable instead
*/ */
public function callFunction(callable $func, array &$params = []) public function callFunction(callable $func, array &$params = [])
@ -295,12 +322,12 @@ class Dispatcher
/** /**
* Invokes a method. * Invokes a method.
* *
* @param array{class-string|object, string} $func Class method * @param array{0: class-string|object, 1: string} $func Class method.
* @param array<int, mixed> &$params Class method parameters * @param array<int, mixed> &$params Class method parameters.
* *
* @return mixed Function results * @return mixed Function results.
* @throws TypeError For nonexistent class name. * @throws TypeError For nonexistent class name.
* @deprecated 3.7.0 Use invokeCallable instead * @deprecated 3.7.0 Use invokeCallable instead.
*/ */
public function invokeMethod(array $func, array &$params = []) public function invokeMethod(array $func, array &$params = [])
{ {
@ -310,12 +337,12 @@ class Dispatcher
/** /**
* Invokes a callable (anonymous function or Class->method). * Invokes a callable (anonymous function or Class->method).
* *
* @param array{class-string|object, string}|Callable $func Class method * @param array{0: class-string|object, 1: string}|callable $func Class method.
* @param array<int, mixed> &$params Class method parameters * @param array<int, mixed> &$params Class method parameters.
* *
* @return mixed Function results * @return mixed Function results.
* @throws TypeError For nonexistent class name. * @throws TypeError For nonexistent class name.
* @throws InvalidArgumentException If the constructor requires parameters * @throws InvalidArgumentException If the constructor requires parameters.
* @version 3.7.0 * @version 3.7.0
*/ */
public function invokeCallable($func, array &$params = []) public function invokeCallable($func, array &$params = [])
@ -323,56 +350,46 @@ class Dispatcher
// If this is a directly callable function, call it // If this is a directly callable function, call it
if (is_array($func) === false) { if (is_array($func) === false) {
$this->verifyValidFunction($func); $this->verifyValidFunction($func);
return call_user_func_array($func, $params); return call_user_func_array($func, $params);
} }
[$class, $method] = $func; [$class, $method] = $func;
$resolvedClass = null;
// Only execute the container handler if it's not a Flight class $mustUseTheContainer = $this->containerHandler !== null && (
if ( (is_object($class) === true && strpos(get_class($class), 'flight\\') === false)
$this->containerHandler !== null && || is_string($class)
( );
(
is_object($class) === true && if ($mustUseTheContainer === true) {
strpos(get_class($class), 'flight\\') === false $resolvedClass = $this->resolveContainerClass($class, $params);
) ||
is_string($class) === true if ($resolvedClass) {
)
) {
$containerHandler = $this->containerHandler;
$resolvedClass = $this->resolveContainerClass($containerHandler, $class, $params);
if ($resolvedClass !== null) {
$class = $resolvedClass; $class = $resolvedClass;
} }
} }
$this->verifyValidClassCallable($class, $method, $resolvedClass); $this->verifyValidClassCallable($class, $method, $resolvedClass ?? null);
// Class is a string, and method exists, create the object by hand and inject only the Engine // Class is a string, and method exists, create the object by hand and inject only the Engine
if (is_string($class) === true) { if (is_string($class)) {
$class = new $class($this->engine); $class = new $class($this->engine);
} }
return call_user_func_array([ $class, $method ], $params); return call_user_func_array([$class, $method], $params);
} }
/** /**
* Handles invalid callback types. * Handles invalid callback types.
* *
* @param callable-string|(Closure(): mixed)|array{class-string|object, string} $callback * @param callable-string|(callable(): mixed)|array{0: class-string|object, 1: string} $callback
* Callback function * Callback function.
* *
* @throws InvalidArgumentException If `$callback` is an invalid type * @throws InvalidArgumentException If `$callback` is an invalid type.
*/ */
protected function verifyValidFunction($callback): void protected function verifyValidFunction($callback): void
{ {
$isInvalidFunctionName = ( if (is_string($callback) && !function_exists($callback)) {
is_string($callback)
&& !function_exists($callback)
);
if ($isInvalidFunctionName) {
throw new InvalidArgumentException('Invalid callback specified.'); throw new InvalidArgumentException('Invalid callback specified.');
} }
} }
@ -381,84 +398,77 @@ class Dispatcher
/** /**
* Verifies if the provided class and method are valid callable. * Verifies if the provided class and method are valid callable.
* *
* @param string|object $class The class name. * @param class-string|object $class The class name.
* @param string $method The method name. * @param string $method The method name.
* @param object|null $resolvedClass The resolved class. * @param object|null $resolvedClass The resolved class.
* *
* @throws Exception If the class or method is not found. * @throws Exception If the class or method is not found.
*
* @return void
*/ */
protected function verifyValidClassCallable($class, $method, $resolvedClass): void protected function verifyValidClassCallable($class, $method, $resolvedClass): void
{ {
$final_exception = null; $exception = null;
// Final check to make sure it's actually a class and a method, or throw an error // Final check to make sure it's actually a class and a method, or throw an error
if (is_object($class) === false && class_exists($class) === false) { if (is_object($class) === false && class_exists($class) === false) {
$final_exception = new Exception("Class '$class' not found. Is it being correctly autoloaded with Flight::path()?"); $exception = new Exception("Class '$class' not found. Is it being correctly autoloaded with Flight::path()?");
// If this tried to resolve a class in a container and failed somehow, throw the exception // If this tried to resolve a class in a container and failed somehow, throw the exception
} elseif (isset($resolvedClass) === false && $this->containerException !== null) { } elseif (!$resolvedClass && $this->containerException !== null) {
$final_exception = $this->containerException; $exception = $this->containerException;
// Class is there, but no method // Class is there, but no method
} elseif (is_object($class) === true && method_exists($class, $method) === false) { } elseif (is_object($class) === true && method_exists($class, $method) === false) {
$final_exception = new Exception("Class found, but method '" . get_class($class) . "::$method' not found."); $classNamespace = get_class($class);
$exception = new Exception("Class found, but method '$classNamespace::$method' not found.");
} }
if ($final_exception !== null) { if ($exception !== null) {
$this->fixOutputBuffering(); $this->fixOutputBuffering();
throw $final_exception;
throw $exception;
} }
} }
/** /**
* Resolves the container class. * Resolves the container class.
* *
* @param callable|object $container_handler Dependency injection container * @param class-string $class Class name.
* @param class-string $class Class name * @param array<int, mixed> &$params Class constructor parameters.
* @param array<int, mixed> &$params Class constructor parameters
* *
* @return object Class object * @return ?object Class object.
*/ */
protected function resolveContainerClass($container_handler, $class, array &$params) protected function resolveContainerClass(string $class, array &$params)
{ {
$class_object = null;
// PSR-11 // PSR-11
if ( if (
is_object($container_handler) === true && is_a($this->containerHandler, '\Psr\Container\ContainerInterface')
method_exists($container_handler, 'has') === true && && $this->containerHandler->has($class)
$container_handler->has($class)
) { ) {
$class_object = call_user_func([$container_handler, 'get'], $class); return $this->containerHandler->get($class);
}
// Just a callable where you configure the behavior (Dice, PHP-DI, etc.) // Just a callable where you configure the behavior (Dice, PHP-DI, etc.)
} elseif (is_callable($container_handler) === true) { if (is_callable($this->containerHandler)) {
// This is to catch all the error that could be thrown by whatever container you are using /* This is to catch all the error that could be thrown by whatever
container you are using */
try { try {
$class_object = call_user_func($container_handler, $class, $params); return ($this->containerHandler)($class, $params);
} catch (Exception $e) {
// could not resolve a class for some reason
$class_object = null;
// could not resolve a class for some reason
} catch (Exception $exception) {
// If the container throws an exception, we need to catch it // If the container throws an exception, we need to catch it
// and store it somewhere. If we just let it throw itself, it // and store it somewhere. If we just let it throw itself, it
// doesn't properly close the output buffers and can cause other // doesn't properly close the output buffers and can cause other
// issues. // issues.
// This is thrown in the verifyValidClassCallable method // This is thrown in the verifyValidClassCallable method.
$this->containerException = $e; $this->containerException = $exception;
} }
} }
return $class_object; return null;
} }
/** /** Because this could throw an exception in the middle of an output buffer, */
* Because this could throw an exception in the middle of an output buffer,
*
* @return void
*/
protected function fixOutputBuffering(): void protected function fixOutputBuffering(): void
{ {
// Cause PHPUnit has 1 level of output buffering by default // Cause PHPUnit has 1 level of output buffering by default

@ -139,7 +139,7 @@ class Response
*/ */
public function status(?int $code = null) public function status(?int $code = null)
{ {
if (null === $code) { if ($code === null) {
return $this->status; return $this->status;
} }
@ -279,19 +279,22 @@ class Response
*/ */
public function cache($expires): self public function cache($expires): self
{ {
if (false === $expires) { if ($expires === false) {
$this->headers['Expires'] = 'Mon, 26 Jul 1997 05:00:00 GMT'; $this->headers['Expires'] = 'Mon, 26 Jul 1997 05:00:00 GMT';
$this->headers['Cache-Control'] = [ $this->headers['Cache-Control'] = [
'no-store, no-cache, must-revalidate', 'no-store, no-cache, must-revalidate',
'post-check=0, pre-check=0', 'post-check=0, pre-check=0',
'max-age=0', 'max-age=0',
]; ];
$this->headers['Pragma'] = 'no-cache'; $this->headers['Pragma'] = 'no-cache';
} else { } else {
$expires = \is_int($expires) ? $expires : strtotime($expires); $expires = \is_int($expires) ? $expires : strtotime($expires);
$this->headers['Expires'] = gmdate('D, d M Y H:i:s', $expires) . ' GMT'; $this->headers['Expires'] = gmdate('D, d M Y H:i:s', $expires) . ' GMT';
$this->headers['Cache-Control'] = 'max-age=' . ($expires - time()); $this->headers['Cache-Control'] = 'max-age=' . ($expires - time());
if (isset($this->headers['Pragma']) && 'no-cache' == $this->headers['Pragma']) {
if (isset($this->headers['Pragma']) && $this->headers['Pragma'] === 'no-cache') {
unset($this->headers['Pragma']); unset($this->headers['Pragma']);
} }
} }
@ -307,7 +310,7 @@ class Response
public function sendHeaders(): self public function sendHeaders(): self
{ {
// Send status code header // Send status code header
if (false !== strpos(\PHP_SAPI, 'cgi')) { if (strpos(\PHP_SAPI, 'cgi') !== false) {
// @codeCoverageIgnoreStart // @codeCoverageIgnoreStart
$this->setRealHeader( $this->setRealHeader(
sprintf( sprintf(

@ -105,7 +105,7 @@ class Route
public function matchUrl(string $url, bool $case_sensitive = false): bool public function matchUrl(string $url, bool $case_sensitive = false): bool
{ {
// Wildcard or exact match // Wildcard or exact match
if ('*' === $this->pattern || $this->pattern === $url) { if ($this->pattern === '*' || $this->pattern === $url) {
return true; return true;
} }
@ -120,8 +120,9 @@ class Route
for ($i = 0; $i < $len; $i++) { for ($i = 0; $i < $len; $i++) {
if ($url[$i] === '/') { if ($url[$i] === '/') {
$n++; ++$n;
} }
if ($n === $count) { if ($n === $count) {
break; break;
} }
@ -153,24 +154,20 @@ class Route
$regex $regex
); );
if ('/' === $last_char) { // Fix trailing slash $regex .= $last_char === '/' ? '?' : '/?';
$regex .= '?';
} else { // Allow trailing slash
$regex .= '/?';
}
// Attempt to match route and named parameters // Attempt to match route and named parameters
if (preg_match('#^' . $regex . '(?:\?[\s\S]*)?$#' . (($case_sensitive) ? '' : 'i'), $url, $matches)) { if (!preg_match('#^' . $regex . '(?:\?[\s\S]*)?$#' . (($case_sensitive) ? '' : 'i'), $url, $matches)) {
foreach ($ids as $k => $v) { return false;
$this->params[$k] = (\array_key_exists($k, $matches)) ? urldecode($matches[$k]) : null; }
}
$this->regex = $regex;
return true; foreach (array_keys($ids) as $k) {
$this->params[$k] = (\array_key_exists($k, $matches)) ? urldecode($matches[$k]) : null;
} }
return false; $this->regex = $regex;
return true;
} }
/** /**

@ -88,7 +88,7 @@ class View
*/ */
public function clear(?string $key = null): self public function clear(?string $key = null): self
{ {
if (null === $key) { if ($key === null) {
$this->vars = []; $this->vars = [];
} else { } else {
unset($this->vars[$key]); unset($this->vars[$key]);
@ -169,7 +169,7 @@ class View
$is_windows = \strtoupper(\substr(PHP_OS, 0, 3)) === 'WIN'; $is_windows = \strtoupper(\substr(PHP_OS, 0, 3)) === 'WIN';
if (('/' == \substr($file, 0, 1)) || ($is_windows === true && ':' == \substr($file, 1, 1))) { if ((\substr($file, 0, 1) === '/') || ($is_windows && \substr($file, 1, 1) === ':')) {
return $file; return $file;
} }

@ -95,7 +95,7 @@ class Collection implements ArrayAccess, Iterator, Countable, JsonSerializable
#[\ReturnTypeWillChange] #[\ReturnTypeWillChange]
public function offsetSet($offset, $value): void public function offsetSet($offset, $value): void
{ {
if (null === $offset) { if ($offset === null) {
$this->data[] = $value; $this->data[] = $value;
} else { } else {
$this->data[$offset] = $value; $this->data[$offset] = $value;
@ -166,9 +166,7 @@ class Collection implements ArrayAccess, Iterator, Countable, JsonSerializable
*/ */
public function valid(): bool public function valid(): bool
{ {
$key = key($this->data); return key($this->data) !== null;
return null !== $key;
} }
/** /**

@ -0,0 +1,8 @@
<?php
declare(strict_types=1);
// This file is only here so that the PHP8 attribute for doesn't throw an error in files
class ReturnTypeWillChange
{
}

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

@ -9,35 +9,42 @@
</description> </description>
<arg name="colors" /> <arg name="colors" />
<arg name="tab-width" value="4" /> <arg name="tab-width" value="4" />
<rule ref="PSR12" />
<rule ref="PSR1"> <rule ref="PSR1">
<exclude name="PSR1.Classes.ClassDeclaration.MissingNamespace" /> <exclude name="PSR1.Classes.ClassDeclaration.MissingNamespace" />
</rule> </rule>
<rule ref="PSR12" />
<rule ref="Generic"> <rule ref="Generic">
<exclude name="Generic.Files.LineEndings.InvalidEOLChar" />
<exclude name="Generic.PHP.ClosingPHPTag.NotFound" /> <exclude name="Generic.PHP.ClosingPHPTag.NotFound" />
<exclude name="Generic.PHP.UpperCaseConstant.Found" /> <exclude name="Generic.PHP.UpperCaseConstant.Found" />
<exclude name="Generic.Arrays.DisallowShortArraySyntax.Found" /> <exclude name="Generic.Arrays.DisallowShortArraySyntax.Found" />
<exclude name="Generic.Files.EndFileNoNewline.Found" /> <exclude name="Generic.Files.EndFileNoNewline.Found" />
<exclude name="Generic.Files.LowercasedFilename.NotFound" />
<exclude name="Generic.Commenting.DocComment.TagValueIndent" /> <exclude name="Generic.Commenting.DocComment.TagValueIndent" />
<exclude name="Generic.Classes.OpeningBraceSameLine.BraceOnNewLine" /> <exclude name="Generic.Classes.OpeningBraceSameLine.BraceOnNewLine" />
<exclude name="Generic.WhiteSpace.DisallowSpaceIndent.SpacesUsed" /> <exclude name="Generic.WhiteSpace.DisallowSpaceIndent.SpacesUsed" />
<exclude name="Generic.Files.LowercasedFilename.NotFound" /> <exclude name="Generic.Commenting.DocComment.ContentAfterOpen" />
<exclude name="Generic.Commenting.DocComment.ContentBeforeClose" />
<exclude name="Generic.Commenting.DocComment.MissingShort" />
<exclude name="Generic.Commenting.DocComment.SpacingBeforeShort" />
<exclude name="Generic.Functions.OpeningFunctionBraceKernighanRitchie.BraceOnNewLine" /> <exclude name="Generic.Functions.OpeningFunctionBraceKernighanRitchie.BraceOnNewLine" />
<exclude name="Generic.Formatting.MultipleStatementAlignment.NotSameWarning" /> <exclude name="Generic.Formatting.MultipleStatementAlignment.NotSameWarning" />
<exclude name="Generic.Formatting.SpaceAfterNot.Incorrect" />
<exclude name="Generic.Commenting.DocComment.SpacingBeforeShort" />
<exclude name="Generic.Commenting.DocComment.ContentAfterOpen" />
<exclude name="Generic.Functions.OpeningFunctionBraceBsdAllman.BraceOnSameLine" /> <exclude name="Generic.Functions.OpeningFunctionBraceBsdAllman.BraceOnSameLine" />
<exclude name="Generic.PHP.DisallowRequestSuperglobal.Found" /> <exclude name="Generic.PHP.DisallowRequestSuperglobal.Found" />
<exclude name="Generic.Commenting.DocComment.ContentBeforeClose" /> </rule>
<exclude name="Generic.ControlStructures.DisallowYodaConditions.Found" /> <rule ref="Generic.Files.LineLength">
<exclude name="Generic.Strings.UnnecessaryStringConcat.Found" /> <properties>
<exclude name="Generic.CodeAnalysis.AssignmentInCondition.FoundInWhileCondition" /> <property name="ignoreComments" value="true" />
<exclude name="Generic.Commenting.DocComment.MissingShort" /> </properties>
<exclude name="Generic.Commenting.DocComment.SpacingBeforeTags" /> </rule>
<exclude name="Generic.WhiteSpace.ArbitraryParenthesesSpacing.SpaceAfterOpen" /> <rule ref="Generic.Formatting.SpaceAfterNot">
<exclude name="Generic.WhiteSpace.ArbitraryParenthesesSpacing.SpaceBeforeClose" /> <properties>
<property name="spacing" value="0" />
</properties>
</rule>
<rule ref="Generic.WhiteSpace.ArbitraryParenthesesSpacing">
<properties>
<property name="ignoreNewlines" value="true" />
</properties>
</rule> </rule>
<file>flight/</file> <file>flight/</file>
<file>tests/</file> <file>tests/</file>

@ -1,6 +0,0 @@
parameters:
ignoreErrors:
-
message: "#^Parameter \\#2 \\$callback of method flight\\\\core\\\\Dispatcher\\:\\:set\\(\\) expects Closure\\(\\)\\: mixed, array\\{\\$this\\(flight\\\\Engine\\), literal\\-string&non\\-falsy\\-string\\} given\\.$#"
count: 1
path: flight/Engine.php

@ -37,9 +37,7 @@ class DispatcherTest extends TestCase
public function testFunctionMapping(): void public function testFunctionMapping(): void
{ {
$this->dispatcher->set('map2', function (): string { $this->dispatcher->set('map2', fn (): string => 'hello');
return 'hello';
});
$this->assertSame('hello', $this->dispatcher->run('map2')); $this->assertSame('hello', $this->dispatcher->run('map2'));
} }
@ -61,6 +59,9 @@ class DispatcherTest extends TestCase
->set('map-event', $customFunction) ->set('map-event', $customFunction)
->set('map-event-2', $anotherFunction); ->set('map-event-2', $anotherFunction);
$this->assertTrue($this->dispatcher->has('map-event'));
$this->assertTrue($this->dispatcher->has('map-event-2'));
$this->dispatcher->clear(); $this->dispatcher->clear();
$this->assertFalse($this->dispatcher->has('map-event')); $this->assertFalse($this->dispatcher->has('map-event'));
@ -76,6 +77,9 @@ class DispatcherTest extends TestCase
->set('map-event', $customFunction) ->set('map-event', $customFunction)
->set('map-event-2', $anotherFunction); ->set('map-event-2', $anotherFunction);
$this->assertTrue($this->dispatcher->has('map-event'));
$this->assertTrue($this->dispatcher->has('map-event-2'));
$this->dispatcher->clear('map-event'); $this->dispatcher->clear('map-event');
$this->assertFalse($this->dispatcher->has('map-event')); $this->assertFalse($this->dispatcher->has('map-event'));
@ -105,9 +109,7 @@ class DispatcherTest extends TestCase
public function testBeforeAndAfter(): void public function testBeforeAndAfter(): void
{ {
$this->dispatcher->set('hello', function (string $name): string { $this->dispatcher->set('hello', fn (string $name): string => "Hello, $name!");
return "Hello, $name!";
});
$this->dispatcher $this->dispatcher
->hook('hello', Dispatcher::FILTER_BEFORE, function (array &$params): void { ->hook('hello', Dispatcher::FILTER_BEFORE, function (array &$params): void {
@ -124,6 +126,25 @@ class DispatcherTest extends TestCase
$this->assertSame('Hello, Fred! Have a nice day!', $result); $this->assertSame('Hello, Fred! Have a nice day!', $result);
} }
public function testBeforeAndAfterWithShortAfterFilterSyntax(): void
{
$this->dispatcher->set('hello', fn (string $name): string => "Hello, $name!");
$this->dispatcher
->hook('hello', Dispatcher::FILTER_BEFORE, function (array &$params): void {
// Manipulate the parameter
$params[0] = 'Fred';
})
->hook('hello', Dispatcher::FILTER_AFTER, function (string &$output): void {
// Manipulate the output
$output .= ' Have a nice day!';
});
$result = $this->dispatcher->run('hello', ['Bob']);
$this->assertSame('Hello, Fred! Have a nice day!', $result);
}
public function testInvalidCallback(): void public function testInvalidCallback(): void
{ {
$this->expectException(Exception::class); $this->expectException(Exception::class);
@ -245,7 +266,7 @@ class DispatcherTest extends TestCase
public function testInvokeMethod(): void public function testInvokeMethod(): void
{ {
$class = new TesterClass('param1', 'param2', 'param3', 'param4', 'param5', 'param6'); $class = new TesterClass('param1', 'param2', 'param3', 'param4', 'param5', 'param6');
$result = $this->dispatcher->invokeMethod([ $class, 'instanceMethod' ]); $result = $this->dispatcher->invokeMethod([$class, 'instanceMethod']);
$this->assertSame('param1', $class->param2); $this->assertSame('param1', $class->param2);
} }
@ -271,7 +292,7 @@ class DispatcherTest extends TestCase
public function testExecuteStringClassNoConstructArraySyntax(): void public function testExecuteStringClassNoConstructArraySyntax(): void
{ {
$result = $this->dispatcher->execute([ Hello::class, 'sayHi' ]); $result = $this->dispatcher->execute([Hello::class, 'sayHi']);
$this->assertSame('hello', $result); $this->assertSame('hello', $result);
} }
@ -298,7 +319,7 @@ class DispatcherTest extends TestCase
$engine = new Engine(); $engine = new Engine();
$engine->set('test_me_out', 'You got it boss!'); $engine->set('test_me_out', 'You got it boss!');
$this->dispatcher->setEngine($engine); $this->dispatcher->setEngine($engine);
$result = $this->dispatcher->execute([ ContainerDefault::class, 'testTheContainer' ]); $result = $this->dispatcher->execute([ContainerDefault::class, 'testTheContainer']);
$this->assertSame('You got it boss!', $result); $this->assertSame('You got it boss!', $result);
} }
@ -306,6 +327,6 @@ class DispatcherTest extends TestCase
{ {
$this->expectException(TypeError::class); $this->expectException(TypeError::class);
$this->expectExceptionMessageMatches('#tests\\\\classes\\\\ContainerDefault::__construct\(\).+flight\\\\Engine, null given#'); $this->expectExceptionMessageMatches('#tests\\\\classes\\\\ContainerDefault::__construct\(\).+flight\\\\Engine, null given#');
$result = $this->dispatcher->execute([ ContainerDefault::class, 'testTheContainer' ]); $result = $this->dispatcher->execute([ContainerDefault::class, 'testTheContainer']);
} }
} }

@ -21,7 +21,7 @@ class RedirectTest extends TestCase
public function getBaseUrl($base, $url) public function getBaseUrl($base, $url)
{ {
if ('/' !== $base && false === strpos($url, '://')) { if ($base !== '/' && strpos($url, '://') === false) {
$url = preg_replace('#/+#', '/', $base . '/' . $url); $url = preg_replace('#/+#', '/', $base . '/' . $url);
} }
@ -67,11 +67,7 @@ class RedirectTest extends TestCase
public function testBaseOverride() public function testBaseOverride()
{ {
$url = 'login'; $url = 'login';
if (null !== $this->app->get('flight.base_url')) { $base = $this->app->get('flight.base_url') ?? $this->app->request()->base;
$base = $this->app->get('flight.base_url');
} else {
$base = $this->app->request()->base;
}
self::assertEquals('/testdir/login', $this->getBaseUrl($base, $url)); self::assertEquals('/testdir/login', $this->getBaseUrl($base, $url));
} }

@ -336,12 +336,14 @@ class RouterTest extends TestCase
{ {
$this->router->map('GET /api/intune/hey', [$this, 'ok']); $this->router->map('GET /api/intune/hey', [$this, 'ok']);
$error_description = 'error_description=AADSTS65004%3a+User+declined+to+consent+to+access+the';
$error_description .= '+app.%0d%0aTrace+ID%3a+747c0cc1-ccbd-4e53-8e2f-48812eb24100%0d%0a';
$error_description .= 'Correlation+ID%3a+362e3cb3-20ef-400b-904e-9983bd989184%0d%0a';
$error_description .= 'Timestamp%3a+2022-09-08+09%3a58%3a12Z';
$query_params = [ $query_params = [
'error=access_denied', 'error=access_denied',
'error_description=AADSTS65004%3a+User+declined+to+consent+to+access+the' $error_description,
. '+app.%0d%0aTrace+ID%3a+747c0cc1-ccbd-4e53-8e2f-48812eb24100%0d%0a'
. 'Correlation+ID%3a+362e3cb3-20ef-400b-904e-9983bd989184%0d%0a'
. 'Timestamp%3a+2022-09-08+09%3a58%3a12Z',
'error_uri=https%3a%2f%2flogin.microsoftonline.com%2ferror%3fcode%3d65004', 'error_uri=https%3a%2f%2flogin.microsoftonline.com%2ferror%3fcode%3d65004',
'admin_consent=True', 'admin_consent=True',
'state=x2EUE0fcSj#' 'state=x2EUE0fcSj#'

Loading…
Cancel
Save