Laravel Facade 實現原理揭祕

在使用Laravel 框架的時候會看到很多 Cache::get() 這樣的用法,稱之爲 Facade,門面。
但是代碼中即沒有看到使用 Cache 相關的命名空間,且在 Composer 自動加載中也沒有相關的自動加載規則。那這是如何實現的呢?讓我們從框架源碼去發現。

Laravel 的入口文件是 public/index.PHP,此文件載入了 autoload.php, app.php 2個文件:

require __DIR__.'/../bootstrap/autoload.php';  
$app = require_once __DIR__.'/../bootstrap/app.php';  

顧名思義 autoload.php 實現了自動加載,app.php 和容器相關。
初始化容器的過程這裏不詳細解說,不是本文重點。

==============================重點來了================================

初始化容器後,執行了以下代碼:

// 得到 App\Http\Kernel 實例對象  
$kernel = $app->make(Illuminate\Contracts\Http\Kernel::class);  

// 執行對象handle 方法,此方法繼承自 Illuminate\Foundation\Http\Kernel  
$response = $kernel->handle(  
    $request = Illuminate\Http\Request::capture()  
);  

讓我們去看下 handle() 做了些什麼:

public function handle($request)  
{  
    //......省略......  
    $response = $this->sendRequestThroughRouter($request);  
    //......省略......  
}  
sendRequestThroughRouter 方法:
[php] view plain copy print?
protected function sendRequestThroughRouter($request)  
{  
    //......省略......  
    // 啓動一些啓動器,諸如異常處理,配置,日誌,Facade,運行環境監測等  
    $this->bootstrap();  
    //......省略......  
}  

bootstrap 方法:

public function bootstrap()  
{  
    if (! $this->app->hasBeenBootstrapped()) {  
       $this->app->bootstrapWith($this->bootstrappers());  
    }  
}  
$this->bootstrappers() 中返回 $this->bootstrappers 保存的數據:
[php] view plain copy print?
protected $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',  
];  

可以看到 :

'Illuminate\Foundation\Bootstrap\RegisterFacades',  

此類就是實現 Facade 的一部分,bootstrap 方法中 $this->app->bootstrapWith 會調用類的 bootstrap 方法:

class RegisterFacades  
{  
    public function bootstrap(Application $app)  
    {  
        //......省略......  
        AliasLoader::getInstance($app->make('config')->get('app.aliases'))->register();  
    }  
} 

$app->make(‘config’)->get(‘app.aliases’) 返回的是 config/app.php 配置文件中 ‘aliases’ 鍵對應的值,
我們繼續往下看 AliasLoader::getInstance 方法:

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

默認情況下 static::instancenullnewstatic( aliases)。
new static 是一個自 5.3以後的新特性,參見:https://secure.php.net/manual/zh/language.oop5.late-static-bindings.php
回頭再看

AliasLoader::getInstance($app->make('config')->get('app.aliases'))->register();  

中調用了 AliasLoader->register 方法:

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

        $this->registered = true;  
    }  
}  

prependToLoaderStack 方法:
這裏註冊了當前對象中 load 方法爲自動加載函數

protected function prependToLoaderStack()  
{  
    spl_autoload_register([$this, 'load'], true, true);  
}  

load 方法:

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

這裏的 this>aliasesAliasLoader:getInstancenewstatic( aliases) 時構造函數中設置的:

private function __construct($aliases)  
{  
    $this->aliases = $aliases;  
}  

這裏 class_alias 是實現 Facade 的核心要點之一,該函數原型:

bool class_alias ( string $original, string $alias[, bool $autoload = TRUE ] )  

第三個參數默認爲 true,意味着如果原始類(string $original)沒有加載,則自動加載。
更多該函數的解釋請自行翻閱手冊。

現在可以解答爲什麼 Cache 沒有使用命名空間,composer 沒有設置自動加載的情況可以立即使用了:
自動加載函數會嘗試加載 Cache 這個別名對應的實際類所在的文件(加載這個類文件時會去調用SPL自動加載函數隊列中其他函數,比如 composer 自動加載函數,並遵從規則)

接下來說一下 Cache::get() 中爲什麼可以像靜態方法一樣調用任何類的方法(哪怕不是靜態方法)。
找到並打開 Cache 類文件:vendor/laravel/framework/src/Illuminate/Support/Facades/Cache.php
文件內容是這樣的:

namespace Illuminate\Support\Facades;  
/** 
 * @see \Illuminate\Cache\CacheManager 
 * @see \Illuminate\Cache\Repository 
 */  
class Cache extends Facade  
{  
    /** 
     * Get the registered name of the component. 
     * 
     * @return string 
     */  
    protected static function getFacadeAccessor()  
    {  
        return 'cache';  
    }  
}  

看一下 父類 Illuminate\Support\Facades,發現父類中實現了魔術方法 __callStatic:

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

謎底揭開了,原來是通過魔術方法去實現的。

簡單的寫了一段代碼,用於實現類似 Facade 的調用方式:

namespace Illuminate\Support\Facades {  

    class Facades {  
        public function __call($name, $params) {  
            return call_user_func_array([$this, $name], $params);  
        }   
        public static function __callStatic($name, $params) {  
            return call_user_func_array([new static(), $name], $params);  
        }    
    }  

    class Cache extends Facades {  
        protected function fn($a, $b) {  
            echo "function parameters: ${a} and ${b}<br>";      
        }  
        protected function static_fn($a, $b) {  
            echo "static function parameters: ${a} and ${b}<br>";        
        }  
    }  

}  
namespace {  

    class Autoload {  
        public $aliases;  
        public function __construct($aliases = []) {  
            $this->aliases = $aliases;  
        }  
        public function register() {  
            spl_autoload_register([$this, 'load'], true, true);  
            return $this;  
        }  
        public function load($alias) {  
            if (isset($this->aliases[$alias])) {  
                return class_alias($this->aliases[$alias], $alias);  
            }      
        }  
    }  

    $aliases = [  
        'Cache' => Illuminate\Support\Facades\Cache::class,  
    ];  
    $autoloader = (new Autoload($aliases))->register();  
    Cache::fn(3,6);  
    Cache::static_fn(4,7);  

}  
發佈了26 篇原創文章 · 獲贊 62 · 訪問量 13萬+
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章