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的管道類實現方式,所以使用語法極其相似,不過實現過程不一致,等到有空的時候專門寫一篇詳解管道模式的部落格