Symfony 2.8とPHP-DIのAuto Wiring
この記事は Symfony Advent Calendar 2015 の16日目です。昨日は@Issei_Mさんの「最近のSymfony Standard Editionのディレクトリ構成」でした。
内製フレームワークからの移行先を探してSymfonyを調査中です。その過程でDIのAuto Wiringの使い方を確認したので、ここにまとめます。
概要
Symfony 2.8ではサービスのAuto Wiringが可能になりました (New in Symfony 2.8: Service Auto Wiring (Symfony Blog))。これまでは、services.ymlにサービスの名前とクラス名の定義、依存先のサービスも(たとえばコンストラクタの引数として)指定が必要でした。Auto Wiring有効時は、コンストラクタのタイプヒンティングを利用して、これらの定義が簡略化できるようになっています。
また、Symfonyとは別のフレームワークになりますが、PHP向けのDIコンテナのひとつにPHP-DI があり、これもAuto Wiringをサポートしています。また、アノテーションを利用してプロパティに直接注入できる機能も備え、注入だけのためにコンストラクタを作成する必要がありません。Symfonyとの連携機能も用意されており、状況によっては、Symfonyのサービスコンテナよりさらに記述を簡略化できそうです。
この記事では、以下2点の基本的な使い方をご紹介します。
試した環境は次のとおりです。
Symfony 2.8 の AutoWiring
たとえば、サービスAにサービスBを注入したいとします。従来は、A, Bともにservices.ymlに定義が必要でした。
Auto Wiringを有効にすると、サービスAのargumentsの指定は不要になり、サービスBの定義全体も不要となります。ServiceAクラスのコンストラクタのタイプヒンティングで指定されたクラスが自動的に注入されます。
実際に簡単なコントローラを作成して試してみます。
プロジェクト作成
組み込みサーバの起動確認のため、ブラウザから http://localhost:8000/app_dev.php/ にアクセスしてWelcome画面の表示を確認します。
コントローラとサービスの作成
例として、/sample/hello/{名前} にアクセスすると「こんにちは{名前}さん」というメッセージが表示されるようにします。コントローラを次のように作成します。
- 名前を受け取って「こんにちは{名前}さん」というメッセージを構築するGreetingServiceを作成。
- コントローラはGreetingServiceを呼び出してメッセージを作成。
- コントローラは、サービスとして作成 (How to Define Controllers as Services (The Symfony CookBook)にしたがって作成)
先の例に当てはめると、service_Aがコントローラ、service_BがGreetingServiceにあたります。GreetingService、コントローラ、services.ymlの定義は次のようにしました。
これで http://localhost:8000/sample/hello/JohnSmith にアクセスすると「こんにちは、JohnSmith さん。」と表示されます。
Auto Wiringを利用する
次に、SampleControllerに対してAuto Wiringを有効にしてみます。有効にするには、autowiring: true を指定します。GreetingServiceについてのサービス定義は不要になるため削除してしまいます。services.ymlは次のとおりとなります。
SampleControllerがGreetingServiceを必要としていることはコンストラクタのタイプヒンティングに明記されており、services.ymlには、argumentの指定と、GreetingServiceのサービスとしての定義は不要になりました。SampleControllerクラスとGreetingServiceクラスには変更ありません。
なお、Auto Wiringが可能なのはタイプヒンティングに具象クラスが指定されている場合に限られるようです。サービスのinterfaceをclass (実装)と分けて定義している場合は、コンストラクタのタイプヒンティングにはinterface名を指定するかと思いますが、この場合には以下のとおりエラーになります。実装クラスを特定できないためでしょう。
Unable to autowire argument of type "AppBundle\GreetingServiceInterface" for the service "app.controller.hello".
依存のネストがある場合
さらに、GreetingServiceが別のサービスを利用するように拡張してみます。氏名を大文字化するCapitalizationServiceを追加し、GreetingServiceから利用するようにします。この場合でも、新しいサービスCapitalizationServiceをservices.ymlに記述する必要はなく、クラスの追加・修正だけで済みます。
SymfonyでのPHP-DIの利用
ここまで作成した処理を、PHP-DIを利用したものに書き換えてみます。
PHP-DIのSymfonyでの利用方法は、PHP-DIのサイトに説明があります。
PHP-DIの導入
まず、PHP-DIをcomposerでインストールします。
なお、私の環境では、インストール中のキャッシュクリアの処理でファイル削除失敗のエラーになってしまったため、app/cache/dev, app/cache/dev_oldを削除してcomposerを実行しなおしました。(php app/console cache:clear でも同様にエラーになります・・・なんとかならないものでしょうか・・・)
インストール後、app/AppKernel.phpに以下の処理を追加する必要があります。(バンドルとして追加されるわけではないようです。)
PHP-DIは次のような仕組みでSymfonyのサービスコンテナと共存するようです。
- Symfonyのサービスコンテナを、php-di/symfony2-bridgeが提供するもの(DI\Bridge\Symfony\SymfonyContainerBridge)にすり替え。
- php-di/symfony2-bridgeのサービスコンテナは、Symfonyのサービスコンテナクラスを継承しており、Symfonyのサービスコンテナと同様に動作。
- ただし、取得しようとしたサービスが見つからない場合に、PHP-DIに委譲。
したがって、PHP-DI導入後も先のプログラムソースのままで動作しますが、Symfonyのサービスコンテナの処理が働いている結果であり、PHP-DIには委譲されていないはずです。
PHP-DI経由でサービスを利用
PHP-DIは、明示的に定義のない場合は、オブジェクトの名前(サービス名)を、そのままクラス名として解釈してオブジェクト(サービス)を取得しようとします。したがって、コントローラクラスの@Routeのservice=にクラス名を指定できるようになります。
このとき、services.ymlにはSampleControllerの定義はもはや必要ありません。
SampleControllerの依存先(GreetingService)についてもSymfony 2.8と同様にタイプヒンティングからクラスが特定されコンストラクタ経由で注入されますので、やはりサービスの定義は不要です。
Symfonyが管理するサービスを利用
PHP-DIが管理するサービスに対して、Symfonyが管理するサービスを注入できます。
How to Define Controllers as Services (The Symfony CookBook によれば、サービスとして作成したコントローラからテンプレートを使うには、"templating" サービス(EngineInterfaceインタフェースの実装クラス)の注入が必要です。これをPHP-DIで管理されたコントローラに注入する方法のひとつは、インタフェースとオブジェクトの対応関係を明示的にPHP-DIに指定してやることです。この対応関係は、PHP-DI導入時にAppKernelに追加したPHP-DIの初期化処理に追加します。
\DI\get('templating') は、「コンテナから名前 templating で取得したオブジェクト」を表します。ここでのオブジェクト取得もまた、Symfony管理→PHP-DI管理の順に探索されるものと思われます。
この指定を追加した上で、SampleControllerのコンストラクタにEngineInterfaceを追加すれば、"templating"サービスが注入されます。
プロパティへの注入
PHP-DIでは@Injectを指定したプロパティにサービスを注入できます。この場合は、コンストラクタのタイプヒンティングの代わりに、プロパティの@varでクラスを指定します。
ただし、アノテーションはデフォルトで無効となっており、有効にするにはPHP-DI初期化時に指定する必要があります。
なお、Symfonyのアノテーションと異なり、@Inject の名前空間は考慮されていないようで、Inject の use を記述する必要がありません。
@Injectを使ってSymfonyが管理するサービスを利用
この@Injectを使ってSymfony管理のサービスを注入することもできます。@Injectの引数にサービス名を指定します。
この場合は、EngineInterfaceと"templating"サービスの対応関係を定義する必要はありません。
まとめ
- Symfony 2.8のAuto Wiringを使うと、サービスに別のサービスを注入する場合に後者のサービスの定義は省略でき、記述が簡素化されます。
- また、PHP-DIをSymfonyと連携させた場合は、
明日は tarokamikaze さんです。よろしくお願いします。