Laravel Route(路由)注册源码分析
基于laravel10
分析
我们能够看到路由会在App\Providers\RouteServiceProvider
这个服务里面注册
这里就先不分析这个服务在哪里被触发执行的了
我们看一下这个服务里面做了什么处理
在这个RouteServiceProvider
类中有一个boot
方法,这个方法中做了两个事情,一个是配置限流,一个是加载路由,限流这个我们先直接跳过,主要是分析路由是如果加载注册的
public function boot(): void
{
$this->configureRateLimiting();
$this->routes(function () {
...
});
}
我们看一下routes
方法做了事情
protected function routes(Closure $routesCallback)
{
$this->loadRoutesUsing = $routesCallback;
return $this;
}
这个方法就是把传进来的闭包传进来赋值给了loadRoutesUsing
这个成员变量,但是在哪里被触发了呢
我们可以看到App\Providers\RouteServiceProvider
这个类的父类Illuminate\Foundation\Support\Providers\RouteServiceProvider
中的register
方法
看一下这个方法里面做了什么事情
public function register()
{
$this->booted(function () {
...
});
}
这个方法中做了一个事情,就是把闭包传给booted
方法,这个方法中就是把闭包放到了bootedCallbacks
这个数组中
如果分析过laravel的生命周期(一个请求的执行过程)就知道,当服务的所有boot
方法执行完后,就会执行bootedCallbacks
数组闭包
public function booted(Closure $callback)
{
$this->bootedCallbacks[] = $callback;
}
就会执行到RouteServiceProvider
的register
方法中放到bootedCallbacks
数组中的那个闭包
我们看一下这个闭包做了什么事情
function () {
//设置跟控制器空间命名
$this->setRootControllerNamespace();
//如果存在路由缓存,就会优先加载理由缓存
if ($this->routesAreCached()) {
//加载路由缓存,这里我们就不分析这个了
$this->loadCachedRoutes();
} else {
//加载路由
$this->loadRoutes();
//这里我们先混个眼熟,后面再分析这里的作用是什么
$this->app->booted(function () {
$this->app['router']->getRoutes()->refreshNameLookups();
$this->app['router']->getRoutes()->refreshActionLookups();
}
}
现在我们分析一下loadRoutes
方法做了什么事情
这个方法里面就是如果loadRoutesUsing
这个不为null就会去执行这个闭包,我记得以前的版本是map
方法
protected function loadRoutes()
{
if (! is_null($this->loadRoutesUsing)) {
$this->app->call($this->loadRoutesUsing);
} elseif (method_exists($this, 'map')) {
$this->app->call([$this, 'map']);
}
}
当前这个版本会执行loadRoutesUsing
这个,这个就是对应在boot
方法中传入routes
方法的闭包
我们看一下这个闭包做了什么事情
function () {
Route::middleware('api')
->prefix('api')
->group(base_path('routes/api.php'));
Route::middleware('web')
->group(base_path('routes/web.php'));
}
这个闭包里面就是调用Illuminate\Support\Facades\Route
静态代理类
就会去找容器中找router
这个绑定的实际类
protected static function getFacadeAccessor()
{
return 'router';
}
就会找到这个单例绑定,就会去初始化Illuminate\Routing\Router
这个类
$this->app->singleton('router', function ($app) {
//Illuminate\Routing\Router
return new Router($app['events'], $app);
});
在上面闭包中调用Illuminate\Support\Facades\Route
静态代理类的方法实际上是去调用Illuminate\Routing\Router
这个类的方法
我们就以这个例子来分析一下做了哪些事情
Route::middleware('api')
->prefix('api')
->group(base_path('routes/api.php'));
在这个例子中,最开始是调用了middleware
方法,但是在Illuminate\Routing\Router
类中找不到这个方法,就会触发__call
这个魔术方法
我们看一下__call
这个方法里面做了哪些事情
public function __call($method, $parameters)
{
//首先会检查是否有注册宏,如果存在就会去调用这个宏
if (static::hasMacro($method)) {
return $this->macroCall($method, $parameters);
}
//判断方法是否是middleware
if ($method === 'middleware') {
//因为中间件方法可以数组 也可以传多个参数 所有单独做了处理
return (new RouteRegistrar($this))->attribute($method, is_array($parameters[0]) ? $parameters[0] : $parameters);
}
//如果方法不是where 但是是以where开头(表示框架自带的验证规则)
if ($method !== 'where' && Str::startsWith($method, 'where')) {
return (new RouteRegistrar($this))->{$method}(...$parameters);
}
//上面都不满足就会执行这个 其他的方法都是用这个参数规则
return (new RouteRegistrar($this))->attribute($method, array_key_exists(0, $parameters) ? $parameters[0] : true);
}
如果不是自定义了宏方法,都是调用RouteRegistrar
类的attribute
方法
我们看一下RouteRegistrar
类的attribute
做了什么事情
public function attribute($key, $value)
{
//['as', 'controller', 'domain', 'middleware', 'name', 'namespace', 'prefix', 'scopeBindings', 'where', 'withoutMiddleware']
//判断key是否在这个里面,没有就抛出异常
if (! in_array($key, $this->allowedAttributes)) {
throw new InvalidArgumentException("Attribute [{$key}] does not exist.");
}
//判断key是否middleware
if ($key === 'middleware') {
foreach ($value as $index => $middleware) {
//这里的目的可能是为了防止出现异常情况
$value[$index] = (string) $middleware;
}
}
//[ 'name' => 'as','scopeBindings' => 'scope_bindings', 'withoutMiddleware' => 'excluded_middleware' ]
$attributeKey = Arr::get($this->aliases, $key, $key);
//这里是排除中间件
if ($key === 'withoutMiddleware') {
$value = array_merge(
(array) ($this->attributes[$attributeKey] ?? []), Arr::wrap($value)
);
}
//放到attributes数组中
$this->attributes[$attributeKey] = $value;
return $this;
}
attribute
方法就分析完了,最后返回了$this
调用prefix
方法的时候也是进入了attribute
方法,组装到了attributes
数组里面
重点是group
方法,这是一个节点,我们看一下调用group
的时候做了什么事情Route::xxx()->group()
和Route::group()
是两个不同的group方法,最终都是进入到了Route
类的group
方法中
我们先看分析一下Route::xxx()->group()
这种情况下的group
方法做了什么处理
/**
* Create a route group with shared attributes.
*
* @param \Closure|array|string $callback
* @return $this
*/
public function group($callback)
{
$this->router->group($this->attributes, $callback);
return $this;
}
这个方法里面做了一个事情,就是调用router
属性的group
方法,传入了attributes
数组和参数(可以是闭包/数组/字符串(文件路径))
这个router
就是这个Illuminate\Routing\Router
类的实例,Route::group()
这种情况下的group
方法就是这个类中的group
方法
我们看一下这个Illuminate\Routing\Router
类中的group
方法做了什么事情
public function group(array $attributes, $routes)
{
foreach (Arr::wrap($routes) as $groupRoutes) {
//更新group堆栈,这里的目的就是拿到上一个节点的group合并当的group,然后放到数组的最后一个
$this->updateGroupStack($attributes);
//加载路由
$this->loadRoutes($groupRoutes);
//因为当前这个节点已经处理完了,就会移除最后一个元素,回到上一个节点,这样才不会破坏数据
array_pop($this->groupStack);
}
return $this;
}
我们看一下updateGroupStack
这个方法中做了哪些事情
protected function updateGroupStack(array $attributes)
{
//如果group堆栈不为空
if ($this->hasGroupStack()) {
//这里就是去拿最后一个节点的数组和当前传入的数组做合并
$attributes = $this->mergeWithLastGroup($attributes);
}
//放入group堆栈数组的最后面
$this->groupStack[] = $attributes;
}
我们看一下mergeWithLastGroup
方法做了哪些事情
public function mergeWithLastGroup($new, $prependExistingPrefix = true)
{
//这里就是传入新的数组,group堆栈数组的最后一个元素,前缀是拼接在后面还是前面(默认是后面)
return RouteGroup::merge($new, end($this->groupStack), $prependExistingPrefix);
}
我们看一下RouteGroup
类的merge
方法做了什么事情
public static function merge($new, $old, $prependExistingPrefix = true)
{
//如果新数组存在domain 则以新的为准
if (isset($new['domain'])) {
unset($old['domain']);
}
//如果新数组中存在控制器 则以新的为准
if (isset($new['controller'])) {
unset($old['controller']);
}
//这里就会去做合并了 这里做了别名拼接,空间命名拼接,前缀拼接,where合并(参数正则规则)
//最后把这些合并到以前
$new = array_merge(static::formatAs($new, $old), [
'namespace' => static::formatNamespace($new, $old),
'prefix' => static::formatPrefix($new, $old, $prependExistingPrefix),
'where' => static::formatWhere($new, $old),
]);
//这里是做递归合并(去除上面刚刚处理好的那几个旧数组中的,因为这几个已经做好了处理)
//剩下的就是如果key相同就会变成一个数组放到这key下面,比如middleware 就的和新的合并到一起变成
return array_merge_recursive(Arr::except(
$old, ['namespace', 'prefix', 'where', 'as']
), $new);
}
我们回到group
方法
处理完了updateGroupStack
方法后,调用了loadRoutes
方法loadRoutes
方法就是加载理由,我们看一下这个方法中做了什么事情
protected function loadRoutes($routes)
{
//判断是否是一个闭包
if ($routes instanceof Closure) {
//调用闭包传入Router单例类的实例进去
$routes($this);
} else {
//这里是路由文件加载
(new RouteFileRegistrar($this))->register($routes);
}
}
这个路由文件加载类的register
方法就做了引入文件的操作,$router = $this->router
这个的目的是为了在引入的这个文件中拿到这个变量
public function register($routes)
{
$router = $this->router;
require $routes;
}
loadRoutes
方法就分析完了,group
中的最后一步就是移除groupStack
中的最后一个元素,因为当前节点的事情做了,就需要移除,不然就会造成设置的中间件,前缀这些和定义的不一致
在每一个group
节点中,都会存在Route::get/post()...
我们来分析一个最终是怎么把路由注册好的
我们以get
路由注册为例来分析,其他的都是一样的
public function get($uri, $action = null)
{
//这里就是去添加路由
return $this->addRoute(['GET', 'HEAD'], $uri, $action);
}
我们来分析一下addRoute
方法做了什么事情
public function addRoute($methods, $uri, $action)
{
//这里就是去创建一个路由和添加路由
return $this->routes->add($this->createRoute($methods, $uri, $action));
}
我们先来分析怎么创建路由的,看一下createRoute
方法
protected function createRoute($methods, $uri, $action)
{
//这里是判断$action是否是路由到控制器
if ($this->actionReferencesController($action)) {
//这里是做对应的操作
$action = $this->convertToControllerAction($action);
}
...
}
我们来看一下convertToControllerAction
方法做了什么事情
protected function convertToControllerAction($action)
{
//如果是一个字符串比如`TestController@test`或者`test`直接是方法的
if (is_string($action)) {
//变成一个数组 key为`uses`
$action = ['uses' => $action];
}
//这里就是判断group堆栈不为空,代表存在,那就可能会有attributes数组属性
if ($this->hasGroupStack()) {
//这里就是做如果是`test`直接是方法的 拼接上控制器
$action['uses'] = $this->prependGroupController($action['uses']);
//这里是拼接上空间命名
$action['uses'] = $this->prependGroupNamespace($action['uses']);
}
$action['controller'] = $action['uses'];
return $action;
}
回到createRoute
方法,后续做了什么事情
protected function createRoute($methods, $uri, $action)
{
...
$route = $this->newRoute(
$methods, $this->prefix($uri), $action
);
...
}
看一下newRoute
方法做了什么事情
public function newRoute($methods, $uri, $action)
{
return (new Route($methods, $uri, $action))
//设置router实例
->setRouter($this)
//设置容器
->setContainer($this->container);
}
分析一下Route
实例化的时候做了什么事情
public function __construct($methods, $uri, $action)
{
$this->uri = $uri;
$this->methods = (array) $methods;
//主要是看这个的parseAction方法
$this->action = Arr::except($this->parseAction($action), ['prefix']);
if (in_array('GET', $this->methods) && ! in_array('HEAD', $this->methods)) {
$this->methods[] = 'HEAD';
}
//这里是组装前缀 如果在get/post....前后调用prefix 就会把前缀拼接到了最前面,一般我们也不会在这个上面设置前缀(也尽量不要去设置,因为它不是预期结果)
$this->prefix(is_array($action) ? Arr::get($action, 'prefix') : '');
}
我们来分析一下parseAction
这个方法做了什么事情
protected function parseAction($action)
{
return RouteAction::parse($this->uri, $action);
}
public static function parse($uri, $action)
{
if (is_null($action)) {
//如果是null 则给它一个默认闭包,里面做了一个异常抛出
return static::missingAction($uri);
}
//这里是反射这个是否可以被调用列如[TestController::class, 'test']这种就可以被调用或者是闭包
if (Reflector::isCallable($action, true)) {
return ! is_array($action) ? ['uses' => $action] : [
'uses' => $action[0].'@'.$action[1],
'controller' => $action[0].'@'.$action[1],
];
}
elseif (! isset($action['uses'])) {
//这里和上面是一样的
//这里是检测单个元素是否可以被调用[TestController::class, 'test']]这种就可以被调用或者是闭包
$action['uses'] = static::findCallable($action);
}
//这里是判断不是序列化闭包 且是string 但是不包含@符
if (! static::containsSerializedClosure($action) && is_string($action['uses']) && ! str_contains($action['uses'], '@')) {
//只有一个类名判断是否存在__invoke存在就拼接'@__invoke',不存在就抛出异常;
$action['uses'] = static::makeInvokable($action['uses']);
}
return $action;
}
newRoute
方法就分析完了
我们继续看createRoute
方法的后续操作
protected function createRoute($methods, $uri, $action)
{
...
if ($this->hasGroupStack()) {
//这里是合并group属性到路由实例上面 这个里面有合并前缀prefix 但是那个方法传入的是false 就会为了和上面前缀拼接做对应 ,这里就不进去分析这个方法了,和上面的一样
$this->mergeGroupAttributesIntoRoute($route);
}
//这里是添加where正则规则到路由
$this->addWhereClausesToRoute($route);
return $route;
}
createRoute
方法就分析完了
后面就是add(添加路由)了 add
方法在RouteCollection
类中
我们看一下add里面做了什么事情
public function add(Route $route)
{
//添加到集合里面
$this->addToCollections($route);
//这里是有点问题的 我觉得可以不用再这里调用这个
$this->addLookups($route);
return $route;
}
protected function addToCollections($route)
{
//拿到路由
$domainAndUri = $route->getDomain().$route->uri();
foreach ($route->methods() as $method) {
//给对应的请求方法追加路由
$this->routes[$method][$domainAndUri] = $route;
}
//所有路由
$this->allRoutes[$method.$domainAndUri] = $route;
}
我们来分析一下addLookups
这个方法
这个方法里面给别名和action增加了一个数组方便查找 但是有一个问题 如果我在get后面添加name或者domain这里就体现不出来
因为这里是调用get
方法的时候就执行了 所以就会导致我查找name的时候找不到 或者设置了domain但是不生效
就是因为这里的问题 所以框架在booted中又加了一个booted(这个在最开时候的时候有提到)
protected function addLookups($route)
{
if ($name = $route->getName()) {
$this->nameList[$name] = $route;
}
$action = $route->getAction();
if (isset($action['controller'])) {
$this->addToActionList($action, $route);
}
}
就是这个东西,这里就是为了解决上面那个问题 所以我是觉得addLookups
在这里感觉没有必要执行
这个地方如果没有其他地方注册路由 我觉得没有必要加booted方法 直接调用这里面的两个方法就可以了
$this->app->booted(function () {
$this->app['router']->getRoutes()->refreshNameLookups();
$this->app['router']->getRoutes()->refreshActionLookups();
}
get
方法就执行完了,是分析的Route::get()
这种情况
还有一种情况就是Route::xxx()->get()
这种情况做了一些处理,最终也是调用了上面分析的get
方法,但是也差不多,就是提前处理了一下attribute属性,这里就先做过多的分析了
以上就是路由注册的整个执行流程了,如果有写的有误的地方,请大佬们指正
本作品采用《CC 协议》,转载必须注明作者和本文链接
写得很好,值得学习
谢谢分享