このエントリは Symfony Advent Calendar 2014 - Qiita の 3 日目の記事です。

はじめに

こんにちは、@ttskch です。

最近、小規模なアプリは Silex で作ることが多いんですが、ページネーションの ServiceProvider を探してもどうにも良いものがなかったので、自分で作ってしまおうと考えました。

はじめは KnpLabs/knp-components を使って実装をゴリゴリ書いてたんですが、作っているうちに KnpPaginatorBundle を思いっきり再発明している感じになってきたので、いっそのこと KnpPaginatorBundle をラップする形で実装できないかな?と思い、その方針で作ってみました。

この記事では、その過程で実際にやったことについてまとめてみたいと思います。

注意

この記事、読みづらいです笑 リンクしているソースコード(GitHub)と見比べながら頑張って読み進めてみてください m(_ _)m

作ったもの

実際に作った ServiceProvider は こちら で公開しています。

ServiceProvider としての処理は /src/Provider/PaginationServiceProvider.php のみです。 コード量は少ないですが、実際に作るのはなかなか骨が折れました。

やったこと

KnpPaginatorBundle が Symfony で使われる際には、様々な初期化を経た上でコンテナの ['knp_paginator'] に Paginator のオブジェクトが入っている状態になります。

Silex でこれを利用するためには、Symfony 上でバンドルが初期化される手順を(無理やり)トレースする必要があったので、 バンドルのソースを読んで何がどうなってるのかを調べながら、それを Silex 上に再現していく、ということを行いました。

以下、実際に行った調査の流れに従って列挙していきます。

1. 何はともあれメインクラスを見る

まずはバンドルのメインクラスを見ます。

/KnpPaginatorBundle.php

  • PaginatorConfigurationPass
  • PaginatorAwarePass

という 2 つの Compiler Pass クラスが追加されているので、これらの中身を見てみます。

PaginatorConfigurationPass

/DependencyInjection/Compiler/PaginatorConfigurationPass.php

どうやら 'knp_paginator.subscriber' のタグが付いているサービスを Symfony 標準の EventDispatcher に登録しているようです。

/Resources/config/paginator.xml を見ると、'knp_paginator.subscriber' でタグ付けされているのは

  • Knp\Component\Pager\Event\Subscriber\Paginate\PaginationSubscriber
  • Knp\Component\Pager\Event\Subscriber\Sortable\SortableSubscriber
  • Knp\Component\Pager\Event\Subscriber\Filtration\FiltrationSubscriber
  • Knp\Bundle\PaginatorBundle\Subscriber\SlidingPaginationSubscriber

の 4 つなので、これらの EventSubscriber が Silex 標準の EventDispatcher に登録できれば良さそうです。

ServiceProvider の実装では #L124-127 この辺で行っています。

PaginatorAwarePass

こちらは 'knp_paginator.injectable' のタグが付いているサービスがいたら Paginator をインジェクトする、という処理をしているようです。

/Resources/config/paginator.xml には 'knp_paginator.injectable' でタグ付けされているサービスはいないので、ユーザが PaginatorAware なクラスを作りたいときのためのオプションのようです。

今回はひとまずそこまでは対応しないことにして、この部分は無視しました。(PR 歓迎です!)

2. DI エクステンションを見る

次に、Symfony に自動で登録される DI エクステンションの中身を見ます。

/DependencyInjection/KnpPaginatorExtension.php

まずはお決まりで、設定ファイル /Resources/config/paginator.xml をコンテナに登録していますね。

あとは 'knp_paginator' サービスの setDefaultPaginatorOptions メソッドを呼んで、Paginator 本体のデフォルトパラメータを設定しているようです。

設定のデフォルト値そのものは /DependencyInjection/Configuration.php に書かれていますね。

これを元に、ServiceProvider の実装では

#L47-L67 この辺で設定のデフォルト値とユーザの設定値をマージして、 #L83-L90 この辺で Paginator のデフォルトパラメータにセットしています。

3. サービス定義ファイルを見る

最後に、DI エクステンションでコンテナに登録されていた設定ファイルの中身を見ます。

/Resources/config/paginator.xml

とにかく上から順番に見ていきます。

#L8-L10 ここはクラス名をパラメータにしているだけなので無視でよさそうですね。

#L14-L15 ふむふむ。Paginator 本体は初期化時に Symfony 標準の EventDispatcher がインジェクトされるんですね。 ServiceProvider では #L82 こんなふうに Silex 標準の EventDispatcher を渡しておけばよさそうです。

#L18-L28 ここはさっき既に見ましたね。EventSubscriber として登録させるためのタグ付けです。

#L32-L35 先ほど Paginator 本体にデフォルトパラメータをセットするところで出てきたパラメータですが、 SlidingPaginationSubscriber の初期化にも必要なようです。 ServiceProvider の実装では #L110-L115 ここで行っています。

#L38 'knp_paginator.subscriber' タグで EventSubscriber として登録するだけじゃなく、onKernelRequest メソッドだけは 'kernel.event_listener' タグで kernel.request イベントのリスナーとして直接登録する必要があるようです。 ServiceProvider では #L128 こんなふうにしました。

#L41-L44, #L52-L55 Processor とやらを作って、それを TwigExtension のクラスにインジェクトしています。 Processor の初期化には 'templating.helper.router''translator' というサービスが必要ですが、どちらも Symfony のフレームワークで定義されているサービスです。 Symfony のソースを追ってみた結果、ServiceProvider で Processor を初期化するには #L37-L41 こんなふうにすればよさそうだと分かりました。なかなか強引ですね ^^;

#L46-L50 あと残るはここだけですが、このサービスはバンドル内で特に利用されていません。 'templating.helper' タグが付いていますが、これは(Twig を使わずに)PHP でテンプレートを書く場合にテンプレート内で使えるヘルパーとして登録するためのタグのようです。(@okapon_pon さんに教えてもらいました) 公式ドキュメントの この辺 に、Twig の {{ path('_welcome') }} を PHP でやる場合は $view['router']->generate('_welcome')、という記載がありますが、こんな感じで PHP のテンプレートから $view['knp_pagination'] を使えるようにするためのものですね。 というわけで、今回は Twig を使う前提ということにして、この部分は無視しました。(PR 歓迎です!)

完成!

ここまでで、Symfony 上で KnpPaginatorBundle が初期化される手順を一通りトレースできました。お疲れさまでした!(自分に)

それでは、最後に改めて ServiceProvider のソースを見てみてください。 /src/Provider/PaginationServiceProvider.php

そんなに難しいことはしてないですね。

課題点

KnpPaginatorBundle に限らず、Symfony のバンドルを簡単に ServiceProvider としてラップできたら需要大きそうだなーと思いながらトライしたのですが、これを試してみて、バンドルの ServiceProvider 化には以下のような課題点があることが分かりました。

  • バンドルが Symfony 上で初期化されるときの手順を無理やりトレースしいてるので、バンドル側で外部 I/F の後方互換を壊さずに内部の初期化手順が変更されたりした場合にも普通に影響を受けちゃう
  • なので、composer.json に "knplabs/knp-paginator-bundle": "~2.4" とか書けない(”2.4.1” とベタ書きしてます)
  • バンドルの Symfony への依存度によっては、Silex に Symfony のコンポーネントを大量に追加しないと初期化できない
  • ContainerAware なクラスとかをインジェクトする必要があると詰む

感想

  • 今まで AppKernel に追加すれば使える、ぐらいの理解しかしてませんでしたが、バンドルの初期化の流れを追ってみたことでフレームワークの理解が少し深まりました
  • Silex ユーザの多くは Symfony ユーザ なので(?)、使い慣れた knp_paginator が Silex で使えるのは便利に思ってもらえると信じてます
  • とは言え、どっちかというとフレームワーク依存の少ないシンプルなページネーションのライブラリが欲しいという結論に至ったので、何かいいの知ってる人がいたらぜひ教えてください!

追記

フレームワーク依存の少ないシンプルなページネーションのライブラリが欲しいという結論に至ったので、何かいいの知ってる人がいたらぜひ教えてください!

@koriym さんに whiteoctober/Pagerfanta を教えていただきました!使ってみます!