-
-
Notifications
You must be signed in to change notification settings - Fork 3
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
base: master
Are you sure you want to change the base?
Changes from all commits
e721d2e
4001809
04301d3
923b46e
9a634a0
d50a47f
5eca613
ed59507
aecde9c
1f9b4b1
d6dfa20
4aecddf
a397a07
424a924
9e37429
fe1628f
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -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 | ||
{ | ||
|
@@ -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( | ||
|
@@ -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
|
||
} | ||
|
||
$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
|
||
} | ||
|
||
/** | ||
* Get a map of object property names mapped to keys in the data array. | ||
* | ||
|
@@ -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
|
||
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
|
||
} | ||
|
||
if (is_array($nestedRules)) { | ||
$keyPath = null; | ||
if (str_contains($key, '.')) { | ||
Check warning on line 328 in src/FormHydrator.php
|
||
$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
|
||
if ($item instanceof Nested) { | ||
$pathKeys = $keyPath ?? [$key]; | ||
$nestedMap = $this->getNestedMap($item, [...$parentKeys, ...$pathKeys]); | ||
Check warning on line 339 in src/FormHydrator.php
|
||
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
|
||
$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
|
||
]; | ||
} | ||
} | ||
|
||
return $dotMap; | ||
Check warning on line 374 in src/FormHydrator.php
|
||
} | ||
|
||
/** | ||
* @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; | ||
} | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Does that display whole form?
There was a problem hiding this comment.
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 🤷♂️