はじめに
ビルダーパターンなどを使っていると、配列で様々なオプションを渡したくなる時があります。その時に、値の存在確認や、型のチェックなどをスマートにこなせるSymfonyのOptionResolver Componentの紹介です。
問題提起
Car
クラス、Engine
クラス、Tire
クラスの三つがあります。Car
クラスはEngine
とTire
のセッターを持ち、Engine
とTire
はそれぞれint
型のパワーとサイズをセッターで受け取ります。
実装は以下の通りです。
<?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
を作成します。
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;
}
}
この状態ではエンジンのパワーとタイヤのサイズが固定されてしまっています。これらの値をオプションで変更できるようにしましょう。
パワーとサイズを変えられるようにしたものが下になります。
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()で定義した物を必ず満たすことが保証されます。なので、値の存在確認や型チェックをする必要がなくなります。
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
の具体的な使い方をみてみます。
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
$resolver->setAllowedValues('optionName1', ['VALUE1', 'VALUE2', 'VALUE3']);
$resolver->setDeprecated('optionName2');
$resolver->setRequired('optionName3')
最後に
今回は、SymfonyのOptionResolverの紹介をしました。こちらは、Symfonyに全く依存しない素のPHPのライブラリとなっているので、Laravelなどのプロジェクトでも利用することが可能です。
Symfonyには便利なコンポーネントが他にもいろいろあるので、これから紹介していこうと思います。