PHP で JSON を特定のクラスにパースする方法

## はじめに

結論をいうと、厳密にはそのような方法はありませんが、代替方法のご提案です。

PHP 標準の [json_decode() 関数](http://php.net/manual/ja/function.json-decode.php) は、JSON 文字列をオブジェクトに変換しますが、そのオブジェクトは * stdClass * のオブジェクトに固定されています。
しかしそうではなく、自作の、特定のクラスにパースしたいという要求はあるでしょう。

## コード補完を有効にするだけ

まず、なぜそのようなことをしたいのか。おそらく多いのは「[エディタのコード補完機能を活用したい](https://netbeans.org/kb/74/php/editorguide_ja.html#vartyp-cmnt)」という理由ではないでしょうか。
それだけではないかもしれませんが、先ず補完さえ有効になれば良い場合は、Doc コメントで `@var` アノテーションを書くという手があります。 `@var` アノテーション は「変数型ヒント」のことで、以下のコード例の中では `/* @var ` ではじまる行のことです。

```php
// コード補完さえ有効になれば良い場合
class TestClass
{
    public $a;
    public $b;
    public $c;
}

$json = '{"a": 1, "b": 2, "c": 3}';

/* @var $obj \TestClass */
$obj  = json_decode($json);

// ※ $obj は TestClass のオブジェクトとして、以降、コード補完が有効になる。
```

これは私の使っている NetBeans IDE で有効です。このコメントで補完が有効になります。
調べた限り [Eclipse](https://qiita.com/ngyuki/items/507b5cba051f69fcaa4e) 、[PhpStorm](https://qiita.com/n_slender/items/3e85b6dd5869b34598bd#%E5%A4%89%E6%95%B0%E3%81%8C%E3%81%A9%E3%81%AE%E5%9E%8B%E3%81%AA%E3%81%AE%E3%81%8B%E3%82%92%E6%8C%87%E5%AE%9A%E3%81%99%E3%82%8B%E3%83%89%E3%82%AD%E3%83%A5%E3%83%A1%E3%83%B3%E3%83%86%E3%83%BC%E3%82%B7%E3%83%A7%E3%83%B3%E3%82%B3%E3%83%A1%E3%83%B3%E3%83%88) 、[ZendStudi](https://www.dondari.com/@var%E3%82%B3%E3%83%A1%E3%83%B3%E3%83%88%E3%81%A7%E5%A4%89%E6%95%B0%E3%81%AE%E5%9E%8B%E3%82%92%E6%98%8E%E8%A8%98%E3%81%99%E3%82%8B) でも有効です。その他のエディタ・IDE でも対応しているかもしれません。

## 汎用的にパース（制限あり）

コード補完だけでなく、どうしても特定のクラスにパースしたいのだという場合もあるかと思います。以下のコード例のようにリフレクションを使った関数 `json_decode_class()` を定義することで簡単に全てのプロパティをコピーできます。
ただし、この単純な関数はネストしたオブジェクトには対応していません。もしネストしたオブジェクトがあればそれらは stdClass のオブジェクトになります。

```php
// 簡易的にオブジェクトにパースしたい場合
function json_decode_class($json, $className)
{
    $reflClass = new ReflectionClass($className);
    $obj = $reflClass->newInstance();
    
    $objj = json_decode($json);
    $reflObject = new ReflectionObject($objj);
    $props = $reflObject->getProperties(ReflectionProperty::IS_PUBLIC);
    
    foreach ($props as $prop) {
        $obj->{$prop->getName()} = $objj->{$prop->getName()};
    }
    
    return $obj;
}

class TestClassB
{
    public $a;
    public $b;
    public $c;
}

$json = '{"a": 1, "b": 2, "c": 3, "d": 4}';

$objb = json_decode_class($json, 'TestClassB');
var_dump($objb);
```

このコード例の JSON ではクラスに存在しないプロパティが１件存在していますが、PHP はこのような場合、しれっと新しいプロパティを追加してエラーも例外も出しません。
JSON の利用は、様々な REST サービスからの情報取得が多いかと思いますが、それらサービスの突然の仕様変更にも自サービスで障害が出ない柔軟性ととらえるか、不正データの発見の遅れにつながる悩ましい問題ととらえるかは運用によるかと思います。

## ネストされたオブジェクトを再帰的にパースする

ネストされたオブジェクトを再帰的にパースしたい場合、リフレクションで Doc コメントを読み出して利用する方法があります。
当然ながら Doc コメントをこまめに書いておく必要があります。

以下の、より複雑な関数 `json_decode_class()` ではこの方法を使っています。

```php
// Docコメントを利用して再帰的にオブジェクトにパースしたい場合
function json_decode_class($jsonStr, $className)
{
    $reflClass = new ReflectionClass($className);
    $obj = $reflClass->newInstance();
    $json = json_decode($jsonStr);
    
    function object_parse($json, $obj)
    {
        $reflJson = new ReflectionObject($json);
        $props = $reflJson->getProperties(ReflectionProperty::IS_PUBLIC);
        foreach ($props as $prop) {
            if (!is_object($json->{$prop->getName()})) {
                $obj->{$prop->getName()} = $json->{$prop->getName()};
                continue;
            }
            
            $reflProp = new ReflectionProperty($obj, $prop->getName());
            if (!$reflProp) {
                $obj->{$prop->getName()} = $json->{$prop->getName()};
                continue;
            }
            
            $docComment = $reflProp->getDocComment();
            $matches = null;
            preg_match('/(?P<annotation>@var)\s+(?P<typeName>[^\s]+)\s+/m', $docComment, $matches);
            if (count($matches) <= 0) {
                $obj->{$prop->getName()} = $json->{$prop->getName()};
                continue;
            }
            
            try {
                $tmpReflObj = new ReflectionClass($matches['typeName']);
            } catch (ReflectionException $exc) {
                $obj->{$prop->getName()} = $json->{$prop->getName()};
                continue;
            }
            $tmpObj = $tmpReflObj->newInstance();
            
            $obj->{$prop->getName()} = object_parse($json->{$prop->getName()}, $tmpObj);
        }
        return $obj;
    }
    return object_parse($json, $obj);
}

class TestClassG
{
    public $e1;
    public $e2;
}

class TestClassF
{
    public $a;
    public $b;
    public $c;
    /**
     *
     * @var TestClassG
     */
    public $e;
}

$json = '{"a": 1, "b": 2, "c": 3, "d": 4, "e":{"e1": 5, "e2": 6}}';

$objf = json_decode_class($json, 'TestClassF');
var_dump($objf);
```

期待した結果が得られました。

## コンストラクタでパースする

リフレクションを使った上記の方法ではパフォーマンス的に不利、あるいは上述したように複雑なパースをしたい場合に対応できないなどの不都合があります。
下記のコードのようにコンストラクタに JSON 文字列を渡し、クラスごとに丁寧に明示的にパースするのが現実的な解法かもしれません。

```php
// リフレクションを使いたくない場合
class TestClassC
{
    public $a;
    public $b;
    public $c;
    public function __construct($json)
    {
        $obj = json_decode($json);
        $this->a = $obj->a;
        $this->b = $obj->b;
        $this->c = $obj->c;
    }
}

$json = '{"a": 1, "b": 2, "c": 3}';

$objc = new TestClassC($json);
var_dump($objc);

return;
```

## 最後に

はたしてここまでする必要があるでしょうか。正直なところ個人的には、コード補完さえできれば十分で、特定のクラスにパースしたい理由はあまり見つかりません。

しかしまあ、手段があるならいつかは役に立つ可能性があります。

JSON の利用は様々なサービスに広がりを見せており、将来は PHP 本体でも何らかの対応がなされるかもしれませんね。

## 蛇足

「JSON はあるけれどコード補完のためだけにクラスを書くのがめんどい」という場合のためのジェネレータを作りました。

```php
echo "\n\n\n";

function class_generate($obj)
{
    $buffer = '';
    $className = 'TheClass_' . md5(uniqid(rand(),1));
    
    $buffer .= 'class ' . $className . "\n";
    $buffer .= "{\n";
    
    $reflObj = new ReflectionObject($obj);
    $props = $reflObj->getProperties(ReflectionProperty::IS_PUBLIC);
    foreach ($props as $prop) {
        if (!is_object($obj->{$prop->getName()})) {
            $buffer .= '    public $' . $prop->getName() . ";\n";
        } else {
            $subClassName = class_generate($obj->{$prop->getName()});
            $buffer .= "    /**\n";
            $buffer .= "     * @var $subClassName\n" ;
            $buffer .= "     */\n";
            $buffer .= '    public $' . $prop->getName() . ";\n";
        }
    }
    $buffer .= "}\n";
    
    echo $buffer;
    
    return $className;
}

$json = '{"a": 1, "b": 2, "c": 3, "d": 4, "e":{"e1": 5, "e2": {"h1": 6}}}';
class_generate(json_decode($json));
```