照猫画虎 实现 min-laravel 框架系列之路由
- laravel
- 2020-08-14
- 129
- 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);// 清除 facadeFacade::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();}}