在 PHP 中管道(Pipeline) 能帮我们做什么?

前言

看到这个标题,你或许会疑惑:“什么是管道?”;“它有什么用?”;”它有什么优势?“;“它能解决什么”‘下面,就以我的视角来给你解开它。

什么是管道?

管道,在 PHP 开发中其实我们很少有听到有人说管道这个词,在 Linux 操作中听到的比较多 “管道操作符 | ”,其实这里我们要探讨的管道和这个也是一个概念,就如名字一样,“管道”,当数据从管道的一头进入后,经过内部的处理,最终再从另一头出来。

在经过管道的途中,我们可以对我们传入的数据或者处理结果进行相应的调整以达到我们的目的。

它有什么用?

经过上面的介绍,你可能对管道有了一定的认识,但是仅限于理论层面的,相当抽象,甚至于还无法接受这种思想。如果我现在告诉你,如果你使用过 Laravel,其实你早就已经用过它了,你可能会惊讶。是的,在大多是情况下,你确实已经使用过它了,在 Laravel 中最常见的功能 「中间件」,就是基于管道来实现的。

中间件

通过使用中间件,可以在对传入的 Request $request 进行操作,甚至修改它的值,我们也可以调用 $next($request) ,来获取结果后对处理的 Response 添加 Header ,我们也可以在接到一些特殊请求后直接处理返回,比如处理跨域,大多数时候就是在这里处理的。如果你有兴趣,还可以往下看看

它能解决什么?

是的,很多时候我们学习一个新事物的时候,这个事物能给我们带来什么,通常是我们最关心的。

试着想一下,你现在有一个电商程序,在最初的时候,你只需要顾客提交商品创建订单、支付,这一切看起来似乎很简单。

// 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();
    }
}

看起来似乎也还好,但是如果我们还有更多这样类似的需求呢?你会发现有点儿不妙,我们代码方向变了,而且更乱了,更加的不好控制,可能过一段时间,我都已经“不敢承认”,这曾经是自己写的代码了,看起来我们需要解决这个问题,现在,是时候让文章的主角“管道”出场了。

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 中管道的源码,从而来帮助你更加深刻的去理解它。

参考资料

我来评几句
登录后评论

已发表评论数()

相关站点

热门文章