Laravel 基础 - 门面

今天查找问题时,突然看到关于 Laravel 中 Facade 一句描述:

其实 Facade 类利用了 __callStatic() 这个魔术方法来延迟调用容器中的对象的方法,这里不过多讲解,你只需要知道Facade实现了将对它调用的静态方法映射到绑定类的动态方法上

瞬间对 Facde 实现原理感兴趣了,平时只是使用 Facade ,大概了解到的是 Facade 通过 Provider 中 register 方法注册到容器中,其中 register 方法中的 key 与 Facade 中方法返回的注册 key 相同即可使用,还没有研究过后面实现原理,更没有考虑如何通过静态方法可以调用实例的静态方法。

这里就通过分析代码了解一下 Facade 实现

简介

门面为应用的服务容器中的绑定类提供了一个"静态"接口。Laravel 内置了很多门面,你可能在不知道的情况下正在使用它们。Laravel 的门面作为服务容器中的底层类的"静态代理",相比于传统静态方法,在维护时能够提供更加易于测试、更加灵活的、简明且富有表现力的语法。

Laravel 的所有门面都定义在 Illuminate\Support\Facades 命名空间下,所以我们可以轻松访问到门面:

use Illuminate\Support\Facades\Cache;

Route::get('/cache', function () {
    return Cache::get('key');
});

在整个Laravel文档中,很多例子使用了门面来演示框架的各种功能特性

何时使用门面

门面有诸多优点,其提供了简单、易记的语法,让我们无需记住长长的类名即可使用Laravel提供的功能特性,此外,由于他们对 PHP 动态方法的独特用法,使得它们很容易测试。

但是,使用门面也有需要注意的地方,一个最主要的危险就是类范围蠕变。由于门面如此好用并且不需要注入,在单个类中使用过多门面,会让类很容易变得越来越大。使用依赖注入则会让此类问题缓解,因为一个巨大的构造函数会让我们很容易判断出类在变大。因此,使用门面的时候要尤其注意类的大小,以便控制其有限职责。

注:构建与 Laravel 交互的第三方扩展包时,最好注入 Laravel 契约而不是使用门面,因为扩展包在 Laravel 之外构建,你将不能访问 Laravel 的门面测试辅助函数。

工作原理

在 Laravel 应用中,门面就是一个为容器中对象提供访问方式的类。该机制原理由 Facade 类实现。Laravel 自带的门面,以及我们创建的自定义门面,都会继承自 Illuminate\Support\Facades\Facade 基类。

门面类只需要实现一个方法:getFacadeAccessor。正是 getFacadeAccessor 方法定义了从容器中解析什么,然后 Facade 基类使用魔术方法 __callStatic() 从你的门面中调用解析对象。

下面通过代码分析 Facade 中 Cookie 使用来分析 Facade 工作原理

<?php

namespace App\Http\Controllers;

use Illuminate\Support\Facades\Cookie;

class CookieTestController extends Controller
{
    public function setCookie( Request $request )
    {
        Cookie::queue( 'wan', 'test', 60, '/', '.onlywan.cc', false, true );
    }
}

注意我们在顶部引用了 Cookie 门面。

下面我们查看 Illuminate\Support\Facades\Cookie 类的源码,会发现其中并没有静态方法 queue

<?php

namespace Illuminate\Support\Facades;

/**
 * @see \Illuminate\Cookie\CookieJar
 */
class Cookie extends Facade
{
    /**
     * Determine if a cookie exists on the request.
     *
     * @param  string  $key
     * @return bool
     */
    public static function has($key)
    {
        return ! is_null(static::$app['request']->cookie($key, null));
    }

    /**
     * Retrieve a cookie from the request.
     *
     * @param  string  $key
     * @param  mixed   $default
     * @return string
     */
    public static function get($key = null, $default = null)
    {
        return static::$app['request']->cookie($key, $default);
    }

    /**
     * Get the registered name of the component.
     *
     * @return string
     */
    protected static function getFacadeAccessor()
    {
        return 'cookie';
    }
}

Cookie 门面继承了 Facade 基类并定义了 getFacadeAccessor 方法,该方法的工作就是返回服务容器绑定类的别名,当用户引用 Cookie 类的任何静态方法时,Laravel 从服务容器中解析 cookie 绑定,然后在解析出的对象上调用所有请求方法。

如何绑定cookie的呢,则是通过 config/app.php 中 providers 的 Illuminate\Cookie\CookieServiceProvider::class 绑定的

<?php

namespace Illuminate\Cookie;

use Illuminate\Support\ServiceProvider;

class CookieServiceProvider extends ServiceProvider
{
    /**
     * Register the service provider.
     *
     * @return void
     */
    public function register()
    {
        $this->app->singleton('cookie', function ($app) {
            $config = $app['config']['session'];

            return (new CookieJar)->setDefaultPathAndDomain($config['path'], $config['domain'], $config['secure']);
        });
    }
}

可以看出来 Facade 中 Cookie 对应绑定的实现类为 Illuminate\Cookie\CookieJar。具体 CookieJar的实现,大家可以自己看源码,下面继续分析如何将静态方法映射到绑定类的动态方法上。

如上面所说,主要是 Facade 的 __callStatic() 魔术方法实现的

__callStatic() 魔术方法作用:当调用当前环境下未定义或不可见的类属性或方法时,该方法会被调用。

下面是Facade 的 __callStatic() 魔术方法实现

/**
* Handle dynamic, static calls to the object.
*
* @param  string  $method
* @param  array   $args
* @return mixed
*
* @throws \RuntimeException
*/
public static function __callStatic($method, $args)
{
   $instance = static::getFacadeRoot();

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

   switch (count($args)) {
       case 0:
           return $instance->$method();
       case 1:
           return $instance->$method($args[0]);
       case 2:
           return $instance->$method($args[0], $args[1]);
       case 3:
           return $instance->$method($args[0], $args[1], $args[2]);
       case 4:
           return $instance->$method($args[0], $args[1], $args[2], $args[3]);
       default:
           return call_user_func_array([$instance, $method], $args);
   }
}

首先获得注册的实例,然后调用实例的 method 方法。

下面是获得实例对象 getFacadeRoot 实现

/**
* Get the root object behind the facade.
*
* @return mixed
*/
public static function getFacadeRoot()
{
   return static::resolveFacadeInstance(static::getFacadeAccessor());
}

其中 getFacadeAccesssor() 即 Cookie 门面中实现的方法,返回绑定名 "cookie"。

resolveFacadeInstance 方法

/**
* Resolve the facade root instance from the container.
*
* @param  string|object  $name
* @return mixed
*/
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];
}

分析代码,如果这个Instance已经在列表中,则直接使用;否则从 $app 获取。如果第一次使用,肯定会从 $app 中获取

查看 $app 权限,权限protected,所以应该是通过 set 方法初始化的。

protected static $app;

/**
* Set the application instance.
*
* @param  \Illuminate\Contracts\Foundation\Application  $app
* @return void
*/
public static function setFacadeApplication($app)
{
   static::$app = $app;
}

剩下就是找 $app 如何初始化的;通过查找发现是 Laravel 框架在初始化 Http Kernel 时初始化的,代码如下

bootstrap/app.php文件中

$app->singleton(
    Illuminate\Contracts\Http\Kernel::class,
    App\Http\Kernel::class
);

App\Http\Kernel

<?php

namespace App\Console;

use Illuminate\Console\Scheduling\Schedule;
use Illuminate\Foundation\Console\Kernel as ConsoleKernel;

class Kernel extends ConsoleKernel
{
    /**
     * The Artisan commands provided by your application.
     *
     * @var array
     */
    protected $commands = [
        // Commands\Inspire::class,
    ];

    /**
     * Define the application's command schedule.
     *
     * @param  \Illuminate\Console\Scheduling\Schedule  $schedule
     * @return void
     */
    protected function schedule(Schedule $schedule)
    {
        // $schedule->command('inspire')
        //          ->hourly();
    }
}

ConsoleKernel基类中 $bootstrappers 数组中

protected $bootstrappers = [
   // ...
   'Illuminate\Foundation\Bootstrap\RegisterFacades',
   // ...
];

Illuminate\Foundation\Bootstrap\RegisterFacades

<?php

namespace Illuminate\Foundation\Bootstrap;

use Illuminate\Support\Facades\Facade;
use Illuminate\Foundation\AliasLoader;
use Illuminate\Contracts\Foundation\Application;

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

这里看到了设置 $app ,这里的 $app 即为 Laravel 框架的核心:容器类Illuminate\Contracts\Foundation\Application

总结

到这里 Laravel 的 Facade 也算是分析完了,通过了解原理,方便后面自己设计实现时参考使用,而具体使用起来相对于依赖注入及辅助函数等的优缺点还得在后面项目中通过使用进行总结。