PHPでProtocol Buffersを使う

Googleが2008年にオープンソースとして公開したProtocol BuffersをPHPで利用するための手順を記します。

## Protocol Buffersとは？

Protocol Buffersはprotoファイルという共通のIDLから言語を問わずにやりとりが行えるシリアライザが簡単に書ける素敵なツールです。protoファイルは下記のようなデータを定義する独自の言語となり、このファイルからprotocコンパイラを利用して対象の言語のシリアライザを作成します。

````
message Person {
  required int32 id = 1;
  required string name = 2;
  optional string email = 3;
}
````

## Protocol Buffersのライブラリを選ぶ上でのポイント

Protocol Buffersはprotoファイルから言語別のシリアライザを生成するため、protoc対応（もしくはprotoファイルのパーサー/ジェネレーター）の対応が出来ていることが一つの大きなポイントです。

初めての場合は下記の4点について確認をしておけば間違いが有りません。

* protoc、もしくはprotoファイルのパーサーが付属していること
* 型の最大値のチェックや32bit、64bit環境の考慮が出来ていること、string型の場合utf8のチェックが入っていること
* テストケースが容易に確認出来ること
* 自分のユースケースにあっていること

特にprotocやprotoファイルのパーサー/ジェネレーターがないProtocol Buffers実装は何かを作る上では全く実用的ではありませんので注意してください。

## PHPにおけるProtocolBuffersの注意点

PHPでは32bit環境ではintのMAXが`2147483647`、64bit環境では`9223372036854775807`となり、それ以上の値に関してはfloatとなります。

PHPの内部的にはint型はlongで保持され、float型はdoubleで保持されます。PHP実装自体の制限として`PHP_INT_MAX`を超える値や、浮動小数点を扱う場合は多少ずれることがあるので注意しておきましょう。

## PHPのProtocolBuffers実装について

#### pure php

* drslump/Protobuf-PHP (https://github.com/drslump/Protobuf-PHP)
  * PHP5.3対応でPHPで実装されたproto parser/generatorを同梱しているので楽
* bramp/protoc-gen-php (https://github.com/bramp/protoc-gen-php)
  * PHP5.3対応で純正protocのpluginを使ったgeneratorを同梱しています
* pb4php (https://code.google.com/p/pb4php/)
* undr/phpbuf (https://github.com/undr/phpbuf)

pure php版の実装はどれも拡張に比べれば読みやすく、また自分で改造が行い易いので楽なのですがProtocolBuffersの仕様上ほぼ全てのbyte列に対してordやchr関数を呼ぶ事になる為速度的なデメリットがあります。

drslumpの実装はメソッドのコール回数が多くなるため他の実装に比べて多少遅いのが難点ですが、メンテナンス性や拡張性に優れ実運用で扱いやすい実装です。

#### extension

* chobie/php-protocolbuffers (https://github.com/chobie/php-protocolbuffers)

PECL ProtocolBuffersになりました

## Install

※前提条件としてphpの開発環境の構築は済んでいるものとします。

#### protocol buffersのインストール

まずはcode.google.comのProtocol Buffersのページにアクセスし、protocol buffersをインストールしましょう

https://code.google.com/p/protobuf/

![protocolbuffers](https://qiita-image-store.s3.amazonaws.com/0/24324/d593da1d-6533-697e-60eb-bb41cd5083c9.png)

````
mkdir ~/src && cd ~/src
curl -o protobuf-2.5.0.tar.bz2 https://protobuf.googlecode.com/files/protobuf-2.5.0.tar.bz2
tar xjf protobuf-2.5.0.tar.gz2
cd protobuf-2.5.0
# prefixなどは適宜置き換えてください
./configure
make
sudo make install
````

#### php-protocolbuffersのインストール

次にphp-protocolbuffersのインストールを行います。試す場合は必ずプロダクション環境ではなく、開発環境で問題が起きない状態にしてから試してください。

````
cd ~/src
git clone https://github.com/chobie/php-protocolbuffers.git
cd php-protocolbuffers
phpize
./configure
make
# 必ずmake testを行ってエラーがでないか確認して下さい。エラーがでた場合は修正するので教えてくださいね
make test
sudo make install

# install後はphp.iniにprotocolbuffers.soを追加します
sudo vi /etc/php5/conf.d/protocolbuffers.ini
extension=protocolbuffers.so

# protoc-gen-phpを作成します
cd contrib
make
sudo ln -s `pwd`/protoc-gen-php /usr/local/bin/protoc-gen-php
````

### protoc-gen-phpのインストール

メッセージを作成する場合はprotoc-gen-phpが必要となります。

```
#$HOME/.composer/composer.jsonに以下エントリを追加する
{
    "require": {
        "protocolbuffers/protoc-gen-php": "dev-master"
    }
}
composer global install
# .bashrcなどに追加する
export PATH=$HOME/.composer/vendor/bin:$PATH
```

#### サンプルの作成 

それでは手始めにお約束のチュートリアルを作成してみましょう。

````
cd ~/src
mkdir addressbook-pb
cd addressbook-pb
cat > addressbook.proto <<EOF
package Tutorial;

message Person {
  required string name = 1;
  required int32 id = 2;
  optional string email = 3;

  enum PhoneType {
    MOBILE = 0;
    HOME = 1;
    WORK = 2;
  }

  message PhoneNumber {
    required string number = 1;
    optional PhoneType type = 2 [default = HOME];
  }

  repeated PhoneNumber phone = 4;
}
EOF
````

続いてprotoファイルからPHPクラスを作成します。エラーが何も出なければ作成は完了しています。
実行後のファイルは次のURLからも取得出来ます。 https://gist.github.com/chobie/8aa643bb728cad2b0460

````
mkdir src
protoc --php_out=src addressbook.proto -I . -I /usr/local/include -I /usr/include -I $HOME/src/php-protocolbuffers/contrib/
````

あと一息です。サンプルのPHPスクリプトを作成しましょう。

````
cat > example.php <<EOF
<?php
require __DIR__ . DIRECTORY_SEPARATOR . "autoload.php";

\$phone = new Tutorial\Person\PhoneNumber();
\$phone->setNumber("0000-0000-0000");
\$phone->setType(Tutorial\Person\PhoneType::MOBILE);

\$person = new Tutorial\Person();
\$person->setId(100);
\$person->setName("chobie");
\$person->setEmail("example@example.com");
\$person->appendPhone(\$phone);

echo \$person->serializeToString(\$person);
EOF

php example.php | hexdump -C
````

##### 実行結果

![Screen Shot 2014-01-31 at 3.26.48.png](https://qiita-image-store.s3.amazonaws.com/0/24324/b87b9cf1-0a86-0dff-e6c8-950dbbca3dbc.png)

それでは、var_dumpするとどうなるのでしょうか？

![Screen Shot 2014-01-31 at 3.27.07.png](https://qiita-image-store.s3.amazonaws.com/0/24324/8f98bf66-2dad-6164-f345-544185bf7f51.png)

POPOなobjectで可搬性も高いですね。

### serialize後のサイズについて

シリアライズフォーマットと言えばシリアライズ後のサイズも気になりますね。一般的に使われているPHPのシリアライザで先程のTutorial_Personクラスをシリアライズするとどのようになるのでしょうか？

````
Protocol Buffers =>  51bytes
MessagePack => 151bytes
PECL mongo's bson => 154bytes
serialize =>  266bytes
````

このような結果になりました。Binary形式のシリアライザのほうがText形式のシリアライザに比べておおよそ倍以上サイズで有利となるのが分かりますね。

デシリアライズの速度は対象の文字列(bytes)のサイズによって大きくなるので単純にファイルサイズが小さい事はそれだけ実行にかかるコストも小さくなります。


といってもPHPの場合高速な拡張側でのデシリアライズよりもObjectの生成や`__wake`, `__sleep`メソッドの実行等に時間を取られるのでそこまで神経質にならなくて問題ありません。

※MessagePackはとても優秀なserializer/deserializerですがPHPでクラスをserializeする場合はクラス名などが含まれてしまうため現状の実装では複雑なクラスのシリアライズに向いていません。

## 総括

ProtocolBuffersを使うことでmessageから簡単にPHPのクラスを作ることができます。
Serialize/Deserializeだけでなく基盤として使うのも非常に便利ですのでぜひお試しください。