【Laravel系列6.4】管道过滤器
管道過濾器
通過之前的三篇文章,我們已經學習完了服務容器相關的內容,可以說,服務容器就是整個 Laravel 框架的靈魂,從啟動的第一步開始就是創建容器并且加載所有的服務對象。而說起管道,其實大家也不會太陌生,在程序開發的世界中,管道模式的應用隨處可見,同樣在 Laravel 框架中,它也是核心一般的存在。甚至可以說,管道和服務容器的組合,才讓我們有了一個這樣的框架可以使用。
什么是管道
前面說過,管道模式非常常見,為什么這么說呢?
ps?-ef?|?grep?php常見不?經常用吧?這個 Linux 命令就是一個管道命令。前面一條命令的結果交給后面一條命令來執行,就像一條管道一樣讓這個命令請求的結果向下流動,這就是管道模式的應用。
除了這個你還能想到什么呢?如果你跟過我的 PHP 設計模式系列的話,那么 責任鏈模式 很明顯就是管道模式在 面向對象 語言中的應用呀。
管道模式一般是和過濾器一起使用的,什么是過濾器呢?其實就是我們要處理請求的那些中間方法,比如說上面命令中的 grep ,或者是 wc 、awk 這些的命令。大家其實很快就能發現,在 Laravel 框架中,我們的中間件就是一個個的過濾器。而我們要處理的數據,就是那個 Request 請求對象。
Laravel 中管道的加載應用
還記得我們在服務容器中看到過的一個 sendRequestThroughRouter() 方法嗎?另外在最早講中間件時,我們也講過這里,我們再來看看它的代碼。
protected?function?sendRequestThroughRouter($request) {$this->app->instance('request',?$request);Facade::clearResolvedInstance('request');$this->bootstrap();return?(new?Pipeline($this->app))->send($request)->through($this->app->shouldSkipMiddleware()???[]?:?$this->middleware)->then($this->dispatchToRouter()); }在這段代碼中,最后返回的那個 Pipeline 對象就是一個管道對象。我們來看看它的這幾個方法是什么意思。
public?function?__construct(Container?$container?=?null) {$this->container?=?$container; }public?function?send($passable) {$this->passable?=?$passable;return?$this; }public?function?through($pipes) {$this->pipes?=?is_array($pipes)???$pipes?:?func_get_args();return?$this; }構造函數、send() 和 through() 方法都比較簡單,就是給當前的對象中的屬性賦值,這個沒什么特別的。不過在 Pipeline 對象中,所有的方法都是會 return 一個 $this ,其實也就是實現了對象的鏈式調用。
重點在于 then() 方法。
public?function?then(Closure?$destination) {$pipeline?=?array_reduce(array_reverse($this->pipes()),?$this->carry(),?$this->prepareDestination($destination));return?$pipeline($this->passable); }這個方法也出乎意料的簡單吧?里面只用了一個 array_reduce() ,OK,到這里,你就可以和面試官吹牛了,Laravel 中的管道,或者說中間件,其實最核心的就是這個 array_reduce() 方法。要搞清楚 then() 方法是在干什么,我們就要先搞明白 array_reduce() 是在干嘛。
array_reduce
array_reduce() 這個函數在官方文檔的簽名是這樣的:
array_reduce(array?$array,?callable?$callback,?mixed?$initial?=?null):?mixed它的作用是將回調函數 callback 迭代地作用到 array 數組中的每一個單元中,從而將數組簡化為單一的值。如果指定了可選參數 initial,該參數將用作處理開始時的初始值,如果數組為空,則會作為最終結果返回。
callback 這個回調函數會有兩個參數,分別是 carry 攜帶上次迭代的返回值,如果迭代是第一次,那么這個值就是 initial 。另一個參數是 item ,也就是數組中的每個值。
看不懂吧?正常,我也看不懂,別慌,看例子。
function?sum($carry,?$item) {$carry?+=?$item;return?$carry; }function?product($carry,?$item) {$carry?*=?$item;return?$carry; }$a?=?array(1,?2,?3,?4,?5); $x?=?array();var_dump(array_reduce($a,?"sum"));?//?int(15) var_dump(array_reduce($a,?"product",?10));?//?int(1200),?because:?10*1*2*3*4*5 var_dump(array_reduce($x,?"sum",?"No?data?to?reduce"));?//?string(17)?"No?data?to?reduce"這段代碼是官網上的例子。我們定義了一個 sum() 方法用于累加,另外再定義了一個 product() 方法用于階乘。前兩段測試的結果可以看出,通過將第一個數組傳遞進去,然后調用 sum() 方法,我們完成了累加的功能,輸出了一個唯一的結果值。第二段則是增加了第三個參數給了個默認的 10 ,結果就是多乘了一個 10 的累乘結果。而最后一段則是一個空的數組,返回的是 initial 給定的結果。
框架中 array_reduce 的參數
搞清楚了 array_reduce() 我們再回來看看框架源碼中給出的參數。第一個參數是使用 array_reverse() 返回之后的 pipes 里面的內容,這個 pipes 是我們通過 through() 方法傳遞進來的。再回到 Kernel 中,我們會發現這個方法傳遞進去的參數正是我們框架中加載的中間件 $middleware 成員變量。
之前的 bootstrap() 過程中,我們已經將所有的 app/Http/Kernel.php 中注冊的中間件綁定注冊到了服務容器中。因此,這個 pipes 數組中,就是我們所有的中間件信息。
接下來第二個參數是調用的一個 carry() 函數,它在 array_reduce() 方法中代表的是 callback 那個回調函數。
protected?function?carry() {return?function?($stack,?$pipe)?{return?function?($passable)?use?($stack,?$pipe)?{try?{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];}$carry?=?method_exists($pipe,?$this->method)??$pipe->{$this->method}(...$parameters):?$pipe(...$parameters);return?$this->handleCarry($carry);}?catch?(Throwable?$e)?{return?$this->handleException($passable,?$e);}};}; }這個方法就復雜許多了。我們一步步的來看。
參數不用多說了吧,stack 是上一次的返回值,pipe 是當前我們要處理的值,也就是當前的中間件對象。在這個回調函數中又調用了一層回調函數,并將這兩個值通過 use 傳遞進去。而在里面的這個回調函數中,我們的參數是 passable 這個變量。這個 passable 又是哪里來的?別急,我們先看這個函數內部的實現,最后會再說到 passable 這個問題。
進入函數內部的 try 代碼段中,第一個判斷,如果 pipe 是一個回調函數,直接調用它并返回;第二個判斷,如果 pipe 不是一個對象而是一個 string 的話,解構 pipe 信息,服務容器 make 它,并且準備好參數;最后一個 else 也就是 pipe 是一個對象,那么將 passable 和 stack 作為它的參數。最后,如果對象都有了,就會統一調用對象的 handle 方法,這個方法名也就是 $this->method 屬性定義的方法名。在最底下 $carry 調用對象或者回調函數的執行方法。handle 熟悉不?我們自定義中間件時,要實現的就是這個方法。參考:【Laravel系列3.4】中間件在路由與控制器中的應用?https://mp.weixin.qq.com/s/9340q7F_hKrrxgf4o1LNMw。
最終返回的就是這個 $carry 變量,它是啥?中間件中 return next() 的東西呀,管道中的下一個回調函數。
上面的代碼我們是嵌套了兩層的回調函數,通過之間的學習,我們知道回調函數是有延遲加載的特性的,也就說,這一堆代碼是在我們最終調用這個回調函數的時候才會觸發的,那么它是在什么時候調用的呢?
public?function?then(Closure?$destination) {$pipeline?=?array_reduce(array_reverse($this->pipes()),?$this->carry(),?$this->prepareDestination($destination));return?$pipeline($this->passable); }沒錯,then() 方法最后的這個 return 這里,現在知道 passable 是從哪里傳遞進去的了吧。注意,這個 passable 和最后那個默認 initial 參數,都是我們當前的請求 Request 對象和路由 Route 對象。也就是說,在整個 Laravel 框架中,我們管道中流動的,正是我們的 Request 對象,而最后返回的,則是各個中間件以及控制器處理完成之后的 Response 對象。中間件、控制器甚至路由,其實都是我們管道中的一個個的過濾器,根據我們的條件情況以及業務情況,可以隨時中斷或者對請求進行處理,這下也就理解了什么我們可以在中間件返回,也可以在路由直接返回頁面結果了吧。
好吧,學習一個管道,其實我們又把整個請求響應流程梳理了一遍。收獲滿滿吧!
直接寫一個管道應用來測試
直接調試管道可能比較復雜,因為 Laravel 框架加載的內容非常多,不過我們可以自己寫一個管道應用來測試,并且可以設置斷點來方便地調試。
首先,我們需要定義幾個過濾器,也就是我們的中間件啦,不過我們不需要去實現 Laravel 規范的,只需要有 handle() 方法就可以了。
class?AddDollar {public?function?handle($text,?$next){return?$next("$".$text."$");} }class?AddTime {public?function?handle($text,?$next){$t?=?$next($text);return?$t?.?time();} }class?EmailChange {public?function?handle($text,?$next){return?$next(str_replace("@",?"#",?$text));} }沒有什么特殊的功能,我們過濾掉 Email 中的 @ 符號變成 # 號,這個很多網站有會這樣的功能,避免被爬取 Email 地址。另外兩個就是增加符號和時間戳。在 AddTime 的處理中,我們使用的是 后置 中間件的功能,也就是在中間件完成處理后再添加內容。這個在中間件相關的課程中我們也已經講過了。
接下來,就是使用管道來進行處理。
Route::get('pipeline/test1',?function(){$pipes?=?[\App\PipelineTest\EmailChange::class,\App\PipelineTest\AddTime::class,new?\App\PipelineTest\AddDollar(),function($text,?$next){return?$next("【".$text."】");},];return?app(\Illuminate\Pipeline\Pipeline::class)->send("測試內容看看替換Email:zyblog@zyblog.ddd")->through($pipes)->then(function?($text)?{return?$text?.?"end";});//?$【測試內容看看替換Email:zyblog#zyblog.ddd】$end1630978948});在這段測試代碼中,我們對 pipes 數組使用了類字符串、實例對象、回調函數三種方式來實現中間件過濾器,可以看到最后的輸出結果正是我們想要的內容。
大家可以在這里設置斷點然后進入到 Pipeline 中查看這些中間件是如何調用運行的,為什么要使用 array_reverse() 反轉中間件的順序,為什么后置中間件會在最后才去添加數據內容。這一塊的調試就留給大家自己來吧!
總結
服務容器、管道(中間件)可以說是 Laravel 框架中最最核心的內容,也可以說整個框架就是建立在這兩個模式之下的。對于服務容器的理解,就是要解決類的依賴問題,而對于管道的理解,則是要解決請求和響應的數據流問題。本身我們做 Web 開發,實際上就是在做對請求和響應這兩條數據流的各種操作而已。
理解了最核心的兩部分內容之后,下篇文章的課程中我們再來看看在 Laravel 中非常常用的 門面 功能是怎樣實現的。
參考文檔:
Laravel 中的 Pipeline — 管道設計范式 :https://learnku.com/laravel/t/7543/pipeline-pipeline-design-paradigm-in-laravel
Laravel 管道流原理 :https://learnku.com/articles/5206/the-use-of-php-built-in-function-array-reduce-in-laravel
總結
以上是生活随笔為你收集整理的【Laravel系列6.4】管道过滤器的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: suppo aaa 0.75-php,f
- 下一篇: 泉州集训之HSY的day1