老司机带你实现 Laravel 路由注册功能
感慨
学习总是那么的不容易,希望让兄弟们领略Laravel编程技巧,走进Laravel深处的我更不容易,每次给大家写代码,我总是思考这么一个问题,如何能让大家明白我想让你们明白的东西,不可否认这是一件非常困难的事,所以我只能感慨:我太难了。完成这篇博文已经是2019年11月28日凌晨2点了,我也要睡了。
终极目标
之前写了一篇博文《大家对 Laravel 的源代码和架构感兴趣么?》,相信很多人已经看过了,也都表示了极大的兴趣,这也是我对大家的承诺,所以从上一篇博文《详解 PHP 反射的基本使用》开始,我就准备给大家讲解Laravel的相关知识,讲解Laravel的代码非常不容易,需要阅读者具备相当的编程能力,对设计模式有一定的认识和顽强的毅力,最后但并不是最不重要的就是极大的好奇心。所以为了大家能够读懂,我会给大家编写功能和Laravel相当的程序,也可以说是简化版,这样最有助于大家的理解,如果大家理解了我写的,日后再来阅读Laravel,将会轻车熟路。
本节目标
在本篇博文中,我将会给大家实现一个类似Laravel路由注册的功能,这个功能在Laravel当中非常重要,希望大家能够理解,代码我已经上传到了码云laravel-route-register,这是我在下班之后给大家写的,实属不易,希望大家珍惜,仓库代码截图如下:
这个debug.php文件的测试代码如下:
上图就是我们今天所要实现的功能,这个嵌套是完全没有限制的,你可以任意组合api,希望大家把源代码下载下来,再配合我写的这篇博文,一定要读懂它,这是你理解任何PHP框架所不可或缺的必要步骤。
阅读准备
在阅读以下的内容之前,请先参考laravel路由相关文档,Laravel路由,首先搞清楚Laravel注册路由的可操作API。
文件简介
上面我已经贴出了这个包的几个文件,下面我会仔细的讲解下列文件:
- Router文件是我们的路由器文件,我们注册路由就是通过它进行的,比如它给我们提供的api有:get,post,where,namespace,prefix,这几个方法是我们操纵路由器的接口。
- RouteRegistrar文件,中文翻译过来就是:路由注册商,这个文件的内容很简单,它的作用是提供给我们路由嵌套的能力,之所以group方法能够做到路由嵌套,就是因为这个类和Router文件相互作用的结果,只凭它是无法做到这一点的。
- 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给你代码提示,你可以按我下面的方法操作:
比如我的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方法:
最内层的回调调用完之后,从groupStack中弹出最后一个元素,因为最内层group之外的路由用不到,所以必须弹出,到这里最内层的group方法调用完成,返回到RouteRegistrar的group方法,
还记得RouterRegistrar的group方法是从哪里调用的吗?
它是在最外层的group方法的回调中啊,所以我们继续回到
最后就是弹出groupStack了,这一步操作前面已经讲过了。
重点
我上面之所以要列出每一步的值,就是希望大家清楚我的代码到底做了一件什么事,代码是如何实现无限循环嵌套的,以及每一层的约束是如何合并的,等等。
联系我
上面我尽可能详细的给大家讲解代码的执行,如果你还是不明白的话,可以联系我,或者是加我的QQ群,大家可以多多交流:
本作品采用《CC 协议》,转载必须注明作者和本文链接
mark
先支持一下
nice
辛苦了
辛苦辛苦
辛苦了 :+1:
厉害了,看这种代码,最大的好处是学习思路,还有一些不常用的函数和技巧。
花了一小时看完,不知道作者又花了多少时间完成。辛苦了
感谢老司机,收获良多 :+1: :+1: :+1:
支持
先点个赞
楼主大大,能在讲实际代码之前先大致梳理下整个逻辑吗?这样的话,代码阅读起来效率会高 :joy:
laravel 的源码好像比这个更加复杂 :grin:
:+1:顶一个
支持作者多出点这种文章 :+1:
我问下在路由里面定义的中间件,如何在控制器里魔术方法前调用?
畅享Laravel系列
Very nice,But I find a very small mistake.
Ok,I speak Chinese.
__callStatic function内部可以修改增加省略号
return (new RouteRegistrar(self::getInstance()))->$name(...$arguments);
代码里面加一下注释最好了。
大神你好,我打印了一下返回值,为啥where下的name的值是空的

这里是不是缺了一个参数
哈哈,看的好开心。不过感觉跟着看看懂了,但是自己写还是写不出来,咋整啊
底子不够啊,看不懂 雨里雾里的
楼主写作风格幽默、诙谐,还是大大的良民。
nice
最内层的回调调用完之后,从 groupStack 中弹出最后一个元素,因为最内层 group 之外的路由用不到,所以必须弹出、 这一句不太理解,为啥弹出?最后一个元素不才是合并的最终路由吗
能看下作者的头发吗,我看看我往哪个方向努力。
真的不错,好厉害。花了好几个小时,才慢慢懂。