SIGN IN SIGN UP

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