Laravel Facade原理及使用

Laravel Facade原理及使用

laravel过于庞大,加之笔者水平有限,所以后面的源码解读会按模块功能介绍,希望能帮大家稍微捋顺下思路,即使能够帮助大家回顾几个函数也好。如发现错误,还望指正。

  • facade工作方式,允许我们可以通过静态调用的方式直接使用容器中的服务
  • 原理讲解,在laravel的routes/web.php等路由文件下,经常可以看到类似的写法
<?php
Route::get('zbc', function () {
    app()->make(App\Http\Controllers\Zbc\TestController::class);
});
// 可以看到通过Route::get方法添加了此路由规则(不仅如此laravel存在大量这般的静态调用方式),但是并没有看到此文件中引用Route类,并且laravel框架中并没有此Route类,
// 通过打印get_declared_classes确实存在此Route(route)类,只有一个解释,那就是laravel引导过程中‘生成了’这个类,下面就会讲解如何‘生成’的这个类,这个类是根据什么‘生成’的。
  • 上文讲到在index.php中拿到了laravel的’黑盒子‘$kernel,下面继续看kernel的handle方法
// Illuminate\Foundation\Http\Kernel文件中
/**
 * Handle an incoming HTTP request.
 *
 * @param  \Illuminate\Http\Request  $request
 * @return \Illuminate\Http\Response
 */
public function handle($request)
{	
    // 这里的方法大家看名字就能大概知道什么用处,本问只讲解sendRequestThroughRouter中的facade注册部分
    try {
        $request->enableHttpMethodParameterOverride();
		// 通过路由或者中间件处理给定的请求
        // 跳转到sendRequestThroughRouter方法
        $response = $this->sendRequestThroughRouter($request);
    } catch (Exception $e) {
        $this->reportException($e);

        $response = $this->renderException($request, $e);
    } catch (Throwable $e) {
        $this->reportException($e = new FatalThrowableError($e));

        $response = $this->renderException($request, $e);
    }

    $this->app['events']->dispatch(
        new RequestHandled($request, $response)
    );

    return $response;
}

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

    // 引导app
    // 跳转到bootstrap方法
    $this->bootstrap();
	
    // laravel的pipeline以后会专门讲解
    return (new Pipeline($this->app))
        ->send($request)
        ->through($this->app->shouldSkipMiddleware() ? [] : $this->middleware)
        ->then($this->dispatchToRouter());
}

// $this->app是make的时候通过构造方法注入进来的(参考上篇文章)
public function bootstrap()
{   
    if (! $this->app->hasBeenBootstrapped()) {
        // 如果laravel不能存在引导完成标志,就进行引导
        // 跳转到bootstrapWith方法,传递的参数如下
        // protected $bootstrappers = [
        //     \Illuminate\Foundation\Bootstrap\LoadEnvironmentVariables::class,
        //     \Illuminate\Foundation\Bootstrap\LoadConfiguration::class,
        //     \Illuminate\Foundation\Bootstrap\HandleExceptions::class,
        //     我们的facade'生成'(注册)就在此完成
        //     \Illuminate\Foundation\Bootstrap\RegisterFacades::class,
        //     \Illuminate\Foundation\Bootstrap\RegisterProviders::class,
        //     \Illuminate\Foundation\Bootstrap\BootProviders::class,
    	// ];
        $this->app->bootstrapWith($this->bootstrappers());
    }
}

// Application下的bootstrapWith方法
public function bootstrapWith(array $bootstrappers)
{   
    $this->hasBeenBootstrapped = true;

    foreach ($bootstrappers as $bootstrapper) {
        $this['events']->dispatch('bootstrapping: ' . $bootstrapper, [$this]);
        // 本文只讲解facaderegister
        // 所以此处应该的$$bootstrapper = Illuminate\Foundation\Bootstrap\RegisterFacades
        // 如果看过前面的文章可以知道容器并没有绑定过此abstract更不可能存在解析过的instance
        // 所以容器的make方法走的一定是php的反射机制,然后调用bootstrap方法
        // 跳转到RegisterFacades的bootstrap方法
        $this->make($bootstrapper)->bootstrap($this);

        $this['events']->dispatch('bootstrapped: ' . $bootstrapper, [$this]);
    }
}

public function bootstrap(Application $app)
{
    Facade::clearResolvedInstances();

    Facade::setFacadeApplication($app);
	
    // 跳转到getInstance方法
    AliasLoader::getInstance(array_merge(
        // 拿到config/app.php下的aliases数组 如下
        // 'App' => Illuminate\Support\Facades\App::class,
        // 'Arr' => Illuminate\Support\Arr::class,
        // ...
        // 'Route' => Illuminate\Support\Facades\Route::class,
        // ...
        // 'Validator' => Illuminate\Support\Facades\Validator::class,
        // 'View' => Illuminate\Support\Facades\View::class,
        $app->make('config')->get('app.aliases', []),
        // 需要修改composer.json文件 配合包自动发现 这个类就是这个用处的
        $app->make(PackageManifest::class)->aliases() 
    ))->register();
}

// 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;
}

// 继续看register方法
/**
 * Register the loader on the auto-loader stack.
 *
 * @return void
 */
public function register()
{
    if (!$this->registered) {
        // 继续跳转prependToLoaderStack到方法
        $this->prependToLoaderStack();

        $this->registered = true;
    }
}

// 可以看到此方法在加载函数队列首部添加了一个load加载函数
// spl_autoload_register方法的参数在composer第一篇有讲解
protected function prependToLoaderStack()
{	
    // 跳转到load方法
    spl_autoload_register([$this, 'load'], true, true);
}

/**
 * Load a class alias if it is registered.
 *
 * @param  string  $alias
 * @return bool|null
 */
public function load($alias)
{   
    if (static::$facadeNamespace && strpos($alias, static::$facadeNamespace) === 0) {
        $this->loadFacade($alias);
        return true;
    }
	
    // 由于
    if (isset($this->aliases[$alias])) {
        // 重点!!!
        // 在此案例中$alias传递进来的是我们在路由文件web.php中使用的Route
        return class_alias($this->aliases[$alias], $alias);
    }
    
    // 为了方便理解,可做如下简单修改
    // config/app.php的aliases数组中存在Route映射 条件表达式为true
    if (isset($this->aliases[$alias])) {
        dump($alias);	// 打印为Route,就是我们在web.php中使用的Route
        dump(get_declared_classes()); // 一堆类名组成的数组 但是不包括route
        // 关键在此函数 class_alias 第一个参数是original class 第二个参数是给这个类起的别名
        // 最重要的第三个参数默认为true 表示如果原类没找到 是否可以通过别名自动加载这个类
        // class_alias返回bool
        // 返回的只是bool值,load加载器并没有真正的引入实际的Illuminate\Support\Facades\Route类
        // 所以php会继续调用composer的加载器真正的加载此类,此加载器只是取巧的设置了别名,方便使用
        class_alias($this->aliases[$alias], $alias);
        dump(get_declared_classes()); // 一堆类名组成的数组 但是包括了route
        return true;
    }
}

// 下面看Illuminate\Support\Facades\Route类
// 非常简单只有一个方法,并没有发现web.php中的Route::get方法,便会触发php的魔术方法__callStatic(请在自省查阅手册),这是laravel facade静态调用实现的根本方式
// 跳转到父类Facade
class Route extends Facade
{
    /**
     * Get the registered name of the component.
     *
     * @return string
     */
    protected static function getFacadeAccessor()
    {
        return 'router';
    }
}


// Illuminate\Support\Facades\Facade类
public static function __callStatic($method, $args)
{	
    // 使用static关键字,实现延迟绑定,此案例中代表Illuminate\Support\Facades\Route
    // 跳转到getFacadeRoot方法
    $instance = static::getFacadeRoot();

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

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

/**
 * Get the root object behind the facade.
 *
 * @return mixed
 */
// 官方注释已经很完美了,获取门面背后的真实对象
public static function getFacadeRoot()
{   
    // 注意使用的是static 静态延迟绑定 此案例中代表Illuminate\Support\Facades\Route
    // 跳转到resolveFacadeInstance方法
    return static::resolveFacadeInstance(static::getFacadeAccessor());
    // return static::resolveFacadeInstance('router');
}

/**
 * Resolve the facade root instance from the container.
 *
 * @param  object|string  $name
 * @return mixed
 */
// 从容器中解析门面对应的根对象
protected static function resolveFacadeInstance($name)
{
    if (is_object($name)) {
        return $name;
    }

    if (isset(static::$resolvedInstance[$name])) {
        return static::$resolvedInstance[$name];
    }
	
    if (static::$app) {
        // $app为Application对象但是为什么采用数组的形式进行访问呢($app['router'])
        // 因为Application类继承了Container类,而Container实现了spl类库提供的ArrayAccess接口
        // 关于ArrayAccess请自行查阅文档
        // 当通过数组的形式访问对象的时候 会触发offsetGet方法,跳转到Container的offsetGet方法
        return static::$resolvedInstance[$name] = static::$app[$name];
    }
}

/**
 * Get the value at a given offset.
 *
 * @param  string  $key
 * @return mixed
 */
public function offsetGet($key)
{	
    // $key在此案例中等于router
    // 通过容器进行解析router
    // router在Application中实例化的registerBaseServiceProvider中实现的注册,前面的文章有讲解
    // 一路返回到__callStatic方法中
    return $this->make($key);
}

public static function __callStatic($method, $args)
{	
    // 使用static关键字,实现延迟绑定,此案例中代表Illuminate\Support\Facades\Route
    // 跳转到getFacadeRoot方法
    $instance = static::getFacadeRoot();

    if (! $instance) {
        throw new RuntimeException('A facade root has not been set.');
    }
	// 到此调用$router的get方法 完美!!!
    // 对应的方法看这里 Illuminate\Routing\Router::get
    return $instance->$method(...$args);
}

以上便是laravel facade的基本实现方式,大家可能已经看出来这就是php实现门面(代理)模式的方法。

个人不喜欢使用门面模式,更喜欢直接调用系统函数等方式直接从容器中解析对象,感觉可以规避一下__callStatic的调用。

下面讲解如何在laravel中使用自己的facade

1. 创建契约
<?php

namespace App\Contracts;

interface MyFacade
{
    public function thereYouGo();
}

2. 创建服务
<?php

namespace App\Services;

use App\Contracts\MyFacade;

class TestService implements MyFacade
{
    public function thereYouGo()
    {
        echo '春风习习可盈袖, 不及伊人半点红';
    }
}

3. 创建服务提供者 php artisan make:provider MyFacadeProvider
<?php

namespace App\Providers;

use Illuminate\Support\ServiceProvider;
use App\Services\TestService;

class MyFacadeProvider extends ServiceProvider
{
    /**
     * Register services.
     *
     * @return void
     */
    public function register()
    {	
        // 注意此处的$abstract(MyLove)要和facade中getFacadeAccessor方法返回值一致
        $this->app->bind('MyLove', function () {
            return new TestService();
        });
    }

    /**
     * Bootstrap services.
     *
     * @return void
     */
    public function boot()
    {
        //
    }
}

4. 创建门面
<?php

namespace App\Facades;

use Illuminate\Support\Facades\Facade;

class MyFacade extends Facade
{
    protected static function getFacadeAccessor()
    {   
        return 'MyLove';
    }
}

5. 注册服务和门面 config/app.php下添加
'providers' => [
	...
    App\Providers\MyFacadeProvider::class,
],
'aliases' => [
    ...
    'MyFacade' => App\Services\TestService::class,
]
   
6. 测试
use App\Facades\MyFacade;
Route::get('myfacade', function () {
    MyFacade::thereYouGo();
});

可以看到实现一个facade真的费时费力,并且性能不好,不建议自行创建facade使用,更建议使用容器直接解析,当然硬编码可能更适合追求速度的开发,不管怎样开心撸码最重要。

今天没有下集预告

posted @ 2020-08-03 16:03  alwayslinger  阅读(2336)  评论(0编辑  收藏  举报