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; }