SymfonyのOptionResolverの紹介

## はじめに
ビルダーパターンなどを使っていると、配列で様々なオプションを渡したくなる時があります。その時に、値の存在確認や、型のチェックなどをスマートにこなせるSymfonyのOptionResolver Componentの紹介です。

## 問題提起

`Car`クラス、`Engine`クラス、`Tire`クラスの三つがあります。`Car`クラスは`Engine`と`Tire`のセッターを持ち、`Engine`と`Tire`はそれぞれ`int`型のパワーとサイズをセッターで受け取ります。

実装は以下の通りです。

```php
<?php

class Car
{
    protected Engine $engine;

    protected Tire $tire;

    public function setEngine(Engine $engine): void
    {
        $this->engine = $engine;
    }

    public function setTire(Tire $tire): void
    {
        $this->tire = $tire;
    }
}

#------------------------------------------------------------

class Engine
{
    protected int $power;

    public function setPower(int $power): void
    {
        $this->power = $power;
    }
}

#------------------------------------------------------------

class Tire
{
    private int $size;

    public function setSize(int $size): void
    {
        $this->size = $size;
    }
}
```

`CarBuilder`でこれらのパーツを組み合わせて`Car`を作成します。

```php:CarBuilder.php
class CarBuilder
{
    public function build(): Car
    {
        $car = new Car;
        $tire = new Tire;
        $engine = new Engine;

        $engine->setPower(50);
        $tire->setSize(20);

        $car->setEngine($engine);
        $car->setTire($tire);
        return $car;
    }
}
```

この状態ではエンジンのパワーとタイヤのサイズが固定されてしまっています。これらの値をオプションで変更できるようにしましょう。

パワーとサイズを変えられるようにしたものが下になります。

```php:CarBuilder.php
class CarBuilder
{
    public function build(array $options): Car
    {
        $car = new Car;
        $tire = new Tire;
        $engine = new Engine;

        $engine->setPower($options['power'] ?? 50);
        $tire->setSize($options['size'] ?? 20);

        $car->setEngine($engine);
        $car->setTire($tire);
        return $car;
    }
}
```

`$option`を引数でもらい、その中にパワーとサイズが入っていればその値を、入っていなければデフォルトの値を使います。
確かにこれで動きます。が、いちいち`$options`に値が入っているか確認するのはイマイチですよね。また、`int`型以外の値が入って来た場合、エラーになってしまいます。`$size`と`$power`をそのまま引数でもらうこともできますが、こちらもオプションの内容が増える度にコードを書き換える必要があり、オプションの数が10や20になった時にとんでもないことになります。

そこで登場するのが`OptionResolver`です。

## OptionResolver版

`OptionResolver`を使うと、先ほどの`CarBuilder`はこのように書けます。`configureOptions()`メソッドを新たに作り、その引数に渡された`$optionsResolver`に許可するオプションを定義します。
以下では、`size`と`power`の二つを定義し、デフォルト値はそれぞれ50と20、型は`int`型としています。

`build()`メソッドに渡ってくる`$option`は**configureOptions()で定義した物を必ず満たすことが保証されます**。なので、値の存在確認や型チェックをする必要がなくなります。

```php

use Symfony\Component\OptionsResolver\OptionsResolver;

class CarBuilder
{
    public function build(array $options): Car
    {
        $car = new Car;
        $tire = new Tire;
        $engine = new Engine;

        $engine->setPower($options['power']);
        $tire->setSize($options['size']);

        $car->setEngine($engine);
        $car->setTire($tire);
        return $car;
    }

    public function configureOptions(OptionsResolver $resolver)
    {
        $resolver->setDefaults([
            'power' => 50,
            'size' => 20,
        ]);

        $resolver->setAllowedTypes('size', 'int');
        $resolver->setAllowedTypes('power', 'int');
    }
}
```

### どうなってるの？

魔法みたいな仕組みに見えますが、もちろん魔法ではありません。`CarBuilder`の具体的な使い方をみてみます。

```php
use Symfony\Component\OptionsResolver\OptionsResolver;

$options = ['size' => 10];

$resolver = new OptionsResolver;
$builder = new CarBuilder;

$builder->configureOptions($resolver);
$car = $builder->build($resolver->resolve($options));
```

まず、`OptionResolver`のインスタンスの`$resolver`を作ります。`CarBuilder`の`configureOptions()`メソッドにこの`$resolver`渡すと、オプションの定義が設定されます。
オプションの定義が設定された`$resolver`に対して、配列を引数にして`resolve()`メソッドを呼ぶと、その配列に対してオプションの定義に沿っているかのバリデーションがかけられます。型が違うなどバリデーション違反の場合は、この時点で例外が投げられます。よって、例外が投げられず、`build()`メソッドまで到達した場合は、必ずオプションの定義を満たしている、という訳です。

オプションの定義には、ここで用いた物意外にも次のようなものがあります。詳しくはこちらをご覧ください。
https://symfony.com/doc/current/components/options_resolver.html

```php
$resolver->setAllowedValues('optionName1', ['VALUE1', 'VALUE2', 'VALUE3']);
$resolver->setDeprecated('optionName2');
$resolver->setRequired('optionName3')
```

## 最後に

今回は、SymfonyのOptionResolverの紹介をしました。こちらは、Symfonyに全く依存しない素のPHPのライブラリとなっているので、Laravelなどのプロジェクトでも利用することが可能です。
Symfonyには便利なコンポーネントが他にもいろいろあるので、これから紹介していこうと思います。