From fd6436c69afb6748265eb27a2e1b36276efd3153 Mon Sep 17 00:00:00 2001 From: Alexandre Gomes Gaigalas Date: Sun, 22 Mar 2026 09:22:45 -0300 Subject: [PATCH] Remove Param Sync Parameter synchronization has been removed, favoring the usage of PSR7 interfaces for exchange of data between routines and routes if necessary, allowing the same controller splitting technique. Respect\Rest now uses Respect\Parameter for its Reflection duties, and the main DispatchContext exposes a valid PSR11 container with its injectable dependencies. --- composer.json | 4 +- docs/README.md | 27 +-- src/DispatchContext.php | 84 +++------ src/NotFoundException.php | 12 ++ src/ResolvesCallbackArguments.php | 74 -------- src/Routes/AbstractRoute.php | 8 - src/Routes/Callback.php | 13 +- src/Routes/ControllerRoute.php | 2 +- src/RoutinePipeline.php | 18 +- src/Routines/AbstractRoutine.php | 24 +-- src/Routines/AbstractSyncedRoutine.php | 85 --------- src/Routines/AuthBasic.php | 38 +--- src/Routines/By.php | 11 +- src/Routines/ParamSynced.php | 18 -- src/Routines/Through.php | 11 +- src/Routines/When.php | 11 +- tests/DispatchContextTest.php | 189 ------------------- tests/Psr7InjectionTest.php | 34 ++-- tests/RouterTest.php | 4 +- tests/Routines/AbstractRoutineTest.php | 25 +-- tests/Routines/AbstractSyncedRoutineTest.php | 95 ---------- tests/Routines/ByTest.php | 10 +- tests/Stubs/AbstractRoutine.php | 2 +- 23 files changed, 125 insertions(+), 674 deletions(-) create mode 100644 src/NotFoundException.php delete mode 100644 src/ResolvesCallbackArguments.php delete mode 100644 src/Routines/AbstractSyncedRoutine.php delete mode 100644 src/Routines/ParamSynced.php delete mode 100644 tests/Routines/AbstractSyncedRoutineTest.php diff --git a/composer.json b/composer.json index ce3f343..91229b6 100644 --- a/composer.json +++ b/composer.json @@ -22,10 +22,12 @@ }, "require": { "php": ">=8.5", + "psr/container": "^2.0", "psr/http-factory": "^1.0", "psr/http-message": "^2.0", "psr/http-server-handler": "^1.0", - "psr/http-server-middleware": "^1.0" + "psr/http-server-middleware": "^1.0", + "respect/parameter": "^1.0" }, "require-dev": { "nyholm/psr7": "^1.8", diff --git a/docs/README.md b/docs/README.md index 0e7bba1..909998c 100644 --- a/docs/README.md +++ b/docs/README.md @@ -220,7 +220,7 @@ $r3->get('/download/*', function(string $file, ResponseInterface $response) { ``` 1. Parameters are matched by type, not position. Mix them freely with route parameters. - 2. This works with callback routes and class controller methods alike. + 2. This works with callback routes, class controller methods, and routine callbacks alike. ## PSR-15 Integration @@ -291,11 +291,9 @@ $r3->get('/documents/*', function($documentId) { ``` 1. This will match the route only if the callback on *when* is matched. - 2. The `$documentId` param must have the same name in the action and the - condition (but does not need to appear in the same order). + 2. Route parameters are passed positionally, matching the order of the `/*` segments. 3. You can specify more than one parameter per condition callback. 4. You can chain conditions: `when($cb1)->when($cb2)->when($etc)` - 5. Conditions will also sync with parameters on bound classes and instance methods. This makes it possible to validate parameters using any custom routine and not just data types such as `int` or `string`. @@ -324,7 +322,7 @@ $r3->get('/artists/*/albums/*', function($artistName, $albumName) { ``` 1. This will execute the callback defined with *by* before the route action. - 2. Parameters are synced by name, not by order, like with `when`. + 2. Route parameters are passed positionally, matching the order of the `/*` segments. 3. You can specify more than one parameter per proxy callback. 4. You can chain proxies: `by($cb1)->by($cb2)->by($etc)` 5. A `return false` from a proxy will stop the execution of any following proxies @@ -350,7 +348,6 @@ $r3->post('/artists/*/albums/*', function($artistName, $albumName) { 1. `by` proxies will be executed before the route action, `through` proxies will be executed after. 2. You are free to use them separately or in tandem. - 3. `through` can also receive parameters by name. When processing something after the route has run, it's often desirable to process its output as well. This can be achieved with a nested closure: @@ -386,23 +383,6 @@ A simple way of applying routines to every route on the router is: $r3->always('By', $logRoutine); ``` -You can use the param sync to take advantage of this: -```php -$r3->always('When', function($user=null) { - if ($user) { - return strlen($user) > 3; - } -}); - -$r3->any('/products', function () { /***/ }); -$r3->any('/users/*', function ($user) { /***/ }); -$r3->any('/users/*/products', function ($user) { /***/ }); -$r3->any('/listeners/*', function ($user) { /***/ }); -``` - -Since there are three routes with the `$user` parameter, `when` will -verify them all automatically by name. - ## File Extensions Use the `fileExtension` routine to map URL extensions to response transformations: @@ -556,7 +536,6 @@ appended to the route. Custom routines have the option of several different inte which can be implemented: * `IgnorableFileExtension` - Instructs the router to ignore the file extension in requests. - * `ParamSynced` - Syncs parameters with the route function/method. * `ProxyableBy` - Instructs the router to run method `by()` before the route. * `ProxyableThrough` - Instructs the router to run method `through()` after the route. * `ProxyableWhen` - Instructs the router to run method `when()` to validate the route match. diff --git a/src/DispatchContext.php b/src/DispatchContext.php index 9c6d5cd..8814149 100644 --- a/src/DispatchContext.php +++ b/src/DispatchContext.php @@ -4,26 +4,25 @@ namespace Respect\Rest; +use Psr\Container\ContainerInterface; use Psr\Http\Message\ResponseFactoryInterface; use Psr\Http\Message\ResponseInterface; use Psr\Http\Message\ServerRequestInterface; use Psr\Http\Message\StreamFactoryInterface; -use ReflectionFunctionAbstract; -use ReflectionParameter; +use Respect\Parameter\Resolver; use Respect\Rest\Routes\AbstractRoute; -use Respect\Rest\Routines\ParamSynced; -use Respect\Rest\Routines\Routinable; use Throwable; use function is_a; use function rawurldecode; use function rtrim; use function set_error_handler; +use function sprintf; use function strtolower; use function strtoupper; /** Internal routing context wrapping a PSR-7 server request */ -final class DispatchContext +final class DispatchContext implements ContainerInterface { /** @var array */ public array $params = []; @@ -53,6 +52,8 @@ final class DispatchContext /** @var array */ private array $sideRoutes = []; + private Resolver|null $resolver = null; + public function __construct( public ServerRequestInterface $request, public ResponseFactoryInterface&StreamFactoryInterface $factory, @@ -180,33 +181,6 @@ public function response(): ResponseInterface|null } } - /** @param array $params */ - public function routineCall( - string $type, - string $method, - Routinable $routine, - array &$params, - AbstractRoute $route, - ): mixed { - $reflection = $route->getTargetReflection($method); - - $callbackParameters = []; - - if (!$routine instanceof ParamSynced) { - $callbackParameters = $params; - } elseif ($reflection !== null) { - foreach ($routine->getParameters() as $parameter) { - $callbackParameters[] = $this->extractRouteParam( - $reflection, - $parameter, - $params, - ); - } - } - - return $routine->{$type}($this, $callbackParameters); - } - public function forward(AbstractRoute $route): ResponseInterface|null { $this->route = $route; @@ -230,6 +204,30 @@ public function setResponder(Responder $responder): void $this->responder = $responder; } + public function resolver(): Resolver + { + return $this->resolver ??= new Resolver($this); + } + + public function has(string $id): bool + { + return is_a($id, ServerRequestInterface::class, true) + || is_a($id, ResponseInterface::class, true); + } + + public function get(string $id): mixed + { + if (is_a($id, ServerRequestInterface::class, true)) { + return $this->request; + } + + if (is_a($id, ResponseInterface::class, true)) { + return $this->ensureResponseDraft(); + } + + throw new NotFoundException(sprintf('No entry found for "%s"', $id)); + } + /** @return callable|null The previous error handler, or null */ protected function prepareForErrorForwards(AbstractRoute $route): callable|null { @@ -310,28 +308,6 @@ protected function forwardToStatusRoute(ResponseInterface $preparedResponse): Re return null; } - /** @param array $params */ - protected function extractRouteParam( - ReflectionFunctionAbstract $callback, - ReflectionParameter $routeParam, - array &$params, - ): mixed { - foreach ($callback->getParameters() as $callbackParamReflection) { - if ( - $callbackParamReflection->getName() === $routeParam->getName() - && isset($params[$callbackParamReflection->getPosition()]) - ) { - return $params[$callbackParamReflection->getPosition()]; - } - } - - if ($routeParam->isDefaultValueAvailable()) { - return $routeParam->getDefaultValue(); - } - - return null; - } - protected function finalizeResponse(mixed $response): ResponseInterface { return $this->responder()->finalize( diff --git a/src/NotFoundException.php b/src/NotFoundException.php new file mode 100644 index 0000000..a9ede7a --- /dev/null +++ b/src/NotFoundException.php @@ -0,0 +1,12 @@ + $params URL-extracted parameters - * - * @return array Resolved argument list - */ - protected function resolveCallbackArguments( - ReflectionFunctionAbstract $reflection, - array $params, - DispatchContext $context, - ): array { - $refParams = $reflection->getParameters(); - - // No declared parameters — pass all URL params through (supports func_get_args()) - if ($refParams === []) { - return $params; - } - - $args = []; - $paramIndex = 0; - $hasPsrInjection = false; - - foreach ($refParams as $refParam) { - $type = $refParam->getType(); - - if ($type instanceof ReflectionNamedType && !$type->isBuiltin()) { - $typeName = $type->getName(); - - if (is_a($typeName, ServerRequestInterface::class, true)) { - $args[] = $context->request; - $hasPsrInjection = true; - continue; - } - - if (is_a($typeName, ResponseInterface::class, true)) { - $args[] = $context->factory->createResponse(); - $hasPsrInjection = true; - continue; - } - } - - $default = $refParam->isDefaultValueAvailable() ? $refParam->getDefaultValue() : null; - $args[] = $params[$paramIndex] ?? $default; - $paramIndex++; - } - - // No PSR-7 injection happened — pass params directly (faster, preserves original behavior) - if (!$hasPsrInjection) { - return $params; - } - - return $args; - } -} diff --git a/src/Routes/AbstractRoute.php b/src/Routes/AbstractRoute.php index e4be33d..d1e0db6 100644 --- a/src/Routes/AbstractRoute.php +++ b/src/Routes/AbstractRoute.php @@ -7,7 +7,6 @@ use ReflectionClass; use ReflectionFunctionAbstract; use Respect\Rest\DispatchContext; -use Respect\Rest\ResolvesCallbackArguments; use Respect\Rest\Routines\IgnorableFileExtension; use Respect\Rest\Routines\Routinable; use Respect\Rest\Routines\Unique; @@ -55,8 +54,6 @@ // phpcs:ignore SlevomatCodingStandard.Classes.SuperfluousAbstractClassNaming.SuperfluousPrefix abstract class AbstractRoute { - use ResolvesCallbackArguments; - public const string CATCHALL_IDENTIFIER = '/**'; public const array CORE_METHODS = ['GET', 'HEAD', 'POST', 'PUT', 'PATCH', 'DELETE']; @@ -135,11 +132,6 @@ public function getTargetMethod(string $method): string return $method; } - public function getTargetReflection(string $method): ReflectionFunctionAbstract|null - { - return $this->getReflection($this->getTargetMethod($method)); - } - /** @param array $params */ public function dispatchTarget(string $method, array &$params, DispatchContext $context): mixed { diff --git a/src/Routes/Callback.php b/src/Routes/Callback.php index 60bbf83..0bf9ca1 100644 --- a/src/Routes/Callback.php +++ b/src/Routes/Callback.php @@ -4,14 +4,11 @@ namespace Respect\Rest\Routes; -use Closure; -use ReflectionFunction; use ReflectionFunctionAbstract; -use ReflectionMethod; +use Respect\Parameter\Resolver; use Respect\Rest\DispatchContext; use function array_merge; -use function is_array; class Callback extends AbstractRoute { @@ -31,11 +28,7 @@ public function __construct( public function getCallbackReflection(): ReflectionFunctionAbstract { - if (is_array($this->callback)) { - return new ReflectionMethod($this->callback[0], $this->callback[1]); - } - - return new ReflectionFunction(Closure::fromCallable($this->callback)); + return Resolver::reflectCallable($this->callback); } public function getReflection(string $method): ReflectionFunctionAbstract @@ -51,7 +44,7 @@ public function getReflection(string $method): ReflectionFunctionAbstract public function runTarget(string $method, array &$params, DispatchContext $context): mixed { $reflection = $this->getReflection($method); - $args = $this->resolveCallbackArguments($reflection, array_merge($params, $this->arguments), $context); + $args = $context->resolver()->resolve($reflection, array_merge($params, $this->arguments)); return ($this->callback)(...$args); } diff --git a/src/Routes/ControllerRoute.php b/src/Routes/ControllerRoute.php index 275eb97..cccf4af 100644 --- a/src/Routes/ControllerRoute.php +++ b/src/Routes/ControllerRoute.php @@ -97,7 +97,7 @@ protected function invokeTarget(object $target, string $method, array &$params, { $reflection = $this->getReflection($method); if ($reflection !== null) { - $args = $this->resolveCallbackArguments($reflection, $params, $context); + $args = $context->resolver()->resolve($reflection, $params); return $target->$method(...$args); } diff --git a/src/RoutinePipeline.php b/src/RoutinePipeline.php index 55f424c..4b4c7fd 100644 --- a/src/RoutinePipeline.php +++ b/src/RoutinePipeline.php @@ -20,7 +20,7 @@ public function matches(DispatchContext $context, AbstractRoute $route, array $p foreach ($route->routines as $routine) { if ( $routine instanceof ProxyableWhen - && !$context->routineCall('when', $context->method(), $routine, $params, $route) + && !$routine->when($context, $params) ) { return false; } @@ -36,13 +36,7 @@ public function processBy(DispatchContext $context, AbstractRoute $route): mixed continue; } - $result = $context->routineCall( - 'by', - $context->method(), - $routine, - $context->params, - $route, - ); + $result = $routine->by($context, $context->params); if ($result instanceof AbstractRoute || $result instanceof ResponseInterface || $result === false) { return $result; @@ -59,13 +53,7 @@ public function processThrough(DispatchContext $context, AbstractRoute $route, m continue; } - $proxyCallback = $context->routineCall( - 'through', - $context->method(), - $routine, - $context->params, - $route, - ); + $proxyCallback = $routine->through($context, $context->params); if (!is_callable($proxyCallback)) { continue; diff --git a/src/Routines/AbstractRoutine.php b/src/Routines/AbstractRoutine.php index cf816ac..cf7cd19 100644 --- a/src/Routines/AbstractRoutine.php +++ b/src/Routines/AbstractRoutine.php @@ -4,35 +4,19 @@ namespace Respect\Rest\Routines; -use InvalidArgumentException; - -use function class_exists; -use function is_callable; -use function is_string; -use function method_exists; - /** Base class for callback routines */ // phpcs:ignore SlevomatCodingStandard.Classes.SuperfluousAbstractClassNaming.SuperfluousPrefix abstract class AbstractRoutine implements Routinable { - protected mixed $callback; + /** @var callable */ + protected $callback; - public function __construct(mixed $callback) + public function __construct(callable $callback) { - if (is_string($callback) && class_exists($callback) && method_exists($callback, '__invoke')) { - $this->callback = $callback; - - return; - } - - if (!is_callable($callback)) { - throw new InvalidArgumentException('Routine callback must be... guess what... callable!'); - } - $this->callback = $callback; } - protected function getCallback(): mixed + protected function getCallback(): callable { return $this->callback; } diff --git a/src/Routines/AbstractSyncedRoutine.php b/src/Routines/AbstractSyncedRoutine.php deleted file mode 100644 index a2ae90b..0000000 --- a/src/Routines/AbstractSyncedRoutine.php +++ /dev/null @@ -1,85 +0,0 @@ - */ - public function getParameters(): array - { - $reflection = $this->getReflection(); - if ($reflection instanceof ReflectionFunctionAbstract) { - return $reflection->getParameters(); - } - - return []; - } - - /** @param array $params */ - public function execute(DispatchContext $context, array $params): mixed - { - $callback = $this->getCallback(); - if (is_string($callback)) { - $reflection = $this->getReflection(); - if ($reflection instanceof ReflectionClass) { - $routineInstance = $reflection->newInstanceArgs($params); - assert(is_callable($routineInstance)); - - return $routineInstance(); - } - } - - $reflection = $this->getReflection(); - if ($reflection instanceof ReflectionFunction || $reflection instanceof ReflectionMethod) { - $args = $this->resolveCallbackArguments($reflection, $params, $context); - - return $callback(...$args); - } - - return $callback(...$params); - } - - protected function getReflection(): Reflector - { - $callback = $this->getCallback(); - if (is_array($callback)) { - return new ReflectionMethod($callback[0], $callback[1]); - } - - if ($callback instanceof Closure) { - return new ReflectionFunction($callback); - } - - if (is_string($callback)) { - /** @var class-string $callback */ // phpcs:ignore SlevomatCodingStandard.Commenting.InlineDocCommentDeclaration.MissingVariable - - return new ReflectionClass($callback); - } - - return new ReflectionObject($callback); - } -} diff --git a/src/Routines/AuthBasic.php b/src/Routines/AuthBasic.php index ac988c7..9ec198e 100644 --- a/src/Routines/AuthBasic.php +++ b/src/Routines/AuthBasic.php @@ -4,27 +4,21 @@ namespace Respect\Rest\Routines; -use Closure; use Psr\Http\Message\ResponseInterface; use Psr\Http\Message\ServerRequestInterface; -use ReflectionFunction; use ReflectionFunctionAbstract; -use ReflectionMethod; -use ReflectionNamedType; +use Respect\Parameter\Resolver; use Respect\Rest\DispatchContext; -use Respect\Rest\ResolvesCallbackArguments; use function array_merge; use function base64_decode; use function explode; -use function is_a; -use function is_array; use function stripos; use function substr; final class AuthBasic extends AbstractRoutine implements ProxyableBy { - use ResolvesCallbackArguments; + private ReflectionFunctionAbstract|null $reflection = null; public function __construct(public string $realm, mixed $callback) { @@ -46,10 +40,9 @@ public function by(DispatchContext $context, array $params): mixed } $allParams = array_merge($credentials, $params); - $args = $this->resolveCallbackArguments( + $args = $context->resolver()->resolve( $this->getCallbackReflection(), $allParams, - $context, ); $callbackResponse = ($this->callback)(...$args); @@ -70,29 +63,14 @@ private function unauthorizedResponse(DispatchContext $context): ResponseInterfa private function callbackAcceptsPsr7(): bool { - foreach ($this->getCallbackReflection()->getParameters() as $param) { - $type = $param->getType(); - - if (!$type instanceof ReflectionNamedType || $type->isBuiltin()) { - continue; - } - - if (is_a($type->getName(), ServerRequestInterface::class, true)) { - return true; - } - } - - return false; + return Resolver::acceptsType( + $this->getCallbackReflection(), + ServerRequestInterface::class, + ); } private function getCallbackReflection(): ReflectionFunctionAbstract { - $callback = $this->getCallback(); - - if (is_array($callback)) { - return new ReflectionMethod($callback[0], $callback[1]); - } - - return new ReflectionFunction(Closure::fromCallable($callback)); + return $this->reflection ??= Resolver::reflectCallable($this->getCallback()); } } diff --git a/src/Routines/By.php b/src/Routines/By.php index a651559..55e9530 100644 --- a/src/Routines/By.php +++ b/src/Routines/By.php @@ -4,15 +4,22 @@ namespace Respect\Rest\Routines; +use ReflectionFunctionAbstract; +use Respect\Parameter\Resolver; use Respect\Rest\DispatchContext; /** Generic routine executed before the route */ -final class By extends AbstractSyncedRoutine implements ProxyableBy +final class By extends AbstractRoutine implements ProxyableBy { + private ReflectionFunctionAbstract|null $reflection = null; + /** @param array $params */ // phpcs:ignore Generic.NamingConventions.ConstructorName.OldStyle public function by(DispatchContext $context, array $params): mixed { - return $this->execute($context, $params); + $this->reflection ??= Resolver::reflectCallable($this->getCallback()); + $args = $context->resolver()->resolve($this->reflection, $params); + + return ($this->getCallback())(...$args); } } diff --git a/src/Routines/ParamSynced.php b/src/Routines/ParamSynced.php deleted file mode 100644 index 479c93f..0000000 --- a/src/Routines/ParamSynced.php +++ /dev/null @@ -1,18 +0,0 @@ - - */ - public function getParameters(): array; -} diff --git a/src/Routines/Through.php b/src/Routines/Through.php index be0cff6..814b3a3 100644 --- a/src/Routines/Through.php +++ b/src/Routines/Through.php @@ -4,15 +4,22 @@ namespace Respect\Rest\Routines; +use ReflectionFunctionAbstract; +use Respect\Parameter\Resolver; use Respect\Rest\DispatchContext; /** Generic routine executed after the route */ -final class Through extends AbstractSyncedRoutine implements ProxyableThrough +final class Through extends AbstractRoutine implements ProxyableThrough { + private ReflectionFunctionAbstract|null $reflection = null; + /** @param array $params */ // phpcs:ignore Generic.NamingConventions.ConstructorName.OldStyle public function through(DispatchContext $context, array $params): mixed { - return $this->execute($context, $params); + $this->reflection ??= Resolver::reflectCallable($this->getCallback()); + $args = $context->resolver()->resolve($this->reflection, $params); + + return ($this->getCallback())(...$args); } } diff --git a/src/Routines/When.php b/src/Routines/When.php index bf69e9c..5179964 100644 --- a/src/Routines/When.php +++ b/src/Routines/When.php @@ -4,15 +4,22 @@ namespace Respect\Rest\Routines; +use ReflectionFunctionAbstract; +use Respect\Parameter\Resolver; use Respect\Rest\DispatchContext; /** Generic routine executed before route matching */ -final class When extends AbstractSyncedRoutine implements ProxyableWhen +final class When extends AbstractRoutine implements ProxyableWhen { + private ReflectionFunctionAbstract|null $reflection = null; + /** @param array $params */ // phpcs:ignore Generic.NamingConventions.ConstructorName.OldStyle public function when(DispatchContext $context, array $params): mixed { - return $this->execute($context, $params); + $this->reflection ??= Resolver::reflectCallable($this->getCallback()); + $args = $context->resolver()->resolve($this->reflection, $params); + + return ($this->getCallback())(...$args); } } diff --git a/tests/DispatchContextTest.php b/tests/DispatchContextTest.php index 4ced3f8..ac81132 100644 --- a/tests/DispatchContextTest.php +++ b/tests/DispatchContextTest.php @@ -4,7 +4,6 @@ namespace Respect\Rest\Test; -use Closure; use Nyholm\Psr7\Factory\Psr17Factory; use Nyholm\Psr7\ServerRequest; use PHPUnit\Framework\Attributes\AllowMockObjectsWithoutExpectations; @@ -13,7 +12,6 @@ use PHPUnit\Framework\MockObject\MockObject; use PHPUnit\Framework\TestCase; use Psr\Http\Message\ResponseInterface; -use ReflectionFunction; use Respect\Rest\DispatchContext; use Respect\Rest\Responder; use Respect\Rest\Routes; @@ -22,13 +20,11 @@ use function array_unique; use function array_walk; -use function func_get_args; use function implode; use function is_callable; use function md5; use function rand; use function str_replace; -use function strtolower; /** @covers Respect\Rest\DispatchContext */ #[AllowMockObjectsWithoutExpectations] @@ -343,147 +339,6 @@ public function testDeveloperCanReturnCallablesToProcessOutputAfterTargetRuns(): ); } - /** - * @param array $params - * - * @covers Respect\Rest\DispatchContext::response - */ - #[DataProvider('providerForParamSyncedRoutines')] - public function testParamSyncedRoutinesShouldAllReferenceTheSameValuesByTheirNames( - string $scenario, - array $params, - ): void { - $context = $this->newContext(new ServerRequest('GET', '/version')); - $context->params = $params; - - $route = $this->getMockForRoute( - 'GET', - '/version', - 'MySoftwareName', - 'GET', - $params, - ); - - $checkers = []; - switch ($scenario) { - case 'pureSynced': - $checkers = [ - static function ($majorVersion, $minorVersion, $patchVersion): void { - self::assertCount(3, func_get_args()); - self::assertSame(15, $majorVersion); - self::assertSame(10, $minorVersion); - self::assertSame(5, $patchVersion); - }, - static function ($patchVersion, $minorVersion, $majorVersion): void { - self::assertCount(3, func_get_args()); - self::assertSame(15, $majorVersion); - self::assertSame(10, $minorVersion); - self::assertSame(5, $patchVersion); - }, - static function ($majorVersion): void { - self::assertCount(1, func_get_args()); - self::assertSame(15, $majorVersion); - }, - static function (): void { - self::assertCount(0, func_get_args()); - }, - ]; - break; - case 'pureNulls': - $checkers = [ - static function ($majorVersion, $minorVersion, $patchVersion): void { - self::assertCount(3, func_get_args()); - self::assertNull($majorVersion); - self::assertNull($minorVersion); - self::assertNull($patchVersion); - }, - static function ($patchVersion, $minorVersion, $majorVersion): void { - self::assertCount(3, func_get_args()); - self::assertNull($majorVersion); - self::assertNull($minorVersion); - self::assertNull($patchVersion); - }, - static function ($patchVersion, $minorVersion, $majorVersion): void { - self::assertCount(3, func_get_args()); - self::assertNull($majorVersion); - self::assertNull($minorVersion); - self::assertNull($patchVersion); - }, - static function ($majorVersion): void { - self::assertCount(1, func_get_args()); - self::assertNull($majorVersion); - }, - ]; - break; - case 'pureDefaults': - $checkers = [ - static function ($majorVersion = 15, $minorVersion = 10, $patchVersion = 5): void { - self::assertCount(3, func_get_args()); - self::assertSame(15, $majorVersion); - self::assertSame(10, $minorVersion); - self::assertSame(5, $patchVersion); - }, - static function ($patchVersion = 5, $minorVersion = 10, $majorVersion = 15): void { - self::assertCount(3, func_get_args()); - self::assertSame(15, $majorVersion); - self::assertSame(10, $minorVersion); - self::assertSame(5, $patchVersion); - }, - static function ($majorVersion = 15): void { - self::assertCount(1, func_get_args()); - self::assertSame(15, $majorVersion); - }, - static function (): void { - self::assertCount(0, func_get_args()); - }, - ]; - break; - case 'mixed': - $checkers = [ - static function ($majorVersion, $minorVersion, $patchVersion = 5): void { - self::assertCount(3, func_get_args()); - self::assertSame(15, $majorVersion); - self::assertSame(10, $minorVersion); - self::assertSame(5, $patchVersion); - }, - static function ($majorVersion, $minorVersion, $patchVersion): void { - self::assertCount(3, func_get_args()); - self::assertSame(15, $majorVersion); - self::assertSame(10, $minorVersion); - self::assertNull($patchVersion); - }, - static function (): void { - self::assertCount(0, func_get_args()); - }, - ]; - break; - default: - self::fail('Unknown provider scenario'); - } - - foreach ($checkers as $checker) { - $route->appendRoutine($this->getMockForProxyableRoutine($route, 'By', $checker)); - } - - $context->route = $route; - - $response = $context->response(); - - self::assertNotNull($response); - self::assertEquals('MySoftwareName', (string) $response->getBody()); - } - - /** @return array> */ - public static function providerForParamSyncedRoutines(): array - { - return [ - ['pureSynced', [15, 10, 5]], - ['pureNulls', []], - ['pureDefaults', []], - ['mixed', [15, 10]], - ]; - } - public function testConvertingToStringCallsResponse(): void { $context = $this->newContext(new ServerRequest('GET', '/users/alganet/lists')); @@ -522,22 +377,6 @@ public function testContextAcceptsAnInjectedResponder(): void self::assertSame('Some list items', (string) $response->getBody()); } - public function test_unsynced_param_comes_as_null(): void - { - $context = $this->newContext(new ServerRequest('GET', '/')); - $context->route = new Routes\Callback('GET', '/', static function ($bar) { - return 'ok'; - }); - $args = []; - $routine = new Routines\By(static function ($foo, $bar, $baz) use (&$args): void { - $args = func_get_args(); - }); - $context->route->appendRoutine($routine); - $dummy = ['bar']; - $context->routineCall('by', 'GET', $routine, $dummy, $context->route); - self::assertEquals([null, 'bar', null], $args); - } - public function test_exception_rethrown_when_no_exception_route_matches(): void { $context = $this->newContext(new ServerRequest('GET', '/')); @@ -554,34 +393,6 @@ static function (): never { $context->response(); } - /** - * @param Routes\AbstractRoute&MockObject $route - * - * @return MockObject&Routines\Routinable - */ - protected function getMockForProxyableRoutine( - Routes\AbstractRoute $route, - string $name, - Closure $implementation, - ): MockObject { - $routine = $this->getMockForRoutine(['Proxyable' . $name, 'ParamSynced']); - $reflection = new ReflectionFunction($implementation); - $route->expects($this->any()) - ->method('getReflection') - ->with('GET') - ->willReturn($reflection); - $routine->expects($this->any()) - ->method('getParameters') - ->willReturn($reflection->getParameters()); - $routine->expects($this->any()) - ->method(strtolower($name) ?: $name) // @phpstan-ignore argument.type - ->willReturnCallback(static function ($request, $params) use ($implementation) { - return $implementation(...$params); - }); - - return $routine; - } - /** * @param array $targetParams * diff --git a/tests/Psr7InjectionTest.php b/tests/Psr7InjectionTest.php index a546c6b..509855b 100644 --- a/tests/Psr7InjectionTest.php +++ b/tests/Psr7InjectionTest.php @@ -207,6 +207,23 @@ public function authBasicSkipsAuthForGetWhenRequestInjected(): void self::assertNotEquals(401, $postOk->getStatusCode()); } + #[Test] + public function routineThroughCallbackReceivesResponseWhenTypeHinted(): void + { + $this->router->get('/wrapped', static function () { + return 'content'; + })->through(static function (ResponseInterface $res) { + return static function ($data) { + return strtoupper($data); + }; + }); + + $response = $this->router->dispatch(new ServerRequest('GET', '/wrapped'))->response(); + + self::assertNotNull($response); + self::assertEquals('CONTENT', (string) $response->getBody()); + } + #[Test] public function authBasicWithoutTypeHintsStillPassesParams(): void { @@ -225,21 +242,4 @@ public function authBasicWithoutTypeHintsStillPassesParams(): void self::assertEquals(['john', 'doe', 'a', 'b'], $captured); } - - #[Test] - public function routineThroughCallbackReceivesResponseWhenTypeHinted(): void - { - $this->router->get('/wrapped', static function () { - return 'content'; - })->through(static function (ResponseInterface $res) { - return static function ($data) { - return strtoupper($data); - }; - }); - - $response = $this->router->dispatch(new ServerRequest('GET', '/wrapped'))->response(); - - self::assertNotNull($response); - self::assertEquals('CONTENT', (string) $response->getBody()); - } } diff --git a/tests/RouterTest.php b/tests/RouterTest.php index 7cb594f..a4251e5 100644 --- a/tests/RouterTest.php +++ b/tests/RouterTest.php @@ -921,7 +921,7 @@ public function testMultipleProxies(): void })->by($proxy1)->through($proxy2)->through($proxy3); $this->router->dispatch(new ServerRequest('get', '/users/abc/def/ghi'))->response(); self::assertSame( - ['abc', 'main', 'def', 'ghi'], + ['abc', 'main', 'abc', 'abc'], $result, ); } @@ -938,7 +938,7 @@ public function testProxyParamsByReference(): void }; $this->router->get('/users/*/*', $callback)->by($proxy1); $this->router->dispatch(new ServerRequest('get', '/users/abc/def'))->response(); - self::assertEquals(['def', null], $resultProxy); + self::assertEquals(['abc', 'def'], $resultProxy); self::assertEquals(['abc', 'def'], $resultCallback); } diff --git a/tests/Routines/AbstractRoutineTest.php b/tests/Routines/AbstractRoutineTest.php index 3627f39..d8bf4de 100644 --- a/tests/Routines/AbstractRoutineTest.php +++ b/tests/Routines/AbstractRoutineTest.php @@ -4,17 +4,16 @@ namespace Respect\Rest\Test\Routines; -use InvalidArgumentException; use PHPUnit\Framework\Attributes\DataProvider; use PHPUnit\Framework\TestCase; use Respect\Rest\Test\Stubs\AbstractRoutine as Stub; use Respect\Rest\Test\Stubs\WhenAlwaysTrue as InstanceWithInvoke; -use StdClass; +use TypeError; final class AbstractRoutineTest extends TestCase { #[DataProvider('provide_valid_constructor_arguments')] - public function test_valid_constructor_arguments(mixed $argument): void + public function test_valid_constructor_arguments(callable $argument): void { self::assertInstanceOf( 'Respect\Rest\Routines\AbstractRoutine', @@ -26,7 +25,7 @@ public function test_valid_constructor_arguments(mixed $argument): void ); } - /** @return array> */ + /** @return array> */ public static function provide_valid_constructor_arguments(): array { return [ @@ -37,24 +36,12 @@ static function () { ], [['DateTime', 'createFromFormat']], [new InstanceWithInvoke()], - ['Respect\Rest\Test\Stubs\WhenAlwaysTrue'], ]; } - #[DataProvider('provide_invalid_constructor_arguments')] - public function test_invalid_constructor_arguments(mixed $argument): void + public function test_invalid_constructor_arguments(): void { - self::expectException(InvalidArgumentException::class); - self::expectExceptionMessage('Routine callback must be... guess what... callable!'); - new Stub($argument); - } - - /** @return array> */ - public static function provide_invalid_constructor_arguments(): array - { - return [ - ['this_function_name_does_not_exist'], - [new StdClass()], - ]; + self::expectException(TypeError::class); + new Stub('this_function_name_does_not_exist'); // @phpstan-ignore argument.type } } diff --git a/tests/Routines/AbstractSyncedRoutineTest.php b/tests/Routines/AbstractSyncedRoutineTest.php deleted file mode 100644 index af1c719..0000000 --- a/tests/Routines/AbstractSyncedRoutineTest.php +++ /dev/null @@ -1,95 +0,0 @@ -object = new By(static function ($userId, $blogId) { - return 'from AbstractSyncedRoutine implementation callback'; - }); - } - - /** @covers Respect\Rest\Routines\AbstractSyncedRoutine */ - public function testGetParameters(): void - { - self::assertInstanceOf('Respect\Rest\Routines\ParamSynced', $this->object); - $parameters = $this->object->getParameters(); - self::assertCount(2, $parameters); - self::assertEquals('userId', $parameters[0]->name); - self::assertEquals('blogId', $parameters[1]->name); - self::assertInstanceOf('ReflectionParameter', $parameters[0]); - } - - /** - * @covers Respect\Rest\Routines\AbstractSyncedRoutine - * @covers Respect\Rest\Routines\AbstractRoutine - */ - public function test_getParameters_with_an_array(): void - { - $callback = ['DateTime', 'createFromFormat']; - $stub = $this->getMockBuilder(AbstractSyncedRoutine::class) - ->onlyMethods(['getCallback']) - ->disableOriginalConstructor() - ->getMock(); - $stub->method('getCallback')->willReturn($callback); - - self::assertContainsOnlyInstancesOf( - 'ReflectionParameter', - $result = $stub->getParameters(), - ); - self::assertCount(3, $result); - } - - /** - * @covers Respect\Rest\Routines\AbstractSyncedRoutine - * @covers Respect\Rest\Routines\AbstractRoutine - */ - public function test_getParameters_with_function(): void - { - $callback = static function ($name) { - return 'Hello ' . $name; - }; - $stub = $this->getMockBuilder(AbstractSyncedRoutine::class) - ->onlyMethods(['getCallback']) - ->disableOriginalConstructor() - ->getMock(); - $stub->method('getCallback')->willReturn($callback); - - self::assertContainsOnlyInstancesOf( - 'ReflectionParameter', - $result = $stub->getParameters(), - ); - self::assertCount(1, $result); - } - - /** - * @covers Respect\Rest\Routines\AbstractSyncedRoutine - * @covers Respect\Rest\Routines\AbstractRoutine - */ - public function test_getParameters_with_callable_instance(): void - { - $callableInstance = new ByClassWithInvoke(); - - $stub = $this->getMockBuilder(AbstractSyncedRoutine::class) - ->onlyMethods(['getCallback']) - ->disableOriginalConstructor() - ->getMock(); - $stub->method('getCallback')->willReturn($callableInstance); - - self::assertCount(0, $stub->getParameters()); - } -} diff --git a/tests/Routines/ByTest.php b/tests/Routines/ByTest.php index 98c2e64..b396b7d 100644 --- a/tests/Routines/ByTest.php +++ b/tests/Routines/ByTest.php @@ -42,7 +42,7 @@ public function test_by_with_an_anonymous_function(): void /** * @covers Respect\Rest\Routines\By - * @covers Respect\Rest\Routines\AbstractSyncedRoutine + * @covers Respect\Rest\Routines\AbstractRoutine */ public function test_by_on_a_route(): void { @@ -61,15 +61,15 @@ public function test_by_on_a_route(): void /** * @covers Respect\Rest\Routines\By - * @covers Respect\Rest\Routines\AbstractSyncedRoutine + * @covers Respect\Rest\Routines\AbstractRoutine */ - public function test_by_on_a_route_with_classname(): void + public function test_by_on_a_route_with_invocable_instance(): void { $router = new Router('', new Psr17Factory()); $router->get('/', static function () { return 'route'; }) - ->by('Respect\Rest\Test\Stubs\ByClassWithInvoke'); + ->by(new ByClassWithInvoke()); self::assertEquals( $expected = 'route', (string) $router->dispatch(new ServerRequest('GET', '/')), @@ -78,7 +78,7 @@ public function test_by_on_a_route_with_classname(): void /** * @covers Respect\Rest\Routines\By - * @covers Respect\Rest\Routines\AbstractSyncedRoutine + * @covers Respect\Rest\Routines\AbstractRoutine */ public function test_by_with_a_callable_class_on_a_route(): void { diff --git a/tests/Stubs/AbstractRoutine.php b/tests/Stubs/AbstractRoutine.php index 7031719..7c4f05c 100644 --- a/tests/Stubs/AbstractRoutine.php +++ b/tests/Stubs/AbstractRoutine.php @@ -11,7 +11,7 @@ */ class AbstractRoutine extends RestAbstractRoutine { - public function getCallback(): mixed + public function getCallback(): callable { return $this->callback; }