Laravel Under The Hood - Facades
Photo by Edilson Borges on Unsplash
Hello Facades
👋
You've just installed a fresh Laravel application, booted it up, and got the welcome page. Like everyone else, you try to see how it's rendered, so you hop into the web.php
file and encounter this code
<?php
use Illuminate\Support\Facades\Route;
Route::get('/', function () {
return view('welcome');
});
It's obvious how we got the welcome view, but you're curious about how Laravel's router works, so you decide to dive into the code. The initial assumption is: There's a Route
class on which we're calling a static method get()
. However, upon clicking it, there is no get()
method there. So, what kind of dark magic is happening? Let's demystify this!
Regular Facades
Please note that I stripped most of the PHPDocs and inlined the types just for simplicity, "..." refers to more code.
I strongly suggest opening your IDE and following along with the code to avoid any confusion.
Following our example, let's explore the Route
class
<?php
namespace Illuminate\Support\Facades;
class Route extends Facade
{
// ...
protected static function getFacadeAccessor(): string
{
return 'router';
}
}
There's not much here, just the getFacadeAccessor()
method that returns the string router
. Keeping this in mind, let's move to the parent class
<?php
namespace Illuminate\Support\Facades;
use RuntimeException;
// ...
abstract class Facade
{
// ...
public static function __callStatic(string $method, array $args): mixed
{
$instance = static::getFacadeRoot();
if (! $instance) {
throw new RuntimeException('A facade root has not been set.');
}
return $instance->$method(...$args);
}
}
Within the parent class, there are lots of methods, there isn't a get()
method though. But, there is an interesting one, the __callStatic()
method. It's a magic method, invoked whenever an undefined static method, like get()
in our case, is called. Therefore, our call __callStatic('get', ['/', Closure()])
represents what we passed when invoking Route::get()
, the route /
and a Closure()
that returns the welcome view.
When __callStatic()
gets triggered, it first attempts to set a variable $instance
by calling getFacadeRoot()
, the $instance
holds the actual class to which the call should be forwarded, let's take a closer look, it will make sense in a bit
// Facade.php
public static function getFacadeRoot()
{
return static::resolveFacadeInstance(static::getFacadeAccessor());
}
Hey, look it is the getFacadeAccessor()
from the child class Route
, which we know returned the string router
. This router
string is then passed to resolveFacadeInstance()
, which attempts to resolve it to a class, a sort of mapping that says "What class does this string represent?", let's see
// Facade.php
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];
}
}
It first checks if a static array, $resolvedInstance
, has a value set with the given $name
(which, again, is router
). If it finds a match, it just returns that value. This is Laravel caching to optimize performance a little bit. This caching occurs within a single request, if this method is called multiple times with the same argument within the same request, it uses the cached value. Let's assume it's the initial call and proceed.
It then checks if $app
is set, and $app
is an instance of the application container
// Facade.php
protected static \Illuminate\Contracts\Foundation\Application $app;
If you're curious about what an application container is, think of it as a box where your classes are stored. When you need those classes, you simply reach into that box. Sometimes this container performs a bit of magic, even if the box is empty, and you reach to grab a class, it will get it for you. That's a topic for another article.
Now, you might wonder, "When is $app
set?", because it needs to be, otherwise, we won't have our $instance
. This application container gets set during our application's bootstrapping process. Let's take a quick look at the \Illuminate\Foundation\Http\Kernel
class
<?php
namespace Illuminate\Foundation\Http;
use Illuminate\Http\Request;
use Illuminate\Http\Response;
use Illuminate\Support\Facades\Facade;
use Illuminate\Contracts\Http\Kernel as KernelContract;
// ...
class Kernel implements KernelContract
{
// ...
protected $app;
protected $bootstrappers = [
\Illuminate\Foundation\Bootstrap\LoadEnvironmentVariables::class,
\Illuminate\Foundation\Bootstrap\LoadConfiguration::class,
\Illuminate\Foundation\Bootstrap\HandleExceptions::class,
\Illuminate\Foundation\Bootstrap\RegisterFacades::class, // <- this guy
\Illuminate\Foundation\Bootstrap\RegisterProviders::class,
\Illuminate\Foundation\Bootstrap\BootProviders::class,
];
public function bootstrap(): void
{
if (! $this->app->hasBeenBootstrapped()) {
$this->app->bootstrapWith($this->bootstrappers());
}
}
}
When a request comes through, it's sent to the router. Just before that, the bootstrap()
method is invoked, which uses the bootstrappers
array to prepare the application. If you explore the bootstrapWith()
method in the \Illuminate\Foundation\Application
class, it iterates through these bootstrappers, calling their bootstrap()
method. For simplicity, let's just focus on \Illuminate\Foundation\Bootstrap\RegisterFacades
, which we know contains a bootstrap()
method that will be invoked in bootstrapWith()
<?php
namespace Illuminate\Foundation\Bootstrap;
use Illuminate\Contracts\Foundation\Application;
use Illuminate\Foundation\AliasLoader;
use Illuminate\Foundation\PackageManifest;
use Illuminate\Support\Facades\Facade;
class RegisterFacades
{
// ...
public function bootstrap(Application $app): void
{
Facade::clearResolvedInstances();
Facade::setFacadeApplication($app); // Interested here
AliasLoader::getInstance(array_merge(
$app->make('config')->get('app.aliases', []),
$app->make(PackageManifest::class)->aliases()
))->register();
}
}
And there it is, we're setting the application container on the Facade
class using the static method setFacadeApplication()
// RegisterFacades.php
public static function setFacadeApplication($app)
{
static::$app = $app;
}
See, we assign the $app
property that we're testing within resolveFacadeInstance()
. This answers the question, let's continue
// Facade.php
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];
}
}
We confirmed that $app
is set during the application bootstrapping. The next step is to check whether the resolved instance should be cached by verifying $cached
, which defaults to true. Finally, we retrieve the instance from the application container, in our case, it's like asking static::$app['router']
to provide any class bound to the string router
. Now, you might wonder why we access $app
like an array despite it being an instance of the application container, so an object. Well, you're right! However, the application container implements a PHP interface called ArrayAccess
, allowing array-like access. We can take a look at it to confirm this fact
<?php
namespace Illuminate\Container;
use ArrayAccess; // <- this guy
use Illuminate\Contracts\Container\Container as ContainerContract;
class Container implements ArrayAccess, ContainerContract {
// ...
}
So the resolveFacadeInstance()
indeed returns an instance bound to the router
string, specifically, \Illuminate\Routing\Router
. How did I know? Take a look at the Route
facade, often, you will find a PHPDoc @see
hinting at what this facade conceals or, more precisely, to what class our method calls will be proxied.
Now, back to our __callStatic
method
<?php
namespace Illuminate\Support\Facades;
use RuntimeException;
// ...
abstract class Facade
{
// ...
public static function __callStatic(string $method, array $args): mixed
{
$instance = static::getFacadeRoot();
if (! $instance) {
throw new RuntimeException('A facade root has not been set.');
}
return $instance->$method(...$args);
}
}
We have $instance
, an object of the \Illuminate\Routing\Router
class. We test if is it set (which, in our case, is confirmed), and we directly invoke the method on it. So, we end up with
// Facade.php
return $instance->get('/', Closure());
And now, you can confirm the get()
exists within the \Illuminate\Routing\Router
class
<?php
namespace Illuminate\Routing;
use Illuminate\Routing\Route;
use Illuminate\Contracts\Routing\BindingRegistrar;
use Illuminate\Contracts\Routing\Registrar as RegistrarContract;
// ...
class Router implements BindingRegistrar, RegistrarContract
{
// ...
public function get(string $uri, array|string|callable|null $action = null): Route
{
return $this->addRoute(['GET', 'HEAD'], $uri, $action);
}
}
That wraps it up! Wasn't that difficult in the end? To recap, a facade returns a string that's bound to the container. For instance, hello-world
might be bound to the HelloWorld
class. When we statically invoke an undefined method on a facade, HelloWorldFacade
for example, __callStatic()
steps in. It resolves the string registered in its getFacadeAccessor()
method to whatever is bound within the container and proxies our call to that retrieved instance. Thus, we end up with (new HelloWorld())->method()
. That's the essence of it! Still didn't click for you? let's create our facade then!
Let's make our Facade
Say we have this class
<?php
namespace App\Http\Controllers;
class HelloWorld
{
public function greet(): string {
return "Hello, World!";
}
}
The goal is to invoke HelloWorld::greet()
. To do this, we'll bind our class to the application container. First, navigate to AppServiceProvider
.
<?php
namespace App\Providers;
use App\Http\Controllers;
use Illuminate\Support\ServiceProvider;
class AppServiceProvider extends ServiceProvider
{
public function register(): void
{
$this->app->bind('hello-world', function ($app) {
return new HelloWorld;
});
}
// ...
}
Now, whenever we request hello-world
from our application container (or the box, as I mentioned earlier), it returns an instance of HelloWorld
. What's left? Simply create a facade that returns the string hello-world
.
<?php
namespace App\Http\Facades;
use Illuminate\Support\Facades\Facade;
class HelloWorldFacade extends Facade
{
protected static function getFacadeAccessor()
{
return 'hello-world';
}
}
With this in place, we're ready to use it. Let's call it within our web.php
<?php
use App\Http\Facades;
use Illuminate\Support\Facades\Route;
Route::get('/', function () {
return HelloWorldFacade::greet(); // Hello, World!
});
We know that greet()
does not exist on the HelloWorldFacade
facade, __callStatic()
is triggered. It pulls a class represented by a string (hello-world
in our case) from the application container. And we have already made this binding in the AppServiceProvider
, we instructed it to provide an instance of HelloWorld
whenever someone requests a hello-world
. Consequently, any call, such as greet()
, will operate on that retrieved instance of HelloWorld
. And that's it.
Congratulations! You've created your very own facade!
Laravel Real-Time Facades
Now that you have a good understanding of facades, there's one more magic trick to unveil. Imagine being able to call HelloWorld::greet()
without creating a facade, using real-time facades.
Let's have a look
<?php
use Facades\App\Http\Controllers; // Notice the prefix
use Illuminate\Support\Facades\Route;
Route::get('/', function () {
return HelloWorld::greet(); // Hello, World!
});
By prefixing the controller's namespace with Facades
, we achieve the same result as earlier. But, it's certain that the HelloWorld
controller doesn't have any static method named greet()
! And where does Facades\App\Http\Controllers\HelloWorld
even come from? I understand this might seem like some sorcery, but once you grasp it, it's quite simple.
Let's take a closer look at \Illuminate\Foundation\Bootstrap\RegisterFacades
we checked earlier, the class responsible for setting the $app
<?php
namespace Illuminate\Foundation\Bootstrap;
use Illuminate\Contracts\Foundation\Application;
use Illuminate\Foundation\AliasLoader;
use Illuminate\Foundation\PackageManifest;
use Illuminate\Support\Facades\Facade;
class RegisterFacades
{
public function bootstrap(Application $app): void
{
Facade::clearResolvedInstances();
Facade::setFacadeApplication($app);
AliasLoader::getInstance(array_merge(
$app->make('config')->get('app.aliases', []),
$app->make(PackageManifest::class)->aliases()
))->register(); // Interested here
}
}
You can see at the very end, the register()
method is invoked. Let's take a peek inside
<?php
namespace Illuminate\Foundation;
class AliasLoader
{
// ...
protected $registered = false;
public function register(): void
{
if (! $this->registered) {
$this->prependToLoaderStack();
$this->registered = true;
}
}
}
The $registered
variable is initially set to false
. Therefore, we enter the if
statement and call the prependToLoaderStack()
method. Now, let's explore its implementation
// AliasLoader.php
protected function prependToLoaderStack(): void
{
spl_autoload_register([$this, 'load'], true, true);
}
This is where the magic happens! Laravel is calling the spl_autoload_register()
function, a built-in PHP function that triggers when attempting to access an undefined class. It defines the logic to execute in such situations. In this case, Laravel chooses to invoke the load()
method when encountering an undefined call. Additionally, spl_autoload_register()
automatically passes the name of the undefined class to whichever method or function it calls.
Let's explore the load()
method, it must be the key
// AliasLoader.php
public function load($alias)
{
if (static::$facadeNamespace && str_starts_with($alias, static::$facadeNamespace)) {
$this->loadFacade($alias);
return true;
}
if (isset($this->aliases[$alias])) {
return class_alias($this->aliases[$alias], $alias);
}
}
We check if $facadeNamespace
is set, and if whatever class passed, in our case Facades\App\Http\Controllers\HelloWorld
starts with whatever is set in $facadeNamespace
The logic checks if $facadeNamespace
is set and if the passed class, in our case Facades\App\Http\Controllers\HelloWorld
(which is undefined), starts with the value specified in $facadeNamespace
// AliasLoader.php
protected static $facadeNamespace = 'Facades\\';
Since we've prefixed our controller's namespace with Facades
, satisfying the condition, we proceed to loadFacade()
// AliasLoader.php
protected function loadFacade($alias)
{
require $this->ensureFacadeExists($alias);
}
Here, the method requires whatever path is returned from ensureFacadeExists()
. So, the next step is to delve into its implementation
// AliasLoader.php
protected function ensureFacadeExists($alias)
{
if (is_file($path = storage_path('framework/cache/facade-'.sha1($alias).'.php'))) {
return $path;
}
file_put_contents($path, $this->formatFacadeStub(
$alias, file_get_contents(__DIR__.'/stubs/facade.stub')
));
return $path;
}
First, a check is made to ascertain if a file named framework/cache/facade-'.sha1($alias).'.php'
exists. In our case, this file isn't present, triggering the next step: file_put_contents()
. This function creates a file and saves it to the specified $path
. The file's content is generated by formatFacadeStub()
, which, judging by its name, creates a facade from a stub. If you were to inspect facade.stub
, you'd find the following
<?php
namespace DummyNamespace;
use Illuminate\Support\Facades\Facade;
/**
* @see \DummyTarget
*/
class DummyClass extends Facade
{
/**
* Get the registered name of the component.
*/
protected static function getFacadeAccessor(): string
{
return 'DummyTarget';
}
}
Looks familiar? That's essentially what we did manually. Now, formatFacadeStub()
replaces the dummy content with our undefined class after removing the Facades\\
prefix. This updated facade is then stored. Consequently, when loadFacade()
requires the file, it does so correctly, and it ends up requiring the following file
<?php
namespace Facades\App\Http\Controllers;
use Illuminate\Support\Facades\Facade;
/**
* @see \App\Http\Controllers\HelloWorld
*/
class HelloWorld extends Facade
{
/**
* Get the registered name of the component.
*/
protected static function getFacadeAccessor(): string
{
return 'App\Http\Controllers\HelloWorld';
}
}
And now, in the usual flow, we ask the application container to return any instance bound to the string App\Http\Controllers\HelloWorld
. You might be wondering, we didn't bind this string to anything, we didn't even touch our AppServiceProvider
. But remember what I mentioned about the application container at the very beginning? Even if the box is empty, it will return the instance, but with one condition, the class must not have a constructor. Otherwise, it wouldn't know how to build it for you. In our case, our HelloWorld
class doesn't need any arguments to be constructed. So, the container resolves it, returns it and all the calls get proxied to it.
Recapping real-time facades: We've prefixed our class with Facades
. During application bootstrapping, Laravel registers spl_autoload_register()
, which triggers when we call undefined classes. It eventually leads to the load()
method. Inside load()
, we check if the current undefined class is prefixed with Facades
. It matches, so Laravel tries to load it. Since the facade doesn't exist, it creates it from a stub and then requires the file. And voila! You've got a regular facade, but this one was created on the fly. Pretty cool, huh?
Conclusion
Congratulations on making it this far! I understand it can be a bit overwhelming. Feel free to go back and re-read any sections that didn't quite click for you. Following up with your IDE can also help. But hey, no more black magic, must feel good, at least that's how I felt the first time!
And remember, next time you call a method statically, it might not be the case 🪄
driesvints, ol-serdiuk liked this article