Pipeline

Laravel 的中间件是通过管道类来实现的。

通过内核处理请求的过程中管道的作用来解析管道类!

protected function sendRequestThroughRouter($request){$this->app->instance('request', $request);Facade::clearResolvedInstance('request');$this->bootstrap();return (new Pipeline($this->app)) //这是个 Illuminate\Routing\Pipeline 对象,继承了 Illuminate\Pipeline\Pipeline 对象。->send($request) // 调用 Illuminate\Pipeline\Pipeline 的 send() 方法传入 $request 对象->through($this->app->shouldSkipMiddleware() ? [] : $this->middleware) // 传入需要经过的中间件数组->then($this->dispatchToRouter());// 传入最后执行的闭包并且运行管道}
复制代码

接下来我们看看这段代码是如何让请求通过所有的中间件之后返回的。

代码调用追踪

  1. 约定 (new Pipeline($this->app)) 下面统称 $pipe

  2. $pipe->send($request) // 将 $request 对象赋值给 $pipe->passable

  3. $pipe->pipes 的赋值

   array:5 [▼0 => "App\Http\Middleware\CheckForMaintenanceMode"1 => "Illuminate\Foundation\Http\Middleware\ValidatePostSize"2 => "App\Http\Middleware\TrimStrings"3 => "Illuminate\Foundation\Http\Middleware\ConvertEmptyStringsToNull"4 => "App\Http\Middleware\TrustProxies"]$pipe->through($this->app->shouldSkipMiddleware() ? [] : $this->middleware)
复制代码

4.$pipe->then($this->dispatchToRouter()); 这里是执行父类的 then() 方法

public function then(Closure $destination){$pipeline = array_reduce(array_reverse($this->pipes), $this->carry(), $this->prepareDestination($destination));return $pipeline($this->passable);}
复制代码

array_reverse($this->pipes),就是将刚才存入的中间件顺序反转。

$this->carry() 这里的 $this 指向的对象是 Illuminate\Routing\Pipeline 对象因此调用 carry() 方法是自身的。

$this->prepareDestination($destination) 返回一个闭包

 return function ($passable) use ($destination) {return $destination($passable);};
复制代码

接着开始看

 $pipeline = array_reduce(array_reverse($this->pipes), $this->carry(), $this->prepareDestination($destination));
复制代码

这段代码可以改造成容易读的方式

 $cb    = $this->carry();$stack = $this->prepareDestination($destination);foreach (array_reverse($this->pipes) as $pipe) {$stack = $cb($stack,$pipe);}$pipeline = $stack;
复制代码

先获取一个闭包,然后获取第一个闭包参数 $stack ,之后遍历 pipes 数组来进行迭代,每次迭代会更新下次迭代的 $stack 变量,等迭代完成之后将 $stack 赋值给 $pipeline.

所以我们只要关心最后 $pipeline 拿到的是一个什么东西 那么这里就要解析 $this->carry() 每次执行之后返回的是什么,下面是执行调用的方法。

 protected function carry(){return function ($stack, $pipe) {return function ($passable) use ($stack, $pipe) {try {$slice = parent::carry();$callable = $slice($stack, $pipe);return $callable($passable);} catch (Exception $e) {return $this->handleException($passable, $e);} catch (Throwable $e) {return $this->handleException($passable, new FatalThrowableError($e));}};};}
复制代码

这里其实每次执行返回的就是个新闭包,同时 $stack,$pipe 的值也随着调用存入闭包。为了方便我声明下变量

$cb = function ($passable) use ($stack, $pipe) {try {$slice = parent::carry();$callable = $slice($stack, $pipe);return $callable($passable);} catch (Exception $e) {return $this->handleException($passable, $e);} catch (Throwable $e) {return $this->handleException($passable, new FatalThrowableError($e));}};
复制代码

所以上面 $cb 的值就是 $this->carry() 执行后返回的闭包就像洋葱一样,我们来看封装过程。

第一次封装 $stack1 = $cb($passable) use ($this->prepareDestination($destination),'App\Http\Middleware\TrustProxies')
第二次封装 $stack2 = $cb($passable) use ($stack1,'Illuminate\Foundation\Http\Middleware\ConvertEmptyStringsToNull')
第三次封装 $stack3 = $cb($passable) use ($stack2,'App\Http\Middleware\TrimStrings')
第四次封装 $stack4 = $cb($passable) use ($stack3,'Illuminate\Foundation\Http\Middleware\ValidatePostSize')
第五次封装 $stack5 = $cb($passable) use ($stack4,'App\Http\Middleware\CheckForMaintenanceMode')
复制代码

最后 $pipeline 对象实际就是 $stack5

看到这里我们获取了一个层层封装的闭包,同时我们也看出为什么中间件的顺序先反转了,因为执行的时候是从 $stack5 开始的!那么下一步就是看看如何执行了。

return $pipeline($this->passable);
复制代码

在递归完成之后我们获得了一个 $pipeline 对象, 此时我们触发这个闭包,后面就是连锁反应!这里我用 $stack5 来代替 $pipeline 方便理解。 首先执行

$stack5($this->passable,'App\Http\Middleware\CheckForMaintenanceMode')
复制代码

这段代码是一个起点,也就是点燃整个连锁反应的开始,我们来追踪下去会回到 $cb 这个闭包的逻辑,

$cb = function ($passable) use ($stack, $pipe) {try {$slice = parent::carry();$callable = $slice($stack, $pipe);return $callable($passable);} catch (Exception $e) {return $this->handleException($passable, $e);} catch (Throwable $e) {return $this->handleException($passable, new FatalThrowableError($e));}};
复制代码

这里最终还是调用了 parent::carry(), 执行到了最里层的函数。

 protected function carry(){return function ($stack, $pipe) {return function ($passable) use ($stack, $pipe) {if (is_callable($pipe)) {return $pipe($passable, $stack);} elseif (!is_object($pipe)) {[$name, $parameters] = $this->parsePipeString($pipe);$pipe = $this->getContainer()->make($name);$parameters = array_merge([$passable, $stack], $parameters);} else {$parameters = [$passable, $stack];}$response = method_exists($pipe, $this->method)? $pipe->{$this->method}(...$parameters): $pipe(...$parameters);return $response instanceof Responsable? $response->toResponse($this->container->make(Request::class)): $response;};};}
复制代码

到这里我们已经进入最后的堡垒,由于传入的 $pipe 是中间件的名称,不是闭包所以进入 elseif 中开始执行。 第一次执行:

$stack5 = $cb($passable) use ($stack4,'App\Http\Middleware\CheckForMaintenanceMode')
复制代码
function ($passable) use ($stack, $pipe) {if (is_callable($pipe)) {return $pipe($passable, $stack);} elseif (!is_object($pipe)) {// 进入这里开始执行[$name, $parameters] = $this->parsePipeString($pipe);$pipe = $this->getContainer()->make($name); // 从通过Application对象从容器中生产对应的类,这里不拓展了,就是应用了容器的特性来生产类。$parameters = array_merge([$passable, $stack], $parameters); // 这里非常重要,将 $passable (就是开始的 $request 对象) 和 $stack (就是最近一次调用的$stack4) 合并成数组} else {$parameters = [$passable, $stack];}$response = method_exists($pipe, $this->method)? $pipe->{$this->method}(...$parameters): $pipe(...$parameters); // 调用中间件中$pipe->handle($request, $stack4)return $response instanceof Responsable? $response->toResponse($this->container->make(Request::class)): $response;};
复制代码

分析完上面并没有完成,最后代码运行到

$this->method = 'handle';  默认配置,可以通过 $this->via($method) 来修改。$response = method_exists($pipe, $this->method)? $pipe->{$this->method}(...$parameters): $pipe(...$parameters); // ...$parameters 解构数组参数实际调用 $pipe->handle($request, $stack4)
复制代码

此时只是调用一次闭包,那么之前封装了那么多层都怎么办呢?

接下来我们分析 CheckForMaintenanceMode 中间件的 handle($request, Closure $next) 方法。

public function handle($request, Closure $next){if ($this->app->isDownForMaintenance()) {$data = json_decode(file_get_contents($this->app->storagePath().'/framework/down'), true);if (isset($data['allowed']) && IpUtils::checkIp($request->ip(), (array) $data['allowed'])) {return $next($request);}if ($this->inExceptArray($request)) {return $next($request);}throw new MaintenanceModeException($data['time'], $data['retry'], $data['message']);}return $next($request);}
复制代码

return $next($request); 这句话点亮了一切

实际调用了 $stack4($request) , 我们来看看当时 $stack4 这个闭包里面是啥

$stack4 = $cb($passable) use ($stack3,'Illuminate\Foundation\Http\Middleware\ValidatePostSize')
复制代码

是不是和 $stack5 有点像, 直到这里形成了递归, 同时解答了为什么中间件的格式要按照文档上面说用。

回到最初的封装

第一次封装 $stack1 = $cb($passable) use ($this->prepareDestination($destination),'App\Http\Middleware\TrustProxies')
第二次封装 $stack2 = $cb($passable) use ($stack1,'Illuminate\Foundation\Http\Middleware\ConvertEmptyStringsToNull')
第三次封装 $stack3 = $cb($passable) use ($stack2,'App\Http\Middleware\TrimStrings')
第四次封装 $stack4 = $cb($passable) use ($stack3,'Illuminate\Foundation\Http\Middleware\ValidatePostSize')
第五次封装 $stack5 = $cb($passable) use ($stack4,'App\Http\Middleware\CheckForMaintenanceMode')
复制代码

我们的调用链就变成了

$stack5 = $cb($passable) use ($stack4,'App\Http\Middleware\CheckForMaintenanceMode')
$stack4 = $cb($passable) use ($stack3,'Illuminate\Foundation\Http\Middleware\ValidatePostSize')
$stack3 = $cb($passable) use ($stack2,'App\Http\Middleware\TrimStrings')
$stack2 = $cb($passable) use ($stack1,'Illuminate\Foundation\Http\Middleware\ConvertEmptyStringsToNull')
$stack1 = $cb($passable) use ($this->prepareDestination($destination),'App\Http\Middleware\TrustProxies')
复制代码

最后执行

$this->prepareDestination($destination)$destination = $this->dispatchToRouter();return function ($passable) use ($destination) {return $destination($passable);
}; // 返回一个 $response 对象  ...复制代码

到这里管道的核心代码就结束了,当然是通过在内核启动周期中 关于请求发送到路由获取响应这个实例来解析。

laravel 中路由对系统的管道做了细微的拓展,整体还是没啥变化,就是闭包套闭包,不停地调用,就像剥洋葱。

【Laravel-海贼王系列】第七章,Pipeline 类解析相关推荐

  1. JavaSE进阶 第七章 常用类 (一) String

    目录 1.注意点 2.String类的构造方法 3.String类的常用方法 传送门 1.注意点 java中用双引号括起来的都是String对象,且字符串是不可变的 java中用双引号括起来的字符串都 ...

  2. ppt_第七章_类人DNA与神经元基于催化算子映射编码方式.

    第七章_类人DNA与神经元基于催化算子映射编码方式. AOPM-VECS-IDUQ 十二元基建模与应用, 文件肽化方式. 1 DETA humanoid cognition 1.1DETA human ...

  3. java最小访问原则_Android基础进阶之EffectiveJava翻译系列(第七章:通用原则)

    本章主要讨论语言的具体内容.它讨论了局部变量的处理.控制结构.库的使用.各种数据类型的使用,以及使用反射和本地方法.最后,讨论了优化和命名约定 Item 45:最小化局部变量作用域 作用域:一个花括号 ...

  4. 第七章_类_7.4 类的作用域(加了自己的总结部分)

    每个类都会定义自己的作用域. 在类的作用域之外,普通的数据和函数成员只能由对象.引用或者指针使用成员访问运算符来访问.对于类类型成员则使用作用域运算符访问.不论哪种情况,跟在运算符之后的名字都必须是对 ...

  5. 《java编程思想》第七章 复用类

    组合:在新的类中产生现有类的对象.只是复用了现有程序代码的功能,而非形式. 继承:按照现有类的类型来创建新类,无需改变现有类的形式.采用现有类(基类)的形式,并在其中添加新代码. 都是利用现有类型生成 ...

  6. 第七章-复用类-继承语法-1

    练习2: 从Detergent 中 继承产生一个新的类.覆盖scrub() 并添加一个名为sterilize() 的新的方法. 一:我的答案: 1 package com.learnJava.test ...

  7. dockerfile如何运行镜像内的脚本_第七章 Dockerfile文件解析(一)

    七 Dockerfile文件解析-1 7.1 定义:Dockerfile是用来构建Docker镜像的文件,是由一系列命令和参数构成的脚本 7.2 Dockerfile内容基础知识: 1.每条保留字指令 ...

  8. 《IBM-PC汇编语言程序设计》(第2版)【沈美明 温冬婵】——第七章——自编解析与答案

    7.1 编写一条宏指令CLRB,完成用空格符将一字符区中的字符取代的工作.字符区首地址及其长度为变 元. 答:宏定义如下: CLRB MACRO N, CFIL MOV CX, N CLD MOV A ...

  9. 华文慕课北大操作系统陈向群第七章课后习题解析

    1.(5分) 为了保证CPU执行程序指令时能正确访问存储单元,需要将用户进程中的逻辑地址转换为运行时可由CPU直接寻址的物理地址,这一过程称为: A. 地址映射 B. 地址分配 C. 地址计算 D. ...

最新文章

  1. 十七、生产者消费者问题
  2. DataGridView 分页显示
  3. 教你如何在Android Studio中使用DDMS工具查看logcat——移动测试Android app(app的性能监控与测试)
  4. awk:split()函数、数组、自定义函数
  5. RDD Persistence持久化
  6. Scrapy和MongoDB的应用---爬取
  7. python程序设计基础教程慕课版课后题答案_Python语言程序设计基础
  8. android无线投屏到电视盒子,【沙发管家】教你如何把电脑视频投屏到智能电视/电视盒子上!...
  9. Filenet基金会发放第一期打包节点公开激励
  10. 法兰克焊接机器人编程入门_Fanuc ARC MATE 焊接机器人操作编程.pdf
  11. emmc、Nand flash、Nor flash之间的区别
  12. 脸部匹配测试软件,手把手教你在浏览器中使用脸部识别软件包
  13. Android 内部存储和外部存储
  14. 乔布斯在斯坦福大学的演讲稿【中英】
  15. SN74LVC4245A/(74LVC245) 真是个好东西啊
  16. C++使用ADODB连接数据库
  17. 图片流量节省大杀器:基于 CDN 的 sharpP 自适应图片技术实践
  18. 浅谈IM软件如何建立安全socket连接、登录
  19. mysql 插入学生信息_MySQL经典50题-1-创建数据表和插入数据
  20. 算法描述的一般格式和设计步骤

热门文章

  1. 计算机操作系统:存储器的管理
  2. windows下带超时的telnet探测IP和端口
  3. shell中单引号的嵌套
  4. javascript滚动文字
  5. 学习了Python那么长的世界,有没有玩转过hello word?
  6. 在IIS6中FLV不能播放
  7. linux定时任务的设置
  8. ASP.NET程序员职位要求!
  9. 设备中的c语言代码文件,设备树编译器无法识别包含文件的C语法
  10. Redfield.Sketch.Master中文版