Laravel Under The Hood - What Are Facades?
2024-6-29 23:0:20 Author: hackernoon.com(查看原文) 阅读量:3 收藏

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 that 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 🪄


文章来源: https://hackernoon.com/laravel-under-the-hood-what-are-facades?source=rss
如有侵权请联系:admin#unsafe.sh