From e7baf185d0e74d735d96d37254bdf51b8245da79 Mon Sep 17 00:00:00 2001 From: Alexandre Gomes Gaigalas Date: Sat, 28 Mar 2026 15:36:17 -0300 Subject: [PATCH] Inject routineLookup, increase coverage to 100%, remove dead code - Make NamespaceLookup a constructor dependency injected from Router into all routes and handlers, removing duplicate creation and the setRoutineLookup()/withRoutineNamespace() post-construction API - Add 16 targeted tests reaching 100% class/method coverage; remove two unreachable code paths (ControllerRoute::getTargetMethod HEAD fallback, Router::process empty-dispatch guard) - Remove unused CallbackList methods (hasKey, filterKeysContain, filterKeysNotContain, executeCallback) and their tests - Merge Responder::finalize() dual null-check blocks into one - Make compareOcurrences private and fix typo -> compareOccurrences - Collapse Router::__call() callable branch into single call --- src/DispatchContext.php | 12 +-- src/Handlers/ErrorHandler.php | 25 +++-- src/Handlers/ExceptionHandler.php | 25 ++++- src/Handlers/StatusHandler.php | 13 +-- src/Responder.php | 10 +- src/Router.php | 54 ++++------ src/Routes/AbstractRoute.php | 23 ++--- src/Routes/Callback.php | 6 +- src/Routes/ClassName.php | 8 +- src/Routes/ControllerRoute.php | 6 +- src/Routes/Factory.php | 8 +- src/Routes/Instance.php | 15 ++- src/Routes/StaticValue.php | 5 +- src/Routines/CallbackList.php | 28 ------ tests/DispatchContextTest.php | 25 ++++- tests/DispatchEngineTest.php | 26 +++-- tests/RouterTest.php | 137 ++++++++++++++++++++++++++ tests/Routes/ClassNameTest.php | 9 +- tests/Routes/FactoryTest.php | 22 ++++- tests/Routes/InstanceTest.php | 22 ++++- tests/Routes/StaticValueTest.php | 17 +++- tests/RoutinePipelineTest.php | 22 +++-- tests/Routines/AcceptTest.php | 17 ++++ tests/Routines/CallbackListTest.php | 45 +-------- tests/Stubs/FunkyCallbackList.php | 6 -- tests/Stubs/HeadWithExplicitHead.php | 20 ++++ tests/Stubs/MagicMethodController.php | 18 ++++ 27 files changed, 430 insertions(+), 194 deletions(-) create mode 100644 tests/Stubs/HeadWithExplicitHead.php create mode 100644 tests/Stubs/MagicMethodController.php diff --git a/src/DispatchContext.php b/src/DispatchContext.php index 4b5931b..bc50e54 100644 --- a/src/DispatchContext.php +++ b/src/DispatchContext.php @@ -250,9 +250,9 @@ private function resetHandlerState(): void { foreach ($this->handlers as $handler) { if ($handler instanceof ErrorHandler) { - $handler->errors = []; + $handler->clearErrors(); } elseif ($handler instanceof ExceptionHandler) { - $handler->exception = null; + $handler->clearException(); } } } @@ -277,7 +277,7 @@ static function ( string $errfile = '', int $errline = 0, ) use ($handler): bool { - $handler->errors[] = [$errno, $errstr, $errfile, $errline]; + $handler->addError($errno, $errstr, $errfile, $errline); return true; }, @@ -291,7 +291,7 @@ static function ( private function forwardCollectedErrors(): ResponseInterface|null { foreach ($this->handlers as $handler) { - if ($handler instanceof ErrorHandler && $handler->errors) { + if ($handler instanceof ErrorHandler && $handler->hasErrors()) { return $this->forward($handler); } } @@ -306,8 +306,8 @@ private function catchExceptions(Throwable $e): ResponseInterface|null continue; } - if (is_a($e, $handler->class)) { - $handler->exception = $e; + if ($handler->matches($e)) { + $handler->capture($e); return $this->forward($handler); } diff --git a/src/Handlers/ErrorHandler.php b/src/Handlers/ErrorHandler.php index df5688a..2cace06 100644 --- a/src/Handlers/ErrorHandler.php +++ b/src/Handlers/ErrorHandler.php @@ -4,20 +4,33 @@ namespace Respect\Rest\Handlers; +use Respect\Fluent\Factories\NamespaceLookup; use Respect\Rest\DispatchContext; use Respect\Rest\Routes\Callback; final class ErrorHandler extends Callback { - /** @var callable */ - public $callback; - /** @var array> */ - public array $errors = []; + private array $errors = []; + + public function __construct(NamespaceLookup $routineLookup, callable $callback) + { + parent::__construct($routineLookup, 'ANY', '^$', $callback); + } + + public function addError(int $errno, string $errstr, string $errfile, int $errline): void + { + $this->errors[] = [$errno, $errstr, $errfile, $errline]; + } + + public function hasErrors(): bool + { + return $this->errors !== []; + } - public function __construct(callable $callback) + public function clearErrors(): void { - parent::__construct('ANY', '^$', $callback); + $this->errors = []; } /** @param array $params */ diff --git a/src/Handlers/ExceptionHandler.php b/src/Handlers/ExceptionHandler.php index f5aa703..dbb8a70 100644 --- a/src/Handlers/ExceptionHandler.php +++ b/src/Handlers/ExceptionHandler.php @@ -4,20 +4,35 @@ namespace Respect\Rest\Handlers; +use Respect\Fluent\Factories\NamespaceLookup; use Respect\Rest\DispatchContext; use Respect\Rest\Routes\Callback; use Throwable; +use function is_a; + final class ExceptionHandler extends Callback { - /** @var callable */ - public $callback; + private Throwable|null $exception = null; - public Throwable|null $exception = null; + public function __construct(NamespaceLookup $routineLookup, public private(set) string $class, callable $callback) + { + parent::__construct($routineLookup, 'ANY', '^$', $callback); + } + + public function matches(Throwable $e): bool + { + return is_a($e, $this->class); + } + + public function capture(Throwable $e): void + { + $this->exception = $e; + } - public function __construct(public string $class, callable $callback) + public function clearException(): void { - parent::__construct('ANY', '^$', $callback); + $this->exception = null; } /** @param array $params */ diff --git a/src/Handlers/StatusHandler.php b/src/Handlers/StatusHandler.php index dd03594..98c935e 100644 --- a/src/Handlers/StatusHandler.php +++ b/src/Handlers/StatusHandler.php @@ -4,15 +4,16 @@ namespace Respect\Rest\Handlers; +use Respect\Fluent\Factories\NamespaceLookup; use Respect\Rest\Routes\Callback; final class StatusHandler extends Callback { - /** @var callable */ - public $callback; - - public function __construct(public readonly int|null $statusCode, callable $callback) - { - parent::__construct('ANY', '^$', $callback); + public function __construct( + NamespaceLookup $routineLookup, + public readonly int|null $statusCode, + callable $callback, + ) { + parent::__construct($routineLookup, 'ANY', '^$', $callback); } } diff --git a/src/Responder.php b/src/Responder.php index d76d144..af9d711 100644 --- a/src/Responder.php +++ b/src/Responder.php @@ -60,12 +60,6 @@ public function finalize( ): ResponseInterface { $response = $this->normalize($result); - if ($responseDraft !== null) { - if ($statusOverridden) { - $response = $response->withStatus($responseDraft->getStatusCode(), $responseDraft->getReasonPhrase()); - } - } - foreach ($defaultHeaders as $name => $value) { if ($response->hasHeader($name)) { continue; @@ -75,6 +69,10 @@ public function finalize( } if ($responseDraft !== null) { + if ($statusOverridden) { + $response = $response->withStatus($responseDraft->getStatusCode(), $responseDraft->getReasonPhrase()); + } + foreach ($responseDraft->getHeaders() as $name => $values) { if (!isset($appendedHeaderNames[strtolower($name)])) { $response = $response->withHeader($name, $values); diff --git a/src/Router.php b/src/Router.php index deb2b23..812481d 100644 --- a/src/Router.php +++ b/src/Router.php @@ -59,9 +59,10 @@ final class Router implements MiddlewareInterface, RequestHandlerInterface, Rout public function __construct( protected string $basePath, private ResponseFactoryInterface&StreamFactoryInterface $factory, + NamespaceLookup|null $routineLookup = null, ) { $this->basePath = rtrim($basePath, '/'); - $this->routineLookup = new NamespaceLookup( + $this->routineLookup = $routineLookup ?? new NamespaceLookup( new Ucfirst(), Routinable::class, 'Respect\\Rest\\Routines', @@ -81,18 +82,10 @@ public function always(string $routineName, mixed ...$params): static return $this; } - public function withRoutineNamespace(string $namespace): static - { - $this->routineLookup = $this->routineLookup->withNamespace($namespace); - - return $this; - } - public function appendRoute(AbstractRoute $route): static { $this->routes[] = $route; - $route->basePath = $this->basePath; - $route->setRoutineLookup($this->routineLookup); + $route->setBasePath($this->basePath); foreach ($this->globalRoutines as $routine) { $route->appendRoutine($routine); @@ -106,7 +99,6 @@ public function appendRoute(AbstractRoute $route): static public function appendHandler(AbstractRoute $handler): static { $this->handlers[] = $handler; - $handler->setRoutineLookup($this->routineLookup); foreach ($this->globalRoutines as $routine) { $handler->appendRoutine($routine); @@ -122,7 +114,7 @@ public function callbackRoute( callable $callback, array $arguments = [], ): Routes\Callback { - $route = new Routes\Callback($method, $path, $callback, $arguments); + $route = new Routes\Callback($this->routineLookup, $method, $path, $callback, $arguments); $this->appendRoute($route); return $route; @@ -131,7 +123,7 @@ public function callbackRoute( /** @param array $arguments */ public function classRoute(string $method, string $path, string $class, array $arguments = []): Routes\ClassName { - $route = new Routes\ClassName($method, $path, $class, $arguments); + $route = new Routes\ClassName($this->routineLookup, $method, $path, $class, $arguments); $this->appendRoute($route); return $route; @@ -151,10 +143,6 @@ public function process(ServerRequestInterface $request, RequestHandlerInterface { $context = $this->dispatch($request); - if ($context->route === null && !$context->hasPreparedResponse()) { - return $handler->handle($request); - } - if ($context->route === null) { $response = $context->response(); if ($response !== null && $response->getStatusCode() === 404) { @@ -185,7 +173,7 @@ public function dispatchContext(DispatchContext $context): DispatchContext public function onException(string $className, callable $callback): Handlers\ExceptionHandler { - $handler = new Handlers\ExceptionHandler($className, $callback); + $handler = new Handlers\ExceptionHandler($this->routineLookup, $className, $callback); $this->appendHandler($handler); return $handler; @@ -193,7 +181,7 @@ public function onException(string $className, callable $callback): Handlers\Exc public function onError(callable $callback): Handlers\ErrorHandler { - $handler = new Handlers\ErrorHandler($callback); + $handler = new Handlers\ErrorHandler($this->routineLookup, $callback); $this->appendHandler($handler); return $handler; @@ -201,7 +189,7 @@ public function onError(callable $callback): Handlers\ErrorHandler public function onStatus(int|null $statusCode, callable $callback): Handlers\StatusHandler { - $handler = new Handlers\StatusHandler($statusCode, $callback); + $handler = new Handlers\StatusHandler($this->routineLookup, $statusCode, $callback); $this->appendHandler($handler); return $handler; @@ -215,7 +203,7 @@ public function getHandlers(): array public function factoryRoute(string $method, string $path, string $className, callable $factory): Routes\Factory { - $route = new Routes\Factory($method, $path, $className, $factory); + $route = new Routes\Factory($this->routineLookup, $method, $path, $className, $factory); $this->appendRoute($route); return $route; @@ -223,7 +211,7 @@ public function factoryRoute(string $method, string $path, string $className, ca public function instanceRoute(string $method, string $path, object $instance): Routes\Instance { - $route = new Routes\Instance($method, $path, $instance); + $route = new Routes\Instance($this->routineLookup, $method, $path, $instance); $this->appendRoute($route); return $route; @@ -231,7 +219,7 @@ public function instanceRoute(string $method, string $path, object $instance): R public function staticRoute(string $method, string $path, mixed $staticValue): Routes\StaticValue { - $route = new Routes\StaticValue($method, $path, $staticValue); + $route = new Routes\StaticValue($this->routineLookup, $method, $path, $staticValue); $this->appendRoute($route); return $route; @@ -256,11 +244,6 @@ public function dispatchEngine(): DispatchEngine ); } - public static function compareOcurrences(string $patternA, string $patternB, string $sub): bool - { - return substr_count($patternA, $sub) < substr_count($patternB, $sub); - } - protected function sortRoutesByComplexity(): void { usort( @@ -273,7 +256,7 @@ static function (AbstractRoute $a, AbstractRoute $b): int { return 0; } - $slashCount = Router::compareOcurrences($pa, $pb, '/'); + $slashCount = Router::compareOccurrences($pa, $pb, '/'); $aCatchall = preg_match('#/\*\*$#', $pa); $bCatchall = preg_match('#/\*\*$#', $pb); @@ -285,7 +268,7 @@ static function (AbstractRoute $a, AbstractRoute $b): int { return $slashCount ? 1 : -1; } - if (Router::compareOcurrences($pa, $pb, AbstractRoute::PARAM_IDENTIFIER)) { + if (Router::compareOccurrences($pa, $pb, AbstractRoute::PARAM_IDENTIFIER)) { return -1; } @@ -294,6 +277,11 @@ static function (AbstractRoute $a, AbstractRoute $b): int { ); } + private static function compareOccurrences(string $patternA, string $patternB, string $sub): bool + { + return substr_count($patternA, $sub) < substr_count($patternB, $sub); + } + /** @param array $args */ public function __call(string $method, array $args): AbstractRoute { @@ -315,11 +303,7 @@ public function __call(string $method, array $args): AbstractRoute } if (is_callable($routeTarget)) { - if (!isset($args[2])) { - return $this->callbackRoute($method, $path, $routeTarget); - } - - return $this->callbackRoute($method, $path, $routeTarget, $args[2]); + return $this->callbackRoute($method, $path, $routeTarget, $args[2] ?? []); } if ($routeTarget instanceof Routable) { diff --git a/src/Routes/AbstractRoute.php b/src/Routes/AbstractRoute.php index 7860682..e0f82a1 100644 --- a/src/Routes/AbstractRoute.php +++ b/src/Routes/AbstractRoute.php @@ -65,21 +65,22 @@ abstract class AbstractRoute public const string REGEX_OPTIONAL_PARAM = '(?:/([^/]+))?'; public const string REGEX_INVALID_OPTIONAL_PARAM = '#\(\?\:/\(\[\^/\]\+\)\)\?/#'; - public string $method = ''; + public private(set) string $method = ''; - public string $regexForMatch = ''; + public private(set) string $regexForMatch = ''; - public string $regexForReplace = ''; + public private(set) string $regexForReplace = ''; /** @var array */ - public array $routines = []; + public private(set) array $routines = []; - public string|null $basePath = null; + public private(set) string|null $basePath = null; - private NamespaceLookup $routineLookup; - - public function __construct(string $method, public string $pattern = '') - { + public function __construct( + private NamespaceLookup $routineLookup, + string $method, + public string $pattern, + ) { $this->method = strtoupper($method); [$this->regexForMatch, $this->regexForReplace] @@ -147,9 +148,9 @@ public function appendRoutine(Routinable $routine): static return $this; } - public function setRoutineLookup(NamespaceLookup $lookup): void + public function setBasePath(string|null $basePath): void { - $this->routineLookup = $lookup; + $this->basePath = $basePath; } public function createUri(mixed ...$params): string diff --git a/src/Routes/Callback.php b/src/Routes/Callback.php index 0bf9ca1..a21a3a7 100644 --- a/src/Routes/Callback.php +++ b/src/Routes/Callback.php @@ -5,6 +5,7 @@ namespace Respect\Rest\Routes; use ReflectionFunctionAbstract; +use Respect\Fluent\Factories\NamespaceLookup; use Respect\Parameter\Resolver; use Respect\Rest\DispatchContext; @@ -16,14 +17,15 @@ class Callback extends AbstractRoute /** @param array $arguments */ public function __construct( + NamespaceLookup $routineLookup, string $method, string $pattern, /** @var callable */ protected $callback, /** @var array */ - public array $arguments = [], + public private(set) array $arguments = [], ) { - parent::__construct($method, $pattern); + parent::__construct($routineLookup, $method, $pattern); } public function getCallbackReflection(): ReflectionFunctionAbstract diff --git a/src/Routes/ClassName.php b/src/Routes/ClassName.php index 11c13ee..2ea64ac 100644 --- a/src/Routes/ClassName.php +++ b/src/Routes/ClassName.php @@ -6,6 +6,7 @@ use InvalidArgumentException; use ReflectionClass; +use Respect\Fluent\Factories\NamespaceLookup; use Respect\Rest\DispatchContext; use Respect\Rest\Routable; @@ -18,14 +19,15 @@ final class ClassName extends ControllerRoute /** @param array $constructorParams */ public function __construct( + NamespaceLookup $routineLookup, string $method, string $pattern, - public string $class = '', - public array $constructorParams = [], + public private(set) string $class, + public private(set) array $constructorParams = [], ) { $this->reflectionTarget = $class; - parent::__construct($method, $pattern); + parent::__construct($routineLookup, $method, $pattern); } /** @param array $params */ diff --git a/src/Routes/ControllerRoute.php b/src/Routes/ControllerRoute.php index cccf4af..be5622d 100644 --- a/src/Routes/ControllerRoute.php +++ b/src/Routes/ControllerRoute.php @@ -85,11 +85,7 @@ public function getTargetMethod(string $method): string return 'HEAD'; } - if ($this->getReflection('GET') !== null) { - return 'GET'; - } - - return $method; + return 'GET'; } /** @param array $params */ diff --git a/src/Routes/Factory.php b/src/Routes/Factory.php index b631d14..631282b 100644 --- a/src/Routes/Factory.php +++ b/src/Routes/Factory.php @@ -5,6 +5,7 @@ namespace Respect\Rest\Routes; use InvalidArgumentException; +use Respect\Fluent\Factories\NamespaceLookup; use Respect\Rest\DispatchContext; use Respect\Rest\Routable; @@ -13,15 +14,16 @@ final class Factory extends ControllerRoute protected object|null $instance = null; public function __construct( + NamespaceLookup $routineLookup, string $method, string $pattern, - public string $class = '', + public private(set) string $class, /** @var callable */ - public $factory = null, + protected $factory, ) { $this->reflectionTarget = $class; - parent::__construct($method, $pattern); + parent::__construct($routineLookup, $method, $pattern); } /** @param array $params */ diff --git a/src/Routes/Instance.php b/src/Routes/Instance.php index d8e4c35..9dae8f7 100644 --- a/src/Routes/Instance.php +++ b/src/Routes/Instance.php @@ -5,19 +5,24 @@ namespace Respect\Rest\Routes; use InvalidArgumentException; +use Respect\Fluent\Factories\NamespaceLookup; use Respect\Rest\DispatchContext; use Respect\Rest\Routable; final class Instance extends ControllerRoute { - public string $class = ''; - - public function __construct(string $method, string $pattern, protected object $instance) - { + public private(set) string $class = ''; + + public function __construct( + NamespaceLookup $routineLookup, + string $method, + string $pattern, + protected object $instance, + ) { $this->class = $instance::class; $this->reflectionTarget = $instance; - parent::__construct($method, $pattern); + parent::__construct($routineLookup, $method, $pattern); } /** @param array $params */ diff --git a/src/Routes/StaticValue.php b/src/Routes/StaticValue.php index d3c121b..320fc20 100644 --- a/src/Routes/StaticValue.php +++ b/src/Routes/StaticValue.php @@ -6,15 +6,16 @@ use ReflectionFunctionAbstract; use ReflectionMethod; +use Respect\Fluent\Factories\NamespaceLookup; use Respect\Rest\DispatchContext; final class StaticValue extends AbstractRoute { protected ReflectionMethod $reflection; - public function __construct(string $method, string $pattern, protected mixed $value) + public function __construct(NamespaceLookup $routineLookup, string $method, string $pattern, protected mixed $value) { - parent::__construct($method, $pattern); + parent::__construct($routineLookup, $method, $pattern); $this->reflection = new ReflectionMethod($this, 'returnValue'); } diff --git a/src/Routines/CallbackList.php b/src/Routines/CallbackList.php index baaccf3..d84e0a8 100644 --- a/src/Routines/CallbackList.php +++ b/src/Routines/CallbackList.php @@ -9,7 +9,6 @@ use function array_filter; use function array_keys; -use function strpos; /** * Facilitates the keyed callback lists for routines. @@ -46,35 +45,8 @@ public function getKeys(): array return array_keys($this->getArrayCopy()); } - public function hasKey(string $key): bool - { - return isset($this->$key); - } - - /** @return array */ - public function filterKeysContain(string $needle): array - { - return array_filter($this->getKeys(), static function ($key) use ($needle) { - return strpos($key, $needle) !== false; - }); - } - - /** @return array */ - public function filterKeysNotContain(string $needle): array - { - return array_filter($this->getKeys(), static function ($key) use ($needle) { - return strpos($key, $needle) === false; - }); - } - protected function getCallback(string $key): callable { return $this->$key; } - - /** @param array $params */ - protected function executeCallback(string $key, array $params): mixed - { - return ($this->$key)(...$params); - } } diff --git a/tests/DispatchContextTest.php b/tests/DispatchContextTest.php index 258000d..fe3c0ef 100644 --- a/tests/DispatchContextTest.php +++ b/tests/DispatchContextTest.php @@ -12,7 +12,10 @@ use PHPUnit\Framework\MockObject\MockObject; use PHPUnit\Framework\TestCase; use Psr\Http\Message\ResponseInterface; +use Respect\Fluent\Factories\NamespaceLookup; +use Respect\Fluent\Resolvers\Ucfirst; use Respect\Rest\DispatchContext; +use Respect\Rest\NotFoundException; use Respect\Rest\Responder; use Respect\Rest\Routes; use Respect\Rest\Routines; @@ -392,6 +395,21 @@ static function (): never { $context->response(); } + public function test_get_throws_not_found_for_unknown_id(): void + { + $context = $this->newContext(new ServerRequest('GET', '/')); + + $this->expectException(NotFoundException::class); + $context->get('SomeUnknownClass'); + } + + public function test_to_string_returns_empty_when_no_route(): void + { + $context = $this->newContext(new ServerRequest('GET', '/')); + + self::assertSame('', (string) $context); + } + /** * @param array $targetParams * @@ -406,8 +424,13 @@ protected function getMockForRoute( ): Routes\AbstractRoute { $hasTarget = $target !== null; + $lookup = new NamespaceLookup( + new Ucfirst(), + Routines\Routinable::class, + 'Respect\\Rest\\Routines', + ); $route = $this->getMockBuilder('Respect\Rest\Routes\AbstractRoute') - ->setConstructorArgs([$method, $pattern]) + ->setConstructorArgs([$lookup, $method, $pattern]) ->onlyMethods(['getReflection', 'runTarget']) ->getMock(); diff --git a/tests/DispatchEngineTest.php b/tests/DispatchEngineTest.php index 4f3e5b2..fa80747 100644 --- a/tests/DispatchEngineTest.php +++ b/tests/DispatchEngineTest.php @@ -7,11 +7,14 @@ use Nyholm\Psr7\Factory\Psr17Factory; use Nyholm\Psr7\ServerRequest; use PHPUnit\Framework\TestCase; +use Respect\Fluent\Factories\NamespaceLookup; +use Respect\Fluent\Resolvers\Ucfirst; use Respect\Rest\DispatchEngine; use Respect\Rest\RouteProvider; use Respect\Rest\Routes\AbstractRoute; use Respect\Rest\Routes\Callback; use Respect\Rest\Routes\StaticValue; +use Respect\Rest\Routines\Routinable; use RuntimeException; /** @covers Respect\Rest\DispatchEngine */ @@ -19,15 +22,18 @@ final class DispatchEngineTest extends TestCase { private Psr17Factory $factory; + private NamespaceLookup $lookup; + protected function setUp(): void { $this->factory = new Psr17Factory(); + $this->lookup = new NamespaceLookup(new Ucfirst(), Routinable::class, 'Respect\\Rest\\Routines'); } public function testMatchingRouteConfiguresContext(): void { $engine = $this->engine([ - new StaticValue('GET', '/hello', 'world'), + new StaticValue($this->lookup, 'GET', '/hello', 'world'), ]); $context = $engine->dispatch(new ServerRequest('GET', '/hello')); @@ -41,7 +47,7 @@ public function testMatchingRouteConfiguresContext(): void public function testNoMatchReturns404(): void { $engine = $this->engine([ - new StaticValue('GET', '/exists', 'ok'), + new StaticValue($this->lookup, 'GET', '/exists', 'ok'), ]); $context = $engine->dispatch(new ServerRequest('GET', '/not-found')); @@ -55,7 +61,7 @@ public function testNoMatchReturns404(): void public function testWrongMethodReturns405WithAllowHeader(): void { $engine = $this->engine([ - new StaticValue('GET', '/resource', 'ok'), + new StaticValue($this->lookup, 'GET', '/resource', 'ok'), ]); $context = $engine->dispatch(new ServerRequest('DELETE', '/resource')); @@ -70,8 +76,8 @@ public function testWrongMethodReturns405WithAllowHeader(): void public function testGlobalOptionsReturns204WithAllMethods(): void { $engine = $this->engine([ - new StaticValue('GET', '/a', 'ok'), - new StaticValue('POST', '/b', 'ok'), + new StaticValue($this->lookup, 'GET', '/a', 'ok'), + new StaticValue($this->lookup, 'POST', '/b', 'ok'), ]); $context = $engine->dispatch(new ServerRequest('OPTIONS', '*')); @@ -89,8 +95,8 @@ public function testGlobalOptionsReturns204WithAllMethods(): void public function testOptionsOnSpecificPathReturns204(): void { $engine = $this->engine([ - new StaticValue('GET', '/resource', 'ok'), - new StaticValue('POST', '/resource', 'ok'), + new StaticValue($this->lookup, 'GET', '/resource', 'ok'), + new StaticValue($this->lookup, 'POST', '/resource', 'ok'), ]); $context = $engine->dispatch(new ServerRequest('OPTIONS', '/resource')); @@ -108,7 +114,7 @@ public function testBasePathPrefixIsStripped(): void { $provider = $this->createStub(RouteProvider::class); $provider->method('getRoutes')->willReturn([ - new StaticValue('GET', '/resource', 'found'), + new StaticValue($this->lookup, 'GET', '/resource', 'found'), ]); $provider->method('getBasePath')->willReturn('/api'); @@ -127,7 +133,7 @@ public function testBasePathPrefixIsStripped(): void public function testHandleReturnsPsr7Response(): void { $engine = $this->engine([ - new StaticValue('GET', '/hello', 'world'), + new StaticValue($this->lookup, 'GET', '/hello', 'world'), ]); $response = $engine->handle(new ServerRequest('GET', '/hello')); @@ -139,7 +145,7 @@ public function testHandleReturnsPsr7Response(): void public function testHandlePropagatesUnhandledExceptions(): void { $engine = $this->engine([ - new Callback('GET', '/boom', static function (): never { + new Callback($this->lookup, 'GET', '/boom', static function (): never { throw new RuntimeException('fail'); }), ]); diff --git a/tests/RouterTest.php b/tests/RouterTest.php index c3a3c78..32faabd 100644 --- a/tests/RouterTest.php +++ b/tests/RouterTest.php @@ -22,10 +22,13 @@ use Respect\Rest\Routines; use Respect\Rest\Test\Stubs\HeadFactoryController; use Respect\Rest\Test\Stubs\HeadTest as HeadTestStub; +use Respect\Rest\Test\Stubs\HeadWithExplicitHead; +use Respect\Rest\Test\Stubs\MagicMethodController; use Respect\Rest\Test\Stubs\MyController; use Respect\Rest\Test\Stubs\MyOptionalParamRoute; use Respect\Rest\Test\Stubs\RouteKnowsNothing; use Respect\Rest\Test\Stubs\StubRoutable; +use RuntimeException; use SplObjectStorage; use stdClass; @@ -1169,6 +1172,45 @@ public function testFileExtensionCascading(): void self::assertSame('{about:en}', (string) $response->getBody()); } + public function testFileExtensionBySkipsNonMatchingKeys(): void + { + $router = self::newRouter(); + // .html is longer than .en, so sorted first — doesn't match .en → continue (line 43) + $router->get('/data/*', static fn(string $name) => $name) + ->fileExtension([ + '.html' => static fn($d) => '' . $d . '', + '.en' => static fn($d) => $d . ':en', + ]); + + $response = $router->dispatch(new ServerRequest('GET', '/data/item.en'))->response(); + self::assertNotNull($response); + self::assertSame('item:en', (string) $response->getBody()); + } + + public function testFileExtensionByReturnsNullWhenNoExtensionMatchesRemaining(): void + { + $router = self::newRouter(); + // Two FileExtension routines: one for language, one for format + // Use different lengths to ensure sort order is deterministic + $router->get('/page/*', static fn(string $slug) => $slug) + ->fileExtension(['.en' => static fn($d) => $d . ':en']) + ->fileExtension(['.json' => static fn($d) => '{' . $d . '}']); + + // URL has .en only → match strips .en into remaining + // First FileExtension (.en): remaining=.en → matches → remaining becomes '' + // Second FileExtension (.json): remaining='' → returns null immediately + // For line 59 coverage, we need remaining non-empty but no match: + // Reverse order so .json FileExtension sees .en as remaining + $router2 = self::newRouter(); + $router2->get('/page/*', static fn(string $slug) => $slug) + ->fileExtension(['.json' => static fn($d) => '{' . $d . '}']) + ->fileExtension(['.en' => static fn($d) => $d . ':en']); + + $response = $router2->dispatch(new ServerRequest('GET', '/page/about.en'))->response(); + self::assertNotNull($response); + self::assertSame('about:en', (string) $response->getBody()); + } + public function testFileExtensionLenientUnknownExtension(): void { $router = self::newRouter(); @@ -2579,6 +2621,101 @@ public function test_dispatch_context_is_passed_through_to_route_callback(): voi self::assertSame('dispatched', (string) $response->getBody()); } + public function test_array_path_binds_multiple_routes(): void + { + $router = self::newRouter(); + /** @phpstan-ignore argument.type (testing dynamic __call array path support) */ + $router->get(['/a', '/b', '/c'], static fn() => 'shared'); + + self::assertSame('shared', self::responseBody($router->dispatch(new ServerRequest('GET', '/a')))); + self::assertSame('shared', self::responseBody($router->dispatch(new ServerRequest('GET', '/b')))); + self::assertSame('shared', self::responseBody($router->dispatch(new ServerRequest('GET', '/c')))); + } + + public function test_two_catchall_routes_are_sorted_by_depth(): void + { + $router = self::newRouter(); + $router->get('/shallow/**', static fn() => 'shallow'); + $router->get('/deep/nested/**', static fn() => 'deep'); + + self::assertSame('deep', self::responseBody($router->dispatch(new ServerRequest('GET', '/deep/nested/x')))); + self::assertSame('shallow', self::responseBody($router->dispatch(new ServerRequest('GET', '/shallow/y')))); + } + + public function test_process_delegates_when_no_route_no_prepared_response(): void + { + $router = self::newRouter(); + $handler = new class implements RequestHandlerInterface { + public function handle(ServerRequestInterface $request): ResponseInterface + { + return (new Psr17Factory())->createResponse(200) + ->withBody((new Psr17Factory())->createStream('delegated')); + } + }; + + // No routes registered, dispatch to empty path → no route, no prepared response + $response = $router->process(new ServerRequest('GET', ''), $handler); + self::assertSame('delegated', (string) $response->getBody()); + } + + public function test_process_delegates_on_status_route_404(): void + { + $router = self::newRouter(); + $router->get('/exists', static fn() => 'ok'); + $router->onStatus(404, static fn() => 'custom 404'); + + $handler = new class implements RequestHandlerInterface { + public function handle(ServerRequestInterface $request): ResponseInterface + { + return (new Psr17Factory())->createResponse(200) + ->withBody((new Psr17Factory())->createStream('from next')); + } + }; + + // 404 prepared response with status route → delegates to handler + $response = $router->process(new ServerRequest('GET', '/nope'), $handler); + self::assertSame(200, $response->getStatusCode()); + self::assertSame('from next', (string) $response->getBody()); + } + + public function test_controller_route_with_explicit_head_method(): void + { + $router = self::newRouter(); + $router->any('/', HeadWithExplicitHead::class); + + $response = $router->dispatch(new ServerRequest('HEAD', '/'))->response(); + self::assertNotNull($response); + self::assertSame('', (string) $response->getBody()); + + // GET still works normally + $getResponse = $router->dispatch(new ServerRequest('GET', '/'))->response(); + self::assertNotNull($getResponse); + self::assertSame('get-response', (string) $getResponse->getBody()); + } + + public function test_exception_handler_skips_non_exception_handlers(): void + { + $router = self::newRouter(); + $router->onStatus(500, static fn() => 'status handler'); + $router->onException('RuntimeException', static fn($e) => 'caught: ' . $e->getMessage()); + $router->get('/', static function (): never { + throw new RuntimeException('whoops'); + }); + + $response = $router->handle(new ServerRequest('GET', '/')); + self::assertSame('caught: whoops', (string) $response->getBody()); + } + + public function test_magic_call_controller_dispatches_without_reflection(): void + { + $router = self::newRouter(); + $router->get('/magic/*', MagicMethodController::class); + + $response = $router->dispatch(new ServerRequest('GET', '/magic/hello'))->response(); + self::assertNotNull($response); + self::assertSame('GET:hello', (string) $response->getBody()); + } + private static function responseBody(DispatchContext $request): string { $response = $request->response(); diff --git a/tests/Routes/ClassNameTest.php b/tests/Routes/ClassNameTest.php index 63f3630..b4dfa4c 100644 --- a/tests/Routes/ClassNameTest.php +++ b/tests/Routes/ClassNameTest.php @@ -5,7 +5,10 @@ namespace Respect\Rest\Test\Routes; use PHPUnit\Framework\TestCase; +use Respect\Fluent\Factories\NamespaceLookup; +use Respect\Fluent\Resolvers\Ucfirst; use Respect\Rest\Routes\ClassName; +use Respect\Rest\Routines\Routinable; /** @covers Respect\Rest\Routes\ClassName */ final class ClassNameTest extends TestCase @@ -13,7 +16,8 @@ final class ClassNameTest extends TestCase /** @covers Respect\Rest\Routes\ClassName::getReflection */ public function test_getReflection_should_return_instance_of_current_routed_class(): void { - $route = new ClassName('any', '/', 'DateTime'); + $lookup = new NamespaceLookup(new Ucfirst(), Routinable::class, 'Respect\\Rest\\Routines'); + $route = new ClassName($lookup, 'any', '/', 'DateTime'); $refl = $route->getReflection('format'); self::assertInstanceOf('ReflectionMethod', $refl); } @@ -21,7 +25,8 @@ public function test_getReflection_should_return_instance_of_current_routed_clas /** @covers Respect\Rest\Routes\ClassName::getReflection */ public function test_getReflection_should_return_instance_make_it_snap(): void { - $route = new ClassName('any', '/', 'DateTime'); + $lookup = new NamespaceLookup(new Ucfirst(), Routinable::class, 'Respect\\Rest\\Routines'); + $route = new ClassName($lookup, 'any', '/', 'DateTime'); $refl = $route->getReflection('oXoXoXoXoXo'); self::assertNull($refl); } diff --git a/tests/Routes/FactoryTest.php b/tests/Routes/FactoryTest.php index f96f62d..1bf4d4c 100644 --- a/tests/Routes/FactoryTest.php +++ b/tests/Routes/FactoryTest.php @@ -5,8 +5,16 @@ namespace Respect\Rest\Test\Routes; use DateTime; +use InvalidArgumentException; +use Nyholm\Psr7\Factory\Psr17Factory; +use Nyholm\Psr7\ServerRequest; use PHPUnit\Framework\TestCase; +use Respect\Fluent\Factories\NamespaceLookup; +use Respect\Fluent\Resolvers\Ucfirst; +use Respect\Rest\DispatchContext; use Respect\Rest\Routes\Factory; +use Respect\Rest\Routines\Routinable; +use stdClass; /** @covers Respect\Rest\Routes\Factory */ final class FactoryTest extends TestCase @@ -14,10 +22,22 @@ final class FactoryTest extends TestCase /** @covers Respect\Rest\Routes\Factory::getReflection */ public function test_getReflection_should_return_instance_of_current_routed_class(): void { - $route = new Factory('any', '/', 'DateTime', static function () { + $lookup = new NamespaceLookup(new Ucfirst(), Routinable::class, 'Respect\\Rest\\Routines'); + $route = new Factory($lookup, 'any', '/', 'DateTime', static function () { return new DateTime(); }); $refl = $route->getReflection('format'); self::assertInstanceOf('ReflectionMethod', $refl); } + + public function test_runTarget_throws_when_factory_returns_non_routable(): void + { + $lookup = new NamespaceLookup(new Ucfirst(), Routinable::class, 'Respect\\Rest\\Routines'); + $route = new Factory($lookup, 'GET', '/', stdClass::class, static fn() => new stdClass()); + $params = []; + $context = new DispatchContext(new ServerRequest('GET', '/'), new Psr17Factory()); + + $this->expectException(InvalidArgumentException::class); + $route->runTarget('GET', $params, $context); + } } diff --git a/tests/Routes/InstanceTest.php b/tests/Routes/InstanceTest.php index be0b8e9..ddf9c7b 100644 --- a/tests/Routes/InstanceTest.php +++ b/tests/Routes/InstanceTest.php @@ -5,8 +5,16 @@ namespace Respect\Rest\Test\Routes; use DateTime; +use InvalidArgumentException; +use Nyholm\Psr7\Factory\Psr17Factory; +use Nyholm\Psr7\ServerRequest; use PHPUnit\Framework\TestCase; +use Respect\Fluent\Factories\NamespaceLookup; +use Respect\Fluent\Resolvers\Ucfirst; +use Respect\Rest\DispatchContext; use Respect\Rest\Routes\Instance; +use Respect\Rest\Routines\Routinable; +use stdClass; /** @covers Respect\Rest\Routes\Instance */ final class InstanceTest extends TestCase @@ -14,8 +22,20 @@ final class InstanceTest extends TestCase /** @covers Respect\Rest\Routes\Instance::getReflection */ public function test_getReflection_should_return_instance_of_current_routed_class(): void { - $route = new Instance('any', '/', new DateTime()); + $lookup = new NamespaceLookup(new Ucfirst(), Routinable::class, 'Respect\\Rest\\Routines'); + $route = new Instance($lookup, 'any', '/', new DateTime()); $refl = $route->getReflection('format'); self::assertInstanceOf('ReflectionMethod', $refl); } + + public function test_runTarget_throws_when_instance_not_routable(): void + { + $lookup = new NamespaceLookup(new Ucfirst(), Routinable::class, 'Respect\\Rest\\Routines'); + $route = new Instance($lookup, 'GET', '/', new stdClass()); + $params = []; + $context = new DispatchContext(new ServerRequest('GET', '/'), new Psr17Factory()); + + $this->expectException(InvalidArgumentException::class); + $route->runTarget('GET', $params, $context); + } } diff --git a/tests/Routes/StaticValueTest.php b/tests/Routes/StaticValueTest.php index 9ceacd5..e7a55ac 100644 --- a/tests/Routes/StaticValueTest.php +++ b/tests/Routes/StaticValueTest.php @@ -7,8 +7,11 @@ use Nyholm\Psr7\Factory\Psr17Factory; use Nyholm\Psr7\ServerRequest; use PHPUnit\Framework\TestCase; +use Respect\Fluent\Factories\NamespaceLookup; +use Respect\Fluent\Resolvers\Ucfirst; use Respect\Rest\DispatchContext; use Respect\Rest\Routes\StaticValue; +use Respect\Rest\Routines\Routinable; /** @covers Respect\Rest\Routes\StaticValue */ final class StaticValueTest extends TestCase @@ -16,7 +19,12 @@ final class StaticValueTest extends TestCase /** @covers Respect\Rest\Routes\StaticValue::getReflection */ public function test_getReflection_should_return_instance_of_current_routed_class(): void { - $route = new StaticValue('any', '/', ['foo']); + $route = new StaticValue( + new NamespaceLookup(new Ucfirst(), Routinable::class, 'Respect\\Rest\\Routines'), + 'any', + '/', + ['foo'], + ); $refl = $route->getReflection('format'); self::assertInstanceOf('ReflectionMethod', $refl); } @@ -24,7 +32,12 @@ public function test_getReflection_should_return_instance_of_current_routed_clas /** @covers Respect\Rest\Routes\StaticValue::runTarget */ public function test_runTarget_returns_value(): void { - $route = new StaticValue('any', '/', ['foo']); + $route = new StaticValue( + new NamespaceLookup(new Ucfirst(), Routinable::class, 'Respect\\Rest\\Routines'), + 'any', + '/', + ['foo'], + ); $p = ['']; $context = new DispatchContext( new ServerRequest('GET', '/'), diff --git a/tests/RoutinePipelineTest.php b/tests/RoutinePipelineTest.php index 3c6159d..32c97b6 100644 --- a/tests/RoutinePipelineTest.php +++ b/tests/RoutinePipelineTest.php @@ -8,10 +8,13 @@ use Nyholm\Psr7\ServerRequest; use PHPUnit\Framework\TestCase; use Psr\Http\Message\ResponseInterface; +use Respect\Fluent\Factories\NamespaceLookup; +use Respect\Fluent\Resolvers\Ucfirst; use Respect\Rest\DispatchContext; use Respect\Rest\Routes\Callback; use Respect\Rest\RoutinePipeline; use Respect\Rest\Routines\By; +use Respect\Rest\Routines\Routinable; use Respect\Rest\Routines\Through; use Respect\Rest\Routines\When; @@ -20,17 +23,20 @@ final class RoutinePipelineTest extends TestCase { private Psr17Factory $factory; + private NamespaceLookup $lookup; + private RoutinePipeline $pipeline; protected function setUp(): void { $this->factory = new Psr17Factory(); + $this->lookup = new NamespaceLookup(new Ucfirst(), Routinable::class, 'Respect\\Rest\\Routines'); $this->pipeline = new RoutinePipeline(); } public function testMatchesReturnsTrueWithNoWhenRoutines(): void { - $route = new Callback('GET', '/test', static fn(): string => 'ok'); + $route = new Callback($this->lookup, 'GET', '/test', static fn(): string => 'ok'); $context = $this->newContext(); $context->configureRoute($route); $params = []; @@ -40,7 +46,7 @@ public function testMatchesReturnsTrueWithNoWhenRoutines(): void public function testMatchesReturnsFalseWhenWhenRoutineReturnsFalse(): void { - $route = new Callback('GET', '/test', static fn(): string => 'ok'); + $route = new Callback($this->lookup, 'GET', '/test', static fn(): string => 'ok'); $route->appendRoutine(new When(static fn(): bool => false)); $context = $this->newContext(); $context->configureRoute($route); @@ -51,7 +57,7 @@ public function testMatchesReturnsFalseWhenWhenRoutineReturnsFalse(): void public function testMatchesReturnsTrueWhenWhenRoutineReturnsTrue(): void { - $route = new Callback('GET', '/test', static fn(): string => 'ok'); + $route = new Callback($this->lookup, 'GET', '/test', static fn(): string => 'ok'); $route->appendRoutine(new When(static fn(): bool => true)); $context = $this->newContext(); $context->configureRoute($route); @@ -62,7 +68,7 @@ public function testMatchesReturnsTrueWhenWhenRoutineReturnsTrue(): void public function testProcessByReturnsNullWithNoByRoutines(): void { - $route = new Callback('GET', '/test', static fn(): string => 'ok'); + $route = new Callback($this->lookup, 'GET', '/test', static fn(): string => 'ok'); $context = $this->newContext(); $context->configureRoute($route); @@ -71,7 +77,7 @@ public function testProcessByReturnsNullWithNoByRoutines(): void public function testProcessByReturnsResponseWhenByReturnsResponse(): void { - $route = new Callback('GET', '/test', static fn(): string => 'ok'); + $route = new Callback($this->lookup, 'GET', '/test', static fn(): string => 'ok'); $response = $this->factory->createResponse(401); $route->appendRoutine(new By(static fn() => $response)); $context = $this->newContext(); @@ -85,7 +91,7 @@ public function testProcessByReturnsResponseWhenByReturnsResponse(): void public function testProcessByReturnsFalseWhenByReturnsFalse(): void { - $route = new Callback('GET', '/test', static fn(): string => 'ok'); + $route = new Callback($this->lookup, 'GET', '/test', static fn(): string => 'ok'); $route->appendRoutine(new By(static fn(): bool => false)); $context = $this->newContext(); $context->configureRoute($route); @@ -95,7 +101,7 @@ public function testProcessByReturnsFalseWhenByReturnsFalse(): void public function testProcessThroughChainsCallableResults(): void { - $route = new Callback('GET', '/test', static fn(): string => 'ok'); + $route = new Callback($this->lookup, 'GET', '/test', static fn(): string => 'ok'); $route->appendRoutine(new Through(static fn() => static fn(string $v): string => $v . '-A')); $route->appendRoutine(new Through(static fn() => static fn(string $v): string => $v . '-B')); $context = $this->newContext(); @@ -108,7 +114,7 @@ public function testProcessThroughChainsCallableResults(): void public function testProcessThroughSkipsNonCallableResults(): void { - $route = new Callback('GET', '/test', static fn(): string => 'ok'); + $route = new Callback($this->lookup, 'GET', '/test', static fn(): string => 'ok'); $route->appendRoutine(new Through(static fn(): null => null)); $context = $this->newContext(); $context->configureRoute($route); diff --git a/tests/Routines/AcceptTest.php b/tests/Routines/AcceptTest.php index e5b6a02..6b4e5b1 100644 --- a/tests/Routines/AcceptTest.php +++ b/tests/Routines/AcceptTest.php @@ -136,6 +136,23 @@ public function testNonHttpPrefixedHeaderIsUsedDirectly(): void self::assertTrue($routine->when($context, $params)); } + public function testEmptySegmentsInAcceptHeaderAreSkipped(): void + { + $params = []; + $context = $this->newContext('Accept', ', , text/html, ,'); + + self::assertTrue($this->accept->when($context, $params)); + self::assertSame('text/html', $context->response()?->getHeaderLine('Content-Type')); + } + + public function testByReturnsNull(): void + { + $params = []; + $context = $this->newContext('Accept', 'text/html'); + + self::assertNull($this->accept->by($context, $params)); + } + private function newContext(string $header, string $value): DispatchContext { return new DispatchContext( diff --git a/tests/Routines/CallbackListTest.php b/tests/Routines/CallbackListTest.php index fc0504d..0002c0e 100644 --- a/tests/Routines/CallbackListTest.php +++ b/tests/Routines/CallbackListTest.php @@ -7,6 +7,7 @@ use Closure; use PHPUnit\Framework\TestCase; use Respect\Rest\Test\Stubs\FunkyCallbackList; +use UnexpectedValueException; /** @covers Respect\Rest\Routines\CallbackList */ final class CallbackListTest extends TestCase @@ -29,15 +30,6 @@ protected function setUp(): void $this->object = new FunkyCallbackList($ar); } - /** @covers Respect\Rest\Routines\CallbackList::executeCallback */ - public function testExecuteCallback(): void - { - self::assertEquals('<p></p>', $this->object->funkyExecuteCallback('a', ['

'])); - self::assertTrue($this->object->funkyExecuteCallback('b', [])); - self::assertFalse($this->object->funkyExecuteCallback('c', ['d', 'abc'])); - self::assertTrue($this->object->funkyExecuteCallback('e', [4])); - } - /** @covers Respect\Rest\Routines\CallbackList::getCallback */ public function testGetCallback(): void { @@ -71,38 +63,11 @@ public function testGetKeys(): void self::assertContains('e', $a); } - /** @covers Respect\Rest\Routines\CallbackList::hasKey */ - public function testHasKey(): void + public function testConstructorThrowsWhenNoCallablesProvided(): void { - self::assertTrue($this->object->hasKey('a')); - self::assertTrue($this->object->hasKey('b')); - self::assertTrue($this->object->hasKey('c')); - self::assertFalse($this->object->hasKey('d')); - self::assertTrue($this->object->hasKey('e')); - } - - /** @covers Respect\Rest\Routines\CallbackList::filterKeysContain */ - public function testFilterKeysContain(): void - { - $a = $this->object->filterKeysContain('b'); - self::assertCount(1, $a); - self::assertNotContains('a', $a); - self::assertContains('b', $a); - self::assertNotContains('c', $a); - self::assertNotContains('d', $a); - self::assertNotContains('e', $a); - } - - /** @covers Respect\Rest\Routines\CallbackList::filterKeysNotContain */ - public function testFilterKeysNotContain(): void - { - $a = $this->object->filterKeysNotContain('b'); - self::assertCount(3, $a); - self::assertContains('a', $a); - self::assertNotContains('b', $a); - self::assertContains('c', $a); - self::assertNotContains('d', $a); - self::assertContains('e', $a); + $this->expectException(UnexpectedValueException::class); + /** @phpstan-ignore argument.type */ + new FunkyCallbackList(['not_a_callable_string_xyz']); } protected function tearDown(): void diff --git a/tests/Stubs/FunkyCallbackList.php b/tests/Stubs/FunkyCallbackList.php index fbb8b51..29ee622 100644 --- a/tests/Stubs/FunkyCallbackList.php +++ b/tests/Stubs/FunkyCallbackList.php @@ -8,12 +8,6 @@ class FunkyCallbackList extends CallbackList { - /** @param array $params */ - public function funkyExecuteCallback(string $key, array $params): mixed - { - return $this->executeCallback($key, $params); - } - public function funkyGetCallback(string $key): callable { return $this->getCallback($key); diff --git a/tests/Stubs/HeadWithExplicitHead.php b/tests/Stubs/HeadWithExplicitHead.php new file mode 100644 index 0000000..8a62e9c --- /dev/null +++ b/tests/Stubs/HeadWithExplicitHead.php @@ -0,0 +1,20 @@ + $args */ + public function __call(string $name, array $args): string + { + return $name . ':' . implode(',', $args); + } +}