Несколько дней назад я исправлял ненадежный тест, и оказалось, что мне нужны уникальные и действительные значения на моей фабрике. Laravel обертывает FakerPHP, к которому мы обычно обращаемся через помощник fake()
. FakerPHP поставляется с такими модификаторами, как valid()
и unique()
, но вы можете использовать только один за раз, поэтому вы не можете использовать 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?» Ну ничего, мы так можем, но не будем! Мы используем фреймворк по нескольким причинам, одна из которых — обновления. Что произойдет, если 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()
. Он также использует черту ForwardsCalls
из Laravel, которая позволяет ему пересылать вызовы базового объекта.
У этого типажа есть два метода:
forwardCallTo
иforwardDecoratedCallTo
. Используйте последнее, если хотите объединить методы декорированного объекта. В нашем случае у нас всегда будет один вызов.
Нам также необходимо реализовать UniqueAndValidGenerator
, который является пользовательским модификатором, но это не является целью статьи. Если вас интересует реализация, этот класс по сути представляет собой смесь ValidGenerator и UniqueGenerator , поставляемых с FakerPHP, вы можете найти его здесь .
Теперь давайте расширим структуру в 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); } } }
Вот почему мы определили метод fakerAbstractName()
, который генерирует то же имя, что и привязка помощника fake()
в контейнере.
Перепроверьте код выше, если вы его пропустили, я оставил комментарий.
Теперь каждый раз, когда мы вызываем fake()
, будет возвращаться экземпляр FakerGenerator
, и у нас будет доступ к введенному нами пользовательскому модификатору. Каждый раз, когда мы вызываем вызов, которого нет в классе FakerGenerator
, срабатывает __call()
, который пересылает его базовому Generator
с помощью метода forwardCallTo()
.
Вот и все! Наконец-то я могу сделать fake()->uniqueAndValid()->randomElement()
, и это работает просто великолепно!
Прежде чем мы закончим, я хочу отметить, что это не чистый шаблон декоратора. Однако узоры не являются священными текстами; настройте их в соответствии со своими потребностями и решите проблему.
Фреймворки невероятно полезны, а Laravel имеет множество встроенных функций. Однако они не могут охватить все крайние случаи ваших проектов, и иногда вы можете зайти в тупик. Когда это произойдет, вы всегда можете расширить структуру. Мы увидели, насколько это просто, и я надеюсь, что вы поняли основную идею, которая выходит за рамки этого примера с Faker.
Всегда начинайте с простого и ищите самое простое решение проблемы. Сложности возникнут, когда это будет необходимо, поэтому, если базовое наследование помогает, нет необходимости реализовывать декоратор или что-то еще. Когда вы расширяете структуру, убедитесь, что вы не заходите слишком далеко, когда потери перевешивают выгоду. Вы не хотите в конечном итоге поддерживать часть фреймворка самостоятельно.