feature #65297 [PropertyAccess] Add wildcard reads (Boulea7)
This PR was merged into the 8.2 branch.
Discussion
----------
[PropertyAccess] Add wildcard reads
| Q | A
| ------------- | ---
| Branch? | 8.2
| Bug fix? | no
| New feature? | yes
| Deprecations? | no
| Issues | Ref #52723
| License | MIT
A property path reads one branch of the graph: each element picks a single key or property. Collecting the same field from every item of a collection needs a loop, and one more per level of nesting.
This adds `[*]`, a path element meaning "every element here". It is opt-in, and off by default.
```php
$accessor = PropertyAccess::createPropertyAccessorBuilder()
->enableWildcardReads()
->getPropertyAccessor();
$people = [
['name' => 'Ada', 'jobs' => [['title' => 'programmer'], ['title' => 'writer']]],
['name' => 'Grace', 'jobs' => [['title' => 'computer scientist']]],
];
$accessor->getValue($people, '[*][name]');
// ['Ada', 'Grace']
$accessor->getValue($people, '[*][jobs][*][title]');
// [['programmer', 'writer'], ['computer scientist']]
```
In a Symfony application, turn it on with:
```yaml
framework:
property_access:
wildcard_reads: true
```
### Public API
* `PropertyAccessorBuilder::enableWildcardReads()`, `disableWildcardReads()` and `isWildcardReadsEnabled()`
* a `$wildcardReads` argument on the `PropertyAccessor` constructor, default `false`
* the `framework.property_access.wildcard_reads` option, default `false`
`PropertyPath` and `PropertyPathInterface` are untouched: the accessor recognises the `*` element it already parses. This is the difference with #52723, which added `isWildcard()` to `PropertyPathInterface` and made every implementation carry it.
With the option off, the accessor behaves exactly as before: `[*]` reads the index named `*`, `[\*]` reads the index named `\*`, and both can be written to.
### Shape of the result
One entry per matched element, in iteration order, keys dropped. The shape follows the path, never the data:
```php
$accessor->getValue([['tags' => ['a', 'b']], ['tags' => ['c']]], '[*][tags]'); // [['a', 'b'], ['c']]
$accessor->getValue([['tags' => ['a', 'b']], ['tags' => 'c']], '[*][tags]'); // [['a', 'b'], 'c']
```
Each wildcard adds one level of nesting, so a path with a fixed number of wildcards always returns the same shape. A trailing `[*]` returns the elements of the collection unchanged.
### What can be traversed
Anything iterable: arrays, `ArrayObject`, Doctrine's `ArrayCollection`, a plain `Iterator` or `IteratorAggregate`, a generator. Anything else throws `NoSuchIndexException`.
An empty collection gives `[]`. A missing index inside an element gives `null`, or throws with `enableExceptionOnInvalidIndex()`, as it does without a wildcard.
### Reading a literal `*` index
With wildcards enabled, a path element of `\*` addresses the index named `*`, on both sides. An extra backslash escapes the previous one, so a path element of `\\*` addresses the index named `\*`, and so on for deeper nestings.
Watch the quoting: in a single-quoted PHP string, `'[\*]'` and `'[\\*]'` are the same path.
```php
$data = [];
$accessor->setValue($data, '[\*]', 'star');
$accessor->getValue($data, '[\*]'); // 'star', the index named *
$accessor->setValue($data, '[\\\\*]', 'backslash-star');
$accessor->getValue($data, '[\\\\*]'); // 'backslash-star', the index named \*
```
### Writing
`[*]` reads only. `setValue()` on a path holding one throws `InvalidArgumentException`, and `isWritable()` returns `false`.
### Points for the documentation
* The feature is opt-in, through the builder or `framework.property_access.wildcard_reads`.
* One entry per matched element, one nesting level per wildcard, keys dropped.
* What can be traversed, and the exception raised for the rest.
* `\*` as a path element for a literal `*` index, `\\*` for a literal `\*` index, and the quoting trap that comes with writing those in PHP source.
* A wildcard path cannot be written to, so it does not belong in a form's `property_path`.
Thanks `@Brajk19` for the original proposal, and `@HypeMC` for the review.
Commits
-------
4a28f9cfb1a [PropertyAccess] Add opt-in wildcard reads A
Alexandre Daubois committed
496eaee41f0c4f35d430c8284dbc5af703a25418