【PHP】オブジェクトを連想配列に変換する方法まとめ

オブジェクトを連想配列に変換する方法をまとめました。

以前、[PHP にマジックメソッドとして `__toArray()` を追加しようという要望があったのですが、その要望は実現すること無くクローズされており](https://bugs.php.net/bug.php?id=38508)ので、現状 PHP としてこれといった変換方法は提供されていません。

そのためケースに合わせて変換の方法を使い分ける必要があります。

## 方法1: 型キャストする

最もお手軽な方法です。

プロパティが非公開( `protected` や `private` )の場合、純粋なプロパティ名として取得できない点には注意が必要です。

### 変換コード

```php
class ChildProperty
{
    public $publicProperty = true;
    protected $protectedProperty = true;
    private $privateProperty = true;
}

class ParentProperty
{
    public $publicProperty = true;
    protected $protectedProperty = true;
    private $privateProperty = true;

    public $childProperties = [];

    public function __construct()
    {
        $this->childProperties['key1'] = new ChildProperty();
        $this->childProperties['key2'] = new ChildProperty();
    }
}

$property = new ParentProperty();
$array = (array)$property;
```

### 結果

以下は `var_dump($array)` をした結果です。

```
array(4) {
	["publicProperty"]=>
	bool(true)
	["*protectedProperty"]=>
	bool(true)
	["App\Controller\Admin\ParentPropertyprivateProperty"]=>
	bool(true)
	["childProperties"]=>
	array(2) {
		["key1"]=>
		object(App\Controller\Admin\ChildProperty)#167 (3) {
			["publicProperty"]=>
			bool(true)
			["protectedProperty":protected]=>
			bool(true)
			["privateProperty":"App\Controller\Admin\ChildProperty":private]=>
			bool(true)
		}
		["key2"]=>
		object(App\Controller\Admin\ChildProperty)#168 (3) {
			["publicProperty"]=>
			bool(true)
			["protectedProperty":protected]=>
			bool(true)
			["privateProperty":"App\Controller\Admin\ChildProperty":private]=>
			bool(true)
		}
	}
}
```


## 方法2: get\_object\_vars 関数を使う

[get_object_vars()](https://www.php.net/manual/ja/function.get-object-vars) は関数の呼び出し元のスコープから見て公開されているプロパティを連想配列として取得できる関数です。

一階層目は連想配列に変換できていますが、二階層目の `$childProperties` が変換されていないことから再帰的な変換はしてくれないことが分かります。

### 変換コード

```php
class ChildProperty
{
    public $publicProperty = true;
    protected $protectedProperty = true;
    private $privateProperty = true;
}

class ParentProperty
{
    public $publicProperty = true;
    protected $protectedProperty = true;
    private $privateProperty = true;

    public $childProperties = [];

    public function __construct()
    {
        $this->childProperties['key1'] = new ChildProperty();
        $this->childProperties['key2'] = new ChildProperty();
    }

    /**
     * @return array
     */
    public function toArray()
    {
        return get_object_vars($this);
    }
}

$property = new ParentProperty();
$array = $property->toArray();
```

### 結果

```
array(4) {
	["publicProperty"]=>
	bool(true)
	["protectedProperty"]=>
	bool(true)
	["privateProperty"]=>
	bool(true)
	["childProperties"]=>
	array(2) {
		["key1"]=>
		object(App\Controller\Admin\ChildProperty)#167 (3) {
			["publicProperty"]=>
			bool(true)
			["protectedProperty":protected]=>
			bool(true)
			["privateProperty":"App\Controller\Admin\ChildProperty":private]=>
			bool(true)
		}
		["key2"]=>
		object(App\Controller\Admin\ChildProperty)#168 (3) {
			["publicProperty"]=>
			bool(true)
			["protectedProperty":protected]=>
			bool(true)
			["privateProperty":"App\Controller\Admin\ChildProperty":private]=>
			bool(true)
		}
	}
}
```

## 方法3: json\_encode と json\_decode を使う

一度オブジェクトを JSON 文字列化した後に配列としてデコードし直して変換する方法です。一階層目以降も上手く変換できています。public のプロパティのみが含まれるのが特徴です。

[JsonSerializable インターフェース](https://www.php.net/manual/ja/class.jsonserializable.php) の [jsonSerialize\(\)](https://www.php.net/manual/ja/jsonserializable.jsonserialize.php) を実装することで、変換時に含めたいプロパティを指定することも可能です。

### 変換コード

```php
class ChildProperty
{
    public $publicProperty = true;
    protected $protectedProperty = true;
    private $privateProperty = true;
}

class ParentProperty
{
    public $publicProperty = true;
    protected $protectedProperty = true;
    private $privateProperty = true;

    public $childProperties = [];

    public function __construct()
    {
        $this->childProperties['key1'] = new ChildProperty();
        $this->childProperties['key2'] = new ChildProperty();
    }
}

$property = new ParentProperty();
$array = json_decode(json_encode($property), true);
```

### 結果

```
array(2) {
	["publicProperty"]=>
	bool(true)
	["childProperties"]=>
	array(2) {
		["key1"]=>
		array(1) {
			["publicProperty"]=>
			bool(true)
		}
		["key2"]=>
		array(1) {
			["publicProperty"]=>
			bool(true)
		}
	}
}
```

## 方法4: 再帰的に配列変換メソッドを呼び出す実装を書いて頑張る

自分で変換を実装すると変換時の挙動を調整したくなったときに融通が効きます。

以下、少し長いですが、非公開のプロパティも含めた全てのプロパティを変換するサンプルを書いてみました。~~（書いてる途中一体何をやってるんだ俺はという感情に襲われました）~~

### 変換コード

```php
interface Arrayable
{
    public function toArray(): array;
}

class ChildProperty implements Arrayable
{
    public $publicProperty = true;
    protected $protectedProperty = true;
    private $privateProperty = true;

    public function toArray(): array
    {
        return get_object_vars($this) ?? [];
    }
}

class ParentProperty implements Arrayable
{
    public $publicProperty = true;
    protected $protectedProperty = true;
    private $privateProperty = true;

    public $childProperties = [];

    public function __construct()
    {
        $this->childProperties['key1'] = new ChildProperty();
        $this->childProperties['key2'] = new ChildProperty();
    }

    public function toArray(): array
    {
        $properties = get_object_vars($this);
        return $this->toArrayRecursive($properties);
    }

    private function toArrayRecursive($property)
    {
        // 配列の場合は中身を再帰的に配列に変換
        if (is_array($property)) {
            $array = [];
            foreach ($property as $key => $value) {
                $array[$key] = $this->toArrayRecursive($value);
            }
            return $array;
        }
        // 配列変換が実装されているオブジェクトの場合は変換
        if (is_object($property)) {
            if (!($property instanceof Arrayable)) {
                throw new \LogicException('配列に変換できないオブジェクトが含まれています');
            }
            return $property->toArray();
        }
        // プリミティブ型の場合
        return $property;
    }
}

$property = new ParentProperty();
$array = $property->toArray();
```

### 結果

```
array(4) {
  ["publicProperty"]=>
  bool(true)
  ["protectedProperty"]=>
  bool(true)
  ["privateProperty"]=>
  bool(true)
  ["childProperties"]=>
  array(2) {
    ["key1"]=>
    array(3) {
      ["publicProperty"]=>
      bool(true)
      ["protectedProperty"]=>
      bool(true)
      ["privateProperty"]=>
      bool(true)
    }
    ["key2"]=>
    array(3) {
      ["publicProperty"]=>
      bool(true)
      ["protectedProperty"]=>
      bool(true)
      ["privateProperty"]=>
      bool(true)
    }
  }
}
```

## 各方法の速度比較

参考までに PHP 7.3 環境でそれぞれの方法を2万回繰り返したときの実行速度を比較した結果を貼っておきます。型キャストの方法が最も高速という結果になりました。（相当数繰り返すようなケースでもなければ意識する必要は殆ど無いと思います。）

![速度比較.png](https://qiita-image-store.s3.amazonaws.com/0/59802/2d6178b8-5699-2fde-0e12-e7c6efe38246.png)


## 結果の早見表

<table>
<tr><th>方法</th><th>実行速度</th><th>トップ階層</th><th>二階層目以降</th></tr>
<tr><th>方法1: 型キャスト</th><td>0.0016秒</td><td><b>配列に変換される</b></td><td>配列に変換されない</td></tr>
<tr><th>方法2: get_object_vars</th><td>0.0077秒</td><td><b>配列に変換される</b></td><td>配列に変換されない</td></tr>
<tr><th>方法3: json_encode</th><td>0.0374秒</td><td><b>配列に変換される</b></td><td><b>配列に変換される</b></td></tr>
<tr><th>方法4: 自前変換</th><td>0.0387秒</td><td><b>配列に変換される</b></td><td><b>配列に変換される</b></td></tr>
</table>


## まとめ

今回の方法の中だと「方法3: json\_encode と json\_decode を使う」が最も柔軟で簡単な感じがしました。

[JsonSerializable インターフェース](https://www.php.net/manual/ja/class.jsonserializable.php) を実装すれば変換時に含めたいプロパティをハンドリングできる点も大きいと思います。例えば CakePHP では <a href="https://github.com/cakephp/cakephp/blob/4.1.4/src/Datasource/EntityInterface.php#L29" target="_blank">DB の個々の行データを表す Entity クラスも標準で JsonSerializable が実装されていたり</a>します。

ただ、初見で `json_decode(json_encode($var), true)` のようなコードを見ても意図が伝わりにくいので、意味付けのためにもヘルパー関数を用意しておくと良いかもしれません。

```php
function toArray($var): array
{
    return json_decode(json_encode($var), true);
}
```

他にも方法はあると思いますが、単一責任の原則の観点でいうとオブジェクト側の責任で変換する方法の方が好ましいと思います。

他にいい方法があればご教示ください。