最近は社内で利用するPHPパッケージの開発やリファクタリングを行っているのですが、そこで大活躍してくれているのが、Symfonyコンポーネント達です。

その中で個人的にお気に入りのPropertyAccessコンポーネントを紹介したいと思います。

インストールの仕方

Composerで楽々インストール出来ます。

$ composer require symfony/property-access

準備

以下の様にオブジェクトを準備します。

<?php

use Symfony\Component\PropertyAccess\PropertyAccess;
$accessor = PropertyAccess::createPropertyAccessor();

基本的な使い方

配列の読み込み

<?php

$person = [
    'name' => 'ナガイ',
];
$people = [
    ['name' => 'ナガイ'],
    ['name' => 'ホシノ'],
];

var_dump($accessor->getValue($person, '[name]'));
var_dump($accessor->getValue($person, '[age]'));
var_dump($accessor->getValue($people, '[1][name]'));
string(9) "ナガイ"
NULL
string(9) "ホシノ"

配列の場合はブラケットでキーを囲む必要があります。
入れ子になっている配列にも対応してます。

オブジェクトの読み込み

<?php

class Person
{
    public $name;
    private $gender;

    public function getGender()
    {
        return $this->gender;
    }
    public function setGender($gender)
    {
        $this->gender = $gender;
    }
}
$person = new Person();
$person->name = 'ナガイ';
$person->setGender('男性');

var_dump($accessor->getValue($person, 'name'));
var_dump($accessor->getValue($person, 'gender'));
string(9) "ナガイ"
string(6) "男性"

公開プロパティの他になんとプライベートなプロパティもGetterが設定されていれば自動的に見つけてくれます!
もちろんエスパーではないので、見つけられるメソッドには下記のような法則がありますが、とても気が利いていますね。

property()
getProperty()
isProperty()
hasProperty()

柔軟性がないなあと感じる方がいるかも知れませんが、個人的にはベストプラクティスへ誘われている感じが心地良いです。

ちなみに

<?php

$accessor->getValue($person, 'birthday'));

の様にアクセス出来ないプロパティへの読み込みをしようとすると NoSuchPropertyException という例外が投げられます。
この辺りは、例外が発生しない配列に対する振る舞いと違いますね。

配列への書き込み

<?php

$person = [];
$accessor->setValue($person, '[name]', 'ナガイ');

var_dump($accessor->getValue($person, '[name]'));
string(9) "ナガイ"

読み込み時と使い方が一貫性があって、とても分かりやすいインタフェースだと思います。

オブジェクトへの書き込み

<?php

class Person
{
    public $name;
    private $gender;

    public function getGender()
    {
        return $this->gender;
    }
    public function setGender($gender)
    {
        $this->gender = $gender;
    }
}
$person = new Person();
$accessor->setValue($person, 'name', 'ホシノ');
$accessor->setValue($person, 'gender', '男性');


var_dump($accessor->getValue($person, 'name'));
var_dump($accessor->getValue($person, 'gender'));
string(9) "ホシノ"
string(6) "男性"

こちらも読み込み時と同じで、プライベートプロパティへの書き込みもSetterがあれば、それを通して自動的にセットしてくれます。
対応しているSetter名のルールは下記になります。

property()
setProperty()

オブジェクトへの書き込み(プロパティが配列の場合)

プロパティが配列の場合は、ちょっと特殊なケースにも対応しています。

<?php

class Person
{
    private $children = [];

    public function getChildren()
    {
        return $this->children;
    }
    public function addChild($child)
    {
        $this->children[] = $child;
    }
    public function removeChild($child)
    {
        $index = array_search($child, $children);
        if (isset($this->children[$index])) {
            unset($this->children[$index]);
        }
    }
}
$person = new Person();
$accessor->setValue($person, 'children', ['ホシノ', 'ヒシダ']);

var_dump($accessor->getValue($person, 'children'));
array(2) {
  [0]=>
  string(9) "ホシノ"
  [1]=>
  string(9) "ヒシダ"
}

なかなか手の込んだ振る舞いをしてくれていることが分かりますね。
addメソッドやremoveメソッドをちゃんと単数形で探し出してくれるところなんか、とてもスマートだと思います。

add + Propertyの単数形 + ()
remove + Propertyの単数形 + ()

英単語の単数形&複数形間の変換箇所はCakePHPのInflectorクラス等が有名ですが、このコンポーネントでは自前のStringUtilクラスで実装されている様です。

配列への読み書きチェック

基本的に配列であればOKというゆるめのチェックになっています。

<?php

$array = [];
var_dump($accessor->isReadable($array, '[hoge]'));
var_dump($accessor->isWritable($array, '[hoge]'));

$array = 'string';
var_dump($accessor->isReadable($array, '[hoge]'));
var_dump($accessor->isWritable($array, '[hoge]'));
bool(true)
bool(true)
bool(false)
bool(false)

オブジェクトへの読み書きチェック

オブジェクトの方が若干厳密なチェックになります。

<?php

class Person
{
    private $name;

    public $age;

    public function getName()
    {
        return $this->name;
    }
    public function setName($name)
    {
        $this->name = $name;
    }
}
$person = new Person();

var_dump($accessor->isReadable($person, 'name'));
var_dump($accessor->isWritable($person, 'name'));
var_dump($accessor->isReadable($person, 'age'));
var_dump($accessor->isWritable($person, 'age'));
var_dump($accessor->isReadable($person, 'birthday'));
var_dump($accessor->isWritable($person, 'birthday'));
bool(true)
bool(true)
bool(true)
bool(true)
bool(false)
bool(false)

配列とオブジェクトで若干チェックの厳しさが違うところに注意ですね。

主な使いどころ

  1. 配列に対するisset関数やempty関数の省略
  2. 型が分からないオブジェクトに対するアクセス

なんかに力を発揮してくれます。
個人的には、型が分からないオブジェクトに対してアクセスする時に、よく使ってますね。

終わりに

これはこのコンポーネントに限らず、Symfonyプロジェクトにいえることなんですが、やんわりとベストプラクティスに導いてくれるところがとても気に入ってます。

また、ツールとしてだけでなく、シンプルに纏められた機能や分かりやすいインタフェースなど参考にしたくなるような設計なのも使っていて気持ちの良いところなのかもしれませんね。

Symfonyコンポーネントには、PropertyAccess以外にもまだまだ便利なものが沢山あると思いますので、どんどん試して紹介していきたいなと思います!