Laravel 中介軟體使用及原始碼分析

夏學智發表於2017-04-26

什麼是中介軟體

對於一個Web應用來說,在一個請求真正處理前,我們可能會對請求做各種各樣的判斷,然後才可以讓它繼續傳遞到更深層次中。
而如果我們用if else這樣子來,一旦需要判斷的條件越來越來多,會使得程式碼更加難以維護,系統間的耦合會增加,而中介軟體就可以解決這個問題。
我們可以把這些判斷獨立出來做成中介軟體,可以很方便的過濾請求。

Laravel中的中介軟體的特點及使用

  • 所有的中介軟體都放在 app/Http/Middleware 目錄內。
  • 要建立一個新的中介軟體,則可以使用 make:middleware 這個 Artisan 命令。此命令將會在 app/Http/Middleware 目錄內設定一個名稱為 OldMiddleware 的類。
  • 分為前置中介軟體/後置中介軟體
  • 全域性中介軟體每個 HTTP 請求都經過,設定app/Http/Kernel.php 的 $middleware 屬性
  • 路由中介軟體指派中介軟體給特定路由。設定app/Http/Kernel.php 的$routeMiddleware屬性。
  • 中介軟體可以接收自定義的引數
  • Terminable中介軟體,可以在 HTTP 響應被髮送到瀏覽器之後才執行。

前置中介軟體 / 後置中介軟體

前置中介軟體:前置中介軟體執行的時間點是在每一個請求處理之前

<?php
namespace App\Http\Middleware;
use Closure;

class BeforeMiddleware
{
    public function handle($request, Closure $next)
    {
        // 執行動作

        return $next($request);
    }
}

後置中介軟體:後置中介軟體執行的時間點是在請求處理之後

<?php
namespace App\Http\Middleware;
use Closure;

class AfterMiddleware
{
    public function handle($request, Closure $next)
    {
        $response = $next($request);

        // 執行動作

        return $response;
    }
}

使用中介軟體

全域性中介軟體

若是希望每個 HTTP 請求都經過一箇中介軟體,只要將中介軟體的類加入到 app/Http/Kernel.php 的 $middleware 屬性清單列表中。

路由指派中介軟體

如果你要指派中介軟體給特定路由,你得使用路由指派中介軟體,兩個步驟完成。

  1. 先在 app/Http/Kernel.php 給中介軟體設定一個好記的鍵。預設情況下,這個檔案內的 $routeMiddleware 屬性已包含了Laravel目前設定的中介軟體,你只需要在清單列表中加上一組自定義的鍵即可。
    protected $routeMiddleware = [
    'auth' => \App\Http\Middleware\Authenticate::class,
    'auth.basic' => \Illuminate\Auth\Middleware\AuthenticateWithBasicAuth::class,
    'guest' => \App\Http\Middleware\RedirectIfAuthenticated::class,
    ];
  2. 中介軟體一旦在 HTTP kernel 檔案內被定義,即可在路由選項內使用 middleware 鍵值指定。
// 即可在路由選項內使用 middleware 鍵值指定
Route::get('admin/profile', ['middleware' => 'auth', function () {
    //
}]);

// 使用一組陣列為路由指定多箇中介軟體
Route::get('/', ['middleware' => ['first', 'second'], function () {
    //
}]);

// 除了使用陣列之外,你也可以在路由的定義之後鏈式呼叫 middleware 方法
Route::get('/', function () {
    //
}])->middleware(['first', 'second']);

實現

觸發中介軟體的程式碼

在Laravel中,中介軟體的實現其實是依賴於Illuminate\Pipeline\Pipeline這個類實現的,我們先來看看觸發中介軟體的程式碼。

// kernell類handle方法呼叫。
$response = $this->sendRequestThroughRouter($request);

/**
 * Send the given request through the middleware / router.
 *
 * @param  \Illuminate\Http\Request  $request
 * @return \Illuminate\Http\Response
 */
protected function sendRequestThroughRouter($request)
{
    // 在app容器中繫結一個request例項。
    $this->app->instance('request', $request);
    // 刪除門面(靜態呼叫)產生的例項。
    Facade::clearResolvedInstance('request');
    // 使用容器的bootstrapWith方法呼叫$bootstrappers陣列中類。
    $this->bootstrap();

    // 中介軟體的核心程式碼
    return (new Pipeline($this->app))
                ->send($request)
                ->through($this->app->shouldSkipMiddleware() ? [] : $this->middleware)
                ->then($this->dispatchToRouter());
}

/**
 * Bootstrap the application for HTTP requests.
 *
 * @return void
 */
public function bootstrap()
{
    if (! $this->app->hasBeenBootstrapped()) {
        $this->app->bootstrapWith($this->bootstrappers());
    }
}

/**
 * The bootstrap classes for the application.
 *
 * @var array
 */
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',
];

核心程式碼

函式array_reduce

瞭解核心程式碼以前,需要先了解一下array_reduce函式的用法。

重點,回撥函式sum,接收兩個引數。

carry
攜帶上次迭代裡的值(如果本次迭代是第一次,那麼這個值是6 initial)。

item
攜帶了本次迭代的值。

<?php
function sum($carry, $item)
{
    return $carry + $item;
}

$a = array(1, 2, 3, 4, 5);
// 21  6+1+2+3+4+5
echo array_reduce($a, "sum",6);

模擬laravel中介軟體的程式碼

  • array_reduce最終返回一個可執行的閉包。
  • call_user_func 執行了這個閉包,而這個閉包中兩個變數$carry、$item。
  • $carry是上一個迴圈元素返回的可執行閉包,$item是當前陣列元素。
  • 上一個迴圈元素返回的可執行閉包,又使用了$carry、$item。依次迴圈。
  • 類中的$next變數代指$carry,所以在$next()之前的執行邏輯先於$carry執行,之後的邏輯就晚於$carry執行。
    
    <?php
    interface Milldeware
    {
    public static function handle(Closure $next);
    }
    class test1 implements Milldeware
    {
    public static function handle(Closure $next)
    {
        echo 'test11' . PHP_EOL;
        $next();
    }
    }

class test2 implements Milldeware
{
public static function handle(Closure $next)
{
echo 'test21' . PHP_EOL;
$next();
}
}
class test3 implements Milldeware
{

public static function handle(Closure $next)
{
    echo 'test31' . PHP_EOL;
    $next();
    echo 'test32' . PHP_EOL;
}

}
class test4 implements Milldeware
{
public static function handle(Closure $next)
{
$next();
echo 'test42' . PHP_EOL;
}
}

class test5 implements Milldeware
{
public static function handle(Closure $next)
{
echo 'test51' . PHP_EOL;
$next();
echo 'test52' . PHP_EOL;
}
}
class test6 implements Milldeware
{
public static function handle(Closure $next)
{
echo 'test61' . PHP_EOL;
$next();
}
}
function then()
{
$pipe = [
'test1',
'test2',
'test3',
'test4',
'test5',
'test6'
];

$firstSlice = function () {
    echo 'firstSlice' . PHP_EOL;
};

$pipe = array_reverse($pipe);
$callback = array_reduce($pipe, function ($carry, $item) {
    return function () use ($carry, $item) {
        return $item::handle($carry);
    };
}, $firstSlice);
//var_dump($callback);die;
call_user_func($callback);

}
then();

/*
test11
test21
test31
test51
test61
firstSlice
test52
test42
test32
/


### 真實laravel的程式碼

class Pipeline implements PipelineContract
{
/**

  • The container implementation.
  • @var \Illuminate\Contracts\Container\Container
    */
    protected $container;

    /**

  • The object being passed through the pipeline.
  • @var mixed
    */
    protected $passable;

    /**

  • The array of class pipes.
  • @var array
    */
    protected $pipes = [];

    /**

  • The method to call on each pipe.
  • @var string
    */
    protected $method = 'handle';

    /**

  • Create a new class instance.
  • @param \Illuminate\Contracts\Container\Container $container
  • @return void
    */
    public function __construct(Container $container)
    {
    $this->container = $container;
    }

    /**

  • Set the object being sent through the pipeline.
  • 設定一個通過管道傳輸的物件。
  • @param mixed $passable
  • @return $this
    */
    public function send($passable)
    {
    $this->passable = $passable;

    return $this;

    }

    /**

  • Set the array of pipes.
  • 設定一個管道流經的陣列。
  • @param array|mixed $pipes //通過func_get_args函式,我們可以傳陣列,也可以傳多個引數(打散的陣列)。
  • @return $this
    */
    public function through($pipes)
    {
    $this->pipes = is_array($pipes) ? $pipes : func_get_args();

    return $this;

    }

    /**

  • Set the method to call on the pipes.
  • 設定每個流經的陣列都呼叫的方法名稱,預設是handle方法。
  • @param string $method
  • @return $this
    */
    public function via($method)
    {
    $this->method = $method;

    return $this;

    }

    /**

  • Run the pipeline with a final destination callback.
  • 執行管道,得到一個最終(流經陣列遞迴合併)的回撥函式。
  • @param \Closure $destination
  • @return mixed
    */
    public function then(Closure $destination)
    {
    // 將傳入的閉包包裝成一個Slice閉包,getInitialSlice和getSlice返回的格式是相同的。
    // $destination在實際laravel中是$this->dispatchToRouter()
    $firstSlice = $this->getInitialSlice($destination);

    $pipes = array_reverse($this->pipes);
    
    return call_user_func(
        array_reduce($pipes, $this->getSlice(), $firstSlice), $this->passable
    );

    }

    /**

  • Get a Closure that represents a slice of the application onion.
  • @return \Closure
    */
    protected function getSlice()
    {
    return function ($stack, $pipe) {
    return function ($passable) use ($stack, $pipe) {
    // If the pipe is an instance of a Closure, we will just call it directly but
    // otherwise we will resolve the pipes out of the container and call it with
    // the appropriate method and arguments, returning the results back out.
    // 如果是Closure直接呼叫它,否則處理一下變成可呼叫的Closure。
    if ($pipe instanceof Closure) {
    return call_user_func($pipe, $passable, $stack);
    } else {
    // parsePipeString的方式決定了我們在使用中介軟體時的方式,比如引數的傳入方式。
    // 文件中“在路由中可使用冒號 : 來區隔中介軟體名稱與指派引數,多個引數可使用逗號作為分隔”
    list($name, $parameters) = $this->parsePipeString($pipe);
    // $pipe通過parsePipeString分解,然後使用$this->container->make($name)獲取例項,我們看出只要是可以被make獲取的例項都可以當做管道的處理棧。
    return call_user_func_array([$this->container->make($name), $this->method],
    array_merge([$passable, $stack], $parameters));
    }
    };
    };
    }

    /**

  • Get the initial slice to begin the stack call.
  • @param \Closure $destination
  • @return \Closure
    */
    protected function getInitialSlice(Closure $destination)
    {
    return function ($passable) use ($destination) {
    return call_user_func($destination, $passable);
    };
    }

    /**

  • Parse full pipe string to get name and parameters.
  • @param string $pipe
  • @return array
    */
    protected function parsePipeString($pipe)
    {
    // 通過這行程式碼我們看出,$pipe是字串,可以是用‘:’來分隔中介軟體名稱與指派引數。
    list($name, $parameters) = array_pad(explode(':', $pipe, 2), 2, []);

    // 通過這行程式碼我們看出:多個引數可使用逗號作為分隔。
    if (is_string($parameters)) {
        $parameters = explode(',', $parameters);
    }
    
    return [$name, $parameters];

    }
    }

    
    #### 生成最終匿名函式的過程
//array_reduce執行
//第一次時得到如下簡化的匿名函式返回,將會繼續作為第一個引數進行迭代:        
    object(Closure)#id (1) {
      ["static"]=>
      array(2) {
        ["stack"]=>
        object(Closure)#1 (0) { // $this->prepareDestination($destination)
        }
        ["pipe"]=>
        string(15) "Illuminate\Foundation\Http\Middleware\ConvertEmptyStringsToNull"
      }
    }
//第二次:
    object(Closure)#id (1) {
      ["static"]=>
      array(2) {
        ["stack"]=>
        object(Closure)#id (1) {
          ["static"]=>
          array(2) {
            ["stack"]=>
            object(Closure)#1 (0) { // $this->prepareDestination($destination)
            }
            ["pipe"]=>
            string(15) "Illuminate\Foundation\Http\Middleware\ConvertEmptyStringsToNull"
          }
        }
        ["pipe"]=>
        string(15) "App\Http\Middleware\TrimStrings"
      }
    }
//第三次:
    object(Closure)#id (1) {
      ["static"]=>
      array(2) {
        ["stack"]=>
        object(Closure)#id (1) {
          ["static"]=>
          array(2) {
            ["stack"]=>
            object(Closure)#id (1) {
              ["static"]=>
              array(2) {
                ["stack"]=>
                object(Closure)#1 (0) { // $this->prepareDestination($destination)
                }
                ["pipe"]=>
                string(15) "Illuminate\Foundation\Http\Middleware\ConvertEmptyStringsToNull"
              }
            }
            ["pipe"]=>
            string(15) "App\Http\Middleware\TrimStrings"
          }
        }
        ["pipe"]=>
        string(15) "Illuminate\Foundation\Http\Middleware\ValidatePostSize"
      }
    }

terminate方法的呼叫時機

在Http的Kernel類中有個terminate方法,此方法程式碼如下:

    /**
     * Call the terminate method on any terminable middleware.
     *
     * @param  \Illuminate\Http\Request  $request
     * @param  \Illuminate\Http\Response  $response
     * @return void
     */
    public function terminate($request, $response)
    {
        $middlewares = $this->app->shouldSkipMiddleware() ? [] : array_merge(
            $this->gatherRouteMiddlewares($request),
            $this->middleware
        );

        foreach ($middlewares as $middleware) {
            list($name, $parameters) = $this->parseMiddleware($middleware);

            $instance = $this->app->make($name);

            if (method_exists($instance, 'terminate')) {
                $instance->terminate($request, $response);
            }
        }

        $this->app->terminate();
    }

通過$instance->terminate($request, $response);程式碼可以看出,將$response傳入了中介軟體的terminate方法。

相關文章