CakePHPでRESTful APIを作って、Backbone.jsのデータの永続化をサーバサイドで行う


CakePHPでRESTful APIを作って、Backbone.jsのデータの永続化をサーバサイドで行う方法をメモしておきます。RESTful APIは、FuelPHP等、その他のPHPフレームワーク、Ruby on Rails等でも簡単に作成することができますので、各々好きなものを選択してください。ここでは、CakePHPを使った例を示します。
ここで制作したアプリケーションは、以下よりダウンロードできます。
Backbone ToDos with CakePHP RESTful API – GitHub

開発環境

開発環境は以下の通りです。それぞれ執筆時点での最新バージョンを用いました。

ソフトウェアバージョン
Apache2.2.25
PHP5.4.19
MySQL5.5.33
CakePHP2.3.9
Backbone.js1.0.0

基本方針

目的を単純化するため、Backbone.jsのアプリケーションは、自作せずに、パッケージされているToDoアプリを利用します。サンプルアプリケーションでは、localStorageを利用してデータの永続化を行っていますが、これを変更して、サーバサイドでデータの永続化を行うようにします。
また、CakePHPで、Backboneアプリケーションを出力する等も、ここでは行いません。CakePHPは、あくまでRESTful APIを構築することのみに利用します。

backbonejs-todos-with-cakephp-restful-api-01

ベースアプリのソースコード

jashkenas/backbone/examples – GitHub

ベースアプリのデモ

Backbone.js ToDos

Backbone.jsの経験が浅い方は、以下に勉強のロードマップを提案しておりますので、参考にしてみてください。
Backbone.js入門 – 初学者の為のロードマップ

構成

以下の通りにファイルを配置しました。

Backbone ToDosへは、 http://localhost/backbone/でアクセスできるものとし、RESTful APIへは、 http://localhost/cakephp/todosでアクセスできるものとします。

CakePHPでRESTful APIを作る

CakePHPでは、RESTful APIを簡単に作ることができます。その方法は、Cookbookにも示されています。
REST — CakePHP Cookbook v2.x documentation

それでは早速見ていきましょう。

スキーマ

スキーマは以下の通りです。テーブル1つのシンプルな構成です。

backbonejs-todos-with-cakephp-restful-api-02

ルーター

app/Config/routes.phpに以下を追記します。routesをオーバーライドする他の設定より前に呼び出す必要があるため、先頭に記述するのが良いでしょう。

Router::mapResources('todos');を設定することで、以下の表のように、HTTPリクエストメソッドとリクエストURLの組み合わせによってコントローラアクションが呼ばれるようになります。

HTTP formatURL.format対応するコントローラアクション
GET/todos.formatTodosController::index()
GET/todos/123.formatTodosController::view(123)
POST/todos.formatTodosController::add()
PUT/todos/123.formatTodosController::edit(123)
DELETE/todos/123.formatTodosController::delete(123)
POST/todos/123.formatTodosController::edit(123)

REST #簡単なセットアップ — CakePHP Cookbook v2.x documentationより

例えば、同じ /todos.jsonがリクエストされたとしても、 GETであった場合は、Todoコントローラのindexアクションが呼ばれ、 POSTであった場合は、Todoコントローラのaddアクションが呼ばれるといった具合です。
これらのルーティングは変更可能ですが、ここでは割愛します。詳しくは、REST — CakePHP Cookbook v2.x documentationを参照してください。

また、 Router::parseExtensions('json');を設定することで、JSONフォーマットのリクエスト&レスポンスを適切に処理してくれるようになります。

モデル

cake bake model等としたもので構いません。バリデーション等は、必要に応じて行ってください。

コントローラ

コントローラアクションを実装します。以下では、Cookbookのサンプルをベースに、シンプルな処理しか行っておりませんので、必要に応じて追加してください。

9行目。
RequestHandlerComponentを有効にしています。このコンポーネントには、たくさんの機能があるのですが、ここでは、クライアントからJSONデータをコントローラへPOSTした場合に、自動的に解析され $this->request->dataの配列に割り当ててくれる機能等を有効利用しています。
他にも、 Router::parseExtensions()と組み合わせて使われることで、 /todos.jsonといったリクエストを受けたとき、また、 /todosといった拡張子なしのリクエストに対しても、ヘッダが application/jsonであった場合は、JSONをレスポンスしてくれるようになります。
詳しくは以下のCookbookを参照してください。
リクエストハンドリング — CakePHP Cookbook v2.x documentation

それぞれのコントローラアクションの $this->set()で、 _serializeキーを指定して、データをシリアライズしています。これにより、JSONでレスポンスが行われます。

以下のようなデータが登録されていた場合、

indexアクションでは、以下のようなJSONを返します。

ビュー

RequestHandlerComponentによって、ビューファイルの定義を省略できるため、ビューファイルは作っていません。レスポンスデータを加工したい場合は、ビューファイルを使うようにしましょう。以下のCookbookに詳しいです。
REST — CakePHP Cookbook v2.x documentation
JSONとXMLビュー — CakePHP Cookbook v2.x documentation

ビューファイルを使ったレスポンスデータ加工の例

以下に、ビューファイルを使ったレスポンスデータ加工の例を示します。
まずは、コントローラです。 $this->set()_serializeキーの指定を削除します。

ビューファイルで、 created, modifiedフィールドを削除しています。(Cookbookに倣って例を示しています。セレクトのときにフィールドを指定すればいいやんって言うツッコミはなしで。)

ステップアップ

これまでに、とりあえず動かすためのRESTful APIの構築について見てきました。現実的に使えるものとする為に、いくつか処理を加えましょう。

出力されるJSONの型を最適化する

既に気付かれた方もいるかもしれませんが、上記に例を示した通り、CakePHPのRESTful APIから返されたJSONは、MySQL上の型がintegerのものもstringになっています。(id, order)
これは、あまりイケてない状態です。例えば、以下は今回使っているToDoアプリ内で行われている処理ですが、型が違うことによって結果が変わってしまいます。

JavaScriptアプリケーション側で正すのも一つの方法ですが、API側で正しておいた方が、より良いでしょうから、修正してみましょう。
初めはCakePHPでJSON出力するときに起こっていることかと思いましたが、これはPHPでMySQLからセレクトしたときに起きる現象だということがわかりました。調べている経過をtweetしていたら、@cakephperさんから助け舟があり、その後、調べていくうちに、対処法がわかりました。 PDO::ATTR_EMULATE_PREPARESfalseに設定しましょう。
※ tinyint(1)がbooleanになるのは、CakePHPの仕様です。

CakePHPでのやり方は、いくつかありますが、例えば、コントローラアクションで以下のように設定します。

これらに関しては、@cakephperさんが、ブログにまとめてくれているので、詳しくはそちらを参照してください。
CakePHPのfind()で取得したデータが全てstring型になるのを、DBのカラムの型に合わせてint型で値を取得する方法(mysql) – cakephperの日記(CakePHP, MongoDB)

また、関連ツイートを@zuborawkaさんがまとめてくれていますので、あわせて参照してもらえると経緯が分かりやすいと思います。
CakePHP+MySQLで、数値型カラムの結果を文字列ではなく数値で取得する – Togetter

セキュリティ対策

以下の記事にある通り、JSONを返すAPIを構築する際、いくつか対応しておくべき事柄があります。
HPのイタい入門書を読んでAjaxのXSSについて検討した(3)~JSON等の想定外読み出しによる攻撃~ – ockeghem(徳丸浩)の日記

記事から対策の要点を引用します。

  • 対策の基本はX-Requested-Withヘッダのチェック
  • Content-Typeは正しく application/json; charset=UTF-8 と指定
  • IEに顔を立てて、X-Content-Type-Options: nosniff を指定(あるいは不要か?)
  • JSON生成ライブラリで設定できる最大限のエスケープ

これらを受けて、コントローラに beforeFilterを追加し、以下の対策を施しました。

  • Ajaxのみ受け付けるようにする
  • X-Content-Type-Options: nosniffを付与する

あわせて、ビューファイルを追加し、JSON生成時のエスケープ対象を追加しました。

探してみましたが、コンポーネント等で指定する方法は分かりませんでした。

なお、 Content-Typeapplication/json; charset=UTF-8にする設定は、 RequestHandlerComponentが行ってくれます。
CakePHP側の設定は以上です。

Backbone.js ToDoアプリを修正する

データの永続化をサーバサイドで行うための修正をします。

コレクション

まずはコレクションです。

7行目。
localStorageは、利用しないので、コメントアウトするか削除しておきましょう。

9行目。
urlは、fetchメソッドでの通信先となるURLを設定します。CakePHPで作ったAPIを指定しましょう。拡張子を含める必要はありません。

11行目。
parseを定義し、受け取ったJSONを加工します。
BackboneのCollection#fetchでは、 [{"id":1,"title":"foo"},{"id":2,"title":"bar"}]という形でデータを受け取ることを期待しています。CakePHPのRESTful APIが返すJSONの例として見た通り、そのようにはなっていませんので、ここでフォーマットしています。

一般的なWebAPIでも、 {"meta": {"xxx": "xxx"},"results": [{...},{...}...]}と言った形式で返されることは、よくあることなので、覚えておきましょう。

モデル

続いて、モデルを修正します。

12行目の urlRootは、モデルの urlメソッドの内部で利用されます。更新・削除等で、idが設定されているモデルのURLは、”[urlRoot]/id”となります。

14行目。
コレクションの時と同じく、 parseを定義し、受け取ったJSONを加工します。
Modelでは、 {"id":1,"title":"foo"}という形でデータを受け取ることを期待しています。

まとめと注意点

以上で完了です。
ToDoの登録や更新・削除等を行って、データが更新されていることを確認してみてください。

注意点として、サンプルのToDoアプリでは、サーバサイドでの実行結果に関わらず、DOMを更新しているということが挙げられます。アプリケーションの性質によっては、レスポンスを受けてDOMを更新するように変更する等の対処が必要でしょう。
今回は、APIの更新系のレスポンスについては、簡易的な実装しか行っておりませんので、作り込みが必要です。その際、エラーであった場合は、ステータスコードも指定しておいた方が丁寧でしょう。

CakePHPとBackboneを密に絡ませた開発については、以下の記事が参考になると思います。
最近の案件でのJavaScript開発周辺について、書いてみる | be-hase.com

以上です。