1. 程式人生 > >php利用yield寫一個簡單中介軟體

php利用yield寫一個簡單中介軟體

yield 協程

1.初識Generator

Generator , 一種可以返回迭代器的生成器,當程式執行到yield的時候,當前程式就喚起協程記錄上下文,然後主函式繼續操作,當需要操作的時候,在通過迭代器的next重新調起

function xrange($start, $end, $step = 1) {  
    for ($i = $start; $i <= $end; $i += $step) {  
        yield $i;  
    }  
}  

foreach (xrange(1, 1000) as $num) {  
    echo $num
, "\n"; } /* * 1 * 2 * ... * 1000 */

如果瞭解過迭代器的朋友,就可以通過上面這一段程式碼看出Generators的執行流程

 Generators::rewind() 重置迭代器

 Generators::valid() 檢查迭代器是否被關閉
 Generators::current() 返回當前產生的值
 Generators::next() 生成器繼續執行

 Generators::valid() 
 Generators::current() 
 Generators::next() 
 ...
 Generators::valid() 直到返回 false 迭代結束

2.Generator應用

很多不瞭解的朋友看完可能會表示這有什麼用呢?

舉個栗子:
比如從資料庫取出數億條資料,這個時候要求用一次請求加響應返回所有值該怎麼辦呢?獲取所有值,然後輸出,這樣肯定不行,因為會造成PHP記憶體溢位的,因為資料量太大了。如果這時候用yield就可以將資料分段獲取,理論上這樣是可以取出無限的資料的。

一般的獲取方式 :

資料庫連線.....
$sql = "select * from `user` limit 0,500000000";
$stat = $pdo->query($sql);
$data = $stat->fetchAll();  //mysql buffered query遍歷巨大的查詢結果導致的記憶體溢位
var_dump($data);

yield獲取方式:

資料庫連線.....
function get(){
    $sql = "select * from `user` limit 0,500000000";
    $stat = $pdo->query($sql);
    while ($row = $stat->fetch()) {
        yield $row;
    }
}

foreach (get() as $row) {
    var_dump($row);
}

3.深入瞭解Generator

看完這些之後可能有朋友又要問了,這跟標題的中介軟體有什麼關係嗎

是的上面說的這些確實跟中介軟體沒關係,只是單純的介紹yield,但是你以為yield只能這樣玩嗎?
在我查閱了http://php.net/manual/zh/class.generator.php 內的Generators資料之後我發現了一個函式
Generator::send

官方的介紹 :

向生成器中傳入一個值,並且當做 yield 表示式的結果,然後繼續執行生成器。
如果當這個方法被呼叫時,生成器不在 yield 表示式,那麼在傳入值之前,它會先執行到第一個 yield 表示式。As such it is not necessary to “prime” PHP generators with a Generator::next() call (like it is done in Python).

這代表了什麼,這代表了我們可以使用yield進行雙向通訊

再舉個栗子

$ben = call_user_func(function (){
    $hello = (yield 'my name is ben ,what\'s your name'.PHP_EOL);
    echo $hello;
});

$sayHello = $ben->current();
echo $sayHello;
$ben->send('hi ben ,my name is alex');


/* 
 * output
 * 
 * my name is ben ,what's your name
 * hi ben ,my name is alex 
 */  

這樣ben跟alex他們兩個就實現了一次相互問好,在這個例子中我們可以發現,yield跟以往的return不同,它不僅可以返回資料,還可以獲取外部返回的資料

而且不僅僅能夠send,PHP還提供了一個throw,允許我們返回一個異常給Generator

$Generatorg = call_user_func(function(){
    $hello = (yield '[yield] say hello'.PHP_EOL);
    echo $hello.PHP_EOL;
    try{
        $jump = (yield '[yield] I jump,you jump'.PHP_EOL);
    }catch(Exception $e){
        echo '[Exception]'.$e->getMessage().PHP_EOL;
    }
});

$hello = $Generatorg->current();
echo $hello;
$jump = $Generatorg->send('[main] say hello');
echo $jump;
$Generatorg->throw(new Exception('[main] No,I can\'t jump'));

/*
 * output
 *
 * [yield] say hello
 * [main] say hello
 * [yield] I jump,you jump
 * [Exception][main] No,I can't jump
 */

4.中介軟體

在瞭解了yield那麼多語法之後,就要開始說說我們的主題了,中介軟體,具體思路是以迭代器的方式呼叫函式,先current執行第一個yield之前的程式碼,再用send或者next執行下一段程式碼,下面就是簡單的實現

function middleware($handlers,$arguments = []){
    //函式棧
    $stack = [];
    $result = null;

    foreach ($handlers as $handler) {
        // 每次迴圈之前重置,只能儲存最後一個處理程式的返回值
        $result = null;
        $generator = call_user_func_array($handler, $arguments);

        if ($generator instanceof \Generator) {
            //將協程函式入棧,為重入做準備
            $stack[] = $generator;

            //獲取協程返回引數
            $yieldValue = $generator->current();

            //檢查是否重入函式棧
            if ($yieldValue === false) {
                break;
            }
        } elseif ($generator !== null) {
            //重入協程引數
            $result = $generator;
        }
    }

    $return = ($result !== null);
    //將協程函數出棧
    while ($generator = array_pop($stack)) {
        if ($return) {
            $generator->send($result);
        } else {
            $generator->next();
        }
    }
}



$abc = function(){
    echo "this is abc start \n";
    yield;
    echo "this is abc end \n";
};

$qwe = function (){
    echo "this is qwe start \n";
    $a = yield;
    echo $a."\n";
    echo "this is qwe end \n";
};
$one = function (){
    return 1;
};

middleware([$abc,$qwe,$one]);

/*
 * output
 * 
 * this is abc start 
 * this is qwe start
 * 1
 * this is qwe end 
 * this is abc end 
 */

通過middleware()方法我們就實現了一個這樣的效果

(begin) ----------------> function() -----------------> (end)
            ^   ^   ^                   ^   ^   ^
            |   |   |                   |   |   |
            |   |   +------- M1() ------+   |   |
            |   +----------- ...  ----------+   |
            +--------------- Mn() --------------+

雖然這個函式還有許多不足的地方,但是已經實現了簡單的實現了管道模式

5.將函式封裝並且用“laravel”式的語法來實現

檔案 Middleware.php

namespace Middleware;

use Generator;

class Middleware
{
    /**
     * 預設載入的中介軟體
     *
     * @var array
     */
    protected $handlers = [];

    /**
     * 執行時傳遞給每個中介軟體的引數
     *
     * @var array|callable
     */
    protected $arguments;

    /**
     * 設定在中介軟體中傳輸的引數
     *
     * @param $arguments
     * @return self $this
     */
    public function send(...$arguments)
    {
        $this->arguments = $arguments;

        return $this;
    }

    /**
     * 設定經過的中介軟體
     *
     * @param $handle
     * @return $this
     */
    public function through($handle)
    {
        $this->handlers = is_array($handle) ? $handle : func_get_args();

        return $this;
    }

    /**
     * 執行中介軟體到達
     *
     * @param \Closure $destination
     * @return null|mixed
     */
    public function then(\Closure $destination)
    {
        $stack = [];
        $arguments = $this->arguments;
        foreach ($this->handlers as $handler) {
            $generator = call_user_func_array($handler, $arguments);

            if ($generator instanceof Generator) {
                $stack[] = $generator;

                $yieldValue = $generator->current();
                if ($yieldValue === false) {
                    break;
                }elseif($yieldValue instanceof Arguments){
                    //替換傳遞引數
                    $arguments = $yieldValue->toArray();
                }
            }
        }

        $result = $destination(...$arguments);
        $isSend = ($result !== null);
        $getReturnValue = version_compare(PHP_VERSION, '7.0.0', '>=');
        //重入函式棧
        while ($generator = array_pop($stack)) {
            /* @var $generator Generator */
            if ($isSend) {
                $generator->send($result);
            }else{
                $generator->next();
            }

            if ($getReturnValue) {
                $result = $generator->getReturn();
                $isSend = ($result !== null);
            }else{
                $isSend = false;
            }
        }

        return $result;
    }
}

檔案 Arguments.php

namespace Middleware;

/**
 * ArrayAccess 是PHP提供的一個預定義介面,用來提供陣列式的訪問
 * 可以參考http://php.net/manual/zh/class.arrayaccess.php
 */
use ArrayAccess;

/**
 * 這個類是用來提供中介軟體引數的
 * 比如中介軟體B需要一個由中介軟體A專門提供的引數,
 * 那麼中介軟體A可以通過 “yield new Arguments('foo','bar','baz')”將引數傳給中介軟體B
 */
class Arguments implements ArrayAccess
{
    private $arguments;

    /**
     * 註冊傳遞的引數
     *
     * Arguments constructor.
     * @param array $param
     */
    public function __construct($param)
    {
        $this->arguments = is_array($param) ? $param : func_get_args();
    }

    /**
     * 獲取引數
     *
     * @return array
     */
    public function toArray()
    {
        return $this->arguments;
    }

    /**
     * @param mixed $offset
     * @return mixed
     */
    public function offsetExists($offset)
    {
        return array_key_exists($offset,$this->arguments);
    }

    /**
     * @param mixed $offset
     * @return mixed
     */
    public function offsetGet($offset)
    {
        return $this->offsetExists($offset) ? $this->arguments[$offset] : null;
    }

    /**
     * @param mixed $offset
     * @param mixed $value
     */
    public function offsetSet($offset, $value)
    {
        $this->arguments[$offset] = $value;
    }

    /**
     * @param mixed $offset
     */
    public function offsetUnset($offset)
    {
        unset($this->arguments[$offset]);
    }
}

使用 Middleware

$handle = [
    function($object){
        $object->hello = 'hello ';
    },
    function($object){
        $object->hello .= 'world';
    },
];

(new Middleware)
    ->send(new stdClass)
    ->through($handle)
    ->then(function($object){
        echo $object->hello;
    });

/*
 * output
 * 
 * hello world
 */

本人曾參考laravel的管道類實現方式,所以使用語法極其相似,不過實現過程不一致,等到有空的時候專門寫一篇詳解管道模式的部落格

參考資料: