paint-brush
Laravel под капотом — как расширить фреймворкк@oussamamater
1,145 чтения
1,145 чтения

Laravel под капотом — как расширить фреймворк

к Oussama Mater6m2024/05/29
Read on Terminal Reader

Слишком долго; Читать

Laravel является оболочкой FakerPHP, к которому мы обычно обращаемся через хелпер `fake()`. Faker PHP поставляется с такими модификаторами, как `valid()` и `unique()`, но вы можете использовать только один за раз. Это заставило меня задуматься, а что, если мы захотим создать свой модификатор? Например, uniqueAndValid() или любой другой модификатор.
featured image - Laravel под капотом — как расширить фреймворк
Oussama Mater HackerNoon profile picture

Несколько дней назад я исправлял ненадежный тест, и оказалось, что мне нужны уникальные и действительные значения на моей фабрике. 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 привязывает синглтон к контейнеру. Однако, если мы посмотрим на абстракцию, то увидим, что это обычный класс, не реализующий никакого интерфейса, а объект создается с помощью фабрики. Это усложняет ситуацию. Почему?


  1. Потому что, если бы это был интерфейс, мы могли бы просто создать новый класс, расширяющий базовый класс \Faker\Generator , добавить некоторые новые функции и повторно привязать его к контейнеру. Но у нас нет такой роскоши.


  2. Здесь задействован завод. Это означает, что это не простое создание экземпляра; есть какая-то логика. В этом случае фабрика добавляет несколько провайдеров (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.


Всегда начинайте с простого и ищите самое простое решение проблемы. Сложности возникнут, когда это будет необходимо, поэтому, если базовое наследование помогает, нет необходимости реализовывать декоратор или что-то еще. Когда вы расширяете структуру, убедитесь, что вы не заходите слишком далеко, когда потери перевешивают выгоду. Вы не хотите в конечном итоге поддерживать часть фреймворка самостоятельно.