思考并回答以下问题:
前言
作为Laravel极其重要的一部分,route功能贯穿着整个网络请求,是request生命周期的主干。本文主要讲述route服务的注册与启动、路由的属性注册。本篇内容相对简单,更多的是框架添加路由的整体设计流程。
route服务的注册
Laravel在接受到请求后,先进行了服务容器与http核心的初始化,再进行了请求request的构造与分发。
route服务的注册-RoutingServiceProvider发生在服务容器container的初始化上;
route服务的启动与加载-RouteServiceProvider发生在request的分发上。
route服务的注册-RoutingServiceProvider
所有需要Laravel服务的请求都会加载入口文件index.php:
1 | require __DIR__.'/../vendor/autoload.php'; |
第一句我们在之前的博客提过,是实现PSR0、PSR4标准自动加载的功能模块,第二句就是今天说的Container的初始化:
1 |
|
Application:
1 | namespace Illuminate\Foundation; |
路由服务的注册就在registerBaseServiceProviders()这个函数中:
1 | /** |
RoutingServiceProvider:
1 | namespace Illuminate\Routing; |
可以看到,RoutingServiceProvider做的事情比较简单,就是向服务容易中注册router。
route服务的启动与加载-RouteServiceProvider
Laravel在初始化Application后,就要进行http/Kernel的构造:
1 | $kernel = $app->make(Illuminate\Contracts\Http\Kernel::class); |
初始化结束后,就会调用handle函数,这个函数用于Laravel各个功能服务的注册启动,还有request的分发:
1 | public function handle($request) |
路由服务的启动与加载就在其中一个函数中bootstrap,这个函数用于各种服务的注册与启动,比较复杂,我们有机会在以后单独来说。
总之,这个函数会调用RouteServiceProvider这个类的两个函数:注册-register、启动-boot。
由于route的注册工作由之前RoutingServiceProvider完成,所以RouteServiceProvider的register是空的,这里它只负责路由的启动与加载工作,我们主要看boot:
1 | namespace Illuminate\Foundation\Support\Providers; |
1 | class Application extends Container implements ApplicationContract, CachesConfiguration, CachesRoutes, HttpKernelInterface |
从boot中可以看出,Laravel首先去寻找路由的缓存文件,没有缓存文件再去进行加载路由。缓存文件一般在bootstrap/cache/routes.php文件中。
加载路由主要调用map函数,这个函数一般在App\Providers\RouteServiceProvider这个类中,这个类继承上面的Illuminate\Foundation\Support\Providers\RouteServiceProvider:
1 | use Illuminate\Foundation\Support\Providers\RouteServiceProvider as ServiceProvider; |
Laravel将路由分为两个大组:api、web。这两个部分的路由分别写在两个文件中:routes/web.php、routes/api.php。
路由的加载
所谓的路由加载,就是将定义路由时添加的属性,例如’name’、’domain’、’scheme’等等保存起来,以待后用。
- Laravel定义路由的属性的方法很灵活,可以定义在路由群组前,例如:
1 | Route::domain('route.domain.name')->group(function() { |
- 可以定义在路由群组中,例如:
1 | Route::group('domain' => 'group.domain.name',function() { |
- 可以定义在method的前面,例如:
1 | Route::domain('route.domain.name')->get('foo','controller@method'); |
- 可以定义在method中,例如:
1 | Route::get('foo', ['domain' => 'route.domain.name','use' => 'controller@method']); |
- 还可以定义在method后,例如:
1 | Route::get('{one}', 'use' => 'controller@method')->where('one', '(.+)'); |
事实上,路由的加载功能主要有三个类负责:
Illuminate\Routing\Router、Illuminate\Routing\Route、Illuminate\Routing\RouteRegistrar。
Router在整个路由功能中都是起着中枢的作用,RouteRegistrar主要负责位于group、method这些函数之前的属性注册,例如上面的第一种和第三种,route主要负责位于group、method这些函数之后的属性注册,例如第五种。
RouteRegistrar路由加载
属性注册
当我们想要在Route后面直接利用domain()、name()等函数来为路由注册属性的时候,我们实际调用的是router的魔术方法__call():
1 | namespace Illuminate\Routing; |
在类RouteRegistrar中:
1 | class RouteRegistrar |
添加路由
注册属性之后,创建路由的时候,可以仅仅提供uri,可以提供uri与闭包,可以提供uri与控制器,可以提供uri与数组:
1 | Route::as('Foo')->namespace('Namespace\\Example\\')->get('foo/bar'); // 仅仅uri |
利用get、post等方法创建新的路由时,会调用类RouteRegistrar中的魔术方法__call():
Illuminate\Routing\RouteRegistrar.php
1 | class RouteRegistrar |
也就是说,RouteRegistrar在这里会为闭包或控制器等所有非数组的action添加use键,然后才会去router中创建路由。
添加路由群组
注册属性之后,还可以创建路由群组,但是这时路由群组不允许添加属性action:
1 | class RouteRegistrar |
Router路由群组加载
路由群组的功能可以不断叠加递归,因此每次调用group,都要用新路由群组的属性与旧路由群组属性合并,以待新的路由去继承。group参数可以是闭包函数,也可以是包含定义路由的文件路径。
Illuminate\Routing\Router.php
1 |
|
关于路由群组属性的合并:
- prefix、as、namespace这几个属性会连接在一起,例如prefix1/prefix2/prefix3。
- where属性数组相同的会被替换,不同的会被合并。
- domain属性会被替换。
- 其他属性,例如middleware数组会直接被合并,即使存在相同的元素。
1 |
|
Router路由加载
添加路由需要很多步骤,需要将路由本身的属性和路由群组的属性相结合。
1 | class Router implements BindingRegistrar, RegistrarContract |
从上面来看,添加一个新的路由需要:
- 给路由的控制器添加group的namespace
- 给路由的uri添加group的prefix前缀
- 创建新的路由
- 更新路由的属性信息
- 为路由添加router-pattern正则约束
- 路由添加到RouteCollection中
控制器namespace
路由控制器的命名空间一般不用特别指定,默认值是\App\Http\Controllers,每次创建新的路由,都要将默认的命名空间添加到控制器中去:
1 | protected function actionReferencesController($action) |
uri前缀
在创建新的路由前,需要将路由群组的prefix添加到路由的uri中:
1 | protected function prefix($uri) |
创建新的路由
路由的创建需要Route类:
1 | protected function newRoute($methods, $uri, $action) |
关于Router类添加新的路由我们在下一部分详细说。
更新路由属性信息
创建新的路由之后,需要将路由本身的属性action与路由群组的属性结合在一起:
1 | public function hasGroupStack() |
添加全局正则约束到路由
上一篇文章我们说过,我们可以为路由通过pattern方法添加全局的参数正则约束,所有每次添加新的路由都要将这个全局正则约束添加到路由中:
1 | public function pattern($key, $pattern) |
Route路由加载
前面说过,路由的创建是由Route这个类完成的:
1 | public function __construct($methods, $uri, $action) |
由此可以看出,路由的创建主要是路由的各个属性的初始化,其中值得注意的有两个:action与prefix action解析
1 | protected function parseAction($action) |
我们可以看出,添加新的路由时,action属性需要利用RouteAction类:
1 | class RouteAction |
前面的博客我们说过,创建路由的时候,除了为路由分配控制器之外,还可以为路由分配闭包函数,还有类函数,例如之前说的单动作控制器:
1 | $router->get('foo/bar2', [‘domain’ => 'www.example.com', 'Illumi |
因此,解析action主要做两件事:
- 为闭包函数添加use键。对于此时没有use键的路由,由于之前在Router中已经为控制器添加use键,因此这时没有use键的,必然是闭包函数,在这里直接或者在action中寻找闭包函数后,为闭包函数添加use键。
- 单动作控制器添加__invoke 。对于单动作控制器来说,此时已经和控制器一样拥有’use’键,但是并没有@符号,此时就会调用makeInvokable函数来将__invoke添加到后面。
prefix前缀
路由自身也有prefix属性,而且这个属性要加在其他prefix的最前面,作为路由的uri:
1 | public function prefix($prefix) |
Route路由属性加载
除了RouteRegistrar之外,Route也可以为路由添加属性:
prefix前缀
1 | public function prefix($prefix) |
where正则约束
1 | public function where($name, $expression = null) |
middleware中间件
1 | public function middleware($middleware = null) |
uses控制器
1 | public function uses($action) |
name命名
1 | public function name($name) |
RouteCollection添加路由
在上面的部分,我们看到添加路由的代码:
1 | protected function addRoute($methods, $uri, $action) |
新创建的路由会加入到RouteCollection中,会更新类中的routes、allRoutes、nameList、actionList。
1 |
|
我们在上面路由的注册启动章节说道,路由的启动是namespace Illuminate\Foundation\Support\Providers\RouteServiceProvider完成的,调用的是boot函数:
1 | public function boot() |
在最后一句,程序将会在所有服务都启动后运行refreshNameLookups函数,把所有的name属性加载到RouteCollection中:
1 | class RouteCollection extends AbstractRouteCollection |
测试样例如下:
1 | public function testRouteCollectionCanRefreshNameLookups() |