依存性の注入、"DependencyInjection"

[Symfony Component Advent Calendar 2022](https://qiita.com/advent-calendar/2022/symfony-component)の5日目の記事です。

## 最初に
[Symfony](https://symfony.com)は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](https://symfony.com/doc/current/components/dependency_injection.html)は、依存性の注入の設定・実行を行います。PSR-11互換のサービスコンテナを提供し、依存したオブジェクトを簡単に注入することができます。Symfony以外でも利用できます。


### インストール

```shell
composer require symfony/dependency-injection
```

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

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

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

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

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

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

```php

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

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

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

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


```php

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`からオブジェクトを取得します。

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

はい。めんどくさい。

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

DependencyInjectionでは設定ファイルを読み込んで依存情報を設定するやり方も提供されています。
YAMLで記述するので、YAMLコンポーネントを別途インストールします。

```shell
composer require symfony/yaml
```


まず、上記と同じ設定をYAMLに記述します。

```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`を使います。

```php

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を使って体験してもらいたい機能です。