Symfony Best Practice - 実践編 Chapter5-4-
前回
いじった結果は随時GitHubにあげていきます。
フックを使う
ドキュメントに沿って確認してみよう。 まずはdemoアプリですでに登録されているイベントリスナーを確認する。
ロケールの切り替えを行うイベントリスナーが登録されている。 イベント一覧によると、Requestイベントをフックしている。
// AppBundle\EventListener\RedirectToPreferredLocaleListener public function onKernelRequest(GetResponseEvent $event)
サービスとして登録されているか確認。
# /app/config/service.yml app.redirect_to_preferred_locale_listener: class: AppBundle\EventListener\RedirectToPreferredLocaleListener arguments: ['@router', '%app_locales%', '%locale%'] tags: - { name: kernel.event_listener, event: kernel.request, method: onKernelRequest }
イベントのフックにはListenerとSubscriberを使う方法があるみたい。 ドキュメントによると、
- Subscriberはイベントに関する知識をクラスではなく、サービス定義に集約できるので、再利用性に優れている。(Symfony内部ではこっちを使っている)
- ListenerはバンドルごとにON/OFFの切り替え設定ができるので、柔軟性に優れている。
ユーザーの権限チェックとかアプリ全体で使うようなチェックはSubscriberで処理するのが良さそう。
Symfony Best Practice - 実践編 Chapter5-3-
前回
いじった結果は随時GitHubにあげていきます。
ParamConverterを使う
BlogControllerでParamConverterを使う場合と、使わない場合の書き方の比較をしてみる。
変更前は、ParamConverterを使っている。
// BlogController.php /** * @Route("/comment/{postSlug}/new", name="comment_new") * @Method("POST") * @Security("is_granted('IS_AUTHENTICATED_FULLY')") * @ParamConverter("post", options={"mapping": {"postSlug": "slug"}}) */ public function commentNewAction(Request $request, Post $post)
そもそもParamConverterは何をしてくれるかドキュメントで確認。 ParamConverterにはDoctrine ConverterとDateTime Converterの2種類があって、今回はDoctrine Converterを使ってるということがわかった。
Doctrine ConverterによりpostSlug
というパラメーターでコントローラーに渡されてきた値を、$post
のslug
とマッピングさせて値の比較をしている。
ということは、slug
というパラメーターでコントローラーに渡してあげればParam Converte使わなくても動きそうなので、以下のように書き換えてみる。
// BlogController.php /** * @Route("/comment/{slug}/new", name="comment_new") * @Method("POST") * @Security("is_granted('IS_AUTHENTICATED_FULLY')") */ public function commentNewAction(Request $request, Post $post)
パラメーターはTwig側で設定されてるので、Twigの方も書き換える。
// /app/Resources/views/blog/_comment_form.html.twig {{ form_start(form, { method: 'POST', action: path('comment_new', { 'slug': post.slug }) }) }}
これでParamConverterを使わなくても動くようになった。 ただし、ドキュメントによると以下の条件の下で実現してるそうな。
- ルーティングに
{id}
が設定されている場合は、find ()
メソッドで主キーをフェッチ。 - ワイルドカードに指定されているプロパティで、エンティティにあるプロパティを
findOneBy()
でフェッチ。
上記挙動はoptionsを指定すると変えられる。
複雑なことをしたい場合は、Entityクラスに対応するRepositoryクラスを作って、@Entity
アノテーションでRepositoryクラスのメソッドを指定して検索させることができる。
感想
「EntityとRepositoryの違いって何?」と思っていた時期があったけど、この挙動を知ると役割の違いが明確化できていいな。 Best Practiceでは、Twigをデザイナーさんに触ってもらうことを考えているので、Entityでの名称を画面に持ち込みたくない(意識させたくない)場合とかに使ってねという意図を感じなくもない。
Symfony Best Practice - 実践編 Chapter5-2-
前回
いじった結果は随時GitHubにあげていきます。
あえて@Template
アノテーションを使ってみる
ベストプラクティスでは「使ってはいけない」とされている@Template
アノテーションをあえて使ってみる。
なるほど。すっきりはするけど、Responseオブジェクトを返しているというよりは、arrayを返しているようにしか見えないから、「なんでこれで画面が表示されるんだろう?」となるのは理解出来る。
しかも、アノテーション(SensioFrameworkExtraBundle)のドキュメントによると、コントローラーがResponseオブジェクトを返す場合は、@Template
の設定内容が無視されるらしい。
本当かどうか、やってみた。
// BlogController /** * @Route("/", defaults={"page": "1"}, name="blog_index") * @Route("/page/{page}", requirements={"page": "[1-9]\d*"}, name="blog_index_paginated") * @Method("GET") * @Cache(smaxage="10") * @Template("blog/index.html.twig") */ public function indexAction($page) { dump($page); $posts = $this->getDoctrine()->getRepository(Post::class)->findLatest($page); return $this->render('blog/test.html.twig', ['posts' => $posts]); // return ['posts' => $posts]; }
レスポンスにするテストページは以下のようにした。
{% extends 'base.html.twig' %} {% block body_id 'blog_index' %} {% block main %} <h1>This is test page</h1> {% endblock %} {% block sidebar %} {{ parent() }} {{ show_source_code(_self) }} {% endblock %}
実行結果
アノテーションよりResponseオブジェクトが優先されていることが確認できました。
Symfony Best Practice - 実践編 Chapter5-
前回
いじった結果は随時GitHubにあげていきます。
コントローラーのactionに渡されるパラメータをのぞく
まずはデバッグしてパラメータをのぞき見してみる。
// AppBundle\Controller\BlogController public function indexAction($page) { dump($page); // ~ }
てやると、
こんな風に出る。 2ページ目はどうかな。
え。。パラメーター文字列になるんだ。。ボタンで移動したときは文字列になって、トップページから1ページ目に遷移したときだけ数値になってるぽい。
ルーティングを見ると理解できる。デフォルトのルーティングはpage
が数値で設定されていて、/page
がルーティングに入ってきたときは文字列にしている。
// AppBundle\Controller\BlogController /** * @Route("/", defaults={"page": 1}, name="blog_index") * @Route("/page/{page}", requirements={"page": "[1-9]\d*"}, name="blog_index_paginated") * ~ */ public function indexAction($page)
気持ち悪いから、デフォルトも文字列にしておこうっと。
/** * @Route("/", defaults={"page": "1"}, name="blog_index") ~ */ public function indexAction($page)
せっかくなので、本家にPRだしてみよう。マージされたら嬉しいな。
Symfony Best Practice - 実践編 Chapter3-
せっかくBest Practiceを翻訳したので、手を動かしながら中を見ていきます。 Chapter2まではプロジェクトの初期設定なので、割愛します。
いじった結果は随時GitHubにあげていきます。
DB接続設定
デフォルトではSQLiteを使うように設定されていますが、実際のお仕事ではMySQLを使うことが多いと思いますので、MySQLに切り替えます。
(積読消化。正月休みにがっつり読む予定。)
- 作者: Baron Schwartz,Peter Zaitsev,Vadim Tkachenko,菊池研自,株式会社クイープ
- 出版社/メーカー: オライリージャパン
- 発売日: 2013/11/25
- メディア: 大型本
- この商品を含むブログ (7件) を見る
- 作者: Bill Karwin,和田卓人,和田省二,児島修
- 出版社/メーカー: オライリージャパン
- 発売日: 2013/01/26
- メディア: 大型本
- 購入: 9人 クリック: 698回
- この商品を含むブログ (45件) を見る
閑話休題。修正が必要なファイルは以下の2箇所。
# app/config/parameters.yml # 削除 database_url: 'sqlite:///%kernel.root_dir%/data/blog.sqlite' # 追加 database_host: 127.0.0.1 database_port: null database_name: symfony database_user: root database_password: null
# app/config/config.yml # Doctrine Configuration (used to access databases and manipulate their information) doctrine: dbal: # if you don't want to use SQLite, comment the two following lines # driver: "pdo_sqlite" # path: "%kernel.root_dir%/data/blog.sqlite" # uncomment the following lines to use a database different than SQLite driver: pdo_mysql host: "%database_host%" port: "%database_port%" dbname: "%database_name%" user: "%database_user%" password: "%database_password%" charset: UTF8
デフォルトでMySQLの設定がされてるので、良心的。
ここで以下のコマンドを流すと、app/config/parameters.yml
のdatabase_name
で指定した名前のデータベースが作成されます。
$ php bin/console doctrine:database:create
さて、画面はどうなるかな。
あ。。。データがないのか。。。
app/config/config.yml
でコメントアウトした"%kernel.root_dir%/data/blog.sqlite"
を読んでるっぽい。
SQLiteの中身をダンプして確認。
$ sqlite3 ./app/data/blog.sqlite SQLite version 3.8.5 2014-08-15 22:37:57 Enter ".help" for usage hints. sqlite> .tables symfony_demo_comment symfony_demo_post symfony_demo_user sqlite> .output ./dump.txt sqlite> .dump sqlite> .output stdout sqlite>.exit
以下を参照して、SQLiteのダンプをMySQLに突っ込む。
$ mysql -uroot symfony < target.sql ERROR 1064 (42000) at line 2: You have an error in your SQL syntax; check the manual that corresponds to your MySQL server version for the right syntax to use near 'CLOB NOT NULL, authorEmail VARCHAR(255) NOT NULL, publishedAt DATETIME NOT NULL,' at line 1
MySqlにCLOB型がないみたいなので、target.sql
のCLOBをTEXTに変更
$ mysql -uroot symfony < target.sql ERROR 1064 (42000) at line 2: You have an error in your SQL syntax; check the manual that corresponds to your MySQL server version for the right syntax to use near 'NOT NULL, authorEmail VARCHAR(255) NOT NULL, publishedAt DATETIME NOT NULL, PRIM' at line 1
NOT DEFERRABLE INITIALLY IMMEDIATE
の指定がMySQLだと効かないみたいなので、この部分を削除して、SET FOREIGN_KEY_CHECKS=0;
を追加。
詳細はapp/data/target.sql
参照
これでMySQLでデモアプリが表示された。
アプリに関する設定
app/config/
配下のconfig.yml
について。「環境ごとに設定を書き換えられるように」Symfonyが以下を用意してくれている。
このコンセプトに反するから、「変更されることが予想されいてない設定は定数として定義しましょう。」ということ。
config_prod.yml
は本番環境の設定config_dev.yml
は開発環境の設定config_test.yml
はファンクショナルテスト用の環境設定。ブラウザからはアクセスできない。*1
環境設定ファイルの読み込みは、AppKernel.registerContainerConfiguration()
で実行されている。
「semantic dependency injection」とはなんだろう
まずは辞書的な「semantic」の意味を調べる。
セマンティックとは、一般的には「意味」や「意味論」に関することを指す語である。IT用語としては、コンピュータに文書や情報の持つ意味を正確に解釈させ、文書の関連付けや情報収集などの処理を自動的に行わせる技術について用られる語である。
う〜ん。わからん。Best Practiceでは*Extension
を使うのを「semantic dependency injection」と言っているので、「How to Load Service Configuration inside a Bundle」をチェック。
Symfonyでは、様々なサービスを使っていることに気づくでしょう。これらのサービス群は、あなたのアプリのapp/config/
ディレクトリに登録されていることでしょう。しかし他のプロジェクトから、そのバンドルを使いたくなったときには、バンドル自体にサービス設定が内包されていた方が良いでしょう。
ふむ。バンドル単体を公開したい人に向けたBestPracticeですかね。バンドルの利用者が設定をごにょごにょしなくても済むように、バンドルの中にサービスの設定を閉じ込めておこうという趣旨であることは理解できました。