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;
}

就会执行到RouteServiceProviderregister方法中放到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 协议》,转载必须注明作者和本文链接
本帖由系统于 8个月前 自动加精
《L05 电商实战》
从零开发一个电商项目,功能包括电商后台、商品 & SKU 管理、购物车、订单管理、支付宝支付、微信支付、订单退款流程、优惠券等
《G01 Go 实战入门》
从零开始带你一步步开发一个 Go 博客项目,让你在最短的时间内学会使用 Go 进行编码。项目结构很大程度上参考了 Laravel。
讨论数量: 2

写得很好,值得学习

8个月前 评论

谢谢分享

8个月前 评论

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