├── CHANGELOG.md
├── LICENSE.md
├── README.md
├── composer.json
├── config
└── di.php
├── rector.php
└── src
├── Attribute
├── Hint.php
├── Placeholder.php
└── Safe.php
├── Exception
├── PropertyNotSupportNestedValuesException.php
├── StaticObjectPropertyException.php
├── UndefinedArrayElementException.php
├── UndefinedObjectPropertyException.php
└── ValueNotFoundException.php
├── Field.php
├── FieldFactory.php
├── FormHydrator.php
├── FormModel.php
├── FormModelInputData.php
├── FormModelInterface.php
├── NonArrayTypeCaster.php
└── ValidationRulesEnricher.php
/CHANGELOG.md:
--------------------------------------------------------------------------------
1 | # Yii Form Model Change Log
2 |
3 | ## 1.0.2 under development
4 |
5 | - Chg #75: Change PHP constraint in `composer.json` to `8.1 - 8.4` (@vjik)
6 |
7 | ## 1.0.1 September 13, 2024
8 |
9 | - Bug #67: Use both properties with rules from PHP attributes and provided via `getRules()` method at the same time
10 | to mark as ready to populate (@vjik)
11 |
12 | ## 1.0.0 August 27, 2024
13 |
14 | - Initial release.
15 |
--------------------------------------------------------------------------------
/LICENSE.md:
--------------------------------------------------------------------------------
1 | Copyright © 2008 by Yii Software ()
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 |
4 |
5 |
Yii Form Model
6 |
7 |
8 |
9 | [](https://packagist.org/packages/yiisoft/form-model)
10 | [](https://packagist.org/packages/yiisoft/form-model)
11 | [](https://github.com/yiisoft/form-model/actions/workflows/build.yml)
12 | [](https://codecov.io/gh/yiisoft/form-model)
13 | [](https://dashboard.stryker-mutator.io/reports/github.com/yiisoft/form-model/master)
14 | [](https://github.com/yiisoft/form-model/actions?query=workflow%3A%22static+analysis%22)
15 | [](https://shepherd.dev/github/yiisoft/form-model)
16 | [](https://shepherd.dev/github/yiisoft/form-model)
17 |
18 | The package provides a base for form models and helps to fill them with data, validate them and display them.
19 |
20 | ## Requirements
21 |
22 | - PHP 8.1 or higher.
23 | - `mbstring` PHP extension.
24 |
25 | ## Installation
26 |
27 | The package could be installed with [Composer](https://getcomposer.org):
28 |
29 | ```shell
30 | composer require yiisoft/form-model
31 | ```
32 |
33 | ## General usage
34 |
35 | Define a [form model](docs/guide/en/form-model.md):
36 |
37 | ```php
38 | use Yiisoft\FormModel\Attribute\Safe;
39 | use Yiisoft\FormModel\FormModel;
40 | use Yiisoft\Validator\Rule\Email;
41 | use Yiisoft\Validator\Rule\Length;
42 | use Yiisoft\Validator\Rule\Required;
43 |
44 | final class LoginForm extends FormModel
45 | {
46 | #[Label('Your login')]
47 | #[Required]
48 | #[Length(min: 4, max: 40, skipOnEmpty: true)]
49 | #[Email(skipOnEmpty: true)]
50 | private ?string $login = null;
51 |
52 | #[Label('Your password')]
53 | #[Required]
54 | #[Length(min: 8, skipOnEmpty: true)]
55 | private ?string $password = null;
56 |
57 | #[Label('Remember me for 1 week')]
58 | #[Safe]
59 | private bool $rememberMe = false;
60 | }
61 | ```
62 |
63 | Fill it with data and validate using [form hydrator](docs/guide/en/form-hydrator.md):
64 |
65 | ```php
66 | use Psr\Http\Message\RequestInterface;
67 | use Yiisoft\FormModel\FormHydrator;
68 | use Yiisoft\FormModel\FormModel;
69 |
70 | final class AuthController
71 | {
72 | public function login(RequestInterface $request, FormHydrator $formHydrator): ResponseInterface
73 | {
74 | $formModel = new LoginForm();
75 | $errors = [];
76 | if ($formHydrator->populateFromPostAndValidate($formModel, $request)) {
77 | $errors = $formModel->getValidationResult()->getErrorMessagesIndexedByProperty();
78 | }
79 |
80 | // You can pass $formModel and $errors to the view now.
81 | }
82 | }
83 | ```
84 |
85 | Display it using [fields](docs/guide/en/displaying-fields.md) in the view:
86 |
87 | ```php
88 | use Yiisoft\FormModel\Field;
89 | use Yiisoft\FormModel\FormModel;
90 |
91 | echo Field::text($formModel, 'login');
92 | echo Field::password($formModel, 'password');
93 | echo Field::checkbox($formModel, 'rememberMe');
94 |
95 | // ...
96 | ```
97 |
98 | ## Documentation
99 |
100 | - [Guide](docs/guide/en/README.md)
101 | - [Internals](docs/internals.md)
102 |
103 | If you need help or have a question, the [Yii Forum](https://forum.yiiframework.com/c/yii-3-0/63) is a good place for
104 | that. You may also check out other [Yii Community Resources](https://www.yiiframework.com/community).
105 |
106 | ## License
107 |
108 | The Yii Form Model is free software. It is released under the terms of the BSD License.
109 | Please see [`LICENSE`](./LICENSE.md) for more information.
110 |
111 | Maintained by [Yii Software](https://www.yiiframework.com/).
112 |
113 | ## Support the project
114 |
115 | [](https://opencollective.com/yiisoft)
116 |
117 | ## Follow updates
118 |
119 | [](https://www.yiiframework.com/)
120 | [](https://twitter.com/yiiframework)
121 | [](https://t.me/yii3en)
122 | [](https://www.facebook.com/groups/yiitalk)
123 | [](https://yiiframework.com/go/slack)
124 |
--------------------------------------------------------------------------------
/composer.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "yiisoft/form-model",
3 | "type": "library",
4 | "description": "Provides a base for form models and helps to fill, validate and display them.",
5 | "keywords": [
6 | "form",
7 | "model"
8 | ],
9 | "homepage": "https://www.yiiframework.com/",
10 | "license": "BSD-3-Clause",
11 | "support": {
12 | "issues": "https://github.com/yiisoft/form-model/issues?state=open",
13 | "source": "https://github.com/yiisoft/form-model",
14 | "forum": "https://www.yiiframework.com/forum/",
15 | "wiki": "https://www.yiiframework.com/wiki/",
16 | "irc": "ircs://irc.libera.chat:6697/yii",
17 | "chat": "https://t.me/yii3en"
18 | },
19 | "funding": [
20 | {
21 | "type": "opencollective",
22 | "url": "https://opencollective.com/yiisoft"
23 | },
24 | {
25 | "type": "github",
26 | "url": "https://github.com/sponsors/yiisoft"
27 | }
28 | ],
29 | "require": {
30 | "php": "8.1 - 8.4",
31 | "ext-mbstring": "*",
32 | "psr/http-message": "^1.0 || ^2.0",
33 | "yiisoft/form": "^1.0",
34 | "yiisoft/html": "^3.3",
35 | "yiisoft/hydrator": "^1.3",
36 | "yiisoft/strings": "^2.3",
37 | "yiisoft/validator": "^2.1"
38 | },
39 | "require-dev": {
40 | "httpsoft/http-message": "^1.1.6",
41 | "maglnet/composer-require-checker": "^4.7.1",
42 | "phpunit/phpunit": "^10.5.45",
43 | "rector/rector": "^2.0.11",
44 | "roave/infection-static-analysis-plugin": "^1.35",
45 | "spatie/phpunit-watcher": "^1.24",
46 | "vimeo/psalm": "^5.26.1 || ^6.10",
47 | "yiisoft/di": "^1.3"
48 | },
49 | "autoload": {
50 | "psr-4": {
51 | "Yiisoft\\FormModel\\": "src"
52 | }
53 | },
54 | "autoload-dev": {
55 | "psr-4": {
56 | "Yiisoft\\FormModel\\Tests\\": "tests"
57 | }
58 | },
59 | "extra": {
60 | "config-plugin-options": {
61 | "source-directory": "config"
62 | },
63 | "config-plugin": {
64 | "di": "di.php"
65 | }
66 | },
67 | "config": {
68 | "sort-packages": true,
69 | "allow-plugins": {
70 | "infection/extension-installer": true,
71 | "composer/package-versions-deprecated": true
72 | }
73 | },
74 | "scripts": {
75 | "test": "phpunit --testdox --no-interaction",
76 | "test-watch": "phpunit-watcher watch"
77 | }
78 | }
79 |
--------------------------------------------------------------------------------
/config/di.php:
--------------------------------------------------------------------------------
1 | [
16 | '__construct()' => [
17 | 'hydrator' => DynamicReference::to([
18 | 'class' => Hydrator::class,
19 | '__construct()' => [
20 | 'typeCaster' => new CompositeTypeCaster(
21 | new NullTypeCaster(emptyString: true),
22 | new PhpNativeTypeCaster(),
23 | new NonArrayTypeCaster(),
24 | new HydratorTypeCaster(),
25 | ),
26 | ],
27 | ]),
28 | ],
29 | ],
30 | ];
31 |
--------------------------------------------------------------------------------
/rector.php:
--------------------------------------------------------------------------------
1 | paths([
13 | __DIR__ . '/src',
14 | __DIR__ . '/tests',
15 | ]);
16 |
17 | // register a single rule
18 | $rectorConfig->rule(InlineConstructorDefaultToPropertyRector::class);
19 |
20 | // define sets of rules
21 | $rectorConfig->sets([
22 | LevelSetList::UP_TO_PHP_81,
23 | ]);
24 |
25 | $rectorConfig->skip([
26 | ClosureToArrowFunctionRector::class,
27 | ReadOnlyPropertyRector::class,
28 | ]);
29 | };
30 |
--------------------------------------------------------------------------------
/src/Attribute/Hint.php:
--------------------------------------------------------------------------------
1 | hint;
20 | }
21 | }
22 |
--------------------------------------------------------------------------------
/src/Attribute/Placeholder.php:
--------------------------------------------------------------------------------
1 | placeholder;
20 | }
21 | }
22 |
--------------------------------------------------------------------------------
/src/Attribute/Safe.php:
--------------------------------------------------------------------------------
1 | value;
25 | }
26 | }
27 |
--------------------------------------------------------------------------------
/src/Exception/StaticObjectPropertyException.php:
--------------------------------------------------------------------------------
1 | content($content);
62 | }
63 |
64 | return $field;
65 | }
66 |
67 | /**
68 | * Create a button group field.
69 | *
70 | * @param array $config Widget config.
71 | * @param string|null $theme Theme to use. If not specified, default theme is used.
72 | * @return ButtonGroup
73 | */
74 | final public static function buttonGroup(array $config = [], ?string $theme = null): ButtonGroup
75 | {
76 | return ButtonGroup::widget(config: $config, theme: $theme ?? static::DEFAULT_THEME);
77 | }
78 |
79 | /**
80 | * Create a checkbox field.
81 | *
82 | * @param FormModelInterface $formModel Model to take value from.
83 | * @param string $property Model property name to take value from.
84 | * @param array $config Widget config.
85 | * @param string|null $theme Theme to use. If not specified, default theme is used.
86 | * @return Checkbox
87 | */
88 | final public static function checkbox(
89 | FormModelInterface $formModel,
90 | string $property,
91 | array $config = [],
92 | ?string $theme = null,
93 | ): Checkbox {
94 | return Checkbox::widget(config: $config, theme: $theme ?? static::DEFAULT_THEME)
95 | ->inputData(new FormModelInputData($formModel, $property));
96 | }
97 |
98 | /**
99 | * Create checkboxes list field.
100 | *
101 | * @param FormModelInterface $formModel Model to take value from.
102 | * @param string $property Model property name to take value from.
103 | * @param array $config Widget config.
104 | * @param string|null $theme Theme to use. If not specified, default theme is used.
105 | * @return CheckboxList
106 | */
107 | final public static function checkboxList(
108 | FormModelInterface $formModel,
109 | string $property,
110 | array $config = [],
111 | ?string $theme = null,
112 | ): CheckboxList {
113 | return CheckboxList::widget(config: $config, theme: $theme ?? static::DEFAULT_THEME)
114 | ->inputData(new FormModelInputData($formModel, $property));
115 | }
116 |
117 | /**
118 | * Create a date field.
119 | *
120 | * @param FormModelInterface $formModel Model to take value from.
121 | * @param string $property Model property name to take value from.
122 | * @param array $config Widget config.
123 | * @param string|null $theme Theme to use. If not specified, default theme is used.
124 | * @return Date
125 | */
126 | final public static function date(
127 | FormModelInterface $formModel,
128 | string $property,
129 | array $config = [],
130 | ?string $theme = null,
131 | ): Date {
132 | return Date::widget(config: $config, theme: $theme ?? static::DEFAULT_THEME)
133 | ->inputData(new FormModelInputData($formModel, $property));
134 | }
135 |
136 | /**
137 | * Create a local date and time field.
138 | *
139 | * @param FormModelInterface $formModel Model to take value from.
140 | * @param string $property Model property name to take value from.
141 | * @param array $config Widget config.
142 | * @param string|null $theme Theme to use. If not specified, default theme is used.
143 | * @return DateTimeLocal
144 | */
145 | final public static function dateTimeLocal(
146 | FormModelInterface $formModel,
147 | string $property,
148 | array $config = [],
149 | ?string $theme = null,
150 | ): DateTimeLocal {
151 | return DateTimeLocal::widget(config: $config, theme: $theme ?? static::DEFAULT_THEME)
152 | ->inputData(new FormModelInputData($formModel, $property));
153 | }
154 |
155 | /**
156 | * Create an email field.
157 | *
158 | * @param FormModelInterface $formModel Model to take value from.
159 | * @param string $property Model property name to take value from.
160 | * @param array $config Widget config.
161 | * @param string|null $theme Theme to use. If not specified, default theme is used.
162 | * @return Email
163 | */
164 | final public static function email(
165 | FormModelInterface $formModel,
166 | string $property,
167 | array $config = [],
168 | ?string $theme = null,
169 | ): Email {
170 | return Email::widget(config: $config, theme: $theme ?? static::DEFAULT_THEME)
171 | ->inputData(new FormModelInputData($formModel, $property));
172 | }
173 |
174 | /**
175 | * Create errors summary field.
176 | *
177 | * @param FormModelInterface|null $formModel Model to take errors from.
178 | * @param array $config Widget config.
179 | * @param string|null $theme Theme to use. If not specified, default theme is used.
180 | * @return ErrorSummary
181 | */
182 | final public static function errorSummary(
183 | ?FormModelInterface $formModel = null,
184 | array $config = [],
185 | ?string $theme = null,
186 | ): ErrorSummary {
187 | $widget = ErrorSummary::widget(config: $config, theme: $theme ?? static::DEFAULT_THEME);
188 | if ($formModel !== null) {
189 | $widget = $widget->errors(
190 | $formModel->isValidated()
191 | ? $formModel->getValidationResult()->getErrorMessagesIndexedByProperty()
192 | : []
193 | );
194 | }
195 | return $widget;
196 | }
197 |
198 | /**
199 | * Create a fieldset.
200 | *
201 | * @param array $config Widget config.
202 | * @param string|null $theme Theme to use. If not specified, default theme is used.
203 | * @return Fieldset
204 | */
205 | final public static function fieldset(array $config = [], ?string $theme = null): Fieldset
206 | {
207 | return Fieldset::widget(config: $config, theme: $theme ?? static::DEFAULT_THEME);
208 | }
209 |
210 | /**
211 | * Create a file upload field.
212 | *
213 | * @param FormModelInterface $formModel Model to take value from.
214 | * @param string $property Model property name to take value from.
215 | * @param array $config Widget config.
216 | * @param string|null $theme Theme to use. If not specified, default theme is used.
217 | * @return File
218 | */
219 | final public static function file(
220 | FormModelInterface $formModel,
221 | string $property,
222 | array $config = [],
223 | ?string $theme = null,
224 | ): File {
225 | return File::widget(config: $config, theme: $theme ?? static::DEFAULT_THEME)
226 | ->inputData(new FormModelInputData($formModel, $property));
227 | }
228 |
229 | /**
230 | * Create a hidden field.
231 | *
232 | * @param FormModelInterface $formModel Model to take value from.
233 | * @param string $property Model property name to take value from.
234 | * @param array $config Widget config.
235 | * @param string|null $theme Theme to use. If not specified, default theme is used.
236 | * @return Hidden
237 | */
238 | final public static function hidden(
239 | FormModelInterface $formModel,
240 | string $property,
241 | array $config = [],
242 | ?string $theme = null,
243 | ): Hidden {
244 | return Hidden::widget(config: $config, theme: $theme ?? static::DEFAULT_THEME)
245 | ->inputData(new FormModelInputData($formModel, $property));
246 | }
247 |
248 | /**
249 | * Create an image.
250 | *
251 | * @param string|null $url "src" of the image.
252 | * @param array $config Widget config.
253 | * @param string|null $theme Theme to use. If not specified, default theme is used.
254 | * @return Image
255 | */
256 | final public static function image(?string $url = null, array $config = [], ?string $theme = null): Image
257 | {
258 | $field = Image::widget(config: $config, theme: $theme ?? static::DEFAULT_THEME);
259 |
260 | if ($url !== null) {
261 | $field = $field->src($url);
262 | }
263 |
264 | return $field;
265 | }
266 |
267 | /**
268 | * Create a number field.
269 | *
270 | * @param FormModelInterface $formModel Model to take value from.
271 | * @param string $property Model property name to take value from.
272 | * @param array $config Widget config.
273 | * @param string|null $theme Theme to use. If not specified, default theme is used.
274 | * @return Number
275 | */
276 | final public static function number(
277 | FormModelInterface $formModel,
278 | string $property,
279 | array $config = [],
280 | ?string $theme = null,
281 | ): Number {
282 | return Number::widget(config: $config, theme: $theme ?? static::DEFAULT_THEME)
283 | ->inputData(new FormModelInputData($formModel, $property));
284 | }
285 |
286 | /**
287 | * Create a password field.
288 | *
289 | * @param FormModelInterface $formModel Model to take value from.
290 | * @param string $property Model property name to take value from.
291 | * @param array $config Widget config.
292 | * @param string|null $theme Theme to use. If not specified, default theme is used.
293 | * @return Password
294 | */
295 | final public static function password(
296 | FormModelInterface $formModel,
297 | string $property,
298 | array $config = [],
299 | ?string $theme = null,
300 | ): Password {
301 | return Password::widget(config: $config, theme: $theme ?? static::DEFAULT_THEME)
302 | ->inputData(new FormModelInputData($formModel, $property));
303 | }
304 |
305 | /**
306 | * Create a radio list field.
307 | *
308 | * @param FormModelInterface $formModel Model to take value from.
309 | * @param string $property Model property name to take value from.
310 | * @param array $config Widget config.
311 | * @param string|null $theme Theme to use. If not specified, default theme is used.
312 | * @return RadioList
313 | */
314 | final public static function radioList(
315 | FormModelInterface $formModel,
316 | string $property,
317 | array $config = [],
318 | ?string $theme = null,
319 | ): RadioList {
320 | return RadioList::widget(config: $config, theme: $theme ?? static::DEFAULT_THEME)
321 | ->inputData(new FormModelInputData($formModel, $property));
322 | }
323 |
324 | /**
325 | * Create a range field.
326 | *
327 | * @param FormModelInterface $formModel Model to take value from.
328 | * @param string $property Model property name to take value from.
329 | * @param array $config Widget config.
330 | * @param string|null $theme Theme to use. If not specified, default theme is used.
331 | * @return Range
332 | */
333 | final public static function range(
334 | FormModelInterface $formModel,
335 | string $property,
336 | array $config = [],
337 | ?string $theme = null,
338 | ): Range {
339 | return Range::widget(config: $config, theme: $theme ?? static::DEFAULT_THEME)
340 | ->inputData(new FormModelInputData($formModel, $property));
341 | }
342 |
343 | /**
344 | * Create a reset button.
345 | *
346 | * @param string|null $content Button content.
347 | * @param array $config Widget config.
348 | * @param string|null $theme Theme to use. If not specified, default theme is used.
349 | * @return ResetButton
350 | */
351 | final public static function resetButton(
352 | ?string $content = null,
353 | array $config = [],
354 | ?string $theme = null,
355 | ): ResetButton {
356 | $field = ResetButton::widget(config: $config, theme: $theme ?? static::DEFAULT_THEME);
357 |
358 | if ($content !== null) {
359 | $field = $field->content($content);
360 | }
361 |
362 | return $field;
363 | }
364 |
365 | /**
366 | * Create a select field.
367 | *
368 | * @param FormModelInterface $formModel Model to take value from.
369 | * @param string $property Model property name to take value from.
370 | * @param array $config Widget config.
371 | * @param string|null $theme Theme to use. If not specified, default theme is used.
372 | * @return Select
373 | */
374 | final public static function select(
375 | FormModelInterface $formModel,
376 | string $property,
377 | array $config = [],
378 | ?string $theme = null,
379 | ): Select {
380 | return Select::widget(config: $config, theme: $theme ?? static::DEFAULT_THEME)
381 | ->inputData(new FormModelInputData($formModel, $property));
382 | }
383 |
384 | /**
385 | * Create a submit button.
386 | *
387 | * @param string|null $content Button content.
388 | * @param array $config Widget config.
389 | * @param string|null $theme Theme to use. If not specified, default theme is used.
390 | * @return SubmitButton
391 | */
392 | final public static function submitButton(
393 | ?string $content = null,
394 | array $config = [],
395 | ?string $theme = null,
396 | ): SubmitButton {
397 | $field = SubmitButton::widget(config: $config, theme: $theme ?? static::DEFAULT_THEME);
398 |
399 | if ($content !== null) {
400 | $field = $field->content($content);
401 | }
402 |
403 | return $field;
404 | }
405 |
406 | /**
407 | * Create a phone number field.
408 | *
409 | * @param FormModelInterface $formModel Model to take value from.
410 | * @param string $property Model property name to take value from.
411 | * @param array $config Widget config.
412 | * @param string|null $theme Theme to use. If not specified, default theme is used.
413 | * @return Telephone
414 | */
415 | final public static function telephone(
416 | FormModelInterface $formModel,
417 | string $property,
418 | array $config = [],
419 | ?string $theme = null,
420 | ): Telephone {
421 | return Telephone::widget(config: $config, theme: $theme ?? static::DEFAULT_THEME)
422 | ->inputData(new FormModelInputData($formModel, $property));
423 | }
424 |
425 | /**
426 | * Create a text field.
427 | *
428 | * @param FormModelInterface $formModel Model to take value from.
429 | * @param string $property Model property name to take value from.
430 | * @param array $config Widget config.
431 | * @param string|null $theme Theme to use. If not specified, default theme is used.
432 | * @return Text
433 | */
434 | final public static function text(
435 | FormModelInterface $formModel,
436 | string $property,
437 | array $config = [],
438 | ?string $theme = null,
439 | ): Text {
440 | return Text::widget(config: $config, theme: $theme ?? static::DEFAULT_THEME)
441 | ->inputData(new FormModelInputData($formModel, $property));
442 | }
443 |
444 | /**
445 | * Create a text area field.
446 | *
447 | * @param FormModelInterface $formModel Model to take value from.
448 | * @param string $property Model property name to take value from.
449 | * @param array $config Widget config.
450 | * @param string|null $theme Theme to use. If not specified, default theme is used.
451 | * @return Textarea
452 | */
453 | final public static function textarea(
454 | FormModelInterface $formModel,
455 | string $property,
456 | array $config = [],
457 | ?string $theme = null,
458 | ): Textarea {
459 | return Textarea::widget(config: $config, theme: $theme ?? static::DEFAULT_THEME)
460 | ->inputData(new FormModelInputData($formModel, $property));
461 | }
462 |
463 | /**
464 | * Create a time field.
465 | *
466 | * @param FormModelInterface $formModel Model to take value from.
467 | * @param string $property Model property name to take value from.
468 | * @param array $config Widget config.
469 | * @param string|null $theme Theme to use. If not specified, default theme is used.
470 | * @return Time
471 | */
472 | final public static function time(
473 | FormModelInterface $formModel,
474 | string $property,
475 | array $config = [],
476 | ?string $theme = null,
477 | ): Time {
478 | return Time::widget(config: $config, theme: $theme ?? static::DEFAULT_THEME)
479 | ->inputData(new FormModelInputData($formModel, $property));
480 | }
481 |
482 | /**
483 | * Create a URL input field.
484 | *
485 | * @param FormModelInterface $formModel Model to take value from.
486 | * @param string $property Model property name to take value from.
487 | * @param array $config Widget config.
488 | * @param string|null $theme Theme to use. If not specified, default theme is used.
489 | * @return Url
490 | */
491 | final public static function url(
492 | FormModelInterface $formModel,
493 | string $property,
494 | array $config = [],
495 | ?string $theme = null,
496 | ): Url {
497 | return Url::widget(config: $config, theme: $theme ?? static::DEFAULT_THEME)
498 | ->inputData(new FormModelInputData($formModel, $property));
499 | }
500 |
501 | /**
502 | * Create a field label.
503 | *
504 | * @param FormModelInterface $formModel Model to create label for.
505 | * @param string $property Model property name to create label for.
506 | * @param array $config Widget config.
507 | * @param string|null $theme Theme to use. If not specified, default theme is used.
508 | * @return Label
509 | */
510 | final public static function label(
511 | FormModelInterface $formModel,
512 | string $property,
513 | array $config = [],
514 | ?string $theme = null,
515 | ): Label {
516 | return Label::widget(config: $config, theme: $theme ?? static::DEFAULT_THEME)
517 | ->inputData(new FormModelInputData($formModel, $property));
518 | }
519 |
520 | /**
521 | * Create a field hint.
522 | *
523 | * @param FormModelInterface $formModel Model to create hint for.
524 | * @param string $property Model property name to create hint for.
525 | * @param array $config Widget config.
526 | * @param string|null $theme Theme to use. If not specified, default theme is used.
527 | * @return Hint
528 | */
529 | final public static function hint(
530 | FormModelInterface $formModel,
531 | string $property,
532 | array $config = [],
533 | ?string $theme = null,
534 | ): Hint {
535 | return Hint::widget(config: $config, theme: $theme ?? static::DEFAULT_THEME)
536 | ->inputData(new FormModelInputData($formModel, $property));
537 | }
538 |
539 | /**
540 | * Create an error for a field.
541 | *
542 | * @param FormModelInterface $formModel Model to create error for.
543 | * @param string $property Model property name to create error for.
544 | * @param array $config Widget config.
545 | * @param string|null $theme Theme to use. If not specified, default theme is used.
546 | * @return Error
547 | */
548 | final public static function error(
549 | FormModelInterface $formModel,
550 | string $property,
551 | array $config = [],
552 | ?string $theme = null,
553 | ): Error {
554 | return Error::widget(config: $config, theme: $theme ?? static::DEFAULT_THEME)
555 | ->inputData(new FormModelInputData($formModel, $property));
556 | }
557 | }
558 |
--------------------------------------------------------------------------------
/src/FieldFactory.php:
--------------------------------------------------------------------------------
1 | defaultTheme);
62 |
63 | if ($content !== null) {
64 | $field = $field->content($content);
65 | }
66 |
67 | return $field;
68 | }
69 |
70 | /**
71 | * Create a button group field.
72 | *
73 | * @param array $config Widget config.
74 | * @param string|null $theme Theme to use. If not specified, default theme is used.
75 | * @return ButtonGroup
76 | */
77 | final public function buttonGroup(array $config = [], ?string $theme = null): ButtonGroup
78 | {
79 | return ButtonGroup::widget(config: $config, theme: $theme ?? $this->defaultTheme);
80 | }
81 |
82 | /**
83 | * Create a checkbox field.
84 | *
85 | * @param FormModelInterface $formModel Model to take value from.
86 | * @param string $property Model property name to take value from.
87 | * @param array $config Widget config.
88 | * @param string|null $theme Theme to use. If not specified, default theme is used.
89 | * @return Checkbox
90 | */
91 | final public function checkbox(
92 | FormModelInterface $formModel,
93 | string $property,
94 | array $config = [],
95 | ?string $theme = null,
96 | ): Checkbox {
97 | return Checkbox::widget(config: $config, theme: $theme ?? $this->defaultTheme)
98 | ->inputData(new FormModelInputData($formModel, $property));
99 | }
100 |
101 | /**
102 | * Create checkboxes list field.
103 | *
104 | * @param FormModelInterface $formModel Model to take value from.
105 | * @param string $property Model property name to take value from.
106 | * @param array $config Widget config.
107 | * @param string|null $theme Theme to use. If not specified, default theme is used.
108 | * @return CheckboxList
109 | */
110 | final public function checkboxList(
111 | FormModelInterface $formModel,
112 | string $property,
113 | array $config = [],
114 | ?string $theme = null,
115 | ): CheckboxList {
116 | return CheckboxList::widget(config: $config, theme: $theme ?? $this->defaultTheme)
117 | ->inputData(new FormModelInputData($formModel, $property));
118 | }
119 |
120 | /**
121 | * Create a date field.
122 | *
123 | * @param FormModelInterface $formModel Model to take value from.
124 | * @param string $property Model property name to take value from.
125 | * @param array $config Widget config.
126 | * @param string|null $theme Theme to use. If not specified, default theme is used.
127 | * @return Date
128 | */
129 | final public function date(
130 | FormModelInterface $formModel,
131 | string $property,
132 | array $config = [],
133 | ?string $theme = null,
134 | ): Date {
135 | return Date::widget(config: $config, theme: $theme ?? $this->defaultTheme)
136 | ->inputData(new FormModelInputData($formModel, $property));
137 | }
138 |
139 | /**
140 | * Create a local date and time field.
141 | *
142 | * @param FormModelInterface $formModel Model to take value from.
143 | * @param string $property Model property name to take value from.
144 | * @param array $config Widget config.
145 | * @param string|null $theme Theme to use. If not specified, default theme is used.
146 | * @return DateTimeLocal
147 | */
148 | final public function dateTimeLocal(
149 | FormModelInterface $formModel,
150 | string $property,
151 | array $config = [],
152 | ?string $theme = null,
153 | ): DateTimeLocal {
154 | return DateTimeLocal::widget(config: $config, theme: $theme ?? $this->defaultTheme)
155 | ->inputData(new FormModelInputData($formModel, $property));
156 | }
157 |
158 | /**
159 | * Create an email field.
160 | *
161 | * @param FormModelInterface $formModel Model to take value from.
162 | * @param string $property Model property name to take value from.
163 | * @param array $config Widget config.
164 | * @param string|null $theme Theme to use. If not specified, default theme is used.
165 | * @return Email
166 | */
167 | final public function email(
168 | FormModelInterface $formModel,
169 | string $property,
170 | array $config = [],
171 | ?string $theme = null,
172 | ): Email {
173 | return Email::widget(config: $config, theme: $theme ?? $this->defaultTheme)
174 | ->inputData(new FormModelInputData($formModel, $property));
175 | }
176 |
177 | /**
178 | * Create errors summary field.
179 | *
180 | * @param FormModelInterface|null $formModel Model to take errors from.
181 | * @param array $config Widget config.
182 | * @param string|null $theme Theme to use. If not specified, default theme is used.
183 | * @return ErrorSummary
184 | */
185 | final public function errorSummary(
186 | ?FormModelInterface $formModel = null,
187 | array $config = [],
188 | ?string $theme = null,
189 | ): ErrorSummary {
190 | $widget = ErrorSummary::widget(config: $config, theme: $theme ?? $this->defaultTheme);
191 | if ($formModel !== null) {
192 | $widget = $widget->errors(
193 | $formModel->isValidated()
194 | ? $formModel->getValidationResult()->getErrorMessagesIndexedByProperty()
195 | : []
196 | );
197 | }
198 | return $widget;
199 | }
200 |
201 | /**
202 | * Create a fieldset.
203 | *
204 | * @param array $config Widget config.
205 | * @param string|null $theme Theme to use. If not specified, default theme is used.
206 | * @return Fieldset
207 | */
208 | final public function fieldset(array $config = [], ?string $theme = null): Fieldset
209 | {
210 | return Fieldset::widget(config: $config, theme: $theme ?? $this->defaultTheme);
211 | }
212 |
213 | /**
214 | * Create a file upload field.
215 | *
216 | * @param FormModelInterface $formModel Model to take value from.
217 | * @param string $property Model property name to take value from.
218 | * @param array $config Widget config.
219 | * @param string|null $theme Theme to use. If not specified, default theme is used.
220 | * @return File
221 | */
222 | final public function file(
223 | FormModelInterface $formModel,
224 | string $property,
225 | array $config = [],
226 | ?string $theme = null,
227 | ): File {
228 | return File::widget(config: $config, theme: $theme ?? $this->defaultTheme)
229 | ->inputData(new FormModelInputData($formModel, $property));
230 | }
231 |
232 | /**
233 | * Create a hidden field.
234 | *
235 | * @param FormModelInterface $formModel Model to take value from.
236 | * @param string $property Model property name to take value from.
237 | * @param array $config Widget config.
238 | * @param string|null $theme Theme to use. If not specified, default theme is used.
239 | * @return Hidden
240 | */
241 | final public function hidden(
242 | FormModelInterface $formModel,
243 | string $property,
244 | array $config = [],
245 | ?string $theme = null,
246 | ): Hidden {
247 | return Hidden::widget(config: $config, theme: $theme ?? $this->defaultTheme)
248 | ->inputData(new FormModelInputData($formModel, $property));
249 | }
250 |
251 | /**
252 | * Create an image.
253 | *
254 | * @param string|null $url "src" of the image.
255 | * @param array $config Widget config.
256 | * @param string|null $theme Theme to use. If not specified, default theme is used.
257 | * @return Image
258 | */
259 | final public function image(?string $url = null, array $config = [], ?string $theme = null): Image
260 | {
261 | $field = Image::widget(config: $config, theme: $theme ?? $this->defaultTheme);
262 |
263 | if ($url !== null) {
264 | $field = $field->src($url);
265 | }
266 |
267 | return $field;
268 | }
269 |
270 | /**
271 | * Create a number field.
272 | *
273 | * @param FormModelInterface $formModel Model to take value from.
274 | * @param string $property Model property name to take value from.
275 | * @param array $config Widget config.
276 | * @param string|null $theme Theme to use. If not specified, default theme is used.
277 | * @return Number
278 | */
279 | final public function number(
280 | FormModelInterface $formModel,
281 | string $property,
282 | array $config = [],
283 | ?string $theme = null,
284 | ): Number {
285 | return Number::widget(config: $config, theme: $theme ?? $this->defaultTheme)
286 | ->inputData(new FormModelInputData($formModel, $property));
287 | }
288 |
289 | /**
290 | * Create a password field.
291 | *
292 | * @param FormModelInterface $formModel Model to take value from.
293 | * @param string $property Model property name to take value from.
294 | * @param array $config Widget config.
295 | * @param string|null $theme Theme to use. If not specified, default theme is used.
296 | * @return Password
297 | */
298 | final public function password(
299 | FormModelInterface $formModel,
300 | string $property,
301 | array $config = [],
302 | ?string $theme = null,
303 | ): Password {
304 | return Password::widget(config: $config, theme: $theme ?? $this->defaultTheme)
305 | ->inputData(new FormModelInputData($formModel, $property));
306 | }
307 |
308 | /**
309 | * Create a radio list field.
310 | *
311 | * @param FormModelInterface $formModel Model to take value from.
312 | * @param string $property Model property name to take value from.
313 | * @param array $config Widget config.
314 | * @param string|null $theme Theme to use. If not specified, default theme is used.
315 | * @return RadioList
316 | */
317 | final public function radioList(
318 | FormModelInterface $formModel,
319 | string $property,
320 | array $config = [],
321 | ?string $theme = null,
322 | ): RadioList {
323 | return RadioList::widget(config: $config, theme: $theme ?? $this->defaultTheme)
324 | ->inputData(new FormModelInputData($formModel, $property));
325 | }
326 |
327 | /**
328 | * Create a range field.
329 | *
330 | * @param FormModelInterface $formModel Model to take value from.
331 | * @param string $property Model property name to take value from.
332 | * @param array $config Widget config.
333 | * @param string|null $theme Theme to use. If not specified, default theme is used.
334 | * @return Range
335 | */
336 | final public function range(
337 | FormModelInterface $formModel,
338 | string $property,
339 | array $config = [],
340 | ?string $theme = null,
341 | ): Range {
342 | return Range::widget(config: $config, theme: $theme ?? $this->defaultTheme)
343 | ->inputData(new FormModelInputData($formModel, $property));
344 | }
345 |
346 | /**
347 | * Create a reset button.
348 | *
349 | * @param string|null $content Button content.
350 | * @param array $config Widget config.
351 | * @param string|null $theme Theme to use. If not specified, default theme is used.
352 | * @return ResetButton
353 | */
354 | final public function resetButton(
355 | ?string $content = null,
356 | array $config = [],
357 | ?string $theme = null,
358 | ): ResetButton {
359 | $field = ResetButton::widget(config: $config, theme: $theme ?? $this->defaultTheme);
360 |
361 | if ($content !== null) {
362 | $field = $field->content($content);
363 | }
364 |
365 | return $field;
366 | }
367 |
368 | /**
369 | * Create a select field.
370 | *
371 | * @param FormModelInterface $formModel Model to take value from.
372 | * @param string $property Model property name to take value from.
373 | * @param array $config Widget config.
374 | * @param string|null $theme Theme to use. If not specified, default theme is used.
375 | * @return Select
376 | */
377 | final public function select(
378 | FormModelInterface $formModel,
379 | string $property,
380 | array $config = [],
381 | ?string $theme = null,
382 | ): Select {
383 | return Select::widget(config: $config, theme: $theme ?? $this->defaultTheme)
384 | ->inputData(new FormModelInputData($formModel, $property));
385 | }
386 |
387 | /**
388 | * Create a submit button.
389 | *
390 | * @param string|null $content Button content.
391 | * @param array $config Widget config.
392 | * @param string|null $theme Theme to use. If not specified, default theme is used.
393 | * @return SubmitButton
394 | */
395 | final public function submitButton(
396 | ?string $content = null,
397 | array $config = [],
398 | ?string $theme = null,
399 | ): SubmitButton {
400 | $field = SubmitButton::widget(config: $config, theme: $theme ?? $this->defaultTheme);
401 |
402 | if ($content !== null) {
403 | $field = $field->content($content);
404 | }
405 |
406 | return $field;
407 | }
408 |
409 | /**
410 | * Create a phone number field.
411 | *
412 | * @param FormModelInterface $formModel Model to take value from.
413 | * @param string $property Model property name to take value from.
414 | * @param array $config Widget config.
415 | * @param string|null $theme Theme to use. If not specified, default theme is used.
416 | * @return Telephone
417 | */
418 | final public function telephone(
419 | FormModelInterface $formModel,
420 | string $property,
421 | array $config = [],
422 | ?string $theme = null,
423 | ): Telephone {
424 | return Telephone::widget(config: $config, theme: $theme ?? $this->defaultTheme)
425 | ->inputData(new FormModelInputData($formModel, $property));
426 | }
427 |
428 | /**
429 | * Create a text field.
430 | *
431 | * @param FormModelInterface $formModel Model to take value from.
432 | * @param string $property Model property name to take value from.
433 | * @param array $config Widget config.
434 | * @param string|null $theme Theme to use. If not specified, default theme is used.
435 | * @return Text
436 | */
437 | final public function text(
438 | FormModelInterface $formModel,
439 | string $property,
440 | array $config = [],
441 | ?string $theme = null,
442 | ): Text {
443 | return Text::widget(config: $config, theme: $theme ?? $this->defaultTheme)
444 | ->inputData(new FormModelInputData($formModel, $property));
445 | }
446 |
447 | /**
448 | * Create a text area field.
449 | *
450 | * @param FormModelInterface $formModel Model to take value from.
451 | * @param string $property Model property name to take value from.
452 | * @param array $config Widget config.
453 | * @param string|null $theme Theme to use. If not specified, default theme is used.
454 | * @return Textarea
455 | */
456 | final public function textarea(
457 | FormModelInterface $formModel,
458 | string $property,
459 | array $config = [],
460 | ?string $theme = null,
461 | ): Textarea {
462 | return Textarea::widget(config: $config, theme: $theme ?? $this->defaultTheme)
463 | ->inputData(new FormModelInputData($formModel, $property));
464 | }
465 |
466 | /**
467 | * Create a time field.
468 | *
469 | * @param FormModelInterface $formModel Model to take value from.
470 | * @param string $property Model property name to take value from.
471 | * @param array $config Widget config.
472 | * @param string|null $theme Theme to use. If not specified, default theme is used.
473 | * @return Time
474 | */
475 | final public function time(
476 | FormModelInterface $formModel,
477 | string $property,
478 | array $config = [],
479 | ?string $theme = null,
480 | ): Time {
481 | return Time::widget(config: $config, theme: $theme ?? $this->defaultTheme)
482 | ->inputData(new FormModelInputData($formModel, $property));
483 | }
484 |
485 | /**
486 | * Create a URL input field.
487 | *
488 | * @param FormModelInterface $formModel Model to take value from.
489 | * @param string $property Model property name to take value from.
490 | * @param array $config Widget config.
491 | * @param string|null $theme Theme to use. If not specified, default theme is used.
492 | * @return Url
493 | */
494 | final public function url(
495 | FormModelInterface $formModel,
496 | string $property,
497 | array $config = [],
498 | ?string $theme = null,
499 | ): Url {
500 | return Url::widget(config: $config, theme: $theme ?? $this->defaultTheme)
501 | ->inputData(new FormModelInputData($formModel, $property));
502 | }
503 |
504 | /**
505 | * Create a field label.
506 | *
507 | * @param FormModelInterface $formModel Model to create label for.
508 | * @param string $property Model property name to create label for.
509 | * @param array $config Widget config.
510 | * @param string|null $theme Theme to use. If not specified, default theme is used.
511 | * @return Label
512 | */
513 | final public function label(
514 | FormModelInterface $formModel,
515 | string $property,
516 | array $config = [],
517 | ?string $theme = null,
518 | ): Label {
519 | return Label::widget(config: $config, theme: $theme ?? $this->defaultTheme)
520 | ->inputData(new FormModelInputData($formModel, $property));
521 | }
522 |
523 | /**
524 | * Create a field hint.
525 | *
526 | * @param FormModelInterface $formModel Model to create hint for.
527 | * @param string $property Model property name to create hint for.
528 | * @param array $config Widget config.
529 | * @param string|null $theme Theme to use. If not specified, default theme is used.
530 | * @return Hint
531 | */
532 | final public function hint(
533 | FormModelInterface $formModel,
534 | string $property,
535 | array $config = [],
536 | ?string $theme = null,
537 | ): Hint {
538 | return Hint::widget(config: $config, theme: $theme ?? $this->defaultTheme)
539 | ->inputData(new FormModelInputData($formModel, $property));
540 | }
541 |
542 | /**
543 | * Create an error for a field.
544 | *
545 | * @param FormModelInterface $formModel Model to create error for.
546 | * @param string $property Model property name to create error for.
547 | * @param array $config Widget config.
548 | * @param string|null $theme Theme to use. If not specified, default theme is used.
549 | * @return Error
550 | */
551 | final public function error(
552 | FormModelInterface $formModel,
553 | string $property,
554 | array $config = [],
555 | ?string $theme = null,
556 | ): Error {
557 | return Error::widget(config: $config, theme: $theme ?? $this->defaultTheme)
558 | ->inputData(new FormModelInputData($formModel, $property));
559 | }
560 | }
561 |
--------------------------------------------------------------------------------
/src/FormHydrator.php:
--------------------------------------------------------------------------------
1 | getFormName();
65 | if ($scope === '') {
66 | $hydrateData = $data;
67 | } else {
68 | if (!isset($data[$scope]) || !is_array($data[$scope])) {
69 | return false;
70 | }
71 | $hydrateData = $data[$scope];
72 | }
73 |
74 | $this->hydrator->hydrate(
75 | $model,
76 | new ArrayData(
77 | $hydrateData,
78 | $this->createMap($model, $map, $strict),
79 | $strict ?? true
80 | )
81 | );
82 |
83 | return true;
84 | }
85 |
86 | /**
87 | * Validate form model.
88 | *
89 | * @param FormModelInterface $model Form model to validate.
90 | *
91 | * @return Result Validation result.
92 | */
93 | public function validate(FormModelInterface $model): Result
94 | {
95 | return $this->validator->validate($model);
96 | }
97 |
98 | /**
99 | * Fill the model with the data and validate it.
100 | *
101 | * @param FormModelInterface $model Model to fill.
102 | * @param mixed $data Data to fill model with.
103 | * @param ?array $map Map of object property names to keys in the data array to use for hydration.
104 | * If not provided, it may be generated automatically based on presence of property validation rules and a `strict`
105 | * setting.
106 | * @psalm-param MapType $map
107 | * @param ?bool $strict If `false`, fills everything that is in the data. If `null`, fills data that is either
108 | * defined in a map explicitly or allowed via validation rules. If `false`, fills only data defined explicitly
109 | * in a map or only data allowed via validation rules but not both.
110 | * @param ?string $scope Key to use in the data array as a source of data. Usually used when there are multiple
111 | * forms at the same page. If not set, it equals to {@see FormModelInterface::getFormName()}.
112 | *
113 | * @return bool Whether model is filled with data and is valid.
114 | */
115 | public function populateAndValidate(
116 | FormModelInterface $model,
117 | mixed $data,
118 | ?array $map = null,
119 | ?bool $strict = null,
120 | ?string $scope = null
121 | ): bool {
122 | if (!$this->populate($model, $data, $map, $strict, $scope)) {
123 | return false;
124 | }
125 |
126 | return $this->validate($model)->isValid();
127 | }
128 |
129 | /**
130 | * Fill the model with the data parsed from request body.
131 | *
132 | * @param FormModelInterface $model Model to fill.
133 | * @param ServerRequestInterface $request Request to get parsed data from.
134 | * @param ?array $map Map of object property names to keys in the data array to use for hydration.
135 | * If not provided, it may be generated automatically based on presence of property validation rules and a `strict`
136 | * setting.
137 | * @psalm-param MapType $map
138 | * @param ?bool $strict If `false`, fills everything that is in the data. If `null`, fills data that is either
139 | * defined in a map explicitly or allowed via validation rules. If `false`, fills only data defined explicitly
140 | * in a map or only data allowed via validation rules but not both.
141 | * @param ?string $scope Key to use in the data array as a source of data. Usually used when there are multiple
142 | * forms at the same page. If not set, it equals to {@see FormModelInterface::getFormName()}.
143 | */
144 | public function populateFromPost(
145 | FormModelInterface $model,
146 | ServerRequestInterface $request,
147 | ?array $map = null,
148 | ?bool $strict = null,
149 | ?string $scope = null
150 | ): bool {
151 | if ($request->getMethod() !== 'POST') {
152 | return false;
153 | }
154 |
155 | return $this->populate($model, $request->getParsedBody(), $map, $strict, $scope);
156 | }
157 |
158 | /**
159 | * Fill the model with the data parsed from request body and validate it.
160 | *
161 | * @param FormModelInterface $model Model to fill.
162 | * @param ServerRequestInterface $request Request to get parsed data from.
163 | * @param ?array $map Map of object property names to keys in the data array to use for hydration.
164 | * If not provided, it may be generated automatically based on presence of property validation rules and a `strict`
165 | * setting.
166 | * @psalm-param MapType $map
167 | * @param ?bool $strict If `false`, fills everything that is in the data. If `null`, fills data that is either
168 | * defined in a map explicitly or allowed via validation rules. If `false`, fills only data defined explicitly
169 | * in a map or only data allowed via validation rules but not both.
170 | * @param ?string $scope Key to use in the data array as a source of data. Usually used when there are multiple
171 | * forms at the same page. If not set, it equals to {@see FormModelInterface::getFormName()}.
172 | *
173 | * @return bool Whether model is filled with data and is valid.
174 | */
175 | public function populateFromPostAndValidate(
176 | FormModelInterface $model,
177 | ServerRequestInterface $request,
178 | ?array $map = null,
179 | ?bool $strict = null,
180 | ?string $scope = null
181 | ): bool {
182 | if ($request->getMethod() !== 'POST') {
183 | return false;
184 | }
185 |
186 | return $this->populateAndValidate($model, $request->getParsedBody(), $map, $strict, $scope);
187 | }
188 |
189 | /**
190 | * Get a map of object property names mapped to keys in the data array.
191 | *
192 | * @param FormModelInterface $model Model to read validation rules from.
193 | * @param ?array $userMap Explicit map defined by user.
194 | * @psalm-param MapType $userMap
195 | * @param ?bool $strict If `false`, fills everything that is in the data. If `null`, fills data that is either
196 | * defined in a map explicitly or allowed via validation rules. If `false`, fills only data defined explicitly
197 | * in a map or only data allowed via validation rules but not both.
198 | *
199 | * @return array A map of object property names mapped to keys in the data array.
200 | * @psalm-return MapType
201 | */
202 | private function createMap(FormModelInterface $model, ?array $userMap, ?bool $strict): array
203 | {
204 | if ($strict === false) {
205 | return $userMap ?? [];
206 | }
207 |
208 | if ($strict && $userMap !== null) {
209 | return $userMap;
210 | }
211 |
212 | $properties = $this->getPropertiesWithRules($model);
213 | $generatedMap = array_combine($properties, $properties);
214 |
215 | if ($userMap === null) {
216 | return $generatedMap;
217 | }
218 |
219 | return array_merge($generatedMap, $userMap);
220 | }
221 |
222 | /**
223 | * Extract object property names mapped to keys in the data array based on model validation rules.
224 | *
225 | * @return array Object property names mapped to keys in the data array.
226 | * @psalm-return array
227 | */
228 | private function getPropertiesWithRules(FormModelInterface $model): array
229 | {
230 | $parser = new ObjectParser($model, skipStaticProperties: true);
231 | $properties = $this->extractStringKeys($parser->getRules());
232 |
233 | return $model instanceof RulesProviderInterface
234 | ? array_merge($properties, $this->extractStringKeys($model->getRules()))
235 | : $properties;
236 | }
237 |
238 | /**
239 | * Get only string keys from an array.
240 | *
241 | * @return array String keys.
242 | * @psalm-return list
243 | */
244 | private function extractStringKeys(iterable $array): array
245 | {
246 | $result = [];
247 | foreach ($array as $key => $_value) {
248 | if (is_string($key)) {
249 | $result[] = $key;
250 | }
251 | }
252 | return $result;
253 | }
254 | }
255 |
--------------------------------------------------------------------------------
/src/FormModel.php:
--------------------------------------------------------------------------------
1 | readPropertyMetaValue(self::META_HINT, $property) ?? '';
64 | }
65 |
66 | public function getPropertyHints(): array
67 | {
68 | return [];
69 | }
70 |
71 | public function getPropertyLabel(string $property): string
72 | {
73 | return $this->readPropertyMetaValue(self::META_LABEL, $property) ?? $this->generatePropertyLabel($property);
74 | }
75 |
76 | public function getPropertyLabels(): array
77 | {
78 | return [];
79 | }
80 |
81 | public function getPropertyPlaceholder(string $property): string
82 | {
83 | return $this->readPropertyMetaValue(self::META_PLACEHOLDER, $property) ?? '';
84 | }
85 |
86 | public function getPropertyValue(string $property): mixed
87 | {
88 | try {
89 | return $this->readPropertyValue($property);
90 | } catch (PropertyNotSupportNestedValuesException $exception) {
91 | return $exception->getValue() === null
92 | ? null
93 | : throw $exception;
94 | } catch (UndefinedArrayElementException) {
95 | return null;
96 | }
97 | }
98 |
99 | public function getPropertyPlaceholders(): array
100 | {
101 | return [];
102 | }
103 |
104 | public function getFormName(): string
105 | {
106 | if (str_contains(static::class, '@anonymous')) {
107 | return '';
108 | }
109 |
110 | $className = strrchr(static::class, '\\');
111 | if ($className === false) {
112 | return static::class;
113 | }
114 |
115 | return substr($className, 1);
116 | }
117 |
118 | public function hasProperty(string $property): bool
119 | {
120 | try {
121 | $this->readPropertyValue($property);
122 | } catch (ValueNotFoundException) {
123 | return false;
124 | }
125 | return true;
126 | }
127 |
128 | public function isValid(): bool
129 | {
130 | return $this->isValidated() && $this->getValidationResult()->isValid();
131 | }
132 |
133 | public function isValidated(): bool
134 | {
135 | return $this->validationResult !== null;
136 | }
137 |
138 | public function addError(string $message, array $valuePath = []): static
139 | {
140 | $this->getValidationResult()->addErrorWithoutPostProcessing($message, valuePath: $valuePath);
141 | return $this;
142 | }
143 |
144 | public function processValidationResult(Result $result): void
145 | {
146 | $this->validationResult = $result;
147 | }
148 |
149 | public function getValidationResult(): Result
150 | {
151 | if (empty($this->validationResult)) {
152 | throw new LogicException('Validation result is not set.');
153 | }
154 |
155 | return $this->validationResult;
156 | }
157 |
158 | /**
159 | * Returns model property value given a path.
160 | *
161 | * @param string $path Property path.
162 | * @throws UndefinedArrayElementException
163 | * @throws UndefinedObjectPropertyException
164 | * @throws StaticObjectPropertyException
165 | * @throws PropertyNotSupportNestedValuesException
166 | * @throws ValueNotFoundException
167 | * @return mixed Property value.
168 | */
169 | private function readPropertyValue(string $path): mixed
170 | {
171 | $normalizedPath = $this->normalizePath($path);
172 |
173 | $value = $this;
174 | $keys = [[static::class, $this]];
175 | foreach ($normalizedPath as $key) {
176 | $keys[] = [$key, $value];
177 |
178 | if (is_array($value)) {
179 | if (array_key_exists($key, $value)) {
180 | $value = $value[$key];
181 | continue;
182 | }
183 | throw new UndefinedArrayElementException($this->makePropertyPathString($keys));
184 | }
185 |
186 | if (is_object($value)) {
187 | $class = new ReflectionClass($value);
188 | try {
189 | $property = $class->getProperty($key);
190 | } catch (ReflectionException) {
191 | throw new UndefinedObjectPropertyException($this->makePropertyPathString($keys));
192 | }
193 | if ($property->isStatic()) {
194 | throw new StaticObjectPropertyException($this->makePropertyPathString($keys));
195 | }
196 | $value = $property->getValue($value);
197 | continue;
198 | }
199 |
200 | array_pop($keys);
201 | throw new PropertyNotSupportNestedValuesException($this->makePropertyPathString($keys), $value);
202 | }
203 |
204 | return $value;
205 | }
206 |
207 | /**
208 | * Return a meta information for a property at a given path.
209 | *
210 | * @param int $metaKey Determines which meta information to return. One of `FormModel::META_*` constants.
211 | * @param string $path Property path.
212 | * @return ?string Meta information for a property.
213 | *
214 | * @psalm-param self::META_* $metaKey
215 | */
216 | private function readPropertyMetaValue(int $metaKey, string $path): ?string
217 | {
218 | $normalizedPath = $this->normalizePath($path);
219 |
220 | $value = $this;
221 | $n = 0;
222 | foreach ($normalizedPath as $key) {
223 | if ($value instanceof FormModelInterface) {
224 | $nestedProperty = implode('.', array_slice($normalizedPath, $n));
225 | $data = match ($metaKey) {
226 | self::META_LABEL => $value->getPropertyLabels(),
227 | self::META_HINT => $value->getPropertyHints(),
228 | self::META_PLACEHOLDER => $value->getPropertyPlaceholders(),
229 | };
230 | if (array_key_exists($nestedProperty, $data)) {
231 | return $data[$nestedProperty];
232 | }
233 | }
234 |
235 | $class = new ReflectionClass($value);
236 | try {
237 | $property = $class->getProperty($key);
238 | } catch (ReflectionException) {
239 | return null;
240 | }
241 | if ($property->isStatic()) {
242 | return null;
243 | }
244 |
245 | $valueByAttribute = $this->getPropertyMetaValueByAttribute($metaKey, $property);
246 | if ($valueByAttribute !== null) {
247 | return $valueByAttribute;
248 | }
249 |
250 | $value = $property->getValue($value);
251 | if (!is_object($value)) {
252 | return null;
253 | }
254 |
255 | $n++;
256 | }
257 |
258 | return null;
259 | }
260 |
261 | /**
262 | * Generates a user-friendly property label based on the given property name.
263 | *
264 | * This is done by replacing underscores, dashes and dots with blanks and changing the first letter of each word to
265 | * upper case.
266 | *
267 | * For example, 'department_name' or 'DepartmentName' will generate 'Department Name'.
268 | *
269 | * @param string $property The property name.
270 | *
271 | * @return string The property label.
272 | */
273 | private function generatePropertyLabel(string $property): string
274 | {
275 | if (self::$inflector === null) {
276 | self::$inflector = new Inflector();
277 | }
278 |
279 | return StringHelper::uppercaseFirstCharacterInEachWord(
280 | self::$inflector->toWords($property)
281 | );
282 | }
283 |
284 | /**
285 | * Normalize property path and return it as an array.
286 | *
287 | * @return string[] Normalized property path as an array.
288 | */
289 | private function normalizePath(string $path): array
290 | {
291 | $path = str_replace(['][', '['], '.', rtrim($path, ']'));
292 | return StringHelper::parsePath($path);
293 | }
294 |
295 | /**
296 | * Convert array property path to its string representation.
297 | *
298 | * @param array $keys Property path as an array. *
299 | * @psalm-param array $keys
300 | * @return string Property path as string.
301 | */
302 | private function makePropertyPathString(array $keys): string
303 | {
304 | $path = '';
305 | foreach ($keys as $key) {
306 | if ($path !== '') {
307 | if (is_object($key[1])) {
308 | $path .= '::$' . $key[0];
309 | } elseif (is_array($key[1])) {
310 | $path .= '[' . $key[0] . ']';
311 | }
312 | } else {
313 | $path = (string) $key[0];
314 | }
315 | }
316 | return $path;
317 | }
318 |
319 | /**
320 | * @psalm-param self::META_* $metaKey
321 | */
322 | private function getPropertyMetaValueByAttribute(int $metaKey, ReflectionProperty $property): ?string
323 | {
324 | switch ($metaKey) {
325 | /** Try to get label from {@see Label} PHP attribute. */
326 | case self::META_LABEL:
327 | $attributes = $property->getAttributes(Label::class, ReflectionAttribute::IS_INSTANCEOF);
328 | if (!empty($attributes)) {
329 | /** @var Label $instance */
330 | $instance = $attributes[0]->newInstance();
331 |
332 | return $instance->getLabel();
333 | }
334 |
335 | break;
336 | /** Try to get label from {@see Hint} PHP attribute. */
337 | case self::META_HINT:
338 | $attributes = $property->getAttributes(Hint::class, ReflectionAttribute::IS_INSTANCEOF);
339 | if (!empty($attributes)) {
340 | /** @var Hint $instance */
341 | $instance = $attributes[0]->newInstance();
342 |
343 | return $instance->getHint();
344 | }
345 |
346 | break;
347 | /** Try to get label from {@see Placeholder} PHP attribute. */
348 | case self::META_PLACEHOLDER:
349 | $attributes = $property->getAttributes(Placeholder::class, ReflectionAttribute::IS_INSTANCEOF);
350 | if (!empty($attributes)) {
351 | /** @var Placeholder $instance */
352 | $instance = $attributes[0]->newInstance();
353 |
354 | return $instance->getPlaceholder();
355 | }
356 |
357 | break;
358 | }
359 |
360 | return null;
361 | }
362 | }
363 |
--------------------------------------------------------------------------------
/src/FormModelInputData.php:
--------------------------------------------------------------------------------
1 | validationRules === null) {
37 | $rules = RulesNormalizer::normalize(null, $this->model);
38 | $this->validationRules = $rules[$this->property] ?? [];
39 | }
40 | return $this->validationRules;
41 | }
42 |
43 | /**
44 | * Generates an appropriate input name.
45 | *
46 | * This method generates a name that can be used as the input name to collect user input. The name is generated
47 | * according to the form and the property names. For example, if the form name is `Post`
48 | * then the input name generated for the `content` property would be `Post[content]`.
49 | *
50 | * See {@see getPropertyName()} for explanation of property expression.
51 | *
52 | * @throws InvalidArgumentException If the property name contains non-word characters or empty form name for
53 | * tabular inputs.
54 | * @return string The generated input name.
55 | */
56 | public function getName(): string
57 | {
58 | $data = $this->parseProperty($this->property);
59 | $formName = $this->model->getFormName();
60 |
61 | if ($formName === '' && $data['prefix'] === '') {
62 | return $this->property;
63 | }
64 |
65 | if ($formName !== '') {
66 | return "$formName{$data['prefix']}[{$data['name']}]{$data['suffix']}";
67 | }
68 |
69 | throw new InvalidArgumentException('Form name cannot be empty for tabular inputs.');
70 | }
71 |
72 | /**
73 | * @throws UndefinedObjectPropertyException
74 | * @throws StaticObjectPropertyException
75 | * @throws PropertyNotSupportNestedValuesException
76 | * @throws ValueNotFoundException
77 | */
78 | public function getValue(): mixed
79 | {
80 | $parsedName = $this->parseProperty($this->property);
81 | return $this->model->getPropertyValue($parsedName['name'] . $parsedName['suffix']);
82 | }
83 |
84 | public function getLabel(): ?string
85 | {
86 | return $this->model->getPropertyLabel($this->getPropertyName());
87 | }
88 |
89 | public function getHint(): ?string
90 | {
91 | return $this->model->getPropertyHint($this->getPropertyName());
92 | }
93 |
94 | public function getPlaceholder(): ?string
95 | {
96 | $placeholder = $this->model->getPropertyPlaceholder($this->getPropertyName());
97 | return $placeholder === '' ? null : $placeholder;
98 | }
99 |
100 | /**
101 | * Generates an appropriate input ID.
102 | *
103 | * This method converts the result {@see getName()} into a valid input ID.
104 | *
105 | * For example, if {@see getInputName()} returns `Post[content]`, this method will return `post-content`.
106 | *
107 | * @throws InvalidArgumentException If the property name contains non-word characters.
108 | * @return string The generated input ID.
109 | */
110 | public function getId(): string
111 | {
112 | $name = $this->getName();
113 | $name = mb_strtolower($name, 'UTF-8');
114 | return str_replace(['[]', '][', '[', ']', ' ', '.'], ['', '-', '-', '', '-', '-'], $name);
115 | }
116 |
117 | public function isValidated(): bool
118 | {
119 | return $this->model->isValidated();
120 | }
121 |
122 | public function getValidationErrors(): array
123 | {
124 | /** @psalm-var list */
125 | return $this->model->isValidated()
126 | ? $this->model->getValidationResult()->getPropertyErrorMessages($this->getPropertyName())
127 | : [];
128 | }
129 |
130 | private function getPropertyName(): string
131 | {
132 | $property = $this->parseProperty($this->property)['name'];
133 |
134 | if (!$this->model->hasProperty($property)) {
135 | throw new InvalidArgumentException('Property "' . $property . '" does not exist.');
136 | }
137 |
138 | return $property;
139 | }
140 |
141 | /**
142 | * This method parses a property expression and returns an associative array containing
143 | * real property name, prefix and suffix.
144 | * For example: `['name' => 'content', 'prefix' => '', 'suffix' => '[0]']`
145 | *
146 | * A property expression is a property name prefixed and/or suffixed with array indexes. It is mainly used in
147 | * tabular data input and/or input of array type. Below are some examples:
148 | *
149 | * - `[0]content` is used in tabular data input to represent the "content" property for the first model in tabular
150 | * input;
151 | * - `dates[0]` represents the first array element of the "dates" property;
152 | * - `[0]dates[0]` represents the first array element of the "dates" property for the first model in tabular
153 | * input.
154 | *
155 | * @param string $property The property name or expression
156 | *
157 | * @throws InvalidArgumentException If the property name contains non-word characters.
158 | * @return string[] The property name, prefix and suffix.
159 | */
160 | private function parseProperty(string $property): array
161 | {
162 | if (!preg_match('/(^|.*\])([\w\.\+\-_]+)(\[.*|$)/u', $property, $matches)) {
163 | throw new InvalidArgumentException('Property name must contain word characters only.');
164 | }
165 | return [
166 | 'name' => $matches[2],
167 | 'prefix' => $matches[1],
168 | 'suffix' => $matches[3],
169 | ];
170 | }
171 | }
172 |
--------------------------------------------------------------------------------
/src/FormModelInterface.php:
--------------------------------------------------------------------------------
1 | hint).
42 | *
43 | * @psalm-return array
44 | */
45 | public function getPropertyHints(): array;
46 |
47 | /**
48 | * Returns the text label for the specified property.
49 | *
50 | * @param string $property The property name.
51 | *
52 | * @return string The property label.
53 | */
54 | public function getPropertyLabel(string $property): string;
55 |
56 | /**
57 | * Returns the property labels.
58 | *
59 | * Property labels are mainly used for display purpose. For example, given a property `firstName`, we can
60 | * declare a label `First Name` which is more user-friendly and can be displayed to end users.
61 | *
62 | * By default, a property label is generated automatically. This method allows you to
63 | * explicitly specify property labels.
64 | *
65 | * Note, in order to inherit labels defined in the parent class, a child class needs to merge the parent labels
66 | * with child labels using functions such as `array_merge()`.
67 | *
68 | * @return array Property labels (name => label).
69 | *
70 | * {@see getPropertyLabel()}
71 | *
72 | * @psalm-return array
73 | */
74 | public function getPropertyLabels(): array;
75 |
76 | /**
77 | * Returns the text placeholder for the specified property.
78 | *
79 | * @param string $property The property name.
80 | *
81 | * @return string The property placeholder.
82 | */
83 | public function getPropertyPlaceholder(string $property): string;
84 |
85 | /**
86 | * Get a value for a property specified.
87 | *
88 | * @param string $property Name of the property.
89 | * @throws UndefinedObjectPropertyException
90 | * @throws StaticObjectPropertyException
91 | * @throws PropertyNotSupportNestedValuesException
92 | * @throws ValueNotFoundException
93 | * @return mixed Value.
94 | */
95 | public function getPropertyValue(string $property): mixed;
96 |
97 | /**
98 | * Returns the property placeholders.
99 | *
100 | * @return array Property placeholder (name => placeholder).
101 | *
102 | * @psalm-return array
103 | */
104 | public function getPropertyPlaceholders(): array;
105 |
106 | /**
107 | * Returns the form name that this model class should use.
108 | *
109 | * The form name is mainly used by {@see FormModelInputData} to determine how to name the input fields for
110 | * the properties in a model.
111 | * If the form name is "A" and a property name is "b", then the corresponding input name would be "A[b]".
112 | * If the form name is an empty string, then the input name would be "b".
113 | *
114 | * The purpose of the above naming schema is that for forms which contain multiple different models, the properties
115 | * of each model are grouped in sub-arrays of the POST-data, and it is easier to differentiate between them.
116 | *
117 | * @return string The form name of this model class.
118 | */
119 | public function getFormName(): string;
120 |
121 | /**
122 | * If there is such property in the set.
123 | *
124 | * @param string $property Property name.
125 | * @return bool Whether there's such property.
126 | */
127 | public function hasProperty(string $property): bool;
128 |
129 | /**
130 | * @return bool Whether form data is valid.
131 | */
132 | public function isValid(): bool;
133 |
134 | /**
135 | * @return bool Whether form was validated.
136 | */
137 | public function isValidated(): bool;
138 |
139 | /**
140 | * Add an error, the message of which does not require any post-processing.
141 | *
142 | * @see Error::addErrorWithoutPostProcessing()
143 | *
144 | * @throws LogicException When form is not validated.
145 | * @return static Same instance of result.
146 | *
147 | * @psalm-param array $parameters
148 | * @psalm-param list $valuePath
149 | */
150 | public function addError(string $message, array $valuePath = []): static;
151 |
152 | /**
153 | * Returns validation result.
154 | *
155 | * @throws LogicException When validation result is not set.
156 | * @return Result Validation result.
157 | */
158 | public function getValidationResult(): Result;
159 | }
160 |
--------------------------------------------------------------------------------
/src/NonArrayTypeCaster.php:
--------------------------------------------------------------------------------
1 | isArray($context->getReflectionType())) {
27 | return Result::success([]);
28 | }
29 |
30 | return Result::fail();
31 | }
32 |
33 | /**
34 | * Checks if the type provided is an array.
35 | *
36 | * @param ReflectionType|null $type Type to check.
37 | * @return bool If the type is an array.
38 | */
39 | private function isArray(?ReflectionType $type): bool
40 | {
41 | return $type instanceof ReflectionNamedType && $type->isBuiltin() && $type->getName() === 'array';
42 | }
43 | }
44 |
--------------------------------------------------------------------------------
/src/ValidationRulesEnricher.php:
--------------------------------------------------------------------------------
1 | hasWhen($rule)) {
45 | continue;
46 | }
47 | $this->processRequiredToRequired($rule, $enrichment);
48 | }
49 | return $enrichment;
50 | }
51 |
52 | if ($field instanceof Email) {
53 | $enrichment = [];
54 | foreach ($rules as $rule) {
55 | if ($this->hasWhen($rule)) {
56 | continue;
57 | }
58 | $this->processRequiredToRequired($rule, $enrichment);
59 | $this->processLengthToMinMaxLength($rule, $enrichment);
60 | $this->processRegexToPattern($rule, $enrichment);
61 | }
62 | return $enrichment;
63 | }
64 |
65 | if ($field instanceof File) {
66 | $enrichment = [];
67 | foreach ($rules as $rule) {
68 | if ($this->hasWhen($rule)) {
69 | continue;
70 | }
71 | $this->processRequiredToRequired($rule, $enrichment);
72 | }
73 | return $enrichment;
74 | }
75 |
76 | if ($field instanceof Number) {
77 | $enrichment = [];
78 | foreach ($rules as $rule) {
79 | if ($this->hasWhen($rule)) {
80 | continue;
81 | }
82 | $this->processRequiredToRequired($rule, $enrichment);
83 | $this->processAbstractNumberToMinMax($rule, $enrichment);
84 | }
85 | return $enrichment;
86 | }
87 |
88 | if ($field instanceof Password) {
89 | $enrichment = [];
90 | foreach ($rules as $rule) {
91 | if ($this->hasWhen($rule)) {
92 | continue;
93 | }
94 | $this->processRequiredToRequired($rule, $enrichment);
95 | $this->processLengthToMinMaxLength($rule, $enrichment);
96 | $this->processRegexToPattern($rule, $enrichment);
97 | }
98 | return $enrichment;
99 | }
100 |
101 | if ($field instanceof Range) {
102 | $enrichment = [];
103 | foreach ($rules as $rule) {
104 | if ($this->hasWhen($rule)) {
105 | continue;
106 | }
107 | $this->processRequiredToRequired($rule, $enrichment);
108 | $this->processAbstractNumberToMinMax($rule, $enrichment);
109 | }
110 | return $enrichment;
111 | }
112 |
113 | if ($field instanceof Select) {
114 | $enrichment = [];
115 | foreach ($rules as $rule) {
116 | if ($this->hasWhen($rule)) {
117 | continue;
118 | }
119 | $this->processRequiredToRequired($rule, $enrichment);
120 | }
121 | return $enrichment;
122 | }
123 |
124 | if ($field instanceof Telephone) {
125 | $enrichment = [];
126 | foreach ($rules as $rule) {
127 | if ($this->hasWhen($rule)) {
128 | continue;
129 | }
130 | $this->processRequiredToRequired($rule, $enrichment);
131 | $this->processLengthToMinMaxLength($rule, $enrichment);
132 | $this->processRegexToPattern($rule, $enrichment);
133 | }
134 | return $enrichment;
135 | }
136 |
137 | if ($field instanceof Text) {
138 | $enrichment = [];
139 | foreach ($rules as $rule) {
140 | if ($this->hasWhen($rule)) {
141 | continue;
142 | }
143 | $this->processRequiredToRequired($rule, $enrichment);
144 | $this->processLengthToMinMaxLength($rule, $enrichment);
145 | $this->processRegexToPattern($rule, $enrichment);
146 | }
147 | return $enrichment;
148 | }
149 |
150 | if ($field instanceof Textarea) {
151 | $enrichment = [];
152 | foreach ($rules as $rule) {
153 | if ($this->hasWhen($rule)) {
154 | continue;
155 | }
156 | $this->processRequiredToRequired($rule, $enrichment);
157 | $this->processLengthToMinMaxLength($rule, $enrichment);
158 | }
159 | return $enrichment;
160 | }
161 |
162 | if ($field instanceof Url) {
163 | $enrichment = [];
164 | $processedUrl = false;
165 | foreach ($rules as $rule) {
166 | if ($this->hasWhen($rule)) {
167 | continue;
168 | }
169 | $this->processRequiredToRequired($rule, $enrichment);
170 | $this->processLengthToMinMaxLength($rule, $enrichment);
171 | $processedUrl = $processedUrl || $this->processUrlToPattern($rule, $enrichment);
172 | if (!$processedUrl) {
173 | $this->processRegexToPattern($rule, $enrichment);
174 | }
175 | }
176 | return $enrichment;
177 | }
178 |
179 | return null;
180 | }
181 |
182 | /**
183 | * @psalm-param EnrichmentType $enrichment
184 | */
185 | private function processRequiredToRequired(mixed $rule, array &$enrichment): void
186 | {
187 | if ($rule instanceof Required) {
188 | $enrichment['inputAttributes']['required'] = true;
189 | }
190 | }
191 |
192 | /**
193 | * @psalm-param EnrichmentType $enrichment
194 | */
195 | private function processLengthToMinMaxLength(mixed $rule, array &$enrichment): void
196 | {
197 | if ($rule instanceof Length) {
198 | if (null !== $min = $rule->getMin()) {
199 | $enrichment['inputAttributes']['minlength'] = $min;
200 | }
201 | if (null !== $max = $rule->getMax()) {
202 | $enrichment['inputAttributes']['maxlength'] = $max;
203 | }
204 | }
205 | }
206 |
207 | /**
208 | * @psalm-param EnrichmentType $enrichment
209 | */
210 | private function processRegexToPattern(mixed $rule, array &$enrichment): void
211 | {
212 | if ($rule instanceof Regex && !$rule->isNot()) {
213 | $enrichment['inputAttributes']['pattern'] = Html::normalizeRegexpPattern($rule->getPattern());
214 | }
215 | }
216 |
217 | /**
218 | * @psalm-param EnrichmentType $enrichment
219 | */
220 | private function processUrlToPattern(mixed $rule, array &$enrichment): bool
221 | {
222 | if ($rule instanceof UrlRule && !$rule->isIdnEnabled()) {
223 | $enrichment['inputAttributes']['pattern'] = Html::normalizeRegexpPattern($rule->getPattern());
224 | return true;
225 | }
226 | return false;
227 | }
228 |
229 | /**
230 | * @psalm-param EnrichmentType $enrichment
231 | */
232 | private function processAbstractNumberToMinMax(mixed $rule, array &$enrichment): void
233 | {
234 | if ($rule instanceof AbstractNumber) {
235 | if (null !== $min = $rule->getMin()) {
236 | $enrichment['inputAttributes']['min'] = $min;
237 | }
238 | if (null !== $max = $rule->getMax()) {
239 | $enrichment['inputAttributes']['max'] = $max;
240 | }
241 | }
242 | }
243 |
244 | private function hasWhen(mixed $rule): bool
245 | {
246 | return $rule instanceof WhenInterface && $rule->getWhen() !== null;
247 | }
248 | }
249 |
--------------------------------------------------------------------------------