diff --git a/flight/core/Dispatcher.php b/flight/core/Dispatcher.php index cf9fc39..cd49555 100644 --- a/flight/core/Dispatcher.php +++ b/flight/core/Dispatcher.php @@ -4,14 +4,13 @@ declare(strict_types=1); namespace flight\core; -use Exception; use flight\Engine; use InvalidArgumentException; use OutOfBoundsException; +use Psr\Container\ContainerExceptionInterface; use Psr\Container\ContainerInterface as Container; use ReflectionFunction; use Throwable; -use TypeError; /** * Responsible for dispatching named callables. @@ -91,10 +90,10 @@ class Dispatcher /* If dispatcher was extended, use the possibly overridden methods for pre/post filters and event execution. */ $this->runPreFilters($name, $params); - $output = $this->runEvent($name, $params); + $output = $this->runEvent($name, $params); - return $this->runPostFilters($name, $output); - } + return $this->runPostFilters($name, $output); + } // Executes the FilteredCallable, responsible of running its filters. $filteredCallable = $this->get($name); @@ -126,12 +125,12 @@ class Dispatcher /** * @deprecated Don't override this method. * @param string $eventName Callable name. - * @param mixed[] &$params Callable input. + * @param mixed[] $params Callable input. * @return void|never|mixed * @throws Throwable If the callable or its filters throw an `Throwable`. * @throws OutOfBoundsException If callable name is not found. */ - protected function runEvent(string $eventName, array &$params) + protected function runEvent(string $eventName, array $params) { $requestedMethod = $this->get($eventName); @@ -287,22 +286,50 @@ class Dispatcher } /** - * Executes a callback function. - * - * @param callable-string|(callable(): mixed)|array{class-string|object, string} $callback - * Callback function. - * @param array $params Function parameters. + * Executes a callable. * - * @return mixed Function results. - * @throws Exception If `$callback` also throws an `Exception`. + * @param callable|array{class-string|object, string}|string $callback Callable. + * @param mixed[] $params Callable input. + * @return mixed Callable output. + * @throws Throwable If the callable throws an `Throwable`. */ - public function execute($callback, array &$params = []) + public function execute($callback, array $params = []) { - if (is_string($callback) === true && (strpos($callback, '->') !== false || strpos($callback, '::') !== false)) { + $container = $this->containerHandler; + + $this->verifyValidFunction($callback); + + if (is_string($callback)) { $callback = $this->parseStringClassAndMethod($callback); } - return $this->invokeCallable($callback, $params); + if (is_callable($callback) && !is_array($callback)) { + return $callback(...$params); + } + + [$class, $method] = $callback; + $object = null; + + if (is_object($class)) { + return $class->$method(...$params); + } + + if ($this->mustUseContainer($class)) { + $object = $this->resolveContainerClass($class, $params); + + if (is_object($object)) { + $class = $object; + } + } + + $this->verifyValidClassCallable($class, $method, $object); + + // Class is a string, and method exists, create the object by hand and inject only the Engine + if (is_string($class)) { + $class = new $class($this->engine); + } + + return call_user_func_array([$class, $method], $params); } /** @@ -332,122 +359,128 @@ class Dispatcher } /** - * Calls a function. - * - * @param callable $func Name of function to call. - * @param array &$params Function parameters. + * Executes a callable. * - * @return mixed Function results. - * @deprecated 3.7.0 Use invokeCallable instead + * @deprecated Use execute instead. + * @param callable $func Callable. + * @param mixed[] $params Callable input. + * @return mixed Callable output. + * @throws Throwable If the callable throws an `Throwable`. */ - public function callFunction(callable $func, array &$params = []) + public function callFunction(callable $func, array $params = []) { - return $this->invokeCallable($func, $params); + return $this->execute($func, $params); } /** - * Invokes a method. - * - * @param array{0: class-string|object, 1: string} $func Class method. - * @param array &$params Class method parameters. + * Executes a callable. * - * @return mixed Function results. - * @throws TypeError For nonexistent class name. - * @deprecated 3.7.0 Use invokeCallable instead. + * @deprecated Use execute instead. + * @param array{class-string|object, string} $func Callable. + * @param mixed[] $params Callable input. + * @return mixed Callable output. + * @throws Throwable If the callable throws an `Throwable`. */ - public function invokeMethod(array $func, array &$params = []) + public function invokeMethod(array $func, array $params = []) { - return $this->invokeCallable($func, $params); + return $this->execute($func, $params); } /** - * Invokes a callable (anonymous function or Class->method). + * Executes a callable. * - * @param array{0: class-string|object, 1: 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 + * @deprecated Use execute instead. + * @param callable|array{class-string|object, string}|string $func Callable. + * @param mixed[] $params Callable input. + * @return mixed Callable output. + * @throws Throwable If the callable throws an `Throwable`. */ - public function invokeCallable($func, array &$params = []) + public function invokeCallable($func, array $params = []) { - // If this is a directly callable function, call it - if (is_array($func) === false) { - $this->verifyValidFunction($func); + return $this->execute($func, $params); + } - return call_user_func_array($func, $params); + /** + * Verifies if the provided function is valid callable. + * + * @deprecated This method will be removed. + * @param callable|array{class-string|object, string}|string $callback Callable. + * @throws InvalidArgumentException If the function is not valid callable. + */ + protected function verifyValidFunction($callback): void + { + /* + ✔️ function () {} + ✔️ Closure + ✔️ Object that implements __invoke + ✔️ 'existingFunction' + ✔️ 'ExistingClass::existingAccessibleStaticMethod' + ✔️ ['ExistingClass', 'existingAccessibleStaticMethod'] + ✔️ [$object, 'existingAccessibleMethod'] + ✔️ [$object, 'existingAccessibleStaticMethod'] + */ + if (is_callable($callback)) { + return; } - [$class, $method] = $func; + /* + ✔️ ['UnloadedClass', 'method'] + ✔️ ['UnloadedClass', 'staticMethod'] + */ + if ( + is_array($callback) + && count($callback) === 2 + && is_string($callback[0]) + && is_string($callback[1]) + ) { + return; + } - $mustUseTheContainer = $this->mustUseContainer($class); + /* + ✔️ 'UnloadedClass::method' + ✔️ 'UnloadedClass->method' + */ + if (is_string($callback)) { + foreach (self::CALLABLE_STRING_OPERATORS as $operator) { + $callback = explode($operator, $callback); - if ($mustUseTheContainer === true) { - $resolvedClass = $this->resolveContainerClass($class, $params); + if (count($callback) === 2) { + return; + } - if ($resolvedClass) { - $class = $resolvedClass; + [$callback] = $callback; } } - $this->verifyValidClassCallable($class, $method, $resolvedClass ?? null); - - // Class is a string, and method exists, create the object by hand and inject only the Engine - if (is_string($class)) { - $class = new $class($this->engine); - } - - return call_user_func_array([$class, $method], $params); - } - - /** - * Handles invalid callback types. - * - * @param callable-string|(callable(): mixed)|array{0: class-string|object, 1: string} $callback - * Callback function. - * - * @throws InvalidArgumentException If `$callback` is an invalid type. - */ - protected function verifyValidFunction($callback): void - { - if (is_string($callback) && !function_exists($callback)) { - throw new InvalidArgumentException('Invalid callback specified.'); - } + throw new InvalidArgumentException('Invalid callback specified.'); } /** - * Verifies if the provided class and method are valid callable. - * - * @param class-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. + * @deprecated This method will be removed. + * @template T of object + * @param class-string|T $class The class name or object. + * @param ?T $resolvedClass A class instance. + * @return void|never + * @throws InvalidArgumentException If the class or method is not found. + * @throws Throwable If the container throws an exception. */ - protected function verifyValidClassCallable($class, $method, $resolvedClass): void + protected function verifyValidClassCallable($class, string $method, ?object $resolvedClass): void { $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) { - $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 (!$resolvedClass && $this->containerException !== null) { + if (!is_object($class) && !class_exists($class)) { + $message = "Class '$class' not found. Is it being correctly autoloaded with Flight::path()?"; + $exception = new InvalidArgumentException($message); + } elseif ($this->containerException) { $exception = $this->containerException; - - // Class is there, but no method - } elseif (is_object($class) === true && method_exists($class, $method) === false) { - $classNamespace = get_class($class); - $exception = new Exception("Class found, but method '$classNamespace::$method' not found."); + } elseif (is_object($class) && !method_exists($class, $method)) { + $fqcn = get_class($class); + $exception = new InvalidArgumentException("Class found, but method '$fqcn::$method' not found."); } - if ($exception !== null) { + if ($exception) { $this->fixOutputBuffering(); throw $exception; @@ -455,39 +488,38 @@ class Dispatcher } /** - * Resolves the container class. + * Resolves a class from the container. * - * @param class-string $class Class name. - * @param array &$params Class constructor parameters. - * - * @return ?object Class object. + * @deprecated This method will be removed. + * @template T of object + * @param class-string $class The class name. + * @param mixed[] $params Class constructor arguments. + * @return ?T The resolved class instance, or null if not found. */ - public function resolveContainerClass(string $class, array &$params) + public function resolveContainerClass(string $class, array $params): ?object { - // PSR-11 - if (is_a($this->containerHandler, '\Psr\Container\ContainerInterface')) { + $container = $this->containerHandler; + + if ($container instanceof Container) { try { - return $this->containerHandler->get($class); - } catch (Throwable $exception) { + return $container->get($class); + } catch (ContainerExceptionInterface $exception) { + $this->containerException = $exception; + return null; } } - // Just a callable where you configure the behavior (Dice, PHP-DI, etc.) - if (is_callable($this->containerHandler)) { - /* This is to catch all the error that could be thrown by whatever - container you are using */ + if (is_callable($container)) { try { - return ($this->containerHandler)($class, $params); - - // could not resolve a class for some reason - } catch (Exception $exception) { + return $container($class, $params); + } catch (Throwable $throwable) { // 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 = $exception; + $this->containerException = $throwable; } }