照猫画虎 实现 min-laravel 框架系列之路由
- laravel
- 2020-08-14
- 3591
- 0
路由
laravel 系统的路由部分也是使用 symfony 提供的 Routing 组件
整体分三个阶段:
- 注册路由
- 路由寻找
- 执行路由
加入依赖
在 minlaravelframework/framework 的 composer.json 配置文件中添加依赖包
"require": {
....
"symfony/dotenv": "^5.0",
"vlucas/phpdotenv": "^4.0",
"symfony/routing": "^5.0",
"symfony/http-kernel": "^5.0"
},
在 minlaravel 目录下更新, composer update
类汇总
- Illuminate\Routing\Router 总管类,对外提供服务的类,
- Illuminate\Routing\RouteCollection 路由集合类
- Illuminate\Routing\Route 路由类,每条路由对应一个类
- Illuminate\Routing\RouteRegistrar 实现路由嵌套功能
- Illuminate\Routing\UrlGenerator url 相关类
- Illuminate\Routing\RouteFileRegistrar 加载路由文件的类
- Illuminate\Routing\RouteGroup 路由属性的合并
说明:
1、laravel 系统中实际上对外提供路由功能的类是 Router 类,但是系统实现了一个门面名字取得确实 Route, 这块容易让人搞晕
2、为了使用上美观,在路由这块,laravel 使用了大量的魔术方法
路由文件的加载
在 laravel 系统中,config/app.php 配置了路由服务提供者 App\Providers\RouteServiceProvider::class,通过此类,完成路由配置文件的加载
App\Providers\RouteServiceProvider 服务提供者
该类继承 Illuminate\Foundation\Support\Providers\RouteServiceProvider ,核心方法为 boot 和 map
boot 方法
该方法主要在父类中实现,即 Illuminate\Foundation\Support\Providers\RouteServiceProvider 类
public function boot()
{
// 设置控制器命名空间
$this->setRootControllerNamespace();
// 路由文件缓存,暂时不考虑 待定
if ($this->routesAreCached()) {
$this->loadCachedRoutes();
} else {
// 加载路由,调用 map 方法
$this->loadRoutes();
// 注册一些启动事件
$this->app->booted(function () {
$this->app['router']->getRoutes()->refreshNameLookups();
$this->app['router']->getRoutes()->refreshActionLookups();
});
}
}
说明:
$this->app['router']->getRoutes()
对应的 RouteCollection 类,主要功能是在加载完 web.php 和 api.php 之后,更新两个内存变量,方便后边使用
map 方法
public function map()
{
$this->mapApiRoutes();
$this->mapWebRoutes();
}
// web.php 的路由加载、这里的Route 本质上对应的 Router 类提供的功能,不要乱了
protected function mapWebRoutes()
{
Route::middleware('web')
->namespace($this->namespace)
->group(base_path('routes/web.php'));
}
// api.php 对应的路由配置
protected function mapApiRoutes()
{
Route::prefix('api')
->middleware('api')
->namespace($this->namespace)
->group(base_path('routes/api.php'));
}
说明:
在 group 中,会加载 routes 路径下的 web.php | api.php 两个路由文件,prefix、namespace、group 都是 Router 的类方法,但是通过 php 的魔术方法,使得出现上边的静态式调用的形式
路由加载
假如在 routes/web.php 文件下,我们配置如下路由,以此来解释 laravel 框架解析加载路由的过程
Route::get('/index', function () {
echo "ok";
});
Route::get('home1', 'HomeController@index');
Route::get('home2/{id}', 'HomeController@index');
路由加载过程:因为存在路由嵌套的功能,所以,大致步骤如下:
- 创建 Route 类,并处理 methods、uri、action 相关参数
- 通过 groupStack 判断解析栈,需要合并处理一些属性,ex:namespace、prefix等
- 将路由类 Route 添加到 RouteCollection 类变量中,方便以后路由寻址的时候使用
Illuminate\Routing\Route 路由类
在 web.php | api.php 中的路由配置,每一条路由对应一个 Route 实例类
初始化函数
methods :
(
[0] => GET
[1] => HEAD
)
uri :
home2/{id}
action:
(
[prefix] => prefix
[uses] => App\Http\Controllers\HomeController@index
[controller] => App\Http\Controllers\HomeController@index
)
public function __construct($methods, $uri, $action)
{
$this->uri = $uri;
$this->methods = (array) $methods;
$this->action = Arr::except($this->parseAction($action), ['prefix']);
// 这一步不知道有什么用处
if (in_array('GET', $this->methods) && ! in_array('HEAD', $this->methods)) {
$this->methods[] = 'HEAD';
}
$this->prefix(is_array($action) ? Arr::get($action, 'prefix') : '');
}
parseAction 方法
该方法尝试去解析 action 参数
// RouteAction 提供 action 解析方法
protected function parseAction($action)
{
return RouteAction::parse($this->uri, $action);
}
RouteAction::parse 方法
public static function parse($uri, $action)
{
// 没有设置 action 时,处理办法
if (is_null($action)) {
return static::missingAction($uri);
}
// action 是一个闭包时,action 如果是数组的话,就是 [Obj,method]
if (is_callable($action, true)) {
return ! is_array($action) ? ['uses' => $action] : [
'uses' => $action[0].'@'.$action[1],
'controller' => $action[0].'@'.$action[1],
];
} elseif (! isset($action['uses'])) {
// 在 action 中找到一个可执行的闭包函数,作为路由对应的处理函数
$action['uses'] = static::findCallable($action);
}
// 如果 uses 的格式不为 obj@method 是,需要做一步处理
if (is_string($action['uses']) && ! Str::contains($action['uses'], '@')) {
$action['uses'] = static::makeInvokable($action['uses']);
}
return $action;
}
// 封装一个会抛出异常的闭包,只有当访问到此路由时才会报错
protected static function missingAction($uri)
{
return ['uses' => function () use ($uri) {
throw new LogicException("Route for [{$uri}] has no action.");
}];
}
// 在 action 中找一个可以执行的闭包函数,作为请求处理函数
protected static function findCallable(array $action)
{
return Arr::first($action, function ($value, $key) {
return is_callable($value) && is_numeric($key);
});
}
// 使用了php魔术方法 __invoke,可以 obj() 调用
protected static function makeInvokable($action)
{
if (! method_exists($action, '__invoke')) {
throw new UnexpectedValueException("Invalid route action: [{$action}].");
}
return $action.'@__invoke';
}
prefix 方法
public function prefix($prefix)
{
$this->updatePrefixOnAction($prefix);
$uri = rtrim($prefix, '/').'/'.ltrim($this->uri, '/');
return $this->setUri($uri !== '/' ? trim($uri, '/') : $uri);
}
Illuminate\Routing\RouteCollection 路由集合类
add 方法
添加一个路由类 Route 到此集合中
public function add(Route $route)
{
$this->addToCollections($route);
$this->addLookups($route);
return $route;
}
addToCollections 方法
更新 routes 和 allRoutes 属性,数组下标不一样,不通用途
protected function addToCollections($route)
{
$domainAndUri = $route->getDomain().$route->uri();
$method = '';
foreach ($route->methods() as $method) {
$this->routes[$method][$domainAndUri] = $route;
}
$this->allRoutes[$method.$domainAndUri] = $route;
}
addLookups 方法
更新 nameList 、actionList 属性
protected function addLookups($route)
{
//
if ($name = $route->getName()) {
$this->nameList[$name] = $route;
}
//
$action = $route->getAction();
if (isset($action['controller'])) {
$this->addToActionList($action, $route);
}
}
protected function addToActionList($action, $route)
{
$this->actionList[trim($action['controller'], '\\')] = $route;
}
Illuminate\Routing\Router 路由处理类
这个类是提供路由对外函数的类,包括 group、get、post 等方法
get 方法
// post | post | put | delete | options | any 方法都一样
public function get($uri, $action = null)
{
return $this->addRoute(['GET', 'HEAD'], $uri, $action);
}
// 这里的 routes 指 RouteCollection 类
public function addRoute($methods, $uri, $action)
{
return $this->routes->add($this->createRoute($methods, $uri, $action));
}
createRoute 方法
// 创建路由类
protected function createRoute($methods, $uri, $action)
{
// 如果处理类是一个控制器类,解析一下
if ($this->actionReferencesController($action)) {
$action = $this->convertToControllerAction($action);
}
// new 一个 Route 类
$route = $this->newRoute(
$methods, $this->prefix($uri), $action
);
// 如果嵌套的话,需要处理一些属性值
if ($this->hasGroupStack()) {
$this->mergeGroupAttributesIntoRoute($route);
}
// 添加路由的条件
$this->addWhereClausesToRoute($route);
return $route;
}
public function newRoute($methods, $uri, $action)
{
return (new Route($methods, $uri, $action))
->setRouter($this)
->setContainer($this->container);
}
mergeGroupAttributesIntoRoute 方法
合并当前栈中的数据
protected function mergeGroupAttributesIntoRoute($route)
{
$route->setAction($this->mergeWithLastGroup(
$route->getAction(),
$prependExistingPrefix = false
));
}
// RouteGroup 是处理属性合并类
// namespace、prefix、where 这三个属性会叠加合并
// domain 直接替换
// 其余属性直接覆盖
public function mergeWithLastGroup($new, $prependExistingPrefix = true)
{
return RouteGroup::merge($new, end($this->groupStack), $prependExistingPrefix);
}
addWhereClausesToRoute 方法
处理路由条件
protected function addWhereClausesToRoute($route)
{
$route->where(array_merge(
$this->patterns, $route->getAction()['where'] ?? []
));
return $route;
}
路由寻址
添加 facade 和 别名配置
// config/app
'aliases' => [
....
'Response' => Illuminate\Support\Facades\Response::class,
],
// Illuminate\Foundation\Application::registerCoreContainerAliases
[
'router' => [\Illuminate\Routing\Router::class, \Illuminate\Contracts\Routing\Registrar::class, \Illuminate\Contracts\Routing\BindingRegistrar::class],
'session' => [\Illuminate\Session\SessionManager::class],
]
Illuminate\Foundation\Http\Kernel 处理请求
laravel 利用管道模式实现了请求中间件功能,在这里暂时不考虑这个;
handle 方法
public function handle($request)
{
try {
$request->enableHttpMethodParameterOverride();
$response = $this->sendRequestThroughRouter($request);
} catch (Throwable $e) {
$this->reportException($e);
$response = $this->renderException($request, $e);
}
$this->app['events']->dispatch(
new RequestHandled($request, $response)
);
return $response;
}
protected function sendRequestThroughRouter($request)
{
// 绑定实例
$this->app->instance('request', $request);
// 清除 facade
Facade::clearResolvedInstance('request');
// 引导程序启动
$this->bootstrap();
// 利用管道过滤并处理请求
return (new Pipeline($this->app))
->send($request)
->through($this->app->shouldSkipMiddleware() ? [] : $this->middleware)
->then($this->dispatchToRouter());
// 为了暂时不管管道逻辑,这里可以改为
return $this->dispatchToRouter()($request);
}
dispatchToRouter 方法
通过这个函数可以看出,最终触发路由寻址的是 Illuminate\Routing\Router:: dispatch 方法
protected function dispatchToRouter()
{
return function ($request) {
$this->app->instance('request', $request);
return $this->router->dispatch($request);
};
}
Illuminate\Routing\Router 路由寻址部分
dispatch 方法
public function dispatch(Request $request)
{
$this->currentRequest = $request;
return $this->dispatchToRoute($request);
}
// 处理请求,并且返回 Response 影响
public function dispatchToRoute(Request $request)
{
return $this->runRoute($request, $this->findRoute($request));
}
findRoute 方法
// routes 指的是 Illuminate\Routing\RouteCollection 类
protected function findRoute($request)
{
$this->current = $route = $this->routes->match($request);
$this->container->instance(Route::class, $route);
return $route;
}
runRoute 方法
在找到路由类之后,生成响应
Illuminate\Routing\RouteCollection 路由寻址部分
match 方法
public function match(Request $request)
{
// 依据请求方法获取该类型下所有路由,ex:如果是 GET 请求,会返回所有的 GET 路由类,方便下边查询
$routes = $this->get($request->getMethod());
$route = $this->matchAgainstRoutes($routes, $request);
return $this->handleMatchedRoute($request, $route);
}
// 匹配路由
protected function matchAgainstRoutes(array $routes, $request, $includingMethod = true)
{
// 依据 isFallback 分成两类
[$fallbacks, $routes] = collect($routes)->partition(function ($route) {
return $route->isFallback;
});
// 把闭包类放在最后,
return $routes->merge($fallbacks)->first(function (Route $route) use ($request, $includingMethod) {
return $route->matches($request, $includingMethod);
});
}
handleMatchedRoute 方法
处理匹配到的路由
protected function handleMatchedRoute(Request $request, $route)
{
if (! is_null($route)) {
return $route->bind($request);
}
// 如果没有找到,找找其他方法下有没有对应的路由,ex:如果是 GET,但是没有找到路由,那么就在 POST、PUT 等方法中找找,看有没有
$others = $this->checkForAlternateVerbs($request);
if (count($others) > 0) {
return $this->getRouteForMethods($request, $others);
}
throw new NotFoundHttpException;
}
Illuminate\Routing\Route 路由寻址部分
matches 方法
public function matches(Request $request, $includingMethod = true)
{
// 每一个遍历过的 Route 类都会转成 Symfony\Component\Routing\CompiledRoute 类,这个有点费解
$this->compileRoute();
foreach ($this->getValidators() as $validator) {
if (! $includingMethod && $validator instanceof MethodValidator) {
continue;
}
if (! $validator->matches($this, $request)) {
return false;
}
}
return true;
}
// 找到所有的验证器,验证规则
public static function getValidators()
{
if (isset(static::$validators)) {
return static::$validators;
}
// To match the route, we will use a chain of responsibility pattern with the
// validator implementations. We will spin through each one making sure it
// passes and then we will know if the route as a whole matches request.
return static::$validators = [
new UriValidator, new MethodValidator,
new SchemeValidator, new HostValidator,
];
}
bind 方法
主要处理路由的参数
public function bind(Request $request)
{
$this->compileRoute();
$this->parameters = (new RouteParameterBinder($this))
->parameters($request);
$this->originalParameters = $this->parameters;
return $this;
}
run 方法
运行路由的 action,
public function run()
{
$this->container = $this->container ?: new Container;
try {
if ($this->isControllerAction()) {
return $this->runController();
}
return $this->runCallable();
} catch (HttpResponseException $e) {
return $e->getResponse();
}
}