Laravel Pipeline原理及使用
開發中可能遇到非常冗長的邏輯,以至於我們想將針對性邏輯拆分出來,但是又拿不准該如何拆分才能實現較高的擴展性並保證較高的維護性,或者說不知道如何優雅的將待處理的數據在多個邏輯中傳遞,那么面向切面編程(AOP)可能會幫助到你。本文講解laravel中一個AOP的良好實現Pipeline。 不了解array_reduce的請先查看手冊
直接上簡易代碼
<?php
interface MyFilter
{
public static function handle(array $request, callable $next);
}
class FilterScum implements MyFilter
{
public static function handle($request, $next)
{
// 前置中間件一
echo '前置中間件一執行', PHP_EOL;
// some logic inc
array_push($request, 'handled by scum filter');
$next($request);
}
}
class FilterPieces implements MyFilter
{
public static function handle($request, $next)
{
// 前置中間件二
echo '前置中間件二執行', PHP_EOL;
array_push($request, 'handled by piece filter');
$next($request);
}
}
class FilterDot implements MyFilter
{
public static function handle($request, $next)
{
$request = $next($request);
echo '后置中間件一執行', PHP_EOL;
array_push($request, 'handled by dot filter');
var_dump($request);
}
}
// 主邏輯閉包
$mainLogic = function ($request) {
echo 'echo from main logic', PHP_EOL;
array_push($request, 'through main logic');
var_dump($request);
echo 'main logic processed', PHP_EOL;
return $request;
};
$filters = [
FilterScum::class,
FilterPieces::class,
FilterDot::class
];
$filters = array_reverse($filters);
$callableOnion = array_reduce($filters, function ($destination, $filter) {
return function ($request) use ($destination, $filter) {
return $filter::handle($request, $destination);
};
}, $mainLogic);
// $callableOnion(['got no filtered yet']);
call_user_func($callableOnion, ['got no filtered yet']);
// 執行文件 輸出如下
前置中間件一執行
前置中間件二執行
echo from main logic
array(4) {
[0]=>
string(19) "got no filtered yet"
[1]=>
string(22) "handled by scum filter"
[2]=>
string(23) "handled by piece filter"
[3]=>
string(18) "through main logic"
}
main logic processed
后置中間件一執行
array(5) {
[0]=>
string(19) "got no filtered yet"
[1]=>
string(22) "handled by scum filter"
[2]=>
string(23) "handled by piece filter"
[3]=>
string(18) "through main logic"
[4]=>
string(21) "handled by dot filter"
}
callableOnion產生的講解(偽代碼)
執行array_reduce
step1
return function () { FilterDot::handle($mainLogic) };
step2
return function () {
FilterPieces::handle (
function () { Filter::handle($mainLogic) }
)
};
step3 將他移動到FilterDot類中吧 可能會幫助大家理解
return function () {
FilterScum::handle(){
function () {
FilterPieces::handle (
function () { Filter::handle($mainLogic()) }
)
}
};
當調用step3生成的最終的閉包的時候,先執行了FilterScum的handle方法,其中echo了字符串,向數組中追加了元素,最后調用了傳遞的閉包,
即在step2生成的閉包,也就是執行了FilterPieces的handle方法,其中echo了字符串,向數組中追加了元素,最后調用了傳遞的閉包
即在step1生成的閉包, 也就是執行了FilterDot的handle方法,只不過他先調用了主邏輯閉包,后執行了自身的邏輯
希望看到這里大家能夠明白為什么要進行一次array_reverse
以上代碼中的filter就是主邏輯中拆分出去的切面,將龐大的邏輯拆分成小而針對性強的邏輯,並通過調用閉包的方式,傳遞各個切面處理后的數據到另外的切面,從而實現龐大的主邏輯,這樣可以實現主邏輯的拆分、擴展,同時保證代碼的可維護性。
使用過laravel中間件的道友可能會發現和上面的代碼非常類似,沒錯laravel中的中間件就是面向切面編程的良好實現,那么laravel又是如何確定一個請求需要走過哪些中間件,並且最終調用哪段代碼邏輯,並生成響應的呢?請出我們本篇的主角 Pipeline,讓我們再次感受laravel的優雅吧!
// Illuminate\Foundation\Http\Kernel::sendRequestThroughRouter方法
protected function sendRequestThroughRouter($request)
{
$this->app->instance('request', $request);
Facade::clearResolvedInstance('request');
$this->bootstrap();
// 這段代碼真的非常漂亮,實現流接口,面向對象,且人類可讀,我們一起結合生活實際感受下
// new Pipeline 首先獲得Pipeline實例 對應的實際場景:找到一個管道
// send方法 從管道的一頭送入經過管道各個節點處理的數據(laravel 6 中真的可以是任何數據) 對應的實際場景:從管道一頭送入包含雜質的原料(包含三角形,方形的雜質和我們真正需要的圓形原料)
// through方法 $this->middleware就是 App\Http\Kernel中的middleware全局中間件數組,表示請求要先經過這些中間件的過濾,若中間件中返回false,則管道停止前進,不會進行路由匹配 對應的實際場景:在管道的各個節點上加上圓形的篩板,只有圓形的原料可以通過,雜質都被剔除掉了
// then方法,經過了前面中間件的過濾,然后進行路由匹配,執行實際的業務邏輯 對應的實際場景:得到了圓形的原材料,生產產品交付用戶
return (new Pipeline($this->app))
->send($request)
->through($this->app->shouldSkipMiddleware() ? [] : $this->middleware)
->then($this->dispatchToRouter());
}
// 跳轉到send方法
public function send($passable)
{
// 要通過管道依次傳遞到不同切面(中間件)的任何東西
$this->passable = $passable;
return $this;
}
// 跳轉到through方法
public function through($pipes)
{
// 傳遞的數據要經過的切面(中間件)
$this->pipes = is_array($pipes) ? $pipes : func_get_args();
return $this;
}
// 執行最終的邏輯 產生響應返回給用戶
// 結合文章開頭的簡易代碼
public function then(Closure $destination)
{
// 生成一條指向目的地,且搭建好了各個篩板(中間件)的管道
$pipeline = array_reduce(
// 跳轉到carry方法 其實就是文章開頭array_reduce的第二個閉包
array_reverse($this->pipes()), $this->carry(), $this-
// prepareDestination 將最終要執行的方法 分裝成閉包
>prepareDestination($destination)
);
// 向管道中投遞要處理的數據,生成響應並返回
// 就是本文開頭的 call_user_func($callableOnion, ['got no filtered yet']);
return $pipeline($this->passable);
}
// 返回供array_reduce迭代使用的閉包
protected function carry()
{
return function ($stack, $pipe) {
return function ($passable) use ($stack, $pipe) {
try {
if (is_callable($pipe)) {
// 本篇之說明最簡單的$pipe是個閉包的情況,其他分支會在后面的使用示例中展示
// 迭代的將閉包傳遞閉包中,並返回
return $pipe($passable, $stack);
} elseif (! is_object($pipe)) {
// dump($pipe);
[$name, $parameters] = $this->parsePipeString($pipe);
$pipe = $this->getContainer()->make($name);
$parameters = array_merge([$passable, $stack], $parameters);
} else {
$parameters = [$passable, $stack];
}
$carry = method_exists($pipe, $this->method)
? $pipe->{$this->method}(...$parameters)
: $pipe(...$parameters);
return $this->handleCarry($carry);
} catch (Exception $e) {
return $this->handleException($passable, $e);
} catch (Throwable $e) {
return $this->handleException($passable, new FatalThrowableError($e));
}
};
};
}
好了,理論講解就到這里吧,今天的文章只要搞懂array_reduce方法就差不多能夠理解了,下面講解laravel pipeline的使用 毫無疑問laravel依然為我們准備好了相應的服務。
使用
我們實現一個簡單的字符串過濾轉換管道
1 創建契約
<?php
namespace App\Contracts;
use Closure;
interface MyPipeline
{
public function myViaFunc($raw, Closure $next);
}
2 創建管道中間件
<?php
namespace App\Pipes;
use Closure;
use App\Contracts\MyPipeline;
class UcFilter implements MyPipeline
{
public function myViaFunc($string, Closure $next)
{
$string = ucfirst($string);
return $next($string);
}
}
3 使用管道
// 其中的via thenReturn方法非常簡單,請自行查看
Route::get('pipe', function () {
// $barString = app(\Illuminate\Pipeline\Pipeline::class)
$barString = (new \Illuminate\Pipeline\Pipeline(app('app')))
->via('myViaFunc')
->send('some foo string')
->through([App\Pipes\UcFilter::class])
->thenReturn();
dump($barString); // Some foo string
});
Route::get('pipes', function () {
$barString = app(\Illuminate\Pipeline\Pipeline::class)
->via('myViaFunc')
->send('some foo string')
->through([App\Pipes\UcFilter::class])
->then(function ($raw) {
dump($raw); // Some foo string
return substr($raw, 2);
});
dump($barString); // me foo string
});
// thenReturn和then方法的區別,then方法顯示的指定了管道的最后一站而已,也就是說產品生產完交付給用戶前的最后一站。如果你查閱源碼可以發現thenReturn方法就是調用的then方法。希望你是自己發現的。
// 隨便折磨一下管道
Route::get('pipess', function () {
$barString = app(\Illuminate\Pipeline\Pipeline::class)
->via('myViaFunc')
->send('some foo string')
->through(
// 你甚至可以這樣折磨管道 完全因為laravel pipeline的強大
// 對應carry方法中的各種分支 當然前提是能夠通過容器進行解析的類
function ($passable, $next) {
return $next(ucfirst($passable));
},
SomeFilterClass::class,
new SomeClass()
)
->thenReturn();
dump($barString);
});
// 在實際的應用中可能這樣使用更有意義些
public function create(Request $request, Pipeline $pipeline)
{
$something = $pipeline->send($request->someField)
->through([
過濾字符串類1::class,
過濾字符串類2::class,
過濾字符串類3::class
...
])->thenReturn();
SomeModel::doSomeShit([]);
...
}
通過上面的不恰當例子,希望能夠幫助大家認識管道的使用情景。
不出意外下篇就是laravel系列的完結了,返回響應給用戶。
發現錯誤,還望指教,感謝!!!