最近のSymfonyの依存性注入まとめ

[Symfony Advent Calendar 2023](https://qiita.com/advent-calendar/2023/symfony)の12日目です。

最近のSymfonyの依存性注入の進化がやばいの、知ってましたか？Symfony6.3からなんかすごいです。まとめてみましょう。


## パラメータのオートワイヤリング

今までSymfonyはクラスやインターフェイスの注入はオートワイヤリング（自動注入）の恩恵を受けて、設定せずともオブジェクトが注入されていましたが、パラメータなどの文字列や数値はオートワイヤリングできなかったので、設定する必要がありました。


```yaml:config/services.yaml
parameters:
    app.year: 2024

services:
    App\Service\SomeService:
        arguments:
            $year: '%app.year%'
            $month: '%env(APP_MONTH)%'
```

```php:SomeService.php
class SomeService
{
    public function __construct(
        private readonly int $year,
        private readonly int $month
    ){
    }
}
```

こんな感じ。
Symfony6.3から`#[Autowire]`アトリビュートが追加され、パラメータや環境変数を自動注入することができるようになりました。services.yamlの記述量が減ります。やったね。

```diff:config/services.yaml
parameters:
    app.year: 2024

- services:
-     App\Service\SomeService:
-         arguments:
-             $year: '%app.year%'
-             $month: '%env(APP_MONTH)%'
```

```diff:SomeService.php
class SomeService
{
    public function __construct(
+         #[Autowire(param: 'app.year')] private readonly int $year
+         #[Autowire(env: 'APP_MONTH')] private readonly int $month
-         private readonly int $year,
-         private readonly int $month
    ){
    }
}
```

### 遅延

注入されるクラスのインスタンス化が重かったりする場合や、常に使うわけではないクラスをDIしている場合でも、通常であれば最初にどうしてもインスタンス化する必要があります。でも、これって必要なくてもインスタンス化処理を行わなければいけないのでパフォーマンスに悪影響を及ぼします。そこで、Symfonyは必要になるまでインスタンス化を遅延させる方法をいくつか用意しました。以降はその遅延方法の紹介になります。まず、`#[Autoconfigure]`による遅延です。

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

```php:SampleController.php
class SampleController
{
    public function __construct(
        private readonly SampleService $service
    )
    {
        dump('controller instatiated');
    }

    #[Route('/sample')]
    public function index()
    {
        dump('index executed.');
        $message = $this->service->hello();

        return [$message];
    }
}
```

```php:SampleService.php
class SampleService
{
    public function __construct(
        private readonly ClockInterface $clock
    )
    {
        dump('service instatiated');
    }

    public function hello()
    {
        dump('hello executed.');
        $message = 'Hello. It is ' . $this->clock->now()->format('Y-m-d H:i:s');

        return $message;
    }
}
```

プログラムの内容は超適当ですが、それぞれのメソッドで実行した直後に`dump()`してメッセージを出力するようにしています。
通常であれば、以下のような順番で実行されます。

```
service instatiated.
controller instatiated.
index executed.
hello executed.
```

つまり、

1. サービスのインスタンス化
1. コントローラのインスタンス化
1. コントローラアクションの実行
1. サービスメソッドの実行

となります。ここで、SampleServiceにアトリビュートを足し、必要になったらインスタンス化するようにします。

```diff:SampleService.php
+ #[Autoconfigure(lazy: true)]
class SampleService
{
    public function __construct(
```

アトリビュート1つ追加しただけですが、実行結果がこうなります。

```
controller instatiated.
index executed.
hello executed.
service instatiated.
```

順番は

1. コントローラのインスタンス化
1. コントローラアクションの実行
1. サービスメソッドの実行
1. サービスのインスタンス化

と変わっています。
これは、コントローラのインスタンス化、コントローラアクションの実行直後では**まだサービスメソッドにアクセスしていない**ためです。
では、サービスメソッドの実行よりもサービスのインスタンス化の方が後にメッセージ出力されているのはなぜでしょう？
これは、**メソッド実行直後はサービスのプロパティにアクセスしていない**ためで、プロパティへのアクセスが必要になったタイミングで、インスタンス化されます。なんだこれ。

これが遅延注入です。

### サービスクロージャ

Symfony6.3から**メソッドだけ**注入できるようになりました。これも遅延戦略のひとつで、注入したメソッドを呼ぶまではサービスをインスタンス化しません。`#[AutowireCallable]`アトリビュートを使います。


```php
class SampleController
{
    public function __construct(
        #[AutowireCallable(service: SampleService, method: 'hello']
        private readonly \Closure $helloMethod
    )
    {
    }

    public function index()
    {
        $message = ($this->helloMethod)();

        return ['message' => $message];
    }
}
```

### サービスサブスクライバ

サービスサブスクライバは、ざっくり言うとちっちゃいサービスコンテナを作ります。これも遅延戦略のひとつです。Symfony 6.4, 7で使えるようになりました。

使いたいクラスに`ServiceSubscriberInterface`を実装して使います。例えば、ユーザの種別によって使うサービスを変えたい場合

```php:GoldMemberService.php
class GoldMemberService implements MemberService
{
    public function invoke(User $user)
    {
        // なにか処理
    }
}
```

```php:SilverMemberService.php
class SilverMemberService implements MemberService
{
    public function invoke(User $user)
    {
        // なにか処理
    }
}
```

```php:SampleService.php
class SampleService implements ServiceSubscriberInterface
{
    public static function getSubscribedServices(): array
    {
        return [
            'silver' => SilverMemberService::class,
            'gold' => GoldMemberService::class,
        ];
    }

    public function __construce(private readonly ContainerInterface $container)
    {
    }

    public function addPoint(User $user)
    {
        if ($this->container->has($user->getType())) {
            $service = $this->container->get($user->getType());
            $service->invoke($user);
        }
    }
}
```

`ServiceSubscriberInterface`は`public static function getSubscribedService()`を実装する必要があります。（staticな点に注意）
ここに注入したいクラスの情報を配列で渡します。キーはエイリアス名になります。
この小さなサービスコンテナを使うには`ContainerInterface`をプロパティとして用意する必要があります。
このプロパティに指定したコンテナから`get()`メソッドを使ってサービスを取得します。取得したサービスを実行すれば、必要なタイミングでインスタンス化されます。

## まとめ

というわけで最近のSymfonyの依存性注入についてでした。

パラメータの自動注入はとても楽です。ぜひ。
遅延も`#[Autoconfigure]`は楽です。こちらもぜひ。
サービスクロージャもサービスの一部だけに依存したいときに便利です。用法容量におきをつけて
サービスサブスクライバはまだ未知数です。事例がほしいです