中间件

在 laravel 框架中,中间件为过滤访应用的HTTP请求提供了一个方便的机制。在处理逻辑之前,会通过中间件,且只有通过了中间件才会继续执行逻辑代码。它的主要作用就是过滤Http请求(php aritsan是没有中间件机制的),同时也让系统的层次(Http过滤层)更明确,使用起来也很优雅。

中间件本身分为两种,一种是所有http的,另一种则是针对route的。一个有中间件的请求周期是:Request得先经过Http中间件,才能进行Router,再经过Requset所对应Route的Route中间件, 最后才会进入相应的Controller代码。

Illuminate\Pipeline\Pipeline类是实现 laravel 中间件功能的核心。他的作用是,将一系列有序可执行的任务依次执行。

请求

./bootstrap/app.php中绑定了Illuminate\Contracts\Http\Kernel抽象的实现为App\Http\Kernel,在./public/index.php中调用了App\Http\Kernel::handle并传入了$request对象。

/**
 * Handle an incoming HTTP request.
 * 处理传入的HTTP请求
 * @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']->dispatch(
        new Events\RequestHandled($request, $response)
    );

    return $response;
}

/**
 * Send the given request through the middleware / router.
 * 将request请求传递到中间件、路由
 * @param  \Illuminate\Http\Request  $request
 * @return \Illuminate\Http\Response
 */
protected function sendRequestThroughRouter($request)
{
$this->app->instance('request', $request);

Facade::clearResolvedInstance('request');

$this->bootstrap();

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

管道

Pipeline(管道)顾名思义,就是将一系列任务按一定顺序在管道里面依次执行。其中任务可以是匿名函数,也可以是拥有特定方法的类或对象。

Kernel类中分别调用了Pipeline的三个方法:send()是设置了管道调用时要发送的对象,through()设置管道的任务数组,在then()方法中会依次调用。

then()方法中使用了array_reduce()函数:

array_reduce ( array $array , callable $callback [, mixed $initial = NULL ] ) : mixed

array_reduce() 将回调函数 callback 迭代地作用到 array 数组中的每一个单元中,从而将数组简化为单一的值。

  • array:输入的 array。
  • callback ( mixed $carry , mixed $item ) : mixed
    • carry:携带上次迭代里的值; 如果本次迭代是第一次,那么这个值是 initial。
    • item:携带了本次迭代的值。
  • initial:如果指定了可选参数 initial,该参数将在处理开始前使用,或者当处理结束,数组为空时的最后一个结果。

返回结果值,如果array为空并且没有initial,返回结果为NULL

./src/Illuminate/Routing/Pipeline.php

<?php

namespace Illuminate\Routing;

use Closure;
use Exception;
use Throwable;
use Illuminate\Http\Request;
use Illuminate\Contracts\Debug\ExceptionHandler;
use Illuminate\Pipeline\Pipeline as BasePipeline;
use Symfony\Component\Debug\Exception\FatalThrowableError;

/**
 * This extended pipeline catches any exceptions that occur during each slice.
 *
 * The exceptions are converted to HTTP responses for proper middleware handling.
 * 管道类的扩展,捕获异常并转换为HTTP响应,以进行适当的中间件处理。
 */
class Pipeline extends BasePipeline
{
    /**
     * Get the final piece of the Closure onion.
     * 准备管道调用 array_reduce() 函数的初始值initial
     * @param  \Closure  $destination
     * @return \Closure
     */
    protected function prepareDestination(Closure $destination)
    {
        return function ($passable) use ($destination) {
            try {
                return $destination($passable);
            } catch (Exception $e) {
                return $this->handleException($passable, $e);
            } catch (Throwable $e) {
                return $this->handleException($passable, new FatalThrowableError($e));
            }
        };
    }

    /**
     * Get a Closure that represents a slice of the application onion.
     * 获取一个管道调用 array_reduce() 函数的闭包。
     * @return \Closure
     */
    protected function carry()
    {
        return function ($stack, $pipe) {
            return function ($passable) use ($stack, $pipe) {
                try {
                    $slice = parent::carry();

                    $callable = $slice($stack, $pipe);

                    return $callable($passable);
                } catch (Exception $e) {
                    return $this->handleException($passable, $e);
                } catch (Throwable $e) {
                    return $this->handleException($passable, new FatalThrowableError($e));
                }
            };
        };
    }

    /**
     * Handle the given exception.
     * 处理异常
     * @param  mixed  $passable
     * @param  \Exception  $e
     * @return mixed
     *
     * @throws \Exception
     */
    protected function handleException($passable, Exception $e)
    {
        if (! $this->container->bound(ExceptionHandler::class) ||
            ! $passable instanceof Request) {
            throw $e;
        }

        $handler = $this->container->make(ExceptionHandler::class);

        $handler->report($e);

        $response = $handler->render($passable, $e);

        if (method_exists($response, 'withException')) {
            $response->withException($e);
        }

        return $response;
    }
}

./src/Illuminate/Pipeline/Pipeline.php

<?php

namespace Illuminate\Pipeline;

use Closure;
use RuntimeException;
use Illuminate\Http\Request;
use Illuminate\Contracts\Container\Container;
use Illuminate\Contracts\Support\Responsable;
use Illuminate\Contracts\Pipeline\Pipeline as PipelineContract;

class Pipeline implements PipelineContract
{
    /**
     * The container implementation.
     *
     * @var \Illuminate\Contracts\Container\Container
     */
    protected $container;

    /**
     * The object being passed through the pipeline.
     * 传入 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|null $container
     * @return void
     */
    public function __construct(Container $container = null)
    {
        $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
     * @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.
     * 设置执行类任务或者对象任务的调用方法
     * @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)
    {
        $pipeline = array_reduce(
            array_reverse($this->pipes), $this->carry(), $this->prepareDestination($destination)
        );
        return $pipeline($this->passable);
    }

    /**
     * Run the pipeline and return the result.
     * 执行管道并返回结果
     * @return mixed
     */
    public function thenReturn()
    {
        return $this->then(function ($passable) {
            return $passable;
        });
    }

    /**
     * Get the final piece of the Closure onion.
     * 准备管道调用 array_reduce() 函数的初始值initial
     * 对任务 $destination 使用匿名函数进行包装
     * @param \Closure $destination
     * @return \Closure
     */
    protected function prepareDestination(Closure $destination)
    {
        return function ($passable) use ($destination) {
            return $destination($passable);
        };
    }

    /**
     * Get a Closure that represents a slice of the application onion.
     * 获取一个管道调用 array_reduce() 函数的闭包。
     * @return \Closure
     */
    protected function carry()
    {
        return function ($stack, $pipe) {
            return function ($passable) use ($stack, $pipe) {
                if (is_callable($pipe)) {
                    //如果要执行的任务 $pipe 是一个匿名函数的话,立即执行这个匿名函数并返回其结果;
                    return $pipe($passable, $stack);
                } elseif (!is_object($pipe)) {
                    //如果 $pipe 不是对象的话(为字符串),将从 $pipe 中解析出来任务名称和可能存在的参数
                    [$name, $parameters] = $this->parsePipeString($pipe);
                    $pipe = $this->getContainer()->make($name);
                    $parameters = array_merge([$passable, $stack], $parameters);
                } else {
                    //如果 $pipe 是一个对象的话,构建出任务执行所需的参数
                    $parameters = [$passable, $stack];
                }
                //调用任务对象并返回其结果
                $response = method_exists($pipe, $this->method)
                    ? $pipe->{$this->method}(...$parameters)
                    : $pipe(...$parameters);

                return $response instanceof Responsable
                    ? $response->toResponse($this->getContainer()->make(Request::class))
                    : $response;
            };
        };
    }

    /**
     * Parse full pipe string to get name and parameters.
     * 将字符串管道转为管道名和参数
     * 比如中间件 throttle:60,1 的设置,解析出任务名称 throttle,参数 [60,1]
     * @param string $pipe
     * @return array
     */
    protected function parsePipeString($pipe)
    {
        [$name, $parameters] = array_pad(explode(':', $pipe, 2), 2, []);

        if (is_string($parameters)) {
            $parameters = explode(',', $parameters);
        }

        return [$name, $parameters];
    }

    /**
     * Get the container instance.
     *
     * @return \Illuminate\Contracts\Container\Container
     *
     * @throws \RuntimeException
     */
    protected function getContainer()
    {
        if (!$this->container) {
            throw new RuntimeException('A container instance has not been passed to the Pipeline.');
        }

        return $this->container;
    }
}

流程

暂且不管KernelRouter是怎么拼装中间件的,假设现在只有两个中间件:

protected $middleware = [
    \App\Http\Middleware\A::class,
    \App\Http\Middleware\B::class,
];

Kernel中调用链如下:

return (new Pipeline($this->app))
                    ->send($request)
                    ->through($this->app->shouldSkipMiddleware() ? [] : $this->middleware)
                    ->then($this->dispatchToRouter());
  • 通过send()方法,设置$request对象为传入任务的参数。
  • 通过through()方法,设置管道任务为中间件。
  • 通过then()方法开始执行整个管道,传入了dispatchToRouter()方法作为管道的初始值。

dispatchToRouter()返回的是一个接收$request的闭包。

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

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

then()

管道的核心是then()方法,在此方法中,首先翻转了中间件数组,然后依次调用carry()方法,此方法返回的是个闭包函数,在then的最后会执行此闭包函数。

所以最后的流程为:

第一次调用

调用carry()返回的闭包,此闭包调用后返回的依然是个闭包,同时将此闭包当做第一个参数传给下一次调用

function carry() {
    //$stack 为 dispatchToRouter()返回的闭包;$pipe为中间件B;
    return function ($stack, $pipe) {
        //此闭包调用后返回的依然是个闭包,同时将此闭包当做第一个参数传给下一次调用
        return function ($passable) use ($stack, $pipe) {
        };
    }
}

第二次调用

array_reduce()函数的第二个参数(闭包)的两个参数分别为

  • $stack 为第一次调用返回的闭包;
  • $pipe 为中间件A;

执行

经过两次调用后,得到的结果为一个闭包,此闭包会在then()方法的最后调用$pipeline($this->passable);

function ($passable) use ($stack, $pipe) {}
  • $passable 为 $request 对象
  • $stack 为第一次调用返回的闭包;
  • $pipe 为中间件A;

目前只分析当前闭包的对象部分,闭包、字符串等逻辑一样:

//如果 $pipe 是一个对象的话,构建出任务执行所需的参数
$parameters = [$passable, $stack];
//调用任务对象并返回其结果
$response = method_exists($pipe, $this->method)
    ? $pipe->{$this->method}(...$parameters)
    : $pipe(...$parameters);

可见,此闭包最后调用的是A中间件的handle方法,传递的参数分别是$request对象,上一次调用返回的闭包。

我们看一个常见的中间件处理方法:

public function handle($request, Closure $next)
{
    //TODO something
    return $next($request);
}

A中间件中处理完逻辑后,最后返回的是调用’上一次调用返回的闭包‘,等同于$pipeline($this->passable);

总结

中间件的整个流程可以理解为,先反向注册,然后从反向的最后开始依次调用。

注册栈:dispath()、B、A,注册完成后返回的回调为最后一个注册的A,然后在A中调用前一个B,依次执行。

powered by Gitbook更新时间: 2021-05-27 10:52:40

results matching ""

    No results matching ""