├── 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 | Yii 4 | 5 |

Yii Form Model

6 |
7 |

8 | 9 | [![Latest Stable Version](https://poser.pugx.org/yiisoft/form-model/v)](https://packagist.org/packages/yiisoft/form-model) 10 | [![Total Downloads](https://poser.pugx.org/yiisoft/form-model/downloads)](https://packagist.org/packages/yiisoft/form-model) 11 | [![Build status](https://github.com/yiisoft/form-model/actions/workflows/build.yml/badge.svg)](https://github.com/yiisoft/form-model/actions/workflows/build.yml) 12 | [![Code Coverage](https://codecov.io/gh/yiisoft/form-model/branch/master/graph/badge.svg)](https://codecov.io/gh/yiisoft/form-model) 13 | [![Mutation testing badge](https://img.shields.io/endpoint?style=flat&url=https%3A%2F%2Fbadge-api.stryker-mutator.io%2Fgithub.com%2Fyiisoft%2Fform-model%2Fmaster)](https://dashboard.stryker-mutator.io/reports/github.com/yiisoft/form-model/master) 14 | [![static analysis](https://github.com/yiisoft/form-model/workflows/static%20analysis/badge.svg)](https://github.com/yiisoft/form-model/actions?query=workflow%3A%22static+analysis%22) 15 | [![type-coverage](https://shepherd.dev/github/yiisoft/form-model/coverage.svg)](https://shepherd.dev/github/yiisoft/form-model) 16 | [![psalm-level](https://shepherd.dev/github/yiisoft/form-model/level.svg)](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 | [![Open Collective](https://img.shields.io/badge/Open%20Collective-sponsor-7eadf1?logo=open%20collective&logoColor=7eadf1&labelColor=555555)](https://opencollective.com/yiisoft) 116 | 117 | ## Follow updates 118 | 119 | [![Official website](https://img.shields.io/badge/Powered_by-Yii_Framework-green.svg?style=flat)](https://www.yiiframework.com/) 120 | [![Twitter](https://img.shields.io/badge/twitter-follow-1DA1F2?logo=twitter&logoColor=1DA1F2&labelColor=555555?style=flat)](https://twitter.com/yiiframework) 121 | [![Telegram](https://img.shields.io/badge/telegram-join-1DA1F2?style=flat&logo=telegram)](https://t.me/yii3en) 122 | [![Facebook](https://img.shields.io/badge/facebook-join-1DA1F2?style=flat&logo=facebook&logoColor=ffffff)](https://www.facebook.com/groups/yiitalk) 123 | [![Slack](https://img.shields.io/badge/slack-join-1DA1F2?style=flat&logo=slack)](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 | --------------------------------------------------------------------------------