Laravel: Что такое Фасады и как они работают

Источник: «Reaching for Facades»
Фасады, их любят или ненавидят. Но они естественная часть того, чем сегодня является Laravel. Фасады Laravel это не совсем фасады, не так ли?

Фасады, люди, кажется, их любят или ненавидят. В любом случае они естественная часть того, чем сегодня является Laravel. Однако Фасады не являются строго фасадами, не так ли? Вместо этого они статические аксессоры для разрешения класса из контейнера.

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

Одной из ненавистной мне вещей были Фасады. Я был в лагере сварливых, жалующихся на вызов статических методов и т.д. Но я не знал как они работают в Laravel. Я присоединился к шуму повторяя, что говорили другие разработчики, не зная, о чём говорю.

Перенесёмся в сегодня, и я понял как работаю Фасады — и знаете что? Я определённо изменил свою мелодию. Я хочу написать руководство не для того, чтобы все со мной согласились, хотя и должны. А для того, чтобы вы тоже могли понять, как работают фасады и в чём их преимущества.

Это не совсем руководство, так как я буду вас знакомить с существующим кодом написанным мной. Вместо написания кода по мере написания руководства, для объяснения естественных моментов рефакторинга. Код, с которым я собираюсь вас познакомить — Laravel пакет Get Send Stack, вы можете найти на GitHub.

При создании пакета, я сделал то, что обычно делаю, и начал создавать API интеграцию с использованием HTTP Фасада — используя интерфейс/контракт для воздействия на DI контейнер для внедрения экземпляра при необходимости. Позвольте провести вас через эти этапы кода. Мы начнём с того, что сначала не будем использовать DI контейнер.

class AddSubscriberController
{
public function __invoke(AddSubscriberRequest $request)
{
$client = new Client(
url: strval(config('services.sendstack.url')),
token: strval(config('services.sendstack.token')),
);

try {
$subscriber = $client->subscribers()->create(
request: new SubscriberRequest(
email: $request->get('email'),
firstName: $request->get('first_name'),
lastName: $request->get('last_name'),
optIn: $request->get('opt_in'),
),
);
} catch (Throwable $exception) {
throw new FailedToSubscribeException(
message: $exception->getMessage(),
previous: $exception,
);
}

// return redirect or response.
}
}

Это немного многословно, но ясно видно, что вы делаете. Вы создаёте клиент, отправляете через него запрос и перехватываете потенциальное исключение. Наконец, возвращая перенаправление или ответ в зависимости от того был это API или веб контроллер. Код неплох. Вы можете протестировать его. Вы можете гарантировать поведение внутри контроллера без особых усилий.

Однако если пакет изменил способ интеграции с ним. Везде, где вы обновляли клиент для работы с API, вам пришлось бы пройтись по кодовой базе и внести все необходимые изменения. Это идеальный момент для рефакторинга, поскольку вы экономите свою будущую работу, работая умнее. Используя напрямую DI контейнер, давайте рассмотрим версию кода прошедшую рефакторинг.

class AddSubscriberController
{
public function __construct(
private readonly ClientContract $client,
) {}

public function __invoke(AddSubscriberRequest $request)
{
try {
$subscriber = $this->client->subscribers()->create(
request: new SubscriberRequest(
email: $request->get('email'),
firstName: $request->get('first_name'),
lastName: $request->get('last_name'),
optIn: $request->get('opt_in'),
),
);
} catch (Throwable $exception) {
throw new FailedToSubscribeException(
message: $exception->getMessage(),
previous: $exception,
);
}

// return redirect or response.
}
}

Теперь чище и более управляемый, мы внедряем из DI контейнер контракт/интерфейс, который будет разрешать клиента для нас — поскольку поставщик пакета дал подробную инструкцию создания клиента. В этом подходе нет ничего плохого; это шаблон активно используемый мною в моём коде. Я могу заменить реализацию, для получения другого результата и по-прежнему использовать тот же API пакет, что и интерфейс/контракт. Но опять, пока я использую контейнер — борюсь ли я с фреймворком? Одна из вещей, которая нравится в Laravel многим из нас — и мы можем поблагодарить за это Eloquent. Нам не нужно возиться с жонглированием контейнером для создания новой модели или чего-то в этом роде. Мы привыкли статически вызывать то, что хотим и когда хотим. Итак, давайте посмотрим на приведённый выше пример, используя Фасад, который я создал с помощью пакета.

class AddSubscriberController
{
public function __invoke(AddSubscriberRequest $request)
{
try {
$subscriber = SendStack::subscribers()->create(
request: new SubscriberRequest(
email: $request->get('email'),
firstName: $request->get('first_name'),
lastName: $request->get('last_name'),
optIn: $request->get('opt_in'),
),
);
} catch (Throwable $exception) {
throw new FailedToSubscribeException(
message: $exception->getMessage(),
previous: $exception,
);
}

// return redirect or response.
}
}

Больше не нужно беспокоится о контейнерах — и мы возвращаем то знакомое ощущение Laravel, которого нам не хватало. Преимущество в том, что реализация выглядит чистой и вы получаем тот же результат. Каковы недостатки? Потому что они конечно же есть. Единственный недостаток в том, что нельзя переключить реализацию, так как Фасад статичен по отношению к своей реализации. Но по моему опыту, переход от Провайдера А к Провайдеру Б, говоря о внешних сервисах, сложнее, чем создание и привязка новой реализации к контейнеру. Люди, которые всегда бьют в этот барабан, смотрят на проблему в узком методологическом плане. На самом деле смена провайдеров требует значительных усилий не только с точки зрения кода, поэтому всегда есть время сосредоточится на реализации чего-то другого там, где нужно. Иногда у нового провайдера есть то, чего нет у старого. Возможно, нужно отправить дополнительные данные в запросах и т.д.

Моя точка зрения здесь заключается в том, что, хотя принципы SOLID великолепны, и вам следует обращаться к ним за советом. Часто они являются несбыточной мечтой, которая не работает на практике, или вы собираетесь потратить так много времени на написания функции, что область применения изменится до того, как вы закончите. Борьба с фреймворком на каждом шагу не поможет создавать хорошие продукты. Вы создаёте хорошие продукты принимая далеко не идеальные решения и признаёте необходимость внесения изменений.

Как это связано с Фасадами? Как видно из примеров кода, Фасады упрощают задачу во многих отношениях. Ни один из способов не является правильным или не правильным способом. Фасад позволит реализовать более дружелюбную реализацию, но заставит вас пойти по определённому пути. Использование контейнера даст больше гибкости в будущем, но это не волшебная пуля и сопряжено с определённым риском. Просто создавать новые экземпляры, когда они вам нужны, легко, но так же и лениво, когда есть лучшие способы достичь того же результата.

Как на самом деле выглядит Фасад? Вот код из пакета.

declare(strict_types=1);

namespace SendStack\Laravel\Facades;

use Illuminate\Support\Facades\Facade;
use SendStack\Laravel\Contracts\ClientContract;
use SendStack\Laravel\Http\Resources\SubscribersResource;
use SendStack\Laravel\Http\Resources\TagResource;

/**
* @method static SubscribersResource subscribers()
* @method static TagResource tags()
* @method static bool isActiveSubscriber(string $email)
*
* @see ClientContract
*/

class SendStack extends Facade
{
protected static function getFacadeAccessor()
{
return ClientContract::class;
}
}

У него есть защищённый статический метод получения класса, который необходимо создать. А класс, который мы расширяем, будет перенаправлять все статические вызовы в этот класс после разрешения из контейнера. Люди говорят об этом, как о ругательстве, но на самом деле это ничем не отличается от создания псевдонима контейнера, кроме синтаксиса. В своём примере я добавил docblock для методов реализации/интерфейса для улучшения авто-завершения IDE, но это всего лишь дополнительный шаг, который мне нравиться делать.

Мораль этой истории в том, что Фасады не зло и на самом деле могут быть весьма полезными, так что игнорируйте ненавистников и принимайте их как я. От этого вы будете счастливее и намного продуктивнее.

Дополнительные материалы

Предыдущая Статья

Laravel: Перенос проекта с Webpack на Vite

Следующая Статья

Laravel: Внедрение зависимости и Сервис контейнер