Facades

未匹配的标注

什么是 Facades#

Facades 是我们在 Laravel 应用开发中使用频率很高的一个组件,叫组件不太合适,其实它们是一组静态类接口或者说代理,让开发者能简单的访问绑定到服务容器里的各种服务。Laravel 文档中对 Facades 的解释如下:

Facades 为应用程序的 服务容器 中可用的类提供了一个「静态」接口。Laravel 本身附带许多的 facades,甚至你可能在不知情的状况下已经在使用他们!Laravel 「facades」作为在服务容器内基类的「静态代理」,拥有简洁、易表达的语法优点,同时维持着比传统静态方法更高的可测试性和灵活性。

我们经常用的 Route 就是一个 Facade, 它是 \Illuminate\Support\Facades\Route 类的别名,这个 Facade 类代理的是注册到服务容器里的 router 服务,所以通过 Route 类我们就能够方便地使用 router 服务中提供的各种服务,而其中涉及到的服务解析完全是隐式地由 Laravel 完成的,这在一定程度上让应用程序代码变的简洁了不少。下面我们会大概看一下 Facades 从被注册进 Laravel 框架到被应用程序使用这中间的流程。Facades 是和 ServiceProvider 紧密配合的所以如果你了解了中间的这些流程对开发自定义 Laravel 组件会很有帮助。

注册 Facades#

说到 Facades 注册又要回到再介绍其它核心组建时提到过很多次的 Bootstrap 阶段了,在让请求通过中间件和路由之前有一个启动应用程序的过程:

//Class: \Illuminate\Foundation\Http\Kernel

protected function sendRequestThroughRouter($request)
{
    $this->app->instance('request', $request);

    Facade::clearResolvedInstance('request');

    $this->bootstrap();

    return (new Pipeline($this->app))
                    ->send($request)
                    ->through($this->app->shouldSkipMiddleware() ? [] : $this->middleware)
                    ->then($this->dispatchToRouter());
}

//引导启动Laravel应用程序
public function bootstrap()
{
    if (! $this->app->hasBeenBootstrapped()) {
     /**依次执行$bootstrappers中每一个bootstrapper的bootstrap()函数
         $bootstrappers = [
                 'Illuminate\Foundation\Bootstrap\DetectEnvironment',
                 'Illuminate\Foundation\Bootstrap\LoadConfiguration',
                 'Illuminate\Foundation\Bootstrap\ConfigureLogging',
                 'Illuminate\Foundation\Bootstrap\HandleExceptions',
                 'Illuminate\Foundation\Bootstrap\RegisterFacades',
                 'Illuminate\Foundation\Bootstrap\RegisterProviders',
                 'Illuminate\Foundation\Bootstrap\BootProviders',
            ];*/
            $this->app->bootstrapWith($this->bootstrappers());
    }
}

在启动应用的过程中 Illuminate\Foundation\Bootstrap\RegisterFacades 这个阶段会注册应用程序里用到的 Facades。

class RegisterFacades
{
    /**
     * Bootstrap the given application.
     *
     * @param  \Illuminate\Contracts\Foundation\Application  $app
     * @return void
     */
    public function bootstrap(Application $app)
    {
        Facade::clearResolvedInstances();

        Facade::setFacadeApplication($app);

        AliasLoader::getInstance(array_merge(
            $app->make('config')->get('app.aliases', []),
            $app->make(PackageManifest::class)->aliases()
        ))->register();
    }
}

在这里会通过 AliasLoader 类的实例将为所有 Facades 注册别名,Facades 和别名的对应关系存放在 config/app.php 文件的 $aliases 数组中

'aliases' => [

    'App' => Illuminate\Support\Facades\App::class,
    'Artisan' => Illuminate\Support\Facades\Artisan::class,
    'Auth' => Illuminate\Support\Facades\Auth::class,
    ......
    'Route' => Illuminate\Support\Facades\Route::class,
    ......
]

看一下 AliasLoader 里是如何注册这些别名的

// class: Illuminate\Foundation\AliasLoader
public static function getInstance(array $aliases = [])
{
    if (is_null(static::$instance)) {
        return static::$instance = new static($aliases);
    }

    $aliases = array_merge(static::$instance->getAliases(), $aliases);

    static::$instance->setAliases($aliases);

    return static::$instance;
}

public function register()
{
    if (! $this->registered) {
        $this->prependToLoaderStack();

        $this->registered = true;
    }
}

protected function prependToLoaderStack()
{
    // 把AliasLoader::load()放入自动加载函数队列中,并置于队列头部
    spl_autoload_register([$this, 'load'], true, true);
}

通过上面的代码段可以看到 AliasLoader 将 load 方法注册到了 SPL __autoload 函数队列的头部。看一下 load 方法的源码:

public function load($alias)
{
    if (isset($this->aliases[$alias])) {
        return class_alias($this->aliases[$alias], $alias);
    }
}

在 load 方法里把 $aliases 配置里的 Facade 类创建了对应的别名,比如当我们使用别名类 Route 时 PHP 会通过 AliasLoader 的 load 方法为 Illuminate\Support\Facades\Route 类创建一个别名类 Route,所以我们在程序里使用别 Route 其实使用的就是 Illuminate\Support\Facades\Route 类。

解析 Facade 代理的服务#

把 Facades 注册到框架后我们在应用程序里就能使用其中的 Facade 了,比如注册路由时我们经常用 Route::get('/uri', 'Controller@action);,那么 Route 是怎么代理到路由服务的呢,这就涉及到在 Facade 里服务的隐式解析了, 我们看一下 Route 类的源码:

class Route extends Facade
{
    /**
     * Get the registered name of the component.
     *
     * @return string
     */
    protected static function getFacadeAccessor()
    {
        return 'router';
    }
}

只有简单的一个方法,并没有 get, post, delete 等那些路由方法,父类里也没有,不过我们知道调用类不存在的静态方法时会触发 PHP 的__callStatic 静态方法

public static function __callStatic($method, $args)
{
    $instance = static::getFacadeRoot();

    if (! $instance) {
        throw new RuntimeException('A facade root has not been set.');
    }

    return $instance->$method(...$args);
}

//获取Facade根对象
public static function getFacadeRoot()
{
    return static::resolveFacadeInstance(static::getFacadeAccessor());
}

/**
 * 从服务容器里解析出Facade对应的服务
 */
protected static function resolveFacadeInstance($name)
{
    if (is_object($name)) {
        return $name;
    }

    if (isset(static::$resolvedInstance[$name])) {
        return static::$resolvedInstance[$name];
    }

    return static::$resolvedInstance[$name] = static::$app[$name];
}

通过在子类 Route Facade 里设置的 accessor (字符串 router), 从服务容器中解析出对应的服务,router 服务是在应用程序初始化时的 registerBaseServiceProviders 阶段(具体可以看 Application 的构造方法)被 \Illuminate\Routing\RoutingServiceProvider 注册到服务容器里的:

class RoutingServiceProvider extends ServiceProvider
{
    /**
     * Register the service provider.
     *
     * @return void
     */
    public function register()
    {
        $this->registerRouter();
        ......
    }

    /**
     * Register the router instance.
     *
     * @return void
     */
    protected function registerRouter()
    {
        $this->app->singleton('router', function ($app) {
            return new Router($app['events'], $app);
        });
    }
    ......
}

router 服务对应的类就是 \Illuminate\Routing\Router, 所以 Route Facade 实际上代理的就是这个类,Route::get 实际上调用的是 \Illuminate\Routing\Router 对象的 get 方法

/**
 * Register a new GET route with the router.
 *
 * @param  string  $uri
 * @param  \Closure|array|string|null  $action
 * @return \Illuminate\Routing\Route
 */
public function get($uri, $action = null)
{
    return $this->addRoute(['GET', 'HEAD'], $uri, $action);
}

补充两点:

  1. 解析服务时用的 static::$app 是在最开始的 RegisterFacades 里设置的,它引用的是服务容器。

  2. static::$app ['router']; 以数组访问的形式能够从服务容器解析出 router 服务是因为服务容器实现了 SPL 的 ArrayAccess 接口,对这个没有概念的可以看下官方文档 ArrayAccess

总结#

通过梳理 Facade 的注册和使用流程我们可以看到 Facade 和服务提供器(ServiceProvider)是紧密配合的,所以如果以后自己写 Laravel 自定义服务时除了通过组件的 ServiceProvider 将服务注册进服务容器,还可以在组件中提供一个 Facade 让应用程序能够方便的访问你写的自定义服务。

本文章首发在 LearnKu.com 网站上。

上一篇 下一篇
《L02 从零构建论坛系统》
以构建论坛项目 LaraBBS 为线索,展开对 Laravel 框架的全面学习。应用程序架构思路贴近 Laravel 框架的设计哲学。
《G01 Go 实战入门》
从零开始带你一步步开发一个 Go 博客项目,让你在最短的时间内学会使用 Go 进行编码。项目结构很大程度上参考了 Laravel。
讨论数量: 0
发起讨论 查看所有版本


暂无话题~