【CakePHP 2.x】REST APIの構築+マルチバイト文字のエスケープ防止(uXXXXの形式になるのを防止)
CakePHP 2.xでREST APIを作る必要があったのでメモ。
以下の3点について書いてます。
1. レスポンスをJSONで返す 2. マルチバイト文字のエスケープ防止 3. PHPUnitでテストする際の注意点
SecurityComponentとの絡みはまた次回。
特にPHPUnitで、JSONの結果が取れずにハマりましたorz
APIの検証に限って言えばPHPUnitを使う必要はないですが、あれこれ使うのは嫌という方の参考になれば。
環境
- PHP 5.3.29
- CakePHP 2.6.3
1. レスポンスをJSONで返す
コンポーネントを追加
レスポンスをJSONで返す際は、RequestHandlerComponent
を使うとシンプルにできます。
まずは、使用するコントローラにコンポーネントを追加しましょう。
/* app/Controller/ApiController.php */ <?php App::uses('AppController', 'Controller'); class ApiController extends AppController { // コンポーネントを追加 public $components = array('RequestHandler'); }
余談ですが、App::uses('AppController', 'Controller');
の記述がないと、PHPUnitが上手く動きません^^;
ルーティングを追加
/* app/Config/routes.php */ <?php /* 省略 */ // 拡張子.jsonの呼び出しを許可 // 呼び出した場合、RequestHandlerComponentが自動でJSONにフォーマットしてくれる Router::parseExtensions('json'); // 対象のルートを追加 // `ext`を指定することで、拡張子を強制的に付けてくれる Router::connect('/api/:action', array('controller' => 'api', 'ext' => 'json'));
アクションを追加
/* app/Controller/ApiController.php */ <?php App::uses('AppController', 'Controller'); class ApiController extends AppController { /* 省略 */ // アクションを追加 // setで出力値を設定後、_serializeにキー名を指定することで、自動でJSONにフォーマットされる public function sample() { $this->set('result', true); $this->set('message', 'This is sample.'); $this->set('_serialize', array('result', 'message')); } }
出力結果
/* http://[ドメイン]/api/sample.json */ { "result": 1, "message": "This is sample." }
2. マルチバイト文字のエスケープ防止
このままでは、日本語のようなマルチバイト文字は自動でuXXXXの形式にエスケープされてしまいます。
クライアント側でデコードすれば気にする必要はないのですが、サーバ側でデバッグしようと思うと、地味に面倒です。
例えばこんな感じにエスケープされます。
テスト => \u30C6\u30B9\u30C8
原因と対策
uXXXXの形式にエスケープされてしまうのは、PHP標準のjson_encode関数が原因です。
このjson_encode関数を通すと、自動でuXXXXの形式にエスケープされてしまいます。
RequestHandlerComponentもこの関数を使用して、JSONを生成しているために現象が発生していますので、その辺りのコードをうまく書き換える必要があります。
エスケープ防止用の関数を追加
PHP 5.4以上の場合は、json_encode関数にJSON_UNESCAPED_UNICODE
のオプションを付けることで、エスケープさせないようにできます。
<?php /* 例 */ var_dump(json_encode(array('test' => 'てすと'), JSON_UNESCAPED_UNICODE)); // string(20) "{"test":"てすと"}"
ただしPHP 5.3以下の場合は、このオプションが存在しないため、自作の必要があります。
――すみません、PHP 5.4以上でも上記のメソッド使うより、uXXXXの形式にエスケープされた文字をデコードする方が、カスタマイズが少なくて済みそうです。以降、後者の手順の説明となります。
今回は、こちらの記事のソースコードを使わせていただきたいと思います。
PHPでUnicodeアンエスケープしたJSONを出力する関数 - オープンソースこねこね
php自体の拡張ということで、Config辺りにフォルダを切って作成してみました。
もし正しい作法があれば教えてください^^;
/* app/Config/Extension/encode.php */ <?php /** * Unicodeエスケープされた文字列をUTF-8文字列に戻す。 * 参考:http://d.hatena.ne.jp/iizukaw/20090422 * @param unknown_type $str */ function unicode_encode($str) { return preg_replace_callback("/\\\\u([0-9a-zA-Z]{4})/", "encode_callback", $str); } function encode_callback($matches) { return mb_convert_encoding(pack("H*", $matches[1]), "UTF-8", "UTF-16"); }
さらに上記のコードをbootstrap.php辺りで読み込みます。
こちらもとりあえず自分好みに書いてますが、正しい作法があればそちらに従ってください。
/* app/Config/bootstrap.php */ <?php /* 省略 */ /** * PHP拡張読み込み * Extensionディレクトリ内のファイルを全てrequireする */ $extension_path = APP.DS.'Config'.DS.'Extension'; if ($handle = opendir($extension_path)) { while (false !== ($file = readdir($handle))) { $filename = $extension_path.$file; if (file_exists($filename) && !is_dir($filename)) { require_once($filename); } } closedir($handle); }
これで、エスケープ防止用の関数ができました。
JsonViewを拡張する
RequestHandlerComponentでは、JsonView
というViewを使い、_serializeのデータをJSONにフォーマットしています。
今回はこのJsonViewを継承し、フォーマット部分を書き換えたいと思います。
ViewディレクトリにJsonView拡張用のJsonMultiByteView.php
を生成します。
/* app/View/JsonMultiByteView.php */ <?php App::uses('JsonView', 'View'); // JsonViewを継承 class JsonMultiByteView extends JsonView { // オーバーライド protected function _serialize($serialize) { //既存処理(エスケープされる) $json = parent::_serialize($serialize); // エスケープをデコード return unicode_encode($json); } }
拡張したJsonMultiByteViewとJsonViewを差し替える
beforeRenderで差し替えました。
/* app/Controller/ApiController.php */ <?php App::uses('AppController', 'Controller'); class ApiController extends AppController { /* 省略 */ // JsonViewとJsonMultiByteViewを差し替え public function beforeRender() { if ($this->viewClass === 'Json') { $this->viewClass = 'JsonMultiByte'; } } }
日本語が表示されるか確認
/* app/Controller/ApiController.php */ <?php App::uses('AppController', 'Controller'); class ApiController extends AppController { /* 省略 */ // アクションの出力を日本語に変更 public function sample() { $this->set('result', true); $this->set('message', 'これはサンプルです'); // 英語→日本語に変更 $this->set('_serialize', array('result', 'message')); } }
/* http://[ドメイン]/api/sample.json */ { "result": 1, "message": "これはサンプルです" }
問題なし!
もしデバッグ時のみに限定したい場合は、Configure::read('debug')の値を確認して分岐すると良いかもしれません。
3. PHPUnitでテストする際の注意点
コントローラのテストのみサンプルを記載します。
通常通り、testActionメソッドが使用できますが、returnオプションを付けるという点だけ注意が必要です。
/* app/Test/Case/Controller/ApiControllerTest.php */ <?php App::uses('Controller', 'Controller'); class ApiControllerTest extends ControllerTestCase { // GETの場合 public function testSampleGet() { $json = $this->testAction('/api/sample', array( 'method' => 'GET', 'return' => 'contents' // ※必須 )); $result = json_decord($json, true); $this->assertTrue($result['result']); } // POSTの場合 public function testSamplePost() { $data = array( 'Sample' => array( 'id' => 1, 'password' => 'password', ) ); $json = $this->testAction('/api/sample', array( 'data' => $data, 'method' => 'POST', 'return' => 'contents' // ※必須 )); $result = json_decord($json, true); $this->assertTrue($result['result']); } }
returnオプションはcontents
以外のものを指定し、$this->contents
でJSONを取得する形でも大丈夫です。
しかし、returnオプションが未指定だと、$this->contents
がnullとなるのでご注意ください。