DEV Community

Recca Tsai
Recca Tsai

Posted on • Edited on • Originally published at recca0120.github.io

How Laravel Facade Resolves Instances from the Container

Originally published at recca0120.github.io

Continuing from }}">the previous post on Laravel Container, this post looks at the relationship between Container and Facade.

bind vs singleton

First, prepare a FakeApi class:

namespace App;

class FakeApi
{
    private string $token;

    public function __construct(string $token)
    {
        $this->token = $token;
    }

    public function getToken(): string
    {
        return $this->token;
    }
}
Enter fullscreen mode Exit fullscreen mode

Register it in AppServiceProvider using bind with a random token:

namespace App\Providers;

use App\FakeApi;
use Illuminate\Support\ServiceProvider;
use Illuminate\Support\Str;

class AppServiceProvider extends ServiceProvider
{
    public function register()
    {
        $this->app->bind(FakeApi::class, fn() => new FakeApi(Str::random()));
    }
}
Enter fullscreen mode Exit fullscreen mode

Write a test that resolves FakeApi from the Container twice and compares the tokens:

namespace Tests\Feature;

use App\FakeApi;
use Tests\TestCase;

class FacadeTest extends TestCase
{
    public function test_facade(): void
    {
        $fakeApi = app(FakeApi::class);
        $fakeApi2 = app(FakeApi::class);

        self::assertEquals($fakeApi->getToken(), $fakeApi2->getToken());
    }
}
Enter fullscreen mode Exit fullscreen mode

The test fails because bind creates a new instance every time. Switching to singleton fixes it:

$this->app->singleton(FakeApi::class, fn() => new FakeApi(Str::random()));
Enter fullscreen mode Exit fullscreen mode

Facade Is Just a Proxy for the Container

Create a Facade for FakeApi:

namespace App\Facades;

use Illuminate\Support\Facades\Facade;

class FakeApi extends Facade
{
    protected static function getFacadeAccessor()
    {
        return \App\FakeApi::class;
    }
}
Enter fullscreen mode Exit fullscreen mode

Test that the Facade and the instance resolved directly from the Container are the same:

namespace Tests\Feature;

use App\Facades\FakeApi as FakeApiFacade;
use App\FakeApi;
use Tests\TestCase;

class FacadeTest extends TestCase
{
    public function test_facade(): void
    {
        $fakeApi = app(FakeApi::class);

        self::assertEquals($fakeApi->getToken(), FakeApiFacade::getToken());
    }
}
Enter fullscreen mode Exit fullscreen mode

A Facade uses the key returned by getFacadeAccessor to resolve an instance from the Container, then forwards static method calls to instance methods.

Facade → Container → Instance resolution flow

getFacadeAccessor Can Be Any String

For example, Laravel's built-in DB Facade returns the string 'db' instead of a class name:

namespace Illuminate\Support\Facades;

class DB extends Facade
{
    protected static function getFacadeAccessor()
    {
        return 'db';
    }
}
Enter fullscreen mode Exit fullscreen mode

We can do the same by registering an alias in the Container:

namespace App\Providers;

use App\FakeApi;
use Illuminate\Support\ServiceProvider;
use Illuminate\Support\Str;

class AppServiceProvider extends ServiceProvider
{
    public function register()
    {
        $this->app->singleton(FakeApi::class, fn() => new FakeApi(Str::random()));
        $this->app->singleton('fake-api', fn() => $this->app->make(FakeApi::class));
    }
}
Enter fullscreen mode Exit fullscreen mode
namespace App\Facades;

use Illuminate\Support\Facades\Facade;

class FakeApi extends Facade
{
    protected static function getFacadeAccessor()
    {
        return 'fake-api';
    }
}
Enter fullscreen mode Exit fullscreen mode

The key registered in the Container is just a string (FakeApi::class is essentially a string too), so either a class name or a custom string works.

If you want to find where a built-in Laravel Facade is registered, just grep for it:

grep -rnw ./vendor -e "\$this->app->\(singleton\|bind\)('db'"
Enter fullscreen mode Exit fullscreen mode

References

Top comments (0)