搜尋
首頁php框架ThinkPHPThinkPHP6.0管道模式與中介軟體的實現分析

說明

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。如有侵權,請聯絡admin@php.cn刪除

熱AI工具

Undresser.AI Undress

Undresser.AI Undress

人工智慧驅動的應用程序,用於創建逼真的裸體照片

AI Clothes Remover

AI Clothes Remover

用於從照片中去除衣服的線上人工智慧工具。

Undress AI Tool

Undress AI Tool

免費脫衣圖片

Clothoff.io

Clothoff.io

AI脫衣器

Video Face Swap

Video Face Swap

使用我們完全免費的人工智慧換臉工具,輕鬆在任何影片中換臉!

熱工具

SublimeText3 Linux新版

SublimeText3 Linux新版

SublimeText3 Linux最新版

SecLists

SecLists

SecLists是最終安全測試人員的伙伴。它是一個包含各種類型清單的集合,這些清單在安全評估過程中經常使用,而且都在一個地方。 SecLists透過方便地提供安全測試人員可能需要的所有列表,幫助提高安全測試的效率和生產力。清單類型包括使用者名稱、密碼、URL、模糊測試有效載荷、敏感資料模式、Web shell等等。測試人員只需將此儲存庫拉到新的測試機上,他就可以存取所需的每種類型的清單。

SublimeText3漢化版

SublimeText3漢化版

中文版,非常好用

VSCode Windows 64位元 下載

VSCode Windows 64位元 下載

微軟推出的免費、功能強大的一款IDE編輯器

PhpStorm Mac 版本

PhpStorm Mac 版本

最新(2018.2.1 )專業的PHP整合開發工具