diff --git a/composer.json b/composer.json index 028f2da..2be3e4b 100644 --- a/composer.json +++ b/composer.json @@ -41,9 +41,11 @@ }, "require-dev": { "ext-pdo_sqlite": "*", - "phpunit/phpunit": "^9.5", - "phpstan/phpstan": "^1.10", + "league/container": "^4.2", + "level-2/dice": "^4.0", "phpstan/extension-installer": "^1.3", + "phpstan/phpstan": "^1.10", + "phpunit/phpunit": "^9.5", "rregeer/phpunit-coverage-check": "^0.3.1", "squizlabs/php_codesniffer": "^3.8" }, diff --git a/flight/Engine.php b/flight/Engine.php index effb0af..62342b6 100644 --- a/flight/Engine.php +++ b/flight/Engine.php @@ -30,17 +30,17 @@ use flight\net\Route; * @method void halt(int $code = 200, string $message = '', bool $actuallyExit = true) Stops processing and returns a given response. * * # Routing - * @method Route route(string $pattern, callable $callback, bool $pass_route = false, string $alias = '') + * @method Route route(string $pattern, callable|string $callback, bool $pass_route = false, string $alias = '') * Routes a URL to a callback function with all applicable methods * @method void group(string $pattern, callable $callback, array $group_middlewares = []) * Groups a set of routes together under a common prefix. - * @method Route post(string $pattern, callable $callback, bool $pass_route = false, string $alias = '') + * @method Route post(string $pattern, callable|string $callback, bool $pass_route = false, string $alias = '') * Routes a POST URL to a callback function. - * @method Route put(string $pattern, callable $callback, bool $pass_route = false, string $alias = '') + * @method Route put(string $pattern, callable|string $callback, bool $pass_route = false, string $alias = '') * Routes a PUT URL to a callback function. - * @method Route patch(string $pattern, callable $callback, bool $pass_route = false, string $alias = '') + * @method Route patch(string $pattern, callable|string $callback, bool $pass_route = false, string $alias = '') * Routes a PATCH URL to a callback function. - * @method Route delete(string $pattern, callable $callback, bool $pass_route = false, string $alias = '') + * @method Route delete(string $pattern, callable|string $callback, bool $pass_route = false, string $alias = '') * Routes a DELETE URL to a callback function. * @method Router router() Gets router * @method string getUrl(string $alias) Gets a url from an alias @@ -138,6 +138,9 @@ class Engine $this->dispatcher->reset(); } + // Add this class to Dispatcher + $this->dispatcher->setEngine($this); + // Register default components $this->loader->register('request', Request::class); $this->loader->register('response', Response::class); @@ -217,6 +220,18 @@ class Engine $this->error($e); } + /** + * Registers the container handler + * + * @param callable|object $containerHandler Callback function or PSR-11 Container object that sets the container and how it will inject classes + * + * @return void + */ + public function registerContainerHandler($containerHandler): void + { + $this->dispatcher->setContainerHandler($containerHandler); + } + /** * Maps a callback to a framework method. * @@ -605,11 +620,11 @@ class Engine * Routes a URL to a callback function. * * @param string $pattern URL pattern to match - * @param callable $callback Callback function + * @param callable|string $callback Callback function * @param bool $pass_route Pass the matching route object to the callback * @param string $alias The alias for the route */ - public function _route(string $pattern, callable $callback, bool $pass_route = false, string $alias = ''): Route + public function _route(string $pattern, $callback, bool $pass_route = false, string $alias = ''): Route { return $this->router()->map($pattern, $callback, $pass_route, $alias); } @@ -630,10 +645,10 @@ class Engine * Routes a URL to a callback function. * * @param string $pattern URL pattern to match - * @param callable $callback Callback function + * @param callable|string $callback Callback function or string class->method * @param bool $pass_route Pass the matching route object to the callback */ - public function _post(string $pattern, callable $callback, bool $pass_route = false, string $route_alias = ''): void + public function _post(string $pattern, $callback, bool $pass_route = false, string $route_alias = ''): void { $this->router()->map('POST ' . $pattern, $callback, $pass_route, $route_alias); } @@ -642,10 +657,10 @@ class Engine * Routes a URL to a callback function. * * @param string $pattern URL pattern to match - * @param callable $callback Callback function + * @param callable|string $callback Callback function or string class->method * @param bool $pass_route Pass the matching route object to the callback */ - public function _put(string $pattern, callable $callback, bool $pass_route = false, string $route_alias = ''): void + public function _put(string $pattern, $callback, bool $pass_route = false, string $route_alias = ''): void { $this->router()->map('PUT ' . $pattern, $callback, $pass_route, $route_alias); } @@ -654,10 +669,10 @@ class Engine * Routes a URL to a callback function. * * @param string $pattern URL pattern to match - * @param callable $callback Callback function + * @param callable|string $callback Callback function or string class->method * @param bool $pass_route Pass the matching route object to the callback */ - public function _patch(string $pattern, callable $callback, bool $pass_route = false, string $route_alias = ''): void + public function _patch(string $pattern, $callback, bool $pass_route = false, string $route_alias = ''): void { $this->router()->map('PATCH ' . $pattern, $callback, $pass_route, $route_alias); } @@ -666,10 +681,10 @@ class Engine * Routes a URL to a callback function. * * @param string $pattern URL pattern to match - * @param callable $callback Callback function + * @param callable|string $callback Callback function or string class->method * @param bool $pass_route Pass the matching route object to the callback */ - public function _delete(string $pattern, callable $callback, bool $pass_route = false, string $route_alias = ''): void + public function _delete(string $pattern, $callback, bool $pass_route = false, string $route_alias = ''): void { $this->router()->map('DELETE ' . $pattern, $callback, $pass_route, $route_alias); } diff --git a/flight/Flight.php b/flight/Flight.php index 6e29781..0a42489 100644 --- a/flight/Flight.php +++ b/flight/Flight.php @@ -19,24 +19,25 @@ require_once __DIR__ . '/autoload.php'; * @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(?int $code = null) Stops the framework and sends a response. - * @method static void halt(int $code = 200, string $message = '', bool $actuallyExit = true) + * @method static void start() Starts the framework. + * @method static void path(string $path) Adds a path for autoloading classes. + * @method static void stop(?int $code = null) Stops the framework and sends a response. + * @method static void halt(int $code = 200, string $message = '', bool $actuallyExit = true) * Stop the framework with an optional status code and message. + * @method static void registerContainerHandler(callable|object $containerHandler) Registers a container handler. * * # Routing - * @method static Route route(string $pattern, callable $callback, bool $pass_route = false, string $alias = '') + * @method static Route route(string $pattern, callable|string $callback, bool $pass_route = false, string $alias = '') * Maps a URL pattern to a callback with all applicable methods. * @method static void group(string $pattern, callable $callback, callable[] $group_middlewares = []) * Groups a set of routes together under a common prefix. - * @method static Route post(string $pattern, callable $callback, bool $pass_route = false, string $alias = '') + * @method static Route post(string $pattern, callable|string $callback, bool $pass_route = false, string $alias = '') * Routes a POST URL to a callback function. - * @method static Route put(string $pattern, callable $callback, bool $pass_route = false, string $alias = '') + * @method static Route put(string $pattern, callable|string $callback, bool $pass_route = false, string $alias = '') * Routes a PUT URL to a callback function. - * @method static Route patch(string $pattern, callable $callback, bool $pass_route = false, string $alias = '') + * @method static Route patch(string $pattern, callable|string $callback, bool $pass_route = false, string $alias = '') * Routes a PATCH URL to a callback function. - * @method static Route delete(string $pattern, callable $callback, bool $pass_route = false, string $alias = '') + * @method static Route delete(string $pattern, callable|string $callback, bool $pass_route = false, string $alias = '') * Routes a DELETE URL to a callback function. * @method static Router router() Returns Router instance. * @method static string getUrl(string $alias, array $params = []) Gets a url from an alias @@ -101,34 +102,6 @@ class Flight { } - /** - * Registers a class to a framework method. - * - * # Usage example: - * ``` - * Flight::register('user', User::class); - * - * Flight::user(); # <- Return a User instance - * ``` - * - * @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): void - { - static::__callStatic('register', [$name, $class, $params, $callback]); - } - - /** Unregisters a class. */ - public static function unregister(string $methodName): void - { - static::__callStatic('unregister', [$methodName]); - } - /** * Handles calls to static methods. * @@ -140,7 +113,7 @@ class Flight */ public static function __callStatic(string $name, array $params) { - return Dispatcher::invokeMethod([self::app(), $name], $params); + return self::app()->{$name}(...$params); } /** @return Engine Application instance */ diff --git a/flight/core/Dispatcher.php b/flight/core/Dispatcher.php index 6af0d66..6a167a8 100644 --- a/flight/core/Dispatcher.php +++ b/flight/core/Dispatcher.php @@ -6,8 +6,8 @@ namespace flight\core; use Closure; use Exception; +use flight\Engine; use InvalidArgumentException; -use ReflectionClass; use TypeError; /** @@ -25,6 +25,12 @@ class Dispatcher 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 */ + protected $containerException = null; + + /** @var ?Engine $engine Engine instance */ + protected ?Engine $engine = null; + /** @var array Mapped events. */ protected array $events = []; @@ -35,6 +41,30 @@ class Dispatcher */ protected array $filters = []; + /** + * This is a container for the dependency injection. + * + * @var callable|object|null + */ + protected $containerHandler = null; + + /** + * Sets the dependency injection container handler. + * + * @param callable|object $containerHandler Dependency injection container + * + * @return void + */ + public function setContainerHandler($containerHandler): void + { + $this->containerHandler = $containerHandler; + } + + public function setEngine(Engine $engine): void + { + $this->engine = $engine; + } + /** * Dispatches an event. * @@ -80,10 +110,10 @@ class Dispatcher $requestedMethod = $this->get($eventName); if ($requestedMethod === null) { - throw new Exception("Event '$eventName' isn't found."); + throw new Exception("Event '{$eventName}' isn't found."); } - return $requestedMethod(...$params); + return $this->execute($requestedMethod, $params); } /** @@ -194,7 +224,7 @@ class Dispatcher * * @throws Exception If an event throws an `Exception` or if `$filters` contains an invalid filter. */ - public static function filter(array $filters, array &$params, &$output): void + public function filter(array $filters, array &$params, &$output): void { foreach ($filters as $key => $callback) { if (!is_callable($callback)) { @@ -219,22 +249,33 @@ class Dispatcher * @return mixed Function results * @throws Exception If `$callback` also throws an `Exception`. */ - public static function execute($callback, array &$params = []) + public function execute($callback, array &$params = []) { - $isInvalidFunctionName = ( - is_string($callback) - && !function_exists($callback) - ); - - if ($isInvalidFunctionName) { - throw new InvalidArgumentException('Invalid callback specified.'); + if (is_string($callback) === true && (strpos($callback, '->') !== false || strpos($callback, '::') !== false)) { + $callback = $this->parseStringClassAndMethod($callback); } - if (is_array($callback)) { - return self::invokeMethod($callback, $params); + return $this->invokeCallable($callback, $params); + } + + /** + * Parses a string into a class and method. + * + * @param string $classAndMethod Class and method + * + * @return array{class-string|object, string} Class and method + */ + public function parseStringClassAndMethod(string $classAndMethod): array + { + $class_parts = explode('->', $classAndMethod); + if (count($class_parts) === 1) { + $class_parts = explode('::', $class_parts[0]); } - return self::callFunction($callback, $params); + $class = $class_parts[0]; + $method = $class_parts[1]; + + return [ $class, $method ]; } /** @@ -244,10 +285,11 @@ class Dispatcher * @param array &$params Function parameters * * @return mixed Function results + * @deprecated 3.7.0 Use invokeCallable instead */ - public static function callFunction(callable $func, array &$params = []) + public function callFunction(callable $func, array &$params = []) { - return call_user_func_array($func, $params); + return $this->invokeCallable($func, $params); } /** @@ -257,34 +299,172 @@ class Dispatcher * @param array &$params Class method parameters * * @return mixed Function results - * @throws TypeError For unexistent class name. + * @throws TypeError For nonexistent class name. + * @deprecated 3.7.0 Use invokeCallable instead */ - public static function invokeMethod(array $func, array &$params = []) + public function invokeMethod(array $func, array &$params = []) { - [$class, $method] = $func; + return $this->invokeCallable($func, $params); + } - if (is_string($class) && class_exists($class)) { - $constructor = (new ReflectionClass($class))->getConstructor(); - $constructorParamsNumber = 0; + /** + * Invokes a callable (anonymous function or Class->method). + * + * @param array{class-string|object, string}|Callable $func Class method + * @param array &$params Class method parameters + * + * @return mixed Function results + * @throws TypeError For nonexistent class name. + * @throws InvalidArgumentException If the constructor requires parameters + * @version 3.7.0 + */ + public function invokeCallable($func, array &$params = []) + { + // If this is a directly callable function, call it + if (is_array($func) === false) { + $this->verifyValidFunction($func); + return call_user_func_array($func, $params); + } - if ($constructor !== null) { - $constructorParamsNumber = count($constructor->getParameters()); + [$class, $method] = $func; + $resolvedClass = null; + + // Only execute the container handler if it's not a Flight class + if ( + $this->containerHandler !== null && + ( + ( + is_object($class) === true && + strpos(get_class($class), 'flight\\') === false + ) || + is_string($class) === true + ) + ) { + $containerHandler = $this->containerHandler; + $resolvedClass = $this->resolveContainerClass($containerHandler, $class, $params); + if ($resolvedClass !== null) { + $class = $resolvedClass; } + } - if ($constructorParamsNumber > 0) { - $exceptionMessage = "Method '$class::$method' cannot be called statically. "; - $exceptionMessage .= sprintf( - "$class::__construct require $constructorParamsNumber parameter%s", - $constructorParamsNumber > 1 ? 's' : '' - ); + $this->verifyValidClassCallable($class, $method, $resolvedClass); - throw new InvalidArgumentException($exceptionMessage, E_ERROR); - } + // Class is a string, and method exists, create the object by hand and inject only the Engine + if (is_string($class) === true) { + $class = new $class($this->engine); + } + + return call_user_func_array([ $class, $method ], $params); + } + + /** + * Handles invalid callback types. + * + * @param callable-string|(Closure(): mixed)|array{class-string|object, string} $callback + * Callback function + * + * @throws InvalidArgumentException If `$callback` is an invalid type + */ + protected function verifyValidFunction($callback): void + { + $isInvalidFunctionName = ( + is_string($callback) + && !function_exists($callback) + ); + + if ($isInvalidFunctionName) { + throw new InvalidArgumentException('Invalid callback specified.'); + } + } + + + /** + * Verifies if the provided class and method are valid callable. + * + * @param string|object $class The class name. + * @param string $method The method name. + * @param object|null $resolvedClass The resolved class. + * + * @throws Exception If the class or method is not found. + * + * @return void + */ + protected function verifyValidClassCallable($class, $method, $resolvedClass): void + { + $final_exception = null; + + // Final check to make sure it's actually a class and a method, or throw an error + if (is_object($class) === false && class_exists($class) === false) { + $final_exception = new Exception("Class '$class' not found. Is it being correctly autoloaded with Flight::path()?"); + + // If this tried to resolve a class in a container and failed somehow, throw the exception + } elseif (isset($resolvedClass) === false && $this->containerException !== null) { + $final_exception = $this->containerException; + + // Class is there, but no method + } elseif (is_object($class) === true && method_exists($class, $method) === false) { + $final_exception = new Exception("Class found, but method '" . get_class($class) . "::$method' not found."); + } - $class = new $class(); + if ($final_exception !== null) { + $this->fixOutputBuffering(); + throw $final_exception; } + } - return call_user_func_array([$class, $method], $params); + /** + * Resolves the container class. + * + * @param callable|object $container_handler Dependency injection container + * @param class-string $class Class name + * @param array &$params Class constructor parameters + * + * @return object Class object + */ + protected function resolveContainerClass($container_handler, $class, array &$params) + { + $class_object = null; + + // PSR-11 + if ( + is_object($container_handler) === true && + method_exists($container_handler, 'has') === true && + $container_handler->has($class) + ) { + $class_object = call_user_func([$container_handler, 'get'], $class); + + // Just a callable where you configure the behavior (Dice, PHP-DI, etc.) + } elseif (is_callable($container_handler) === true) { + // This is to catch all the error that could be thrown by whatever container you are using + try { + $class_object = call_user_func($container_handler, $class, $params); + } catch (Exception $e) { + // could not resolve a class for some reason + $class_object = null; + + // If the container throws an exception, we need to catch it + // and store it somewhere. If we just let it throw itself, it + // doesn't properly close the output buffers and can cause other + // issues. + // This is thrown in the verifyValidClassCallable method + $this->containerException = $e; + } + } + + return $class_object; + } + + /** + * Because this could throw an exception in the middle of an output buffer, + * + * @return void + */ + protected function fixOutputBuffering(): void + { + // Cause PHPUnit has 1 level of output buffering by default + if (ob_get_level() > (getenv('PHPUNIT_TEST') ? 1 : 0)) { + ob_end_clean(); + } } /** diff --git a/flight/net/Route.php b/flight/net/Route.php index 4abf2ae..0b8b9d7 100644 --- a/flight/net/Route.php +++ b/flight/net/Route.php @@ -81,11 +81,11 @@ class Route * Constructor. * * @param string $pattern URL pattern - * @param callable $callback Callback function + * @param callable|string $callback Callback function * @param array $methods HTTP methods * @param bool $pass Pass self in callback parameters */ - public function __construct(string $pattern, callable $callback, array $methods, bool $pass, string $alias = '') + public function __construct(string $pattern, $callback, array $methods, bool $pass, string $alias = '') { $this->pattern = $pattern; $this->callback = $callback; diff --git a/flight/net/Router.php b/flight/net/Router.php index 81556cd..d494dbb 100644 --- a/flight/net/Router.php +++ b/flight/net/Router.php @@ -80,11 +80,11 @@ class Router * Maps a URL pattern to a callback function. * * @param string $pattern URL pattern to match. - * @param callable $callback Callback function. + * @param callable|string $callback Callback function or string class->method * @param bool $pass_route Pass the matching route object to the callback. * @param string $route_alias Alias for the route. */ - public function map(string $pattern, callable $callback, bool $pass_route = false, string $route_alias = ''): Route + public function map(string $pattern, $callback, bool $pass_route = false, string $route_alias = ''): Route { // This means that the route ies defined in a group, but the defined route is the base @@ -133,11 +133,11 @@ class Router * Creates a GET based route * * @param string $pattern URL pattern to match - * @param callable $callback Callback function + * @param callable|string $callback Callback function or string class->method * @param bool $pass_route Pass the matching route object to the callback * @param string $alias Alias for the route */ - public function get(string $pattern, callable $callback, bool $pass_route = false, string $alias = ''): Route + public function get(string $pattern, $callback, bool $pass_route = false, string $alias = ''): Route { return $this->map('GET ' . $pattern, $callback, $pass_route, $alias); } @@ -146,11 +146,11 @@ class Router * Creates a POST based route * * @param string $pattern URL pattern to match - * @param callable $callback Callback function + * @param callable|string $callback Callback function or string class->method * @param bool $pass_route Pass the matching route object to the callback * @param string $alias Alias for the route */ - public function post(string $pattern, callable $callback, bool $pass_route = false, string $alias = ''): Route + public function post(string $pattern, $callback, bool $pass_route = false, string $alias = ''): Route { return $this->map('POST ' . $pattern, $callback, $pass_route, $alias); } @@ -159,11 +159,11 @@ class Router * Creates a PUT based route * * @param string $pattern URL pattern to match - * @param callable $callback Callback function + * @param callable|string $callback Callback function or string class->method * @param bool $pass_route Pass the matching route object to the callback * @param string $alias Alias for the route */ - public function put(string $pattern, callable $callback, bool $pass_route = false, string $alias = ''): Route + public function put(string $pattern, $callback, bool $pass_route = false, string $alias = ''): Route { return $this->map('PUT ' . $pattern, $callback, $pass_route, $alias); } @@ -172,11 +172,11 @@ class Router * Creates a PATCH based route * * @param string $pattern URL pattern to match - * @param callable $callback Callback function + * @param callable|string $callback Callback function or string class->method * @param bool $pass_route Pass the matching route object to the callback * @param string $alias Alias for the route */ - public function patch(string $pattern, callable $callback, bool $pass_route = false, string $alias = ''): Route + public function patch(string $pattern, $callback, bool $pass_route = false, string $alias = ''): Route { return $this->map('PATCH ' . $pattern, $callback, $pass_route, $alias); } @@ -185,11 +185,11 @@ class Router * Creates a DELETE based route * * @param string $pattern URL pattern to match - * @param callable $callback Callback function + * @param callable|string $callback Callback function or string class->method * @param bool $pass_route Pass the matching route object to the callback * @param string $alias Alias for the route */ - public function delete(string $pattern, callable $callback, bool $pass_route = false, string $alias = ''): Route + public function delete(string $pattern, $callback, bool $pass_route = false, string $alias = ''): Route { return $this->map('DELETE ' . $pattern, $callback, $pass_route, $alias); } diff --git a/tests/DispatcherTest.php b/tests/DispatcherTest.php index 826c190..d4e1ca1 100644 --- a/tests/DispatcherTest.php +++ b/tests/DispatcherTest.php @@ -4,13 +4,16 @@ declare(strict_types=1); namespace tests; +use ArgumentCountError; use Closure; use Exception; use flight\core\Dispatcher; +use flight\Engine; use InvalidArgumentException; use PharIo\Manifest\InvalidEmailException; use tests\classes\Hello; use PHPUnit\Framework\TestCase; +use tests\classes\ContainerDefault; use tests\classes\TesterClass; use TypeError; @@ -107,11 +110,11 @@ class DispatcherTest extends TestCase }); $this->dispatcher - ->hook('hello', $this->dispatcher::FILTER_BEFORE, function (array &$params): void { + ->hook('hello', 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 { + ->hook('hello', Dispatcher::FILTER_AFTER, function (array &$params, string &$output): void { // Manipulate the output $output .= ' Have a nice day!'; }); @@ -123,9 +126,10 @@ class DispatcherTest extends TestCase public function testInvalidCallback(): void { - $this->expectException(TypeError::class); + $this->expectException(Exception::class); + $this->expectExceptionMessage("Class 'NonExistentClass' not found. Is it being correctly autoloaded with Flight::path()?"); - Dispatcher::execute(['NonExistentClass', 'nonExistentMethod']); + $this->dispatcher->execute(['NonExistentClass', 'nonExistentMethod']); } public function testInvalidCallableString(): void @@ -133,27 +137,13 @@ class DispatcherTest extends TestCase $this->expectException(InvalidArgumentException::class); $this->expectExceptionMessage('Invalid callback specified.'); - Dispatcher::execute('inexistentGlobalFunction'); - } - - public function testInvalidCallbackBecauseConstructorParameters(): void - { - $class = TesterClass::class; - $method = 'instanceMethod'; - $exceptionMessage = "Method '$class::$method' cannot be called statically. "; - $exceptionMessage .= "$class::__construct require 6 parameters"; - - $this->expectException(InvalidArgumentException::class); - $this->expectExceptionMessage($exceptionMessage); - - static $params = []; - Dispatcher::invokeMethod([$class, $method], $params); + $this->dispatcher->execute('nonexistentGlobalFunction'); } // It will be useful for executing instance Controller methods statically public function testCanExecuteAnNonStaticMethodStatically(): void { - $this->assertSame('hello', Dispatcher::execute([Hello::class, 'sayHi'])); + $this->assertSame('hello', $this->dispatcher->execute([Hello::class, 'sayHi'])); } public function testItThrowsAnExceptionWhenRunAnUnregistedEventName(): void @@ -191,7 +181,7 @@ class DispatcherTest extends TestCase { set_error_handler(function (int $errno, string $errstr): void { $this->assertSame(E_USER_NOTICE, $errno); - $this->assertSame("Event 'myMethod' has been overriden!", $errstr); + $this->assertSame("Event 'myMethod' has been overridden!", $errstr); }); $this->dispatcher->set('myMethod', function (): string { @@ -199,10 +189,10 @@ class DispatcherTest extends TestCase }); $this->dispatcher->set('myMethod', function (): string { - return 'Overriden'; + return 'Overridden'; }); - $this->assertSame('Overriden', $this->dispatcher->run('myMethod')); + $this->assertSame('Overridden', $this->dispatcher->run('myMethod')); restore_error_handler(); } @@ -218,7 +208,7 @@ class DispatcherTest extends TestCase return 'Original'; }) ->hook('myMethod', 'invalid', function (array &$params, &$output): void { - $output = 'Overriden'; + $output = 'Overridden'; }); $this->assertSame('Original', $this->dispatcher->run('myMethod')); @@ -237,7 +227,7 @@ class DispatcherTest extends TestCase $validCallable = function (): void { }; - Dispatcher::filter([$validCallable, $invalidCallable], $params, $output); + $this->dispatcher->filter([$validCallable, $invalidCallable], $params, $output); } public function testCallFunction6Params(): void @@ -247,8 +237,75 @@ class DispatcherTest extends TestCase }; $params = ['param1', 'param2', 'param3', 'param4', 'param5', 'param6']; - $result = Dispatcher::callFunction($func, $params); + $result = $this->dispatcher->callFunction($func, $params); $this->assertSame('helloparam1param2param3param4param5param6', $result); } + + public function testInvokeMethod(): void + { + $class = new TesterClass('param1', 'param2', 'param3', 'param4', 'param5', 'param6'); + $result = $this->dispatcher->invokeMethod([ $class, 'instanceMethod' ]); + + $this->assertSame('param1', $class->param2); + } + + public function testExecuteStringClassBadConstructParams(): void + { + $this->expectException(ArgumentCountError::class); + $this->expectExceptionMessageMatches('#Too few arguments to function tests\\\\classes\\\\TesterClass::__construct\(\), 1 passed .+ and exactly 6 expected#'); + $this->dispatcher->execute(TesterClass::class . '->instanceMethod'); + } + + public function testExecuteStringClassNoConstruct(): void + { + $result = $this->dispatcher->execute(Hello::class . '->sayHi'); + $this->assertSame('hello', $result); + } + + public function testExecuteStringClassNoConstructDoubleColon(): void + { + $result = $this->dispatcher->execute(Hello::class . '::sayHi'); + $this->assertSame('hello', $result); + } + + public function testExecuteStringClassNoConstructArraySyntax(): void + { + $result = $this->dispatcher->execute([ Hello::class, 'sayHi' ]); + $this->assertSame('hello', $result); + } + + public function testExecuteStringClassDefaultContainer(): void + { + $engine = new Engine(); + $engine->set('test_me_out', 'You got it boss!'); + $this->dispatcher->setEngine($engine); + $result = $this->dispatcher->execute(ContainerDefault::class . '->testTheContainer'); + $this->assertSame('You got it boss!', $result); + } + + public function testExecuteStringClassDefaultContainerDoubleColon(): void + { + $engine = new Engine(); + $engine->set('test_me_out', 'You got it boss!'); + $this->dispatcher->setEngine($engine); + $result = $this->dispatcher->execute(ContainerDefault::class . '::testTheContainer'); + $this->assertSame('You got it boss!', $result); + } + + public function testExecuteStringClassDefaultContainerArraySyntax(): void + { + $engine = new Engine(); + $engine->set('test_me_out', 'You got it boss!'); + $this->dispatcher->setEngine($engine); + $result = $this->dispatcher->execute([ ContainerDefault::class, 'testTheContainer' ]); + $this->assertSame('You got it boss!', $result); + } + + public function testExecuteStringClassDefaultContainerButForgotInjectingEngine(): void + { + $this->expectException(TypeError::class); + $this->expectExceptionMessageMatches('#Argument 1 passed to tests\\\\classes\\\\ContainerDefault::__construct\(\) must be an instance of flight\\\\Engine, null given#'); + $result = $this->dispatcher->execute([ ContainerDefault::class, 'testTheContainer' ]); + } } diff --git a/tests/EngineTest.php b/tests/EngineTest.php index a977e3a..a8d58ef 100644 --- a/tests/EngineTest.php +++ b/tests/EngineTest.php @@ -5,12 +5,15 @@ declare(strict_types=1); namespace tests; use Exception; -use Flight; +use flight\database\PdoWrapper; use flight\Engine; use flight\net\Request; use flight\net\Response; use flight\util\Collection; +use PDOException; use PHPUnit\Framework\TestCase; +use tests\classes\Container; +use tests\classes\ContainerDefault; // phpcs:ignoreFile PSR2.Methods.MethodDeclaration.Underscore class EngineTest extends TestCase @@ -676,4 +679,166 @@ class EngineTest extends TestCase $engine->start(); $this->expectOutputString('before456before123OKafter123456after123'); } + + public function testContainerDice() { + $engine = new Engine(); + $dice = new \Dice\Dice(); + $dice = $dice->addRules([ + PdoWrapper::class => [ + 'shared' => true, + 'constructParams' => [ 'sqlite::memory:' ] + ] + ]); + $engine->registerContainerHandler(function ($class, $params) use ($dice) { + return $dice->create($class, $params); + }); + + $engine->route('/container', Container::class.'->testTheContainer'); + $engine->request()->url = '/container'; + $engine->start(); + + $this->expectOutputString('yay! I injected a collection, and it has 1 items'); + } + + public function testContainerDicePdoWrapperTest() { + $engine = new Engine(); + $dice = new \Dice\Dice(); + $dice = $dice->addRules([ + PdoWrapper::class => [ + 'shared' => true, + 'constructParams' => [ 'sqlite::memory:' ] + ] + ]); + $engine->registerContainerHandler(function ($class, $params) use ($dice) { + return $dice->create($class, $params); + }); + + $engine->route('/container', Container::class.'->testThePdoWrapper'); + $engine->request()->url = '/container'; + $engine->start(); + + $this->expectOutputString('Yay! I injected a PdoWrapper, and it returned the number 5 from the database!'); + } + + public function testContainerDiceFlightEngine() { + $engine = new Engine(); + $engine->set('test_me_out', 'You got it boss!'); + $dice = new \Dice\Dice(); + $dice = $dice->addRule('*', [ + 'substitutions' => [ + Engine::class => $engine + ] + ]); + $engine->registerContainerHandler(function ($class, $params) use ($dice) { + return $dice->create($class, $params); + }); + + $engine->route('/container', ContainerDefault::class.'->echoTheContainer'); + $engine->request()->url = '/container'; + $engine->start(); + + $this->expectOutputString('You got it boss!'); + } + + public function testContainerDicePdoWrapperTestBadParams() { + $engine = new Engine(); + $dice = new \Dice\Dice(); + $engine->registerContainerHandler(function ($class, $params) use ($dice) { + return $dice->create($class, $params); + }); + + $engine->route('/container', Container::class.'->testThePdoWrapper'); + $engine->request()->url = '/container'; + + $this->expectException(PDOException::class); + $this->expectExceptionMessage("invalid data source name"); + + $engine->start(); + } + + public function testContainerDiceBadClass() { + $engine = new Engine(); + $dice = new \Dice\Dice(); + $engine->registerContainerHandler(function ($class, $params) use ($dice) { + return $dice->create($class, $params); + }); + + $engine->route('/container', 'BadClass->testTheContainer'); + $engine->request()->url = '/container'; + + $this->expectException(Exception::class); + $this->expectExceptionMessage("Class 'BadClass' not found. Is it being correctly autoloaded with Flight::path()?"); + + $engine->start(); + } + + public function testContainerDiceBadMethod() { + $engine = new Engine(); + $dice = new \Dice\Dice(); + $dice = $dice->addRules([ + PdoWrapper::class => [ + 'shared' => true, + 'constructParams' => [ 'sqlite::memory:' ] + ] + ]); + $engine->registerContainerHandler(function ($class, $params) use ($dice) { + return $dice->create($class, $params); + }); + + $engine->route('/container', Container::class.'->badMethod'); + $engine->request()->url = '/container'; + + $this->expectException(Exception::class); + $this->expectExceptionMessage("Class found, but method 'tests\classes\Container::badMethod' not found."); + + $engine->start(); + } + + public function testContainerPsr11() { + $engine = new Engine(); + $container = new \League\Container\Container(); + $container->add(Container::class)->addArgument(Collection::class)->addArgument(PdoWrapper::class); + $container->add(Collection::class); + $container->add(PdoWrapper::class)->addArgument('sqlite::memory:'); + $engine->registerContainerHandler($container); + + $engine->route('/container', Container::class.'->testTheContainer'); + $engine->request()->url = '/container'; + $engine->start(); + + $this->expectOutputString('yay! I injected a collection, and it has 1 items'); + } + + public function testContainerPsr11ClassNotFound() { + $engine = new Engine(); + $container = new \League\Container\Container(); + $container->add(Container::class)->addArgument(Collection::class); + $container->add(Collection::class); + $engine->registerContainerHandler($container); + + $engine->route('/container', 'BadClass->testTheContainer'); + $engine->request()->url = '/container'; + + $this->expectException(Exception::class); + $this->expectExceptionMessage("Class 'BadClass' not found. Is it being correctly autoloaded with Flight::path()?"); + + $engine->start(); + } + + public function testContainerPsr11MethodNotFound() { + $engine = new Engine(); + $container = new \League\Container\Container(); + $container->add(Container::class)->addArgument(Collection::class)->addArgument(PdoWrapper::class); + $container->add(Collection::class); + $container->add(PdoWrapper::class)->addArgument('sqlite::memory:'); + $engine->registerContainerHandler($container); + + $engine->route('/container', Container::class.'->badMethod'); + $engine->request()->url = '/container'; + + $this->expectException(Exception::class); + $this->expectExceptionMessage("Class found, but method 'tests\classes\Container::badMethod' not found."); + + $engine->start(); + } } diff --git a/tests/classes/Container.php b/tests/classes/Container.php new file mode 100644 index 0000000..5b9d216 --- /dev/null +++ b/tests/classes/Container.php @@ -0,0 +1,32 @@ +collection = $collection; + $this->pdoWrapper = $pdoWrapper; + } + + public function testTheContainer() + { + $this->collection->whatever = 'yay!'; + echo 'yay! I injected a collection, and it has ' . $this->collection->count() . ' items'; + } + + public function testThePdoWrapper() + { + $value = intval($this->pdoWrapper->fetchField('SELECT 5')); + echo 'Yay! I injected a PdoWrapper, and it returned the number ' . $value . ' from the database!'; + } +} diff --git a/tests/classes/ContainerDefault.php b/tests/classes/ContainerDefault.php new file mode 100644 index 0000000..a71ff1f --- /dev/null +++ b/tests/classes/ContainerDefault.php @@ -0,0 +1,32 @@ +app = $engine; + } + + public function testTheContainer() + { + return $this->app->get('test_me_out'); + } + + public function echoTheContainer() + { + echo $this->app->get('test_me_out'); + } + + public function testUi() + { + echo 'Route text: The container successfully injected a value into the engine! Engine class: ' . get_class($this->app) . ' test_me_out Value: ' . $this->app->get('test_me_out') . ''; + } +} diff --git a/tests/server/LayoutMiddleware.php b/tests/server/LayoutMiddleware.php index b2e59bb..500cbd7 100644 --- a/tests/server/LayoutMiddleware.php +++ b/tests/server/LayoutMiddleware.php @@ -81,6 +81,8 @@ class LayoutMiddleware
  • Slash in Param
  • UTF8 URL
  • UTF8 URL w/ Param
  • +
  • Dice Container
  • +
  • No Container Registered
  • HTML; echo '
    '; diff --git a/tests/server/index.php b/tests/server/index.php index f00144f..769dc0c 100644 --- a/tests/server/index.php +++ b/tests/server/index.php @@ -2,6 +2,10 @@ declare(strict_types=1); +use flight\database\PdoWrapper; +use tests\classes\Container; +use tests\classes\ContainerDefault; + /* * This is the test file where we can open up a quick test server and make * sure that the UI is really working the way we would expect it to. @@ -139,6 +143,10 @@ Flight::group('', function () { Flight::route('/redirect/@id', function ($id) { echo 'Route text: This route status is that it ' . ($id === 'before/after' ? 'succeeded' : 'failed') . ' URL Param: ' . $id . ''; }); + + Flight::set('test_me_out', 'You got it boss!'); // used in /no-container route + Flight::route('/no-container', ContainerDefault::class . '->testUi'); + Flight::route('/dice', Container::class . '->testThePdoWrapper'); }, [ new LayoutMiddleware() ]); // Test 9: JSON output (should not output any other html) @@ -167,4 +175,23 @@ Flight::map('notFound', function () { echo "Go back"; }); +Flight::map('start', function () { + + if (Flight::request()->url === '/dice') { + $dice = new \Dice\Dice(); + $dice = $dice->addRules([ + PdoWrapper::class => [ + 'shared' => true, + 'constructParams' => [ 'sqlite::memory:' ] + ] + ]); + Flight::registerContainerHandler(function ($class, $params) use ($dice) { + return $dice->create($class, $params); + }); + } + + // Default start behavior now + Flight::_start(); +}); + Flight::start();