- laravel中的管道(Pipeline)是什么?
所谓管道(Pipeline)设计模式,就是把数据传递给一个任务队列,由任务队列按次序依次对数据进行加工处理。在laravel框架中,这里的数据就是http请求,任务队列包含了一个又一个的中间件。
类比1:以流水线或流水管道作类比,流水线上的产品(http请求),依次经过一个又一个的加工单元(对应一个又一个的中间件)进行处理,最后生成产品(http响应)。
类比2:同样的,也可以与linux下的管道作类比,
cat helloworld.txt | grep "hello world" | rev | > output.txt
- laravel中如何使用pipeline?
Laravel 在框架中的很多地方使用了管道设计模式,最常见的就是中间件的实现。
当请求最终到达控制器动作被处理前,会先经过一系列的中间件。每个中间价都有一个独立的职责,例如,设置 Cookie、判断是否登录以及阻止 CSRF 攻击等等。每个阶段都会对请求进行处理,如果请求通过就会被传递给下一个处理,不通过就会返回相应的 HTTP 响应。
这种机制使得我们很容易在请求最终到达应用代码前添加处理操作,当然如果不需要这个处理操作你也可以随时移除而不影响请求的生命周期。 - Pipeline有什么优点?
1. 将复杂的处理流程分解成独立的子任务,从而方便测试每个子任务;
2. 被分解的子任务可以被不同的处理进程复用,避免代码冗余。(这里说的不同的处理进程是指,针对不同的http请求,采用不同的子任务组合来处理)
3. 在复杂进程中添加、移除和替换子任务非常轻松,对已存在的进程没有任何影响。 - Pipeline有什么缺点?
1. 虽然每个子任务变得简单了,但是当你再度尝试将这些子任务组合成完整进程时有一定复杂性;
2. 你还需要保证独立子任务测试通过后整体的流程能正常工作,这有一定的不确定性。(因为在管道中流动的是http请求,并且子任务可以修改http请求,这样就存在前一次的修改内容导致下一个子任务的执行失败的可能性)
3. 当你看到的都是一个个子任务时,对理解整体流程带来困难(盲人摸象的故事想必大家很熟悉,正是此理)。 - 代码理解
这里只局部分析管道实现的三个文件,它们并没有组成一个完整的工作流程。想要了解完整流程,还需要研究后面的几个文件。分析的文件:
下面的部分是对Pipeline接口的定义,定义了send、through、via、then四个方法。(这里不知道via方法具体是干嘛的)
1. laravel/vendor/laravel/framework/src/Illuminate/Contracts/Pipeline/Pipeline.php
2. laravel/vendor/laravel/framework/src/Illuminate/Pipeline/Pipeline.php
3. laravel/vendor/laravel/framework/src/Illuminate/Routing/Pipeline.php
了解完整流程,还需要看以下文件:
1. laravel/public/index.php
2. laravel/vendor/laravel/framework/src/Illuminate/Foundation/Http/Kernel.php
3. laravel/vendor/laravel/framework/src/Illuminate/Foundation/Application.php
laravel\vendor/laravel/framework/src/Illuminate/Contracts/Pipeline/Pipeline.php
1 <?php 2 3 namespace Illuminate\Contracts\Pipeline; 4 5 use Closure; 6 7 interface Pipeline 8 { 9 /** 10 * Set the traveler object being sent on the pipeline. 11 * 12 * @param mixed $traveler 13 * @return $this 14 */ 15 public function send($traveler); 16 17 /** 18 * Set the stops of the pipeline. 19 * 20 * @param dynamic|array $stops 21 * @return $this 22 */ 23 public function through($stops); 24 25 /** 26 * Set the method to call on the stops. 27 * 28 * @param string $method 29 * @return $this 30 */ 31 public function via($method); 32 33 /** 34 * Run the pipeline with a final destination callback. 35 * 36 * @param \Closure $destination 37 * @return mixed 38 */ 39 public function then(Closure $destination); 40 }
接下来,是对管道的实现.
protected $container; //保存服务容器的实例
protected $passable; //保存传入的http请求
protected $pipes = []; //保存子任务队列,子任务可以使闭包函数,也可以使类名与参数名的字符串组合
protected $method = 'handle'; //当子任务是类名+参数的字符串组合时,$method指定在管道处理到该类子任务时,该类子任务用来处理http请求的方法名。$method默认是handle,但是可以通过 via()方法修改
在一次http的请求过程中,以下方法的被调用的过程是:send() -> through() -> then() 。
public function send($passable){} //传入初始的http请求
public function through($pipes){} //设置管道的子任务队列
public function via($method){} //设置$method的值
public function then(Closure $destination){} //启动管道,用设定的子任务队列去处理http请求
protected function getSlice(){} //本函数返回array_reduce()中所需要的第二个参数,callback函数
protected function getInitialSlice(Closure $destination){} //本函数对应array_reduce()中所需要的第三个参数,初始化值
protected function parsePipeString($pipe){} //针对子任务是类名+参数名的字符串组合,提取类名和参数
由上面可知,整个管道的处理逻辑主要集中在then()方法中。
then()方法中,最难懂的是下面这句话:
1 return call_user_func( 2 array_reduce($pipes, $this->getSlice(), $firstSlice), $this->passable 3 );结合getSlice()方法,array_reduce()的处理过程实际上是,利用了闭包函数的特点,用闭包函数保存了局部作用域中的参数$stack和$pipe,并将保存了局部scope的闭包函数作为对象,压如由$stack保存的堆栈中,当将整个逆序的子任务队列的执行函数的闭包函数形式压入栈中后,再通过call\_user\_func(),传入$this->passable(即http请求),从栈中依次弹出闭包函数处理请求。
laravel\vendor/laravel/framework/src/Illuminate/Pipeline/Pipeline.php
1 <?php 2 3 namespace Illuminate\Pipeline; 4 5 use Closure; 6 use Illuminate\Contracts\Container\Container; 7 use Illuminate\Contracts\Pipeline\Pipeline as PipelineContract; 8 9 class Pipeline implements PipelineContract 10 { 11 /** 12 * The container implementation. 13 * 14 * @var \Illuminate\Contracts\Container\Container 15 */ 16 protected $container; 17 18 /** 19 * The object being passed through the pipeline. 20 * 21 * @var mixed 22 */ 23 protected $passable; 24 25 /** 26 * The array of class pipes. 27 * 28 * @var array 29 */ 30 protected $pipes = []; 31 32 /** 33 * The method to call on each pipe. 34 * 35 * @var string 36 */ 37 protected $method = 'handle'; 38 39 /** 40 * Create a new class instance. 41 * 42 * @param \Illuminate\Contracts\Container\Container $container 43 * @return void 44 */ 45 public function __construct(Container $container) 46 { 47 $this->container = $container; 48 } 49 50 /** 51 * Set the object being sent through the pipeline. 52 * 53 * @param mixed $passable 54 * @return $this 55 */ 56 public function send($passable) 57 { 58 $this->passable = $passable; 59 60 return $this; 61 } 62 63 /** 64 * Set the array of pipes. 65 * 66 * @param array|mixed $pipes 67 * @return $this 68 */ 69 public function through($pipes) 70 { 71 $this->pipes = is_array($pipes) ? $pipes : func_get_args(); 72 73 return $this; 74 } 75 76 /** 77 * Set the method to call on the pipes. 78 * 79 * @param string $method 80 * @return $this 81 */ 82 public function via($method) 83 { 84 $this->method = $method; 85 86 return $this; 87 } 88 89 /** 90 * Run the pipeline with a final destination callback. 91 * 92 * @param \Closure $destination 93 * @return mixed 94 */ 95 public function then(Closure $destination) 96 { 97 $firstSlice = $this->getInitialSlice($destination); 98 99 $pipes = array_reverse($this->pipes); 100 101 return call_user_func( 102 array_reduce($pipes, $this->getSlice(), $firstSlice), $this->passable 103 ); 104 } 105 106 /** 107 * Get a Closure that represents a slice of the application onion. 108 * 109 * @return \Closure 110 */ 111 protected function getSlice() 112 { 113 return function ($stack, $pipe) { 114 return function ($passable) use ($stack, $pipe) { 115 // If the pipe is an instance of a Closure, we will just call it directly but 116 // otherwise we'll resolve the pipes out of the container and call it with 117 // the appropriate method and arguments, returning the results back out. 118 if ($pipe instanceof Closure) { 119 return call_user_func($pipe, $passable, $stack); 120 } else { 121 list($name, $parameters) = $this->parsePipeString($pipe); 122 123 return call_user_func_array([$this->container->make($name), $this->method], 124 array_merge([$passable, $stack], $parameters)); 125 } 126 }; 127 }; 128 } 129 130 /** 131 * Get the initial slice to begin the stack call. 132 * 133 * @param \Closure $destination 134 * @return \Closure 135 */ 136 protected function getInitialSlice(Closure $destination) 137 { 138 return function ($passable) use ($destination) { 139 return call_user_func($destination, $passable); 140 }; 141 } 142 143 /** 144 * Parse full pipe string to get name and parameters. 145 * 146 * @param string $pipe 147 * @return array 148 */ 149 protected function parsePipeString($pipe) 150 { 151 list($name, $parameters) = array_pad(explode(':', $pipe, 2), 2, []); 152 153 if (is_string($parameters)) { 154 $parameters = explode(',', $parameters); 155 } 156 157 return [$name, $parameters]; 158 } 159 }
下面是对管道添加了异常处理的实现。
laravel\vendor/laravel/framework/src/Illuminate/Routing/Pipeline.php
1 <?php 2 3 namespace Illuminate\Routing; 4 5 use Closure; 6 use Throwable; 7 use Exception; 8 use Illuminate\Http\Request; 9 use Illuminate\Contracts\Debug\ExceptionHandler; 10 use Illuminate\Pipeline\Pipeline as BasePipeline; 11 use Symfony\Component\Debug\Exception\FatalThrowableError; 12 13 /** 14 * This extended pipeline catches any exceptions that occur during each slice. 15 * 16 * The exceptions are converted to HTTP responses for proper middleware handling. 17 */ 18 class Pipeline extends BasePipeline 19 { 20 /** 21 * Get a Closure that represents a slice of the application onion. 22 * 23 * @return \Closure 24 */ 25 protected function getSlice() 26 { 27 return function ($stack, $pipe) { 28 return function ($passable) use ($stack, $pipe) { 29 try { 30 $slice = parent::getSlice(); 31 32 return call_user_func($slice($stack, $pipe), $passable); 33 } catch (Exception $e) { 34 return $this->handleException($passable, $e); 35 } catch (Throwable $e) { 36 return $this->handleException($passable, new FatalThrowableError($e)); 37 } 38 }; 39 }; 40 } 41 42 /** 43 * Get the initial slice to begin the stack call. 44 * 45 * @param \Closure $destination 46 * @return \Closure 47 */ 48 protected function getInitialSlice(Closure $destination) 49 { 50 return function ($passable) use ($destination) { 51 try { 52 return call_user_func($destination, $passable); 53 } catch (Exception $e) { 54 return $this->handleException($passable, $e); 55 } catch (Throwable $e) { 56 return $this->handleException($passable, new FatalThrowableError($e)); 57 } 58 }; 59 } 60 61 /** 62 * Handle the given exception. 63 * 64 * @param mixed $passable 65 * @param \Exception $e 66 * @return mixed 67 * 68 * @throws \Exception 69 */ 70 protected function handleException($passable, Exception $e) 71 { 72 if (! $this->container->bound(ExceptionHandler::class) || ! $passable instanceof Request) { 73 throw $e; 74 } 75 76 $handler = $this->container->make(ExceptionHandler::class); 77 78 $handler->report($e); 79 80 $response = $handler->render($passable, $e); 81 82 if (method_exists($response, 'withException')) { 83 $response->withException($e); 84 } 85 86 return $response; 87 } 88 }
参考文献:
1. Laravel 中管道设计模式的使用 —— 中间件实现原理探究
2. 不依赖于任何框架的管道