在 PHP 中管道(Pipeline) 能幫咱們作什麼?

前言

看到這個標題,你或許會疑惑:「什麼是管道?」;「它有什麼用?」;」它有什麼優點?「;「它能解決什麼」‘下面,就以個人視角來給你解開它。php

什麼是管道?

管道,在 PHP 開發中其實咱們不多有聽到有人說管道這個詞,在 Linux 操做中聽到的比較多 「管道操做符 |」,其實這裏咱們要探討的管道和這個也是一個概念,就如名字同樣,「管道」,當數據從管道的一頭進入後,通過內部的處理,最終再從另外一頭出來。
在通過管道的途中,咱們能夠對咱們傳入的數據或者處理結果進行相應的調整以達到咱們的目的。laravel

它有什麼用?

通過上面的介紹,你可能對管道有了必定的認識,可是僅限於理論層面的,至關抽象,甚至於還沒法接受這種思想。若是我如今告訴你,若是你使用過 Laravel,其實你早就已經用過它了,你可能會驚訝。是的,在大可能是狀況下,你確實已經使用過它了,在 Laravel 中最多見的功能 「中間件」,就是基於管道來實現的。git

中間件

經過使用中間件,能夠在對傳入的 Request $request 進行操做,甚至修改它的值,咱們也能夠調用 $next($request) ,來獲取結果後對處理的 Response 添加 Header ,咱們也能夠在接到一些特殊請求後直接處理返回,好比處理跨域,大多數時候就是在這裏處理的。若是你有興趣,還能夠往下看看github

它能解決什麼?

是的,不少時候咱們學習一個新事物的時候,這個事物能給咱們帶來什麼,一般是咱們最關心的。
試着想一下,你如今有一個電商程序,在最初的時候,你只須要顧客提交商品建立訂單、支付,這一切看起來彷佛很簡單。編程

// OrderService.php
class OrderService {
    // 建立訂單
    public function create(){
        // some code
        return new Order();
    }
    public function pay(Order $order){
        // some code
        $payInterface = new AliPay($order);
        return $payInterface->response();
    }
}

如今我功能基本有了,可是新的需求下來了,商城新加了一個會員卡,一級會員打 9.5 折,二級會員 9 折,三級會員 8.5 折,如今咱們的代碼可能成了這樣。跨域

class OrderService {
    public function pay(Order $order){
        $vipLevel = (int) Auth::user()->vipLevel();
        $vipMappings = [
            0 => 1,
            1 => 0.95,
            2 => 0.9,
            3 => 0.85,
        ];
        // 是的 這爲了保險起見,當取值出現問題時,默認爲 1 ,即爲不打折。
        $order->amount = bcmul($order->amount,$vipMappings[$vipLevel] ?? 1);
        $payInterface = new AliPay($order);
        return $payInterface->response();
    }
}

這看起來還好,只是折扣計算使得你原來的代碼再也不那麼「好看」了,若是接下來我告訴你,咱們加入了優惠券系統?部分商品須要打折,你會怎麼樣?一塊兒來看看。數組

// ...
// ...
// ...
class OrderService {
    public function pay(Order $order){
        bcscale(2);
        $amount = '0';
        foreach($order->products as $product){
            // 這裏的 discount 咱們就當他是跟下面 VIP 同樣的小數那樣。
            // 這裏彷佛還應該考慮限時折,這裏就不展開寫了。
            $amount = bcadd($amount,bcmul($product->price,$product->discount));
        }
        $order->amount = $amount;
        $vipLevel = (int) Auth::user()->vipLevel();
        $vipMappings = [
            0 => 1,
            1 => 0.95,
            2 => 0.9,
            3 => 0.85,
        ];
        // 是的 這爲了保險起見,當取值出現問題時,默認爲 1 ,即爲不打折。
        $order->amount = bcmul($order->amount,$vipMappings[$vipLevel] ?? 1);
        $payInterface = new AliPay($order);
        return $payInterface->response();
    }
}

看起來彷佛也還好,可是若是咱們還有更多這樣相似的需求呢?你會發現有點兒不妙,咱們代碼方向變了,並且更亂了,更加的很差控制,可能過一段時間,我都已經「不敢認可」,這曾經是本身寫的代碼了,看起來咱們須要解決這個問題,如今,是時候讓文章的主角「管道」出場了。app

Laravel 中的管道

在 Laravel 中,Laravel 已經幫助咱們實現了一個管道 \Illuminate\Pipeline\Pipeline ,咱們只須要根據須要,把數據放入管道,而後使用咱們本身的處理器去處理數據,最後在管道的另一頭將數據輸出,數據從進入管道後就再也不去關內心面會發生什麼,就像水同樣,它只須要關心從管道另外一頭輸出數據,首先咱們要開始簡化一下 pay 方法,讓他看起來儘可能保持簡潔。異步

class OrderService {
    public function pay(Order $order){
        try{
        $order = pipeline('order_service::pay',$order);
        }catch (Exception $e){
            // log..
            // 這裏咱們能夠記錄日誌,也能夠不記錄,直接不處理這個異常,而後由調用者來處理這個異常,
            // 由於這裏在關東中可能出現了一些狀況須要中止向下傳播
            throw $e;
        }
        $payInterface = new AliPay($order);
        return $payInterface->response();
    }
}

如今引入了一個 pipeline 方法可是如今並不存在這個方法,須要去建立它,用來作管道的觸發埋點。函數

如今咱們建立了一個管道函數,用來幫助咱們簡化調用,以便在其餘地方進行復用,這個方法接收2個參數,一個是埋點,一個是要傳遞的數據

// functions.php
if(!function_exists('pipeline')){
    function pipeline($pipe,$data){
        $pipeline = resolve(Pipeline::class);
        $handlers = config('pipeline.'.$pipe);
        return $pipeline->through($handlers)->send($data)->thenReturn();
    }
}

如今在配置文件中定義一下這個觸發點關聯的處理器,就像註冊事件那樣。

// config/pipeline.php
return [
    'order_service::pay'=>[
        // 產品折扣
        ProductDiscount::class,
        // 會員卡折扣
        UserVipDiscount::class,
        // 使用優惠券
        CouponUsing::class,
    ],
];

接着咱們來實現這些不一樣的處理器

class ProductDiscount {
    public function handle($order,$next){
        bcscale(2);
        $amount = '0';
        foreach($order->products as $product) {
            // 這裏的 discount 咱們就當他是跟下面 VIP 同樣的小數那樣。
            // 這裏彷佛還應該考慮限時折,這裏就不展開寫了。
            $amount = bcadd($amount,bcmul($product->price,$product->discount));
        }
        $order->amount = $amount;
        return $next($order);
    }
}
class UserVipDiscount {
    public function handle($order,$next) {
        $vipLevel = (int) Auth::user()->vipLevel();
                $vipMappings = [
                    0 => 1,
                    1 => 0.95,
                    2 => 0.9,
                    3 => 0.85,
                ];
        // 是的 這爲了保險起見,當取值出現問題時,默認爲 1 ,即爲不打折。
        $order->amount = bcmul($order->amount,$vipMappings[$vipLevel] ?? 1);
        return $next($order);
    }
}

如今就對代碼實現了一個解耦,同時保持了 pay 方法的潔淨,使得其更加單一,可是,這並不完美,爲何?

試想一下,管道中任意一個處理器返回數據錯了,按照預期,他應該老是接收一個 Order 對象,而且返回也應該是一個 Order 對象,一直這樣重複下去,到最終管道的結果也應該是一個 Order 對象,讓咱們能夠給 AliPay 對象做爲構造方法傳遞進去調用,可是在這裏,咱們並無去限制它,你也能夠本身來實現這一部分。

它有什麼優點?

管道和事件

經過上面籠統的介紹,和簡單的應用,你可能會以爲,這樣看起來,管道不是和事件很類似了麼?

都是註冊、而後觸發、最後獲取結果、異常處理。看起來是很類似,尤爲是前面的步驟,註冊、觸發、異常處理,可是獲取結果,好比在 Laravel 中,若是一個事件上註冊了多個事件
處理器,那在觸發事件後,咱們能夠在觸發函數接收到每一個事件處理器的返回值組成的數組,是的,若是上面的栗子咱們用事件去作,那麼就會返回三個 Order,這三個可能並無關係,由於當商品打折後使用會員卡時,咱們使用會員卡前的價格就應該是打折後的了,而事件就不同,事件始終都是使用的 源對象 ,也就是說在咱們後面的步驟中,實際上使用的仍是咱們觸發事件時所傳遞的那個 Order 對象,可是 Pipeline 會順序、準確的處理 Order 對象,並在最終返回一個對象給咱們。除此以外,還有一個不一樣點,多數狀況下管道的應用老是同步的,而事件則是能夠選擇使用異步來進行處理,進一步提高咱們業務處理效率。

因此說,根據不一樣的場景來選擇適合業務的方案,不但能夠優化咱們的代碼,還能夠提高性能。

他有點兒像 Hook

若是你看到過一些有關 PHP 中和 插件功能實現的文章或者 ThinkPHP 3.2 的代碼,你可能會以爲,它有點兒像 Hook,
可是若是你梳理完這二者的實現原理,你會發現,其實 「Hooks」 更像「事件」。

更酷的方案 「AOP(面向切面編程)」

經過上面的應用,咱們知道管道能夠幫咱們解決這種簡單的問題,可是如今還有一種能加 cool 的方案,使用 AOP 中的前置通知或者環繞通知,咱們也能夠作到不侵入原有業務的狀況下實現這個功能,若是你感興趣能夠去看看 Swoft 中關於 AOP) 章節的介紹。

可是,AOP 也是徹底可以替代管道,只是在這個例子中能夠徹底替代,但願你能明白這一點,管道可與在代碼進行中的任意一個階段埋點,這一點 AOP 是無法作到的。

結束

看完了以後,你是否很好奇,管道是怎麼實現的 ?經過這個連接,你能夠了解到在 Laravel 中管道的源碼,從而來幫助你更加深入的去理解它。

參考資料

相關文章
相關標籤/搜索