How to use Laravel Facades safely

It's not exactly clear in the Laravel documentation, but there's one thing the Facades do that might introduce accidental bugs in your system:

Facades are like singletons.

Unlike traditional service container bindings with anonymous functions, Facades retain the resolved instance and use that in future Facade calls. Let's have a look at Laravel's code:

    /**
     * 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];
        }
    }

On the first line of the method you can see it's first checking whether there's already a resolved instance of the specific facade and, if so, returns that instance.

This might mean trouble if you, like me, come to believe that you get a fresh instance of the class whenever you call the Facade.

For example, this code might be troublesome:

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

As you can see, because Facades return the same resolved instance, any properties you set on the Facade instance will remain for the future calls to the same Facade. That might not always be the behaviour you're looking for.

If your Facade is some kind of a builder and contains methods that are meant to scope it further to certain models/data, the above approach might result in serious bugs if not tested well.

There are several ways of fixing this.

Fix #1 - Clear the resolved Facade instance

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;
    }
    
    // ...
}

The first parameter should be the same value you return from the getFacadeAccessor() method. In my case, it was the string 'credit-balance'.

This will clear the resolved Facade instance every time you call the forUser() method, forcing the next Facade call to resolve the instance from scratch.

Fix #2 - Return a new instance from scope methods

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);
    }
    
    // ...
}

Here, instead of reusing the same resolved instance whenever we want to scope our calls to a given user, we return a brand new instance of the class. This way we get a clean reset of the builder and we can trust that future calls to the Facade without the scoped user will return the result we expect.


Hope this helped!

Let me know what you think on Twitter.