Laravel5.3之Middleware源碼解析

說明:本文主要學習Laravel的Middleware的源碼設計思想,並將學習心得分享出來,但願對別人有所幫助。Laravel5.3之Decorator Pattern已經聊過Laravel使用了Decorator Pattern來設計Middleware,看Laravel源碼發現其巧妙用了Closure和PHP的一些數組函數來設計Middleware。php

開發環境:Laravel5.3 + PHP7 + OS X 10.11bootstrap

PHP內置函數array_reverse、array_reduce、call_user_func和call_user_func_array

看Laravel源碼以前,先看下這幾個PHP內置函數的使用。首先array_reverse()函數比較簡單,倒置數組,看測試代碼:數組

$pipes = [
    'Pipe1',
    'Pipe2',
    'Pipe3',
    'Pipe4',
    'Pipe5',
    'Pipe6',
];

$pipes = array_reverse($pipes);

var_dump($pipes);

// output
array(6) {
  [0] =>
  string(5) "Pipe6"
  [1] =>
  string(5) "Pipe5"
  [2] =>
  string(5) "Pipe4"
  [3] =>
  string(5) "Pipe3"
  [4] =>
  string(5) "Pipe2"
  [5] =>
  string(5) "Pipe1"
}

array_reduce內置函數主要是用回調函數去迭代數組中每個值,而且每一次回調獲得的結果值做爲下一次回調的初始值,最後返回最終迭代的值:cookie

/**
 * @link http://php.net/manual/zh/function.array-reduce.php
 * @param int $v
 * @param int $w
 *
 * @return int
 */
function rsum($v, $w)
{
    $v += $w;
    return $v;
}

$a = [1, 2, 3, 4, 5];
// 10爲初始值
$b = array_reduce($a, "rsum", 10);
// 最後輸出 (((((10 + 1) + 2) + 3) + 4) + 5) = 25
echo $b . PHP_EOL;

call_user_func()是執行回調函數,並可輸入參數做爲回調函數的參數,看測試代碼:session

class TestCallUserFunc
{
    public function index($request)
    {
        echo $request . PHP_EOL;
    }
}   

/**
 * @param $test
 */
function testCallUserFunc($test)
{
    echo $test . PHP_EOL;
}

// [$class, $method]
call_user_func(['TestCallUserFunc', 'index'], 'pipes'); // 輸出'pipes'

// Closure
call_user_func(function ($passable) {
    echo $passable . PHP_EOL;
}, 'pipes'); // 輸出'pipes'

// function
call_user_func('testCallUserFunc' , 'pipes'); // 輸出'pipes'

call_user_func_array與call_user_func基本同樣,只不過傳入的參數是數組:閉包

class TestCallUserFuncArray
{
    public function index($request)
    {
        echo $request . PHP_EOL;
    }
}

/**
 * @param $test
 */
function testCallUserFuncArray($test)
{
    echo $test . PHP_EOL;
}

// [$class, $method]
call_user_func_array(['TestCallUserFuncArray', 'index'], ['pipes']); // 輸出'pipes'

// Closure
call_user_func_array(function ($passable) {
    echo $passable . PHP_EOL;
}, ['pipes']); // 輸出'pipes'

// function
call_user_func_array('testCallUserFuncArray' , ['pipes']); // 輸出'pipes'

Middleware源碼解析

瞭解了幾個PHP內置函數後再去看下Middleware源碼就比較簡單了。Laravel5.3之IoC Container實例化源碼解析已經聊過Application的實例化,獲得index.php中的$app變量,即\Illuminate\Foundation\Application的實例化對象。而後繼續看下index.php的源碼:app

/**
 * @var \App\Http\Kernel $kernel
 */
$kernel = $app->make(Illuminate\Contracts\Http\Kernel::class);

$response = $kernel->handle(
    $request = Illuminate\Http\Request::capture()
);

$response->send();

$kernel->terminate($request, $response);

首先從容器中解析出Kernel對象,對於\App\Http\Kernel對象的依賴:\Illuminate\Foundation\Application和\Illuminate\Routing\Router,容器會自動解析。看下Kernel的構造函數:ide

/**
     * Create a new HTTP kernel instance.
     *
     * @param  \Illuminate\Contracts\Foundation\Application  $app
     * @param  \Illuminate\Routing\Router  $router
     */
    public function __construct(Application $app, Router $router)
    {
        $this->app    = $app;
        $this->router = $router;

        foreach ($this->middlewareGroups as $key => $middleware) {
            $router->middlewareGroup($key, $middleware);
        }

        foreach ($this->routeMiddleware as $key => $middleware) {
            $router->middleware($key, $middleware);
        }
    }
    
    // \Illuminate\Routing\Router內的方法
    public function middlewareGroup($name, array $middleware)
    {
        $this->middlewareGroups[$name] = $middleware;

        return $this;
    }
    
    public function middleware($name, $class)
    {
        $this->middleware[$name] = $class;

        return $this;
    }

構造函數初始化了幾個中間件數組,$middleware[ ], $middlewareGroups[ ]和$routeMiddleware[ ],Laravel5.0的時候記得中間件數組尚未分的這麼細。而後就是Request的實例化:函數

$request = Illuminate\Http\Request::capture()

這個過程之後再聊吧,無論咋樣,獲得了Illuminate\Http\Request對象,而後傳入Kernel中:post

/**
     * Handle an incoming HTTP request.
     *
     * @param  \Illuminate\Http\Request  $request
     * @return \Illuminate\Http\Response
     */
    public function handle($request)
    {
        try {
            $request->enableHttpMethodParameterOverride();

            $response = $this->sendRequestThroughRouter($request);
        } catch (Exception $e) {
            $this->reportException($e);

            $response = $this->renderException($request, $e);
        } catch (Throwable $e) {
            $this->reportException($e = new FatalThrowableError($e));

            $response = $this->renderException($request, $e);
        }

        $this->app['events']->fire('kernel.handled', [$request, $response]);

        return $response;
    }

主要是sendRequestThroughRouter($request)函數執行了轉換操做:把\Illuminate\Http\Request對象轉換成了\Illuminate\Http\Response,而後經過Kernel的send()方法發送給客戶端。同時,順便觸發了kernel.handled內核已處理請求事件。OK,重點關注下sendRequestThroughRouter($request)方法:

/**
     * Send the given request through the middleware / router.
     *
     * @param  \Illuminate\Http\Request  $request
     * @return \Illuminate\Http\Response
     */
    protected function sendRequestThroughRouter($request)
    {
        $this->app->instance('request', $request);

        Facade::clearResolvedInstance('request');

        /* 依次執行$bootstrappers中每個bootstrapper的bootstrap()函數,作了幾件準備事情:
        1. 環境檢測
        2. 配置加載
        3. 日誌配置
        4. 異常處理
        5. 註冊Facades
        6. 註冊Providers
        7. 啓動服務
         protected $bootstrappers = [
            'Illuminate\Foundation\Bootstrap\DetectEnvironment',
            'Illuminate\Foundation\Bootstrap\LoadConfiguration',
            'Illuminate\Foundation\Bootstrap\ConfigureLogging',
            'Illuminate\Foundation\Bootstrap\HandleExceptions',
            'Illuminate\Foundation\Bootstrap\RegisterFacades',
            'Illuminate\Foundation\Bootstrap\RegisterProviders',
            'Illuminate\Foundation\Bootstrap\BootProviders',
        ];*/
        $this->bootstrap();

        return (new Pipeline($this->app))
                    ->send($request)
                    ->through($this->app->shouldSkipMiddleware() ? [] : $this->middleware)
                    ->then($this->dispatchToRouter());
    }

$this->bootstrap()主要是作了程序初始化工做,之後再聊具體細節。而後是Pipeline來傳輸Request,Laravel中把Pipeline管道單獨拿出來做爲一個service(可看Illuminate/Pipeline文件夾),說明Pipeline作的事情仍是很重要的:主要就是做爲Request的傳輸管道,依次經過$middlewares[ ], 或middlewareGroups[ ], 或$routeMiddleware[ ]這些中間件的前置操做,和控制器的某個action或者直接閉包處理獲得Response,而後又帶着Reponse依次經過$middlewares[ ], 或middlewareGroups[ ], 或$routeMiddleware[ ]這些中間件的後置操做獲得準備就緒的Response,而後經過send()發送給客戶端。
這個過程有點像汽車工廠的生產同樣,Pipeline是傳送帶,起初Request可能就是個汽車空殼子,通過傳送帶旁邊的一個個機械手middleware@before的過濾和操做(如檢查零件剛度是否是合格,殼子尺寸是否是符合要求,給殼子噴個漆或抹個油啥的),而後進入中央控制區加個發動機(Controller@action,或Closure),而後又繼續通過檢查和附加操做middleware@after(如添加個擋風鏡啥的),而後經過門外等着的火車直接運送到消費者手裏send()。在每一步裝配過程當中,都須要Service來支持,Service是經過Container來解析{make()}提供的,而且Service是經過ServiceProvider註冊綁定{bind(),singleton(),instance()}到Container中的。

看下Pipeline的send()和through()源碼:

public function send($passable)
    {
        $this->passable = $passable;

        return $this;
    }
    
    public function through($pipes)
    {
        $this->pipes = is_array($pipes) ? $pipes : func_get_args();

        return $this;
    }

send()傳送的對象是Request,through()所要經過的對象是$middleware[ ],OK,再看下dispatchToRouter()的源碼直接返回一個Closure:

protected function dispatchToRouter()
    {
        return function ($request) {
            $this->app->instance('request', $request);

            return $this->router->dispatch($request);
        };
    }

而後重點看下then()函數源碼:

public function then(Closure $destination)
    {
        $firstSlice = $this->getInitialSlice($destination);

        $pipes = array_reverse($this->pipes);

        // $this->passable = Request對象
        return call_user_func(
            array_reduce($pipes, $this->getSlice(), $firstSlice), $this->passable
        );
    }
    
    protected function getInitialSlice(Closure $destination)
    {
        return function ($passable) use ($destination) {
            return call_user_func($destination, $passable);
        };
    }

這裏假設$middlewares爲(儘管源碼中$middlewares只有一個CheckForMaintenanceMode::class):

$middlewares = [
    CheckForMaintenanceMode::class,
    AddQueuedCookiesToResponse::class,
    StartSession::class,
    ShareErrorsFromSession::class,
    VerifyCsrfToken::class,
];

先得到第一個slice(這裏做者是比做'洋蔥',一層層的穿過,從一側穿過到另外一側,比喻倒也形象)並做爲array_reduce()的初始值,就像上文中array_reduce()測試例子中的10這個初始值,這個初始值如今是個閉包:

$destination = function ($request) {
    $this->app->instance('request', $request);
    return $this->router->dispatch($request);
};

$firstSlice = function ($passable) use ($destination) {
    return call_user_func($destination, $passable);
};

OK,而後要對$middlewares[ ]進行翻轉,爲啥要翻轉呢?
看過這篇Laravel5.3之Decorator Pattern文章就會發現,在Client類利用Decorator Pattern進行依次裝飾的時候,是按照$middlewares[ ]數組中值倒着new的:

public function wrapDecorator(IMiddleware $decorator)
    {
        $decorator = new VerifyCsrfToken($decorator);
        $decorator = new ShareErrorsFromSession($decorator);
        $decorator = new StartSession($decorator);
        $decorator = new AddQueuedCookiesToResponse($decorator);
        $response  = new CheckForMaintenanceMode($decorator);

        return $response;
    }

這樣才能獲得一個符合$middlewares[ ]順序的$response對象:

$response = new CheckForMaintenanceMode(
                new AddQueuedCookiesToResponse(
                    new StartSession(
                        new ShareErrorsFromSession(
                            new VerifyCsrfToken(
                                new Request()
                        )
                    )
                )
            )
        );

看下array_reduce()中的迭代回調函數getSlice(){這個迭代回調函數比做剝洋蔥時獲取每一層洋蔥slice,初始值是$firstSlice}:

protected function getSlice()
    {
        return function ($stack, $pipe) {
            return function ($passable) use ($stack, $pipe) {
                if ($pipe instanceof Closure) {
                    return call_user_func($pipe, $passable, $stack);
                } elseif (! is_object($pipe)) {
                    list($name, $parameters) = $this->parsePipeString($pipe);
                    $pipe = $this->container->make($name);
                    $parameters = array_merge([$passable, $stack], $parameters);
                } else{
                    $parameters = [$passable, $stack];
                }

                return call_user_func_array([$pipe, $this->method], $parameters);
            };
        };
    }

返回的是個閉包,仔細看下第二層閉包裏的邏輯,這裏$middlewares[ ]傳入的是每個中間件的名字,而後經過容器解析出每個中間件對象:

$pipe = $this->container->make($name);

並最後用call_user_func_array([$class, $method], array $parameters)來調用這個$class裏的$method方法,參數是$parameters。

Demo

接下來寫個demo看下整個流程。先簡化下getSlice()函數,這裏就默認$pipe傳入的是類名稱(整個demo中全部class都在同一個文件內):

// PipelineTest.php

// Get the slice in every step.
function getSlice()
{
    return function ($stack, $pipe) {
        return function ($passable) use ($stack, $pipe) {
            /**
             * @var Middleware $pipe
             */
            return call_user_func_array([$pipe, 'handle'], [$passable, $stack]);
        };
    };
}

再把$middlewares[ ]中五個中間件類寫上,對於前置操做和後置操做作個簡化,直接echo字符串:

// PipelineTest.php

<?php

interface Middleware
{
    public static function handle($request, Closure $closure);
}

class CheckForMaintenanceMode implements Middleware
{
    public static function handle($request, Closure $next)
    {
        echo $request . ': Check if the application is in the maintenance status.' . PHP_EOL;
        $next($request);
    }
}

class AddQueuedCookiesToResponse implements Middleware
{
    public static function handle($request, Closure $next)
    {
        $next($request);
        echo $request . ': Add queued cookies to the response.' . PHP_EOL;
    }
}

class StartSession implements Middleware
{
    public static function handle($request, Closure $next)
    {
        echo $request . ': Start session of this request.' . PHP_EOL;
        $next($request);
        echo $request . ': Close session of this response.' . PHP_EOL;
    }
}

class ShareErrorsFromSession implements Middleware
{
    public static function handle($request, Closure $next)
    {
        $next($request);
        echo $request . ': Share the errors variable from response to the views.' . PHP_EOL;
    }
}

class VerifyCsrfToken implements Middleware
{
    public static function handle($request, Closure $next)
    {
        echo $request . ': Verify csrf token when post request.' . PHP_EOL;
        $next($request);
    }
}

給上完整的一個Pipeline類,這裏的Pipeline對Laravel中的Pipeline作了稍微簡化,只選了幾個重要的函數:

// PipelineTest.php

class Pipeline 
{
    /**
     * @var array
     */
    protected $middlewares = [];

    /**
     * @var int
     */
    protected $request;

    // Get the initial slice
    function getInitialSlice(Closure $destination)
    {
        return function ($passable) use ($destination) {
            return call_user_func($destination, $passable);
        };
    }
    
    // Get the slice in every step.
    function getSlice()
    {
        return function ($stack, $pipe) {
            return function ($passable) use ($stack, $pipe) {
                /**
                 * @var Middleware $pipe
                 */
                return call_user_func_array([$pipe, 'handle'], [$passable, $stack]);
            };
        };
    }
    
    // When process the Closure, send it as parameters. Here, input an int number.
    function send(int $request)
    {
        $this->request = $request;
        return $this;
    }

    // Get the middlewares array.
    function through(array $middlewares)
    {
        $this->middlewares = $middlewares;
        return $this;
    }
    
    // Run the Filters.
    function then(Closure $destination)
    {
        $firstSlice = $this->getInitialSlice($destination);
    
        $pipes = array_reverse($this->middlewares);
        
        $run = array_reduce($pipes, $this->getSlice(), $firstSlice);
    
        return call_user_func($run, $this->request);
    }
}

OK,如今開始傳入Request,這裏簡化爲一個整數而不是Request對象了:

// PipelineTest.php

/**
 * @return \Closure
 */
function dispatchToRouter()
{
    return function ($request) {
        echo $request . ': Send Request to the Kernel, and Return Response.' . PHP_EOL;
    };
}

$request = 10;

$middlewares = [
    CheckForMaintenanceMode::class,
    AddQueuedCookiesToResponse::class,
    StartSession::class,
    ShareErrorsFromSession::class,
    VerifyCsrfToken::class,
];

(new Pipeline())->send($request)->through($middlewares)->then(dispatchToRouter());

執行php PipelineTest.php獲得Response:

10: Check if the application is in the maintenance status.
10: Start session of this request.
10: Verify csrf token when post request.
10: Send Request to the Kernel, and Return Response.
10: Share the errors variable from response to the views.
10: Close session of this response.
10: Add queued cookies to the response.

一步一步分析下執行過程:

1.首先獲取$firstSlice

$destination = function ($request) {
    echo $request . ': Send Request to the Kernel, and Return Response.' . PHP_EOL;
};
$firstSlice = function ($passable) use ($destination) {
    return call_user_func($destination, $passable);
};

這時通過初始化後:

$this->request = 10;
$pipes = [
    VerifyCsrfToken::class,
    ShareErrorsFromSession::class,
    StartSession::class,
    AddQueuedCookiesToResponse::class,
    CheckForMaintenanceMode::class,
];

2.執行第一次getSlice()後的結果做爲新的$stack,其值爲:

$stack   = $firstSlice;
$pipe    = VerifyCsrfToken::class;
$stack_1 = function ($passable) use ($stack, $pipe) {
        /**
        * @var Middleware $pipe
        */            
    return call_user_func_array([$pipe, 'handle'], [$passable, $stack]);
};

3.執行第二次getSlice()後的結果做爲新的$stack,其值爲:

$stack   = $stack_1;
$pipe    = ShareErrorsFromSession::class;
$stack_2 = function ($passable) use ($stack, $pipe) {
        /**
        * @var Middleware $pipe
        */            
    return call_user_func_array([$pipe, 'handle'], [$passable, $stack]);
};

4.執行第三次getSlice()後的結果做爲新的$stack,其值爲:

$stack   = $stack_2;
$pipe    = StartSession::class;
$stack_3 = function ($passable) use ($stack, $pipe) {
        /**
        * @var Middleware $pipe
        */            
    return call_user_func_array([$pipe, 'handle'], [$passable, $stack]);
};

5.執行第四次getSlice()後的結果做爲新的$stack,其值爲:

$stack   = $stack_3;
$pipe    = AddQueuedCookiesToResponse::class;
$stack_4 = function ($passable) use ($stack, $pipe) {
        /**
        * @var Middleware $pipe
        */            
    return call_user_func_array([$pipe, 'handle'], [$passable, $stack]);
};

6.執行第五次getSlice()後的結果做爲新的$stack,其值爲:

$stack   = $stack_4;
$pipe    = CheckForMaintenanceMode::class;
$stack_5 = function ($passable) use ($stack, $pipe) {
        /**
        * @var Middleware $pipe
        */            
    return call_user_func_array([$pipe, 'handle'], [$passable, $stack]);
};

這時,$stack_5也就是then()裏的$run,而後執行call_user_func($run, 10),看執行過程:

1.$stack_5(10) = CheckForMaintenanceMode::handle(10, $stack_4)

echo '10: Check if the application is in the maintenance status.' . PHP_EOL;
stack_4(10);

2.$stack_4(10) = AddQueuedCookiesToResponse::handle(10, $stack_3)

$stack_3(10);
echo '10: Add queued cookies to the response.' . PHP_EOL;

3.$stack_3(10) = StartSession::handle(10, $stack_2)

echo '10: Start session of this request.' . PHP_EOL;
$stack_2(10);
echo '10: Close session of this response.' . PHP_EOL;

4.$stack_2(10) = ShareErrorsFromSession::handle(10, $stack_1)

$stack_1(10);
echo '10: Share the errors variable from response to the views.' . PHP_EOL;

5.$stack_1(10) = VerifyCsrfToken::handle(10, $firstSlice)

echo '10: Verify csrf token when post request.' . PHP_EOL;
$firstSlice(10);

6.$firstSlice(10) =

$firstSlice(10) = call_user_func($destination, 10) = echo '10: Send Request to the Kernel, and Return Response.' . PHP_EOL;

OK,再把上面執行順序整理一下:

1. echo '10: Check if the application is in the maintenance status.' . PHP_EOL; // 第一個step

3_1. echo '10: Start session of this request.' . PHP_EOL; // 第三個step

5. echo '10: Verify csrf token when post request.' . PHP_EOL; // 第五個step

6.echo '10: Send Request to the Kernel, and Return Response.' . PHP_EOL; //第六個step

4. echo '10: Share the errors variable from response to the views.' . PHP_EOL; // 第四個step

3_2. echo '10: Close session of this response.' . PHP_EOL; // 第三個step

2. echo '10: Add queued cookies to the response.' . PHP_EOL; // 第二個step

通過上面的一步步分析,就能很清楚Laravel源碼中Middleware的執行步驟了。再複雜的步驟只要一步步拆解,就很清晰每一步的邏輯,而後把步驟組裝,就能知道全貌了。

總結:本文主要學習了Laravel的Middleware的源碼,學習完後就知道沒有什麼神祕之處,只須要動手一步步拆解就行。後面再學習下Container的源碼,到時見。

相關文章
相關標籤/搜索