Symfony Best Practice - 実践編 Chapter5-4-

前回

hanahirodev.hatenablog.com

いじった結果は随時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-

前回

hanahirodev.hatenablog.com

いじった結果は随時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というパラメーターでコントローラーに渡されてきた値を、$postslugマッピングさせて値の比較をしている。 ということは、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-

前回

hanahirodev.hatenablog.com

いじった結果は随時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 %}

実行結果

f:id:hiroyuki-hanai:20161116014754p:plain

アノテーションよりResponseオブジェクトが優先されていることが確認できました。

Symfony Best Practice - 実践編 Chapter5-

前回

hanahirodev.hatenablog.com

いじった結果は随時GitHubにあげていきます。


コントローラーのactionに渡されるパラメータをのぞく

まずはデバッグしてパラメータをのぞき見してみる。

// AppBundle\Controller\BlogController
public function indexAction($page)
    {
        dump($page);
        // ~
    }

てやると、

f:id:hiroyuki-hanai:20161112192145p:plain

こんな風に出る。 2ページ目はどうかな。

f:id:hiroyuki-hanai:20161112192330p:plain

え。。パラメーター文字列になるんだ。。ボタンで移動したときは文字列になって、トップページから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だしてみよう。マージされたら嬉しいな。

github.com

Symfony Best Practice - 実践編 Chapter3-

hanahirodev.hatenablog.com

せっかくBest Practiceを翻訳したので、手を動かしながら中を見ていきます。 Chapter2まではプロジェクトの初期設定なので、割愛します。

いじった結果は随時GitHubにあげていきます。


DB接続設定

デフォルトではSQLiteを使うように設定されていますが、実際のお仕事ではMySQLを使うことが多いと思いますので、MySQLに切り替えます。

(積読消化。正月休みにがっつり読む予定。)

実践ハイパフォーマンスMySQL 第3版

実践ハイパフォーマンスMySQL 第3版

SQLアンチパターン

SQLアンチパターン

閑話休題。修正が必要なファイルは以下の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.ymldatabase_nameで指定した名前のデータベースが作成されます。

$ php bin/console doctrine:database:create

さて、画面はどうなるかな。

f:id:hiroyuki-hanai:20161112025849p:plain

あ。。。データがないのか。。。 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に突っ込む。

blog.gufii.net

$ 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ですかね。バンドルの利用者が設定をごにょごにょしなくても済むように、バンドルの中にサービスの設定を閉じ込めておこうという趣旨であることは理解できました。