首頁 > php框架 > ThinkPHP > 主體

ThinkPHP6.0管道模式與中介軟體的實現分析

藏色散人
發布: 2019-11-08 17:58:00
轉載
4073 人瀏覽過

說明

ThinkPHP 6.0 RC5 開始使用了管道模式來實現中間件,比起先前版本的實作更加簡潔、有序。這篇文章對其實作細節進行分析。

首先我們從入口檔案public/index.php 開始,$http = (new App())->http;

取得一個http 類別的實例後呼叫它的run 方法:$response = $http->run();,然後它的run 方法又呼叫了runWithRequest 方法:

protected function runWithRequest(Request $request)
{
    .
    .
    .
    return $this->app->middleware->pipeline()
        ->send($request)
        ->then(function ($request) {
            return $this->dispatchToRoute($request);
        });
}
登入後複製

中間件的執行都在最後的return 語句中。

pipeline、through、send 方法

$this->app->middleware->pipeline() 的 pipeline 方法:
public function pipeline(string $type = 'global')
{
    return (new Pipeline())  
           // array_map将所有中间件转换成闭包,闭包的特点:
          // 1. 传入参数:$request,请求实例; $next,一个闭包
          // 2. 返回一个Response实例
        ->through(array_map(function ($middleware) {
            return function ($request, $next) use ($middleware) {
                list($call, $param) = $middleware;
                if (is_array($call) && is_string($call[0])) {
                    $call = [$this->app->make($call[0]), $call[1]];
                }
                 // 该语句执行中间件类实例的handle方法,传入的参数是外部传进来的$request和$next
                 // 还有一个$param是中间件接收的参数
                $response = call_user_func($call, $request, $next, $param);
                if (!$response instanceof Response) {
                    throw new LogicException('The middleware must return Response instance');
                }
                return $response;
            };
            // 将中间件排序
        }, $this->sortMiddleware($this->queue[$type] ?? [])))
        ->whenException([$this, 'handleException']);
}
登入後複製

through 方法程式碼:

public function through($pipes)
{
    $this->pipes = is_array($pipes) ? $pipes : func_get_args();
    return $this;
}
登入後複製

前面呼叫through 是傳入的array_map (...) 把中間件封裝為一個個閉包,through 則是把這些閉包保存在Pipeline 類別的$pipes 屬性中。

PHP 的 array_map 方法簽章:

array_map ( callable $callback , array $array1 [, array $... ] ) : array
登入後複製

$callback 迭代作用於每一個 $array 的元素,傳回新的值。所以,最後得到$pipes 中每個閉包的形式特徵是這樣的(偽代碼):

function ($request, $next) {
    $response = handle($request, $next, $param);
    return $response;
}
登入後複製
登入後複製

該閉包接收兩個參數,一個是請求實例,一個是回調用函數,handle 方法處理後得到相應並返回。

through 傳回一個Pipeline 類別的實例,接著呼叫send 方法:

public function send($passable)
{
    $this->passable = $passable;
    return $this;
}
登入後複製

該方法很簡單,只是將傳入的請求實例保存在$passable 成員變量,最後同樣返回Pipeline 類的實例,這樣就可以鍊式呼叫Pipeline 類別的其他方法。

then,carry 方法

send 方法之後,接著呼叫then 方法:

return $this->app->middleware->pipeline()
            ->send($request)
            ->then(function ($request) {
                return $this->dispatchToRoute($request);
            });
登入後複製

這裡的then 接收一個閉包作為參數,這個閉包實際上包含了控制器操作的執行程式碼。

then 方法碼:

public function then(Closure $destination)
{
    $pipeline = array_reduce(
        //用于迭代的数组(中间件闭包),这里将其倒序
        array_reverse($this->pipes),
        // array_reduce需要的回调函数
        $this->carry(),
        //这里是迭代的初始值
        function ($passable) use ($destination) {
            try {
                return $destination($passable);
            } catch (Throwable | Exception $e) {
                return $this->handleException($passable, $e);
            }
        });
    return $pipeline($this->passable);
}
登入後複製

carry 程式碼:

protected function carry()
{
    // 1. $stack 上次迭代得到的值,如果是第一次迭代,其值是后面的「初始值
    // 2. $pipe 本次迭代的值
    return function ($stack, $pipe) {
        return function ($passable) use ($stack, $pipe) {
            try {
                return $pipe($passable, $stack);
            } catch (Throwable | Exception $e) {
                return $this->handleException($passable, $e);
            }
        };
    };
}
登入後複製

為了更方便分析原理,我們把carry 方法內聯到then 中去,並去掉錯誤捕獲的程式碼,得到:

public function then(Closure $destination)
{
    $pipeline = array_reduce(
        array_reverse($this->pipes),
        function ($stack, $pipe) {
            return function ($passable) use ($stack, $pipe) {
                return $pipe($passable, $stack);
            };
        },
        function ($passable) use ($destination) {
            return $destination($passable);
        });
    return $pipeline($this->passable);
}
登入後複製

這裡關鍵是理解array_reduce 以及$pipeline($this->passable) 的執行過程,這兩個過程可以類比於“包洋蔥”和“剝洋蔥”的過程。

array_reduce 第一次迭代,$stack 初始值為:

(A)

function ($passable) use ($destination) {
    return $destination($passable);
});
登入後複製

回呼函數的傳回值為:

(B)

function ($passable) use ($stack, $pipe) {
    return $pipe($passable, $stack);
};
登入後複製

將A 代入B 可以得到第一次迭代之後的$stack 的值:

(C)

function ($passable) use ($stack, $pipe) {
    return $pipe($passable, 
        function ($passable) use ($destination) {
            return $destination($passable);
        })
    );
};
登入後複製

第二次迭代,同理,將C 代入B 可得:

(D)

// 伪代码
// 每一层的$pipe都代表一个中间件闭包
function ($passable) use ($stack, $pipe) {
    return $pipe($passable,  //倒数第二层中间件
        function ($passable) use ($stack, $pipe) {
            return $pipe($passable,  //倒数第一层中间件
                function ($passable) use ($destination) {
                    return $destination($passable);  //包含控制器操作的闭包
                })
            );
        };
    );
};
登入後複製

以此類推,有多少個中間件,就代入幾次,最後一次得到$stack 就回傳$pipeline。由於前面對中間件閉包進行了倒序,排在前面的閉包被包裹在更裡層,所以倒序後的閉包越是後面的在外面,從正序來看,則變成越前面的中間件在最外層。

層層包好閉包後,我們得到了一個類似洋蔥結構的「超級」閉包 D,該閉包的結構如上面的程式碼註解所示。最後把 $request 物件傳給這個閉包,執行它:$pipeline($this->passable);,由此開啟一個類似剝洋蔥的過程,接下來我們來看看這洋蔥是怎麼剝開的。

剝洋蔥過程分析

array_map(...) 把每個中間件類別加工成一個類似這種結構的閉包:

function ($request, $next) {
    $response = handle($request, $next, $param);
    return $response;
}
登入後複製
登入後複製

其中handle 是中間件中的入口,其結構特點是這樣的:

public function handle($request, $next, $param) {
    // do sth ------ M1-1 / M2-1
    $response = $next($request);
    // do sth ------ M1-2 / M2-2
    return $response;
}
登入後複製

我們上面的「洋蔥」一共只有兩層,也就是有兩層中間件的閉包,假設M1- 1,M1-2 分別是第一個中間件handle 方法的前置和後值操作點位,第二個中間件同理,是M2-1,M2-2。現在,讓程式執行$pipeline($this->passable),展開來看,也就是執行:

// 伪代码
function ($passable) use ($stack, $pipe) {
    return $pipe($passable,  
        function ($passable) use ($stack, $pipe) {
            return $pipe($passable,  
                function ($passable) use ($destination) {
                    return $destination($passable);  
                })
            );
        };
    );
}($this->passable)
登入後複製

此時,程式要求從:

return $pipe($passable,  
    function ($passable) use ($stack, $pipe) {
        return $pipe($passable,  
            function ($passable) use ($destination) {
                return $destination($passable);  
            })
        );
    };
);
登入後複製

傳回值,也就是若要執行第一個中間件閉包,$passable 對應handle 方法的$request 參數,而下一層閉包

function ($passable) use ($stack, $pipe) {
    return $pipe($passable,  
        function ($passable) use ($destination) {
            return $destination($passable);  
        })
    );
}
登入後複製

則對應handle 方法的$next 參數。

要執行第一個閉包,即要執行第一個閉包的handle 方法,其流程是:先執行M1-1 點位的程式碼,即前置操作,然後執行$response = $next($request);,這時程式進入執行下一個閉包,$next($request) 展開來,也就是:

function ($passable) use ($stack, $pipe) {
    return $pipe($passable,  
        function ($passable) use ($destination) {
            return $destination($passable);  
        })
    );
}($request)
登入後複製

依次類別推,執行該閉包,即執行第二個中間件的handle 方法,此時,先執行M2-1 點位,然後執行$response = $next($request),此時的$next 閉包是:

function ($passable) use ($destination) {
    return $destination($passable);  
})
登入後複製

屬於洋蔥之芯— — 最裡面的一層,也就是包含控制器操作的閉包,展開來看:

function ($passable) use ($destination) {
    return $destination($passable);  
})($request)
登入後複製

最终,我们从 return $destination($passable) 中返回一个 Response 类的实例,也就是,第二层的 $response = $next($request) 语句成功得到了结果,接着执行下面的语句,也就是 M2-2 点位,最后第二层闭包返回结果,也就是第一层闭包的 $response = $next($request) 语句成功得到了结果,然后执行这一层闭包该语句后面的语句,即 M1-2 点位,该点位之后,第一层闭包也成功返回结果,于是,then 方法最终得到了返回结果。

整个过程过来,程序经过的点位顺序是这样的:M1-1→M2-1→控制器操作→M2-2→M1-2→返回结果。

总结

整个过程看起来虽然复杂,但不管中间件有多少层,只要理解了前后两层中间件的这种递推关系,洋葱是怎么一层层剥开又一层层返回的,来多少层都不在话下。

以上是ThinkPHP6.0管道模式與中介軟體的實現分析的詳細內容。更多資訊請關注PHP中文網其他相關文章!

相關標籤:
來源:learnku.com
本網站聲明
本文內容由網友自願投稿,版權歸原作者所有。本站不承擔相應的法律責任。如發現涉嫌抄襲或侵權的內容,請聯絡admin@php.cn
熱門教學
更多>
最新下載
更多>
網站特效
網站源碼
網站素材
前端模板
關於我們 免責聲明 Sitemap
PHP中文網:公益線上PHP培訓,幫助PHP學習者快速成長!