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..b22456e 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 @@ -217,6 +217,18 @@ class Engine $this->error($e); } + /** + * Registers the container handler + * + * @param callable $callback Callback function that sets the container and how it will inject classes + * + * @return void + */ + public function registerContainerHandler($callback): void + { + $this->dispatcher->setContainerHandler($callback); + } + /** * Maps a callback to a framework method. * @@ -605,11 +617,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 +642,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 +654,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 +666,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 +678,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..9a3042c 100644 --- a/flight/Flight.php +++ b/flight/Flight.php @@ -26,17 +26,17 @@ require_once __DIR__ . '/autoload.php'; * 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|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 +101,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 +112,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..67a92f9 100644 --- a/flight/core/Dispatcher.php +++ b/flight/core/Dispatcher.php @@ -35,6 +35,25 @@ class Dispatcher */ protected array $filters = []; + /** + * This is a container for the dependency injection. + * + * @var callable|object|null + */ + protected $container_handler = null; + + /** + * Sets the dependency injection container handler. + * + * @param callable|object $container_handler Dependency injection container + * + * @return void + */ + public function setContainerHandler($container_handler): void + { + $this->container_handler = $container_handler; + } + /** * Dispatches an event. * @@ -80,10 +99,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 +213,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 +238,39 @@ 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 (is_string($callback) === true && (strpos($callback, '->') !== false || strpos($callback, '::') !== false)) { + $callback = $this->parseStringClassAndMethod($callback); + } - if ($isInvalidFunctionName) { - throw new InvalidArgumentException('Invalid callback specified.'); + $this->handleInvalidCallbackType($callback); + + if (is_array($callback) === true) { + return $this->invokeMethod($callback, $params); } - if (is_array($callback)) { - return self::invokeMethod($callback, $params); + return $this->callFunction($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 +280,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,12 +294,48 @@ 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 function invokeMethod(array $func, array &$params = []) + { + return $this->invokeCallable($func, $params); + } + + /** + * 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 */ - public static function invokeMethod(array $func, array &$params = []) + public function invokeCallable($func, array &$params = []) { + // If this is a directly callable function, call it + if (is_array($func) === false) { + return call_user_func_array($func, $params); + } + [$class, $method] = $func; + // Only execute this if it's not a Flight class + if ( + $this->container_handler !== null && + ( + ( + is_object($class) === true && + strpos(get_class($class), 'flight\\') === false + ) || + is_string($class) === true + ) + ) { + $container_handler = $this->container_handler; + $class = $this->resolveContainerClass($container_handler, $class, $params); + } + if (is_string($class) && class_exists($class)) { $constructor = (new ReflectionClass($class))->getConstructor(); $constructorParamsNumber = 0; @@ -284,7 +357,56 @@ class Dispatcher $class = new $class(); } - return call_user_func_array([$class, $method], $params); + 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 handleInvalidCallbackType($callback): void + { + $isInvalidFunctionName = ( + is_string($callback) + && !function_exists($callback) + ); + + if ($isInvalidFunctionName) { + throw new InvalidArgumentException('Invalid callback specified.'); + } + } + + /** + * 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) { + $class_object = call_user_func($container_handler, $class, $params); + } + + return $class_object; } /** 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..2fd0d90 100644 --- a/tests/DispatcherTest.php +++ b/tests/DispatcherTest.php @@ -107,11 +107,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!'; }); @@ -125,7 +125,7 @@ class DispatcherTest extends TestCase { $this->expectException(TypeError::class); - Dispatcher::execute(['NonExistentClass', 'nonExistentMethod']); + $this->dispatcher->execute(['NonExistentClass', 'nonExistentMethod']); } public function testInvalidCallableString(): void @@ -133,7 +133,7 @@ class DispatcherTest extends TestCase $this->expectException(InvalidArgumentException::class); $this->expectExceptionMessage('Invalid callback specified.'); - Dispatcher::execute('inexistentGlobalFunction'); + $this->dispatcher->execute('inexistentGlobalFunction'); } public function testInvalidCallbackBecauseConstructorParameters(): void @@ -147,13 +147,13 @@ class DispatcherTest extends TestCase $this->expectExceptionMessage($exceptionMessage); static $params = []; - Dispatcher::invokeMethod([$class, $method], $params); + $this->dispatcher->invokeMethod([$class, $method], $params); } // 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 @@ -237,7 +237,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,7 +247,7 @@ 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); } diff --git a/tests/EngineTest.php b/tests/EngineTest.php index a977e3a..157bbb6 100644 --- a/tests/EngineTest.php +++ b/tests/EngineTest.php @@ -11,6 +11,7 @@ use flight\net\Request; use flight\net\Response; use flight\util\Collection; use PHPUnit\Framework\TestCase; +use tests\classes\Container; // phpcs:ignoreFile PSR2.Methods.MethodDeclaration.Underscore class EngineTest extends TestCase @@ -676,4 +677,32 @@ class EngineTest extends TestCase $engine->start(); $this->expectOutputString('before456before123OKafter123456after123'); } + + public function testContainerDice() { + $engine = new Engine(); + $dice = new \Dice\Dice(); + $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 testContainerPsr11() { + $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', Container::class.'->testTheContainer'); + $engine->request()->url = '/container'; + $engine->start(); + + $this->expectOutputString('yay! I injected a collection, and it has 1 items'); + } } diff --git a/tests/classes/Container.php b/tests/classes/Container.php new file mode 100644 index 0000000..ffb6fe1 --- /dev/null +++ b/tests/classes/Container.php @@ -0,0 +1,23 @@ +collection = $collection; + } + + public function testTheContainer() + { + $this->collection->whatever = 'yay!'; + echo 'yay! I injected a collection, and it has ' . $this->collection->count() . ' items'; + } +}