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。

     public function then(Closure $destination)
{
//array_reduce — 用回调函数迭代地将数组简化为单一的值
$pipeline = array_reduce(
//array_reverse将数组反向排序,$this->carry()返回一个闭包函数用来迭代
array_reverse($this->pipes), $this->carry(), $this->prepareDestination($destination)
); return $pipeline($this->passable);
} protected function carry()
{
return function ($stack, $pipe) {
return function ($passable) use ($stack, $pipe) {
if (is_callable($pipe)) {
// If the pipe is an instance of a Closure, we will just call it directly but
// otherwise we'll resolve the pipes out of the container and call it with
// the appropriate method and arguments, returning the results back out.
return $pipe($passable, $stack);
} elseif (! is_object($pipe)) {
//解析字符串
list($name, $parameters) = $this->parsePipeString($pipe); // If the pipe is a string we will parse the string and resolve the class out
// of the dependency injection container. We can then build a callable and
// execute the pipe function giving in the parameters that are required.
//从容器中取出相应的对象
$pipe = $this->getContainer()->make($name);
$parameters = array_merge([$passable, $stack], $parameters);
} else {
// If the pipe is already an object we'll just make a callable and pass it to
// the pipe as-is. There is no need to do any extra parsing and formatting
// since the object we're given was already a fully instantiated object.
$parameters = [$passable, $stack];
}
dd($parameters);
//判断中间件对象中是否存在handle方法,并将刚刚的$request对象与$stack闭包作为参数,传入中间件handle方法
return method_exists($pipe, $this->method)
? $pipe->{$this->method}(...$parameters)
: $pipe(...$parameters);
};
};
}

可以看到,管道类中的其他几个方法都是很简单的赋值方法。而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看一看

     protected function carry()
{
return function ($stack, $pipe) {
return function ($passable) use ($stack, $pipe) {
try {
//通过这个函数启动刚刚的管道闭包
$slice = parent::carry();
//$stack, $pipe依然是闭包结果与中间件数组中的值
$callable = $slice($stack, $pipe);
//$passable还是request对象
return $callable($passable);
} catch (Exception $e) {
return $this->handleException($passable, $e);
} catch (Throwable $e) {
return $this->handleException($passable, new FatalThrowableError($e));
}
};
};
}

当这个管道闭包执行完毕之后,中间件暂时就结束了。便开始执行之前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中了。

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

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

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

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

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

laravel5.5源码笔记(六、中间件)的更多相关文章

  1. Tomcat8源码笔记(六)连接器Connector分析

    根据 Tomcat8源码笔记(五)组件Container分析 前文分析,StandardService的初始化重心由 StandardEngine转移到了Connector的初始化,本篇记录下Conn ...

  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源码笔记(八、Eloquent ORM)

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

  6. laravel5.5源码笔记(四、路由)

    今天这篇博文来探索一下laravel的路由.在第一篇讲laravel入口文件的博文里,我们就提到过laravel的路由是在application对象的初始化阶段,通过provider来加载的.这个路由 ...

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

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

  8. laravel5.5源码笔记(二、服务提供者provider)

    laravel里所谓的provider服务提供者,其实是对某一类功能进行整合,与做一些使用前的初始化引导工作.laravel里的服务提供者也分为,系统核心服务提供者.与一般系统服务提供者.例如上一篇博 ...

  9. Tomcat8源码笔记(七)组件启动Server Service Engine Host启动

    一.Tomcat启动的入口 Tomcat初始化简单流程前面博客介绍了一遍,组件除了StandardHost都有博客,欢迎大家指文中错误.Tomcat启动类是Bootstrap,而启动容器启动入口位于 ...

随机推荐

  1. winform界面开发-HTML内容编辑控件

    参照及推荐博客:伍华聪 http://www.cnblogs.com/wuhuacong/archive/2009/07/07/1518346.html http://www.cnblogs.com/ ...

  2. Value与Sql Value

    在使用Value作为参数传递给SqlServer时 实际上传递的是SqlValue 为其赋值的一种方式,可以将datetime类型转换成string类型(yyyy-MM-dd HH:mm:ss)

  3. awk单行脚本快速参考

    AWK单行脚本快速参考 2008年4月28日编辑: Eric Pement eric [at] pement.org 版本 0.26翻译: 董一粟 yisudong [at] gmail.com 最新 ...

  4. MySQL5.7.20编译安装

    1:官网下载source code源码安装文件 https://cdn.mysql.com//Downloads/MySQL-5.7/mysql-boost-5.7.20.tar.gz 2:安装准备 ...

  5. Java学习---程序设计_面试题[2]

    百度2017春招笔试真题编程题集合之买帽子 // 2017-10-09 // 题目描述 // 度度熊想去商场买一顶帽子,商场里有N顶帽子,有些帽子的价格可能相同.度度熊想买一顶价格第三便宜的帽子,问第 ...

  6. 最优化作业 共轭梯度法 matlab代码

    syms f x1 x2 f=(1/2)*x1^2+x2^2; x=[2;1]; a=[1 0;0 2];% A g1=diff(f,x1); g2=diff(f,x2); g=[g1;g2];%导数 ...

  7. homebrew命令

    ruby -e "$(curl -fsSL https://raw.githubusercontent.com/Homebrew/install/master/install)"

  8. ZT 蓝牙的AVDTP协议笔记

    我的电子杂烩饭 http://blog.sina.com.cn/wuchuchu2012 [订阅][手机订阅] 首页 博文目录 图片 关于我 个人资料 Tifnan Qing 微博 加好友 发纸条 写 ...

  9. day13 多线程建立方法

    #01创建多线程    继承Thread类    覆盖run方法:run方法里面运行要执行的代码    创建对象    调用start方法,start方法会开启线程,然后调用run方法 获取线程名字: ...

  10. 利用cobbler无人值守批量安装centos

    准备: 至少两台机器,分别用作cobbler的服务端和安装测试端 准备一个iso的安装文件,最好是4G多的那个dvd包,以前用网易源上那个centos 6.4 x86_64 通过xen安装时就报错:N ...