今天这篇博文来探索一下laravel的路由。在第一篇讲laravel入口文件的博文里,我们就提到过laravel的路由是在application对象的初始化阶段,通过provider来加载的。这个路由服务提供者注册于vendor\laravel\framework\src\Illuminate\Foundation\Application.php的registerBaseServiceProviders方法

protected function registerBaseServiceProviders()
{
$this->register(new EventServiceProvider($this)); $this->register(new LogServiceProvider($this)); $this->register(new RoutingServiceProvider($this));
}

可以看到这个方法对路由provider进行了注册,我们最开始的博文也提到过,这个register方法实际上是运行了provider内部的register方法,现在来看一下这个provider都提供了些什么vendor\laravel\framework\src\Illuminate\Routing\RoutingServiceProvider.php

    public function register()
{
$this->registerRouter(); $this->registerUrlGenerator(); $this->registerRedirector(); $this->registerPsrRequest(); $this->registerPsrResponse(); $this->registerResponseFactory(); $this->registerControllerDispatcher();
}

这个服务提供者类中将许多对象都添加到了laravel的容器中,其中最重要的就是第一个注册的Router类了。Router中包含了我们写在路由文件中的get、post等各种方法,我们在路由文件中所使用的Route::any()方法也是一个门面类,它所代理的对象便是Router。

看过了路由的初始化,再来看一下我们在路由文件中所书写的路由是在什么时候加载到系统中的。在config/app.php文件中的privders数组中有一个名为RouteServiceProvider的服务提供者会跟随laravel系统在加载配置的时候一起加载。这个文件位于\app\Providers\RouteServiceProvider.php刚刚的Routing对路由服务进行了注册,这里的RouteServiceProvider就要通过刚刚加载的系统类来加载我们写在routes路由文件夹中的路由了。

至于这个provider是何时开始启动的,还记得我们第一篇博客中介绍的Illuminate\Foundation\Bootstrap\BootProviders这个provider吗?这个provider在注册时便会将已经注册过的provider,通过application中的boot方法,转发到它们自身的boot方法来启动了。

而RouteServiceProvider这个类的boot方法通过它父类boot方法绕了一圈后又运行了自己的mapWebRoutes方法。

//Illuminate\Foundation\Support\Providers\RouteServiceProvider.php

public function boot()
{
//设置路由中控制器的命名空间
$this->setRootControllerNamespace();
//若路由已有缓存则加载缓存
if ($this->app->routesAreCached()) {
$this->loadCachedRoutes();
} else {
//这个方法启动了子类中的map方法来加载路由
$this->loadRoutes(); $this->app->booted(function () {
$this->app['router']->getRoutes()->refreshNameLookups();
$this->app['router']->getRoutes()->refreshActionLookups();
});
}
} protected function loadRoutes()
{
if (method_exists($this, 'map')) {
//这里又把视线拉回了子类,执行了子类中的map方法
$this->app->call([$this, 'map']);
}
}

这里这个mapWebRoutes方法有点绕,它先是通过门面类将Route变成了Router对象,接着又调用了Router中不存在的方法middleware,通过php的魔术方法__call将执行对象变成了RouteRegistrar对象(\Illuminate\Routing\RouteRegistrar.php)在第三句调用group方法时,又将路由文件的地址传入了Router方法的group方法中。

    protected function mapWebRoutes()
{
//这里的route门面指向依旧是router,middleware方法通过__call重载将对象指向了RouteRegistrar对象
Route::middleware('web')
//RouteRegistrar对象也加载了命名空间
->namespace($this->namespace)
//这里RouteRegistrar对象中的group方法又将对象方法指向了Router中的group方法
->group(base_path('routes/web.php'));
}
//Router类

    public function __call($method, $parameters)
{
if (static::hasMacro($method)) {
return $this->macroCall($method, $parameters);
}
//在这里通过重载实例化对象
if ($method == 'middleware') {
return (new RouteRegistrar($this))->attribute($method, is_array($parameters[0]) ? $parameters[0] : $parameters);
} return (new RouteRegistrar($this))->attribute($method, $parameters[0]);
}
//\Illuminate\Routing\RouteRegistrar.php
public function group($callback)
{
$this->router->group($this->attributes, $callback);
}
//Router类
public function group(array $attributes, $routes)
{
//更新路由栈这个数组
$this->updateGroupStack($attributes); // Once we have updated the group stack, we'll load the provided routes and
// merge in the group's attributes when the routes are created. After we
// have created the routes, we will pop the attributes off the stack.
$this->loadRoutes($routes);
//出栈
array_pop($this->groupStack);
} protected function loadRoutes($routes)
{
//这里判断闭包是因为laravel的路由文件中也允许我们使用group对路由进行分组
if ($routes instanceof Closure) {
$routes($this);
} else {
$router = $this;
//传入的$routes是一个文件路径,在这里将其引入执行,在这里就开始一条一条的导入路由了
require $routes;
}
}

绕了这么一大圈终于把写在routes文件夹中的路由文件加载进laravel系统了。接下来的操作就比较简单了。

先来看一下我的路由文件中写了些什么。

路由文件中只写了两个路由,在Route加载后通过dd(app()->router);打印出来看一下吧。

刚刚我们看见了路由中的get、post、put等数组,那么现在来看一下它们是怎么被添加到路由数组中的

 public static $verbs = ['GET', 'HEAD', 'POST', 'PUT', 'PATCH', 'DELETE', 'OPTIONS'];

     public function any($uri, $action = null)
{
return $this->addRoute(self::$verbs, $uri, $action);
} public function get($uri, $action = null)
{
return $this->addRoute(['GET', 'HEAD'], $uri, $action);
} public function post($uri, $action = null)
{
return $this->addRoute('POST', $uri, $action);
} public function put($uri, $action = null)
{
return $this->addRoute('PUT', $uri, $action);
} public function patch($uri, $action = null)
{
return $this->addRoute('PATCH', $uri, $action);
} public function delete($uri, $action = null)
{
return $this->addRoute('DELETE', $uri, $action);
} public function options($uri, $action = null)
{
return $this->addRoute('OPTIONS', $uri, $action);
} public function any($uri, $action = null)
{
return $this->addRoute(self::$verbs, $uri, $action);
}

通过上面的代码,我们可以发现,包括any在内的各种方式添加的路由都是通过addroute这个方法来添加的,将写在路由文件中的uri和控制器或闭包传入其中。

这里的路由添加过程中对路由进行了多次包装,这么多次调用所做的事情简单来说就两点。

1、将路由添加至route对象。

2、在路由集合中建立路由字典数组,用于后续步骤快速查找路由

这里详细分解一下路由创建的过程

一、这里我们先把流程从addRoute走到createRoute这个方法中来,首先判断了路由是否为控制器,方法很简单就不贴出来了。在15行更新控制器命名空间时,跳到下面的代码块。

 protected function addRoute($methods, $uri, $action)
{
//这里的routes是在构造方法中添加的对象RouteCollection,methods是刚刚传入的get等传输方式
return $this->routes->add($this->createRoute($methods, $uri, $action));
} protected function createRoute($methods, $uri, $action)
{
// If the route is routing to a controller we will parse the route action into
// an acceptable array format before registering it and creating this route
// instance itself. We need to build the Closure that will call this out.
//判断传入的action是否为控制器而不是闭包函数
if ($this->actionReferencesController($action)) {
//更新控制器命名空间
$action = $this->convertToControllerAction($action);
}
//这里的prefix方法用于获取在group处定义的路由前缀,newRoute方法再次将路由包装
$route = $this->newRoute(
$methods, $this->prefix($uri), $action
); // If we have groups that need to be merged, we will merge them now after this
// route has already been created and is ready to go. After we're done with
// the merge we will be ready to return the route back out to the caller.
if ($this->hasGroupStack()) {
//将本次加载的路由组合并至对象属性
$this->mergeGroupAttributesIntoRoute($route);
} //为路由参数添加where限制
$this->addWhereClausesToRoute($route); return $route;
}

二、这里是更新控制器命名空间的部分,这里跑完后再次回到上面那个代码块的19行,这里获取了路由前缀,方法很简单就不贴出来了。然后将get等方法数组,路由前缀与action操作作为参数生成一个路由对象,再跳到下一个代码块。

 protected function convertToControllerAction($action)
{
if (is_string($action)) {
$action = ['uses' => $action];
} // Here we'll merge any group "uses" statement if necessary so that the action
// has the proper clause for this property. Then we can simply set the name
// of the controller on the action and return the action array for usage.
//路由组栈不为空的话,还记得之前路由服务boot的时候调用的group方法吗?
if (! empty($this->groupStack)) {
//更新传入控制器的命名空间
$action['uses'] = $this->prependGroupNamespace($action['uses']);
} // Here we will set this controller name on the action array just so we always
// have a copy of it for reference if we need it. This can be used while we
// search for a controller name or do some other type of fetch operation.
$action['controller'] = $action['uses']; return $action;
} protected function prependGroupNamespace($class)
{
$group = end($this->groupStack);
//返回带有命名空间的控制器全称
return isset($group['namespace']) && strpos($class, '\\') !== 0
? $group['namespace'].'\\'.$class : $class;
}

三、这个代码块new出了route,后续的setrouter与setContainer分别为该对象传入了router与容器对象。route对象在构造方法中进行简单赋值后,通过routerAction对象的parse方法将路由再次进行包装,并设置了路由的前缀,这个方法比较简单就不贴代码了。这个时候再次调回步骤一的第25行,判断路由分组的栈是否为空,将刚刚添加的路由与原路由组合并(路由组将web.php文件看做一个路由分组,我们自己写在路由文件中的group被看做是这个分组中的子分组)。这里合并分组的代码也比较简单,记住各个属性的作用很容易看懂,就不贴出来了。包括再后面的添加where部分也是,值得一提的是route对象中有一个getAction方法,其中调用到了Arr底层对象。这个对象目前对我们来说过于底层了,追踪到这里就好,不需要再往下追溯下去了。这个时候,返回的route变量就作为步骤一第4行的add方法的参数了。见下方代码块。

 protected function newRoute($methods, $uri, $action)
{
return (new Route($methods, $uri, $action))
->setRouter($this)
->setContainer($this->container);
} //laravel\framework\src\Illuminate\Routing\Route.php
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';
} if (isset($this->action['prefix'])) {
$this->prefix($this->action['prefix']);
}
} protected function parseAction($action)
{
return RouteAction::parse($this->uri, $action);
} //\vendor\laravel\framework\src\Illuminate\Routing\RouteAction.php
public static function parse($uri, $action)
{
// If no action is passed in right away, we assume the user will make use of
// fluent routing. In that case, we set a default closure, to be executed
// if the user never explicitly sets an action to handle the given uri.
//如果为空操作将会返回一个报错信息的闭包
if (is_null($action)) {
return static::missingAction($uri);
} // If the action is already a Closure instance, we will just set that instance
// as the "uses" property, because there is nothing else we need to do when
// it is available. Otherwise we will need to find it in the action list.
    //在这里已经成为闭包的action 会直接返回数组 if (is_callable($action)) {
return ['uses' => $action];
} // If no "uses" property has been set, we will dig through the array to find a
// Closure instance within this list. We will set the first Closure we come
// across into the "uses" property that will get fired off by this route.
elseif (! isset($action['uses'])) {
$action['uses'] = static::findCallable($action);
} if (is_string($action['uses']) && ! Str::contains($action['uses'], '@')) {
$action['uses'] = static::makeInvokable($action['uses']);
} return $action;
}

四、这一部分就是之前说的创建路由查找字典的部分了。代码比较简单。

 //\laravel\framework\src\Illuminate\Routing\RouteCollection.php
public function add(Route $route)
{
//将路由添加到集合
$this->addToCollections($route);
//将路由添加到一个多维数组中方便作为字典来查找
$this->addLookups($route); return $route;
} protected function addToCollections($route)
{
//获取为路由定义的域,若有domain属性则返回http或https中的一个?这里没看懂,不过最终还是返回了uri
$domainAndUri = $route->getDomain().$route->uri();
//获取到了路由内的get等方法,遍历添加到routes中
foreach ($route->methods() as $method) {
$this->routes[$method][$domainAndUri] = $route;
} $this->allRoutes[$method.$domainAndUri] = $route;
} protected function addLookups($route)
{
// If the route has a name, we will add it to the name look-up table so that we
// will quickly be able to find any route associate with a name and not have
// to iterate through every route every time we need to perform a look-up.
//获取action
$action = $route->getAction(); //若有as关键字则添加相应的数组属性方便作为字典来查询
if (isset($action['as'])) {
$this->nameList[$action['as']] = $route;
} // When the route is routing to a controller we will also store the action that
// is used by the route. This will let us reverse route to controllers while
// processing a request and easily generate URLs to the given controllers.
if (isset($action['controller'])) {
$this->addToActionList($action, $route);
}
} protected function addToActionList($action, $route)
{
//再次通过controller作为标示存储route路由
$this->actionList[trim($action['controller'], '\\')] = $route;
}

走完这个留流程,路由就被加载完成了。程序的流程就回到了boot部分的group方法了。

laravel5.5源码笔记(四、路由)的更多相关文章

  1. Tomcat8源码笔记(四)Server和Service初始化

    上一章 简单说明下Tomcat各个组件: Server:服务器,Tomcat服务器,一个Tomcat只有一个Server组件; Service:业务层,是Server下最大的子容器,一个Server可 ...

  2. laravel5.5源码笔记(五、Pipeline管道模式)

    Pipeline管道模式,也有人叫它装饰模式.应该说管道是装饰模式的一个变种,虽然思想都是一样的,但这个是闭包的版本,实现方式与传统装饰模式也不太一样.在laravel的源码中算是一个比较核心的设计模 ...

  3. laravel5.5源码笔记(七、数据库初始化)

    laravel中的数据库也是以服务提供者进行初始化的名为DatabaseServiceProvider,在config文件的providers数组中有写.路径为vendor\laravel\frame ...

  4. laravel5.5源码笔记(一、入口应用的初始化)

    laravel的项目入口文件index.php如下 define('LARAVEL_START', microtime(true)); require __DIR__.'/../vendor/auto ...

  5. laravel5.5源码笔记(六、中间件)

    laravel中的中间件作为一个请求与响应的过滤器,主要分为两个功能. 1.在请求到达控制器层之前进行拦截与过滤,只有通过验证的请求才能到达controller层 2.或者是在controller中运 ...

  6. laravel5.5源码笔记(三、门面类facade)

    上次说了provider,那么这次来说说facade 首先是启动的源头,从laravel的kernel类中的$bootstrappers 数组,我们可以看到它的一些系统引导方法,其中的Register ...

  7. laravel5.5源码阅读草稿——路由

    laravel 里的路由是由RouteServiceProvider提供的,其中的boot方法为启动项,调用了父类的boot方法. RouteServiceProvider中的boot方法设置了自己与 ...

  8. jQuery源码笔记——四

    each()实现 var jQuery = function( selector, context ) { return new jQuery.fn.init( selector, context ) ...

  9. laravel5.5源码笔记(八、Eloquent ORM)

    上一篇写到Eloquent ORM的基类Builder类,这次就来看一下这些方便的ORM方法是如何转换成sql语句运行的. 首先还是进入\vendor\laravel\framework\src\Il ...

随机推荐

  1. bootstrap-table 分页增删改查之一(分页)

    记录一下 bootstrap-table插件的使用 先看下效果图 首先是导入js <!--js jquery --> <script type="text/javascri ...

  2. MySQL 8.0复制性能的提升(翻译)

    What’s New With MySQL Replication in MySQL 8.0 MySQL复制从问世到现在已经经历了多个年头,它的稳定性和可靠性也在稳步的提高.这是一个不停进化的过程,由 ...

  3. LINQ入门与标准查询运算符

    LINQ的体系结构 查询表达式的完整语法 一.查询表达式必须以from子句开头,以select 或group子句结束.中间可以使用where,orderby ,join,let和其他子句.具有“延迟计 ...

  4. 初进MFC的世界,太奇妙。第六次作业----未完待续

    又一次的迁徙.我希望能够早些抵达. 第六次作业-未完待续!图形界面,计算器文件 本次的作业是给自己的计算器加一个可视化的窗口,我开始也是很激动和憧憬的,看了很多的界面库,发现并没有想象的那么简单,因为 ...

  5. pip install lxml mysql-python error

    问题0: 在安装 mysql-python时,会出现: sh: mysql_config: not found Traceback (most recent call last): File &quo ...

  6. IOS 对JSON解析的要求

    JOSN格式的原始字符串中, 键名必须为 引号 “” 包含的字符串,值必须是数组("[]" 用中括号包起来的部分),字典("{}" 用中括号包起来的部分),数字 ...

  7. css3动画相关笔记

    1.$(".aa").delay(2500).animate({width:0}); // 延迟 2.setTimeout(function(){ --> css3 anim ...

  8. jQuery UI 实例 – 切换(Toggle)

    toggle()函数用于为每个匹配元素的click事件绑定轮流的处理函数. toggle()是一个特殊的事件函数,用于为匹配元素的click事件绑定多个事件处理函数.每次触发click事件时,togg ...

  9. PHP面试系列 之Linux(三)---- Vi/Vim编辑器

    vi 是 unix 家族下最功能强大的文字编辑器,而 vim 則是 vi 的加强版, 编辑模式   指令 說明 * i 在游標位置進入編輯模式   I 在游標行的第一個非空白字元進入編輯模式 * a ...

  10. 分享一个ASP.NET的弹出层,比较好用!

    网上的一些弹出层的控件多了去了,我很久之前用了一个,效果还不错,但如果应用到ASP.NET的话,会出现“弹出层内的控件runat='server'失效”的情况,具体情况我也不太会描述,但就是那些onc ...