Symfony Best Practice - 実践編 Chapter6,8-

前回

hanahirodev.hatenablog.com

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


Chapter6はテンプレートについてです。 Symfony2で作ったTwigのフィルターをSymfony3で使う形にしてみようと思います。

最終的な形は以下です。

github.com

Twigエクステンションを作る

ベストプラクティスに従って作ってみます。 まずは、エクステンションのクラスを作ります。

  • コンストラクタでメイン言語のtranslatorと併記する言語(sub locale)をインジェクトします。
  • getFilters()/getName() は自前のエクステンションの情報を返せるようにオーバーライドします。
  • multiTrans()でメインの処理を書きます。

https://github.com/hanahiroAze/symfony3_demo/pull/1/files#diff-32cc6ad49a0083779d7ee61592a42aa3

localeをいじるためのEventListenerを書きます。本家のまんまコピーです。

https://github.com/hanahiroAze/symfony3_demo/pull/1/files#diff-4968cc448d20825bee969ece9686fffa

次にサービスに登録します。この時EventListenerも登録してます。

https://github.com/hanahiroAze/symfony3_demo/pull/1/files#diff-1728bfc1c274f341afc1a0275fca694d

ここまでで自作エクステンションの追加が出来ました。

画面から使うために翻訳ファイルに手を入れます。 今回はわかりやすいようにヘッダーの「Symfony Demo」を併記してみます。

翻訳ファイルに「Symfony Demo」の対訳を追加します。

https://github.com/hanahiroAze/symfony3_demo/pull/1/files#diff-1848a926db6eb9fbe0e97b401f58d947

最後にTwigにフィルターを適用します。

https://github.com/hanahiroAze/symfony3_demo/pull/1/files#diff-a2f33b79364623e3cb2737239c5eee62

実行結果

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

無事併記されました!

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