依存関係の自動注入

webmanにおいて依存関係の自動注入はオプション機能であり、この機能はデフォルトで無効になっています。依存関係の自動注入が必要な場合は、php-diの使用をお勧めします。以下はwebmanとphp-diの組み合わせの使い方です。

インストール

composer require psr/container ^1.1.1 php-di/php-di ^6.3 doctrine/annotations ^1.14

設定ファイルconfig/container.phpを変更し、最終的な内容は以下のようになります:

$builder = new \DI\ContainerBuilder();
$builder->addDefinitions(config('dependence', []));
$builder->useAutowiring(true);
$builder->useAnnotations(true);
return $builder->build();

config/container.phpでは最終的にPSR-11準拠のコンテナインスタンスが返されます。もしphp-diを使用したくない場合は、ここで他のPSR-11準拠のコンテナインスタンスを生成して返すことができます。

コンストラクタインジェクション

新しいapp/service/Mailer.phpを作成します(ディレクトリが存在しない場合は自分で作成してください)、内容は以下の通りです:

<?php
namespace app\service;

class Mailer
{
    public function mail($email, $content)
    {
        // メール送信コードは省略
    }
}

app/controller/UserController.phpの内容は以下の通りです:

<?php
namespace app\controller;

use support\Request;
use app\service\Mailer;

class UserController
{
    private $mailer;

    public function __construct(Mailer $mailer)
    {
        $this->mailer = $mailer;
    }

    public function register(Request $request)
    {
        $this->mailer->mail('hello@webman.com', 'Hello and welcome!');
        return response('ok');
    }
}

通常の場合、app\controller\UserControllerをインスタンス化するには以下のコードが必要です:

$mailer = new Mailer;
$user = new UserController($mailer);

php-diを使用すると、開発者はコントローラー内のMailerを手動でインスタンス化する必要がなく、webmanが自動的に処理します。Mailerのインスタンス化の過程で他のクラスへの依存がある場合、webmanは自動的にインスタンス化し、注入します。開発者は初期化作業を一切行う必要がありません。

注意
フレームワークまたはphp-diが作成したインスタンスでなければ、依存関係の自動注入は完了しません。手動でnewされたインスタンスでは依存関係の自動注入ができません。注入が必要な場合は、new文をsupport\Containerインターフェイスに置き換える必要があります、例えば:

use app\service\UserService;
use app\service\LogService;
use support\Container;

// newキーワードで作成されたインスタンスは依存注入できません
$user_service = new UserService;
// newキーワードで作成されたインスタンスは依存注入できません
$log_service = new LogService($path, $name);

// Containerで作成されたインスタンスは依存注入できます
$user_service = Container::get(UserService::class);
// Containerで作成されたインスタンスは依存注入できます
$log_service = Container::make(LogService::class, [$path, $name]);

アノテーションインジェクション

コンストラクタの依存関係の自動注入に加えて、アノテーションによる注入も可能です。先の例を引き続き、app\controller\UserControllerを以下のように変更します:

<?php
namespace app\controller;

use support\Request;
use app\service\Mailer;
use DI\Annotation\Inject;

class UserController
{
    /**
     * @Inject
     * @var Mailer
     */
    private $mailer;

    public function register(Request $request)
    {
        $this->mailer->mail('hello@webman.com', 'Hello and welcome!');
        return response('ok');
    }
}

この例は@Injectアノテーションを使用して注入され、@varアノテーションによってオブジェクトの型が宣言されています。この例はコンストラクタインジェクションと同様の効果を持ちますが、コードがより簡潔です。

注意
webmanはバージョン1.4.6以前ではコントローラーのパラメータ注入をサポートしていません。例えば、以下のコードはwebmanが<=1.4.6の場合にはサポートされません。

<?php
namespace app\controller;

use support\Request;
use app\service\Mailer;

class UserController
{
    // 1.4.6以前のバージョンではコントローラーのパラメータ注入はサポートされていません
    public function register(Request $request, Mailer $mailer)
    {
        $mailer->mail('hello@webman.com', 'Hello and welcome!');
        return response('ok');
    }
}

カスタムコンストラクタインジェクション

時にはコンストラクタに渡すパラメータがクラスのインスタンスではなく、文字列、数字、配列などのデータであることがあります。例えばMailerのコンストラクタにsmtpサーバーのIPアドレスとポートを渡す必要がある場合:

<?php
namespace app\service;

class Mailer
{
    private $smtpHost;

    private $smtpPort;

    public function __construct($smtp_host, $smtp_port)
    {
        $this->smtpHost = $smtp_host;
        $this->smtpPort = $smtp_port;
    }

    public function mail($email, $content)
    {
        // メール送信コードは省略
    }
}

この場合、前述のコンストラクタ自動注入を直接使用することはできません。なぜならphp-di$smtp_host$smtp_portの値を決定できないからです。この場合、カスタム注入を試みることができます。

config/dependence.php(ファイルが存在しない場合は自分で作成してください)に以下のコードを追加します:

return [
    // ... 他の設定は省略

    app\service\Mailer::class =>  new app\service\Mailer('192.168.1.11', 25);
];

こうすることで、依存関係注入が必要とする場合、app\service\Mailerインスタンスは自動的にこの設定で作成されたapp\service\Mailerインスタンスが使用されます。

私たちは、config/dependence.php内でMailerクラスをnewでインスタンス化していることに注意する必要があります。これは本例では問題ありませんが、もしMailerクラスが他のクラスに依存している場合や、Mailerクラス内部でアノテーション注入が使用されている場合、newでの初期化は自動注入をサポートしません。解決策はカスタムインターフェース注入を利用することです。Container::get(クラス名)またはContainer::make(クラス名, [コンストラクタパラメータ])メソッドを使用してクラスを初期化します。

カスタムインターフェース注入

実際のプロジェクトでは、具体的なクラスではなく、インターフェースを使ってプログラムすることが望ましいです。例えば、app\controller\UserControllerにはapp\service\MailerInterfaceをインポートすべきであり、app\service\Mailerではありません。

MailerInterfaceインターフェースを定義します。

<?php
namespace app\service;

interface MailerInterface
{
    public function mail($email, $content);
}

MailerInterfaceインターフェースの実装を定義します。

<?php
namespace app\service;

class Mailer implements MailerInterface
{
    private $smtpHost;

    private $smtpPort;

    public function __construct($smtp_host, $smtp_port)
    {
        $this->smtpHost = $smtp_host;
        $this->smtpPort = $smtp_port;
    }

    public function mail($email, $content)
    {
        // メール送信コードは省略
    }
}

具体的な実装ではなく、MailerInterfaceインターフェースをインポートします。

<?php
namespace app\controller;

use support\Request;
use app\service\MailerInterface;
use DI\Annotation\Inject;

class UserController
{
    /**
     * @Inject
     * @var MailerInterface
     */
    private $mailer;

    public function register(Request $request)
    {
        $this->mailer->mail('hello@webman.com', 'Hello and welcome!');
        return response('ok');
    }
}

config/dependence.phpMailerInterfaceインターフェースの実装として以下のように定義します。

use Psr\Container\ContainerInterface;
return [
    app\service\MailerInterface::class => function(ContainerInterface $container) {
        return $container->make(app\service\Mailer::class, ['smtp_host' => '192.168.1.11', 'smtp_port' => 25]);
    }
];

こうすることで、ビジネスでMailerInterfaceインターフェースを使用する必要がある場合、自動的にMailerの実装が使用されます。

インターフェース重視のプログラミングの利点は、特定のコンポーネントを変更する必要がある際、ビジネスコードを変更する必要がなく、config/dependence.php内の具体的な実装を変更するだけで済むことです。これは単体テストを実施する際にも非常に有用です。

その他のカスタム注入

config/dependence.phpでは、クラスの依存関係だけでなく、文字列、数字、配列などの他の値も定義できます。

例えば、config/dependence.phpで以下のように定義します:

return [
    'smtp_host' => '192.168.1.11',
    'smtp_port' => 25
];

この場合、@Injectを使用してsmtp_hostsmtp_portをクラスのプロパティに注入することができます。

<?php
namespace app\service;

use DI\Annotation\Inject;

class Mailer
{
    /**
     * @Inject("smtp_host")
     */
    private $smtpHost;

    /**
     * @Inject("smtp_port")
     */
    private $smtpPort;

    public function mail($email, $content)
    {
        // メール送信コードは省略
        echo "{$this->smtpHost}:{$this->smtpPort}\n"; // 192.168.1.11:25が出力されます
    }
}

注意:@Inject("key")の中はダブルクオーテーションです

さらなる内容

詳しくはphp-diのマニュアルをご覧ください。