PHPの各種フレームワークにおけるDIについて

qiita.com

QualiArts Advent Calendar 2019、2日目担当の鈴木光です。
タイトルの通り、今回はPHPの各種フレームワークにおけるDIについて書いていこうと思います。

Dependency Injection(DI)

あるクラスが依存するオブジェクトをコンストラクタ、メソッド、セッターなどの引数から注入することでクラスから依存関係を減らすという手法です。テストや仕様変更に強くなるという特徴があり、10年以上前から用いられてきたデザインパターンとなります。
ちなみにDIの説明のためにこちらの記事をとても参考にさせていただきました。

やはりあなた方のDependency Injectionはまちがっている。 — A Day in Serenity (Reloaded) — PHP, FuelPHP, Linux or something

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コンテナの実装をサポートするライブラリとして以下のようなものになります。

サービスロケータ

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: The Right Way

PHP各種フレームワークにおけるDI

Symfony

みんな大好きSymfonyです。PHPフレームワークなどで最初に行うcomposer installを眺めていたらだいたい出てきます。
Symfonyではサービスコンテナを利用します。これはサービスクラスが登録されたDIコンテナです。
また、Symfony ComponentとしてSymfony以外のアプリケーションではsymfony/dependency-injectionコンポーネントとして利用出来ます。Symfonyアプリケーションでは前述のサービスコンテナという仕組みを利用します。使い方は次のとおりです。

  1. config/services.yamlで設定ファイルを書く(自動構築されたプロジェクトならデフォルトで書かれている)
  2. コンストラクタやメソッドの引数にサービスコンテナに登録されたインタフェースの型で変数を定義

以上です。これだけでインスタンスがオブジェクトへ注入されます。まるで魔法のようですが、割と他のフレームワークやライブラリでも同じようなことが出来るようになっています。実際はリフレクションを使用して型を検出し定義ファイルと突き合わせて注入しています。
自動で依存関係を作成し注入することをAutowireやAutowireingと呼び、型を検出して注入することをPHPでは型宣言のことをタイプヒントと呼ぶことから、これをタイプヒントやタイプヒンティング(typehint or typehinting)と呼びます。リフレクションを使用するとパフォーマンスが低下するため、複雑なDIライブラリでは事前コンパイルをするなどしてパフォーマンスを向上させているようです。ちなみにアノテーションで注入するライブラリもあります。

例:PHP-DI 6: turning into a compiled container for maximum performances - The Dependency Injection Container for humans

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

RailsPHPクローンとして一世を風靡した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

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

Yiiにも独自のDIコンテナが存在します。そしてYiiはDIコンテナ上にサービスロケータを実装されており、アプリケーションはそのコンストラクタやメソッドに対して直接オブジェクトを注入するかコンテナを注入することでそのどちらで依存解決を行うか選ぶことが出来ます。
コンテナへの登録はyii\di\Container::set()で行うこと出来ます。複数のサービスを登録するにはyii\di\Container:: setSingletons()yii\di\Container:: setDefinitions()を利用します。ちなみにアプリケーション構成ファイルを用いて設定することも出来ます。

Phalcon

ハイパフォーマンスなフルスタックフレームワークである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のようにコードを積極的に生成してフレームワークはあまり用いず読めばわかるをモットーに保守性を上げていくことが今後の流れなのかもしれません。