【Drupal】Serviceコンテナに連想配列をDIしたい

## はじめに
タイトルの意味、伝わりますでしょうか？

「機能は似ているが外部サービスや要件ごとに異なるクラスを使い分けたい」というケース、よくありますよね。

例えば、Qiita APIやZenn APIなど、複数の外部サービスに対して、それぞれに適した処理を行う必要がある場合に。
一般的にはSwitch-CaseやMatch式による分岐で、処理クラスを切り替える実装がよく見られます。

ただ、この方法では、対応サービスが増えるたびに分岐処理のメンテナンスコストも増加し、OCP（開放閉鎖の原則）違反や保守性低下の原因となります。

本記事では、「**連想配列を利用したサービスのDI（依存性注入）**」によるクラス切り替えの実装パターンを紹介します。

現場で同様の課題を感じている方にとって、新しいアプローチの参考となれば幸いです。

**DrupalのServiceコンテナにServiceを連想配列で渡しちゃいます！**

## 対象読者
- Drupalエンジニア
- SymfonyのServiceコンテナの概念を知っている人

## シナリオ
### ケース①
冒頭でお話したケースです。
外部APIをコールして、API形式に合わせた解析・加工をする。といった実務でもよくあるパターン。
1. 処理をかけたい要素を配列で受け取る。
2. 要素の特性に応じて、外部APIを処理するクラスを選択する。
3. 選択したクラスにて処理を実行する。

### ケース②
DrupalのSSRレンダリングにおいて、要素に応じて表示形式を変更する。
一覧系の実装でよくあるやつですね。

1. 表示したい要素を配列で受け取る。
2. 要素の特性に応じて、テンプレートを切り替える(テンプレート前段階の加工クラスを切り替える)
3. 選択されたクラス(テンプレート)を実行する。

どちらもイメージしやすいはずです。

今回はケース①をテーマに紹介しようと思います。

## GoF - Strategyパターン
> Strategyパターンでは、同じインターフェイスを実装する**交換可能な**「アルゴリズム」をいくつか定義して、プログラム実行時に適切なアルゴリズムを選択します。

今回は、このデザインパターンに則ったアプローチを行います。
- Qiita 用クラス
- Zenn 用クラス
- Note 用クラス

**いくつかのApiProcessorクラスを定義しておく。
あとはServiceContainerに連想配列としてDIし、実行時に戦略的に切り替える**というイメージ。

## 従来の実装
従来って言っちゃいましたけど、よくある実装。
文字列としてタイプを受け取って、それを目印として目的のクラスを呼び出します。

以下のような感じですね。

```php
class ApiResolver {
  public function __construct(
    private QiitaApiProcessor $qiita,
    private ZennApiProcessor $zenn,
    private NoteApiProcessor $note,
  ) {}

  public function fetch(string $type, string $user_name): array {
    switch ($type) {
      case 'qiita':
        return $this->qiita->handle($user_name);
      case 'zenn':
        return $this->zenn->handle($user_name);
      case 'note':
        return $this->note->handle($user_name);
      default:
        throw new \InvalidArgumentException('Unknown type: ' . $type);
    }
  }
}
```

Drupalの場合は、`services.yml`に定義が必要ですね。
```yaml
services:
  app.external.factory:
    class: Drupal\external\Service\ApiFactory
    arguments:
      [
	    "@app.external.qiita",
	    "@app.external.zenn",
	    "@app.external.note",
      ]
  app.external.qiita:
    class: Drupal\external\Service\QiitaApiProcessor
  app.external.zenn:
    class: Drupal\external\Service\ZennApiProcessor
  app.external.note:
    class: Drupal\external\Service\NoteApiProcessor
```

この実装でも、充分綺麗にFactoryMethodを体現してそう。

ただ。
- OCP (開放関済の原則)の観点でよくない。
  → 新しいAPIを機能追加した際に変更箇所多くないです？
- テスト書きづらくないです？

## 連想配列でDIしちゃいましょ。
### 1. API実行クラスのInterfaceを設計しましょ。
```php
interface ApiProcessorInterface {
  public function handle(string $user_name): array;
}
```

### 2. 各API処理クラスを実装しましょ。
```php
final class QiitaApiProcessor implements ApiProcessorInterface {
  public function handle(string $user_name): array {
    // Qiita API の取得ロジック
  }
}

final class ZennApiProcessor implements ApiProcessorInterface {
  public function handle(string $user_name): array {
    // Zenn API の取得ロジック
  }
}

final class NoteApiProcessor implements ApiProcessorInterface {
  public function handle(string $user_name): array {
    // Note API の取得ロジック
  }
}
```
### 3. 処理実行クラスを実装しましょ。
ここで連想配列で処理クラスを受け取ります。
※ このやり方、めちゃくちゃドキュメント探したけど載ってないんですよね...。
```php
class ApiProcessorResolver {
  public function __construct(private iterable $processors) {}

  public function resolve(string $type): ApiProcessorInterface {
    return $this->processors[$type];
  }
}
```
↑ Switch-Caseを使用しなくていいから、各APIのアルゴリズムを抽象化できていい感じですね！

また、DrupalのServiceはシングルトンですので、連想配列で注入された各Processorはすべてシングルトンとして扱うことができます。
※ 一度だけインスタンス化され、繰り返し使い回されうので軽量。
※ 各Processorがそれぞれの外部APIの仕様に凝集できる。

```yaml
services:
  app.external.resolver:
    class: Drupal\external\Service\ApiResolver
    arguments:
      $processors:
        qiita: "@app.external.qiita"
        zenn: "@app.external.zenn"
        note: "@app.external.note"
  app.external.qiita:
    class: Drupal\external\Service\QiitaApiProcessor
  app.external.zenn:
    class: Drupal\external\Service\ZennApiProcessor
  app.external.note:
    class: Drupal\external\Service\NoteApiProcessor
```


## シングルトンの恩恵もある
Resolverクラスに、あらかじめ連想配列として各API処理クラスをセットしておくことで、
Processorは一度だけインスタンス化され、再利用されます。

この点、**今回の手法**と**Drupal(Symfony)のServiceコンテナ**との相性のよさが出てるんじゃないのかなと。

## まとめ
本記事では、外部APIごとに異なる処理クラスを戦略的に切り替える設計として、StrategyパターンとDrupal/SymfonyのServiceコンテナを組み合わせた手法をご紹介しました。

従来のswitch-caseや条件分岐による実装では、新規API追加時に既存コードの修正が必要となり、OCP（開放閉鎖の原則）に反しがちです。

今回のパターンだと。
- 新規API追加時もResolver側の修正が不要
- 実装クラスの単一責任・凝集度の向上
- テストや拡張性の高い設計
- サービスのシングルトン化によるパフォーマンス・管理コストの最適化

といった多くのメリットを享受できます。

特に、DrupalやSymfonyのサービス定義の柔軟性と組み合わせることで、現場でも実践しやすい実装パターンかな、と感じています。

複数の実装を戦略的に切り替えたいケースでは、ぜひ本記事のアプローチを参考にしてみてください。

## おまけ
呼び出しクラスのイメージはこんな感じですかね。
（エラーハンドリング省きました。すみません。）
```php
class ApiHandler {
  public function __construct(private ApiProcessorResolver $resolver) {}

  public function handle(array $data): array {
    $results = [];
    foreach ($data as $type => $user_name) {
      $processor = $this->resolver->getProcessor(task_type: $type);

      foreach ($this->entityStorage->loadMultiple($ids) as $id => $entity) {
	        $results[$type][$id] = $processor->process($user_name);
      }
    }
    return $results;
  }
}
```