diff --git a/README.md b/README.md index b04f29a..724b912 100644 --- a/README.md +++ b/README.md @@ -23,7 +23,7 @@ validate it when needed. The package could be installed with composer: ``` -composer require yiisoft/request-model +composer require yiisoft/request-model --prefer-dist ``` ## General usage @@ -112,20 +112,20 @@ final class ViewPostRequest extends RequestModel Inside the request model class, data is available using the following keys: -| key | source | -| ---------- | ---------------------------- | -| query | $request->getQueryParams() | -| body | $request->getParsedBody() | -| attributes | $request->getAttributes() | -| headers | $request->getHeaders() | -| files | $request->getUploadedFiles() | -| cookie | $request->getCookieParams() | +| key | source | +|------------|-------------------------------| +| query | $request->getQueryParams() | +| body | $request->getParsedBody() | +| attributes | $request->getAttributes() | +| headers | $request->getHeaders() | +| files | $request->getUploadedFiles() | +| cookie | $request->getCookieParams() | | router | $currentRoute->getArguments() | This data can be obtained as follows ```php -$this->requestData['router.id']; +$this->requestData['router']['id']; ``` or through the methods @@ -135,7 +135,36 @@ $this->hasAttribute('body.user_id'); $this->getAttributeValue('body.user_id'); ``` +#### Attributes +You can use attributes in an action handler to get data from a request: + +```php +use Psr\Http\Message\ResponseInterface; +use Yiisoft\RequestModel\Attribute\Request; +use Yiisoft\RequestModel\Attribute\Route; + +final class SimpleController +{ + public function action(#[Route('id')] int $id, #[Request('foo')] $attribute,): ResponseInterface + { + echo $id; + //... + } +} +``` + +Attributes are also supported in closure actions. + +There are several attributes out of the box: + +| Name | Source | +|---------------|---------------------------| +| Body | Parsed body of request | +| Query | Query parameter of URI | +| Request | Attribute of request | +| Route | Argument of current route | +| UploadedFiles | Uploaded files of request | ### Unit testing diff --git a/src/ActionWrapper.php b/src/ActionWrapper.php index 36de211..540d237 100644 --- a/src/ActionWrapper.php +++ b/src/ActionWrapper.php @@ -14,27 +14,30 @@ final class ActionWrapper implements MiddlewareInterface { - private string $class; - private string $method; - private ContainerInterface $container; - private RequestModelFactory $factory; - - public function __construct(ContainerInterface $container, RequestModelFactory $factory, string $class, string $method) - { - $this->container = $container; - $this->factory = $factory; - $this->class = $class; - $this->method = $method; + public function __construct( + private ContainerInterface $container, + private HandlerParametersResolver $parametersResolver, + private string $class, + private string $method + ) { } public function process(ServerRequestInterface $request, RequestHandlerInterface $handler): ResponseInterface { $controller = $this->container->get($this->class); - $params = array_merge([$request, $handler], $this->factory->createInstances($request, $this->getHandlerParams())); - return (new Injector($this->container))->invoke([$controller, $this->method], $params); + $parameters = array_merge( + [$request, $handler], + $this->parametersResolver->resolve($this->getHandlerParameters(), $request), + ); + return (new Injector($this->container))->invoke([$controller, $this->method], $parameters); } - private function getHandlerParams(): array + /** + * @throws \ReflectionException + * + * @return \ReflectionParameter[] + */ + private function getHandlerParameters(): array { return (new ReflectionClass($this->class)) ->getMethod($this->method) diff --git a/src/Attribute/Body.php b/src/Attribute/Body.php new file mode 100644 index 0000000..22844ec --- /dev/null +++ b/src/Attribute/Body.php @@ -0,0 +1,21 @@ +name; + } + + public function getType(): string + { + return self::QUERY_PARAM; + } +} diff --git a/src/Attribute/Request.php b/src/Attribute/Request.php new file mode 100644 index 0000000..076b88f --- /dev/null +++ b/src/Attribute/Request.php @@ -0,0 +1,25 @@ +name; + } +} diff --git a/src/Attribute/Route.php b/src/Attribute/Route.php new file mode 100644 index 0000000..986ef72 --- /dev/null +++ b/src/Attribute/Route.php @@ -0,0 +1,25 @@ +name; + } + + public function getType(): string + { + return self::ROUTE_PARAM; + } +} diff --git a/src/Attribute/UploadedFiles.php b/src/Attribute/UploadedFiles.php new file mode 100644 index 0000000..5534da2 --- /dev/null +++ b/src/Attribute/UploadedFiles.php @@ -0,0 +1,21 @@ +container = $container; - $this->factory = $factory; + public function __construct( + private ContainerInterface $container, + private HandlerParametersResolver $parametersResolver, + callable $callback + ) { $this->callback = $callback; } @@ -35,13 +33,13 @@ public function process(ServerRequestInterface $request, RequestHandlerInterface { $params = array_merge( [$request, $handler], - $this->factory->createInstances($request, $this->getHandlerParams()) + $this->parametersResolver->resolve($this->getHandlerParameters(), $request) ); $response = (new Injector($this->container))->invoke($this->callback, $params); return $response instanceof MiddlewareInterface ? $response->process($request, $handler) : $response; } - private function getHandlerParams(): array + private function getHandlerParameters(): array { return $this ->getReflector() diff --git a/src/HandlerParametersResolver.php b/src/HandlerParametersResolver.php new file mode 100644 index 0000000..acaa17a --- /dev/null +++ b/src/HandlerParametersResolver.php @@ -0,0 +1,65 @@ +getAttributeParams($parameters, $request), + $this->factory->createInstances($request, $parameters) + ); + } + + /** + * @param \ReflectionParameter[] $parameters + * @param ServerRequestInterface $request + * + * @return array + */ + private function getAttributeParams(array $parameters, ServerRequestInterface $request): array + { + $actionParameters = []; + foreach ($parameters as $parameter) { + $attributes = $parameter->getAttributes( + HandlerParameterAttributeInterface::class, + \ReflectionAttribute::IS_INSTANCEOF + ); + foreach ($attributes as $attribute) { + /** @var HandlerParameterAttributeInterface $attributeInstance */ + $attributeInstance = $attribute->newInstance(); + + $actionParameters[$parameter->getName()] = match ($attributeInstance->getType()) { + HandlerParameterAttributeInterface::ROUTE_PARAM => $this + ->currentRoute + ->getArgument($attributeInstance->getName()), + HandlerParameterAttributeInterface::REQUEST_BODY => $request->getParsedBody(), + HandlerParameterAttributeInterface::REQUEST_ATTRIBUTE => $request->getAttribute( + $attributeInstance->getName() + ), + HandlerParameterAttributeInterface::QUERY_PARAM => $request + ->getQueryParams()[$attributeInstance->getName()] ?? null, + HandlerParameterAttributeInterface::UPLOADED_FILES => $request->getUploadedFiles() + }; + } + } + return $actionParameters; + } +} diff --git a/src/MiddlewareFactory.php b/src/MiddlewareFactory.php index e644dbe..227291b 100644 --- a/src/MiddlewareFactory.php +++ b/src/MiddlewareFactory.php @@ -10,8 +10,6 @@ use Yiisoft\Middleware\Dispatcher\MiddlewareFactoryInterface; use function get_debug_type; -use function is_scalar; -use function var_export; final class MiddlewareFactory implements MiddlewareFactoryInterface { @@ -58,14 +56,17 @@ private function validateMiddleware($middlewareDefinition): void return; } - if ($this->isCallable($middlewareDefinition) && (!is_array($middlewareDefinition) || !is_object($middlewareDefinition[0]))) { + if ($this->isCallable($middlewareDefinition) + && (!is_array($middlewareDefinition) + || !is_object($middlewareDefinition[0])) + ) { return; } throw new InvalidArgumentException( sprintf( 'Parameter should be either PSR middleware class name or a callable, "%s" given.', - is_scalar($middlewareDefinition) ? var_export($middlewareDefinition, true) : get_debug_type($middlewareDefinition), + get_debug_type($middlewareDefinition), ) ); } @@ -76,6 +77,12 @@ private function isCallable($definition): bool return true; } - return is_array($definition) && array_keys($definition) === [0, 1] && in_array($definition[1], class_exists($definition[0]) ? get_class_methods($definition[0]) : [], true); + return is_array($definition) + && array_keys($definition) === [0, 1] + && in_array( + $definition[1], + class_exists($definition[0]) ? get_class_methods($definition[0]) : [], + true + ); } } diff --git a/src/RequestModel.php b/src/RequestModel.php index b55e457..e1e1f4f 100644 --- a/src/RequestModel.php +++ b/src/RequestModel.php @@ -16,7 +16,7 @@ public function setRequestData(array $requestData): void $this->requestData = $requestData; } - public function getAttributeValue(string $attribute, $default = null) + public function getAttributeValue(string $attribute, $default = null): mixed { return ArrayHelper::getValueByPath($this->requestData, $attribute, $default, $this->attributeDelimiter); } diff --git a/src/WrapperFactory.php b/src/WrapperFactory.php index 9bd0e57..72b2665 100644 --- a/src/WrapperFactory.php +++ b/src/WrapperFactory.php @@ -9,22 +9,19 @@ final class WrapperFactory { - private ContainerInterface $container; - private RequestModelFactory $requestModelFactory; - - public function __construct(ContainerInterface $container, RequestModelFactory $requestModelFactory) - { - $this->container = $container; - $this->requestModelFactory = $requestModelFactory; + public function __construct( + private ContainerInterface $container, + private HandlerParametersResolver $parametersResolver + ) { } public function createCallableWrapper(callable $callback): MiddlewareInterface { - return new CallableWrapper($this->container, $this->requestModelFactory, $callback); + return new CallableWrapper($this->container, $this->parametersResolver, $callback); } public function createActionWrapper(string $class, string $method): MiddlewareInterface { - return new ActionWrapper($this->container, $this->requestModelFactory, $class, $method); + return new ActionWrapper($this->container, $this->parametersResolver, $class, $method); } } diff --git a/tests/ActionWrapperTest.php b/tests/ActionWrapperTest.php index d0b980e..782b90d 100644 --- a/tests/ActionWrapperTest.php +++ b/tests/ActionWrapperTest.php @@ -4,6 +4,8 @@ namespace Yiisoft\RequestModel\Tests; +use Nyholm\Psr7\Stream; +use Nyholm\Psr7\UploadedFile; use Yiisoft\RequestModel\ActionWrapper; use Yiisoft\RequestModel\Tests\Support\SimpleController; use Yiisoft\RequestModel\Tests\Support\TestCase; @@ -16,7 +18,7 @@ public function testCorrectProcess(): void $wrapper = new ActionWrapper( $container, - $this->createRequestModelFactory($container), + $this->createParametersResolver($container), SimpleController::class, 'action' ); @@ -45,23 +47,74 @@ public function testCorrectProcessRouterParams(): void $wrapper = new ActionWrapper( $container, - $this->createRequestModelFactory($container), + $this->createParametersResolver($container), SimpleController::class, 'anotherAction' ); - $request = $this->createRequest( + $request = $this->createRequest([]); + + $result = $wrapper->process($request, $this->createRequestHandler()); + + $this->assertEquals( [ - 'login' => 'login', - 'password' => 'password', - ] + 'id' => [1], + ], + $result->getHeaders() + ); + } + + public function testCorrectProcessAttributes(): void + { + $container = $this->createContainer(); + + $wrapper = new ActionWrapper( + $container, + $this->createParametersResolver($container), + SimpleController::class, + 'actionUsingAttributes' ); + $body = [ + 'test', + ]; + $stream = Stream::create('test'); + $files = [new UploadedFile($stream, $stream->getSize(), UPLOAD_ERR_OK, 'test.txt')]; + $request = $this->createRequest($body); + $request = $request->withUploadedFiles($files); + $result = $wrapper->process($request, $this->createRequestHandler()); $this->assertEquals( [ 'id' => [1], + 'body' => $body, + 'countFiles' => [1], + ], + $result->getHeaders() + ); + } + + public function testCorrectProcessAttributes2(): void + { + $container = $this->createContainer(); + + $wrapper = new ActionWrapper( + $container, + $this->createParametersResolver($container), + SimpleController::class, + 'actionUsingAttributes2' + ); + + $request = $this->createRequest([]); + $request = $request->withAttribute('attribute', 'test')->withQueryParams(['page' => 1]); + + $result = $wrapper->process($request, $this->createRequestHandler()); + + $this->assertEquals( + [ + 'page' => [1], + 'attribute' => ['test'], ], $result->getHeaders() ); diff --git a/tests/Attribute/BodyTest.php b/tests/Attribute/BodyTest.php new file mode 100644 index 0000000..e896d48 --- /dev/null +++ b/tests/Attribute/BodyTest.php @@ -0,0 +1,18 @@ +assertEquals(HandlerParameterAttributeInterface::REQUEST_BODY, $instance->getType()); + $this->assertNull($instance->getName()); + } +} diff --git a/tests/Attribute/QueryTest.php b/tests/Attribute/QueryTest.php new file mode 100644 index 0000000..0ad2d54 --- /dev/null +++ b/tests/Attribute/QueryTest.php @@ -0,0 +1,18 @@ +assertEquals(HandlerParameterAttributeInterface::QUERY_PARAM, $instance->getType()); + $this->assertEquals('page', $instance->getName()); + } +} diff --git a/tests/Attribute/RequestTest.php b/tests/Attribute/RequestTest.php new file mode 100644 index 0000000..55ef7ad --- /dev/null +++ b/tests/Attribute/RequestTest.php @@ -0,0 +1,18 @@ +assertEquals(HandlerParameterAttributeInterface::REQUEST_ATTRIBUTE, $instance->getType()); + $this->assertEquals('foo', $instance->getName()); + } +} diff --git a/tests/Attribute/RouteTest.php b/tests/Attribute/RouteTest.php new file mode 100644 index 0000000..f8245f6 --- /dev/null +++ b/tests/Attribute/RouteTest.php @@ -0,0 +1,18 @@ +assertEquals(HandlerParameterAttributeInterface::ROUTE_PARAM, $instance->getType()); + $this->assertEquals('id', $instance->getName()); + } +} diff --git a/tests/Attribute/UploadedFilesTest.php b/tests/Attribute/UploadedFilesTest.php new file mode 100644 index 0000000..c0c29ef --- /dev/null +++ b/tests/Attribute/UploadedFilesTest.php @@ -0,0 +1,18 @@ +assertEquals(HandlerParameterAttributeInterface::UPLOADED_FILES, $instance->getType()); + $this->assertNull($instance->getName()); + } +} diff --git a/tests/CallableWrapperTest.php b/tests/CallableWrapperTest.php index 09bae46..78eab2b 100644 --- a/tests/CallableWrapperTest.php +++ b/tests/CallableWrapperTest.php @@ -5,6 +5,9 @@ namespace Yiisoft\RequestModel\Tests; use Nyholm\Psr7\Response; +use Yiisoft\RequestModel\Attribute\Body; +use Yiisoft\RequestModel\Attribute\Request; +use Yiisoft\RequestModel\Attribute\Route; use Yiisoft\RequestModel\CallableWrapper; use Yiisoft\RequestModel\Tests\Support\SimpleController; use Yiisoft\RequestModel\Tests\Support\SimpleMiddleware; @@ -40,6 +43,29 @@ function (SimpleRequestModel $requestModel) { ); } + public function testCorrectProcessClosureWithAttributes(): void + { + $wrapper = $this->createWrapper( + function (#[Route('id')] int $id, #[Body] array $body, #[Request('foo')] string $foo) { + return new Response(400, ['id' => $id, 'body' => $body, 'foo' => $foo]); + } + ); + + $request = $this->createRequest(['test'])->withAttribute('foo', 'bar'); + + $result = $wrapper->process($request, $this->createRequestHandler()); + + $this->assertEquals(400, $result->getStatusCode()); + $this->assertEquals( + [ + 'id' => [1], + 'body' => ['test'], + 'foo' => ['bar'], + ], + $result->getHeaders() + ); + } + public function testCorrectProcessStaticCallable(): void { $controller = new SimpleController(); @@ -104,8 +130,8 @@ public function testCorrectProcessIfCallbackReturnMiddleware(): void private function createWrapper(callable $callback): CallableWrapper { $container = $this->createContainer(); - $requestModelFactory = $this->createRequestModelFactory($container); + $parametersResolver = $this->createParametersResolver($container); - return new CallableWrapper($container, $requestModelFactory, $callback); + return new CallableWrapper($container, $parametersResolver, $callback); } } diff --git a/tests/MiddlewareFactoryTest.php b/tests/MiddlewareFactoryTest.php index a2b4884..e919b75 100644 --- a/tests/MiddlewareFactoryTest.php +++ b/tests/MiddlewareFactoryTest.php @@ -49,8 +49,8 @@ public function testInvalidMiddleware(): void private function createMiddlewareFactory(): MiddlewareFactory { $container = $this->createContainer(); - $requestModelFactory = $this->createRequestModelFactory($container); - $wrapperFactory = new WrapperFactory($container, $requestModelFactory); + $parametersResolver = $this->createParametersResolver($container); + $wrapperFactory = new WrapperFactory($container, $parametersResolver); return new MiddlewareFactory($container, $wrapperFactory); } } diff --git a/tests/Support/SimpleController.php b/tests/Support/SimpleController.php index 6d1dd27..383f1ed 100644 --- a/tests/Support/SimpleController.php +++ b/tests/Support/SimpleController.php @@ -5,6 +5,11 @@ namespace Yiisoft\RequestModel\Tests\Support; use Nyholm\Psr7\Response; +use Yiisoft\RequestModel\Attribute\Body; +use Yiisoft\RequestModel\Attribute\Query; +use Yiisoft\RequestModel\Attribute\Request; +use Yiisoft\RequestModel\Attribute\Route; +use Yiisoft\RequestModel\Attribute\UploadedFiles; final class SimpleController { @@ -22,4 +27,26 @@ public function anotherAction(SimpleRequestModel $request): Response 'id' => $request->getId(), ]); } + + public function actionUsingAttributes( + #[Route('id')] int $id, + #[Body] $body, + #[UploadedFiles] array $files + ): Response { + return new Response(200, [ + 'id' => $id, + 'body' => $body, + 'countFiles' => count($files), + ]); + } + + public function actionUsingAttributes2( + #[Query('page')] int $page, + #[Request('attribute')] $attribute, + ): Response { + return new Response(200, [ + 'page' => $page, + 'attribute' => $attribute, + ]); + } } diff --git a/tests/Support/TestCase.php b/tests/Support/TestCase.php index 6617b82..3120482 100644 --- a/tests/Support/TestCase.php +++ b/tests/Support/TestCase.php @@ -11,6 +11,7 @@ use Psr\Http\Server\RequestHandlerInterface; use Yiisoft\Http\Method; use Yiisoft\Injector\Injector; +use Yiisoft\RequestModel\HandlerParametersResolver; use Yiisoft\RequestModel\RequestModelFactory; use Yiisoft\Router\CurrentRoute; use Yiisoft\Router\Route; @@ -52,10 +53,15 @@ public function createRequestModelFactory(ContainerInterface $container): Reques return new RequestModelFactory($validator, new Injector($container), $this->getCurrentRoute()); } + public function createParametersResolver(ContainerInterface $container): HandlerParametersResolver + { + return new HandlerParametersResolver($this->createRequestModelFactory($container), $this->getCurrentRoute()); + } + private function getCurrentRoute(): CurrentRoute { $currentRoute = new CurrentRoute(); - $currentRoute->setRouteWithArguments(Route::get('/'), ['id' => 1]); + $currentRoute->setRouteWithArguments(Route::get('/'), ['id' => '1']); return $currentRoute; } } diff --git a/tests/WrapperFactoryTest.php b/tests/WrapperFactoryTest.php index 5fc2f85..b112275 100644 --- a/tests/WrapperFactoryTest.php +++ b/tests/WrapperFactoryTest.php @@ -30,7 +30,7 @@ public function testCorrectCreateCallableWrapper(): void private function createWrapperFactory(): WrapperFactory { $container = $this->createContainer(); - $requestModelFactory = $this->createRequestModelFactory($container); - return new WrapperFactory($container, $requestModelFactory); + $parametersResolver = $this->createParametersResolver($container); + return new WrapperFactory($container, $parametersResolver); } }