照猫画虎 实现 min-laravel 框架系列之路由

php   laravel  

路由

laravel 系统的路由部分也是使用 symfony 提供的 Routing 组件

整体分三个阶段:

  • 注册路由
  • 路由寻找
  • 执行路由

加入依赖

  • 在 minlaravelframework/framework 的 composer.json 配置文件中添加依赖包

    1. "require": {
    2. ....
    3. "symfony/dotenv": "^5.0",
    4. "vlucas/phpdotenv": "^4.0",
    5. "symfony/routing": "^5.0",
    6. "symfony/http-kernel": "^5.0"
    7. },
  • 在 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 类

  1. public function boot()
  2. {
  3. // 设置控制器命名空间
  4. $this->setRootControllerNamespace();
  5. // 路由文件缓存,暂时不考虑 待定
  6. if ($this->routesAreCached()) {
  7. $this->loadCachedRoutes();
  8. } else {
  9. // 加载路由,调用 map 方法
  10. $this->loadRoutes();
  11. // 注册一些启动事件
  12. $this->app->booted(function () {
  13. $this->app['router']->getRoutes()->refreshNameLookups();
  14. $this->app['router']->getRoutes()->refreshActionLookups();
  15. });
  16. }
  17. }

说明:
$this->app['router']->getRoutes() 对应的 RouteCollection 类,主要功能是在加载完 web.php 和 api.php 之后,更新两个内存变量,方便后边使用

map 方法

  1. public function map()
  2. {
  3. $this->mapApiRoutes();
  4. $this->mapWebRoutes();
  5. }
  6. // web.php 的路由加载、这里的Route 本质上对应的 Router 类提供的功能,不要乱了
  7. protected function mapWebRoutes()
  8. {
  9. Route::middleware('web')
  10. ->namespace($this->namespace)
  11. ->group(base_path('routes/web.php'));
  12. }
  13. // api.php 对应的路由配置
  14. protected function mapApiRoutes()
  15. {
  16. Route::prefix('api')
  17. ->middleware('api')
  18. ->namespace($this->namespace)
  19. ->group(base_path('routes/api.php'));
  20. }

说明:
在 group 中,会加载 routes 路径下的 web.php | api.php 两个路由文件,prefix、namespace、group 都是 Router 的类方法,但是通过 php 的魔术方法,使得出现上边的静态式调用的形式

路由加载

假如在 routes/web.php 文件下,我们配置如下路由,以此来解释 laravel 框架解析加载路由的过程

  1. Route::get('/index', function () {
  2. echo "ok";
  3. });
  4. Route::get('home1', 'HomeController@index');
  5. Route::get('home2/{id}', 'HomeController@index');

路由加载过程:因为存在路由嵌套的功能,所以,大致步骤如下:

  • 创建 Route 类,并处理 methods、uri、action 相关参数
  • 通过 groupStack 判断解析栈,需要合并处理一些属性,ex:namespace、prefix等
  • 将路由类 Route 添加到 RouteCollection 类变量中,方便以后路由寻址的时候使用

Illuminate\Routing\Route 路由类

在 web.php | api.php 中的路由配置,每一条路由对应一个 Route 实例类

初始化函数

  1. methods
  2. (
  3. [0] => GET
  4. [1] => HEAD
  5. )
  6. uri
  7. home2/{id}
  8. action:
  9. (
  10. [prefix] => prefix
  11. [uses] => App\Http\Controllers\HomeController@index
  12. [controller] => App\Http\Controllers\HomeController@index
  13. )
  14. public function __construct($methods, $uri, $action)
  15. {
  16. $this->uri = $uri;
  17. $this->methods = (array) $methods;
  18. $this->action = Arr::except($this->parseAction($action), ['prefix']);
  19. // 这一步不知道有什么用处
  20. if (in_array('GET', $this->methods) && ! in_array('HEAD', $this->methods)) {
  21. $this->methods[] = 'HEAD';
  22. }
  23. $this->prefix(is_array($action) ? Arr::get($action, 'prefix') : '');
  24. }

parseAction 方法

该方法尝试去解析 action 参数

  1. // RouteAction 提供 action 解析方法
  2. protected function parseAction($action)
  3. {
  4. return RouteAction::parse($this->uri, $action);
  5. }

RouteAction::parse 方法

  1. public static function parse($uri, $action)
  2. {
  3. // 没有设置 action 时,处理办法
  4. if (is_null($action)) {
  5. return static::missingAction($uri);
  6. }
  7. // action 是一个闭包时,action 如果是数组的话,就是 [Obj,method]
  8. if (is_callable($action, true)) {
  9. return ! is_array($action) ? ['uses' => $action] : [
  10. 'uses' => $action[0].'@'.$action[1],
  11. 'controller' => $action[0].'@'.$action[1],
  12. ];
  13. } elseif (! isset($action['uses'])) {
  14. // 在 action 中找到一个可执行的闭包函数,作为路由对应的处理函数
  15. $action['uses'] = static::findCallable($action);
  16. }
  17. // 如果 uses 的格式不为 obj@method 是,需要做一步处理
  18. if (is_string($action['uses']) && ! Str::contains($action['uses'], '@')) {
  19. $action['uses'] = static::makeInvokable($action['uses']);
  20. }
  21. return $action;
  22. }
  23. // 封装一个会抛出异常的闭包,只有当访问到此路由时才会报错
  24. protected static function missingAction($uri)
  25. {
  26. return ['uses' => function () use ($uri) {
  27. throw new LogicException("Route for [{$uri}] has no action.");
  28. }];
  29. }
  30. // 在 action 中找一个可以执行的闭包函数,作为请求处理函数
  31. protected static function findCallable(array $action)
  32. {
  33. return Arr::first($action, function ($value, $key) {
  34. return is_callable($value) && is_numeric($key);
  35. });
  36. }
  37. // 使用了php魔术方法 __invoke,可以 obj() 调用
  38. protected static function makeInvokable($action)
  39. {
  40. if (! method_exists($action, '__invoke')) {
  41. throw new UnexpectedValueException("Invalid route action: [{$action}].");
  42. }
  43. return $action.'@__invoke';
  44. }

prefix 方法

  1. public function prefix($prefix)
  2. {
  3. $this->updatePrefixOnAction($prefix);
  4. $uri = rtrim($prefix, '/').'/'.ltrim($this->uri, '/');
  5. return $this->setUri($uri !== '/' ? trim($uri, '/') : $uri);
  6. }

Illuminate\Routing\RouteCollection 路由集合类

add 方法

添加一个路由类 Route 到此集合中

  1. public function add(Route $route)
  2. {
  3. $this->addToCollections($route);
  4. $this->addLookups($route);
  5. return $route;
  6. }

addToCollections 方法

更新 routes 和 allRoutes 属性,数组下标不一样,不通用途

  1. protected function addToCollections($route)
  2. {
  3. $domainAndUri = $route->getDomain().$route->uri();
  4. $method = '';
  5. foreach ($route->methods() as $method) {
  6. $this->routes[$method][$domainAndUri] = $route;
  7. }
  8. $this->allRoutes[$method.$domainAndUri] = $route;
  9. }

addLookups 方法

更新 nameList 、actionList 属性

  1. protected function addLookups($route)
  2. {
  3. //
  4. if ($name = $route->getName()) {
  5. $this->nameList[$name] = $route;
  6. }
  7. //
  8. $action = $route->getAction();
  9. if (isset($action['controller'])) {
  10. $this->addToActionList($action, $route);
  11. }
  12. }
  13. protected function addToActionList($action, $route)
  14. {
  15. $this->actionList[trim($action['controller'], '\\')] = $route;
  16. }

Illuminate\Routing\Router 路由处理类

这个类是提供路由对外函数的类,包括 group、get、post 等方法

get 方法

  1. // post | post | put | delete | options | any 方法都一样
  2. public function get($uri, $action = null)
  3. {
  4. return $this->addRoute(['GET', 'HEAD'], $uri, $action);
  5. }
  6. // 这里的 routes 指 RouteCollection 类
  7. public function addRoute($methods, $uri, $action)
  8. {
  9. return $this->routes->add($this->createRoute($methods, $uri, $action));
  10. }

createRoute 方法

  1. // 创建路由类
  2. protected function createRoute($methods, $uri, $action)
  3. {
  4. // 如果处理类是一个控制器类,解析一下
  5. if ($this->actionReferencesController($action)) {
  6. $action = $this->convertToControllerAction($action);
  7. }
  8. // new 一个 Route 类
  9. $route = $this->newRoute(
  10. $methods, $this->prefix($uri), $action
  11. );
  12. // 如果嵌套的话,需要处理一些属性值
  13. if ($this->hasGroupStack()) {
  14. $this->mergeGroupAttributesIntoRoute($route);
  15. }
  16. // 添加路由的条件
  17. $this->addWhereClausesToRoute($route);
  18. return $route;
  19. }
  20. public function newRoute($methods, $uri, $action)
  21. {
  22. return (new Route($methods, $uri, $action))
  23. ->setRouter($this)
  24. ->setContainer($this->container);
  25. }

mergeGroupAttributesIntoRoute 方法

合并当前栈中的数据

  1. protected function mergeGroupAttributesIntoRoute($route)
  2. {
  3. $route->setAction($this->mergeWithLastGroup(
  4. $route->getAction(),
  5. $prependExistingPrefix = false
  6. ));
  7. }
  8. // RouteGroup 是处理属性合并类
  9. // namespace、prefix、where 这三个属性会叠加合并
  10. // domain 直接替换
  11. // 其余属性直接覆盖
  12. public function mergeWithLastGroup($new, $prependExistingPrefix = true)
  13. {
  14. return RouteGroup::merge($new, end($this->groupStack), $prependExistingPrefix);
  15. }

addWhereClausesToRoute 方法

处理路由条件

  1. protected function addWhereClausesToRoute($route)
  2. {
  3. $route->where(array_merge(
  4. $this->patterns, $route->getAction()['where'] ?? []
  5. ));
  6. return $route;
  7. }

路由寻址

添加 facade 和 别名配置

  1. // config/app
  2. 'aliases' => [
  3. ....
  4. 'Response' => Illuminate\Support\Facades\Response::class,
  5. ],
  6. // Illuminate\Foundation\Application::registerCoreContainerAliases
  7. [
  8. 'router' => [\Illuminate\Routing\Router::class, \Illuminate\Contracts\Routing\Registrar::class, \Illuminate\Contracts\Routing\BindingRegistrar::class],
  9. 'session' => [\Illuminate\Session\SessionManager::class],
  10. ]

Illuminate\Foundation\Http\Kernel 处理请求

laravel 利用管道模式实现了请求中间件功能,在这里暂时不考虑这个;

handle 方法

  1. public function handle($request)
  2. {
  3. try {
  4. $request->enableHttpMethodParameterOverride();
  5. $response = $this->sendRequestThroughRouter($request);
  6. } catch (Throwable $e) {
  7. $this->reportException($e);
  8. $response = $this->renderException($request, $e);
  9. }
  10. $this->app['events']->dispatch(
  11. new RequestHandled($request, $response)
  12. );
  13. return $response;
  14. }
  15. protected function sendRequestThroughRouter($request)
  16. {
  17. // 绑定实例
  18. $this->app->instance('request', $request);
  19. // 清除 facade
  20. Facade::clearResolvedInstance('request');
  21. // 引导程序启动
  22. $this->bootstrap();
  23. // 利用管道过滤并处理请求
  24. return (new Pipeline($this->app))
  25. ->send($request)
  26. ->through($this->app->shouldSkipMiddleware() ? [] : $this->middleware)
  27. ->then($this->dispatchToRouter());
  28. // 为了暂时不管管道逻辑,这里可以改为
  29. return $this->dispatchToRouter()($request);
  30. }

dispatchToRouter 方法

通过这个函数可以看出,最终触发路由寻址的是 Illuminate\Routing\Router:: dispatch 方法

  1. protected function dispatchToRouter()
  2. {
  3. return function ($request) {
  4. $this->app->instance('request', $request);
  5. return $this->router->dispatch($request);
  6. };
  7. }

Illuminate\Routing\Router 路由寻址部分

dispatch 方法

  1. public function dispatch(Request $request)
  2. {
  3. $this->currentRequest = $request;
  4. return $this->dispatchToRoute($request);
  5. }
  6. // 处理请求,并且返回 Response 影响
  7. public function dispatchToRoute(Request $request)
  8. {
  9. return $this->runRoute($request, $this->findRoute($request));
  10. }

findRoute 方法

  1. // routes 指的是 Illuminate\Routing\RouteCollection 类
  2. protected function findRoute($request)
  3. {
  4. $this->current = $route = $this->routes->match($request);
  5. $this->container->instance(Route::class, $route);
  6. return $route;
  7. }

runRoute 方法

在找到路由类之后,生成响应

Illuminate\Routing\RouteCollection 路由寻址部分

match 方法

  1. public function match(Request $request)
  2. {
  3. // 依据请求方法获取该类型下所有路由,ex:如果是 GET 请求,会返回所有的 GET 路由类,方便下边查询
  4. $routes = $this->get($request->getMethod());
  5. $route = $this->matchAgainstRoutes($routes, $request);
  6. return $this->handleMatchedRoute($request, $route);
  7. }
  8. // 匹配路由
  9. protected function matchAgainstRoutes(array $routes, $request, $includingMethod = true)
  10. {
  11. // 依据 isFallback 分成两类
  12. [$fallbacks, $routes] = collect($routes)->partition(function ($route) {
  13. return $route->isFallback;
  14. });
  15. // 把闭包类放在最后,
  16. return $routes->merge($fallbacks)->first(function (Route $route) use ($request, $includingMethod) {
  17. return $route->matches($request, $includingMethod);
  18. });
  19. }

handleMatchedRoute 方法

处理匹配到的路由

  1. protected function handleMatchedRoute(Request $request, $route)
  2. {
  3. if (! is_null($route)) {
  4. return $route->bind($request);
  5. }
  6. // 如果没有找到,找找其他方法下有没有对应的路由,ex:如果是 GET,但是没有找到路由,那么就在 POST、PUT 等方法中找找,看有没有
  7. $others = $this->checkForAlternateVerbs($request);
  8. if (count($others) > 0) {
  9. return $this->getRouteForMethods($request, $others);
  10. }
  11. throw new NotFoundHttpException;
  12. }

Illuminate\Routing\Route 路由寻址部分

matches 方法

  1. public function matches(Request $request, $includingMethod = true)
  2. {
  3. // 每一个遍历过的 Route 类都会转成 Symfony\Component\Routing\CompiledRoute 类,这个有点费解
  4. $this->compileRoute();
  5. foreach ($this->getValidators() as $validator) {
  6. if (! $includingMethod && $validator instanceof MethodValidator) {
  7. continue;
  8. }
  9. if (! $validator->matches($this, $request)) {
  10. return false;
  11. }
  12. }
  13. return true;
  14. }
  15. // 找到所有的验证器,验证规则
  16. public static function getValidators()
  17. {
  18. if (isset(static::$validators)) {
  19. return static::$validators;
  20. }
  21. // To match the route, we will use a chain of responsibility pattern with the
  22. // validator implementations. We will spin through each one making sure it
  23. // passes and then we will know if the route as a whole matches request.
  24. return static::$validators = [
  25. new UriValidator, new MethodValidator,
  26. new SchemeValidator, new HostValidator,
  27. ];
  28. }

bind 方法

主要处理路由的参数

  1. public function bind(Request $request)
  2. {
  3. $this->compileRoute();
  4. $this->parameters = (new RouteParameterBinder($this))
  5. ->parameters($request);
  6. $this->originalParameters = $this->parameters;
  7. return $this;
  8. }

run 方法

运行路由的 action,

  1. public function run()
  2. {
  3. $this->container = $this->container ?: new Container;
  4. try {
  5. if ($this->isControllerAction()) {
  6. return $this->runController();
  7. }
  8. return $this->runCallable();
  9. } catch (HttpResponseException $e) {
  10. return $e->getResponse();
  11. }
  12. }


评论 0

发表评论

Top