Как безопасно использовать Laravel Фасады
В документации Laravel это не совсем понятно, но Laravel Facade делают одну вещь, которая может привести к случайным ошибкам в вашем приложении: Фасады похожи на синглтоны.
В отличие от традиционных привязок Сервис Контейнеров с анонимными функциями, Laravel Facade сохраняют разрешённый экземпляр и используют его в будущих вызовах Facade. Давайте посмотрим на Laravel код:
/**
* Resolve the facade root instance from the container.
*
* @param string $name
* @return mixed
*/
protected static function resolveFacadeInstance($name)
{
if (isset(static::$resolvedInstance[$name])) {
return static::$resolvedInstance[$name];
}
if (static::$app) {
if (static::$cached) {
return static::$resolvedInstance[$name] = static::$app[$name];
}
return static::$app[$name];
}
}
В первой строке метода вы можете видеть, что он сначала проверяет, существует ли уже разрешённый экземпляр конкретного фасада, и, если да, возвращает этот экземпляр.
Это может привести к проблемам, если вы, как и я, придёте к выводу, что получаете новый экземпляр класса всякий раз, когда вызываете Laravel Facade.
Например, этот код может быть проблематичным:
class CreditBalance
{
/*
* Проверка кредитного баланса для заданного пользователя
*/
public function forUser(User $user)
{
$this->user = $user;
return $this;
}
/*
* Получение кредитного баланса заданного пользователя,
* если пользователь не задан, то аутентифицированного пользователя.
*/
public function getBalance(): int
{
$user = $this->user ?? Auth::user();
return (new CreditBalanceAggregator($user))->balance();
}
}
// Получает баланс Auth::user(), потому что $this->user будет null
$firstBalance = CreditBalance::getBalance();
// Получает баланс для данного пользователя,
// а также устанавливает свойство $this->user
$secondBalance = CreditBalance::forUser($user)->getBalance();
// Поскольку мы ранее задали свойство user в разрешённом экземпляре Facade,
// $this->user всё ещё установлено, и этот метод вернёт баланс пользователя
// из $this->user - результат отличается от нашего первого вызова.
$thirdBalance = CreditBalance::getBalance();
// $firstBalance != $secondBalance
Как видите, поскольку экземпляр Laravel Facade возвращает один и тот же разрешённый экземпляр, любые свойства заданные вами для экземпляра Facade, останутся для будущих вызовах того же Facade. Возможно, это не всегда то поведение, которые вы ищите.
Если ваш Facade является своего рода конструктором и содержит методы, предназначенные для его дальнейшей области действия определённой моделями/данными, описанный выше подход может привести к серьёзным ошибкам, если его не протестировать должным образом.
Есть несколько способов исправления этого.
Способ 1. Очистите разрешённый экземпляр Facade
use Illuminate\Support\Facades\Facade;
class CreditBalance
{
/*
* Проверка кредитного баланса для заданного пользователя
*/
public function forUser(User $user)
{
$this->user = $user;
Facade::clearResolvedInstance('credit-balance');
return $this;
}
// ...
}
Первый параметр должен быть тем же значением, которое вы возвращаете из метода getFacadeAccessor()
. В моём случае это была строка 'credit-balance'
.
Это будет очищать разрешённый экземпляр Laravel Facade каждый раз, когда вы вызываете метод forUser()
, заставляя следующий вызов Facade разрешать экземпляр с нуля.
Способ 2. Возвращение нового экземпляра из методов области видимости
use Illuminate\Support\Facades\Facade;
class CreditBalance
{
public function __construct(User $user = null)
{
$this->user = $user;
}
/*
* Scope the credit balance checker to the given user.
*/
public function forUser(User $user)
{
return new static($user);
}
// ...
}
Здесь вместо повторного использования одного и того же разрешённого экземпляра всякий раз, когда мы хотим распространить наши вызовы для данного пользователя, мы возвращаем совершенно новый экземпляр класса. Таким образом, мы получаем чистый сброс конструктора и можем быть уверены, что будущие вызовы Facade без пользователя с ограниченной областью вернут ожидаемый результат.