Bernhard Schussek
Contributed by Bernhard Schussek in #6595

If you write large applications, you probably find yourself repeating the same code time and again. You probably also developed tools to avoid this. For reusing such tools in different places and applications, they must be configurable to adapt to the structure of the code they are used with. The new PropertyAccess component helps you with that.

Note: The code of this component is not new. It has existed for a long time within the Form component. We decided to extract it to a separate component because others found it useful, and I hope you will too!

Let's look at a simple example: A data grid. Assume you created a class DataGrid for turning nested arrays into tables. The code is simple:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
class DataGrid
{
    private $rows = array();
    
    private $columns = array();
    
    public function __construct($items, array $columns)
    {
        if (!is_array($items) && !$items instanceof \Traversable) {
            throw new \InvalidArgumentException(
                'The grid items should be an array or a \Traversable.'
            );
        }
        
        // Let keys and values contain the column name
        $columns = array_combine($columns, $columns);
        
        // Turn values into human readable names
        $this->columns = array_map(function ($column) {
            return ucfirst(trim(preg_replace(
                // (1) Replace special chars by spaces
                // (2) Insert spaces between lower-case and upper-case
                array('/[_\W]+/', '/([a-z])([A-Z])/'),
                array(' ', '$1 $2'),
                $column
            )));
        }, $columns);
        
        // Store row data
        foreach ($items as $item) {
            $this->rows[] = array_intersect_key($item, $columns);
        }
    }
    
    public function getColumns()
    {
        return $this->columns;
    }
    
    public function getRows()
    {
        return $this->rows;
    }
}

In a controller, you can pass a simple, nested array to this grid:

1
2
3
4
5
6
7
8
$data = array(
    array('id' => 1, 'firstName' => 'Paul', 'lastName' => 'Stanley'),
    array('id' => 2, 'firstName' => 'Gene', 'lastName' => 'Simmons'),
    array('id' => 3, 'firstName' => 'Ace', 'lastName' => 'Frehley'),
    array('id' => 4, 'firstName' => 'Peter', 'lastName' => 'Criss'),
);

$grid = new DataGrid($data, array('firstName', 'lastName'));

Displaying this grid in the template is also very easy:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
<table>
<thead>
    <tr>
    {% for column in grid.columns %}
        <th>{{ column }}</th>
    {% endfor %}
    </tr>
</thead>
<tbody>
{% for row in grid.rows %}
    <tr>
    {% for cell in row %}
        <td>{{ cell }}</td>
    {% endfor %}
    </tr>
{% endfor %}
</tbody>
</table>

As nice and easy as this is, it has one major limitation: The grid can only work with arrays, not with objects. And you do use objects for your domain model, right?

Let's see how we can use the PropertyAccess component to enhance the grid. See below for a slightly adapted DataGrid:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
use Symfony\Component\PropertyAccess\PropertyAccess;

class DataGrid
{
    // ...
    
    public function __construct($items, array $columns)
    {
        // ...
        
        $accessor = PropertyAccess::getPropertyAccessor();
        
        // Store row data
        foreach ($items as $item) {
            $this->rows[] = array_map(function ($path) use ($item, $accessor) {
                return $accessor->getValue($item, $path);
            }, $columns);
        }
    }
    
    // ...
}

We also need to adapt the controller to make the previous example work:

1
$grid = new DataGrid($data, array('[firstName]', '[lastName]'));

Ok, we changed some code. Our old stuff still works. What happened?

Did you notice the words between squared brackets that we passed to the DataGrid instance? These are called property paths. Property paths can have various notations:

Path Equivalent to
[index] $data['index']
[index][sub] $data['index']['sub']
prop $data->getProp(), $data->isProp(), $data->hasProp(), $data->__get('prop') or $data->prop, whichever is found first
prop.sub $data->getProp()->getSub(), $data->getProp()->isSub() etc.

Put in other words, property paths configure how to access data in a data structure, be it an array (or \ArrayAccess) or any other object. What the grid in the previous example does translates to this:

1
2
3
4
$accessor = PropertyAccess::getPropertyAccessor();

$row[] = $accessor->getValue($item, '[firstName]');
$row[] = $accessor->getValue($item, '[lastName]');

Which is the equivalent for:

1
2
$row[] = $item['firstName'];
$row[] = $item['lastName'];

Maybe this sounds a bit abstract, but it will become clear very fast after giving some examples. For a start, let our musicians have instruments:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
$data = array(
    array(
        'id' => 1,
        'firstName' => 'Paul',
        'lastName' => 'Stanley',
        'instrument' => array(
            'name' => 'Guitar',
            'type' => 'String instrument',
        ),
    ),
    // ...
);

$grid = new DataGrid($data, array(
    '[firstName]',
    '[lastName]',
    '[instrument][name]',
));

Without changing DataGrid, we added a column showing the value contained in the nested array. Isn't that cool?

As another example, let's introduce a Musician class:

1
2
3
4
5
6
7
8
9
10
11
12
class Musician
{
    public function __construct($id, $firstName, $lastName, array $instrument)
    {
        // ...
    }
    
    public function getId() { /* ... */ }
    public function getFirstName() { /* ... */ }
    public function getLastName() { /* ... */ }
    public function getInstrument() { /* ... */ }
}

The detailed code was left away for brevity, but I think this sample is very self-explanatory. Let's change the grid definition to read values from Musician instances:

1
2
3
4
5
6
7
8
9
10
11
12
13
$data = array(
    new Musician(1, 'Paul', 'Stanley', array(
        'name' => 'Guitar',
        'type' => 'String instrument',
    ),
    // ...
);

$grid = new DataGrid($data, array(
    'firstName',
    'lastName',
    'instrument[name]',
));

Again, we did not have to change the DataGrid class. By using the notation without squared brackets, the grid is now accessing the getters to fill its cells:

1
2
3
$row[] = $item->getFirstName();
$row[] = $item->getLastName();
$row[] = $item->getInstrument()['name'];

We could also turn the instrument into a class and change instrument[name] to instrument.name. I'll leave the details of this change as an exercise to you.

Finally, you can use the PropertyAccessor not only to read from structured data, but also to write into it. For example:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
$data = array(
    array(
        'id' => 1,
        'firstName' => 'Paul',
        'lastName' => 'Stanley',
        'instrument' => array(
            'name' => 'Guitar',
            'type' => 'String instrument',
        ),
    ),
    // ...
);

$accessor->setValue($data, '[0][instrument][name]', 'Vocals');

A last note for those who are concerned about performance: In one of the next Symfony releases, the PropertyAccess component will provide a code generation layer for optimizing the speed of the PropertyAccessor class (faster is always better, right?). If you want to make your code forward-compatible with this feature, make sure to use one global accessor and inject it wherever you need it:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
use Symfony\Component\PropertyAccess\PropertyAccessorInterface;

class DataGrid
{
    // ...
    
    public function __construct($items, array $columns,
            PropertyAccessorInterface $accessor)
    {
        // ...
    }
    
    // ...
}

And in the controller:

1
2
3
4
5
6
use Symfony\Component\PropertyAccess\PropertyAccess;

// Globally unique PropertyAccessor instance
$accessor = PropertyAccess::getPropertyAccessor();

$grid = new DataGrid($data, array(/* ... */), $accessor);

That's all! I hope you have fun with this little new component. I'm curious, so let us know in the comments or via Twitter what you use (or plan to use) it for.

Published in #Living on the edge