search
LoginSignup
1

posted at

updated at

依存性の注入、"DependencyInjection"

Symfony Component Advent Calendar 2022の5日目の記事です。

最初に

SymfonyはPHPのフレームワークのひとつです。しかし、公式サイトの説明文には

Symfony is a set of PHP Components, a Web Application framework, a Philosophy, and a Community — all working together in harmony.
(SymfonyはPHPコンポーネントのセットで、Webアプリケーションフレームワークで、哲学、そしてコミュニティです。それらがハーモニーを奏でながら動作しています。)

と書かれている通り、PHPコンポーネントのセットで、たくさんのコンポーネントを提供しており、それらを組み合わせてひとつのフレームワークとして動作しています。Symfonyのコンポーネントは、Symfony上だけで動作するのではなく、他のPHPフレームワークやアプリケーションでも動作している強力なものが揃っています。

今回はそれらの中から、役立ちそうなもの・お薦めしたいものを紹介していきたいと思います。

※記事内ではautoloadのインポートは省略します。

依存性の注入、"DependencyInjection"

DependencyInjectionは、依存性の注入の設定・実行を行います。PSR-11互換のサービスコンテナを提供し、依存したオブジェクトを簡単に注入することができます。Symfony以外でも利用できます。

インストール

composer require symfony/dependency-injection

そもそも依存性の注入ってなに?

たとえば、以下のようなクラスがあったとします。

class OrderService
{
    public function invoke(Cart $cart)
    {
        // 注文処理
        ...

        // メールを送信
        $mailer = new Mailer();
        $mailer->send('ユーザのメールアドレス', '注文ありがとう!');
    }
}

OrderServiceでは注文処理をしますが、注文処理完了後にメール送信します。メール送信を行うためにMailerのオブジェクトを生成し、メール送信します。この場合、OrderServiceMailerに依存していますが、このMailerのオブジェクトをメソッド内で生成しているので、依存が内包されてしまっています。
この場合、OrderServiceを実行・テストしようとすると、必ずMailerクラスが必要となります。OrderServiceMailerにべったり依存しています。この依存に一定の距離を与えるのが依存性の注入です。(いわゆる、『関心の分離』)

依存性の注入にはいくつかやり方があり、コンストラクターインジェクションセッターインジェクションが有名ですが、SymfonyのDependencyInjectionはいずれも対応しています。


class OrderService
{
    public function __construct(private readonly Mailer $mailer) // コンストラクターインジェクション
    {
    }

    public function setMailer(Mailer $mailer) // セッターインジェクション
    {
        $this->mailer = $mailer;
    }
}

サービスコンテナによる、依存情報の登録

まず、ContainerBuilderという依存を管理するオブジェクトに、クラスの依存情報を登録します。ここには依存する側と依存される側の両方を登録します。


use Symfony\Component\DependencyInjection\ContainerBuilder;
use Symfony\Component\DependencyInjection\Reference;

$containerBuilder = new ContainerBuilder();

// パラメータの登録
$containerBuilder->setParameter('mailer.from', 'Fromのメールアドレス');

// 依存される側の登録
$containerBuilder
    ->register('mailer', Mailer::class)
    ->addArgument('%mailer.from%') // コンストラクタで渡す引数の値の設定
;

// 依存する側の登録(コンストラクタインジェクション)
$containerBuilder
    ->register('order_service', OrderService::class)
    ->addArgument(new Reference('mailer')) // コンストラクタで渡す依存の設定
;

// 依存する側の登録(セッターインジェクション)
$containerBuilder
    ->register('order_service', OrderService::class)
    ->addMethodCall('setMailer', [new Reference('mailer')]) // セッターで渡す依存の設定

そして、利用する際は、このContainerBuilderからオブジェクトを取得します。

$orderService = $containerBuilder->get('order_service');

はい。めんどくさい。

設定ファイルに依存情報を記述する

DependencyInjectionでは設定ファイルを読み込んで依存情報を設定するやり方も提供されています。
まず、上記と同じ設定をYAMLに記述します。

services.yaml
parameters:
    mailer.from: 'Fromアドレス'

services:
    mailer:
        class: Mailer
        arguments: ['%mailer.from%']
    
    #コンストラクタインジェクションの場合
    order_service: 
        class: OrderService
        arguments: ['@mailer']
    
    # セッターインジェクションの場合
    order_service:
        class: OrderService
        calls:
            - [setMailer, ['@mailer']]

そして、ContainerBuilderに設定ファイルを読み込ませるにはYamlFileLoaderを使います。


use Symfony\Component\Config\FileLocator;
use Symfony\Component\DependencyInjection\ContainerBuilder;
use Symfony\Component\DependencyInjection\Loader\YamlFileLoader;

$containerBuilder = new ContainerBuilder();
$loader = new YamlLoader($containerBuilder, new FileLocator(__DIR__));
$loader->load('services.yaml');

$orderService = $containerBuilder->get('order_service');

オブジェクトの呼び出しは同じくgetメソッドを使って呼び出します。

はい。めんどくさい。

設定おまかせ、"オートワイヤリング"

上記のやり方でももちろん設定ができますが、オートワイヤリングという機能により、これらの設定をせずに依存の設定が完了します。

services.yaml
parameters:
    mailer.from: 'Fromアドレス'
services:
    _defaults:
        autowire: true
        autoconfigure: true
    # ...


services:
    mailer:
        class: Mailer
        arguments: ['%mailer.from%']

    # オートワイヤリング
    order_service:
        class: OrderService
        autowire: true

コンストラクタやセッターを自動解析して、依存しているクラスのオブジェクトも登録します。依存しているクラスがvendor内のクラスであってもOKです。
また、コンストラクタ・セッターにInterfaceが使われている場合は、実装クラスを探し出して登録します。定数のパラメータや実装クラスが複数ある時で自動解決が難しい場合のみ、services.yamlに設定を追記します。

まだ、ちょいめんどくさい。

Symfonyでは!

なお、Symfonyではあらかじめservices.yamlが用意されていますが、以下のようになっています。

config/services.yaml
parameters:

services:
    # default configuration for services in *this* file
    _defaults:
        autowire: true      # Automatically injects dependencies in your services.
        autoconfigure: true # Automatically registers your services as commands, event subscribers, etc.

    # makes classes in src/ available to be used as services
    # this creates a service per class whose id is the fully-qualified class name
    App\:
        resource: '../src/'
        exclude:
            - '../src/DependencyInjection/'
            - '../src/Entity/'
            - '../src/Kernel.php'

    # add more service definitions when explicit configuration is needed
    # please note that last definitions always *replace* previous ones

この設定により、DependencyInjection, Entity, Kernel.php以外の全てのクラスが自動的にContainerBuilderに登録されます。
また、セッターインジェクションの場合は、オートワイヤリングせず、本来はservices.yamlに記述する必要があるのですが、クラスのセッターメソッドに#[Required]アトリビュートをつけると、こちらもオートワイヤリングしてくれるようになります。

SomeClass.php

class OrderService
{
    #[Required] // これをつけると自動で呼んでくれる
    public function setMailer(Mailer $mailer) // セッターインジェクション
    {
        $this->mailer = $mailer;
    }
}

はい。便利。

まとめ

今回はDependencyInjectionの紹介でした。単体で使っても非常に強力なコンポーネントですが、Symfony内では一段と輝きます。このオートワイヤリングでの依存性注入は、非常におすすめなので、ぜひ一度Symfonyを使って体験してもらいたい機能です。

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
What you can do with signing up
1