老司机带你实现 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
《L01 基础入门》
我们将带你从零开发一个项目并部署到线上,本课程教授 Web 开发中专业、实用的技能,如 Git 工作流、Laravel Mix 前端工作流等。
《L05 电商实战》
从零开发一个电商项目,功能包括电商后台、商品 & SKU 管理、购物车、订单管理、支付宝支付、微信支付、订单退款流程、优惠券等
讨论数量: 26

先支持一下

4年前 评论

辛苦了

4年前 评论

辛苦辛苦

4年前 评论

辛苦了 :+1:

4年前 评论

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

4年前 评论

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

4年前 评论
黑将军

先点个赞

4年前 评论

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

4年前 评论

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

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

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

4年前 评论
CrazyZard

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

4年前 评论

畅享Laravel系列

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年前

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

4年前 评论

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

LK

4年前 评论
happyqian_ah 4年前

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

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

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

3年前 评论

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

3年前 评论

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

3年前 评论

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

3年前 评论

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

2年前 评论

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