├── .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 | [](https://packagist.org/packages/yiisoft/request-model)
21 | [](https://packagist.org/packages/yiisoft/request-model)
22 | [](https://github.com/yiisoft/request-model/actions?query=workflow%3Abuild)
23 | [](https://scrutinizer-ci.com/g/yiisoft/request-model/?branch=master)
24 | [](https://scrutinizer-ci.com/g/yiisoft/request-model/?branch=master)
25 | [](https://dashboard.stryker-mutator.io/reports/github.com/yiisoft/request-model/master)
26 | [](https://github.com/yiisoft/request-model/actions?query=workflow%3A%22static+analysis%22)
27 | [](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 | [](https://opencollective.com/yiisoft)
223 |
224 | ### Follow updates
225 |
226 | [](https://www.yiiframework.com/)
227 | [](https://twitter.com/yiiframework)
228 | [](https://t.me/yii3en)
229 | [](https://www.facebook.com/groups/yiitalk)
230 | [](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 |
--------------------------------------------------------------------------------