老司机带你实现 Laravel 路由注册功能

感慨

学习总是那么的不容易,希望让兄弟们领略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群,大家可以多多交流:

本作品采用《CC 协议》,转载必须注明作者和本文链接
微信:okayGoHome
本帖由 CrazyZard 于 4年前 加精
Dennis_Ritchie
《L04 微信小程序从零到发布》
从小程序个人账户申请开始,带你一步步进行开发一个微信小程序,直到提交微信控制台上线发布。
《G01 Go 实战入门》
从零开始带你一步步开发一个 Go 博客项目,让你在最短的时间内学会使用 Go 进行编码。项目结构很大程度上参考了 Laravel。
讨论数量: 26

厉害了,看这种代码,最大的好处是学习思路,还有一些不常用的函数和技巧。
花了一小时看完,不知道作者又花了多少时间完成。辛苦了

4年前 评论

Very nice,But I find a very small mistake.
Ok,I speak Chinese.
__callStatic function内部可以修改增加省略号
return (new RouteRegistrar(self::getInstance()))->$name(...$arguments);

4年前 评论
Dennis_Ritchie (楼主) 4年前

楼主写作风格幽默、诙谐,还是大大的良民。

3年前 评论

感谢老司机,收获良多 :+1: :+1: :+1:

4年前 评论

支持作者多出点这种文章 :+1:

4年前 评论

真的不错,好厉害。花了好几个小时,才慢慢懂。

2年前 评论

能看下作者的头发吗,我看看我往哪个方向努力。

3年前 评论

最内层的回调调用完之后,从 groupStack 中弹出最后一个元素,因为最内层 group 之外的路由用不到,所以必须弹出、 这一句不太理解,为啥弹出?最后一个元素不才是合并的最终路由吗

3年前 评论
云客网络工作室

底子不够啊,看不懂 雨里雾里的

3年前 评论

哈哈,看的好开心。不过感觉跟着看看懂了,但是自己写还是写不出来,咋整啊

4年前 评论

大神你好,我打印了一下返回值,为啥where下的name的值是空的
file
这里是不是缺了一个参数

LK

4年前 评论
happyqian_ah 4年前

代码里面加一下注释最好了。

4年前 评论

畅享Laravel系列

4年前 评论
CrazyZard

我问下在路由里面定义的中间件,如何在控制器里魔术方法前调用?

4年前 评论

laravel 的源码好像比这个更加复杂 :grin:

4年前 评论
Dennis_Ritchie (楼主) 4年前

楼主大大,能在讲实际代码之前先大致梳理下整个逻辑吗?这样的话,代码阅读起来效率会高 :joy:

4年前 评论
黑将军

先点个赞

4年前 评论

辛苦了 :+1:

4年前 评论

辛苦辛苦

4年前 评论

辛苦了

4年前 评论

先支持一下

4年前 评论

讨论应以学习和精进为目的。请勿发布不友善或者负能量的内容,与人为善,比聪明更重要!