PHPの各種フレームワークにおけるDIについて
QualiArts Advent Calendar 2019、2日目担当の鈴木光です。
タイトルの通り、今回はPHPの各種フレームワークにおけるDIについて書いていこうと思います。
Dependency Injection(DI)
あるクラスが依存するオブジェクトをコンストラクタ、メソッド、セッターなどの引数から注入することでクラスから依存関係を減らすという手法です。テストや仕様変更に強くなるという特徴があり、10年以上前から用いられてきたデザインパターンとなります。
ちなみにDIの説明のためにこちらの記事をとても参考にさせていただきました。
DIとは
DI自体はそんなに難しいことではありません。以下のようにコンストラクタなど対して依存するオブジェクトを注入すればそれはDIパターンを適用している(DIしている)と言えます。
<?php class UserController { private $userService; public function __construct(UserServiceInterface $userService) { $this->userService = $userService; } }
DIコンテナ
DIコンテナとはDIする際に利用するインスタンスが登録されたオブジェクトです。このDIコンテナの仕様はPSR(PHP Standards Recommendations = PHP標準勧告)のPSR-11で定義されており、Psr\Container\ContainerInterface
を継承することで簡単に実装することが可能です。
DIコンテナを使わない場合はnew UserController(new UserService())
とするところを、new UserController($container[UserService::class])
のようになります。このDIコンテナにインスタンス生成のロジックを集約しようという手法です。
ここで注意していただきたいのはDIするのにDIコンテナは必須ではないというところです。なので次の話に繋がります。
ちなみにDIコンテナの実装をサポートするライブラリとして以下のようなものになります。
- PSR-11: Container interface - PHP-FIG
- Pimple - A simple PHP Dependency Injection Container
- GitHub - Level-2/Dice: Dice - a lightweight Dependency Injection Container for PHP
- GitHub - AcclimateContainer/acclimate-container: Adapters for PHP framework containers to an interoperable interface
- GitHub - ray-di/Ray.Di: Guice style dependency injection framework for PHP
- PHP-DI - The Dependency Injection Container for humans
サービスロケータ
DIコンテナを使うとこういうことも出来てしまいます。
<?php class UserController { private $userService; public function __construct(ContainerInterface $container) { $this->userService = $container[UserService::class]; } }
これがサービスロケータと呼ばれます。アプリケーションは$container
を通じて任意のインスタンスを取り出すことが出来ます。これの問題点はUserControllerがUserServiceではなくDIコンテナに依存していることです。従ってサービスロケータでは依存するオブジェクトが格段に増えています。
しかしUserControllerでは本当にDIコンテナに登録されている全オブジェクトが必要なのでしょうか。この場合だとUserServiceだけで大丈夫のはずです。しかし注入する側はそんなこと知りません。なので中身を知らない場合は完全に実装されたDIコンテナを注入する必要があります。これはテストの実行速度や保守性に関わってきます。
このことからサービスロケータはアンチパターンとされています。
PHP各種フレームワークにおけるDI
Symfony
みんな大好きSymfonyです。PHPのフレームワークなどで最初に行うcomposer install
を眺めていたらだいたい出てきます。
Symfonyではサービスコンテナを利用します。これはサービスクラスが登録されたDIコンテナです。
また、Symfony ComponentとしてSymfony以外のアプリケーションではsymfony/dependency-injection
コンポーネントとして利用出来ます。Symfonyアプリケーションでは前述のサービスコンテナという仕組みを利用します。使い方は次のとおりです。
config/services.yaml
で設定ファイルを書く(自動構築されたプロジェクトならデフォルトで書かれている)- コンストラクタやメソッドの引数にサービスコンテナに登録されたインタフェースの型で変数を定義
以上です。これだけでインスタンスがオブジェクトへ注入されます。まるで魔法のようですが、割と他のフレームワークやライブラリでも同じようなことが出来るようになっています。実際はリフレクションを使用して型を検出し定義ファイルと突き合わせて注入しています。
自動で依存関係を作成し注入することをAutowireやAutowireingと呼び、型を検出して注入することをPHPでは型宣言のことをタイプヒントと呼ぶことから、これをタイプヒントやタイプヒンティング(typehint or typehinting)と呼びます。リフレクションを使用するとパフォーマンスが低下するため、複雑なDIライブラリでは事前コンパイルをするなどしてパフォーマンスを向上させているようです。ちなみにアノテーションで注入するライブラリもあります。
Zend Framework
- GitHub - zendframework/zendframework: Official Zend Framework repository
- Manual - Documentation - Zend Framework
- Manual - Documentation - Zend Framework
- Manual - Documentation - Zend Framework
こちらもみんな大好きZend。composer install
で見ることが多いですね。
ZendではPSR-11準拠のDIコンテナを利用します。また、InstanceManager :: addTypePreference()
というインスタンスメソッドを呼び出して設定することでAutowiringも可能となります。さらにZend\Di Definition
を定義してリフレクションにかかる時間を短縮することも出来ます。
Laravel
最近流行りのLaravelです。Laravelでもサービスコンテナを利用します。こちらは注入する方式としてサービスプロバイダを利用します。使い方は次のとおりです
app/Providers/
配下に新規にServiceProviderを実装したクラスを作る or デフォルトで用意されているAppServiceProviderに実装を記述- 新規でProviderを作成した場合、
config/app.php
のprovidersマップに対象クラスをclassキーワード(::class
)でクラス名解決を行い注入するサービスを設定
wiringする仕組みとしてサービスプロバイダを使っているのですね。これによりAutowiringが実現します。また、サービスプロバイダの中でエイリアスとオブジェクトを結合(登録)するわけですが、それにもいろいろな結合方法があります。
\Illuminate\Contracts\Foundation\Application::bind()
の第2引数のクロージャでnewして結合\Illuminate\Contracts\Foundation\Application::singleton()
を使ってシングルトンとして結合\Illuminate\Contracts\Foundation\Application::instance()
でnewしたインスタンスを結合\Illuminate\Contracts\Foundation\Application::when()->needs()->give()
でプリミティブな値を結合
ちなみにLaravelでは依存オブジェクトを注入しなくても、オブジェクトのメソッドをFacadeという仕組みでクラスメソッドとして呼び出すことができます(なんかDIの意味が薄れてきますが)。
CakePHP
RailsのPHPクローンとして一世を風靡したCakePHPさんです。
CakePHPではサービスロケータとして依存解決を行います。このDIコンテナ流れの中、頑なにCakePHPはサービスロケータで生きています。ちなみにまだかなり先ですが4.1のロードマップにPSR-11準拠のDIコンテナの実験的サポートとありますので気長に待っていればそのうち来るかもしれません。
4.1 Roadmap · cakephp/cakephp Wiki · GitHub
例えばCakePHPではORMのテーブルクラスを取得するために次のように実装します。
TableRegistry::getTableLocator()->get('Articles');
use Cake\ORM\Locator\LocatorAwareTrait;
したクラスで$this->getTableLocator()->get('Articles');
CodeIgniter
Phalconが出るまで最速の地位にあったCodeIgniterではフレームワーク側でDIの仕組みは用意されていません。したがって自身で実装する必要があります。
Slim
- GitHub - slimphp/Slim: Slim is a PHP micro framework that helps you quickly write simple yet powerful web applications and APIs.
- Dependency Container - Slim Framework
PHPのマイクロフレームワークです。SlimにはビルドインでPimpleベースのDIコンテナが実装されており、依存解決はこちらを利用します。
SlimのDIコンテナは非常にシンプルな実装で、結合も単純明快です。わかりやすいのでここにも記します。
<?php $container = new \Slim\Container; $app = new \Slim\App($container); $container = $app->getContainer(); $container['myService'] = function ($container) { $myService = new MyService(); return $myService; }; $app->get('/foo', function ($req, $res, $args) { $myService = $this->get('myService'); return $res; });
また、\Slim\App::get()
などのクロージャ中で$this->myService;
のように暗黙的にサービスを取得できます。また、サービスが登録されているかどうかは$this->has('myService')
として確認できます。
Yii 2
- GitHub - yiisoft/yii2: Yii 2: The Fast, Secure and Professional PHP Framework
- 鍵となる概念: 依存注入コンテナ | Yii 2.0 決定版ガイド | Yii PHP Framework
- 鍵となる概念: 構成情報 | Yii 2.0 決定版ガイド | Yii PHP Framework
- 鍵となる概念: サービス・ロケータ | Yii 2.0 決定版ガイド | Yii PHP Framework
Yiiにも独自のDIコンテナが存在します。そしてYiiはDIコンテナ上にサービスロケータを実装されており、アプリケーションはそのコンストラクタやメソッドに対して直接オブジェクトを注入するかコンテナを注入することでそのどちらで依存解決を行うか選ぶことが出来ます。
コンテナへの登録はyii\di\Container::set()
で行うこと出来ます。複数のサービスを登録するにはyii\di\Container:: setSingletons()
やyii\di\Container:: setDefinitions()
を利用します。ちなみにアプリケーション構成ファイルを用いて設定することも出来ます。
Phalcon
- GitHub - phalcon/cphalcon: High performance, full-stack PHP framework delivered as a C extension.
- 依存性の注入とサービス・ロケーション — Phalcon 3.0.2 ドキュメント (Japanese / 日本語)
ハイパフォーマンスなフルスタックフレームワークであるPhalconではPhalcon\Di
というDIコンテナが実装されています。このインスタンスに対して$di->set()
で登録を行い、$di->getService("logger")
でコンテナから取り出します。シングルトンとして登録したい場合はPhalcon\Di::set()
の第3引数にtrueを設定することでシングルトンになります。
まとめ
いろいろなフレームワークにおけるDIを見てきました。最初はただ依存するオブジェクトを外出ししてnewするタイミングで注入するだけだったのが、フレームワークやライブラリ上にアプリケーションが構築されることを前提とすることでリフレクションやアノテーションを用いて注入されることが多いです。
最近、弊社でもGo言語の機運が高まってきており、Goではそのような注入ではなく自動生成ツールを用いて注入するコードを生成してそれを使ったりします(https://github.com/google/wire)。
自動的に注入されるのは確かに便利ですがこれは宣言的ではありません。最近のPHP7系やPython3.5系のように明示的な型宣言(typehint)、Goのようにコードを積極的に生成してフレームワークはあまり用いず読めばわかるをモットーに保守性を上げていくことが今後の流れなのかもしれません。