Provide a Laravel-like facade for your application

Facades in Laravel have a specific meaning and should not be confused with the facade pattern. According to the Laravel documentation:

Facades provide a "static" interface to classes that are available in the application's service container.

If this is confusing, it becomes clear when you understand the problem facades solve and how they do it.

The problem facades solve

Let's consider a scenario where we have an SVG Loader interface and its implementation with multiple dependencies.

namespace Svg;

interface Loader {
    public function byName(string $name): string;
}

class LocalLoader implements Loader {
    // ...
}
$svgLoader = new Svg\LocalLoader(
    __DIR__ . '/assets/svg',
    new Svg\Normalizer\Bundle(
        new Svg\Normalizer\WhitespaceNormalizer(),
        new Svg\Normalizer\SizeAttributeNormalizer(),
        // ...
    )
);

$svgLoader->byName('mastodon');

We don't instantiate this and other objects repeatedly, but we have a PSR-11 container that takes care of it.

We could use a container that supports auto-wiring or configure it. PSR-11 doesn't define how you add things to the container.

Wherever we want to use the SVG Loader, we inject it as a dependency:

readonly class HtmlComponent {
    public function __construct(
        private Svg\Loader $svgLoader,
        private Template\Renderer $templateRenderer
    ) {
    }

    // ...
}

This approach is sound and generally praised because making dependencies explicit is extremely important.

There are other considerations than making dependencies crystal clear, and sometimes, for reasons, a more concise syntax is prefered:

Facade\Svg::byName('mastodon');

That's what Laravel-like facades "solve". They provide a way to do this. From the docs again:

Laravel facades serve as "static proxies" to underlying classes in the service container, providing the benefit of a terse, expressive syntax ...

An MVP implementation

We don't need Laravel to have facades; a basic implementation is surprisingly simple.

If we would want to provide a facade for exactly one class and one method, we could do this:

namespace Facade

class Svg {
    public static function byName(string $name): string {
        // container() -> \Psr\Container\ContainerInterface
        $svgLoader = container()->get(
            // We used the FQN as the ID for the container
            Svg\Loader::class
        );

        return $svgLoader->byName($name);
    }
}

We need a facade class with the same method name as the proxied, but this time static.

(If we have multiple methods, then it's quite clear we will end up with quite a lot of duplication and maintenance overhead.)

The service locator pattern

When we are calling container(), we are accessing the container that implements ContainerInterface.

It doesn't have to be a function; it can be done in many ways, arguably some worse than others:

$instance = container()->get($id);
// or
$instance = Container::services()->get($id);
// or
global $container;

$instance = $container->get($id);
// or other way

This is the part that is controversial and why some dislike facades.

Ultimately, we are providing another "syntax" to grab objects from the application container from anywhere, anytime.

This is the service locator pattern hidden from plain sight.

The Laravel documentation provides some warnings and thoughtful advice:

However, some care must be taken when using facades. The primary danger of facades is class "scope creep". Since facades are so easy to use and do not require injection, it can be easy to let your classes continue to grow and use many facades in a single class.

A more flexible approach

Typically, we will want facades for multiple classes with various methods.

Here's a possible implementation of a more flexible solution:

namespace Facade;

abstract class Facade
{
    abstract protected static function proxiedId(): string;

    public static function __callStatic(string $name, array $arguments)
    {
        $instance = container()->get(
            static::proxiedId()
        );
        

        return $instance->$name(...$arguments);
    }
}

/**
 * @method static string byName(string $name)
 * 
 * @see Svg\Loader
 */
class Svg extends Facade
{
    protected static function proxiedId(): string
    {
        // We used the FQN as the ID for the container
        return Svg\Loader::class;
    }
}

Compared to what we had, we replaced the "duplicated" method(s) with the magic method.

The consequence of this is losing the autocomplete in the IDEs. To overcome this, we added clues using DocBlocks.

Some duplication is necessary, but considering all, DocBlocks provides fewer headaches - and it's not even required; it's a convenience.

With the introduction of the Facade\Facade base class, we simplified things even more while allowing us to provide helper methods for all facades.

Last words

Of course, Laravel's implementation is far more complex; it provides performance optimizations, has error checking, etc., which you should do in an actual project. But at the end of the day, this is the core of it.

Testability

Interestingly, by far, most of the code in Laravel's Facade class is about providing a way to test them.

All facades have methods like expects, shouldReceive, spy, which might sound familiar because they are from Mockery.

Whenever you call a method like expects(), that is "proxied" to Mockery, which allows you to set up expectations "as usual".

namespace Illuminate\Support\Facades;

Svg extends Facade {
    // ...
}

Svg::shouldReceive('byName')
   ->with('mastodon')
   ->andReturn('<svg>...</svg>');

Those who argue that facades make the code hard to test or even untestable in Laravel might not know this.


How to replicate this behavior or how to test the MVP implementation is for another article, but it's definitely possible.