澳门新浦京手机版Laravel分析 – 核心篇

昨天按照手册教程,动手写一个Auth扩展,按照包独立性的原则,我不希望将Auth::extend()这种方法写在
start.php
中,毫无疑问,我选择了在服务提供器register()方法中注册扩展驱动。然而,事与愿违……

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

文字总是没有代码简明。这里只是将主要的类和流程进行记录,类似于目录或者地图的功能,不至于面对一堆源码的时候摸不到头绪。具体的实现细节还是要看代码。

澳门新浦京手机版 1

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

核心概念

1、 Container
做两件事情。

  • 绑定 bind()
    将具体实现转为闭包,然后和接口对应起来,放在数组bindings中保存。
  • 解析 make()
    查询bindings,将接口的对应实现类找到,通过反射,将类实例化返回。通过ReflectionClass类实现。

2、Application
继承自Container,主要提供下面3个方法。

  • bootstrap() 启动
    依次调用7个Bootstrapper进行启动。
  • register() 注册ServiceProvider
    调用ServiceProvider的register()方法。
  • 澳门新浦京手机版 ,boot()
    将App的状态设为已经启动,调用ServiceProvider的boot()方法。

3、ServiceProvider
所有的功能都是以服务形式进行提供,例如:认证、权限、缓存、路由等。需要提供register()方法向Application注册,可提供boot()方法在Application启动后调用。

4、Contract
一堆接口,当你不知道一个类的主要作用的时候,可以看看其实现的接口。

5、Facade
给类起一个别名,方便调用。(ps:纯属增加复杂度,我更喜欢逻辑上的简单,不喜欢这种少打几个字母的方便)

6、 Bootstrapper
启动器,App也就是框架启动的过程。

发现问题

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

一次请求的过程

// 初始化App,并启动
$app = new Application();  

// 处理请求的核心类,关联路由
$kernel = new Kernel($app,$router);  

// 将$_GET,$_POST等封装到$request里
$request = Request::capture();  

// 根据路由找到对应函数处理请求,生成相应
$response = $kernel->handle($request);  

$response->send();  //  发送响应
$kernel->terminate();  // 善后

当我在 LoauthServiceProvider 中这样写的时候:

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

Application的启动流程

首先,App初始化

  • registerBaseBindings()
  • registerBaseServiceProviders()
  • registerCoreContainerAliases()

App启动,依次调用以下bootstrapper的bootstrap()方法,等到BootProviders启动后,依次再调用bootstrapper的boot()方法。

  • DetectEnvironment 检测环境
  • LoadConfiguration 加载设置
  • ConfigureLogging 设置日志
  • HandleExceptions 捕获异常
  • RegisterFacades 注册Facade
  • RegisterProviders 注册ServiceProvider
  • BootProviders 启动ServiceProvider

下面重点分析后三个Bootstrapper

RegisterFacades()
config/app.php中的别名aliases进行注册。
通过spl_autoload_register注册自动加载器,class_alias方法注册别名。
通过注册别名,可以直接使用例如Route::get()Cache::get()之类的方法,类加载器会自动找到对应的类的方法。

RegisterProviders()
config/app.php中的providers进行注册。ProviderRepository会读取配置文件中的ServiceProvider,并会编译一份做为缓存,然后向App注册。

BootProviders()
调用各provider的boot()方法。

public function register()
{
    //
    Auth::extend('loauth',function($app){});
}

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

如何处理请求

路由是由RouteServiceProvider进行提供,在App启动的过程中会调用Http/routes.php文件,将路由放入RouteCollection中。当有一个请求到来的时候Kernel会匹配到对应的路由,然后调用相应的Controller或者函数进行处理。Controller会渲染模板或者其他的HTTP响应。

具体的框架核心部分也就这么多了。剩下的就是每个服务组件的实现细节问题了。

报错

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

Call to undefined method IlluminateSupportFacadesAuth::extend()

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

寻找原因

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

当时就纳闷了,找原因,怀疑是Auth没注册?检查发现注册了,因为在路由中可以使用;php
artisan clear-compiled
没用;百思不得其解,甚至怀疑是我不小心修改了核心类,还重新下载了一次laravel包,问题依旧。

//IlluminateFoundationSupportProvidersRouteServiceProvider.phppublic 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']);        }    }    

折腾了一晚上,最终我把目光锁定在 AuthServiceProvider 的 $defer 属性。

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

根据手册以及注释,我们得知 $defer
属性是用来延迟加载该服务提供器,说直白点就是延迟执行 register()
方法,只需要配合provides()方法即可实现。举个例子:

    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]);    }

//IlluminateRoutingRouteRegistrar.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;        }    }
public function provides()
{
    return array('auth');
}

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

这个是 AuthServiceProvider
里的方法,当框架初始化的时候,会依次加载服务提供器,如果发现这个服务提供器protected
$defer=true 那么就会调用它的 provides()
方法,其返回的数组包含需要延迟加载的服务名称,这样当我们在路由、控制器或者其他地方调用
Auth::METHOD() 的时候,才会去调用提供器的 register() 方法。

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

确定症结

澳门新浦京手机版 2

那么问题来了,既然是被动延迟加载,也就是说当我调用Auth类方法时应该会自动实例化Auth类啊,为什么我在LoauthServiceProvider中调用的时候却提示方法不存在,但是在路由中却可以呢。

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

我猜测是因为优先级的问题,可能在框架注册
LoauthServiceProvider::register() 的时候,Auth
还没有标记为延迟加载,这就造成了一个先后问题,任何即时加载的服务提供器都无法在register方法中调用延迟加载的服务。

澳门新浦京手机版 3

经过研究,顺利在核心代码中找到证据
IlluminateFoundationProviderRepository

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

public function load(Application $app, array $providers)
{
    //...省略
    // We will go ahead and register all of the eagerly loaded providers with the
    // application so their services can be registered with the application as
    // a provided service. Then we will set the deferred service list on it.
    foreach ($manifest['eager'] as $provider)
    {
        $app->register($this->createProvider($app, $provider));
    }
    //延迟加载标记在即时加载服务之后
    $app->setDeferredServices($manifest['deferred']);
}
 1 public static $verbs = ['GET', 'HEAD', 'POST', 'PUT', 'PATCH', 'DELETE', 'OPTIONS']; 2  3     public function any($uri, $action = null) 4     { 5         return $this->addRoute(self::$verbs, $uri, $action); 6     } 7  8     public function get($uri, $action = null) 9     {10         return $this->addRoute(['GET', 'HEAD'], $uri, $action);11     }12 13     public function post($uri, $action = null)14     {15         return $this->addRoute('POST', $uri, $action);16     }17 18     public function put($uri, $action = null)19     {20         return $this->addRoute('PUT', $uri, $action);21     }22 23     public function patch($uri, $action = null)24     {25         return $this->addRoute('PATCH', $uri, $action);26     }27 28     public function delete($uri, $action = null)29     {30         return $this->addRoute('DELETE', $uri, $action);31     }32 33     public function options($uri, $action = null)34     {35         return $this->addRoute('OPTIONS', $uri, $action);36     }37 38     public function any($uri, $action = null)39     {40         return $this->addRoute(self::$verbs, $uri, $action);41     }

解决之道

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

虽然发现了问题所在,但并不代表问题就解决了,修改核心代码不是个明智的选择,所以只能在我们自己的包里想办法咯,一个解决方案如下:

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

public function register()
{
    //
    $authProvider = new IlluminateAuthAuthServiceProvider($this->app);
    $authProvider->register();
    Auth::extend('loauth',function($app){});
}

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

既然auth还未注册,那么我们手动调用它的register方法帮它注册。

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

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

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

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

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

 1 protected function convertToControllerAction($action) 2     { 3         if (is_string($action)) { 4             $action = ['uses' => $action]; 5         } 6  7         // Here we'll merge any group "uses" statement if necessary so that the action 8         // has the proper clause for this property. Then we can simply set the name 9         // of the controller on the action and return the action array for usage.10         //路由组栈不为空的话,还记得之前路由服务boot的时候调用的group方法吗?11         if (! empty($this->groupStack)) {12             //更新传入控制器的命名空间13             $action['uses'] = $this->prependGroupNamespace($action['uses']);14         }15 16         // Here we will set this controller name on the action array just so we always17         // have a copy of it for reference if we need it. This can be used while we18         // search for a controller name or do some other type of fetch operation.19         $action['controller'] = $action['uses'];20 21         return $action;22     }23 24     protected function prependGroupNamespace($class)25     {26         $group = end($this->groupStack);27                 //返回带有命名空间的控制器全称28         return isset($group['namespace']) && strpos($class, '\') !== 029                 ? $group['namespace'].'\'.$class : $class;30     }

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

 1 protected function newRoute($methods, $uri, $action) 2     { 3         return (new Route($methods, $uri, $action)) 4                     ->setRouter($this) 5                     ->setContainer($this->container); 6     } 7      8  9     //laravelframeworksrcIlluminateRoutingRoute.php10     public function __construct($methods, $uri, $action)11     {12         $this->uri = $uri;13         $this->methods = (array) $methods;14         //将路由操作解析成数组15         $this->action = $this->parseAction($action);16 17         if (in_array('GET', $this->methods) && ! in_array('HEAD', $this->methods)) {18             $this->methods[] = 'HEAD';19         }20 21         if (isset($this->action['prefix'])) {22             $this->prefix($this->action['prefix']);23         }24     }25 26     protected function parseAction($action)27     {28         return RouteAction::parse($this->uri, $action);29     }30 31     //vendorlaravelframeworksrcIlluminateRoutingRouteAction.php32     public static function parse($uri, $action)33     {34         // If no action is passed in right away, we assume the user will make use of35         // fluent routing. In that case, we set a default closure, to be executed36         // if the user never explicitly sets an action to handle the given uri.37         //如果为空操作将会返回一个报错信息的闭包38         if (is_null($action)) {39             return static::missingAction($uri);40         }41 42         // If the action is already a Closure instance, we will just set that instance43         // as the "uses" property, because there is nothing else we need to do when44         // it is available. Otherwise we will need to find it in the action list.45     //在这里已经成为闭包的action 会直接返回数组46 47         if (is_callable($action)) {48             return ['uses' => $action];49         }50 51         // If no "uses" property has been set, we will dig through the array to find a52         // Closure instance within this list. We will set the first Closure we come53         // across into the "uses" property that will get fired off by this route.54         elseif (! isset($action['uses'])) {55             $action['uses'] = static::findCallable($action);56         }57         58         if (is_string($action['uses']) && ! Str::contains($action['uses'], '@')) {59             $action['uses'] = static::makeInvokable($action['uses']);60         }61 62         return $action;63     }

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

 1 //laravelframeworksrcIlluminateRoutingRouteCollection.php 2     public function add(Route $route) 3     { 4         //将路由添加到集合 5         $this->addToCollections($route); 6         //将路由添加到一个多维数组中方便作为字典来查找 7         $this->addLookups($route); 8  9         return $route;10     }11 12     protected function addToCollections($route)13     {14         //获取为路由定义的域,若有domain属性则返回http或https中的一个?这里没看懂,不过最终还是返回了uri15         $domainAndUri = $route->getDomain().$route->uri();16         //获取到了路由内的get等方法,遍历添加到routes中17         foreach ($route->methods() as $method) {18             $this->routes[$method][$domainAndUri] = $route;19         }20 21         $this->allRoutes[$method.$domainAndUri] = $route;22     }23 24     protected function addLookups($route)25     {26         // If the route has a name, we will add it to the name look-up table so that we27         // will quickly be able to find any route associate with a name and not have28         // to iterate through every route every time we need to perform a look-up.29         //获取action30         $action = $route->getAction();31 32         //若有as关键字则添加相应的数组属性方便作为字典来查询33         if (isset($action['as'])) {34             $this->nameList[$action['as']] = $route;35         }36 37         // When the route is routing to a controller we will also store the action that38         // is used by the route. This will let us reverse route to controllers while39         // processing a request and easily generate URLs to the given controllers.40         if (isset($action['controller'])) {41             $this->addToActionList($action, $route);42         }43     }44 45     protected function addToActionList($action, $route)46     {47         //再次通过controller作为标示存储route路由48         $this->actionList[trim($action['controller'], '\')] = $route;49     }

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

发表评论

电子邮件地址不会被公开。 必填项已用*标注