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

laravel中的中间件作为一个请求与响应的过滤器,主要分为两个功能。

1、在请求到达控制器层之前进行拦截与过滤,只有通过验证的请求才能到达controller层

2、或者是在controller中运算完的数据或页面响应返回前进行过滤,通过验证的响应才能返回给客户端

中间件一般通过artisan命令创建

php artisan make:middleware 中间件名称

命令行创建的中间件会保存在/app/Http/Middleware文件夹里

这些中间件需要手动添加在/app/Http/Kernel.php文件的配置中,这个文件继承了kernel核心,在应用入口初始化kernel核心时,vendor\laravel\framework\src\Illuminate\Foundation\Http\Kernel.php这个kernel文件中的配置便覆盖了kernel核心中的相关配置,其中当然便包括了middleware中间件。我们可以在kernel类的构造方法第一行添加打印语句,分别打印$this->middleware,$this->middlewareGroups然后我们就会看见之前写在/app/Http/Kernel.php文件中的中间件了。

不过不止这里一处,在路由加载的时候,也会加载一次系统默认的web数组中的中间件,在返回响应对象的时候进行过滤,这个稍后再看。

接下来看一下中间件是在哪里开始过滤的,在第一篇关于入口文件的博文中,我们了解到laravel是从kernel核心中的handle方法开始的。

    public function handle($request)
    {
        try {
            //启用http方法覆盖参数
            $request->enableHttpMethodParameterOverride();
            //通过路由发送请求
            $response = $this->sendRequestThroughRouter($request);
        } catch (Exception $e) {
            $this->reportException($e);

            $response = $this->renderException($request, $e);
        } catch (Throwable $e) {
            $this->reportException($e = new FatalThrowableError($e));

            $response = $this->renderException($request, $e);
        }

        $this->app['events']->dispatch(
            new Events\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());
    }

 kernel核心的sendRequestThroughRouter方法中最后的return 语句,便开始使用中间件来对系统接到的请求开始进行过滤了。

上一篇博文中,我们提到了装饰模式与laravel在此基础上变种来的管道模式,便是在这里开始运用。

从Pipeline对象一路跳转,发现这个对象的基类为laravel\vendor\laravel\framework\src\Illuminate\Pipeline\Pipeline.php。管道对象的构造方法把laravel系统容器传入了其中。这里不再赘述管道模式的原理,直接来看关键方法then。

 1     public function then(Closure $destination)
 2     {
 3         //array_reduce — 用回调函数迭代地将数组简化为单一的值
 4         $pipeline = array_reduce(
 5             //array_reverse将数组反向排序,$this->carry()返回一个闭包函数用来迭代
 6             array_reverse($this->pipes), $this->carry(), $this->prepareDestination($destination)
 7         );
 8 
 9         return $pipeline($this->passable);
10     }
11 
12     protected function carry()
13     {
14         return function ($stack, $pipe) {
15             return function ($passable) use ($stack, $pipe) {
16                 if (is_callable($pipe)) {
17                     // If the pipe is an instance of a Closure, we will just call it directly but
18                     // otherwise we'll resolve the pipes out of the container and call it with
19                     // the appropriate method and arguments, returning the results back out.
20                     return $pipe($passable, $stack);
21                 } elseif (! is_object($pipe)) {
22                     //解析字符串
23                     list($name, $parameters) = $this->parsePipeString($pipe);
24 
25                     // If the pipe is a string we will parse the string and resolve the class out
26                     // of the dependency injection container. We can then build a callable and
27                     // execute the pipe function giving in the parameters that are required.
28                     //从容器中取出相应的对象
29                     $pipe = $this->getContainer()->make($name);
30                     $parameters = array_merge([$passable, $stack], $parameters);
31                 } else {
32                     // If the pipe is already an object we'll just make a callable and pass it to
33                     // the pipe as-is. There is no need to do any extra parsing and formatting
34                     // since the object we're given was already a fully instantiated object.
35                     $parameters = [$passable, $stack];
36                 }
37                 dd($parameters);
38                 //判断中间件对象中是否存在handle方法,并将刚刚的$request对象与$stack闭包作为参数,传入中间件handle方法
39                 return method_exists($pipe, $this->method)
40                                 ? $pipe->{$this->method}(...$parameters)
41                                 : $pipe(...$parameters);
42             };
43         };
44     }

可以看到,管道类中的其他几个方法都是很简单的赋值方法。而then方法中,写着的也是我们上一次理解管道模式中用到的array_reduce函数,看到这个就亲切多了。array_reverse($this->pipes)也就是将中间件数组逆向一下,因为管道模式的闭包运行时是像洋葱一样从外到内一层一层。而$this->prepareDestination($destination)这个方法则是之前then方法传入的,通过中间件后,需要执行的业务代码,也就是会开始调用控制器了。

关键代码在$this->carry()中这个方法只看最外层的话,就是返回了一个闭包函数,相当于直接把这个闭包写在array_reduce中。里面那个闭包函数则是我们比较熟悉的管道闭包写法了,use的两个参数分别代表本次闭包的执行结果与中间件数组中的一个参数。$passable变量则代表了$request对象。

这个闭包中,用了几个if判断,来确定$pipe的值类型,也就是我们的中间件数组的值中的类型,如果是闭包则直接运行,如果不是对象则从容器中加载出对象。如果剩下的都不是,则代表类型为对象,直接构建参数,开始调用。还记得我们的自定义中间件代码都要写在handle方法中吗,这里的$passable 和 $stack便是运行时传入handle的变量$request 与 Closure $next了。而这里的next方法又是什么呢?

让我们打开路由中的管道文件vendor\laravel\framework\src\Illuminate\Routing\Pipeline.php看一看

 1     protected function carry()
 2     {
 3         return function ($stack, $pipe) {
 4             return function ($passable) use ($stack, $pipe) {
 5                 try {
 6                     //通过这个函数启动刚刚的管道闭包
 7                     $slice = parent::carry();
 8                     //$stack, $pipe依然是闭包结果与中间件数组中的值
 9                     $callable = $slice($stack, $pipe);
10                     //$passable还是request对象
11                     return $callable($passable);
12                 } catch (Exception $e) {
13                     return $this->handleException($passable, $e);
14                 } catch (Throwable $e) {
15                     return $this->handleException($passable, new FatalThrowableError($e));
16                 }
17             };
18         };
19     }

当这个管道闭包执行完毕之后,中间件暂时就结束了。便开始执行之前kernel核心传入的$this->dispatchToRouter()方法了。

 现在来看一下路由通过中间件之后,是如何到达控制器的。dispatchToRouter方法经过一系列跳转之后,到达了Router类的dispatchToRoute方法。其中findRoute方法获取到了Routes对象集合(这个对象中包含了所有的路由对象,在之前介绍路由的章节时介绍过)并通过请求对象从中获取了当前路由(这个查找过程用到了Collection基础对象,由于不是本文重点,暂时当成黑盒方法来看吧)。

    public function dispatchToRoute(Request $request)
    {
        return $this->runRoute($request, $this->findRoute($request));
    }

    //vendor\laravel\framework\src\Illuminate\Routing\Router.php
    protected function runRoute(Request $request, Route $route)
    {
        //将返回路由的闭包函数设置在request对象中
        $request->setRouteResolver(function () use ($route) {
            return $route;
        });
        //绑定事件监听器
        $this->events->dispatch(new Events\RouteMatched($route, $request));

        return $this->prepareResponse($request,
            $this->runRouteWithinStack($route, $request)
        );
    }

    protected function runRouteWithinStack(Route $route, Request $request)
    {
        //是否禁用中间件
        $shouldSkipMiddleware = $this->container->bound('middleware.disable') &&
                                $this->container->make('middleware.disable') === true;
        //获取系统路由中间件,
        $middleware = $shouldSkipMiddleware ? [] : $this->gatherRouteMiddleware($route);
        //跟之前一样的中间件调用方式
        return (new Pipeline($this->container))
                        ->send($request)
                        ->through($middleware)
                        ->then(function ($request) use ($route) {
                            //将执行结果包装成response响应对象
                            return $this->prepareResponse(
                                //进入控制器
                                $request, $route->run()
                            );
                        });
    }

    //vendor\laravel\framework\src\Illuminate\Routing\Route.php    
    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();
        }
    }

开头我们提到了路由加载的时候还会进行一次中间件过滤,就是在这里了。在代码执行到runRoute方法的时候,会再次进行中间件过滤,这次的中间件是写在web数组中的系统中间件。并且这次通过中间件后,就会直接执行控制器,或是写在路由闭包中的代码了。返回的结果会被包装成响应对象依次返回,最终返回到浏览器。

控制器方法是由controllerDispatcher类来启动的这个类位于vendor\laravel\framework\src\Illuminate\Routing\ControllerDispatcher.php,它的dispatch方法接收了路由、控制器、方法三个参数。打印结果如下

这里的controller中会有一个默认的middleware数组,是因为中间件也可以在控制器中定义,不过我们一般直接定义在kernel中了。

 再来看一下这段代码具体做了什么

 1     public function dispatch(Route $route, $controller, $method)
 2     {
 3         //解析依赖
 4         $parameters = $this->resolveClassMethodDependencies(
 5             //获取路由中写定的参数
 6             $route->parametersWithoutNulls(), $controller, $method
 7         );
 8         //判断是否有callAction方法。其实是在controller的父类中执行了call_user_func_array
 9         if (method_exists($controller, 'callAction')) {
10             return $controller->callAction($method, $parameters);
11         }
12         //若没有则当成普通类进行调用返回
13         return $controller->{$method}(...array_values($parameters));
14     }

哦,差点忘了控制器的实例化是在Route类的runController方法中执行了getController

 1     public function getController()
 2     {
 3         if (! $this->controller) {
 4             //从路由中获取控制器字符串
 5             $class = $this->parseControllerCallback()[0];
 6             //通过字符串分割make控制器实例
 7             $this->controller = $this->container->make(ltrim($class, '\\'));
 8         }
 9 
10         return $this->controller;
11     }
12 
13     protected function parseControllerCallback()
14     {
15         //laravel封装的字符串操作类,route的action中记录了我们写在route文件中的控制器相关字符串
16         return Str::parseCallback($this->action['uses']);
17     }

那么,到这里我们又梳理了一次从中间件到控制器的流程。中间件这一章差不多就是这些内容了。

 

posted @ 2019-02-26 16:53  我也很惆怅  阅读(783)  评论(0编辑  收藏  举报