├── .phpunit-watcher.yml ├── .styleci.yml ├── CHANGELOG.md ├── LICENSE.md ├── README.md ├── composer.json ├── infection.json.dist ├── psalm.xml ├── rector.php └── src ├── Attribute ├── Body.php ├── BodyResolver.php ├── HandlerParameterAttributeInterface.php ├── HandlerParameterResolverInterface.php ├── NotFoundValue.php ├── Query.php ├── QueryResolver.php ├── Request.php ├── RequestResolver.php ├── Route.php ├── RouteResolver.php ├── UploadedFiles.php ├── UploadedFilesResolver.php └── ValueNotFoundException.php ├── HandlerParametersResolver.php ├── RequestModel.php ├── RequestModelFactory.php ├── RequestModelInterface.php └── RequestValidationException.php /.phpunit-watcher.yml: -------------------------------------------------------------------------------- 1 | watch: 2 | directories: 3 | - src 4 | - tests 5 | fileMask: '*.php' 6 | notifications: 7 | passingTests: false 8 | failingTests: false 9 | phpunit: 10 | binaryPath: vendor/bin/phpunit 11 | timeout: 180 12 | -------------------------------------------------------------------------------- /.styleci.yml: -------------------------------------------------------------------------------- 1 | preset: psr12 2 | risky: true 3 | 4 | version: 8.1 5 | 6 | finder: 7 | exclude: 8 | - docs 9 | - vendor 10 | 11 | enabled: 12 | - alpha_ordered_traits 13 | - array_indentation 14 | - array_push 15 | - combine_consecutive_issets 16 | - combine_consecutive_unsets 17 | - combine_nested_dirname 18 | - declare_strict_types 19 | - dir_constant 20 | - fully_qualified_strict_types 21 | - function_to_constant 22 | - hash_to_slash_comment 23 | - is_null 24 | - logical_operators 25 | - magic_constant_casing 26 | - magic_method_casing 27 | - method_separation 28 | - modernize_types_casting 29 | - native_function_casing 30 | - native_function_type_declaration_casing 31 | - no_alias_functions 32 | - no_empty_comment 33 | - no_empty_phpdoc 34 | - no_empty_statement 35 | - no_extra_block_blank_lines 36 | - no_short_bool_cast 37 | - no_superfluous_elseif 38 | - no_unneeded_control_parentheses 39 | - no_unneeded_curly_braces 40 | - no_unneeded_final_method 41 | - no_unset_cast 42 | - no_unused_imports 43 | - no_unused_lambda_imports 44 | - no_useless_else 45 | - no_useless_return 46 | - normalize_index_brace 47 | - php_unit_dedicate_assert 48 | - php_unit_dedicate_assert_internal_type 49 | - php_unit_expectation 50 | - php_unit_mock 51 | - php_unit_mock_short_will_return 52 | - php_unit_namespaced 53 | - php_unit_no_expectation_annotation 54 | - phpdoc_no_empty_return 55 | - phpdoc_no_useless_inheritdoc 56 | - phpdoc_order 57 | - phpdoc_property 58 | - phpdoc_scalar 59 | - phpdoc_singular_inheritdoc 60 | - phpdoc_trim 61 | - phpdoc_trim_consecutive_blank_line_separation 62 | - phpdoc_type_to_var 63 | - phpdoc_types 64 | - phpdoc_types_order 65 | - print_to_echo 66 | - regular_callable_call 67 | - return_assignment 68 | - self_accessor 69 | - self_static_accessor 70 | - set_type_to_cast 71 | - short_array_syntax 72 | - short_list_syntax 73 | - simplified_if_return 74 | - single_quote 75 | - standardize_not_equals 76 | - ternary_to_null_coalescing 77 | - trailing_comma_in_multiline_array 78 | - unalign_double_arrow 79 | - unalign_equals 80 | - empty_loop_body_braces 81 | - integer_literal_case 82 | - union_type_without_spaces 83 | 84 | disabled: 85 | - function_declaration 86 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Yii Request Model Change Log 2 | 3 | ## 1.0.0 under development 4 | 5 | - Initial release. 6 | -------------------------------------------------------------------------------- /LICENSE.md: -------------------------------------------------------------------------------- 1 | Copyright © 2008 by Yii Software (https://www.yiiframework.com/) 2 | All rights reserved. 3 | 4 | Redistribution and use in source and binary forms, with or without 5 | modification, are permitted provided that the following conditions 6 | are met: 7 | 8 | * Redistributions of source code must retain the above copyright 9 | notice, this list of conditions and the following disclaimer. 10 | * Redistributions in binary form must reproduce the above copyright 11 | notice, this list of conditions and the following disclaimer in 12 | the documentation and/or other materials provided with the 13 | distribution. 14 | * Neither the name of Yii Software nor the names of its 15 | contributors may be used to endorse or promote products derived 16 | from this software without specific prior written permission. 17 | 18 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS 19 | "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT 20 | LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS 21 | FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE 22 | COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, 23 | INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, 24 | BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; 25 | LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER 26 | CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT 27 | LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN 28 | ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE 29 | POSSIBILITY OF SUCH DAMAGE. 30 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 |

2 | 3 | This package is deprecated. Use 4 | Yii Input HTTP instead. 5 | 6 |

7 |

8 | ❌ 9 |

10 | 11 | --- 12 |

13 | 14 | 15 | 16 |

Yii Request Model

17 |
18 |

19 | 20 | [![Latest Stable Version](https://poser.pugx.org/yiisoft/request-model/v/stable.png)](https://packagist.org/packages/yiisoft/request-model) 21 | [![Total Downloads](https://poser.pugx.org/yiisoft/request-model/downloads.png)](https://packagist.org/packages/yiisoft/request-model) 22 | [![Build status](https://github.com/yiisoft/request-model/workflows/build/badge.svg)](https://github.com/yiisoft/request-model/actions?query=workflow%3Abuild) 23 | [![Scrutinizer Code Quality](https://scrutinizer-ci.com/g/yiisoft/request-model/badges/quality-score.png?b=master)](https://scrutinizer-ci.com/g/yiisoft/request-model/?branch=master) 24 | [![Code Coverage](https://scrutinizer-ci.com/g/yiisoft/request-model/badges/coverage.png?b=master)](https://scrutinizer-ci.com/g/yiisoft/request-model/?branch=master) 25 | [![Mutation testing badge](https://img.shields.io/endpoint?style=flat&url=https%3A%2F%2Fbadge-api.stryker-mutator.io%2Fgithub.com%2Fyiisoft%2Frequest-model%2Fmaster)](https://dashboard.stryker-mutator.io/reports/github.com/yiisoft/request-model/master) 26 | [![static analysis](https://github.com/yiisoft/request-model/workflows/static%20analysis/badge.svg)](https://github.com/yiisoft/request-model/actions?query=workflow%3A%22static+analysis%22) 27 | [![type-coverage](https://shepherd.dev/github/yiisoft/request-model/coverage.svg)](https://shepherd.dev/github/yiisoft/request-model) 28 | 29 | Request model simplifies working with request data. It allows you to decorate data for easy retrieval and automatically 30 | validate it when needed. 31 | 32 | ## Requirements 33 | 34 | - PHP 8.0 or higher. 35 | 36 | ## Installation 37 | 38 | The package could be installed with composer: 39 | 40 | ``` 41 | composer require yiisoft/request-model 42 | ``` 43 | 44 | According to [`yiisoft/middleware-dispatcher`](https://github.com/yiisoft/middleware-dispatcher) docs, you need to set 45 | the implementation of `ParametersResolverInterface` to `HandlerParametersResolver` via container or pass directly. 46 | 47 | ## General usage 48 | 49 | 50 | A simple version of the request model looks like the following: 51 | 52 | ```php 53 | use Yiisoft\RequestModel\RequestModel; 54 | use Yiisoft\Validator\RulesProviderInterface; 55 | use Yiisoft\Validator\Rule\Email; 56 | use Yiisoft\Validator\Rule\Required; 57 | 58 | final class AuthRequest extends RequestModel implements RulesProviderInterface 59 | { 60 | public function getLogin(): string 61 | { 62 | return (string)$this->getAttributeValue('body.login'); 63 | } 64 | 65 | public function getPassword(): string 66 | { 67 | return (string)$this->getAttributeValue('body.password'); 68 | } 69 | 70 | public function getRules(): array 71 | { 72 | return [ 73 | 'body.login' => [ 74 | new Required(), 75 | new Email(), 76 | ], 77 | 'body.password' => [ 78 | new Required(), 79 | ] 80 | ]; 81 | } 82 | } 83 | ``` 84 | 85 | Route: 86 | 87 | ```php 88 | Route::post('/test') 89 | ->action([SimpleController::class, 'action']) 90 | ->name('site/test') 91 | ``` 92 | 93 | Usage in controller: 94 | 95 | ```php 96 | use Psr\Http\Message\ResponseInterface; 97 | 98 | final class SimpleController 99 | { 100 | public function action(AuthRequest $request): ResponseInterface 101 | { 102 | echo $request->getLogin(); 103 | ... 104 | } 105 | } 106 | ``` 107 | 108 | If the data does not pass validation, `RequestValidationException` will be thrown. 109 | If you need to handle an exception and, for example, send a response, you can intercept its middleware. 110 | 111 | For example: 112 | 113 | ```php 114 | final class ExceptionMiddleware implements MiddlewareInterface 115 | { 116 | public function process(ServerRequestInterface $request, RequestHandlerInterface $handler): ResponseInterface 117 | { 118 | try { 119 | return $handler->handle($request); 120 | } catch (RequestValidationException $e) { 121 | return new Response(400, [], $e->getFirstError()); 122 | } 123 | } 124 | } 125 | ``` 126 | 127 | You can use the request model without validation. To do this, you need to remove the `ValidatableModelInterface`. 128 | In this case, the data will be included into the model, but will not be validated. For example: 129 | 130 | ```php 131 | final class ViewPostRequest extends RequestModel 132 | { 133 | public function getId(): int 134 | { 135 | return (int)$this->getAttributeValue('router.id'); 136 | } 137 | } 138 | ``` 139 | 140 | Inside the request model class, data is available using the following keys: 141 | 142 | | key | source | 143 | |------------|-------------------------------| 144 | | query | $request->getQueryParams() | 145 | | body | $request->getParsedBody() | 146 | | attributes | $request->getAttributes() | 147 | | headers | $request->getHeaders() | 148 | | files | $request->getUploadedFiles() | 149 | | cookie | $request->getCookieParams() | 150 | | router | $currentRoute->getArguments() | 151 | 152 | This data can be obtained as follows 153 | 154 | ```php 155 | $this->requestData['router']['id']; 156 | ``` 157 | 158 | or through the methods 159 | 160 | ```php 161 | $this->hasAttribute('body.user_id'); 162 | $this->getAttributeValue('body.user_id'); 163 | ``` 164 | 165 | #### Attributes 166 | 167 | You can use attributes in an action handler to get data from a request: 168 | 169 | ```php 170 | use Psr\Http\Message\ResponseInterface; 171 | use Yiisoft\RequestModel\Attribute\Request; 172 | use Yiisoft\RequestModel\Attribute\Route; 173 | 174 | final class SimpleController 175 | { 176 | public function action(#[Route('id')] int $id, #[Request('foo')] $attribute,): ResponseInterface 177 | { 178 | echo $id; 179 | //... 180 | } 181 | } 182 | ``` 183 | 184 | Attributes are also supported in closure actions. 185 | 186 | There are several attributes out of the box: 187 | 188 | | Name | Source | 189 | |---------------|---------------------------| 190 | | Body | Parsed body of request | 191 | | Query | Query parameter of URI | 192 | | Request | Attribute of request | 193 | | Route | Argument of current route | 194 | | UploadedFiles | Uploaded files of request | 195 | 196 | ### Unit testing 197 | 198 | The package is tested with [PHPUnit](https://phpunit.de/). To run tests: 199 | 200 | ```shell 201 | ./vendor/bin/phpunit 202 | ``` 203 | 204 | ### Mutation testing 205 | 206 | The package tests are checked with [Infection](https://infection.github.io/) mutation framework. To run it: 207 | 208 | ```shell 209 | ./vendor/bin/infection 210 | ``` 211 | 212 | ### Static analysis 213 | 214 | The code is statically analyzed with [Psalm](https://psalm.dev/). To run static analysis: 215 | 216 | ```shell 217 | ./vendor/bin/psalm 218 | ``` 219 | 220 | ### Support the project 221 | 222 | [![Open Collective](https://img.shields.io/badge/Open%20Collective-sponsor-7eadf1?logo=open%20collective&logoColor=7eadf1&labelColor=555555)](https://opencollective.com/yiisoft) 223 | 224 | ### Follow updates 225 | 226 | [![Official website](https://img.shields.io/badge/Powered_by-Yii_Framework-green.svg?style=flat)](https://www.yiiframework.com/) 227 | [![Twitter](https://img.shields.io/badge/twitter-follow-1DA1F2?logo=twitter&logoColor=1DA1F2&labelColor=555555?style=flat)](https://twitter.com/yiiframework) 228 | [![Telegram](https://img.shields.io/badge/telegram-join-1DA1F2?style=flat&logo=telegram)](https://t.me/yii3en) 229 | [![Facebook](https://img.shields.io/badge/facebook-join-1DA1F2?style=flat&logo=facebook&logoColor=ffffff)](https://www.facebook.com/groups/yiitalk) 230 | [![Slack](https://img.shields.io/badge/slack-join-1DA1F2?style=flat&logo=slack)](https://yiiframework.com/go/slack) 231 | 232 | ## License 233 | 234 | The Yii Request Model is free software. It is released under the terms of the BSD License. 235 | Please see [`LICENSE`](./LICENSE.md) for more information. 236 | 237 | Maintained by [Yii Software](https://www.yiiframework.com/). 238 | -------------------------------------------------------------------------------- /composer.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "yiisoft/request-model", 3 | "type": "library", 4 | "description": "Yii Framework Request Model", 5 | "keywords": [ 6 | "request", 7 | "validation" 8 | ], 9 | "homepage": "https://www.yiiframework.com/", 10 | "license": "BSD-3-Clause", 11 | "support": { 12 | "issues": "https://github.com/yiisoft/request-model/issues?state=open", 13 | "forum": "https://www.yiiframework.com/forum/", 14 | "wiki": "https://www.yiiframework.com/wiki/", 15 | "irc": "irc://irc.freenode.net/yii", 16 | "chat": "https://t.me/yii3en", 17 | "source": "https://github.com/yiisoft/request-model" 18 | }, 19 | "require": { 20 | "php": "^8.0", 21 | "psr/container": "^1.0|^2.0", 22 | "psr/http-message": "^1.0", 23 | "psr/http-server-handler": "^1.0", 24 | "psr/http-server-middleware": "^1.0", 25 | "yiisoft/arrays": "^2.0|^3.0", 26 | "yiisoft/injector": "^1.0", 27 | "yiisoft/middleware-dispatcher": "^5.0", 28 | "yiisoft/router": "^3.0", 29 | "yiisoft/validator": "^1.0" 30 | }, 31 | "require-dev": { 32 | "maglnet/composer-require-checker": "^4.2", 33 | "nyholm/psr7": "^1.4", 34 | "phpunit/phpunit": "^9.5", 35 | "rector/rector": "^0.18.0", 36 | "roave/infection-static-analysis-plugin": "^1.16", 37 | "spatie/phpunit-watcher": "^1.23", 38 | "vimeo/psalm": "^4.30|^5.7", 39 | "yiisoft/dummy-provider": "^1.0", 40 | "yiisoft/http": "^1.2", 41 | "yiisoft/test-support": "^3.0", 42 | "yiisoft/translator": "^3.0" 43 | }, 44 | "autoload": { 45 | "psr-4": { 46 | "Yiisoft\\RequestModel\\": "src" 47 | } 48 | }, 49 | "autoload-dev": { 50 | "psr-4": { 51 | "Yiisoft\\RequestModel\\Tests\\": "tests" 52 | } 53 | }, 54 | "extra": { 55 | "branch-alias": { 56 | "dev-master": "1.0.x-dev" 57 | } 58 | }, 59 | "config": { 60 | "sort-packages": true, 61 | "allow-plugins": { 62 | "infection/extension-installer": true, 63 | "composer/package-versions-deprecated": true 64 | } 65 | }, 66 | "scripts": { 67 | "test": "phpunit --testdox --no-interaction", 68 | "test-watch": "phpunit-watcher watch" 69 | } 70 | } 71 | -------------------------------------------------------------------------------- /infection.json.dist: -------------------------------------------------------------------------------- 1 | { 2 | "source": { 3 | "directories": [ 4 | "src" 5 | ] 6 | }, 7 | "logs": { 8 | "text": "php:\/\/stderr", 9 | "stryker": { 10 | "report": "master" 11 | } 12 | }, 13 | "mutators": { 14 | "@default": true 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /psalm.xml: -------------------------------------------------------------------------------- 1 | 2 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | -------------------------------------------------------------------------------- /rector.php: -------------------------------------------------------------------------------- 1 | paths([ 11 | __DIR__ . '/src', 12 | __DIR__ . '/tests', 13 | ]); 14 | 15 | // register a single rule 16 | $rectorConfig->rule(InlineConstructorDefaultToPropertyRector::class); 17 | 18 | // define sets of rules 19 | $rectorConfig->sets([ 20 | LevelSetList::UP_TO_PHP_80, 21 | ]); 22 | }; 23 | -------------------------------------------------------------------------------- /src/Attribute/Body.php: -------------------------------------------------------------------------------- 1 | name; 19 | } 20 | 21 | public function getResolverClassName(): string 22 | { 23 | return BodyResolver::class; 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /src/Attribute/BodyResolver.php: -------------------------------------------------------------------------------- 1 | getParsedBody(); 18 | 19 | if ($attribute->getName() !== null) { 20 | if (!is_array($parsedBody)) { 21 | throw new ValueNotFoundException(); 22 | } 23 | 24 | return $parsedBody[$attribute->getName()] ?? throw new ValueNotFoundException(); 25 | } 26 | 27 | return $parsedBody; 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /src/Attribute/HandlerParameterAttributeInterface.php: -------------------------------------------------------------------------------- 1 | name; 19 | } 20 | 21 | public function getResolverClassName(): string 22 | { 23 | return QueryResolver::class; 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /src/Attribute/QueryResolver.php: -------------------------------------------------------------------------------- 1 | getQueryParams(); 18 | 19 | if ($attribute->getName() !== null) { 20 | return $queryParams[$attribute->getName()] ?? throw new ValueNotFoundException(); 21 | } 22 | 23 | return $queryParams; 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /src/Attribute/Request.php: -------------------------------------------------------------------------------- 1 | name; 19 | } 20 | 21 | public function getResolverClassName(): string 22 | { 23 | return RequestResolver::class; 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /src/Attribute/RequestResolver.php: -------------------------------------------------------------------------------- 1 | getAttribute($attribute->getName(), $notFoundValue); 21 | if ($result === $notFoundValue) { 22 | throw new ValueNotFoundException(); 23 | } 24 | 25 | return $result; 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /src/Attribute/Route.php: -------------------------------------------------------------------------------- 1 | name; 19 | } 20 | 21 | public function getResolverClassName(): string 22 | { 23 | return RouteResolver::class; 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /src/Attribute/RouteResolver.php: -------------------------------------------------------------------------------- 1 | currentRoute->getArgument($attribute->getName()) ?? throw new ValueNotFoundException(); 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /src/Attribute/UploadedFiles.php: -------------------------------------------------------------------------------- 1 | getUploadedFiles(); 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /src/Attribute/ValueNotFoundException.php: -------------------------------------------------------------------------------- 1 | getAttributeParams($parameters, $request), 33 | $this->factory->createInstances($request, $parameters) 34 | ); 35 | } 36 | 37 | /** 38 | * @param \ReflectionParameter[] $parameters 39 | * 40 | * @throws ContainerExceptionInterface 41 | * @throws NotFoundExceptionInterface 42 | */ 43 | private function getAttributeParams(array $parameters, ServerRequestInterface $request): array 44 | { 45 | $actionParameters = []; 46 | foreach ($parameters as $parameter) { 47 | $attributes = $parameter->getAttributes( 48 | HandlerParameterAttributeInterface::class, 49 | \ReflectionAttribute::IS_INSTANCEOF 50 | ); 51 | foreach ($attributes as $attribute) { 52 | /** @var HandlerParameterAttributeInterface $attributeInstance */ 53 | $attributeInstance = $attribute->newInstance(); 54 | $resolver = $this->container->get($attributeInstance->getResolverClassName()); 55 | if (!($resolver instanceof HandlerParameterResolverInterface)) { 56 | throw new \RuntimeException( 57 | sprintf( 58 | 'Resolver "%s" should implement %s.', 59 | $resolver::class, 60 | HandlerParameterResolverInterface::class 61 | ) 62 | ); 63 | } 64 | 65 | try { 66 | /** @var mixed */ 67 | $actionParameters[$parameter->getName()] = $resolver->resolve($attributeInstance, $request); 68 | } catch (ValueNotFoundException) { 69 | } 70 | } 71 | } 72 | return $actionParameters; 73 | } 74 | } 75 | -------------------------------------------------------------------------------- /src/RequestModel.php: -------------------------------------------------------------------------------- 1 | requestData = $requestData; 17 | } 18 | 19 | public function getAttributeValue(string $attribute, mixed $default = null): mixed 20 | { 21 | return ArrayHelper::getValueByPath($this->requestData, $attribute, $default, $this->attributeDelimiter); 22 | } 23 | 24 | public function hasAttribute(string $attribute): bool 25 | { 26 | return ArrayHelper::pathExists($this->requestData, $attribute, true, $this->attributeDelimiter); 27 | } 28 | 29 | public function getRequestData(): array 30 | { 31 | return $this->requestData; 32 | } 33 | 34 | public function getData(): array 35 | { 36 | return $this->requestData; 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /src/RequestModelFactory.php: -------------------------------------------------------------------------------- 1 | getModelRequestClasses($handlerParams) as $modelClass) { 35 | $requestModelInstances[] = $this->processModel($request, $this->injector->make($modelClass)); 36 | } 37 | 38 | return $requestModelInstances; 39 | } 40 | 41 | private function processModel(ServerRequestInterface $request, RequestModelInterface $model): RequestModelInterface 42 | { 43 | $requestData = $this->getRequestData($request); 44 | $model->setRequestData($requestData); 45 | if ($model instanceof RulesProviderInterface) { 46 | $result = $this->validator->validate($model); 47 | if (!$result->isValid()) { 48 | $errorMessagesIndexedByAttribute = $result->getErrorMessagesIndexedByAttribute(); 49 | throw new RequestValidationException($errorMessagesIndexedByAttribute); 50 | } 51 | } 52 | 53 | return $model; 54 | } 55 | 56 | /** 57 | * @param ReflectionParameter[] $handlerParams 58 | * 59 | * @psalm-return list> 60 | */ 61 | private function getModelRequestClasses(array $handlerParams): array 62 | { 63 | $modelClasses = []; 64 | foreach ($handlerParams as $param) { 65 | $type = $param->getType(); 66 | if ( 67 | !$type instanceof ReflectionNamedType 68 | || $type->isBuiltin() 69 | ) { 70 | continue; 71 | } 72 | 73 | $className = $type->getName(); 74 | if (!$this->isInstantiableRequestModel($className)) { 75 | continue; 76 | } 77 | 78 | $modelClasses[] = $className; 79 | } 80 | 81 | return $modelClasses; 82 | } 83 | 84 | /** 85 | * @psalm-param class-string $className 86 | * @psalm-assert-if-true class-string $className 87 | */ 88 | private function isInstantiableRequestModel(string $className): bool 89 | { 90 | $reflectionClass = new ReflectionClass($className); 91 | 92 | return $reflectionClass->isInstantiable() 93 | && $reflectionClass->implementsInterface(RequestModelInterface::class); 94 | } 95 | 96 | private function getRequestData(ServerRequestInterface $request): array 97 | { 98 | return [ 99 | 'query' => $request->getQueryParams(), 100 | 'body' => $request->getParsedBody(), 101 | 'attributes' => $request->getAttributes(), 102 | 'headers' => $request->getHeaders(), 103 | 'files' => $request->getUploadedFiles(), 104 | 'cookie' => $request->getCookieParams(), 105 | 'router' => $this->currentRoute->getArguments(), 106 | ]; 107 | } 108 | } 109 | -------------------------------------------------------------------------------- /src/RequestModelInterface.php: -------------------------------------------------------------------------------- 1 | $errors 17 | */ 18 | public function __construct( 19 | private array $errors 20 | ) { 21 | parent::__construct(self::MESSAGE); 22 | } 23 | 24 | /** 25 | * @psalm-return array 26 | */ 27 | public function getErrors(): array 28 | { 29 | return $this->errors; 30 | } 31 | 32 | /** 33 | * @psalm-return array 34 | */ 35 | public function getFirstErrors(): array 36 | { 37 | if (empty($this->errors)) { 38 | return []; 39 | } 40 | 41 | $result = []; 42 | foreach ($this->errors as $name => $errors) { 43 | if (!empty($errors)) { 44 | $result[$name] = reset($errors); 45 | } 46 | } 47 | 48 | return $result; 49 | } 50 | 51 | public function getFirstError(): ?string 52 | { 53 | $errors = $this->getFirstErrors(); 54 | 55 | return $errors === [] ? null : reset($errors); 56 | } 57 | } 58 | --------------------------------------------------------------------------------