思考并回答以下问题:
前言
作为一个web后台框架,路由无疑是极其重要的一部分。本博客接下来几篇文章都将会围绕路由这一主题来展开讨论,分别讲述:
- 路由的使用
- 路由属性注册
- 路由的正则编译与匹配
- 路由的中间件
- 路由的控制器与参数绑定
- RESTful路由
和之前一样,第一篇将会利用单元测试样例说明我们在平时可能用到的route的api函数用法,后面几篇文章将会剖析Laravel的route源码。下面开始介绍Laravel中路由的各种用法。
路由属性注册
所有Laravel路由都定义在位于routes目录下的路由文件中,这些文件通过框架自动加载。routes/web.php文件定义了web界面的路由,这些路由被分配了web中间件组,从而可以提供session和csrf防护等功能。routes/api.php中的路由是无状态的,被分配了api中间件组。
对大多数应用而言,都是从routes/web.php文件开始定义路由。
路由method方法
我们可以注册路由来响应任何HTTP请求:1
2
3
4
5
6Route::get($uri, $callback);
Route::post($uri, $callback);
Route::put($uri, $callback);
Route::patch($uri, $callback);
Route::delete($uri, $callback);
Route::options($uri, $callback);
有时候还需要注册路由响应多个HTTP请求——这可以通过match方法来实现。或者,可以使用any方法注册一个路由来响应所有HTTP请求:1
2
3
4
5
6
7Route::match(['get', 'post'], '/', function () {
//
});
Route::any('foo', function () {
//
});
值得注意的是,一般的HTML表单仅仅支持get、post,并不支持put、patch、delete等动作,这时候就需要在前端添加一个隐藏的_method字段到给表单中,其值被用作HTTP请求方法名:1
<input type="hidden" name="_method" value="PUT">
在web路由文件中所有请求方式为PUT、POST或DELETE的HTML表单都会包含一个CSRF令牌字段,否则,请求会被拒绝。关于CSRF的更多细节,可以参考浅谈CSRF攻击方式:1
2
3
4<form method="POST" action="/profile">
{{ csrf_field() }}
...
</form>
路由scheme协议
对于web后台框架来说,路由的scheme底层协议一般使用http、https:1
2
3
4
5
6
7Route::get('foo/{bar}', ['http', function () {
}]);
Route::get('foo/{bar}', ['https', function () {
}]);
路由domain子域名
子域名可以像URI一样被分配给路由参数,子域名可以通过路由属性中的domain来指定:1
2
3
4
5
6
7Route::domain('api.name.bar')->get('foo/bar', function ($name) {
return $name;
});
Route::get('foo/bar', ['domain' => 'api.name.bar', function ($name) {
return $name;
}]);
路由prefix前缀
可以为路由添加一个给定URI前缀,通过利用路由属性的prefix指定:
1 | Route::prefix('pre')->get('foo/bar', function () { |
路由where正则约束
可以为路由的URI参数指定正则约束:1
2
3
4
5
6
7Route::get('{one}', ['where' => ['one' => '(.+)'], function () {
}]);
Route::get('{one}', function () {
})->where('one', '(.+)');
如果想要路由参数在全局范围内被给定正则表达式约束,可以使用pattern方法。在RouteServiceProvider类的boot方法中定义约束模式:
1 | public function boot() |
路由middleware中间件
为路由添加中间件,通过利用路由属性的middleware指定:
1 | Route::middleware('web')->get('foo/bar', function () { |
路由namespace属性
可以为路由的控制器添加namespace来指定控制器的命名空间:1
2
3
4
5
6
7Route::namespace('Namespace\\Example\\')->get('foo/bar', function () {
});
Route::get('foo/bar', ['namespace' => 'Namespace\\Example\\', function () {
}]);
路由uses属性
可以为路由添加URI对应的执行逻辑,例如闭包或者控制器:
1 | Route::get('foo/bar', ['uses' => function () { |
路由as别名
可以为路由指定别名,通过路由属性的as来指定:1
2
3
4
5
6
7
8
9
10
11
12
13
14
15Route::as('Foo')->get('foo/bar', function () {
});
Route::name('Foo')->get('foo/bar', function () {
});
Route::get('foo/bar', ['as' => 'Foo', function () {
}]);
Route::get('foo/bar', function () {
})->name('Foo');
路由group群组属性
可以为一系列具有类似属性的路由归为同一组,利用group将这些路由归并到一起:
1 | Route::group( |
group群组的属性分为两类:替换型、递增型。当群组属性与路由属性重复的时候,替换型属性会用路由的属性替换群组的属性,递增型的属性会综合路由和群组的属性。
在上面的例子可以看出:
- domain这个属性是替换型属性,路由的属性会覆盖和替换群组的这几个属性;
- prefix、middleware、namespace、as、where这几个属性是递增型属性,路由的属性和群组属性会相互结合。
另外值得注意的是:
- 路由的prefix属性具有优先级,因此上面第二个路由的uri是routepre/grouppre/additional/111/add,而不是grouppre/routepre/additional/111/add;
- where属性对于相同的路由参数会替换,不同的路由参数会结合,因此上面where中one被替换,two被结合进来
路由参数与匹配
Laravel允许在注册定义路由的时候设定路由参数,以供控制器或者闭包所用。路由参数可以设定在URI中,也可以设定在domain中。
路由编码匹配
对于已编码的请求URI,框架会自动进行解码然后进行匹配:
1 | $router = $this->getRouter(); |
路由参数
路由参数总是通过花括号进行包裹,这些参数在路由被执行时会被传递到路由的闭包。路由参数不能包含-字符,需要的话可以使用_替代。
1 | $router = $this->getRouter(); |
路由可选参数
有时候可能需要指定可选的路由参数,这可以通过在参数名后加一个?标记来实现,这种情况下需要给相应的变量指定默认值:1
2
3
4
5
6
7
8
9
10
11
12
13$router = $this->getRouter();
$router->get('{foo?}/{baz?}', function ($name = 'taylor', $age = 25) {
return $name.$age;
});
$this->assertEquals('fred25', $router->dispatch(Request::create('fred', 'GET'))->getContent());
$router->get('default/{foo?}/{baz?}', function ($name, $age = 25){
return $name.$age;
})->default('name', 'taylor');
$this->assertEquals('fred25', $router->dispatch(Request::create('fred', 'GET'))->getContent());
路由参数正则约束
可以使用路由实例上的where方法来约束路由参数的格式。where方法接收参数名和一个正则表达式来定义该参数如何被约束:
1 | Route::get('user/{name}', function ($name) { |
如果想要路由参数在全局范围内被给定正则表达式约束,可以使用pattern方法。在RouteServiceProvider类的boot方法中定义约束模式:
1 | public function boot() |
值得注意的是,路由参数是不允许出现/字符的,例如:1
2
3
4
5
6
7$router->get('{one?}', ['uses' => function ($one = null){
return $one;
}]);
$request2 = Request::create('foo/bar/baz', 'GET');
$this->assertFalse($route->matches($request2));
上例中one只能匹配foo,不能匹配foo/bar/baz,这时就需要对one进行正则约束:
1 | public function testLeadingParamDoesntReceiveForwardSlashOnEmptyPath() |
路由中间件
HTTP中间件为过滤进入应用的HTTP请求提供了一套便利的机制。例如,Laravel内置了一个中间件来验证用户是否经过认证,如果用户没有经过认证,中间件会将用户重定向到登录页面,否则如果用户经过认证,中间件就会允许请求继续往前进入下一步操作。
Laravel框架自带了一些中间件,包括认证、CSRF保护中间件等等。所有的中间件都位于app/Http/Middleware目录。
中间件之前/之后/终止
一个中间件是请求前还是请求后执行取决于中间件本身。比如,以下中间件会在请求处理前执行一些任务:
1 | class BeforeMiddleware |
有时候中间件可能需要在HTTP响应发送到浏览器之后做一些工作。比如,Laravel内置的“session”中间件会在响应发送到浏览器之后将Session数据写到存储器中,为了实现这个功能,需要定义一个终止中间件并添加terminate方法到这个中间件:1
2
3
4
5
6
7
8
9
10
11
12class StartSession
{
public function handle($request, Closure $next)
{
return $next($request);
}
public function terminate($request, $response)
{
// 存储session数据...
}
}
全局中间件
如果你想要中间件在每一个HTTP请求期间被执行,只需要将相应的中间件类设置到app/Http/Kernel.php的数组属性$middleware中即可。
1 | protected $middleware = |
路由中间件
如果你想要分配中间件到指定路由,可以传递完整的类名:1
2
3
4
5use App\Http\Middleware\CheckAge;
Route::get('admin/profile', function () {
//
})->middleware(CheckAge::class);
或者可以给中间件提供一个别名:1
2
3
4
5
6
7
8
9
10
11
12
13
14public function testDefinedClosureMiddleware()
{
$router = $this->getRouter();
$router->get('foo/bar', ['middleware' => 'foo', function (){
return 'hello';
}]);
$router->aliasMiddleware('foo', function ($request, $next) {
return 'caught';
});
$this->assertEquals('caught', $router->dispatch(Request::create('foo/bar', 'GET'))->getContent());
}
也可以在app/Http/Kernel.php文件中分配给该中间件一个key,默认情况下,该类的$routeMiddleware属性包含了Laravel自带的中间件,要添加你自己的中间件,只需要将其追加到后面并为其分配一个key,例如:1
2
3
4
5
6
7
8
9
10
11
12
13protected $routeMiddleware =
[
'auth' => \Illuminate\Auth\Middleware\Authenticate::class,
'auth.basic' => \Illuminate\Auth\Middleware\AuthenticateWithBasicAuth::class,
'bindings' => \Illuminate\Routing\Middleware\SubstituteBindings::class,
'can' => \Illuminate\Auth\Middleware\Authorize::class,
'guest' => \App\Http\Middleware\RedirectIfAuthenticated::class,
'throttle' => \Illuminate\Routing\Middleware\ThrottleRequests::class,
];
Route::get('admin/profile', function () {
//
})->middleware('auth');
使用数组分配多个中间件到路由:
1 | Route::get('/', function () { |
中间件组
有时候你可能想要通过指定一个键名的方式将相关中间件分到同一个组里面,从而更方便将其分配到路由中,这可以通过使用HTTP Kernel的$middlewareGroups属性实现。
Laravel自带了开箱即用的web和api两个中间件组以分别包含可以应用到Web UI和API路由的通用中间件:1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19protected $middlewareGroups =
[
'web' => [
\App\Http\Middleware\EncryptCookies::class,
\Illuminate\Cookie\Middleware\AddQueuedCookiesToResponse::class,
\Illuminate\Session\Middleware\StartSession::class,
\Illuminate\View\Middleware\ShareErrorsFromSession::class,
\App\Http\Middleware\VerifyCsrfToken::class,
\Illuminate\Routing\Middleware\SubstituteBindings::class,
],
'api' => [
'throttle:60,1',
'auth:api',
],
];
Route::get('/', function () {
//
})->middleware('web');
值得注意的是,中间件组中可以循环嵌套中间件组:1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22public function testMiddlewareGroupsCanReferenceOtherGroups()
{
unset($_SERVER['__middleware.group']);
$router = $this->getRouter();
$router->get('foo/bar', ['middleware' => 'web', function (){
return 'hello';
}]);
$router->aliasMiddleware('two', 'Illuminate\Tests\Routing\RoutingTestMiddlewareGroupTwo');
$router->middlewareGroup('first', ['two:abigail']);
$router->middlewareGroup('web', ['Illuminate\Tests\Routing\RoutingTestMiddlewareGroupOne', 'first']);
$this->assertEquals('caught abigail', $router->dispatch(Request::create('foo/bar', 'GET'))->getContent());
$this->assertTrue($_SERVER['__middleware.group']);
unset($_SERVER['__middleware.group']);
}
中间件参数
中间件还可以接收额外的自定义参数,例如,如果应用需要在执行给定动作之前验证认证用户是否拥有指定的角色,可以创建一个CheckRole来接收角色名作为额外参数。
额外的中间件参数会在$next参数之后传入中间件:1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20namespace App\Http\Middleware;
use Closure;
class CheckRole
{
public function handle($request, Closure $next, $role)
{
if (! $request->user()->hasRole($role))
{
// Redirect...
}
return $next($request);
}
}
Route::put('post/{id}', function ($id) {
//
})->middleware('role:editor');
中间件的顺序
当router中有多个中间件的时候,中间件的执行顺序并不是严格按照中间件数组进行的,框架中存在一个数组$middlewarePriority,规定了这个数组中各个中间件的顺序:1
2
3
4
5
6
7
8
9protected $middlewarePriority =
[
\Illuminate\Session\Middleware\StartSession::class,
\Illuminate\View\Middleware\ShareErrorsFromSession::class,
\Illuminate\Auth\Middleware\Authenticate::class,
\Illuminate\Session\Middleware\AuthenticateSession::class,
\Illuminate\Routing\Middleware\SubstituteBindings::class,
\Illuminate\Auth\Middleware\Authorize::class,
];
当我们使用了上面其中多个中间件的时候,框架会自动按照上面的数组进行排序:1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26public function testMiddlewarePrioritySorting()
{
$middleware = [
Placeholder1::class,
SubstituteBindings::class,
Placeholder2::class,
Authenticate::class,
Placeholder3::class,
];
$router = $this->getRouter();
$router->middlewarePriority = [Authenticate::class, SubstituteBindings::class, Authorize::class];
$route = $router->get('foo', ['middleware' => $middleware, 'uses' => function ($name) {
return $name;
}]);
$this->assertEquals([
Placeholder1::class,
Authenticate::class,
SubstituteBindings::class,
Placeholder2::class,
Placeholder3::class,
], $router->gatherRouteMiddleware($route));
}
控制器
控制器类
更普遍的方法是使用控制器来组织管理这些行为。控制器可以将相关的HTTP请求封装到一个类中进行处理。通常控制器存放在app/Http/Controllers目录中.
所有的Laravel控制器应该继承自Laravel自带的控制器基类Controller,控制器基类提供了一些很方便的方法如middleware,用于添加中间件到控制器动作:
1 | class UserController extends Controller |
1 | Route::get('user/{id}', 'UserController@show'); |
单动作控制器
如果想要定义一个只处理一个动作的控制器,可以在这个控制器中定义__invoke方法,当为这个单动作控制器注册路由的时候,不需要指定方法:1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22public function testDispatchingCallableActionClasses()
{
$router = $this->getRouter();
$router->get('foo/bar', 'Illuminate\Tests\Routing\ActionStub');
$this->assertEquals('hello', $router->dispatch(Request::create('foo/bar', 'GET'))->getContent());
$router->get('foo/bar2', [
'uses' => 'Illuminate\Tests\Routing\ActionStub@func',
]);
$this->assertEquals('hello2', $router->dispatch(Request::create('foo/bar2', 'GET'))->getContent());
}
class ActionStub extends Controller
{
public function __invoke()
{
return 'hello';
}
}
控制器中间件
将中间件放在控制器构造函数中更方便,在控制器的构造函数中使用middleware方法你可以很轻松的分配中间件给该控制器。你甚至可以限定该中间件应用到该控制器类的指定方法:1
2
3
4
5
6
7
8
9class UserController extends Controller
{
public function __construct()
{
$this->middleware('auth');
$this->middleware('log')->only('index');
$this->middleware('subscribed')->except('store');
}
}
callAction方法
值得注意的是每次执行控制器方法都会先执行控制器的callAction函数:
1 | public function callAction($method, $parameters) |
测试样例:1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19unset($_SERVER['__test.controller_callAction_parameters']);
$router->get(($str = str_random()).'/{one}/{two}', 'Illuminate\Tests\Routing\RouteTestAnotherControllerWithParameterStub@oneArgument');
$router->dispatch(Request::create($str.'/one/two', 'GET'));
$this->assertEquals(['one' => 'one', 'two' => 'two'], $_SERVER['__test.controller_callAction_parameters']);
class RouteTestAnotherControllerWithParameterStub extends Controller
{
public function callAction($method, $parameters)
{
$_SERVER['__test.controller_callAction_parameters'] = $parameters;
}
public function oneArgument($one)
{
}
}
__call方法
和普通类一样,若控制器中没有对应classname@method中的method,则会调用类的__call函数。1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20public function testCallableControllerRouting()
{
$router = $this->getRouter();
$router->get('foo/bar', 'Illuminate\Tests\Routing\RouteTestControllerCallableStub@bar');
$router->get('foo/baz', 'Illuminate\Tests\Routing\RouteTestControllerCallableStub@baz');
$this->assertEquals('bar', $router->dispatch(Request::create('foo/bar', 'GET'))->getContent());
$this->assertEquals('baz', $router->dispatch(Request::create('foo/baz', 'GET'))->getContent());
}
class RouteTestControllerCallableStub extends Controller
{
public function __call($method, $arguments = [])
{
return $method;
}
}
路由参数依赖注入与绑定
Laravel使用服务容器解析所有的Laravel控制器,因此,可以在控制器的构造函数中类型声明任何依赖,这些依赖会被自动解析并注入到控制器实例中。路由的参数绑定可以分为两种:显示绑定与隐示绑定。
路由隐示绑定
控制器方法期望输入路由参数,只需要将路由参数放到其他依赖之后1
2
3
4
5
6
7
8Route::put('user/{id}', 'UserController@update');
class UserController extends Controller
{
public function update(Request $request, $id)
{
}
}
可以在控制器的动作方法中进行依赖的类型提示,例如,我们可以在某个方法中类型提示Illuminate\Http\Request实例:
1 | class UserController extends Controller |
可以为控制器的动作方法中添加数据库模型的主键,框架会自动利用主键来获取对应的记录,需要注意的是,route定义路由的路由参数必须和控制器内的变量名相同,例如下例中路由参数userid和控制器参数userid:
1 | Route::put('user/{userid}', 'UserController@update'); |
综合测试样例:1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74public function testImplicitBindingsWithOptionalParameter()
{
unset($_SERVER['__test.controller_callAction_parameters']);
$router->get(($str = str_random()).'/{user}/{defaultNull?}/{team?}', [
'middleware' => SubstituteBindings::class,'uses' => 'Illuminate\Tests\Routing\RouteTestAnotherControllerWithParameterStub@withModels',
]);
$router->dispatch(Request::create($str.'/1', 'GET'));
$values = array_values($_SERVER['__test.controller_callAction_parameters']);
$this->assertInstanceOf('Illuminate\Http\Request', $values[0]);
$this->assertEquals(1, $values[1]->value);
$this->assertNull($values[2]);
$this->assertInstanceOf('Illuminate\Tests\Routing\RoutingTestTeamModel', $values[3]);
}
class RouteTestAnotherControllerWithParameterStub extends Controller
{
public function callAction($method, $parameters)
{
$_SERVER['__test.controller_callAction_parameters'] = $parameters;
}
public function withModels(Request $request, RoutingTestUserModel $user, $defaultNull = null, RoutingTestTeamModel $team = null)
{
}
}
class RoutingTestUserModel extends Model
{
public function getRouteKeyName()
{
return 'id';
}
public function where($key, $value)
{
$this->value = $value;
return $this;
}
public function first()
{
return $this;
}
public function firstOrFail()
{
return $this;
}
}
class RoutingTestTeamModel extends Model
{
public function getRouteKeyName()
{
return 'id';
}
public function where($key, $value)
{
$this->value = $value;
return $this;
}
public function first()
{
return $this;
}
public function firstOrFail()
{
return $this;
}
}
路由显示绑定
除了隐示地转化路由参数外,我们还可以给路由参数显示提供绑定。显示绑定有bind、model两种方法。
- 通过bind为参数绑定闭包函数:
1 | public function testRouteBinding() |
通过bind为参数绑定类方法,可以指定classname@method,也可以直接使用类名,默认会调用类的bind函数:1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38public function testRouteClassBinding()
{
$router = $this->getRouter();
$router->get('foo/{bar}', ['middleware' => SubstituteBindings::class, 'uses' => function ($name) {
return $name;
}]);
$router->bind('bar', 'Illuminate\Tests\Routing\RouteBindingStub');
$this->assertEquals('TAYLOR', $router->dispatch(Request::create('foo/taylor', 'GET'))->getContent());
}
public function testRouteClassMethodBinding()
{
$router = $this->getRouter();
$router->get('foo/{bar}', ['middleware' => SubstituteBindings::class, 'uses' => function ($name) {
return $name;
}]);
$router->bind('bar', 'Illuminate\Tests\Routing\RouteBindingStub@find');
$this->assertEquals('dragon', $router->dispatch(Request::create('foo/Dragon', 'GET'))->getContent());
}
class RouteBindingStub
{
public function bind($value, $route)
{
return strtoupper($value);
}
public function find($value, $route)
{
return strtolower($value);
}
}
通过model为参数绑定数据库模型,路由的参数就不需要和控制器方法中的变量名相同,Laravel会利用路由参数的值去调用where方法查找对应记录:
1 | if ($model = $instance->where($instance->getRouteKeyName(), $value)->first()) { |
测试样例如下:1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31public function testModelBinding()
{
$router = $this->getRouter();
$router->get('foo/{bar}', ['middleware' => SubstituteBindings::class, 'uses' => function($name) {
return $name;
}]);
$router->model('bar', 'Illuminate\Tests\Routing\RouteModelBindingStub');
$this->assertEquals('TAYLOR', $router->dispatch(Request::create('foo/taylor', 'GET'))->getContent());
}
class RouteModelBindingStub
{
public function getRouteKeyName()
{
return 'id';
}
public function where($key, $value)
{
$this->value = $value;
return $this;
}
public function first()
{
return strtoupper($this->value);
}
}
若绑定的model并没有找到对应路由参数的记录,可以在model中定义一个闭包函数,路由参数会调用闭包函数:
1 | public function testModelBindingWithCustomNullReturn() |
router扩展方法
router支持添加自定义的方法,只需要利用macro函数来注册对应的函数名和函数实现:
1 | public function testMacro() |