深入底層之實現 Laravel 路由註冊功能

Dennis_Ritchie發表於2019-11-28

感慨

學習總是那麼的不容易,希望讓兄弟們領略Laravel程式設計技巧,走進Laravel深處的我更不容易,每次給大家寫程式碼,我總是思考這麼一個問題,如何能讓大家明白我想讓你們明白的東西,不可否認這是一件非常困難的事,所以我只能感慨:我太難了。完成這篇博文已經是2019年11月28日凌晨2點了,我也要睡了。

終極目標

之前寫了一篇博文《大家對 Laravel 的原始碼和架構感興趣麼?》,相信很多人已經看過了,也都表示了極大的興趣,這也是我對大家的承諾,所以從上一篇博文《詳解 PHP 反射的基本使用》開始,我就準備給大家講解Laravel的相關知識,講解Laravel的程式碼非常不容易,需要閱讀者具備相當的程式設計能力,對設計模式有一定的認識和頑強的毅力,最後但並不是最不重要的就是極大的好奇心。所以為了大家能夠讀懂,我會給大家編寫功能和Laravel相當的程式,也可以說是簡化版,這樣最有助於大家的理解,如果大家理解了我寫的,日後再來閱讀Laravel,將會輕車熟路。

本節目標

在本篇博文中,我將會給大家實現一個類似Laravel路由註冊的功能,這個功能在Laravel當中非常重要,希望大家能夠理解,程式碼我已經上傳到了碼雲laravel-route-register,這是我在下班之後給大家寫的,實屬不易,希望大家珍惜,倉庫程式碼截圖如下:

老司機帶你實現Laravel路由註冊功能

這個debug.php檔案的測試程式碼如下:

老司機帶你實現Laravel路由註冊功能

上圖就是我們今天所要實現的功能,這個巢狀是完全沒有限制的,你可以任意組合api,希望大家把原始碼下載下來,再配合我寫的這篇博文,一定要讀懂它,這是你理解任何PHP框架所不可或缺的必要步驟。

閱讀準備

在閱讀以下的內容之前,請先參考laravel路由相關文件,Laravel路由,首先搞清楚Laravel註冊路由的可操作API。

檔案簡介

上面我已經貼出了這個包的幾個檔案,下面我會仔細的講解下列檔案:

  1. Router檔案是我們的路由器檔案,我們註冊路由就是通過它進行的,比如它給我們提供的api有:get,post,where,namespace,prefix,這幾個方法是我們操縱路由器的介面。
  2. RouteRegistrar檔案,中文翻譯過來就是:路由註冊商,這個檔案的內容很簡單,它的作用是提供給我們路由巢狀的能力,之所以group方法能夠做到路由巢狀,就是因為這個類和Router檔案相互作用的結果,只憑它是無法做到這一點的。
  3. Route檔案,每一條路由實際上都是一個Route類的物件,所以你明白它的意思了吧?

程式碼講解

在以後的程式碼講解中,我都不會貼出程式碼的完整版,因為這樣很影響博文的感官體驗,幾頁都是程式碼,所以希望大家下載下來,不然你讀這篇博文沒啥意義,真的沒啥用。

首先從最簡單的開始吧!

Router有個靜態的get方法,這個方法就對應著get請求了,如下:

public static function get($rule, $action)
{
    $route = self::newRoute(self::REQUEST_METHOD_GET, $rule, $action);
    self::getInstance()
        ->addRouteToContainer($route);
    return $route;
}

REQUEST_METHOD_GET是我定義的常量為"GET",get方法的第一個引數為路由規則,第二個引數為你的控制器或者是回撥方法,比如:

Router::get('dashboard', 'DashboardController@index');
Router::get('/call', function () {
});

get方法首先呼叫newRoute方法,如下:

private static function newRoute($method, $rule, $action)
{
    $inst = self::getInstance();
    if (!empty($inst->groupStack)) {
        $attributes = end($inst->groupStack);
        $rule = isset($attributes['prefix']) ?
            rtrim($attributes['prefix'], '/') . '/' . $rule : $rule;
        if (is_string($action) && isset($attributes['namespace'])) {
            $action = trim($attributes['namespace'], '\\') . '\\' . $action;
        }

    } else {
        $attributes = [];
    }
    $route = new Route($method, $rule, $action);
    if (isset($attributes['where'])) {
        $route->where($attributes['where']);
    }
    return $route;
}

因為在我們的程式中,Router例項一般都是單例的,所以我把它的構造器宣告為私有的,也就是外部無法呼叫它,getInstance方法如下:

public static function getInstance()
{
    if (!self::$instance) {
        self::$instance = new self();
    }
    return self::$instance;
}

這個方法非常簡單,我們回到方法newRoute中,它首先檢查groupStack屬性是否為空,這個屬性是幹啥的呢?我可以提前告訴大家,之所以我們可以實現group操作,它的功勞功不可沒,這裡我們先假設它的值為空(後面分析group方法的時候,我們再來看它),那麼接下來往下走就建立了一個Route物件了,最後的isset檢查也是和group有關,我們後面再分析,newRoute粗略分析完成,我們回到get方法中,呼叫Router例項物件的addRouteToContainer方法新增到容器中。

private function addRouteToContainer(Route $route)
{
    $this->routeCollection[] = $route;
}

post方法和get方法基本是一樣的,我們不在分析。

這裡給大家一個使用Phpstorm的技巧,如果你的類提供了動態的方法(__call或者__callStatic提供的方法),你想讓Phpstorm給你程式碼提示,你可以按我下面的方法操作:

老司機帶你實現Laravel路由註冊功能

比如我的Router類,它提供了三個靜態方法where,namespace和prefix,我就在Router類的上面寫三個@method ,如果是靜態方法的話,加上static字首,緊接著寫函式的返回值型別,然後方法名,最後括號加上引數名,這是常用的技巧,希望大家熟知。

我們再來看Router類

public static function __callStatic($name, $arguments)
{
    if (in_array($name, ['where', 'namespace', 'prefix'])) {
        return (new RouteRegistrar(self::getInstance()))->$name($arguments[0]);
    } else {
        throw new RuntimeException("method ${name} not exist");
    }
}

它實現了callStatic方法,這就是我們能夠呼叫where,namespace和prefix這三個靜態方法的原因。對callStatic不熟悉的兄弟,可以這樣理解,當你呼叫一個類A的靜態方法show時,如果這個靜態方法show不存在,那麼就會呼叫callStatic方法,第一個引數是你的靜態方法名show,第二個引數 $arguments是一個陣列,包含你傳遞給靜態方法show的所有引數,比如你呼叫A::show(1,2,3,4),那麼 $arguments就是[1,2,3,4],是不是很簡單。進入到callStatic方法中,首先判斷如果我們呼叫的靜態方法為'where', 'namespace', 'prefix'三個中的一個,那麼會返回一個RouteRegistrar類的物件,這個類的實現很簡單,如下:

<?php

class  RouteRegistrar
{
    /**
     * @var $router Router
     * **/
    private $router;

    private $attributes = [];

    public function __construct(Router $router)
    {
        $this->router = $router;
    }

    public function where($name, $value = null)
    {
        if (is_array($name)) {
            foreach ($name as $key => $value) {
                $this->attributes['where'][$key] = $value;
            }
        } else {
            $this->attributes['where'][$name] = $value;
        }
        return $this;
    }

    public function namespace($namespace)
    {
        $this->attributes['namespace'] = $namespace;
        return $this;
    }

    public function prefix($prefix)
    {
        $this->attributes['prefix'] = $prefix;
        return $this;
    }

    public function group(callable $callback)
    {
        $this->router->group($this->attributes, $callback);
        return $this;
    }

}

它的構造方法接受我們的Router物件例項,這個屬性$router屬性會在後面的group方法中用到,後面會將,回到__callStatic方法中,我們解釋一下這個程式碼:

(new RouteRegistrar(self::getInstance()))->$name($arguments[0]);

我要給大家解釋的是,php中的變數名是可以作為方法名進行呼叫的,具體呼叫的哪個方法取決於你的變數的值,比如這裡的$name是where,那麼就是呼叫RouteRegistrar的where方法了,這個大家應該很容易理解,RouteRegistrar類的所有方法的作用和Laravel是一樣的,比如where設定引數的約束,比如對於路由 /admin/order/id :

->where('id', '\d{1,2}')

namespace是用來設定控制器名稱空間字首的,
比如下面這個給控制器OrderController加上Admin\Controller名稱空間字首,就成為了Admin\Controller\OrderController:

->namespace("Admin\\Controller")

prefix是設定路由字首的,比如你有一個/order的路由,那麼prefix為admin的話,路由就變為了/admin/order了:

->prefix("admin")

上面已經詳細講述了where,prefix和namespace的作用,相信大家理解起來沒啥問題了把,還有一點需要記住的是,上面這三個方法都把接受的值儲存在了RouteRegistrar類的attributes屬性中,這個屬性後面會用到,下面我們再來看group方法,從上面的Router的__callStatic方法,我們知道group方法是不能直接被我們使用的,我們要使用group方法只能先呼叫where,prefix和namespace三個動態方法中的一個,前面已經分析過了,他們會返回RouteRegistrar類的例項。

現在我們再來分析group方法,在分析之前,我們把我們們的測試例子貼出來,接下來的分析都是以這段程式碼進行分析,為了大家理解,這麼做了:

Router::where('name', '[a-z]+')
    ->where('id', '\d{1,2}')
    ->prefix("admin")
    ->namespace("Admin\\Controller")
    ->group(function (Router $router) {
        Router::get('dashboard', 'DashboardController@index');
        Router::prefix("order")
            ->group(function () {
                Router::post('add', 'OrderController@add');
                Router::post('index', 'OrderController/index');
            });
    });

前面分析過where,prefix和namespace會把值儲存在attributes中,具體請看上面的程式碼,很簡單,最終當前的RouteRegistrar例項的attributes就是下面這樣:

$arrtibuets = [
    'where' => [
        'id' => '\d{1,2}',
        'name' => '[a-z]+'
    ],
    'prefix' => 'admin',
    'namespace' => 'Admin\\Controller'
];

最外層的最後一個方法是group方法,我們進入到RouteRegistrar類的group方法中:

public function group(callable $callback)
{
    $this->router->group($this->attributes, $callback);
    return $this;
}

這裡它只是呼叫了Router例項的group方法,注意了,我們不能直接呼叫Router例項的group方法,它只是被RouteRegistrar的group方法呼叫,$this->attributes就是我們上面的分析結果,我們進入到Router的group方法中:

public function group($attributes, callable $callback)
{
    $this->updateGroupStack($attributes);
    $callback(self::getInstance());
    array_pop($this->groupStack);
}

這裡首先呼叫updateGroupStack方法,我們來看一下:

private function updateGroupStack(array $attributes)
{
    if (!empty($this->groupStack)) {
        $new_attributes = [];
        $last_attribute = end($this->groupStack);
        $new_attributes['where'] =
            array_merge($last_attribute['where'] ?? [], $attributes['where'] ?? []);
        $new_attributes['prefix'] = isset($last_attribute['prefix'])
            ? ($last_attribute['prefix'] . (isset($attributes['prefix'])
                    ? '/' . $attributes['prefix'] : ''))
            : ($attributes['prefix'] ?? '');
        $new_attributes['namespace'] = isset($last_attribute['namespace'])
            ? ($last_attribute['namespace'] .
                (isset($attributes['namespace']) ? '/' . $attributes['namespace'] : ''))
            : ($attributes['namespace'] ?? '');
            $this->groupStack[] = $new_attributes;
    } else {
        $this->groupStack[] = $attributes;
    }
}

引數$attributes的值,我們們是知道的,這裡的groupStack預設是為空的,所以:

$this->groupStack[] = $attributes;

這段程式碼被呼叫,至於上面的if語句塊,後面再來講,我們們按程式碼流程走。
上面呼叫updateGroupStack方法把attributes儲存在了groupStack,此時groupStack的值為,如下:

$groupStack = [[
    'where' => [
        'id' => '\d{1,2}',
        'name' => '[a-z]+'
    ],
    'prefix' => 'admin',
    'namespace' => 'Admin\\Controller'
]];

可以看到groupStack的第一個元素就是attributes整個陣列,記住這個,後面會用到,回到Router的group方法中,繼續呼叫:

 $callback(self::getInstance());

這裡的$callback就是我們傳遞給RouterRegistrar的group方法的回撥,當前就是:

function (Router $router) {
    Router::get('dashboard', 'DashboardController@index');
    Router::prefix("order")
        ->group(function () {
            Router::post('add', 'OrderController@add');
            Router::post('index', 'OrderController/index');
        });
}

下面進入到回撥中,首先呼叫get方法,這個方法之前我們已經講過了,但是還記得嗎?get方法呼叫了newRoute方法,這個方法裡面有內容,我們說等到後面再講,沒錯,就是現在了,我們來看:

private static function newRoute($method, $rule, $action)
{
    $inst = self::getInstance();
    if (!empty($inst->groupStack)) {
        $attributes = end($inst->groupStack);
        $rule = isset($attributes['prefix']) ?
            rtrim($attributes['prefix'], '/') . '/' . $rule : $rule;
        if (is_string($action) && isset($attributes['namespace'])) {
            $action = trim($attributes['namespace'], '\\') . '\\' . $action;
        }

    } else {
        $attributes = [];
    }
    $route = new Route($method, $rule, $action);
    if (isset($attributes['where'])) {
        $route->where($attributes['where']);
    }
    return $route;
}

經過上面的分析,groupStack裡面有一個元素,所以他會進入到if程式碼分支中,end($inst->groupStack)獲取了groupStack的最後一個元素,因為groupStack中只有一個元素,所以也就是第一個元素,這裡就是,我們再貼出來一次:

$arrtibuets = [
    'where' => [
        'id' => '\d{1,2}',
        'name' => '[a-z]+'
    ],
    'prefix' => 'admin',
    'namespace' => 'Admin\\Controller'
];

上面首先檢查$arrtibuets中是否存在prefix,如果存在的話,就和get方法的第一個引數rule合併,假如rule是order,那麼合併之後就是admin/order了。緊接著檢查$arrtibuets中是否存在namespace,並且get方法的第二個引數action為字串的時候(如果action是回撥方法,那麼名稱空間就沒用了),合併namespace到action的前面,例如action為OrderController,那麼合併之後就是Admin\Controller\OrderController了。這個函式的最後面檢查attributes中是否設定過where,我們這裡有設定的,所以呼叫Route類的where方法。Route類的方法很簡單,就不貼出來了,大家下載下來看,一目瞭然。

get方法呼叫完之後,回到group的回撥方法中,繼續下面的程式碼呼叫,如下:

Router::prefix("order")
        ->group(function () {
            Router::post('add', 'OrderController@add');
            Router::post('index', 'OrderController/index');
        });

這裡再一次的呼叫到prefix方法,同樣的,它會走我們上面的流程,它也是設定RouteRegistrar類的attributes屬性的prefix值,所以這個時候呼叫Router的group方法(還記得RouteRegistrar直接呼叫Router的group方法嗎?):

public function group($attributes, callable $callback)
{
    $this->updateGroupStack($attributes);
    $callback(self::getInstance());
    array_pop($this->groupStack);
}

這裡的$attributes為:

$attributes=[
    'prefix'=>'order'
];

記住這個值,我們再一次呼叫updateGroupStack方法:

private function updateGroupStack(array $attributes)
{
    if (!empty($this->groupStack)) {
        $new_attributes = [];
        $last_attribute = end($this->groupStack);
        $new_attributes['where'] =
            array_merge($last_attribute['where'] ?? [], $attributes['where'] ?? []);
        $new_attributes['prefix'] = isset($last_attribute['prefix'])
            ? ($last_attribute['prefix'] . (isset($attributes['prefix'])
                    ? '/' . $attributes['prefix'] : ''))
            : ($attributes['prefix'] ?? '');
        $new_attributes['namespace'] = isset($last_attribute['namespace'])
            ? ($last_attribute['namespace'] .
                (isset($attributes['namespace']) ? '/' . $attributes['namespace'] : ''))
            : ($attributes['namespace'] ?? '');
            $this->groupStack[] = $new_attributes;
    } else {
        $this->groupStack[] = $attributes;
    }
}

經過上面的分析,此時的groupStack有一個元素就是:

$arrtibuets = [
    'where' => [
        'id' => '\d{1,2}',
        'name' => '[a-z]+'
    ],
    'prefix' => 'admin',
    'namespace' => 'Admin\\Controller'
];

引數$attributes是:

$attributes=[
    'prefix'=>'order'
];

end($this->groupStack)獲取到了最後一個元素,當前就是第一個啊,這裡首先合併where欄位,緊接著再合併prefix,最後namespace,上面合併的程式碼很簡單,大家應該能夠看懂把,哈哈的,合併之後的$new_attributes為:

$arrtibuets = [
    'where' => [
        'id' => '\d{1,2}',
        'name' => '[a-z]+'
    ],
    'prefix' => 'admin/order',
    'namespace' => 'Admin\\Controller'
];

合併完之後,再次把合併的而結果$new_attributes儲存在groupStack中,注意成為groupStack的最後一個元素,所以此時groupStack有2個元素了,好了,分析完上面的,我們再呼叫最內層的group回撥方法:

function () {
    Router::post('add', 'OrderController@add');
    Router::post('index', 'OrderController/index');
}

還記得之前我們說的get請求和post請求是一樣的麼?他也是直接呼叫newRoute方法,我們再次貼出來:

private static function newRoute($method, $rule, $action)
{
    $inst = self::getInstance();
    if (!empty($inst->groupStack)) {
        $attributes = end($inst->groupStack);
        $rule = isset($attributes['prefix']) ?
            rtrim($attributes['prefix'], '/') . '/' . $rule : $rule;
        if (is_string($action) && isset($attributes['namespace'])) {
            $action = trim($attributes['namespace'], '\\') . '\\' . $action;
        }

    } else {
        $attributes = [];
    }
    $route = new Route($method, $rule, $action);
    if (isset($attributes['where'])) {
        $route->where($attributes['where']);
    }
    return $route;
}

注意此時groupStack為:

$arrtibuets = [
    'where' => [
        'id' => '\d{1,2}',
        'name' => '[a-z]+'
    ],
    'prefix' => 'admin/order',
    'namespace' => 'Admin\\Controller'
];

後面的一系列合併操作,和上面講get的時候是一樣的,回撥呼叫完之後,回到Router的group方法:

老司機帶你實現Laravel路由註冊功能

最內層的回撥呼叫完之後,從groupStack中彈出最後一個元素,因為最內層group之外的路由用不到,所以必須彈出,到這裡最內層的group方法呼叫完成,返回到RouteRegistrar的group方法,

老司機帶你實現Laravel路由註冊功能

還記得RouterRegistrar的group方法是從哪裡呼叫的嗎?

老司機帶你實現Laravel路由註冊功能

它是在最外層的group方法的回撥中啊,所以我們繼續回到

老司機帶你實現Laravel路由註冊功能

最後就是彈出groupStack了,這一步操作前面已經講過了。

重點

我上面之所以要列出每一步的值,就是希望大家清楚我的程式碼到底做了一件什麼事,程式碼是如何實現無限迴圈巢狀的,以及每一層的約束是如何合併的,等等。

聯絡我

上面我儘可能詳細的給大家講解程式碼的執行,如果你還是不明白的話,可以聯絡我,或者是加我的QQ群,大家可以多多交流:

如果有不懂的地方,可以加我的qq:1174332406,或者是微信:itshardjs,公眾號:LearnCodeHard

相關文章