如何安全地使用 Laravel Facade
这个问题在 Laravel 文档中也不是特别清晰,不过 Facade 有一件事可能会在系统中导致意外漏洞:
Facade 像单例
与匿名函数绑定的传统服务容器不同,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];
}
}
该方法的第一行中,你可以看到它首先检测该 Facade 是否已经有了解析过的实例,如果有,则返回该实例。
如果你像我一样,开始相信无论何时调用 Facade 都会得到该类的新实例,这可能意味着麻烦。
比如,该代码可能很麻烦:
class CreditBalance
{
/*
* Scope the credit balance checker to the given user.
*/
public function forUser(User $user)
{
$this->user = $user;
return $this;
}
/*
* Get the credit balance of the scoped user,
* falling back to authenticated user.
*/
public function getBalance(): int
{
$user = $this->user ?? Auth::user();
return (new CreditBalanceAggregator($user))->balance();
}
}// This will get the balance of the Auth::user() user,
// because $this->user will be null
$firstBalance = CreditBalance::getBalance();
// And this would get the balance for the given user,
// while also setting the $this->user property.
$secondBalance = CreditBalance::forUser($user)->getBalance();
// Because we have previously set the user on the resolved
// Facade instance, the $this->user property is still set
// and this method will return the balance of the user from
// $this->user property - a different result from our first call.
$thirdBalance = CreditBalance::getBalance();
// $firstBalance != $secondBalance
如你所见,因为 Facade 返回同一个的已解析实例,所以您在 Facade 实例上设置的任何属性都将保留,以便将来调用同一个 Facade。这可能并不总是你想要的行为。
如果你的 Facade 是某种 builder,并且包含的方法旨在将来设置查询范围的某些模型/数据,那么如果没有测试好,上述方法可能会导致严重的错误。
有几种方法可以解决这个问题。
解决方式 #1 - 清除解析过的 Facade 实例
use Illuminate\Support\Facades\Facade;
class CreditBalance
{
/*
* Scope the credit balance checker to the given user.
*/
public function forUser(User $user)
{
$this->user = $user;
Facade::clearResolvedInstance('credit-balance');
return $this;
}
// ...
}
第一个参数应该与你从 getFacadeAccessor()
方法返回的值相同。在我的示例中,它是 'credit-balance'
字符串。
这将在每次你调用 forUser()
方法时清除这个已解析过的 Facade 实例,强制下次 Facade 调用重新解析实例。
解决方式 #2 - 在 scope 方法中返回新实例
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 的调用将返回我们期望的结果。