如果你在写一个大型应用,你会发现你始终在重复着相同的代码,一遍又一遍。也许你会自写工具来避免这种情况。为了将这种工具复用在不同的程序场合,它们必须被配置到合乎代码结构之程度。全新PropertyAccess component祝你实现上述过程。

注意本组件的代码并非全新。它们早已在Form组件中存在许久。我们决定提取代码令其成为全新组件,是因为很多人觉得它有用,我希望你也这样认为。(译注:本文作者Bernhard Schussek ,网名@webmozart,是Symfony重型自动组件“表单”的项目带头人)

我们先看个简单的例子:数据格子(data grid)。假设你已经建立了一个DataGrid类,为的是将可循环的数组转换成表格输出。代码是很简单的:

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;
    }
}

在控制器中,你可直接将简单的嵌套数组传给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'));

把格子显示在模板中也不难:

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>

类似这样的操作,又好又容易,但它有一个主要局限:格子只能与数组配合工作,不能与对象一起。而你经常使用的domain model却是对象,不是吗?

让我们看看怎样利用PropertyAccess组件来强化格子。下面代码是略微修改过的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);
        }
    }
 
    // ...
}

还需要改造一下controller才能让代码像前面的例子那样工作:

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

代码修改之后,老的数据仍然正常运行,发生了什么?

你是否注意到传递到DataGrid对象的被方括号括起来的参数?它们被称为PropertyPath。Property Paths可以有不同的代号写法:

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.

换句话说,property paths配置的是“如何去访问数据结构”,不管是数组,还是对象。前例的格子将作如下“翻译”:

1
2
3
4
$accessor = PropertyAccess::getPropertyAccessor();
 
$row[] = $accessor->getValue($item, '[firstName]');
$row[] = $accessor->getValue($item, '[lastName]');

等同于:

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

听起来较为抽象,但它可以在以下例程之后立即变明确。我们先对音乐家赋与不同的乐器:

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]',
));

不需要变动DataGrid,我们就能添加一列,用于显示嵌套数组中的值。很酷不是吗?

另外一例,我们引入Musician音乐家类:

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() { /* ... */ }
}

版面所限,我们留空具体代码,但是我认为这个例子可以非常好的自我解释。我们现在改变格子的构造器,读取Musician实例:

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]',
));

又一次,我们没有改变DataGrid类本身。通过使用没有方括号的标识,格子系统就能够访问getters来填充自己的单元格:

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

我们现在可以把乐器instrument放到一个类中,再把instrument[name]改为instrument.name。具体内容留给各位当作练习。

最后,通过使用PropertyAccessor,你不光可以从结构化数据中读取,还可以写入数据进去。例如:

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');

对于性能追求者要提醒的一点:在未来的某个Symfony版本中,PropertyAccess组件将提供一个“代码生成”的layer,用于提升PropertyAccessor类的速度(唯快不破?)。如果你想让你的代码向上兼容的话,确保使用一个全局accessor并将其注入到你需要的任何地方:

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)
    {
        // ...
    }
 
    // ...
}

然后是控制器:

1
2
3
4
5
6
use Symfony\Component\PropertyAccess\PropertyAccess;
 
// Globally unique PropertyAccessor instance 全局accessor实例
$accessor = PropertyAccess::getPropertyAccessor();
 
$grid = new DataGrid($data, array(/* ... */), $accessor);

就是这些了!希望你对这个小小的组件有兴趣!我是认真的,希望通过twitter或是下面的评论让我们知道你在自己的项目中使用(或计划使用)它!