FormalBears マルチバインディングで Resolver のパターンを実装してみる（β版）

## はじめに

2018年の PHPerKaigi で、「[SOLIDの原則ってどんなふうに使うの?」](https://speakerdeck.com/hidenorigoto/solidfalseyuan-ze-tutedonnahuunishi-ufalse)  というタイトルで hidenorigoto さんによるセッションがありました。そのセッションの話の中で `Resolverのパターン` が取り上げられていました（Resolver はGoFデザインパターンではありませんが、よくある設計の問題とその解決の１つなのでここではパターンと呼びます）。この記事では、私がふだん使っている [BEAR\.Sunday](http://bearsunday.github.io/manuals/1.0/ja/) で Resolver のパターンを実装するにはどうすれば良いかについて考えたことを書き、[FormalBears](https://github.com/kumamidori/FormalBears) 拡張で試作したコードを紹介します。

## やりたいことのイメージ

「続・SOLIDの原則ってどんなふうに使うの?」発表スライドの [130ページ目](https://speakerdeck.com/hidenorigoto/sok-solidfalseyuan-ze-tutedonnahuunishi-ufalse-opunkurozudofalseyuan-ze-senpaifalsekodeingufalsetobian?slide=130)

上記の図のような設計を BEAR で実装したいと思います。
Resolver クラスの `add` メソッドによるフォーマッター登録の箇所をどうするのかという点が重要です。DIなどの `コンフィグレーション` で行う必要があります。

※Guice では[マルチバインディング](https://github.com/google/guice/wiki/Multibindings)で実現されます。また、私は書き方を知らないのですが、 Symfony、Laravel で同等の機能があると聞いたことがあります。

## 方法の検討

次の２つの機構が必要です。

1. 1つのインターフェイスに対して複数の実装をバインド登録できること
2. 登録したバインド選択肢のうちどれを使うのかを、実行時に動的に決まるキー値によって都度指定できること

FormalBears 拡張を導入してマルチバインディング機能を使うことによりこれらを用意することにしました。

## 実装

### 1つのインターフェイスに対して複数の実装をバインド登録

モジュール設定：

```php
class TodoListModule extends AbstractConfigAwareModule
{
    /**
     * {@inheritdoc}
     */
    protected function configure()
    {
        // 拡張ポイントを定義
        $this->defineExtensionPoint(TodoFormatterInterface::class, TodoFormatterProvider::class);

        // 複数の実装の登録
        // 「通常のTODO」のフォーマッター
        $this->registerExtension(TodoFormatterInterface::class, NormalTodoFormatter::class, NormalTodo::class);
        // 「GoogleカレンダーのTODO」のフォーマッター
        $this->registerExtension(TodoFormatterInterface::class, CalendarTodoFormatter::class, CalendarTodo::class);
    }
    // ...
```

### 実行時に動的に決まるキー値によって都度指定

利用例：

```php
class Todolist extends ResourceObject
{
    /**
     * @var TodoFormatterProvider
     */
    private $todoFormatterProvider;

    /**
     * @var TodoListQueryRepository
     */
    private $todoRepository;

    /**
     * @Inject
     *
     * @param TodoFormatterProvider $todoFormatterProvider
     * @param TodoListQueryRepository $repo
     */
    public function __construct(TodoFormatterProvider $todoFormatterProvider, TodoListQueryRepository $repo)
    {
        $this->todoFormatterProvider = $todoFormatterProvider;
        $this->todoRepository = $repo;
    }

    public function onGet(): self
    {
        $todoList = $this->todoRepository->findAll();
        $formatterProvider = $this->todoFormatterProvider;

        $this->body['list'] = array_map(function (TodoInterface $todo) use ($formatterProvider) {
            // TODOの種類によってどのフォーマッター実装クラスを使うのかを都度動的に決定
            $formatterProvider->setContext(get_class($todo));
            /** @var TodoFormatterInterface $formatter */
            $formatter = $formatterProvider->get();

            return $formatter->format($todo);
        }, $todoList);

        return $this;
    }
}

```

## Gitリポジトリ

[FormalBearsDemo](https://github.com/kumamidori/FormalBearsDemo/blob/master/src/Resource/App/Todolist/Todolist.php) に動作するコードを置きました。

### セッションスライドの図と異なっているところ

- モジュール設定で代替しているので、Resolver クラスの `add` メソッドとフォーマッター の `supports` メソッドはありません

## メリット

- 可変点がモジュール定義に示されているので明らかです。
- TODOの種類が増えた場合は最小限の安全な変更で対応できます。

※通常は下記のようにエンティティごとにフォーマッターのクラスをどれにするか判定するロジックを条件分岐で用意します。TODOの種類が増えたらこの箇所も併せて修正してやる必要があります。

```php
            /** @var TodoFormatterInterface $formatter */
            $formatter = null;
            switch (get_class($todo)) {
                case NormalTodo::class:
                    $formatter = $this->normalFormatter;
                    break;
                case CalendarTodo::class:
                    $formatter = $this->calendarFormatter;
                    break;
                default :
                    throw new \LogicException();
            }
            return $formatter->format($todo);
```

## デメリット

- FormalBears 拡張が必要です。
- `プロバイダ` を直接インジェクションしており、プロバイダのメソッドをアプリケーション内部で直接利用しています（記事タイトルに "β版" と付けたのはこのためです）。

## 編集後記

Guice のマルチバインディングのマニュアルを読んだ当初は、何が嬉しいのか私には分からなかったのですが、実務でよくある使用例（Resolver パターン）を知ったことにより理解することができました。