Фасады, люди любят их и ненавидят. В любом случае, они являются неотъемлемой частью современного Laravel. Однако фасады в Laravel не являются строгой интерпретацией базового шаблона Facade. Так что же они из себя представляют на самом деле? Вместо этого они являются статическими средствами доступа к объекту нужного нам класса из контейнера.
Когда я впервые начал работать с Laravel, я его ненавидел, а после трех лет использования я наконец начал его понимать и принимать. Я был типичным “ненавистником” Laravel, которых сегодня хватает. Когда я научился понимать и принял Laravel таким, какой он есть, вместо того, чтобы пытаться бороться с ним и рамками своего мышления, я полюбил его. Можно сказать, что сейчас я стал ярым его сторонником.
Одной из вещей, которые я ненавидел какое-то время, были именно Фасады. Я был в лагере сварливых, жалующихся на вызовы статических методов, бла-бла-бла. Но моя проблема была в том, что я на самом деле не знал, как они работают в Laravel. Я присоединялся к хайпу вокруг этой темы, повторяя то, что говорили другие “опытные” разработчики, не зная толком, о чем я говорю.
Однако, перенесясь в сегодняшний день, я понимаю как работают Фасады — и знаете что? Это определенно изменило мое отношение к ним. Я хочу написать этот туториал не для того, чтобы вы все со мной согласились, хотя и должны, а для того, чтобы вы тоже могли понять, как работают фасады и в чем их преимущества.
Эта статья не является строгим учебником, так как я буду знакомить вас с существующим кодом, который я написал, тогда как обычно я пишу код по мере написания учебника, чтобы я мог объяснить естественные моменты рефакторинга. Код, с которым я собираюсь вас познакомить - это пакетGet Send Stack Laravel, который вы можете найти наGitHub здесь.
При создании этого пакета я сделал то, что обычно делаю, и начал создавать интеграцию API с использованием HTTP Facade — используя интерфейс/контракт с использованием контейнера 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, вам пришлось бы пройтись по коду и внести все необходимые изменения. В такие моменты наступает время, чтобы задуматься о рефакторинге, так как вы экономите свою будущие силы и время, работая умнее сейчас. Используя непосредственно контейнер внедрения зависимостей, давайте рассмотрим измененную версию приведенного выше кода.
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, которого нам не хватало. Преимущество здесь в том, что опыт разработчиков прост, реализация выглядит чистой, и мы достигаем того же результата. Каковы недостатки? Разумеется, они есть. Единственным недостатком является то, что вы не можете переключать реализацию, так как Facade является статиченым по отношению к своей реализации. Но по моему опыту, переход от провайдера А к провайдеру Б, когда мы говорим о внешних сервисах, сложнее, чем создание и привязка новой реализации к контейнеру. Люди, которые упорно применяют только такой подход (DI контейнер), смотрят на проблему в узком смысле. На самом деле смена провайдеров требует значительных усилий не только с точки зрения кода, поэтому всегда есть достаточно времени, чтобы сосредоточиться на реализации чего-то другого там, где вам нужно. Иногда у нового провайдера есть то, чего нет у старого. Возможно, вам придется отправлять дополнительные данные в ваших запросах и т. д.
Я хочу сказать, что, хотя принципы SOLID важны и неотъемлемы, и вы должны обращаться к ним за советом — они часто являются нереалистичной мечтой, которая не будет работать на практике, или вы собираетесь потратить так много времени на написание функции, что требования успеют измениться еще до того, как вы закончите. Борьба с фреймворком на каждом шагу не поможет вам создавать хорошие продукты. Вы создаете хорошие продукты, принимая далеко не идеальные решения, и признаете необходимость внесения изменений.
Как это связано с фасадами? Как видно из примеров кода, фасады во многих аспектах упрощают задачу. Ни один из способов не является неверным, и ни один из способов не является правильным. Фасад лишь позволит реализовать более дружелюбную реализацию, в стиле всего фреймворка Laravel, но заставит вас пойти по определенному пути. Использование контейнера даст вам больше гибкости в будущем, но это не волшебная палочка и в будущем может быть сопряжено с определенными рисками. Просто обновлять экземпляры одной реализации, когда они вам нужны, легко, но довольно скучно, когда есть лучшие способы добиться того же результата.
Как на самом деле выглядит Фасад? Вот точный код из пакета.
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;
}
}
У него есть защищенный статический метод для получения класса, который ему нужно сконструировать и построить, а класс, который мы расширяем, будет перенаправлять все статические вызовы в этот класс после разрешения из контейнера. Люди говорят об этом, как о ругательстве, но на самом деле это ничем не отличается от создания псевдонима контейнера, кроме синтаксиса. В моем примере я добавил doc blocks для методов реализации/интерфейса, чтобы улучшить подсказки в IDE, но это всего лишь дополнительный шаг, который мне нравится делать.
Мораль этой истории в том, что Фасады не зло и на самом деле могут быть очень полезными, так что игнорируйте ненавистников и принимайте их, как я. Вам будет значительно проще и вы будете намного продуктивнее.
Оригинал статьи.