プロバイダ注入

通常の依存性注入では、各タイプは依存するタイプのそれぞれのインスタンスを正確に1つ取得します。 例えばRealBillingServiceCreditCardProcessorTransactionLog を一つずつ取得します。しかし時には依存する型のインスタンスを複数取得したいこともあるでしょう。 このような場合、Ray.Diはプロバイダを束縛します。プロバイダは get() メソッドが呼び出されたときに値を生成します。

/**
 * @template T
 */
interface ProviderInterface
{
    /**
     * @return T
     */
    public function get();
}

プロバイダによって提供される型を#[Set]アトリビュートで指定します。

class RealBillingService implements BillingServiceInterface
{
    /**
     * @param ProviderInterface<TransactionLogInterface>      $processorProvider
     * @param ProviderInterface<CreditCardProcessorInterface> $transactionLogProvider
     */
    public __construct(
        #[Set(TransactionLogInterface::class)] private ProviderInterface $processorProvider,
        #[Set(CreditCardProcessorInterface::class)] private ProviderInterface $transactionLogProvider
    ) {}

    public chargeOrder(PizzaOrder $order, CreditCard $creditCard): Receipt
    {
        $transactionLog = $this->transactionLogProvider->get();
        $processor = $this->processorProvider->get();
        
        /* プロセッサとトランザクションログをここで使用する */
    }
}

静的解析でジェネリクスをサポートをするためにはphpdocの@paramProviderInterface<TransactionLogInterface>ProviderInterface<CreditCardProcessorInterface>などと表記します。get()メソッドで取得して得られるインスタンスの型が指定され、静的解析でチェックされます。

複数インスタンスのためのプロバイダ

同じ型のインスタンスが複数必要な場合の時もプロバイダを使用します。例えば、ピザのチャージに失敗したときに、サマリーエントリと詳細情報を保存するアプリケーションを考えてみましょう。 プロバイダを使えば、必要なときにいつでも新しいエントリを取得することができます。

class LogFileTransactionLog implements TransactionLogInterface
{
    public function __construct(
        #[Set(TransactionLogInterface::class)] private readonly ProviderInterface $logFileProvider
    ) {}
    
    public logChargeResult(ChargeResult $result): void {
        $summaryEntry = $this->logFileProvider->get();
        $summaryEntry->setText("Charge " + (result.wasSuccessful() ? "success" : "failure"));
        $summaryEntry->save();
        
        if (! $result->wasSuccessful()) {
            $detailEntry = $this->logFileProvider->get();
            $detailEntry->setText("Failure result: " + result);
            $detailEntry->save();
        }
    }
}

遅延ロードのためのプロバイダ

もしある型に依存していて、その型を作るのが特に高価な場合、プロバイダを使ってその作業を先延ばしに、つまり遅延生成することができます。 これはその依存が不必要な時がある場合に特に役立ちます。

class LogFileTransactionLog implements TransactionLogInterface
{
    public function __construct(
        (#[Set(Connection::class)] private ProviderInterface $connectionProvider
    ) {}
    
    public function logChargeResult(ChargeResult $result) {
        /* 失敗した時だけをデータベースに書き込み */
        if (! $result->wasSuccessful()) {
            $connection = $connectionProvider->get();
        }
    }

混在するスコープのためのプロバイダ

より狭いスコープを持つオブジェクトを直接注入すると、アプリケーションで意図しない動作が発生することがあります。 以下の例では、現在のユーザーに依存するシングルトンConsoleTransactionLogがあるとします。 もし、ユーザーを ConsoleTransactionLog のコンストラクタに直接注入したとすると、ユーザーはアプリケーションで一度だけ評価されることになってしまいます。 ユーザーはリクエストごとに変わるので、この動作は正しくありません。その代わりに、プロバイダを使用する必要があります。プロバイダはオンデマンドで値を生成するので、安全にスコープを混在させることができるようになります。

class ConsoleTransactionLog implements TransactionLogInterface
{
    public function __construct(
        #[Set(User::class)] private readonly ProviderInterface $userProvider
    ) {}
    
    public function logConnectException(UnreachableException $e): void
    {
        $user = $this->userProvider->get();
        echo "Connection failed for " . $user . ": " . $e->getMessage();
    }
}