Laravel 路由
路由构造总览
构造方法有:
Route::get、Route::post、Route::put、Route::patch、Route::delete、Route::options、Route::any、Route::match、Route::resource、Route::resources、Route::group
Route::get('foo', function () {
// 基本方式
});
Route::match(['get', 'post'], '/', function () {
// 基本方式
});
Route::any('foo', function () {
// 基本方式
});
Route::get('posts/{post}/comments/{comment}', function ($postId, $commentId) {
// 必选路由参数
});
Route::get('user/{name?}', function ($name = 'John') {
// 可选路由参数
});
Route::get('user/{id}/{name}', function ($id, $name) {
// 正则表达式约束
})->where(['id' => '[0-9]+', 'name' => '[a-z]+']);
// 全局约束 RouteServiceProvider 的 boot 方法
public function boot()
{
Route::pattern('id', '[0-9]+');
parent::boot();
}
Route::get('user/{id}', function ($id) {
// 仅在 {id} 为数字时执行...
});
Route::get('user/profile', function () {
// 命名路由
})->name('profile');
Route::get('user/profile', 'UserController@showProfile')->name('profile');
为命名路由生成:
// 生成 URL...
$url = route('profile');
// 生成重定向...
return redirect()->route('profile');
// 路由组
Route::group(['middleware' => 'auth'], function () {
Route::get('/', function () {
// 使用 `Auth` 中间件
});
Route::get('user/profile', function () {
// 使用 `Auth` 中间件
});
});
命名空间|子域名路由|路由前缀
Route::group(['namespace' => 'Admin','domain' => '{account}.myapp.com','prefix' => 'admin'], function () {
// 在 "App\Http\Controllers\Admin" 命名空间下,子域名为{account}.myapp.com,路由前缀匹配 '/admin' 的控制器
});
Route::resource('photo', 'PhotoController', ['except' => ['create', 'store', 'update', 'destroy'], 'names' => ['create' => 'photo.build'],'middleware' => []);
路由模型绑定
隐式绑定#
Laravel 会自动解析定义在路由或控制器方法(方法包含和路由片段匹配的已声明类型变量)中的 Eloquent 模型
Route::get('api/users/{user}', function (App\User $user) {
return $user->email;
});
显式绑定
RouteServiceProvider 类中的 boot 方法
public function boot()
{
parent::boot();
Route::model('user', App\User::class);
}
Route::get('profile/{user}', function (App\User $user) {
//
});
自定义解析逻辑
public function boot()
{
parent::boot();
Route::bind('user', function ($value) {
return App\User::where('name', $value)->first();
});
}
基本有以下几种形式:uri 分为是否带有参数, action 分为匿名函数或者 Controller@Method 形式,可能还会带一些其他的前置操作
基本构造
Route::get、Route::post、Route::put、Route::patch、Route::delete、Route::options、Route::any、Route::match
以上的构造方法本质是一样的,区别在于第一个参数
public function get($uri, $action = null)
{
return $this->addRoute(['GET', 'HEAD'], $uri, $action);
}
protected function addRoute($methods, $uri, $action)
{
// 创建 $route(\Illuminate\Routing\Route) 对象并加入到集合(\Illuminate\Routing\RouteCollection 路由集合辅助类)里,再返回 $route
return $this->routes->add($this->createRoute($methods, $uri, $action));
}
protected function createRoute($methods, $uri, $action)
{
// $action 若为 Controller@Method|['uses'=>Controller@Method] 形式
if ($this->actionReferencesController($action)) {
$action = $this->convertToControllerAction($action);
}
$route = $this->newRoute(
$methods, $this->prefix($uri), $action
);
// 如果前缀条件栈不为空,则对 $route 进行相应的设置
if ($this->hasGroupStack()) {
$this->mergeGroupAttributesIntoRoute($route);
}
// 将 where 前置条件注入到 $route 对象
$this->addWhereClausesToRoute($route);
return $route;
}
protected function actionReferencesController($action)
{
if (! $action instanceof Closure) {
return is_string($action) || (isset($action['uses']) && is_string($action['uses']));
}
return false;
}
protected function convertToControllerAction($action)
{
if (is_string($action)) {
$action = ['uses' => $action];
}
// 尝试加入前置条件 namespace
if (! empty($this->groupStack)) {
$action['uses'] = $this->prependGroupNamespace($action['uses']);
}
// 通过控制器来获取 action
$action['controller'] = $action['uses'];
// 类似:['controller'=>'namespace\Controller@Method', 'uses'=>'namespace\Controller@Method']
return $action;
}
// $uri 尝试增加前置条件 prefix(group 组中的 prefix,对应给下面的所有路由增加)
protected function prefix($uri)
{
return trim(trim($this->getLastGroupPrefix(), '/').'/'.trim($uri, '/'), '/') ?: '/';
}
public function getLastGroupPrefix()
{
if (! empty($this->groupStack)) {
$last = end($this->groupStack);
return isset($last['prefix']) ? $last['prefix'] : '';
}
return '';
}
protected function newRoute($methods, $uri, $action)
{
return (new Route($methods, $uri, $action))
->setRouter($this)
->setContainer($this->container);
}
// new Route
public function __construct($methods, $uri, $action)
{
$this->uri = $uri;
$this->methods = (array) $methods;
$this->action = $this->parseAction($action);
if (in_array('GET', $this->methods) && ! in_array('HEAD', $this->methods)) {
$this->methods[] = 'HEAD';
}
// 再尝试给 uri 单独的加入 prefix 前缀
if (isset($this->action['prefix'])) {
$this->prefix($this->action['prefix']);
}
}
protected function parseAction($action)
{
// 委托 RouteAction action 辅助类进行解析
return RouteAction::parse($this->uri, $action);
}
public static function parse($uri, $action)
{
if (is_null($action)) {
return static::missingAction($uri); // 抛异常
}
// 匿名函数
if (is_callable($action)) {
return ['uses' => $action];
}
elseif (! isset($action['uses'])) {
$action['uses'] = static::findCallable($action);
}
// 如果 $action['uses'] 类似 Controller 形式,则尝试构造为 Controller@__invoke 形式,即没有指定方法时调用 __invoke 方法
if (is_string($action['uses']) && ! Str::contains($action['uses'], '@')) {
$action['uses'] = static::makeInvokable($action['uses']);
}
return $action;
}
protected static function findCallable(array $action)
{
// 尝试从 $action 数组找到第一个满足可调用且为数字键的值作为 $action 返回
return Arr::first($action, function ($value, $key) {
return is_callable($value) && is_numeric($key);
});
}
public function hasGroupStack()
{
return ! empty($this->groupStack);
}
protected function mergeGroupAttributesIntoRoute($route)
{
$route->setAction($this->mergeWithLastGroup($route->getAction()));
}
public function mergeWithLastGroup($new)
{
// 使用上一层的 groupStack 设置
return RouteGroup::merge($new, end($this->groupStack));
}
protected function addWhereClausesToRoute($route)
{
$route->where(array_merge(
$this->patterns, isset($route->getAction()['where']) ? $route->getAction()['where'] : []
));
return $route;
}
// 返回 \Illuminate\Routing\Route 对象
public function add(Route $route)
{
// 设置路由以何种方式放入路由集合,待后续按此种方式来获取
$this->addToCollections($route);
$this->addLookups($route);
return $route;
}
protected function addToCollections($route)
{
$domainAndUri = $route->domain().$route->uri();
// 表面可以通过 method 和 uri 来获取路由
foreach ($route->methods() as $method) {
$this->routes[$method][$domainAndUri] = $route;
}
$this->allRoutes[$method.$domainAndUri] = $route;
}
protected function addLookups($route)
{
$action = $route->getAction();
// 如果前置条件栈设置了 as ,则将 $route 注入到 $this->nameList,即可以通过名字来获取路由
if (isset($action['as'])) {
$this->nameList[$action['as']] = $route;
}
if (isset($action['controller'])) {
$this->addToActionList($action, $route);
}
}
protected function addToActionList($action, $route)
{
// 表示可以通过控制器获取路由
$this->actionList[trim($action['controller'], '\\')] = $route;
}
流程小结(创建 route ,并将加入到路由集合里进行统一的管理)
根据 action 的形式和前置条件,或转为数组(['use'=> Clause|namespaceController@Method]),或为匿名函数
根据前置条件,或将组 uri 加前缀
创建 route 对象,并将 action 统一为数组,再进行一些其他设置
若存在前置条件,则加入到 route 对象的 action 数组
route 对象加入 where 条件
其他构造
Route::group
public function group(array $attributes, $routes)
{
$this->updateGroupStack($attributes);
$this->loadRoutes($routes);
array_pop($this->groupStack);
}
protected function updateGroupStack(array $attributes)
{
if (! empty($this->groupStack)) {
$attributes = RouteGroup::merge($attributes, end($this->groupStack));
}
$this->groupStack[] = $attributes;
}
protected function loadRoutes($routes)
{
if ($routes instanceof Closure) {
$routes($this); // 注意:每个匿名函数都会有 router 对象
} else {
$router = $this;
require $routes;
}
}
小结
主要通过设置前置条件栈($groupStack),然后运用到组内的所有成员,本质还是基本构造
Route::resource、Route::resources
public function resource($name, $controller, array $options = [])
{
if ($this->container && $this->container->bound(ResourceRegistrar::class)) {
$registrar = $this->container->make(ResourceRegistrar::class);
} else {
$registrar = new ResourceRegistrar($this);
}
$registrar->register($name, $controller, $options);
}
public function __construct(Router $router)
{
$this->router = $router;
}
public function register($name, $controller, array $options = [])
{
if (isset($options['parameters']) && ! isset($this->parameters)) {
$this->parameters = $options['parameters'];
}
if (Str::contains($name, '/')) {
$this->prefixedResource($name, $controller, $options);
return;
}
$base = $this->getResourceWildcard(last(explode('.', $name)));
// ['index', 'create', 'store', 'show', 'edit', 'update', 'destroy']
$defaults = $this->resourceDefaults;
// 生成相应条件下的路由
foreach ($this->getResourceMethods($defaults, $options) as $m) {
$this->{'addResource'.ucfirst($m)}($name, $base, $controller, $options);
}
}
protected function prefixedResource($name, $controller, array $options)
{
list($name, $prefix) = $this->getResourcePrefix($name);
// $me 为 router 对象。本质是将 $name 为 'xx/yy/zz' 的 resource 请求转化为 groupStack 追加 ['prefix'=>'xx/yy'] 的 group 组内请求,对应的匿名函数依然是 $name 为 'zz' 的 resource 请求
$callback = function ($me) use ($name, $controller, $options) {
$me->resource($name, $controller, $options);
};
return $this->router->group(compact('prefix'), $callback);
}
protected function getResourcePrefix($name)
{
$segments = explode('/', $name);
$prefix = implode('/', array_slice($segments, 0, -1));
// 假如 $name 为 'xx/yy/zz', 则返回 ['zz', 'xx/yy']
return [end($segments), $prefix];
}
// 优先从设置里面取值,没有则生成单数形式的字符串,并将字符 '-' 替换为 '_'
public function getResourceWildcard($value)
{
if (isset($this->parameters[$value])) {
$value = $this->parameters[$value];
} elseif (isset(static::$parameterMap[$value])) {
$value = static::$parameterMap[$value];
} elseif ($this->parameters === 'singular' || static::$singularParameters) {
$value = Str::singular($value);
}
return str_replace('-', '_', $value);
}
protected function getResourceMethods($defaults, $options)
{
if (isset($options['only'])) {
return array_intersect($defaults, (array) $options['only']);
} elseif (isset($options['except'])) {
return array_diff($defaults, (array) $options['except']);
}
return $defaults;
}
protected function addResourceIndex($name, $base, $controller, $options)
{
$uri = $this->getResourceUri($name);
$action = $this->getResourceAction($name, $controller, 'index', $options);
return $this->router->get($uri, $action);
}
public function getResourceUri($resource)
{
if (! Str::contains($resource, '.')) {
return $resource;
}
$segments = explode('.', $resource);
$uri = $this->getNestedResourceUri($segments);
// 'xx/{xx}/yy/{yy}/zz'
return str_replace('/{'.$this->getResourceWildcard(end($segments)).'}', '', $uri);
}
protected function getNestedResourceUri(array $segments)
{
// ['xx','yy','zz'] => 'xx/{xx}/yy/{yy}/zz/{zz}'
return implode('/', array_map(function ($s) {
return $s.'/{'.$this->getResourceWildcard($s).'}';
}, $segments));
}
protected function getResourceAction($resource, $controller, $method, $options)
{
$name = $this->getResourceRouteName($resource, $method, $options);
$action = ['as' => $name, 'uses' => $controller.'@'.$method];
if (isset($options['middleware'])) {
$action['middleware'] = $options['middleware'];
}
return $action;
}
protected function getResourceRouteName($resource, $method, $options)
{
$name = $resource;
if (isset($options['names'])) {
if (is_string($options['names'])) {
$name = $options['names'];
} elseif (isset($options['names'][$method])) {
return $options['names'][$method];
}
}
$prefix = isset($options['as']) ? $options['as'].'.' : '';
return trim(sprintf('%s%s.%s', $prefix, $name, $method), '.');
}
小结
资源类型的构造,实际上会被转化为构造多个默认资源的路由,本质依然是基本构造