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.
Nice component, thanks ! I plan to use it to define simple "access routes" to form data.
Briliant! Need to check it out asap! :)
small typo:
by checking https://github.com/symfony/symfony/blob/master/src/Symfony/Component/PropertyAccess/PropertyAccessor.php
readProperty() function, we can see that the access order is:
$data->getProp(), $data->isProp(), $data->hasProp(), $data->__get('prop') $data->prop, whichever is found first
$data->isProp() and $data->hasProp() were in the incorrect order.
Félix: Thanks, fixed.
Awesome! I use it in my data exporter bundle (http://github.com/EE/DataExporter)
This is what i love about symfony community. They give clear, detailed examples and it all make sens! Thank you!
Great component!
Just out of interest, was this known as PropertyPath in the Form component? I have used it before and I think might reopen the project and test it out.
Christopher: Yes, you are right. In fact, the PropertyPath class is still there, although split into two separate classes PropertyPath and PropertyAccessor.