DEV Community

Recca Tsai
Recca Tsai

Posted 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

Top comments (0)