Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Filter data nested forms #68

Open
wants to merge 16 commits into
base: master
Choose a base branch
from
Open
3 changes: 2 additions & 1 deletion CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,8 @@

## 1.0.2 under development

- no changes in this release.
- Bug #68: Populating out forms with a hydrator when displaying fields `Field::text($form->nestedForm, 'text')` of
nested forms (@DAGpro)

## 1.0.1 September 13, 2024

Expand Down
2 changes: 1 addition & 1 deletion composer.json
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,7 @@
"psr/http-message": "^1.0|^2.0",
"yiisoft/form": "^1.0",
"yiisoft/html": "^3.3",
"yiisoft/hydrator": "^1.3",
"yiisoft/hydrator": "dev-master",
"yiisoft/strings": "^2.3",
"yiisoft/validator": "^2.1"
},
Expand Down
14 changes: 14 additions & 0 deletions docs/guide/en/displaying-fields.md
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,13 @@ use Yiisoft\FormModel\FormModel;

/** @var FormModel $formModel */
$field = Field::text($formModel, 'login');

/** Display fields nested forms */
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
/** Display fields nested forms */
/** Display nested form */

Does that display whole form?

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Whole?

nested form field or nested form 🤷‍♂️

$nestedField = Field::text($formModel->nestedForm, 'text');
/** or dot-notation */
$nestedField = Field::text($formModel, 'nestedForm.text');
/** or array notation */
$nestedField = Field::text($formModel, 'nestedForm[text]');
```

or factory (`\Yiisoft\FormModel\FieldFactory`):
Expand All @@ -20,6 +27,13 @@ use Yiisoft\FormModel\FormModel;
/** @var FormModel $formModel */
$factory = new FieldFactory();
$factory->text($formModel, 'login');

/** Display nested form field */
$nestedField = $factory->text($formModel->nestedForm, 'text');
/** or dot-notation */
$nestedField = $factory->text($formModel, 'nestedForm.text');
/** or array notation */
$nestedField = $factory->text($formModel, 'nestedForm[text]');
```

If you want to customize other properties, such as label, hint, etc., use dedicated methods:
Expand Down
265 changes: 246 additions & 19 deletions src/FormHydrator.php
Original file line number Diff line number Diff line change
Expand Up @@ -7,19 +7,22 @@
use Psr\Http\Message\ServerRequestInterface;
use Yiisoft\Hydrator\ArrayData;
use Yiisoft\Hydrator\HydratorInterface;
use Yiisoft\Hydrator\ObjectMap;
use Yiisoft\Validator\Helper\ObjectParser;
use Yiisoft\Validator\Result;
use Yiisoft\Validator\Rule\Nested;
use Yiisoft\Validator\RulesProviderInterface;
use Yiisoft\Validator\ValidatorInterface;

use function array_merge;
use function is_array;
use function is_string;

/**
* Form hydrator fills model with the data and optionally checks the data validity.
*
* @psalm-import-type MapType from ArrayData
* @psalm-import-type RawRulesMap from ValidatorInterface
* @psalm-import-type NormalizedNestedRulesArray from Nested
*/
final class FormHydrator
{
Expand Down Expand Up @@ -68,7 +71,9 @@
if (!isset($data[$scope]) || !is_array($data[$scope])) {
return false;
}
$hydrateData = $data[$scope];

$filteredData = $this->filterDataNestedForms($model, $data);
$hydrateData = array_merge_recursive((array)$data[$model->getFormName()], $filteredData);
}

$this->hydrator->hydrate(
Expand Down Expand Up @@ -186,6 +191,43 @@
return $this->populateAndValidate($model, $request->getParsedBody(), $map, $strict, $scope);
}

private function filterDataNestedForms(FormModelInterface $formModel, array &$data): array
{
$reflection = new \ReflectionClass($formModel);
$properties = $reflection->getProperties(
\ReflectionProperty::IS_PUBLIC |
\ReflectionProperty::IS_PROTECTED |
\ReflectionProperty::IS_PRIVATE,
);

$filteredData = [];
foreach ($properties as $property) {
if ($property->isStatic()) {
continue;
}

if ($property->isReadOnly()) {
continue;

Check warning on line 210 in src/FormHydrator.php

View workflow job for this annotation

GitHub Actions / mutation / PHP 8.2-ubuntu-latest

Escaped Mutant for Mutator "Continue_": --- Original +++ New @@ @@ continue; } if ($property->isReadOnly()) { - continue; + break; } $propertyValue = $property->getValue($formModel); if ($propertyValue instanceof FormModelInterface) {
}

$propertyValue = $property->getValue($formModel);
if ($propertyValue instanceof FormModelInterface) {
$dataNestedForms = $this->filterDataNestedForms($propertyValue, $data);
if (isset($data[$propertyValue->getFormName()])) {
$filteredData[$property->getName()] = array_merge(
(array)$data[$propertyValue->getFormName()],
$dataNestedForms,
);
unset($data[$propertyValue->getFormName()]);
} elseif (!empty($dataNestedForms)) {
$filteredData[$property->getName()] = $dataNestedForms;
}
}
}

return $filteredData;

Check warning on line 228 in src/FormHydrator.php

View workflow job for this annotation

GitHub Actions / mutation / PHP 8.2-ubuntu-latest

Escaped Mutant for Mutator "ArrayOneItem": --- Original +++ New @@ @@ } } } - return $filteredData; + return count($filteredData) > 1 ? array_slice($filteredData, 0, 1, true) : $filteredData; } /** * Get a map of object property names mapped to keys in the data array.
}

/**
* Get a map of object property names mapped to keys in the data array.
*
Expand All @@ -209,46 +251,231 @@
return $userMap;
}

$properties = $this->getPropertiesWithRules($model);
$generatedMap = array_combine($properties, $properties);
$map = $this->getMapFromRules($model);

if ($userMap === null) {
return $generatedMap;
return $map;
}

return array_merge($generatedMap, $userMap);
return $this->mapMerge($userMap, $map);
}

/**
* Extract object property names mapped to keys in the data array based on model validation rules.
*
* @return array Object property names mapped to keys in the data array.
* @psalm-return array<int, string>
* @psalm-return MapType
*/
private function getPropertiesWithRules(FormModelInterface $model): array
private function getMapFromRules(FormModelInterface $model): array
{
$parser = new ObjectParser($model, skipStaticProperties: true);
$properties = $this->extractStringKeys($parser->getRules());
$mapFromAttributes = $this->getMapFromRulesAttributes($parser->getRules());

if ($model instanceof RulesProviderInterface) {
$mapFromProvider = $this->getMapFromRulesProvider($model);
return $this->mapMerge($mapFromAttributes, $mapFromProvider);
}

return $model instanceof RulesProviderInterface
? array_merge($properties, $this->extractStringKeys($model->getRules()))
: $properties;
return $mapFromAttributes;
}

/**
* Get only string keys from an array.
*
* @return array String keys.
* @psalm-return list<string>
* @psalm-return MapType
*/
private function extractStringKeys(iterable $array): array
private function getMapFromRulesAttributes(array $array): array
{
$result = [];
foreach ($array as $key => $_value) {
if (is_string($key)) {
$result[] = $key;
if (is_int($key)) {
continue;
}
$result[$key] = $key;
foreach ($_value as $nestedRule) {

Check warning on line 293 in src/FormHydrator.php

View workflow job for this annotation

GitHub Actions / mutation / PHP 8.2-ubuntu-latest

Escaped Mutant for Mutator "Foreach_": --- Original +++ New @@ @@ continue; } $result[$key] = $key; - foreach ($_value as $nestedRule) { + foreach (array() as $nestedRule) { if ($nestedRule instanceof Nested) { $nestedMap = $this->getNestedMap($nestedRule, [$key]); if ($nestedMap !== null) {
if ($nestedRule instanceof Nested) {
$nestedMap = $this->getNestedMap($nestedRule, [$key]);
if ($nestedMap !== null) {
$result[$key] = new ObjectMap($nestedMap);
}
}
}
}

return $result;
}

/**
* @param array<int, string> $parentKeys
* @psalm-return MapType|null
*/
private function getNestedMap(Nested $rule, array $parentKeys): ?array
{
/**
* @psalm-param $rules NormalizedNestedRulesArray
*/
$rules = $rule->getRules();
if ($rules === null) {
return null;
}

$map = [];
foreach ($rules as $key => $nestedRules) {
if (is_int($key)) {
continue;

Check warning on line 323 in src/FormHydrator.php

View workflow job for this annotation

GitHub Actions / mutation / PHP 8.2-ubuntu-latest

Escaped Mutant for Mutator "Continue_": --- Original +++ New @@ @@ $map = []; foreach ($rules as $key => $nestedRules) { if (is_int($key)) { - continue; + break; } if (is_array($nestedRules)) { $keyPath = null;
}

if (is_array($nestedRules)) {
$keyPath = null;
if (str_contains($key, '.')) {

Check warning on line 328 in src/FormHydrator.php

View workflow job for this annotation

GitHub Actions / mutation / PHP 8.2-ubuntu-latest

Escaped Mutant for Mutator "IfNegation": --- Original +++ New @@ @@ } if (is_array($nestedRules)) { $keyPath = null; - if (str_contains($key, '.')) { + if (!str_contains($key, '.')) { $keyPath = explode('.', $key); $key = reset($keyPath); $dotKeyMap = $this->dotKeyInMap($keyPath, $parentKeys, null);
$keyPath = explode('.', $key);
$key = reset($keyPath);
$dotKeyMap = $this->dotKeyInMap($keyPath, $parentKeys, null);
$map[$key] = $dotKeyMap[$key];
} else {
$map[$key] = [...$parentKeys, $key];
}
foreach ($nestedRules as $item) {

Check warning on line 336 in src/FormHydrator.php

View workflow job for this annotation

GitHub Actions / mutation / PHP 8.2-ubuntu-latest

Escaped Mutant for Mutator "Foreach_": --- Original +++ New @@ @@ } else { $map[$key] = [...$parentKeys, $key]; } - foreach ($nestedRules as $item) { + foreach (array() as $item) { if ($item instanceof Nested) { $pathKeys = $keyPath ?? [$key]; $nestedMap = $this->getNestedMap($item, [...$parentKeys, ...$pathKeys]);
if ($item instanceof Nested) {
$pathKeys = $keyPath ?? [$key];
$nestedMap = $this->getNestedMap($item, [...$parentKeys, ...$pathKeys]);

Check warning on line 339 in src/FormHydrator.php

View workflow job for this annotation

GitHub Actions / mutation / PHP 8.2-ubuntu-latest

Escaped Mutant for Mutator "SpreadOneItem": --- Original +++ New @@ @@ foreach ($nestedRules as $item) { if ($item instanceof Nested) { $pathKeys = $keyPath ?? [$key]; - $nestedMap = $this->getNestedMap($item, [...$parentKeys, ...$pathKeys]); + $nestedMap = $this->getNestedMap($item, [[...$parentKeys][0], ...$pathKeys]); if (isset($keyPath)) { $dotKeyMap = $this->dotKeyInMap($keyPath, $parentKeys, $nestedMap); $map[$key] = $dotKeyMap[$key];
if (isset($keyPath)) {
$dotKeyMap = $this->dotKeyInMap($keyPath, $parentKeys, $nestedMap);
$map[$key] = $dotKeyMap[$key];
} elseif ($nestedMap !== null) {
$map[$key] = new ObjectMap($nestedMap);
}
}
}
}
}

return $map;
}

/**
* @psalm-param array<int, string> $keyPath
* @psalm-param array<int, string> $parentsKeys
* @psalm-param MapType|null $nestedMap
* @psalm-return MapType
*/
private function dotKeyInMap(array $keyPath, array $parentsKeys, ?array $nestedMap): array
{
$dotMap = [];
$reverseKeyPath = array_reverse($keyPath);
foreach ($reverseKeyPath as $key) {
if ($dotMap !== []) {

Check warning on line 365 in src/FormHydrator.php

View workflow job for this annotation

GitHub Actions / mutation / PHP 8.2-ubuntu-latest

Escaped Mutant for Mutator "NotIdentical": --- Original +++ New @@ @@ $dotMap = []; $reverseKeyPath = array_reverse($keyPath); foreach ($reverseKeyPath as $key) { - if ($dotMap !== []) { + if ($dotMap === []) { $dotMap = [$key => new ObjectMap($dotMap)]; } else { $dotMap = [$key => is_array($nestedMap) ? new ObjectMap($nestedMap) : [...$parentsKeys, ...$keyPath]];
$dotMap = [$key => new ObjectMap($dotMap)];
} else {
$dotMap = [
$key => is_array($nestedMap) ? new ObjectMap($nestedMap) : [...$parentsKeys, ...$keyPath],

Check warning on line 369 in src/FormHydrator.php

View workflow job for this annotation

GitHub Actions / mutation / PHP 8.2-ubuntu-latest

Escaped Mutant for Mutator "SpreadOneItem": --- Original +++ New @@ @@ if ($dotMap !== []) { $dotMap = [$key => new ObjectMap($dotMap)]; } else { - $dotMap = [$key => is_array($nestedMap) ? new ObjectMap($nestedMap) : [...$parentsKeys, ...$keyPath]]; + $dotMap = [$key => is_array($nestedMap) ? new ObjectMap($nestedMap) : [[...$parentsKeys][0], ...$keyPath]]; } } return $dotMap;
];
}
}

return $dotMap;

Check warning on line 374 in src/FormHydrator.php

View workflow job for this annotation

GitHub Actions / mutation / PHP 8.2-ubuntu-latest

Escaped Mutant for Mutator "ArrayOneItem": --- Original +++ New @@ @@ $dotMap = [$key => is_array($nestedMap) ? new ObjectMap($nestedMap) : [...$parentsKeys, ...$keyPath]]; } } - return $dotMap; + return count($dotMap) > 1 ? array_slice($dotMap, 0, 1, true) : $dotMap; } /** * @param array<int, string> $path
}

/**
* @param array<int, string> $path
* @psalm-return MapType
*/
private function getMapFromRulesProvider(
RulesProviderInterface $formModel,
array $path = [],
): array {
$mapModel = [];
/**
* @psalm-param $rules RawRulesMap
*/
$rules = $formModel->getRules();
foreach ($rules as $key => $rule) {
if (is_int($key)) {
continue;
}
$mapModel[$key] = [...$path, $key];
if ($rule instanceof Nested) {
$nestedMap = $this->getNestedMap($rule, [...$path, $key]);
if ($nestedMap !== null) {
$mapModel[$key] = new ObjectMap($nestedMap);
}
} elseif (is_array($rule)) {
foreach ($rule as $ruleKey => $item) {
if ($item instanceof Nested) {
$nestedMap = $this->getNestedMap($item, [...$path, $key]);
if ($nestedMap !== null) {
$mapModel[$key] = new ObjectMap($nestedMap);
}
}
}
}
}

$mapNestedModels = $this->getMapNestedModels($formModel, $path);

return $this->mapMerge($mapModel, $mapNestedModels);
}

/**
* @param array<int, string> $path
* @psalm-return MapType
*/
private function getMapNestedModels(RulesProviderInterface $formModel, array $path): array
{
$reflection = new \ReflectionClass($formModel);
$properties = $reflection->getProperties(
\ReflectionProperty::IS_PUBLIC |
\ReflectionProperty::IS_PROTECTED |
\ReflectionProperty::IS_PRIVATE,
);

$propertiesNestedModels = [];
foreach ($properties as $property) {
if ($property->isStatic()) {
continue;
DAGpro marked this conversation as resolved.
Show resolved Hide resolved
}

if ($property->isReadOnly()) {
continue;
}

$propertyValue = $property->getValue($formModel);
if ($propertyValue instanceof RulesProviderInterface) {
$propertiesNestedModels[$property->getName()] = new ObjectMap(
$this->getMapFromRulesProvider(
$propertyValue,
[...$path, $property->getName()],
),
);
}
}

return $propertiesNestedModels;
}

/**
* @psalm-param MapType $map
* @psalm-param MapType $secondMap
* @psalm-return MapType
*/
private function mapMerge(array $map, array $secondMap): array
{
$result = [];
foreach ($map as $key => $value) {
if (isset($secondMap[$key]) && $value instanceof ObjectMap && $secondMap[$key] instanceof ObjectMap) {
$mergedMap = $this->mapMerge($value->map, $secondMap[$key]->map);
$result[$key] = new ObjectMap($mergedMap);
} elseif (isset($secondMap[$key]) && $secondMap[$key] instanceof ObjectMap) {
$result[$key] = $secondMap[$key];
} else {
$result[$key] = $value;
}
}

foreach ($secondMap as $key => $value) {
if (!isset($result[$key])) {
$result[$key] = $value;
}
}

return $result;
}
}
Loading
Loading