[PropertyAccess] Add wildcard reads - #65297
#65297[PropertyAccess] Add wildcard reads#65297Boulea7 wants to merge 2 commits intosymfony:8.2symfony/symfony:8.2from Boulea7:feature/property-access-wildcardsBoulea7/symfony:feature/property-access-wildcardsCopy head branch name to clipboard
Conversation
| $propertyPath = $this->getPropertyPath($propertyPath); | ||
|
|
||
| $propertyValues = $this->readPropertiesUntil($zval, $propertyPath, $propertyPath->getLength(), $this->ignoreInvalidIndices); | ||
| $propertyValues = $this->readPropertiesUntil($zval, $propertyPath, $propertyPath->getLength(), $this->ignoreInvalidIndices, true); |
There was a problem hiding this comment.
Expanding wildcards only in the getter causes some strange behavior, the setter and the getter interpret the same path differently:
$pa = new PropertyAccessor();
$data = [
['name' => 'John'],
['name' => 'Sue'],
];
$pa->setValue($data, '[*][name]', 'X');
var_dump($data);
// [
// ['name' => 'John'],
// ['name' => 'Sue'],
// '*' => ['name' => 'X'],
// ]
var_dump($pa->getValue($data, '[*][name]'));
// ['John', 'Sue', 'X']The setter treats * as a literal key and creates $data['*'] = ['name' => 'X'], while the getter then merges that phantom entry into the wildcard result.
The same asymmetry exists with escaping:
$pa = new PropertyAccessor();
$data = [
'*' => 'One',
];
$pa->setValue($data, '[\*]', 'Two'); // writes the key '\*'
var_dump($pa->getValue($data, '[\*]')); // reads the key '*' → "One"| if ($expandWildcards && $isIndex && '*' === $property) { | ||
| $propertyValues[] = [self::VALUE => $this->readWildcard($zval, $propertyPath, $i + 1, $lastIndex, $ignoreInvalidIndices)]; | ||
| break; | ||
| } | ||
|
|
||
| if ($expandWildcards && $isIndex && '\\*' === $property) { | ||
| $property = '*'; | ||
| } | ||
|
|
There was a problem hiding this comment.
This is actually a BC break: in the current version [*] reads the literal * key, while with this PR it expands as a wildcard and returns a list, e.g.:
$pa = new PropertyAccessor();
$data = [
['name' => 'John'],
['name' => 'Sue'],
];
$pa->setValue($data, '[*][name]', 'X');
var_dump($pa->getValue($data, '[*][name]'));currently returns only "X", while with this PR it returns ['John', 'Sue', 'X'].
The same applies to the escaped form: [\*] currently reads the literal key \*, while with this PR it reads the key *.
|
|
||
| private function readWildcard(array $zval, PropertyPathInterface $propertyPath, int $nextIndex, int $lastIndex, bool $ignoreInvalidIndices): array | ||
| { | ||
| if (!\is_array($zval[self::VALUE]) && (!$zval[self::VALUE] instanceof \ArrayAccess || !$zval[self::VALUE] instanceof \Traversable)) { |
There was a problem hiding this comment.
Why require ArrayAccess? Since the wildcard only foreaches the collection, Iterator/IteratorAggregate would work just fine:
$people = new class() implements \IteratorAggregate {
public function getIterator(): \Iterator
{
yield ['name' => 'John'];
yield ['name' => 'Sue'];
}
};
var_dump((new PropertyAccessor())->getValue($people, '[*][name]'));
// throws a NoSuchIndexException, even though iterating it works fine| foreach ($values as $value) { | ||
| if (!\is_array($value) || !array_is_list($value)) { | ||
| return $values; | ||
| } | ||
| } | ||
|
|
||
| return $values ? array_merge(...$values) : []; |
There was a problem hiding this comment.
The list merging causes some strange inconsistent results, since the shape of the result depends on the data:
$people = [
['languages' => ['English', 'French']],
['languages' => ['English']],
];
$pa->getValue($people, '[*][languages]');
// ['English', 'French', 'English']
$people = [
['languages' => ['English', 'French']],
['languages' => 'English'],
];
$pa->getValue($people, '[*][languages]');
// [['English', 'French'], 'English']A single non-list value disables the merging entirely and the whole result becomes nested, so the same path returns differently shaped results depending on the data.
I'm not sure the merging is needed at all. Without it the result would always be predictable: one entry per item.
|
I ran the branch against these cases, and I confirm both of @HypeMC's findings. There are two more of the same kind. The escape syntax is a second BC break. On 8.2, The result shape depends on the data, not on the path. $pa->getValue([['tags' => ['a', 'b']], ['tags' => ['c']]], '[*][tags]'); // ['a', 'b', 'c']
$pa->getValue([['tags' => ['a', 'b']], ['tags' => ['k' => 'c']]], '[*][tags]'); // [['a', 'b'], ['k' => 'c']]One non-list value anywhere switches the whole result from flat to nested, so a consumer cannot type the return of a fixed path. All four issues have the same root: Would you rework it in that direction? |
This follows up on #52723 with the interface-free approach suggested there.
PropertyPathInterfaceandPropertyPathstay unchanged;PropertyAccessorrecognizes the existing*path element only while reading.An unescaped
[*]collects values across arrays and traversable array-access objects, including nested wildcards.[\*]still reads a literal*key. List results are flattened, matching the behavior proposed in #52723.The component test suite passes on PHP 8.4 and PHP 8.5 (498 tests, 578 assertions).
Thanks @Brajk19 for the original proposal.