Symfony Advent Calendar 2014 20日目の記事です。

Symfony2プロジェクト内でAngularJSアプリケーション

フロントエンドもバックエンドもSymfonyプロジェクトで済ませてしまいたい、そんな人向けの記事です。

SymfonyはAsseticがある関係で生成したファイルをバージョン管理しない流儀だと勝手に思っているので、gruntやgulpで生成とかは一切なしです。

  • バックエンド
  • フロントエンド

の順に書かれています。

バックエンド

SymfonyでREST APIを作る時の話です。

使用するバンドル一覧

リクエスト Content-Type: application/json を受け取る

AngularJSの$httpサービスはデフォルトでJSONのままデータ送信を行うため、そのままではSymfony側でデータが受け取れません。 REQUEST_METHODPUT, DELETE, PATCH のいずれかであればRequest Payloadからデータを読込んでくれるのですが、そうでなければ手動で行うしかありません。そうであってもJSONのままです。

<?php
// 手動でRequest Payloadから読込んでJSONをデコード
json_decode(file_get_contents('php://input'), true);

そこでFOSRestBundleにデコードさせる設定をしておけば、デコード済みJSONが受け取れるようになります。

fos_rest:
    body_listener:
        decoders:
            json: fos_rest.decoder.json

簡単ですね!

フォームのCSRF問題

めでたくJSONリクエストを受け取れるようになりましたが、AngularJSではsymfony/formで作成したフォームをレンダリングする機会がないのでトークンをブラウザに渡す事が出来ません。 ブラウザに渡ってないので、当然サーバーに送信される事も無くcsrfでフォームがエラーになってしまいます。 それでもフォームは便利なので使いたいですよね。

通常ではcsrf_protectionを無効にするかトークンを手動で渡すかの2択ですが、DunglasAngularCsrfBundleを使うとトークンの受け渡し等々を自動的にやってくれます。

これだけだと黒魔術感がありますが、AngularJSには元々トークンの受け渡しを行う仕組みが備わっていて DunglasAngularCsrfBundle はその仕組みを組み込んでくれているという訳です。

設定は至ってシンプルで、どのURLでトークンをブラウザに送信するかどのURLでcsrfのトークンチェックを行うか ぐらいなので適当にドキュメントを見れば済むと思います。

フロントエンド

Symfonyプロジェクト内でAngularJSアプリケーションを作る時の話です。

使用するライブラリ一覧

バンドル

AngularJS

AngularJS(テスト)

Bowerコンポーネントのバージョン管理

bower.jsonをバンドル内に持っておけば、そのバンドルの公開ディレクトリにコンポーネントをインストールしてくれる代物です。 Bowerを使っているなら特に困る事も何も無いので説明する事も無いですね。

サーバーとのやり取り

生のAngularJSだとほぼ$http$resourceを使うかと思いますが、restangular が結構使えるのでおすすめです。

URLの生成にレスポンスのIDを使っているので、子のオブジェクト毎にIDのキーが違ったりすると結構厄介なので注意が必要ですが、レスポンスオブジェクトからリクエストが行えるのがかなり便利だと思ってます。

// GET: /usrs.json
Restangular.all('users').getList().then(function (users) {
    // GET: /users/1/posts.json
    users[0].getList('posts');
});

ただし、URLの生成がわりとルーズなので好みが分かれるのが少し問題です。 その上、SymfonyでREST APIを作る際にもRestangularに合わせたURL構成(とはいっても一般的な構成)にしないと真価を発揮しません。 カッチリ派の方はFOSJsRoutingBundleを使って、、という方が好みかもしれません。

AngularJSへSymfonyの情報を渡す

テストの事もあるので、configモジュール等をappモジュールから参照するようにすると割と便利かなと思ってます。

テストの時は固定のconfigモジュールを作り、実アプリの時はtwigテンプレート内でconfigモジュールを作ります。

<!-- 実アプリ用 -->
<script type="text/javascript">
(function (angular) {
  angular
    .module('config', [])
    .const('BASE_URL', '{{ app.request.baseUrl }}');
})(angular);
</script>
// テスト用
angular
  .module('config', [])
  .const('BASE_URL', 'http://localhost')
;

これでSymfonyの情報(BaseUrl)がAngularJSで参照できるようになります。

// 例えば前述のRestangularにBaseUrlを設定する
angular
  .module('app', ['config'])
  .config(['RestangularProvider', 'BASE_URL', function (RestangularProvider, BASE_URL) {
    RestangularProvider.setBaseUrl(BASE_URL + '/api');
  }])
;

AngularJSテンプレートのURL解決

JmikolaJsAssetsHelperBundleを使えば、twig内のasset()と同じURLがjsからも取得できるようになるのでそれを使います。

app
  .module('app', ['ngRoute'])
  .config(['$routeProvider', function ($routeProvider) {
    $routeProvider.when('/', {
      templateUrl: AssetsHelper.getUrl('/bundles/acmedemobundle/js/views/index.html')
    });
  }])
;

この方法だと、テンプレートの取得にHTTPリクエストが使われるのでそれが嫌な場合はHshnAngularBundleを使うと、生成された$templateCacheからテンプレートが読込まれるため高速です。

AngularJSテンプレートを別ファイルにした際のユニットテスト

何も準備していないとAngularJSがテンプレートを取得する為にHTTPリクエストを飛ばしてしまいます。 ユニットテスト用にローカルサーバーを立てていれば話は別ですが、通常はテストが通りません。 なのでテスト実行前に$templateCache上にテンプレートをセットしておきます。

karma-ng-html2js-preprocessor$templateCacheのキーが合うように設定さえすればキャッシュを生成してくれるのでおすすめです。 オプションが複数あり、指定方法はいくつかあるのですが基本的に下記の通りにすればOKです。

  • JmikolaJsAssetsHelperBundleを使っている場合 => AssetsHelper.getUrl()の戻り値とキーが合うように

  • HshnAngularBundleを使っている場合 => templateUrlとキーが合うように

最後に

役に立つか分かりませんが、何かしらの参考になれば幸いです!