From 0f8727fb06b946f8b888c14d8b8be388752b5ab4 Mon Sep 17 00:00:00 2001
From: Sergei Predvoditelev
Date: Fri, 1 Dec 2023 16:19:57 +0300
Subject: [PATCH 1/5] improve
---
.gitignore | 3 +--
CHANGELOG.md | 2 +-
README.md | 6 +++---
composer.json | 16 ++++++++--------
phpunit.xml.dist | 33 ++++++++++++++++-----------------
5 files changed, 29 insertions(+), 31 deletions(-)
diff --git a/.gitignore b/.gitignore
index 8c81ab2..b37074d 100644
--- a/.gitignore
+++ b/.gitignore
@@ -27,5 +27,4 @@ phpunit.phar
# local phpunit config
/phpunit.xml
# phpunit cache
-.phpunit.result.cache
-/phpunit.cache
+/.phpunit.cache/
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 6a8fbfe..7cd14a2 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -1,4 +1,4 @@
-# _____ Change Log
+# Yii Form Model Change Log
## 1.0.0 under development
diff --git a/README.md b/README.md
index 8e5b84f..e6e8bbb 100644
--- a/README.md
+++ b/README.md
@@ -2,7 +2,7 @@
- Yii _____
+ Yii Form Model
@@ -26,7 +26,7 @@ The package ...
The package could be installed with composer:
```shell
-composer require yiisoft/_____
+composer require yiisoft/form-model
```
## General usage
@@ -74,7 +74,7 @@ Use [ComposerRequireChecker](https://github.com/maglnet/ComposerRequireChecker)
## License
-The Yii _____ is free software. It is released under the terms of the BSD License.
+The Yii Form Model is free software. It is released under the terms of the BSD License.
Please see [`LICENSE`](./LICENSE.md) for more information.
Maintained by [Yii Software](https://www.yiiframework.com/).
diff --git a/composer.json b/composer.json
index 461dab0..1556cc5 100644
--- a/composer.json
+++ b/composer.json
@@ -1,5 +1,5 @@
{
- "name": "yiisoft/_____",
+ "name": "yiisoft/form-model",
"type": "library",
"description": "_____",
"keywords": [
@@ -31,21 +31,21 @@
"php": "^8.1"
},
"require-dev": {
- "maglnet/composer-require-checker": "^4.6",
- "phpunit/phpunit": "^9.5",
- "rector/rector": "^0.17.11",
- "roave/infection-static-analysis-plugin": "^1.16",
+ "maglnet/composer-require-checker": "^4.7",
+ "phpunit/phpunit": "^10.5",
+ "rector/rector": "^0.18.11",
+ "roave/infection-static-analysis-plugin": "^1.34",
"spatie/phpunit-watcher": "^1.23",
- "vimeo/psalm": "^4.30|^5.7"
+ "vimeo/psalm": "^5.16"
},
"autoload": {
"psr-4": {
- "Yiisoft\\_____\\": "src"
+ "Yiisoft\\FormModel\\": "src"
}
},
"autoload-dev": {
"psr-4": {
- "Yiisoft\\_____\\Tests\\": "tests"
+ "Yiisoft\\FormModel\\Tests\\": "tests"
}
},
"config": {
diff --git a/phpunit.xml.dist b/phpunit.xml.dist
index 976580a..4c10342 100644
--- a/phpunit.xml.dist
+++ b/phpunit.xml.dist
@@ -1,31 +1,30 @@
-
-
-
+
./tests
-
+
+
From 675e5b28cc6be45606956d5a05d4bc36f9830d9f Mon Sep 17 00:00:00 2001
From: Sergei Predvoditelev
Date: Fri, 1 Dec 2023 17:23:22 +0300
Subject: [PATCH 2/5] Copy from `yiisoft/form`
---
composer.json | 11 +-
src/.gitkeep | 0
...ropertyNotSupportNestedValuesException.php | 18 +
.../StaticObjectPropertyException.php | 13 +
.../UndefinedArrayElementException.php | 13 +
.../UndefinedObjectPropertyException.php | 13 +
src/Exception/ValueNotFoundException.php | 11 +
src/Field.php | 315 +++++++++++++
src/Field/ErrorSummary.php | 241 ++++++++++
src/FormHydrator.php | 50 ++
src/FormModel.php | 328 +++++++++++++
src/FormModelInputData.php | 176 +++++++
src/FormModelInterface.php | 114 +++++
src/ValidationRulesEnricher.php | 311 +++++++++++++
tests/.gitkeep | 0
.../UndefinedObjectPropertyExceptionTest.php | 21 +
tests/FieldTest.php | 433 ++++++++++++++++++
tests/FormModelInputDataTest.php | 41 ++
tests/FormModelTest.php | 417 +++++++++++++++++
tests/Support/Dto/Coordinates.php | 24 +
tests/Support/Form/CustomFormNameForm.php | 15 +
tests/Support/Form/DefaultFormNameForm.php | 11 +
tests/Support/Form/FormWithNestedProperty.php | 65 +++
.../Support/Form/FormWithNestedStructures.php | 24 +
tests/Support/Form/LoginForm.php | 99 ++++
tests/Support/Form/NestedForm.php | 21 +
tests/Support/Form/NonNamespacedForm.php | 9 +
tests/Support/Form/TestForm.php | 77 ++++
tests/Support/StubInputField.php | 20 +
tests/Support/TestHelper.php | 29 ++
30 files changed, 2918 insertions(+), 2 deletions(-)
delete mode 100644 src/.gitkeep
create mode 100644 src/Exception/PropertyNotSupportNestedValuesException.php
create mode 100644 src/Exception/StaticObjectPropertyException.php
create mode 100644 src/Exception/UndefinedArrayElementException.php
create mode 100644 src/Exception/UndefinedObjectPropertyException.php
create mode 100644 src/Exception/ValueNotFoundException.php
create mode 100644 src/Field.php
create mode 100644 src/Field/ErrorSummary.php
create mode 100644 src/FormHydrator.php
create mode 100644 src/FormModel.php
create mode 100644 src/FormModelInputData.php
create mode 100644 src/FormModelInterface.php
create mode 100644 src/ValidationRulesEnricher.php
delete mode 100644 tests/.gitkeep
create mode 100644 tests/Exception/UndefinedObjectPropertyExceptionTest.php
create mode 100644 tests/FieldTest.php
create mode 100644 tests/FormModelInputDataTest.php
create mode 100644 tests/FormModelTest.php
create mode 100644 tests/Support/Dto/Coordinates.php
create mode 100644 tests/Support/Form/CustomFormNameForm.php
create mode 100644 tests/Support/Form/DefaultFormNameForm.php
create mode 100644 tests/Support/Form/FormWithNestedProperty.php
create mode 100644 tests/Support/Form/FormWithNestedStructures.php
create mode 100644 tests/Support/Form/LoginForm.php
create mode 100644 tests/Support/Form/NestedForm.php
create mode 100644 tests/Support/Form/NonNamespacedForm.php
create mode 100644 tests/Support/Form/TestForm.php
create mode 100644 tests/Support/StubInputField.php
create mode 100644 tests/Support/TestHelper.php
diff --git a/composer.json b/composer.json
index 1556cc5..b871423 100644
--- a/composer.json
+++ b/composer.json
@@ -28,7 +28,13 @@
"minimum-stability": "dev",
"prefer-stable": true,
"require": {
- "php": "^8.1"
+ "php": "^8.1",
+ "yiisoft/form": "^1.0@dev",
+ "yiisoft/html": "^3.3",
+ "yiisoft/hydrator": "dev-master",
+ "yiisoft/hydrator-validator": "dev-master",
+ "yiisoft/strings": "^2.3",
+ "yiisoft/validator": "^1.1"
},
"require-dev": {
"maglnet/composer-require-checker": "^4.7",
@@ -36,7 +42,8 @@
"rector/rector": "^0.18.11",
"roave/infection-static-analysis-plugin": "^1.34",
"spatie/phpunit-watcher": "^1.23",
- "vimeo/psalm": "^5.16"
+ "vimeo/psalm": "^5.16",
+ "yiisoft/test-support": "^3.0"
},
"autoload": {
"psr-4": {
diff --git a/src/.gitkeep b/src/.gitkeep
deleted file mode 100644
index e69de29..0000000
diff --git a/src/Exception/PropertyNotSupportNestedValuesException.php b/src/Exception/PropertyNotSupportNestedValuesException.php
new file mode 100644
index 0000000..8287fe5
--- /dev/null
+++ b/src/Exception/PropertyNotSupportNestedValuesException.php
@@ -0,0 +1,18 @@
+value;
+ }
+}
diff --git a/src/Exception/StaticObjectPropertyException.php b/src/Exception/StaticObjectPropertyException.php
new file mode 100644
index 0000000..d06bc5b
--- /dev/null
+++ b/src/Exception/StaticObjectPropertyException.php
@@ -0,0 +1,13 @@
+content($content);
+ }
+
+ return $field;
+ }
+
+ final public static function buttonGroup(array $config = [], ?string $theme = null): ButtonGroup
+ {
+ return ButtonGroup::widget(config: $config, theme: $theme ?? static::DEFAULT_THEME);
+ }
+
+ final public static function checkbox(
+ FormModelInterface $formModel,
+ string $property,
+ array $config = [],
+ ?string $theme = null,
+ ): Checkbox {
+ return Checkbox::widget(config: $config, theme: $theme ?? static::DEFAULT_THEME)
+ ->inputData(new FormModelInputData($formModel, $property));
+ }
+
+ final public static function checkboxList(
+ FormModelInterface $formModel,
+ string $property,
+ array $config = [],
+ ?string $theme = null,
+ ): CheckboxList {
+ return CheckboxList::widget(config: $config, theme: $theme ?? static::DEFAULT_THEME)
+ ->inputData(new FormModelInputData($formModel, $property));
+ }
+
+ final public static function date(
+ FormModelInterface $formModel,
+ string $property,
+ array $config = [],
+ ?string $theme = null,
+ ): Date {
+ return Date::widget(config: $config, theme: $theme ?? static::DEFAULT_THEME)
+ ->inputData(new FormModelInputData($formModel, $property));
+ }
+
+ final public static function dateTime(
+ FormModelInterface $formModel,
+ string $property,
+ array $config = [],
+ ?string $theme = null,
+ ): DateTime {
+ return DateTime::widget(config: $config, theme: $theme ?? static::DEFAULT_THEME)
+ ->inputData(new FormModelInputData($formModel, $property));
+ }
+
+ final public static function dateTimeLocal(
+ FormModelInterface $formModel,
+ string $property,
+ array $config = [],
+ ?string $theme = null,
+ ): DateTimeLocal {
+ return DateTimeLocal::widget(config: $config, theme: $theme ?? static::DEFAULT_THEME)
+ ->inputData(new FormModelInputData($formModel, $property));
+ }
+
+ final public static function email(
+ FormModelInterface $formModel,
+ string $property,
+ array $config = [],
+ ?string $theme = null,
+ ): Email {
+ return Email::widget(config: $config, theme: $theme ?? static::DEFAULT_THEME)
+ ->inputData(new FormModelInputData($formModel, $property));
+ }
+
+ final public static function errorSummary(
+ FormModelInterface $formModel,
+ array $config = [],
+ ?string $theme = null,
+ ): ErrorSummary {
+ return ErrorSummary::widget(config: $config, theme: $theme ?? static::DEFAULT_THEME)
+ ->validationResult($formModel->getValidationResult());
+ }
+
+ final public static function fieldset(array $config = [], ?string $theme = null): Fieldset
+ {
+ return Fieldset::widget(config: $config, theme: $theme ?? static::DEFAULT_THEME);
+ }
+
+ final public static function file(
+ FormModelInterface $formModel,
+ string $property,
+ array $config = [],
+ ?string $theme = null,
+ ): File {
+ return File::widget(config: $config, theme: $theme ?? static::DEFAULT_THEME)
+ ->inputData(new FormModelInputData($formModel, $property));
+ }
+
+ final public static function hidden(
+ FormModelInterface $formModel,
+ string $property,
+ array $config = [],
+ ?string $theme = null,
+ ): Hidden {
+ return Hidden::widget(config: $config, theme: $theme ?? static::DEFAULT_THEME)
+ ->inputData(new FormModelInputData($formModel, $property));
+ }
+
+ final public static function image(array $config = [], ?string $theme = null): Image
+ {
+ return Image::widget(config: $config, theme: $theme ?? static::DEFAULT_THEME);
+ }
+
+ final public static function number(
+ FormModelInterface $formModel,
+ string $property,
+ array $config = [],
+ ?string $theme = null,
+ ): Number {
+ return Number::widget(config: $config, theme: $theme ?? static::DEFAULT_THEME)
+ ->inputData(new FormModelInputData($formModel, $property));
+ }
+
+ final public static function password(
+ FormModelInterface $formModel,
+ string $property,
+ array $config = [],
+ ?string $theme = null,
+ ): Password {
+ return Password::widget(config: $config, theme: $theme ?? static::DEFAULT_THEME)
+ ->inputData(new FormModelInputData($formModel, $property));
+ }
+
+ final public static function radioList(
+ FormModelInterface $formModel,
+ string $property,
+ array $config = [],
+ ?string $theme = null,
+ ): RadioList {
+ return RadioList::widget(config: $config, theme: $theme ?? static::DEFAULT_THEME)
+ ->inputData(new FormModelInputData($formModel, $property));
+ }
+
+ final public static function range(
+ FormModelInterface $formModel,
+ string $property,
+ array $config = [],
+ ?string $theme = null,
+ ): Range {
+ return Range::widget(config: $config, theme: $theme ?? static::DEFAULT_THEME)
+ ->inputData(new FormModelInputData($formModel, $property));
+ }
+
+ final public static function resetButton(
+ ?string $content = null,
+ array $config = [],
+ ?string $theme = null,
+ ): ResetButton {
+ $field = ResetButton::widget(config: $config, theme: $theme ?? static::DEFAULT_THEME);
+
+ if ($content !== null) {
+ $field = $field->content($content);
+ }
+
+ return $field;
+ }
+
+ final public static function select(
+ FormModelInterface $formModel,
+ string $property,
+ array $config = [],
+ ?string $theme = null,
+ ): Select {
+ return Select::widget(config: $config, theme: $theme ?? static::DEFAULT_THEME)
+ ->inputData(new FormModelInputData($formModel, $property));
+ }
+
+ final public static function submitButton(
+ ?string $content = null,
+ array $config = [],
+ ?string $theme = null,
+ ): SubmitButton {
+ $field = SubmitButton::widget(config: $config, theme: $theme ?? static::DEFAULT_THEME);
+
+ if ($content !== null) {
+ $field = $field->content($content);
+ }
+
+ return $field;
+ }
+
+ final public static function telephone(
+ FormModelInterface $formModel,
+ string $property,
+ array $config = [],
+ ?string $theme = null,
+ ): Telephone {
+ return Telephone::widget(config: $config, theme: $theme ?? static::DEFAULT_THEME)
+ ->inputData(new FormModelInputData($formModel, $property));
+ }
+
+ final public static function text(
+ FormModelInterface $formModel,
+ string $property,
+ array $config = [],
+ ?string $theme = null,
+ ): Text {
+ return Text::widget(config: $config, theme: $theme ?? static::DEFAULT_THEME)
+ ->inputData(new FormModelInputData($formModel, $property));
+ }
+
+ final public static function textarea(
+ FormModelInterface $formModel,
+ string $property,
+ array $config = [],
+ ?string $theme = null,
+ ): Textarea {
+ return Textarea::widget(config: $config, theme: $theme ?? static::DEFAULT_THEME)
+ ->inputData(new FormModelInputData($formModel, $property));
+ }
+
+ final public static function time(
+ FormModelInterface $formModel,
+ string $property,
+ array $config = [],
+ ?string $theme = null,
+ ): Time {
+ return Time::widget(config: $config, theme: $theme ?? static::DEFAULT_THEME)
+ ->inputData(new FormModelInputData($formModel, $property));
+ }
+
+ final public static function url(
+ FormModelInterface $formModel,
+ string $property,
+ array $config = [],
+ ?string $theme = null,
+ ): Url {
+ return Url::widget(config: $config, theme: $theme ?? static::DEFAULT_THEME)
+ ->inputData(new FormModelInputData($formModel, $property));
+ }
+
+ final public static function label(
+ FormModelInterface $formModel,
+ string $property,
+ array $config = [],
+ ?string $theme = null,
+ ): Label {
+ return Label::widget(config: $config, theme: $theme ?? static::DEFAULT_THEME)
+ ->inputData(new FormModelInputData($formModel, $property));
+ }
+
+ final public static function hint(
+ FormModelInterface $formModel,
+ string $property,
+ array $config = [],
+ ?string $theme = null,
+ ): Hint {
+ return Hint::widget(config: $config, theme: $theme ?? static::DEFAULT_THEME)
+ ->inputData(new FormModelInputData($formModel, $property));
+ }
+
+ final public static function error(
+ FormModelInterface $formModel,
+ string $property,
+ array $config = [],
+ ?string $theme = null,
+ ): Error {
+ return Error::widget(config: $config, theme: $theme ?? static::DEFAULT_THEME)
+ ->inputData(new FormModelInputData($formModel, $property));
+ }
+}
diff --git a/src/Field/ErrorSummary.php b/src/Field/ErrorSummary.php
new file mode 100644
index 0000000..5799638
--- /dev/null
+++ b/src/Field/ErrorSummary.php
@@ -0,0 +1,241 @@
+validationResult = $result;
+ return $new;
+ }
+
+ /**
+ * Whether error content should be HTML-encoded.
+ */
+ public function encode(bool $value): self
+ {
+ $new = clone $this;
+ $new->encode = $value;
+ return $new;
+ }
+
+ /**
+ * Whether to show all errors.
+ */
+ public function showAllErrors(bool $value = true): self
+ {
+ $new = clone $this;
+ $new->showAllErrors = $value;
+ return $new;
+ }
+
+ /**
+ * Specific properties to be filtered out when rendering the error summary.
+ *
+ * @param array $names The property names to be included in error summary.
+ */
+ public function onlyProperties(string ...$names): self
+ {
+ $new = clone $this;
+ $new->onlyProperties = $names;
+ return $new;
+ }
+
+ /**
+ * Use only common errors when rendering the error summary.
+ */
+ public function onlyCommonErrors(): self
+ {
+ $new = clone $this;
+ $new->onlyProperties = [''];
+ return $new;
+ }
+
+ /**
+ * Set the footer text for the error summary
+ */
+ public function footer(string $value): self
+ {
+ $new = clone $this;
+ $new->footer = $value;
+ return $new;
+ }
+
+ /**
+ * Set footer attributes for the error summary.
+ *
+ * @param array $values Attribute values indexed by attribute names.
+ *
+ * See {@see Html::renderTagAttributes} for details on how attributes are being rendered.
+ */
+ public function footerAttributes(array $values): self
+ {
+ $new = clone $this;
+ $new->footerAttributes = $values;
+ return $new;
+ }
+
+ /**
+ * Set the header text for the error summary
+ */
+ public function header(string $value): self
+ {
+ $new = clone $this;
+ $new->header = $value;
+ return $new;
+ }
+
+ /**
+ * Set header attributes for the error summary.
+ *
+ * @param array $values Attribute values indexed by attribute names.
+ *
+ * See {@see Html::renderTagAttributes} for details on how attributes are being rendered.
+ */
+ public function headerAttributes(array $values): self
+ {
+ $new = clone $this;
+ $new->headerAttributes = $values;
+ return $new;
+ }
+
+ /**
+ * Set errors list container attributes.
+ *
+ * @param array $attributes Attribute values indexed by attribute names.
+ *
+ * See {@see Html::renderTagAttributes} for details on how attributes are being rendered.
+ */
+ public function listAttributes(array $attributes): self
+ {
+ $new = clone $this;
+ $new->listAttributes = $attributes;
+ return $new;
+ }
+
+ /**
+ * Add one or more CSS classes to the list container tag.
+ *
+ * @param string|null ...$class One or many CSS classes.
+ */
+ public function addListClass(?string ...$class): self
+ {
+ $new = clone $this;
+ Html::addCssClass($new->listAttributes, $class);
+ return $new;
+ }
+
+ /**
+ * Replace current list container tag CSS classes with a new set of classes.
+ *
+ * @param string|null ...$class One or many CSS classes.
+ */
+ public function listClass(?string ...$class): static
+ {
+ $new = clone $this;
+ $new->listAttributes['class'] = array_filter($class, static fn ($c) => $c !== null);
+ return $new;
+ }
+
+ protected function generateContent(): ?string
+ {
+ $messages = $this->collectErrors();
+ if (empty($messages)) {
+ return null;
+ }
+
+ $content = [];
+
+ if ($this->header !== '') {
+ $content[] = Html::p($this->header, $this->headerAttributes)->render();
+ }
+
+ $content[] = Html::ul()
+ ->attributes($this->listAttributes)
+ ->strings($messages, [], $this->encode)
+ ->render();
+
+ if ($this->footer !== '') {
+ $content[] = Html::p($this->footer, $this->footerAttributes)->render();
+ }
+
+ return implode("\n", $content);
+ }
+
+ /**
+ * Return array of the validation errors.
+ *
+ * @return string[] Array of the validation errors.
+ */
+ private function collectErrors(): array
+ {
+ if ($this->validationResult === null) {
+ return [];
+ }
+
+ if ($this->showAllErrors) {
+ $errors = $this->getAllErrors();
+ } elseif ($this->onlyProperties !== []) {
+ $errors = array_intersect_key($this->getFirstErrors(), array_flip($this->onlyProperties));
+ } else {
+ $errors = $this->getFirstErrors();
+ }
+
+ /**
+ * If there are the same error messages for different properties, array_unique will leave gaps between
+ * sequential keys. Applying array_values to reorder array keys.
+ *
+ * @var string[]
+ */
+ return array_values(array_unique($errors));
+ }
+
+ private function getAllErrors(): array
+ {
+ if ($this->onlyProperties === []) {
+ return $this->validationResult?->getErrorMessages() ?? [];
+ }
+
+ $result = [];
+ foreach ($this->validationResult?->getErrorMessagesIndexedByPath() ?? [] as $property => $messages) {
+ if (in_array($property, $this->onlyProperties, true)) {
+ $result[] = $messages;
+ }
+ }
+
+ return array_merge(...$result);
+ }
+
+ private function getFirstErrors(): array
+ {
+ $result = [];
+ foreach ($this->validationResult?->getErrorMessagesIndexedByPath() ?? [] as $property => $messages) {
+ if (isset($messages[0])) {
+ $result[$property] = $messages[0];
+ }
+ }
+ return $result;
+ }
+}
diff --git a/src/FormHydrator.php b/src/FormHydrator.php
new file mode 100644
index 0000000..9e89629
--- /dev/null
+++ b/src/FormHydrator.php
@@ -0,0 +1,50 @@
+getFormName();
+ if ($scope === '') {
+ $hydrateData = $data;
+ } else {
+ if (!isset($data[$scope]) || !is_array($data[$scope])) {
+ return false;
+ }
+ $hydrateData = $data[$scope];
+ }
+
+ $this->hydrator->hydrate($model, new ArrayData($hydrateData, $map, $strict));
+
+ return true;
+ }
+}
diff --git a/src/FormModel.php b/src/FormModel.php
new file mode 100644
index 0000000..72b8039
--- /dev/null
+++ b/src/FormModel.php
@@ -0,0 +1,328 @@
+readPropertyMetaValue(self::META_HINT, $property) ?? '';
+ }
+
+ /**
+ * Returns the property hints.
+ *
+ * Property hints are mainly used for display purpose. For example, given a property `isPublic`, we can declare
+ * a hint `Whether the post should be visible for not logged-in users`, which provides user-friendly description of
+ * the property meaning and can be displayed to end users.
+ *
+ * Unlike label hint will not be generated, if its explicit declaration is omitted.
+ *
+ * Note, in order to inherit hints defined in the parent class, a child class needs to merge the parent hints with
+ * child hints using functions such as `array_merge()`.
+ *
+ * @return array Property hints (name => hint)
+ *
+ * @psalm-return array
+ */
+ public function getPropertyHints(): array
+ {
+ return [];
+ }
+
+ /**
+ * Returns the text label for the specified property.
+ *
+ * @param string $property The property name.
+ *
+ * @return string The property label.
+ */
+ public function getPropertyLabel(string $property): string
+ {
+ return $this->readPropertyMetaValue(self::META_LABEL, $property) ?? $this->generatePropertyLabel($property);
+ }
+
+ /**
+ * Returns the property labels.
+ *
+ * Attribute labels are mainly used for display purpose. For example, given a property `firstName`, we can
+ * declare a label `First Name` which is more user-friendly and can be displayed to end users.
+ *
+ * By default, a property label is generated automatically. This method allows you to
+ * explicitly specify property labels.
+ *
+ * Note, in order to inherit labels defined in the parent class, a child class needs to merge the parent labels
+ * with child labels using functions such as `array_merge()`.
+ *
+ * @return array Property labels (name => label)
+ *
+ * @psalm-return array
+ */
+ public function getPropertyLabels(): array
+ {
+ return [];
+ }
+
+ /**
+ * Returns the text placeholder for the specified property.
+ *
+ * @param string $property The property name.
+ *
+ * @return string The property placeholder.
+ */
+ public function getPropertyPlaceholder(string $property): string
+ {
+ return $this->readPropertyMetaValue(self::META_PLACEHOLDER, $property) ?? '';
+ }
+
+ public function getPropertyValue(string $property): mixed
+ {
+ try {
+ return $this->readPropertyValue($property);
+ } catch (PropertyNotSupportNestedValuesException $exception) {
+ return $exception->getValue() === null
+ ? null
+ : throw $exception;
+ } catch (UndefinedArrayElementException) {
+ return null;
+ }
+ }
+
+ /**
+ * Returns the property placeholders.
+ *
+ * @return array Property placeholders (name => placeholder)
+ *
+ * @psalm-return array
+ */
+ public function getPropertyPlaceholders(): array
+ {
+ return [];
+ }
+
+ /**
+ * Returns the form name that this model class should use.
+ *
+ * The form name is mainly used by {@see \Yiisoft\Form\InputData\HtmlForm} to determine how to name the input
+ * fields for the properties in a model.
+ * If the form name is "A" and a property name is "b", then the corresponding input name would be "A[b]".
+ * If the form name is an empty string, then the input name would be "b".
+ *
+ * The purpose of the above naming schema is that for forms which contain multiple different models, the properties
+ * of each model are grouped in sub-arrays of the POST-data, and it is easier to differentiate between them.
+ *
+ * By default, this method returns the model class name (without the namespace part) as the form name. You may
+ * override it when the model is used in different forms.
+ *
+ * @return string The form name of this model class.
+ */
+ public function getFormName(): string
+ {
+ if (str_contains(static::class, '@anonymous')) {
+ return '';
+ }
+
+ $className = strrchr(static::class, '\\');
+ if ($className === false) {
+ return static::class;
+ }
+
+ return substr($className, 1);
+ }
+
+ /**
+ * If there is such property in the set.
+ */
+ public function hasProperty(string $property): bool
+ {
+ try {
+ $this->readPropertyValue($property);
+ } catch (ValueNotFoundException) {
+ return false;
+ }
+ return true;
+ }
+
+ public function isValid(): bool
+ {
+ return (bool) $this->getValidationResult()?->isValid();
+ }
+
+ /**
+ * @throws UndefinedArrayElementException
+ * @throws UndefinedObjectPropertyException
+ * @throws StaticObjectPropertyException
+ * @throws PropertyNotSupportNestedValuesException
+ * @throws ValueNotFoundException
+ */
+ private function readPropertyValue(string $path): mixed
+ {
+ $path = $this->normalizePath($path);
+
+ $value = $this;
+ $keys = [[static::class, $this]];
+ foreach ($path as $key) {
+ $keys[] = [$key, $value];
+
+ if (is_array($value)) {
+ if (array_key_exists($key, $value)) {
+ $value = $value[$key];
+ continue;
+ }
+ throw new UndefinedArrayElementException($this->makePropertyPathString($keys));
+ }
+
+ if (is_object($value)) {
+ $class = new ReflectionClass($value);
+ try {
+ $property = $class->getProperty($key);
+ } catch (ReflectionException) {
+ throw new UndefinedObjectPropertyException($this->makePropertyPathString($keys));
+ }
+ if ($property->isStatic()) {
+ throw new StaticObjectPropertyException($this->makePropertyPathString($keys));
+ }
+ $value = $property->getValue($value);
+ continue;
+ }
+
+ array_pop($keys);
+ throw new PropertyNotSupportNestedValuesException($this->makePropertyPathString($keys), $value);
+ }
+
+ return $value;
+ }
+
+ private function readPropertyMetaValue(int $metaKey, string $path): ?string
+ {
+ $path = $this->normalizePath($path);
+
+ $value = $this;
+ $n = 0;
+ foreach ($path as $key) {
+ if ($value instanceof self) {
+ $nestedAttribute = implode('.', array_slice($path, $n));
+ $data = match ($metaKey) {
+ self::META_LABEL => $value->getPropertyLabels(),
+ self::META_HINT => $value->getPropertyHints(),
+ self::META_PLACEHOLDER => $value->getPropertyPlaceholders(),
+ default => throw new InvalidArgumentException('Invalid meta key.'),
+ };
+ if (array_key_exists($nestedAttribute, $data)) {
+ return $data[$nestedAttribute];
+ }
+ }
+
+ $class = new ReflectionClass($value);
+ try {
+ $property = $class->getProperty($key);
+ } catch (ReflectionException) {
+ return null;
+ }
+ if ($property->isStatic()) {
+ return null;
+ }
+
+ $value = $property->getValue($value);
+ if (!is_object($value)) {
+ return null;
+ }
+
+ $n++;
+ }
+
+ return null;
+ }
+
+ /**
+ * Generates a user-friendly property label based on the give property name.
+ *
+ * This is done by replacing underscores, dashes and dots with blanks and changing the first letter of each word to
+ * upper case.
+ *
+ * For example, 'department_name' or 'DepartmentName' will generate 'Department Name'.
+ *
+ * @param string $property The property name.
+ *
+ * @return string The property label.
+ */
+ private function generatePropertyLabel(string $property): string
+ {
+ if (self::$inflector === null) {
+ self::$inflector = new Inflector();
+ }
+
+ return StringHelper::uppercaseFirstCharacterInEachWord(
+ self::$inflector->toWords($property)
+ );
+ }
+
+ /**
+ * @return string[]
+ */
+ private function normalizePath(string $path): array
+ {
+ $path = str_replace(['][', '['], '.', rtrim($path, ']'));
+ return StringHelper::parsePath($path);
+ }
+
+ /**
+ * @psalm-param array $keys
+ */
+ private function makePropertyPathString(array $keys): string
+ {
+ $path = '';
+ foreach ($keys as $key) {
+ if ($path !== '') {
+ if (is_object($key[1])) {
+ $path .= '::' . $key[0];
+ } elseif (is_array($key[1])) {
+ $path .= '[' . $key[0] . ']';
+ }
+ } else {
+ $path = (string) $key[0];
+ }
+ }
+ return $path;
+ }
+}
diff --git a/src/FormModelInputData.php b/src/FormModelInputData.php
new file mode 100644
index 0000000..e75ebef
--- /dev/null
+++ b/src/FormModelInputData.php
@@ -0,0 +1,176 @@
+validationRules === null) {
+ $rules = RulesNormalizer::normalize(null, $this->model);
+ $this->validationRules = $rules[$this->property] ?? [];
+ }
+ return $this->validationRules;
+ }
+
+ /**
+ * Generates an appropriate input name.
+ *
+ * This method generates a name that can be used as the input name to collect user input. The name is generated
+ * according to the form and the property names. For example, if the form name is `Post`
+ * then the input name generated for the `content` property would be `Post[content]`.
+ *
+ * See {@see getPropertyName()} for explanation of property expression.
+ *
+ * @throws InvalidArgumentException If the property name contains non-word characters or empty form name for
+ * tabular inputs.
+ * @return string|null The generated input name.
+ */
+ public function getName(): ?string
+ {
+ $data = $this->parseProperty($this->property);
+ $formName = $this->model->getFormName();
+
+ if ($formName === '' && $data['prefix'] === '') {
+ return $this->property;
+ }
+
+ if ($formName !== '') {
+ return "$formName{$data['prefix']}[{$data['name']}]{$data['suffix']}";
+ }
+
+ throw new InvalidArgumentException('formName() cannot be empty for tabular inputs.');
+ }
+
+ /**
+ * @throws UndefinedObjectPropertyException
+ * @throws StaticObjectPropertyException
+ * @throws PropertyNotSupportNestedValuesException
+ * @throws ValueNotFoundException
+ */
+ public function getValue(): mixed
+ {
+ $parsedName = $this->parseProperty($this->property);
+ return $this->model->getPropertyValue($parsedName['name'] . $parsedName['suffix']);
+ }
+
+ public function getLabel(): ?string
+ {
+ return $this->model->getPropertyLabel($this->getPropertyName());
+ }
+
+ public function getHint(): ?string
+ {
+ return $this->model->getPropertyHint($this->getPropertyName());
+ }
+
+ public function getPlaceholder(): ?string
+ {
+ $placeholder = $this->model->getPropertyPlaceholder($this->getPropertyName());
+ return $placeholder === '' ? null : $placeholder;
+ }
+
+ /**
+ * Generates an appropriate input ID.
+ *
+ * This method converts the result {@see getName()} into a valid input ID.
+ *
+ * For example, if {@see getInputName()} returns `Post[content]`, this method will return `post-content`.
+ *
+ * @throws InvalidArgumentException If the property name contains non-word characters.
+ * @return string|null The generated input ID.
+ */
+ public function getId(): ?string
+ {
+ $name = $this->getName();
+ if ($name === null) {
+ return null;
+ }
+
+ $name = mb_strtolower($name, 'UTF-8');
+ return str_replace(['[]', '][', '[', ']', ' ', '.'], ['', '-', '-', '', '-', '-'], $name);
+ }
+
+ public function isValidated(): bool
+ {
+ return $this->model->getValidationResult() !== null;
+ }
+
+ public function getValidationErrors(): array
+ {
+ /** @psalm-var list */
+ return $this->model
+ ->getValidationResult()
+ ?->getAttributeErrorMessages($this->getPropertyName())
+ ?? [];
+ }
+
+ private function getPropertyName(): string
+ {
+ $property = $this->parseProperty($this->property)['name'];
+
+ if (!$this->model->hasProperty($property)) {
+ throw new InvalidArgumentException('Property "' . $property . '" does not exist.');
+ }
+
+ return $property;
+ }
+
+ /**
+ * This method parses a property expression and returns an associative array containing
+ * real property name, prefix and suffix.
+ * For example: `['name' => 'content', 'prefix' => '', 'suffix' => '[0]']`
+ *
+ * A property expression is a property name prefixed and/or suffixed with array indexes. It is mainly used in
+ * tabular data input and/or input of array type. Below are some examples:
+ *
+ * - `[0]content` is used in tabular data input to represent the "content" property for the first model in tabular
+ * input;
+ * - `dates[0]` represents the first array element of the "dates" property;
+ * - `[0]dates[0]` represents the first array element of the "dates" property for the first model in tabular
+ * input.
+ *
+ * @param string $property The property name or expression
+ *
+ * @throws InvalidArgumentException If the property name contains non-word characters.
+ * @return string[] The property name, prefix and suffix.
+ */
+ private function parseProperty(string $property): array
+ {
+ if (!preg_match('/(^|.*\])([\w\.\+\-_]+)(\[.*|$)/u', $property, $matches)) {
+ throw new InvalidArgumentException('Property name must contain word characters only.');
+ }
+ return [
+ 'name' => $matches[2],
+ 'prefix' => $matches[1],
+ 'suffix' => $matches[3],
+ ];
+ }
+}
diff --git a/src/FormModelInterface.php b/src/FormModelInterface.php
new file mode 100644
index 0000000..7c2c32e
--- /dev/null
+++ b/src/FormModelInterface.php
@@ -0,0 +1,114 @@
+ hint)
+ *
+ * @psalm-return array
+ */
+ public function getPropertyHints(): array;
+
+ /**
+ * Returns the text label for the specified property.
+ *
+ * @param string $property The property name.
+ *
+ * @return string The property label.
+ */
+ public function getPropertyLabel(string $property): string;
+
+ /**
+ * Returns the property labels.
+ *
+ * Property labels are mainly used for display purpose. For example, given a property `firstName`, we can
+ * declare a label `First Name` which is more user-friendly and can be displayed to end users.
+ *
+ * By default, a property label is generated automatically. This method allows you to
+ * explicitly specify property labels.
+ *
+ * Note, in order to inherit labels defined in the parent class, a child class needs to merge the parent labels
+ * with child labels using functions such as `array_merge()`.
+ *
+ * @return array Property labels (name => label)
+ *
+ * {@see getPropertyLabel()}
+ *
+ * @psalm-return array
+ */
+ public function getPropertyLabels(): array;
+
+ /**
+ * Returns the text placeholder for the specified property.
+ *
+ * @param string $property The property name.
+ *
+ * @return string The property placeholder.
+ */
+ public function getPropertyPlaceholder(string $property): string;
+
+ /**
+ * @throws UndefinedObjectPropertyException
+ * @throws StaticObjectPropertyException
+ * @throws PropertyNotSupportNestedValuesException
+ * @throws ValueNotFoundException
+ */
+ public function getPropertyValue(string $property): mixed;
+
+ /**
+ * Returns the property placeholders.
+ *
+ * @return array Property placeholder (name => placeholder)
+ *
+ * @psalm-return array
+ */
+ public function getPropertyPlaceholders(): array;
+
+ /**
+ * Returns the form name that this model class should use.
+ *
+ * The form name is mainly used by {@see \Yiisoft\Form\InputData\HtmlForm} to determine how to name the input
+ * fields for the properties in a model.
+ * If the form name is "A" and a property name is "b", then the corresponding input name would be "A[b]".
+ * If the form name is an empty string, then the input name would be "b".
+ *
+ * The purpose of the above naming schema is that for forms which contain multiple different models, the properties
+ * of each model are grouped in sub-arrays of the POST-data, and it is easier to differentiate between them.
+ *
+ * By default, this method returns the model class name (without the namespace part) as the form name. You may
+ * override it when the model is used in different forms.
+ *
+ * @return string The form name of this model class.
+ */
+ public function getFormName(): string;
+
+ /**
+ * If there is such property in the set.
+ */
+ public function hasProperty(string $property): bool;
+
+ public function isValid(): bool;
+}
diff --git a/src/ValidationRulesEnricher.php b/src/ValidationRulesEnricher.php
new file mode 100644
index 0000000..46ccca6
--- /dev/null
+++ b/src/ValidationRulesEnricher.php
@@ -0,0 +1,311 @@
+getWhen() !== null) {
+ continue;
+ }
+
+ if ($rule instanceof Required) {
+ $enrichment['inputAttributes']['required'] = true;
+ }
+ }
+ return $enrichment;
+ }
+
+ if ($field instanceof Email) {
+ $enrichment = [];
+ foreach ($rules as $rule) {
+ if ($rule instanceof WhenInterface && $rule->getWhen() !== null) {
+ continue;
+ }
+
+ if ($rule instanceof Required) {
+ $enrichment['inputAttributes']['required'] = true;
+ }
+
+ if ($rule instanceof Length) {
+ if (null !== $min = $rule->getMin()) {
+ $enrichment['inputAttributes']['minlength'] = $min;
+ }
+ if (null !== $max = $rule->getMax()) {
+ $enrichment['inputAttributes']['maxlength'] = $max;
+ }
+ }
+
+ if ($rule instanceof Regex) {
+ if (!$rule->isNot()) {
+ $enrichment['inputAttributes']['pattern'] = Html::normalizeRegexpPattern(
+ $rule->getPattern()
+ );
+ }
+ }
+ }
+ return $enrichment;
+ }
+
+ if ($field instanceof File) {
+ $enrichment = [];
+ foreach ($rules as $rule) {
+ if ($rule instanceof WhenInterface && $rule->getWhen() !== null) {
+ continue;
+ }
+
+ if ($rule instanceof Required) {
+ $enrichment['inputAttributes']['required'] = true;
+ }
+ }
+ return $enrichment;
+ }
+
+ if ($field instanceof Number) {
+ $enrichment = [];
+ foreach ($rules as $rule) {
+ if ($rule instanceof WhenInterface && $rule->getWhen() !== null) {
+ continue;
+ }
+
+ if ($rule instanceof Required) {
+ $enrichment['inputAttributes']['required'] = true;
+ }
+
+ if ($rule instanceof NumberRule) {
+ if (null !== $min = $rule->getMin()) {
+ $enrichment['inputAttributes']['min'] = $min;
+ }
+ if (null !== $max = $rule->getMax()) {
+ $enrichment['inputAttributes']['max'] = $max;
+ }
+ }
+ }
+ return $enrichment;
+ }
+
+ if ($field instanceof Password) {
+ $enrichment = [];
+ foreach ($rules as $rule) {
+ if ($rule instanceof WhenInterface && $rule->getWhen() !== null) {
+ continue;
+ }
+
+ if ($rule instanceof Required) {
+ $enrichment['inputAttributes']['required'] = true;
+ }
+
+ if ($rule instanceof Length) {
+ if (null !== $min = $rule->getMin()) {
+ $enrichment['inputAttributes']['minlength'] = $min;
+ }
+ if (null !== $max = $rule->getMax()) {
+ $enrichment['inputAttributes']['maxlength'] = $max;
+ }
+ }
+
+ if ($rule instanceof Regex) {
+ if (!$rule->isNot()) {
+ $enrichment['inputAttributes']['pattern'] = Html::normalizeRegexpPattern(
+ $rule->getPattern()
+ );
+ }
+ }
+ }
+ return $enrichment;
+ }
+
+ if ($field instanceof Range) {
+ $enrichment = [];
+ foreach ($rules as $rule) {
+ if ($rule instanceof WhenInterface && $rule->getWhen() !== null) {
+ continue;
+ }
+
+ if ($rule instanceof Required) {
+ $enrichment['inputAttributes']['required'] = true;
+ }
+
+ if ($rule instanceof NumberRule) {
+ if (null !== $min = $rule->getMin()) {
+ $enrichment['inputAttributes']['min'] = $min;
+ }
+ if (null !== $max = $rule->getMax()) {
+ $enrichment['inputAttributes']['max'] = $max;
+ }
+ }
+ }
+ return $enrichment;
+ }
+
+ if ($field instanceof Select) {
+ $enrichment = [];
+ foreach ($rules as $rule) {
+ if ($rule instanceof WhenInterface && $rule->getWhen() !== null) {
+ continue;
+ }
+
+ if ($rule instanceof Required) {
+ $enrichment['inputAttributes']['required'] = true;
+ }
+ }
+ return $enrichment;
+ }
+
+ if ($field instanceof Telephone) {
+ $enrichment = [];
+ foreach ($rules as $rule) {
+ if ($rule instanceof WhenInterface && $rule->getWhen() !== null) {
+ continue;
+ }
+
+ if ($rule instanceof Required) {
+ $enrichment['inputAttributes']['required'] = true;
+ }
+
+ if ($rule instanceof Length) {
+ if (null !== $min = $rule->getMin()) {
+ $enrichment['inputAttributes']['minlength'] = $min;
+ }
+ if (null !== $max = $rule->getMax()) {
+ $enrichment['inputAttributes']['maxlength'] = $max;
+ }
+ }
+
+ if ($rule instanceof Regex) {
+ if (!$rule->isNot()) {
+ $enrichment['inputAttributes']['pattern'] = Html::normalizeRegexpPattern(
+ $rule->getPattern()
+ );
+ }
+ }
+ }
+ return $enrichment;
+ }
+
+ if ($field instanceof Text) {
+ $enrichment = [];
+ foreach ($rules as $rule) {
+ if ($rule instanceof WhenInterface && $rule->getWhen() !== null) {
+ continue;
+ }
+
+ if ($rule instanceof Required) {
+ $enrichment['inputAttributes']['required'] = true;
+ }
+
+ if ($rule instanceof Length) {
+ if (null !== $min = $rule->getMin()) {
+ $enrichment['inputAttributes']['minlength'] = $min;
+ }
+ if (null !== $max = $rule->getMax()) {
+ $enrichment['inputAttributes']['maxlength'] = $max;
+ }
+ }
+
+ if ($rule instanceof Regex) {
+ if (!$rule->isNot()) {
+ $enrichment['inputAttributes']['pattern'] = Html::normalizeRegexpPattern(
+ $rule->getPattern()
+ );
+ }
+ }
+ }
+ return $enrichment;
+ }
+
+ if ($field instanceof Textarea) {
+ $enrichment = [];
+ foreach ($rules as $rule) {
+ if ($rule instanceof WhenInterface && $rule->getWhen() !== null) {
+ continue;
+ }
+
+ if ($rule instanceof Required) {
+ $enrichment['inputAttributes']['required'] = true;
+ }
+
+ if ($rule instanceof Length) {
+ if (null !== $min = $rule->getMin()) {
+ $enrichment['inputAttributes']['minlength'] = $min;
+ }
+ if (null !== $max = $rule->getMax()) {
+ $enrichment['inputAttributes']['maxlength'] = $max;
+ }
+ }
+ }
+ return $enrichment;
+ }
+
+ if ($field instanceof Url) {
+ $enrichment = [];
+ foreach ($rules as $rule) {
+ if ($rule instanceof WhenInterface && $rule->getWhen() !== null) {
+ continue;
+ }
+
+ if ($rule instanceof Required) {
+ $enrichment['inputAttributes']['required'] = true;
+ }
+
+ if ($rule instanceof Length) {
+ if (null !== $min = $rule->getMin()) {
+ $enrichment['inputAttributes']['minlength'] = $min;
+ }
+ if (null !== $max = $rule->getMax()) {
+ $enrichment['inputAttributes']['maxlength'] = $max;
+ }
+ }
+
+ $pattern = null;
+ if ($rule instanceof UrlRule) {
+ $pattern = $rule->isIdnEnabled() ? null : $rule->getPattern();
+ }
+ if ($pattern === null && $rule instanceof Regex) {
+ if (!$rule->isNot()) {
+ $pattern = $rule->getPattern();
+ }
+ }
+ if ($pattern !== null) {
+ $enrichment['inputAttributes']['pattern'] = Html::normalizeRegexpPattern($pattern);
+ }
+ }
+ return $enrichment;
+ }
+
+ return null;
+ }
+}
diff --git a/tests/.gitkeep b/tests/.gitkeep
deleted file mode 100644
index e69de29..0000000
diff --git a/tests/Exception/UndefinedObjectPropertyExceptionTest.php b/tests/Exception/UndefinedObjectPropertyExceptionTest.php
new file mode 100644
index 0000000..811ef22
--- /dev/null
+++ b/tests/Exception/UndefinedObjectPropertyExceptionTest.php
@@ -0,0 +1,21 @@
+assertSame(
+ 'Undefined object property: "test".',
+ $exception->getMessage()
+ );
+ }
+}
diff --git a/tests/FieldTest.php b/tests/FieldTest.php
new file mode 100644
index 0000000..fd9787b
--- /dev/null
+++ b/tests/FieldTest.php
@@ -0,0 +1,433 @@
+render();
+ $this->assertSame(
+ <<
+
+
+ HTML,
+ $result
+ );
+ }
+
+ public function testButtonGroup(): void
+ {
+ $result = Field::buttonGroup()
+ ->buttons(
+ Html::resetButton('Reset Data'),
+ Html::submitButton('Send'),
+ )
+ ->render();
+
+ $this->assertSame(
+ <<
+
+
+
+ HTML,
+ $result
+ );
+ }
+
+ public function testCheckbox(): void
+ {
+ $result = Field::checkbox(new TestForm(), 'blue')->render();
+ $this->assertSame(
+ <<
+
+
+ HTML,
+ $result
+ );
+ }
+
+ public function testCheckboxList(): void
+ {
+ $result = Field::checkboxList(new TestForm(), 'color2')
+ ->items([
+ 'red' => 'Red',
+ 'blue' => 'Blue',
+ ])
+ ->render();
+
+ $expected = <<<'HTML'
+
+ HTML;
+
+ $this->assertSame($expected, $result);
+ }
+
+ public function testDate(): void
+ {
+ $result = Field::date(new TestForm(), 'birthday')->render();
+ $this->assertSame(
+ <<
+
+
+
+ HTML,
+ $result
+ );
+ }
+
+ public function testDateTime(): void
+ {
+ $result = Field::dateTime(new TestForm(), 'xDate')->render();
+ $this->assertSame(
+ <<
+
+
+
+ HTML,
+ $result
+ );
+ }
+
+ public function testDateTimeLocal(): void
+ {
+ $result = Field::dateTimeLocal(new TestForm(), 'partyDate')->render();
+ $this->assertSame(
+ <<
+
+
+
+ HTML,
+ $result
+ );
+ }
+
+ public function testEmail(): void
+ {
+ $result = Field::email(new TestForm(), 'mainEmail')->render();
+ $this->assertSame(
+ <<
+
+
+ Email for notifications.
+
+ HTML,
+ $result
+ );
+ }
+
+ public function testErrorSummary(): void
+ {
+ $result = Field::errorSummary(TestForm::validated())
+ ->onlyProperties('name')
+ ->render();
+
+ $expected = <<<'HTML'
+
+
Please fix the following errors:
+
+ - Value cannot be blank.
+
+
+ HTML;
+
+ $this->assertSame($expected, $result);
+ }
+
+ public function testFieldset(): void
+ {
+ $result = Field::fieldset()
+ ->legend('Choose your color')
+ ->render();
+
+ $expected = <<<'HTML'
+
+
+
+ HTML;
+
+ $this->assertSame($expected, $result);
+ }
+
+ public function testFile(): void
+ {
+ $result = Field::file(new TestForm(), 'avatar')->render();
+ $this->assertSame(
+ <<
+
+
+
+ HTML,
+ $result
+ );
+ }
+
+ public function testHidden(): void
+ {
+ $result = Field::hidden(new TestForm(), 'key')->render();
+ $this->assertSame(
+ '',
+ $result
+ );
+ }
+
+ public function testImage(): void
+ {
+ $result = Field::image()
+ ->src('btn.png')
+ ->alt('Go')
+ ->render();
+ $this->assertSame(
+ <<
+
+
+ HTML,
+ $result
+ );
+ }
+
+ public function testNumber(): void
+ {
+ $result = Field::number(new TestForm(), 'age')->render();
+ $this->assertSame(
+ <<
+
+
+ Full years.
+
+ HTML,
+ $result
+ );
+ }
+
+ public function testPassword(): void
+ {
+ $result = Field::password(new TestForm(), 'oldPassword')->render();
+ $this->assertSame(
+ <<
+
+
+ Enter your old password.
+
+ HTML,
+ $result
+ );
+ }
+
+ public function testRadioList(): void
+ {
+ $result = Field::radioList(new TestForm(), 'color')
+ ->items([
+ 'red' => 'Red',
+ 'blue' => 'Blue',
+ ])
+ ->render();
+
+ $expected = <<<'HTML'
+
+
+
+
+
+
+
Color of box.
+
+ HTML;
+
+ $this->assertSame($expected, $result);
+ }
+
+ public function testRange(): void
+ {
+ $result = Field::range(new TestForm(), 'volume')
+ ->min(1)
+ ->max(100)
+ ->render();
+ $this->assertSame(
+ <<
+
+
+
+ HTML,
+ $result
+ );
+ }
+
+ public function testResetButton(): void
+ {
+ $result = Field::resetButton('Reset form')
+ ->render();
+ $this->assertSame(
+ <<
+
+
+ HTML,
+ $result
+ );
+ }
+
+ public function testSelect(): void
+ {
+ $result = Field::select(new TestForm(), 'count')
+ ->optionsData([
+ 1 => 'One',
+ 2 => 'Two',
+ ])
+ ->render();
+ $this->assertSame(
+ <<
+
+
+
+ HTML,
+ $result
+ );
+ }
+
+ public function testSubmitButton(): void
+ {
+ $result = Field::submitButton('Go!')
+ ->render();
+ $this->assertSame(
+ <<
+
+
+ HTML,
+ $result
+ );
+ }
+
+ public function testTelephone(): void
+ {
+ $result = Field::telephone(new TestForm(), 'number')->render();
+ $this->assertSame(
+ <<
+
+
+ Enter your phone.
+
+ HTML,
+ $result
+ );
+ }
+
+ public function testText(): void
+ {
+ $result = Field::text(new TestForm(), 'name')->render();
+ $this->assertSame(
+ <<
+
+
+ Input your full name.
+
+ HTML,
+ $result
+ );
+ }
+
+ public function testTextarea(): void
+ {
+ $result = Field::textarea(new TestForm(), 'desc')->render();
+ $this->assertSame(
+ <<
+
+
+
+ HTML,
+ $result
+ );
+ }
+
+ public function testTime(): void
+ {
+ $html = Field::time(new TestForm(), 'startTime')->render();
+
+ $expected = <<
+
+
+
+ HTML;
+
+ $this->assertSame($expected, $html);
+ }
+
+ public function testUrl(): void
+ {
+ $result = Field::url(new TestForm(), 'site')->render();
+ $this->assertSame(
+ <<
+
+
+ Enter your site URL.
+
+ HTML,
+ $result
+ );
+ }
+
+ public function testLabel(): void
+ {
+ $result = Field::label(new TestForm(), 'name')->render();
+ $this->assertSame('', $result);
+ }
+
+ public function testHint(): void
+ {
+ $result = Field::hint(new TestForm(), 'name')->render();
+ $this->assertSame('Input your full name.
', $result);
+ }
+
+ public function testError(): void
+ {
+ $result = Field::error(TestForm::validated(), 'name')->render();
+ $this->assertSame('Value cannot be blank.
', $result);
+ }
+}
diff --git a/tests/FormModelInputDataTest.php b/tests/FormModelInputDataTest.php
new file mode 100644
index 0000000..aabc86d
--- /dev/null
+++ b/tests/FormModelInputDataTest.php
@@ -0,0 +1,41 @@
+assertSame($expectedName, $inputData->getName());
+ $this->assertSame($expectedId, $inputData->getId());
+ }
+}
diff --git a/tests/FormModelTest.php b/tests/FormModelTest.php
new file mode 100644
index 0000000..5718c42
--- /dev/null
+++ b/tests/FormModelTest.php
@@ -0,0 +1,417 @@
+assertSame('', $form->getFormName());
+ }
+
+ public function testCustomFormName(): void
+ {
+ $form = new CustomFormNameForm();
+ $this->assertSame('my-best-form-name', $form->getFormName());
+ }
+
+ public function testDefaultFormName(): void
+ {
+ $form = new DefaultFormNameForm();
+ $this->assertSame('DefaultFormNameForm', $form->getFormName());
+ }
+
+ public function testArrayValue(): void
+ {
+ $expected = <<<'HTML'
+
+
+
+
+ HTML;
+
+ $result = StubInputField::widget()
+ ->inputData(new FormModelInputData(new NestedForm(), 'letters[0]'))
+ ->render();
+
+ $this->assertSame($expected, $result);
+ }
+
+ public function testNonExistArrayValue(): void
+ {
+ $widget = StubInputField::widget()->inputData(new FormModelInputData(new NestedForm(), 'letters[1]'));
+
+ $result = $widget->render();
+
+ $this->assertSame(
+ <<
+
+
+
+ HTML,
+ $result
+ );
+ }
+
+ public function testArrayValueIntoObject(): void
+ {
+ $expected = <<<'HTML'
+
+
+
+
+ HTML;
+
+ $result = StubInputField::widget()
+ ->inputData(new FormModelInputData(new NestedForm(), 'object[numbers][1]'))
+ ->render();
+
+ $this->assertSame($expected, $result);
+ }
+
+ public function testGetAttributeHint(): void
+ {
+ $form = new LoginForm();
+
+ $this->assertSame('Write your id or email.', $form->getPropertyHint('login'));
+ $this->assertSame('Write your password.', $form->getPropertyHint('password'));
+ $this->assertEmpty($form->getPropertyHint('noExist'));
+ }
+
+ public function testGetAttributeLabel(): void
+ {
+ $form = new LoginForm();
+
+ $this->assertSame('Login:', $form->getPropertyLabel('login'));
+ $this->assertSame('Testme', $form->getPropertyLabel('testme'));
+ }
+
+ public function testGetAttributesLabels(): void
+ {
+ $form = new LoginForm();
+
+ $expected = [
+ 'login' => 'Login:',
+ 'password' => 'Password:',
+ 'rememberMe' => 'remember Me:',
+ ];
+
+ $this->assertSame($expected, $form->getPropertyLabels());
+ }
+
+ public function testNestedPropertyOnNull(): void
+ {
+ $form = new FormWithNestedProperty();
+
+ $this->assertFalse($form->hasProperty('id.profile'));
+ $this->assertNull($form->getPropertyValue('id.profile'));
+ }
+
+ public function testNestedPropertyOnArray(): void
+ {
+ $form = new FormWithNestedProperty();
+
+ $this->assertFalse($form->hasProperty('meta.profile'));
+ $this->assertNull($form->getPropertyValue('meta.profile'));
+ }
+
+ public function testNestedPropertyOnString(): void
+ {
+ $form = new FormWithNestedProperty();
+
+ $this->assertFalse($form->hasProperty('key.profile'));
+
+ $this->expectException(PropertyNotSupportNestedValuesException::class);
+ $this->expectExceptionMessage(
+ 'Property "' . FormWithNestedProperty::class . '::key" not support nested values.'
+ );
+ $form->getPropertyValue('key.profile');
+ }
+
+ public function testNestedPropertyOnObject(): void
+ {
+ $form = new FormWithNestedProperty();
+
+ $this->assertFalse($form->hasProperty('coordinates.profile'));
+
+ $this->expectException(UndefinedObjectPropertyException::class);
+ $this->expectExceptionMessage(
+ 'Undefined object property: "' . FormWithNestedProperty::class . '::coordinates::profile".'
+ );
+ $form->getPropertyValue('coordinates.profile');
+ }
+
+ public function testGetNestedAttributeHint(): void
+ {
+ $form = new FormWithNestedProperty();
+
+ $this->assertSame('Write your id or email.', $form->getPropertyHint('user.login'));
+ }
+
+ public function testGetNestedAttributeLabel(): void
+ {
+ $form = new FormWithNestedProperty();
+
+ $this->assertSame('Login:', $form->getPropertyLabel('user.login'));
+ }
+
+ public function testGetNestedAttributePlaceHolder(): void
+ {
+ $form = new FormWithNestedProperty();
+
+ $this->assertSame('Type Username or Email.', $form->getPropertyPlaceholder('user.login'));
+ }
+
+ public function testGetAttributePlaceHolder(): void
+ {
+ $form = new LoginForm();
+
+ $this->assertSame('Type Username or Email.', $form->getPropertyPlaceholder('login'));
+ $this->assertSame('Type Password.', $form->getPropertyPlaceholder('password'));
+ $this->assertEmpty($form->getPropertyPlaceholder('noExist'));
+ }
+
+ public function testGetAttributeValue(): void
+ {
+ $form = new LoginForm();
+
+ $form->login('admin');
+ $this->assertSame('admin', $form->getPropertyValue('login'));
+
+ $form->password('123456');
+ $this->assertSame('123456', $form->getPropertyValue('password'));
+
+ $form->rememberMe(true);
+ $this->assertSame(true, $form->getPropertyValue('rememberMe'));
+ }
+
+ public function testGetAttributeValueException(): void
+ {
+ $form = new LoginForm();
+
+ $this->expectException(InvalidArgumentException::class);
+ $this->expectExceptionMessage(
+ 'Undefined object property: "Yiisoft\FormModel\Tests\Support\Form\LoginForm::noExist".'
+ );
+ $form->getPropertyValue('noExist');
+ }
+
+ public function testGetAttributeValueWithNestedAttribute(): void
+ {
+ $form = new FormWithNestedProperty();
+
+ $form->setUserLogin('admin');
+ $this->assertSame('admin', $form->getPropertyValue('user.login'));
+ }
+
+ public function testHasAttribute(): void
+ {
+ $form = new LoginForm();
+
+ $this->assertTrue($form->hasProperty('login'));
+ $this->assertTrue($form->hasProperty('password'));
+ $this->assertTrue($form->hasProperty('rememberMe'));
+ $this->assertFalse($form->hasProperty('noExist'));
+ $this->assertFalse($form->hasProperty('extraField'));
+ }
+
+ public function testHasNestedAttribute(): void
+ {
+ $form = new FormWithNestedProperty();
+
+ $this->assertTrue($form->hasProperty('user.login'));
+ $this->assertTrue($form->hasProperty('user.password'));
+ $this->assertTrue($form->hasProperty('user.rememberMe'));
+ $this->assertFalse($form->hasProperty('noexist'));
+ }
+
+ public function testHasNestedAttributeException(): void
+ {
+ $form = new FormWithNestedProperty();
+
+ $this->assertFalse($form->hasProperty('user.noexist'));
+ }
+
+ public function testLoad(): void
+ {
+ $form = new LoginForm();
+
+ $this->assertNull($form->getLogin());
+ $this->assertNull($form->getPassword());
+ $this->assertFalse($form->getRememberMe());
+
+ $data = [
+ 'LoginForm' => [
+ 'login' => 'admin',
+ 'password' => '123456',
+ 'rememberMe' => true,
+ 'noExist' => 'noExist',
+ ],
+ ];
+
+ $this->assertTrue(TestHelper::createFormHydrator()->populate($form, $data));
+
+ $this->assertSame('admin', $form->getLogin());
+ $this->assertSame('123456', $form->getPassword());
+ $this->assertSame(true, $form->getRememberMe());
+ }
+
+ public function testLoadFailedForm(): void
+ {
+ $form1 = new LoginForm();
+ $form2 = new class () extends FormModel {
+ };
+
+ $data1 = [
+ 'LoginForm2' => [
+ 'login' => 'admin',
+ 'password' => '123456',
+ 'rememberMe' => true,
+ 'noExist' => 'noExist',
+ ],
+ ];
+ $data2 = [];
+
+ $hydrator = TestHelper::createFormHydrator();
+
+ $this->assertFalse($hydrator->populate($form1, $data1));
+ $this->assertFalse($hydrator->populate($form1, $data2));
+
+ $this->assertTrue($hydrator->populate($form2, $data1));
+ $this->assertTrue($hydrator->populate($form2, $data2));
+ }
+
+ public function testLoadWithEmptyScope(): void
+ {
+ $form = new class () extends FormModel {
+ private int $int = 1;
+ private string $string = 'string';
+ private float $float = 3.14;
+ private bool $bool = true;
+ };
+ TestHelper::createFormHydrator()->populate(
+ $form,
+ [
+ 'int' => '2',
+ 'float' => '3.15',
+ 'bool' => '0',
+ 'string' => 555,
+ ],
+ scope: '',
+ );
+
+ $this->assertSame(2, $form->getPropertyValue('int'));
+ $this->assertSame(3.15, $form->getPropertyValue('float'));
+ $this->assertSame(false, $form->getPropertyValue('bool'));
+ $this->assertSame('555', $form->getPropertyValue('string'));
+ }
+
+ public function testLoadWithNestedProperty(): void
+ {
+ $form = new FormWithNestedProperty();
+
+ $data = [
+ 'FormWithNestedProperty' => [
+ 'user.login' => 'admin',
+ ],
+ ];
+
+ $this->assertTrue(TestHelper::createFormHydrator()->populate($form, $data));
+ $this->assertSame('admin', $form->getUserLogin());
+ }
+
+ public function testLoadObjectData(): void
+ {
+ $form = new LoginForm();
+
+ $result = TestHelper::createFormHydrator()->populate($form, new stdClass());
+
+ $this->assertFalse($result);
+ }
+
+ public function testLoadNullData(): void
+ {
+ $form = new LoginForm();
+
+ $result = TestHelper::createFormHydrator()->populate($form, null);
+
+ $this->assertFalse($result);
+ }
+
+ public function testLoadNonArrayScopedData(): void
+ {
+ $form = new LoginForm();
+
+ $result = TestHelper::createFormHydrator()->populate($form, ['LoginForm' => null]);
+
+ $this->assertFalse($result);
+ }
+
+ public function testNonNamespacedFormName(): void
+ {
+ $form = new \NonNamespacedForm();
+ $this->assertSame('NonNamespacedForm', $form->getFormName());
+ }
+
+ public function testPublicAttributes(): void
+ {
+ $form = new class () extends FormModel {
+ public int $int = 1;
+ };
+
+ // check row data value.
+ TestHelper::createFormHydrator()->populate($form, ['int' => '2']);
+ $this->assertSame(2, $form->getPropertyValue('int'));
+ }
+
+ public function testFormWithNestedStructures(): void
+ {
+ $form = new FormWithNestedStructures();
+
+ TestHelper::createFormHydrator()->populate($form, [
+ 'FormWithNestedStructures' => [
+ 'array' => ['a' => 'b', 'nested' => ['c' => 'd']],
+ 'coordinates' => ['latitude' => '12.24', 'longitude' => '56.78'],
+ ],
+ ]);
+
+ $this->assertSame(['a' => 'b', 'nested' => ['c' => 'd']], $form->getPropertyValue('array'));
+
+ $coordinates = $form->getPropertyValue('coordinates');
+ $this->assertInstanceOf(Coordinates::class, $coordinates);
+ $this->assertSame('12.24', $coordinates->getLatitude());
+ $this->assertSame('56.78', $coordinates->getLongitude());
+ }
+}
diff --git a/tests/Support/Dto/Coordinates.php b/tests/Support/Dto/Coordinates.php
new file mode 100644
index 0000000..f3fbe4f
--- /dev/null
+++ b/tests/Support/Dto/Coordinates.php
@@ -0,0 +1,24 @@
+latitude;
+ }
+
+ public function getLongitude(): string
+ {
+ return $this->longitude;
+ }
+}
diff --git a/tests/Support/Form/CustomFormNameForm.php b/tests/Support/Form/CustomFormNameForm.php
new file mode 100644
index 0000000..85ff12f
--- /dev/null
+++ b/tests/Support/Form/CustomFormNameForm.php
@@ -0,0 +1,15 @@
+user = new LoginForm();
+ $this->coordinates = new Coordinates();
+ }
+
+ public function getPropertyLabels(): array
+ {
+ return [
+ 'id' => 'ID',
+ ];
+ }
+
+ public function getPropertyHints(): array
+ {
+ return [
+ 'id' => 'Readonly ID',
+ ];
+ }
+
+ public function getPropertyPlaceholders(): array
+ {
+ return [
+ 'id' => 'Type ID.',
+ ];
+ }
+
+ public function getRules(): array
+ {
+ return [
+ 'id' => [new Required()],
+ 'user' => new Nested(),
+ ];
+ }
+
+ public function setUserLogin(string $login): void
+ {
+ $this->user->login($login);
+ }
+
+ public function getUserLogin(): ?string
+ {
+ return $this->user->getLogin();
+ }
+}
diff --git a/tests/Support/Form/FormWithNestedStructures.php b/tests/Support/Form/FormWithNestedStructures.php
new file mode 100644
index 0000000..1837746
--- /dev/null
+++ b/tests/Support/Form/FormWithNestedStructures.php
@@ -0,0 +1,24 @@
+array;
+ }
+
+ public function getCoordinates(): ?Coordinates
+ {
+ return $this->coordinates;
+ }
+}
diff --git a/tests/Support/Form/LoginForm.php b/tests/Support/Form/LoginForm.php
new file mode 100644
index 0000000..af85239
--- /dev/null
+++ b/tests/Support/Form/LoginForm.php
@@ -0,0 +1,99 @@
+login;
+ }
+
+ public function getPassword(): ?string
+ {
+ return $this->password;
+ }
+
+ public function getRememberMe(): bool
+ {
+ return $this->rememberMe;
+ }
+
+ public function login(string $value): void
+ {
+ $this->login = $value;
+ }
+
+ public function password(string $value): void
+ {
+ $this->password = $value;
+ }
+
+ public function rememberMe(bool $value): void
+ {
+ $this->rememberMe = $value;
+ }
+
+ public function getPropertyHints(): array
+ {
+ return [
+ 'login' => 'Write your id or email.',
+ 'password' => 'Write your password.',
+ ];
+ }
+
+ public function getPropertyLabels(): array
+ {
+ return [
+ 'login' => 'Login:',
+ 'password' => 'Password:',
+ 'rememberMe' => 'remember Me:',
+ ];
+ }
+
+ public function getPropertyPlaceholders(): array
+ {
+ return [
+ 'login' => 'Type Username or Email.',
+ 'password' => 'Type Password.',
+ ];
+ }
+
+ public function getRules(): array
+ {
+ return [
+ 'login' => $this->loginRules(),
+ 'password' => $this->passwordRules(),
+ ];
+ }
+
+ private function loginRules(): array
+ {
+ return [
+ new Required(),
+ new Length(min: 4, max: 40, lessThanMinMessage: 'Is too short.', greaterThanMaxMessage: 'Is too long.'),
+ new Email(),
+ ];
+ }
+
+ private function passwordRules(): array
+ {
+ return [
+ new Required(),
+ new Length(min: 8, lessThanMinMessage: 'Is too short.'),
+ ];
+ }
+}
diff --git a/tests/Support/Form/NestedForm.php b/tests/Support/Form/NestedForm.php
new file mode 100644
index 0000000..8a22a2c
--- /dev/null
+++ b/tests/Support/Form/NestedForm.php
@@ -0,0 +1,21 @@
+object = new class () {
+ public string $name = 'Bo';
+ public array $numbers = [7, 42];
+ };
+ }
+}
diff --git a/tests/Support/Form/NonNamespacedForm.php b/tests/Support/Form/NonNamespacedForm.php
new file mode 100644
index 0000000..12b8a01
--- /dev/null
+++ b/tests/Support/Form/NonNamespacedForm.php
@@ -0,0 +1,9 @@
+ [new Required(), new Length(min: 4)],
+ ];
+ }
+
+ public function getPropertyLabels(): array
+ {
+ return [
+ 'desc' => 'Description',
+ 'site' => 'Your site',
+ 'number' => 'Phone',
+ 'count' => 'Select count',
+ 'color' => 'Select color',
+ 'age' => 'Your age',
+ 'mainEmail' => 'Main email',
+ 'partyDate' => 'Date of party',
+ 'xDate' => 'Date X',
+ 'color2' => 'Select one or more colors',
+ 'blue' => 'Blue color',
+ ];
+ }
+
+ public function getPropertyHints(): array
+ {
+ return [
+ 'name' => 'Input your full name.',
+ 'site' => 'Enter your site URL.',
+ 'number' => 'Enter your phone.',
+ 'color' => 'Color of box.',
+ 'oldPassword' => 'Enter your old password.',
+ 'age' => 'Full years.',
+ 'mainEmail' => 'Email for notifications.',
+ ];
+ }
+
+ public static function validated(): self
+ {
+ $form = new self();
+ (new Validator())->validate($form);
+ return $form;
+ }
+}
diff --git a/tests/Support/StubInputField.php b/tests/Support/StubInputField.php
new file mode 100644
index 0000000..6f4d67e
--- /dev/null
+++ b/tests/Support/StubInputField.php
@@ -0,0 +1,20 @@
+getInputData()->getName(),
+ (string) $this->getInputData()->getValue(),
+ $this->getInputAttributes()
+ )->render();
+ }
+}
diff --git a/tests/Support/TestHelper.php b/tests/Support/TestHelper.php
new file mode 100644
index 0000000..4ac1b50
--- /dev/null
+++ b/tests/Support/TestHelper.php
@@ -0,0 +1,29 @@
+
Date: Fri, 1 Dec 2023 14:26:12 +0000
Subject: [PATCH 3/5] Apply Rector changes (CI)
---
src/Exception/PropertyNotSupportNestedValuesException.php | 2 +-
src/FormHydrator.php | 2 +-
src/FormModelInputData.php | 4 ++--
tests/Support/Dto/Coordinates.php | 4 ++--
tests/Support/Form/FormWithNestedProperty.php | 4 ++--
5 files changed, 8 insertions(+), 8 deletions(-)
diff --git a/src/Exception/PropertyNotSupportNestedValuesException.php b/src/Exception/PropertyNotSupportNestedValuesException.php
index 8287fe5..e7a35b7 100644
--- a/src/Exception/PropertyNotSupportNestedValuesException.php
+++ b/src/Exception/PropertyNotSupportNestedValuesException.php
@@ -6,7 +6,7 @@
final class PropertyNotSupportNestedValuesException extends ValueNotFoundException
{
- public function __construct(string $property, private mixed $value)
+ public function __construct(string $property, private readonly mixed $value)
{
parent::__construct('Property "' . $property . '" not support nested values.');
}
diff --git a/src/FormHydrator.php b/src/FormHydrator.php
index 9e89629..73f66ba 100644
--- a/src/FormHydrator.php
+++ b/src/FormHydrator.php
@@ -15,7 +15,7 @@
final class FormHydrator
{
public function __construct(
- private HydratorInterface $hydrator,
+ private readonly HydratorInterface $hydrator,
) {
}
diff --git a/src/FormModelInputData.php b/src/FormModelInputData.php
index e75ebef..9c6e4cf 100644
--- a/src/FormModelInputData.php
+++ b/src/FormModelInputData.php
@@ -23,8 +23,8 @@ final class FormModelInputData implements InputDataInterface
private ?iterable $validationRules = null;
public function __construct(
- private FormModelInterface $model,
- private string $property,
+ private readonly FormModelInterface $model,
+ private readonly string $property,
) {
}
diff --git a/tests/Support/Dto/Coordinates.php b/tests/Support/Dto/Coordinates.php
index f3fbe4f..a346c2e 100644
--- a/tests/Support/Dto/Coordinates.php
+++ b/tests/Support/Dto/Coordinates.php
@@ -7,8 +7,8 @@
final class Coordinates
{
public function __construct(
- private string $latitude = '',
- private string $longitude = '',
+ private readonly string $latitude = '',
+ private readonly string $longitude = '',
) {
}
diff --git a/tests/Support/Form/FormWithNestedProperty.php b/tests/Support/Form/FormWithNestedProperty.php
index 217ec6f..7bb1335 100644
--- a/tests/Support/Form/FormWithNestedProperty.php
+++ b/tests/Support/Form/FormWithNestedProperty.php
@@ -15,8 +15,8 @@ final class FormWithNestedProperty extends FormModel implements RulesProviderInt
private ?int $id = null;
private string $key = '';
private array $meta = [];
- private Coordinates $coordinates;
- private LoginForm $user;
+ private readonly Coordinates $coordinates;
+ private readonly LoginForm $user;
public function __construct()
{
From b7cca52e5e6ae7299cb50522aa73508ac31718ab Mon Sep 17 00:00:00 2001
From: Sergei Predvoditelev
Date: Fri, 1 Dec 2023 17:28:26 +0300
Subject: [PATCH 4/5] fix
---
composer.json | 1 +
1 file changed, 1 insertion(+)
diff --git a/composer.json b/composer.json
index b871423..ef2833e 100644
--- a/composer.json
+++ b/composer.json
@@ -29,6 +29,7 @@
"prefer-stable": true,
"require": {
"php": "^8.1",
+ "ext-mbstring": "*",
"yiisoft/form": "^1.0@dev",
"yiisoft/html": "^3.3",
"yiisoft/hydrator": "dev-master",
From 5768841d68211de66a57b90a471bc85e2812f90f Mon Sep 17 00:00:00 2001
From: Sergei Predvoditelev
Date: Fri, 1 Dec 2023 17:33:18 +0300
Subject: [PATCH 5/5] improve
---
rector.php | 2 ++
src/Exception/PropertyNotSupportNestedValuesException.php | 6 ++++--
tests/Support/Form/FormWithNestedProperty.php | 4 ++--
3 files changed, 8 insertions(+), 4 deletions(-)
diff --git a/rector.php b/rector.php
index 90bf968..90fea6b 100644
--- a/rector.php
+++ b/rector.php
@@ -6,6 +6,7 @@
use Rector\Config\RectorConfig;
use Rector\Php73\Rector\FuncCall\JsonThrowOnErrorRector;
use Rector\Php74\Rector\Closure\ClosureToArrowFunctionRector;
+use Rector\Php81\Rector\Property\ReadOnlyPropertyRector;
use Rector\Set\ValueObject\LevelSetList;
return static function (RectorConfig $rectorConfig): void {
@@ -25,5 +26,6 @@
$rectorConfig->skip([
ClosureToArrowFunctionRector::class,
JsonThrowOnErrorRector::class,
+ ReadOnlyPropertyRector::class,
]);
};
diff --git a/src/Exception/PropertyNotSupportNestedValuesException.php b/src/Exception/PropertyNotSupportNestedValuesException.php
index e7a35b7..17d194d 100644
--- a/src/Exception/PropertyNotSupportNestedValuesException.php
+++ b/src/Exception/PropertyNotSupportNestedValuesException.php
@@ -6,8 +6,10 @@
final class PropertyNotSupportNestedValuesException extends ValueNotFoundException
{
- public function __construct(string $property, private readonly mixed $value)
- {
+ public function __construct(
+ string $property,
+ private readonly mixed $value,
+ ) {
parent::__construct('Property "' . $property . '" not support nested values.');
}
diff --git a/tests/Support/Form/FormWithNestedProperty.php b/tests/Support/Form/FormWithNestedProperty.php
index 7bb1335..217ec6f 100644
--- a/tests/Support/Form/FormWithNestedProperty.php
+++ b/tests/Support/Form/FormWithNestedProperty.php
@@ -15,8 +15,8 @@ final class FormWithNestedProperty extends FormModel implements RulesProviderInt
private ?int $id = null;
private string $key = '';
private array $meta = [];
- private readonly Coordinates $coordinates;
- private readonly LoginForm $user;
+ private Coordinates $coordinates;
+ private LoginForm $user;
public function __construct()
{