数日前、不安定なテストを修正していたところ、ファクトリー内に一意で有効な値が必要であることがわかりました。Laravel は FakerPHP をラップしており、通常はfake()
ヘルパーを介してアクセスします。FakerPHP にはvalid()
やunique()
などの修飾子が付属していますが、一度に使用できるのは 1 つだけなので、 fake()->unique()->valid()
は実行できません。これはまさに私が必要としていたものです。
これを見て、独自の修飾子を作成したい場合はどうなるかと考えました。たとえば、 uniqueAndValid()
やその他の修飾子です。フレームワークをどのように拡張できるでしょうか。
私の考えをまとめてみます。
過剰に設計されたソリューションに飛び込む前に、よりシンプルなオプションがあるかどうかを確認し、何を扱っているかを理解したいと常に思っています。それでは、 fake()
ヘルパーを見てみましょう。
function fake($locale = null) { if (app()->bound('config')) { $locale ??= app('config')->get('app.faker_locale'); } $locale ??= 'en_US'; $abstract = \Faker\Generator::class.':'.$locale; if (! app()->bound($abstract)) { app()->singleton($abstract, fn () => \Faker\Factory::create($locale)); } return app()->make($abstract); }
コードを読むと、Laravel がコンテナにシングルトンをバインドしていることがわかります。ただし、抽象クラスを調べると、これはインターフェイスを実装しない通常のクラスであり、オブジェクトはファクトリーを介して作成されます。これにより、状況が複雑になります。なぜでしょうか?
インターフェースであれば、ベースの\Faker\Generator
クラスを拡張する新しいクラスを作成し、いくつかの新しい機能を追加して、それをコンテナーに再バインドするだけで済みます。しかし、私たちにはそのような余裕はありません。
ファクトリーが関係しています。つまり、単純なインスタンス化ではなく、何らかのロジックが実行されます。この場合、ファクトリーはいくつかのプロバイダー (PhoneNumber、Text、UserAgent など) を追加します。そのため、再バインドを試みても、元の\Faker\Generator
を返すファクトリーを使用する必要があります。
解決策は?「ポイント 1 で説明したように、新しいジェネレーターを返す独自のファクトリーを作成するのを妨げるものは何なのか?」と思う人もいるかもしれません。ええ、何もありません。それは可能ですが、そうはしません。フレームワークを使用する理由はいくつかありますが、その 1 つは更新です。FakerPHP に新しいプロバイダーが追加されたり、メジャー アップグレードが行われたりするとどうなるでしょうか。Laravel はコードを調整しますが、変更を加えていない人は何も気付きません。ただし、私たちは取り残され、コードが壊れる可能性もあります (おそらく)。ですから、そこまではしたくありません。
基本的なオプションを検討したので、デザイン パターンなどのより高度なオプションについて考え始めることができます。正確な実装は必要ありません。問題に馴染みのあるものがあれば十分です。私が常に、それらを知っておくのは良いことだと言っているのはそのためです。この場合、古い機能を維持しながら新しい機能を追加することで、 Generator
クラスを「装飾」できます。よさそうですね。では、その方法を見てみましょう。
まず、新しいクラスFakerGenerator
を作成しましょう。
<?php namespace App\Support; use Closure; use Faker\Generator; use Illuminate\Support\Traits\ForwardsCalls; class FakerGenerator { use ForwardsCalls; public function __construct(private readonly Generator $generator) { } public function uniqueAndValid(Closure $validator = null): UniqueAndValidGenerator { return new UniqueAndValidGenerator($this->generator, $validator); } public function __call($method, $parameters): mixed { return $this->forwardCallTo($this->generator, $method, $parameters); } }
これは「デコレータ」になります (一種の)。これは、ベースGenerator
依存関係として想定し、新しい修飾子uniqueAndValid()
を導入する単純なクラスです。また、Laravel のForwardsCalls
特性も使用して、ベース オブジェクトへの呼び出しをプロキシできるようにします。
この特性には
forwardCallTo
とforwardDecoratedCallTo
の 2 つのメソッドがあります。後者は、装飾されたオブジェクトでメソッドを連鎖させたい場合に使用します。この場合、常に 1 回の呼び出しになります。
また、カスタム修飾子であるUniqueAndValidGenerator
を実装する必要もありますが、これはこの記事の目的ではありません。実装に興味がある方は、このクラスは基本的に FakerPHP に同梱されているValidGeneratorとUniqueGeneratorを組み合わせたものであり、こちらで見つけることができます。
次に、 AppServiceProvider
でフレームワークを拡張します。
<?php namespace App\Providers; use Closure; use Faker\Generator; use App\Support\FakerGenerator; use Illuminate\Support\ServiceProvider; class AppServiceProvider extends ServiceProvider { public function register(): void { $this->app->extend( $this->fakerAbstractName(), fn (Generator $base) => new FakerGenerator($base) ); } private function fakerAbstractName(): string { // This is important, it matches the name bound by the fake() helper return Generator::class . ':' . app('config')->get('app.faker_locale'); } }
extend()
メソッドは、指定された名前に一致する抽象がコンテナーにバインドされているかどうかを確認します。バインドされている場合は、その値をクロージャーの結果で上書きします。次のコードをご覧ください。
// Laravel: src/Illuminate/Container/Container.php public function extend($abstract, Closure $closure) { $abstract = $this->getAlias($abstract); if (isset($this->instances[$abstract])) { // You are interested here $this->instances[$abstract] = $closure($this->instances[$abstract], $this); $this->rebound($abstract); } else { $this->extenders[$abstract][] = $closure; if ($this->resolved($abstract)) { $this->rebound($abstract); } } }
そのため、コンテナー内でfake()
ヘルパーがバインドするのと同じ名前を生成するfakerAbstractName()
メソッドを定義しました。
見逃していたら上記のコードを再度確認してください。コメントを残しました。
これで、 fake()
を呼び出すたびに、 FakerGenerator
のインスタンスが返され、導入したカスタム修飾子にアクセスできるようになります。 FakerGenerator
クラスに存在しない呼び出しを呼び出すたびに、 __call()
がトリガーされ、 forwardCallTo()
メソッドを使用して基本のGenerator
にプロキシされます。
これで完了です。ついにfake()->uniqueAndValid()->randomElement()
が実行できるようになり、見事に動作します。
最後に、これは純粋なデコレータ パターンではないことを指摘しておきます。ただし、パターンは聖典ではありません。ニーズに合わせて調整し、問題を解決してください。
フレームワークは非常に役に立ちます。Laravel には多くの組み込み機能があります。ただし、フレームワークではプロジェクトのすべてのエッジ ケースをカバーできないため、行き詰まってしまうこともあります。そのような場合は、いつでもフレームワークを拡張できます。フレームワークがいかにシンプルであるかを確認しましたが、この Faker の例以外にも当てはまる主なアイデアを理解していただけたと思います。
常にシンプルに始めて、問題に対する最もシンプルな解決策を探してください。複雑さは必要なときに生じるので、基本的な継承で十分であれば、デコレータやその他のものを実装する必要はありません。フレームワークを拡張するときは、行き過ぎて損失が利益を上回らないように注意してください。フレームワークの一部を自分で管理する羽目になるのは避けてください。