推薦一個 PHP 管道外掛 League\Pipeline

coding01發表於2018-07-08

Pipeline 設計模式

水管太長,只要有一處破了,就會漏水了,而且不利於複雜環境彎曲轉折使用。所以我們都會把水管分成很短的一節一節管道,然後最大化的讓管道大小作用不同,因地制宜,組裝在一起,滿足各種各樣的不同需求。

由此得出 Pipeline 的設計模式,就是將複雜冗長的流程 (processes) 截成各個小流程,小任務。每個最小量化的任務就可以複用,通過組裝不同的小任務,構成複雜多樣的流程 (processes)。

最後將「輸入」引入管道,根據每個小任務對輸入進行操作 (加工、過濾),最後輸出滿足需要的結果。

今天主要學習學習「Pipeline」,順便推薦一個 PHP 外掛:league/pipeline

gulp

第一次知道「pipe」的概念,來自 gulp 的使用。

gulp 是基於 NodeJS 的自動任務執行器,她能自動化地完成Javascriptsassless 等檔案的測試、檢查、合併、壓縮、格式化、瀏覽器自動重新整理、部署檔案生成,並監聽檔案在改動後重復指定的這些步驟。在實現上,她借鑑了 Unix 作業系統的管道 (pipe) 思想,前一級的輸出,直接變成後一級的輸入,使得在操作上非常簡單。

var gulp = require('gulp');
var less = require('gulp-less');
var minifyCSS = require('gulp-csso');
var concat = require('gulp-concat');
var sourcemaps = require('gulp-sourcemaps');

gulp.task('css', function(){
  return gulp.src('client/templates/*.less')
    .pipe(less())
    .pipe(minifyCSS())
    .pipe(gulp.dest('build/css'))
});

gulp.task('js', function(){
  return gulp.src('client/javascript/*.js')
    .pipe(sourcemaps.init())
    .pipe(concat('app.min.js'))
    .pipe(sourcemaps.write())
    .pipe(gulp.dest('build/js'))
});

gulp.task('default', [ 'html', 'css', 'js' ]);

上面的兩個 task 主要是將 less、所有 js 檔案進行解析、壓縮、輸出等流程操作,然後存到對應的資料夾下;每一步操作的輸出就是下一步操作的輸入,猶如管道的流水一般。

Illuminate\Pipeline

Laravel 框架中的中介軟體,就是利用 Illuminate\Pipeline 來實現的,本來想寫寫我對 「Laravel 中介軟體」原始碼的解讀,但發現網上已經有很多帖子都有表述了,所以本文就簡單說說如何使用 Illuminate\Pipeline

寫個 demo

public function demo(Request $request)
{
    $pipe1 = function ($payload, Closure $next) {
        $payload = $payload + 1;
        return $next($payload);
    };

    $pipe2 = function ($payload, Closure $next) {
        $payload = $payload * 3;
        return $next($payload);
    };

    $data = $request->input('data', 0);

    $pipeline = new Pipeline();

    return $pipeline
        ->send($data)
        ->through([$pipe1, $pipe2])
        ->then(function ($data) {
            return $data;
        });
}

對於該原始碼的分析,可以推薦看這篇文章,分析的挺透徹了:

Laravel Pipeline 元件的實現 https://www.insp.top/article/realization-of-pipeline-component-for-laravel

League\Pipeline

上面對 gulpIlluminate\Pipeline 的簡單使用,只是告訴我們「Pipeline」應用比較廣泛。如果讓我們自己也寫一個類似的外掛出來呢,我想應該也不是很難。

下面我拿 League\Pipeline 外掛來扒一扒它的原始碼,看如何實現的。

簡述

This package provides a plug and play implementation of the Pipeline Pattern. It’s an architectural pattern which encapsulates sequential processes. When used, it allows you to mix and match operation, and pipelines, to create new execution chains. The pipeline pattern is often compared to a production line, where each stage performs a certain operation on a given payload/subject. Stages can act on, manipulate, decorate, or even replace the payload.

If you find yourself passing results from one function to another to complete a series of tasks on a given subject, you might want to convert it into a pipeline.

https://pipeline.thephpleague.com/

安裝外掛

composer require league/pipeline

寫個 demo

use League\Pipeline\Pipeline;

// 建立兩個閉包函式
$pipe1 = function ($payload) {
    return $payload + 1;
};

$pipe2 = function ($payload) {
    return $payload * 3;
};

$route->map(
    'GET',
    '/demo',
    function (ServerRequestInterface $request, ResponseInterface $response
    ) use ($service, $pipe1, $pipe2) {
        $params = $request->getQueryParams();

        // 正常使用
        $pipeline1 = (new Pipeline)
            ->pipe($pipe1)
            ->pipe($pipe2);

        $callback1 = $pipeline1->process($params['data']);

        $response->getBody()->write("<h1>正常使用</h1>");
        $response->getBody()->write("<p>結果:$callback1</p>");

        // 使用魔術方法
        $pipeline2 = (new Pipeline())
            ->pipe($pipe1)
            ->pipe($pipe2);

        $callback2 = $pipeline2($params['data']);

        $response->getBody()->write("<h1>使用魔術方法</h1>");
        $response->getBody()->write("<p>結果:$callback2</p>");

        // 使用 Builder
        $builder = new PipelineBuilder();
        $pipeline3 = $builder
            ->add($pipe1)
            ->add($pipe2)
            ->build();

        $callback3 = $pipeline3($params['data']);

        $response->getBody()->write("<h1>使用 Builder</h1>");
        $response->getBody()->write("<p>結果:$callback3</p>");
        return $response;
    }
);

執行結果

解讀原始碼

整個外掛就這幾個檔案:

PipelineInterface

<?php
declare(strict_types=1);

namespace League\Pipeline;

interface PipelineInterface extends StageInterface
{
    /**
     * Create a new pipeline with an appended stage.
     *
     * @return static
     */
    public function pipe(callable $operation): PipelineInterface;
}

interface StageInterface
{
    /**
     * Process the payload.
     *
     * @param mixed $payload
     *
     * @return mixed
     */
    public function __invoke($payload);
}

該介面主要是利用鏈式程式設計的思想,不斷新增管道「pipe」,然後增加一個魔術方法,來讓傳入的引數運轉起來。

先看看這個魔術方法的作用:

mixed __invoke ([ $... ] )
當嘗試以呼叫函式的方式呼叫一個物件時,__invoke() 方法會被自動呼叫。

參考來自:http://php.net/manual/zh/language.oop5.magic.php
如:

<?php
class CallableClass 
{
    function __invoke($x) {
        var_dump($x);
    }
}
$obj = new CallableClass;
$obj(5);
var_dump(is_callable($obj));
?>

返回結果:

int(5)
bool(true)

Pipeline

<?php
declare(strict_types=1);

namespace League\Pipeline;

class Pipeline implements PipelineInterface
{
    /**
     * @var callable[]
     */
    private $stages = [];

    /**
     * @var ProcessorInterface
     */
    private $processor;

    public function __construct(ProcessorInterface $processor = null, callable ...$stages)
    {
        $this->processor = $processor ?? new FingersCrossedProcessor;
        $this->stages = $stages;
    }

    public function pipe(callable $stage): PipelineInterface
    {
        $pipeline = clone $this;
        $pipeline->stages[] = $stage;

        return $pipeline;
    }

    public function process($payload)
    {
        return $this->processor->process($payload, ...$this->stages);
    }

    public function __invoke($payload)
    {
        return $this->process($payload);
    }
}

其中核心類 Pipeline 的作用主要就是兩個:

  1. 新增組裝各個管道「pipe」;
  2. 組裝後,引水流動,執行 process($payload),輸出結果。

Processor

接好各種管道後,那就要「引水入渠」了。該外掛提供了兩個基礎執行類,比較簡單,直接看程式碼就能懂。

// 按照 $stages 陣列順利,遍歷執行管道方法,再將結果傳入下一個管道,讓「水」一層層「流動」起來
class FingersCrossedProcessor implements ProcessorInterface
{
    public function process($payload, callable ...$stages)
    {
        foreach ($stages as $stage) {
            $payload = $stage($payload);
        }

        return $payload;
    }
}

// 增加一個額外的「過濾網」,經過每個管道後的結果,都需要 check,一旦滿足則終止,直接輸出結果。
class InterruptibleProcessor implements ProcessorInterface
{
    /**
     * @var callable
     */
    private $check;

    public function __construct(callable $check)
    {
        $this->check = $check;
    }

    public function process($payload, callable ...$stages)
    {
        $check = $this->check;

        foreach ($stages as $stage) {
            $payload = $stage($payload);

            if (true !== $check($payload)) {
                return $payload;
            }
        }

        return $payload;
    }
}

interface ProcessorInterface
{
    /**
     * Process the payload using multiple stages.
     *
     * @param mixed $payload
     *
     * @return mixed
     */
    public function process($payload, callable ...$stages);
}

我們完全也可以利用該介面,實現我們的方法來組裝管道和「過濾網」。

PipelineBuilder

最後提供了一個 Builder,這個也很好理解:

class PipelineBuilder implements PipelineBuilderInterface
{
    /**
     * @var callable[]
     */
    private $stages = [];

    /**
     * @return self
     */
    public function add(callable $stage): PipelineBuilderInterface
    {
        $this->stages[] = $stage;

        return $this;
    }

    public function build(ProcessorInterface $processor = null): PipelineInterface
    {
        return new Pipeline($processor, ...$this->stages);
    }
}

interface PipelineBuilderInterface
{
    /**
     * Add an stage.
     *
     * @return self
     */
    public function add(callable $stage): PipelineBuilderInterface;

    /**
     * Build a new Pipeline object.
     */
    public function build(ProcessorInterface $processor = null): PipelineInterface;
}

總結

無論是對不同技術的橫向理解,還是基於 Laravel 或者某些開源外掛,我們都能學習到技術之上的通用原理和方法。再將這些原理和方法反作用於我們的實際程式碼開發中。

最近閒來沒事,自己參考 Laravel 去寫個簡易框架,也將League\Pipeline 引入到框架中使用。

「未完待續」

相關文章