PHP でオレオレ JSON 入出力 API を作ってみた

PHP で JSON の入出力を行う自作の API を開発したので, 以下にご紹介します.

# 使い方

まずは実際にこんな感じで動きますよ！ というのをお見せしたいと思います.

## インストール

一番手軽な導入方法は composer を使ったインストールです.
以下のコマンドで, [汎用クラスライブラリ PEACH2](https://trashtoy.github.io/PEACH2/) のクラスファイル一式を取り込みます.

```
$ composer require trashtoy/peach2
```

または [GitHub の配布ページ](https://github.com/trashtoy/PEACH2/releases) からソース一式を手動でダウンロードして展開してください.

## 動作サンプル

以下に簡単なデモコードを紹介します.

### エンコードのサンプル

composer のインストールを行ったディレクトリの配下で, 以下のような PHP ファイルを作成します.

```demo1.php
<?php

use Peach\DF\JsonCodec;

require_once(__DIR__ . "/vendor/autoload.php");

$codec = new JsonCodec();
$data  = [
    "foo" => "TEST",
    "bar" => [3, 5, 7],
    "baz" => [
        "hoge" => true,
        "fuga" => false,
    ],
];
echo $codec->encode($data), PHP_EOL;
```

この PHP を実行すると以下の出力が得られます.

```
{"foo":"TEST","bar":[3,5,7],"baz":{"hoge":true,"fuga":false}}
```

### デコードのサンプル

もちろんデコードも出来ます.

```demo2.php
<?php

use Peach\DF\JsonCodec;

require_once(__DIR__ . "/vendor/autoload.php");

$codec = new JsonCodec();
$json  = '{"foo":"TEST","bar":[3,5,7],"baz":{"hoge":true,"fuga":false}}';
var_dump($codec->decode($json));
```

これを実行すると以下の結果が得られます.

```
class stdClass#14 (3) {
  public $foo =>
  string(4) "TEST"
  public $bar =>
  array(3) {
    [0] =>
    int(3)
    [1] =>
    int(5)
    [2] =>
    int(7)
  }
  public $baz =>
  class stdClass#21 (2) {
    public $hoge =>
    bool(true)
    public $fuga =>
    bool(false)
  }
}
```

### オプションを指定して出力をカスタマイズ

オブジェクトの初期化時にオプションを指定することにより, 出力結果をカスタマイズすることができます.
コンストラクタの引数の有無で, エンコード時の出力がどう変わるか見てみましょう.

```demo3.php
<?php

use Peach\DF\JsonCodec;

require_once(__DIR__ . "/vendor/autoload.php");

$data  = [
    "id" => 15,
    "name" => "太郎",
    "height" => 173.0,
    "weight" => 59.6,
];

$option =
        JsonCodec::PRETTY_PRINT |          // いい感じにインデントする
        JsonCodec::UNESCAPED_UNICODE |     // マルチバイト文字をエスケープしない
        JsonCodec::PRESERVE_ZERO_FRACTION; // 小数点以下がゼロでも勝手に整数化しない

$codec1 = new JsonCodec();
$codec2 = new JsonCodec($option);
echo $codec1->encode($data), PHP_EOL;
echo $codec2->encode($data), PHP_EOL;
```

このコードは以下の結果を出力します.

```
{"id":15,"name":"\u592a\u90ce","height":173,"weight":59.6}
{
    "id": 15,
    "name": "太郎",
    "height": 173.0,
    "weight": 59.6
}
```

ご覧のとおり `$codec1` と `$codec2` で出力結果が変わることが確認できました.

なおデコードの出力もカスタマイズできます.
コンストラクタの第 2 引数にデコードオプションを指定してください.

```demo4.php
<?php

use Peach\DF\JsonCodec;

require_once(__DIR__ . "/vendor/autoload.php");

$option =
        JsonCodec::OBJECT_AS_ARRAY |  // 結果を stdClass ではなく配列にする
        JsonCodec::BIGINT_AS_STRING;  // 巨大整数は文字列に変換する
$codec1 = new JsonCodec();
$codec2 = new JsonCodec(null, $option);

$json = '{"username":"trashtoy","tweet_id":542157944905678851}';
var_dump($codec1->decode($json));
var_dump($codec2->decode($json));
```

このコードは以下の結果となります.

```
class stdClass#18 (2) {
  public $username =>
  string(8) "trashtoy"
  public $tweet_id =>
  double(5.4215794490568E+17)
}
array(2) {
  'username' =>
  string(8) "trashtoy"
  'tweet_id' =>
  string(18) "542157944905678851"
}
```

このように

- `$codec1` の場合: 結果は stdClass オブジェクト, tweet_id の値が double 型の近似値となっている
- `$codec2` の場合: 結果は配列, tweet_id の値が正確に保持できている.

という結果が確認できました.

# 開発経緯

PHP による JSON の入出力といえば `json_encode()`, `json_decode()` が一般的かと思います.
標準関数で提供されているのに何故わざわざ車輪の再発明を？と思われる方も多いでしょう.

## バージョンによってサポートされているオプションが違う

ためしに以下の PHP コードを色んなバージョンの処理系 (5.1.6, 5.2.17, 5.3.0, 5.3.29, 5.4.45, 5.5.35, 5.6.21, 7.0.6) で走らせてみましょう.

```test01.php
<?php
$arr = array("foo" => "テスト", "bar" => 2.0, "baz" => "3.14");
$opt = JSON_NUMERIC_CHECK | JSON_PRETTY_PRINT | JSON_UNESCAPED_UNICODE | JSON_PRESERVE_ZERO_FRACTION;
echo json_encode($arr, $opt), PHP_EOL;
```

結果は以下の通りです.

```5.1.6
Fatal error: Call to undefined function json_encode() in C:\jsontest\test01.php on line 3
```

5.1 系ではまだ json 関数がサポートされていないので Fatal Error.

```5.2.17
Notice: Use of undefined constant JSON_NUMERIC_CHECK - assumed 'JSON_NUMERIC_CHECK' in C:\jsontest\test01.php on line 3

Notice: Use of undefined constant JSON_PRETTY_PRINT - assumed 'JSON_PRETTY_PRINT' in C:\jsontest\test01.php on line 3

Notice: Use of undefined constant JSON_UNESCAPED_UNICODE - assumed 'JSON_UNESCAPED_UNICODE' in C:\jsontest\test01.php on line 3

Notice: Use of undefined constant JSON_PRESERVE_ZERO_FRACTION - assumed 'JSON_PRESERVE_ZERO_FRACTION' in C:\jsontest\test01.php on line 3

Warning: json_encode() expects exactly 1 parameter, 2 given in C:\jsontest\test01.php on line 3

```

json 関数がサポートされた 5.2 系ではまだ引数のオプションがサポートされておらず, 「そんな定数は存在しません」＆「引数の個数が間違っています」という警告が出ています. (最終的に json_encode() は FALSE を返しています)

```5.3.0
Notice: Use of undefined constant JSON_NUMERIC_CHECK - assumed 'JSON_NUMERIC_CHECK' in C:\jsontest\test01.php on line 3

Notice: Use of undefined constant JSON_PRETTY_PRINT - assumed 'JSON_PRETTY_PRINT' in C:\jsontest\test01.php on line 3

Notice: Use of undefined constant JSON_UNESCAPED_UNICODE - assumed 'JSON_UNESCAPED_UNICODE' in C:\jsontest\test01.php on line 3

Notice: Use of undefined constant JSON_PRESERVE_ZERO_FRACTION - assumed 'JSON_PRESERVE_ZERO_FRACTION' in C:\jsontest\test01.php on line 3

Warning: json_encode() expects parameter 2 to be long, string given in C:\jsontest\test01.php on line 3

```

5.3.0 では引数のオプションがどれもサポートされていないので, 引数に文字列が渡された (存在しない定数は文字列として扱われるため) ことになってしまっています.

```5.3.29
Notice: Use of undefined constant JSON_PRETTY_PRINT - assumed 'JSON_PRETTY_PRINT' in C:\jsontest\test01.php on line 3

Notice: Use of undefined constant JSON_UNESCAPED_UNICODE - assumed 'JSON_UNESCAPED_UNICODE' in C:\jsontest\test01.php on line 3

Notice: Use of undefined constant JSON_PRESERVE_ZERO_FRACTION - assumed 'JSON_PRESERVE_ZERO_FRACTION' in C:\jsontest\test01.php on line 3
{"foo":"\u30c6\u30b9\u30c8","bar":2,"baz":3.14}
```

初めてまともな結果が返ってきましたが相変わらず Notice エラーだらけ.
5.3.3 で実装された `JSON_NUMERIC_CHECK` のみかろうじて活きてます.

```5.4.0～5.6.5
Notice: Use of undefined constant JSON_PRESERVE_ZERO_FRACTION - assumed 'JSON_PRESERVE_ZERO_FRACTION' in C:\jsontest\test01.php on line 3

{
    "foo": "テスト",
    "bar": 2,
    "baz": 3.14
}
```

5.4.0 以降ではサポートされたオプションが増えて Notice エラーがずっと減りましたが, `JSON_PRESERVE_ZERO_FRACTION` を認識しないため本来 `2.0` と出力されなければいけない bar の値が整数の 2 になっています.

```5.6.6～7.0.6
{
    "foo": "テスト",
    "bar": 2.0,
    "baz": 3.14
}
```

5.6.6 でようやくすべてのオプションが実装されたので (※2016-05-13 現在) エラーがなくなり, 想定される結果が返ってきました.

このようにバージョンによってエラーが出たり出なかったりするため, 動作しているバージョンを意識して開発を進める必要があります.
本番サーバーのほうが開発サーバーよりバージョンが古くて, 開発環境で出なかったエラーが本番で出てしまったなんて事故があると怖いですね.

それでは自作の JSON 入出力ライブラリで同様の処理を行ってみましょう.

```test02.php
<?php

use Peach\DF\JsonCodec;

require_once(__DIR__ . "/vendor/autoload.php");

$arr = array("foo" => "テスト", "bar" => 2.0, "baz" => "3.14");
$option =
        JsonCodec::NUMERIC_CHECK |
        JsonCodec::PRETTY_PRINT |
        JsonCodec::UNESCAPED_UNICODE |
        JsonCodec::PRESERVE_ZERO_FRACTION;
$codec = new JsonCodec($option);
echo $codec->encode($arr), PHP_EOL;
```

この結果は以下の通りです.

```5.3.0～7.0.6
{
    "foo": "テスト",
    "bar": 2.0,
    "baz": 3.14
}
```

5.3.0 から 7.0.6 (2016-05-13 現在の最新バージョン) までのすべてのバージョンで同じ結果を得ることが出来ました.
PHP のバージョンに依存しない JSON 入出力を実装したい場合は是非ご検討ください.

また, 名前空間が使えないため冗長なコードになりますが, 旧バージョンの [PEACH1](https://trashtoy.github.io/PEACH/) のライブラリを使うことで 5.1.1 から 7 系までのすべてのバージョンでの動作を保証します.
JSON 関数が提供されていない 5.1 系の環境で JSON 入出力を行いたい場合はこのライブラリの出番です！

## ライセンスの問題で標準の JSON 関数が使用できない環境がある

json_encode() と json_decode() のモジュールに適用されている「JSON ライセンス」と呼ばれるライセンスが曲者で, このため一部の Linux ではただ PHP をインストールしただけだと JSON 関数が使えない状況となっています.

- [本の虫: 邪悪なJSONライセンス](http://cpplover.blogspot.jp/2012/03/json.html)
- [PHPのjsonモジュールがDebianなどLinuxディストロからから削除される](http://boscono.hatenablog.com/entry/2013/08/21/233628)

自作のオレオレ API の実装は既存の json_encode() や json_decode() に全く依存しておらず [^1] , MIT ライセンスで提供しています.
共用レンタルサーバーなどの PHP の設定を外部からいじることのできない環境で, 運悪く json 関数が使えなかった場合でも問題なく使えます.
ちなみに PHP7 系ではライセンスの問題がクリアになったみたいですね.[^2]

[^1]: 扱うことのできる JSON のフォーマットは [RFC 7159](https://tools.ietf.org/html/rfc7159) に基づきます
[^2]: 参考: [PHPのJSONライセンス問題が一応決着](http://d.hatena.ne.jp/hnw/20150419)

## オブジェクト指向を有効活用したい

オブジェクト指向の旨味 (ポリモーフィズム・カスタマイズ性・再利用性など) が活きるような API が欲しかったというのも理由の一つです.

今回ご紹介した [JsonCodec](https://trashtoy.github.io/PEACH2/class-Peach.DF.Utf8Codec.html) は, [Codec](https://trashtoy.github.io/PEACH2/class-Peach.DF.Codec.html) というインタフェースを実装しているのですが, 同じ Codec インタフェースの具象クラスである [Base64Codec](https://trashtoy.github.io/PEACH2/class-Peach.DF.Base64Codec.html) や [SerializationCodec](https://trashtoy.github.io/PEACH2/class-Peach.DF.SerializationCodec.html) などと入れ替えたり組み合わせたり, といった使い方が可能です.

以下のサンプルでは

- エンコード: 配列 → JSON → Base64文字列
- デコード: Base64 文字列 → JSON → 配列

という処理をやってくれる Codec を作成しています.

```test03.php
<?php

use Peach\DF\JsonCodec;
use Peach\DF\Base64Codec;
use Peach\DF\CodecChain;

require_once(__DIR__ . "/vendor/autoload.php");

echo "encode", PHP_EOL;
$arr   = array("foo" => 1, "bar" => 2, "baz" => 3);
$codec = new CodecChain(new JsonCodec(), Base64Codec::getInstance());
$str   = $codec->encode($arr);
echo $str, PHP_EOL, PHP_EOL;

echo "decode", PHP_EOL;
$obj = $codec->decode($str);
var_dump($obj);
```

以下出力です.

```
encode
eyJmb28iOjEsImJhciI6MiwiYmF6IjozfQ==

decode
object(stdClass)#17 (3) {
  ["foo"]=>
  int(1)
  ["bar"]=>
  int(2)
  ["baz"]=>
  int(3)
}
```

こんな凝ったことをやる機会というのはほとんどないとは思いますが, こういう風にオブジェクト指向的に書けるよ！というサンプルでした.

# まとめ

- バージョン毎のサポートオプションの違いを吸収し, PHP 5.3 ～ PHP 7 最新版まで同じように使える API を作りました
- composer を使えばサクッと導入できます
- JSON ライセンスの問題も一切無関係
- 標準の JSON 関数と違い, オブジェクト指向の旨味を活かしたコードが書けます

ためしに使ってくれる人が出てくれると嬉しいです.

- GitHub: https://github.com/trashtoy/PEACH2/
- API ドキュメント: https://trashtoy.github.io/PEACH2/