感慨学习总是那么的不容易,希望让兄弟们领略Laravel编程技巧,走进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就是[1,2,3,4],是不是很简单。进入到__callStatic方法中,首先判断如果我们调用的静态方法为'where', 'namespace', 'prefix'三个中的一个,那么会返回一个RouteRegistrar类的对象,这个类的实现很简单,如下:
<?phpclass 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中是否存在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(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群,大家可以多多交流:

.

- 0