Laravel 中间件底层源码剖析


前面学院君已经给大家介绍了 Laravel 一次请求生命周期底层运行逻辑,并分别就请求类和响应类的底层实现和基本使用给大家做了详细的演示,接下来,我们还是围绕请求生命周期中的组件展看,分两篇教程分别介绍中间件的底层原理和路由匹配的底层原理。首先我们来看中间件。

什么是中间件

中间件并不是 Laravel 框架所独有的概念,而是一种被广泛应用的架构模式,尤其是在 Java 中。

在 Laravel 中,通常可以将中间件理解为包裹在应用外面的「层」(就像洋葱那样),用户请求需要经过定义在路由之上的中间件按照顺序层层处理之后才能被最终处理,这样,就方便我们在用户请求被处理前定义一些过滤器或装饰器,比如拒绝用户请求或者在请求中追加参数。

当然,还有一些中间件被定义为在用户请求处理之后、发送响应到用户终端之前执行,我们将这种中间件称之为「终端中间件」,比如用户 Session 和 Cookie 队列的底层实现就借助了终端中间件。

综上,在 Laravel 请求的生命周期中,中间件可以作用于请求处理执行之前和之后,我们可以绘制相应的流程图如下,以便于理解:

注:如果你对这个流程图不太理解,可以查看 Laravel 底层是如何处理 HTTP 请求的这篇教程。

简化起来就是这样:

前面我们在请求类 Request 剖析响应类 Response 剖析中说过,每一个进入 Laravel 应用的请求都会被转化为 Illuminate Request 对象,然后经过所有定义的中间件的处理,以及匹配路由对应处理逻辑处理之后,生成一个 Illuminate Response 响应对象,再经过终端中间件的处理,最终返回给终端用户。

注册中间件

我们可以通过多种方式注册中间件到 Laravel 应用,这项工作主要在 app/Http/Kernel.php 中完成,打开这个文件,可以看到其中已经注册了多个系统内置的中间件:

<?php

namespace App\Http;

use Illuminate\Foundation\Http\Kernel as HttpKernel;

class Kernel extends HttpKernel
{
    /**
     * The application's global HTTP middleware stack.
     *
     * These middleware are run during every request to your application.
     *
     * @var array
     */
    protected $middleware = [
        \App\Http\Middleware\CheckForMaintenanceMode::class,
        \Illuminate\Foundation\Http\Middleware\ValidatePostSize::class,
        \App\Http\Middleware\TrimStrings::class,
        \Illuminate\Foundation\Http\Middleware\ConvertEmptyStringsToNull::class,
        \App\Http\Middleware\TrustProxies::class,
    ];

    /**
     * The application's route middleware groups.
     *
     * @var array
     */
    protected $middlewareGroups = [
        'web' => [
            \App\Http\Middleware\EncryptCookies::class,
            \Illuminate\Cookie\Middleware\AddQueuedCookiesToResponse::class,
            \Illuminate\Session\Middleware\StartSession::class,
            // \Illuminate\Session\Middleware\AuthenticateSession::class,
            \Illuminate\View\Middleware\ShareErrorsFromSession::class,
            \App\Http\Middleware\VerifyCsrfToken::class,
            \Illuminate\Routing\Middleware\SubstituteBindings::class,
        ],

        'api' => [
            'throttle:60,1',
            'bindings',
        ],
    ];

    /**
     * The application's route middleware.
     *
     * These middleware may be assigned to groups or used individually.
     *
     * @var array
     */
    protected $routeMiddleware = [
        'auth' => \App\Http\Middleware\Authenticate::class,
        'auth.basic' => \Illuminate\Auth\Middleware\AuthenticateWithBasicAuth::class,
        'bindings' => \Illuminate\Routing\Middleware\SubstituteBindings::class,
        'cache.headers' => \Illuminate\Http\Middleware\SetCacheHeaders::class,
        'can' => \Illuminate\Auth\Middleware\Authorize::class,
        'guest' => \App\Http\Middleware\RedirectIfAuthenticated::class,
        'signed' => \Illuminate\Routing\Middleware\ValidateSignature::class,
        'throttle' => \Illuminate\Routing\Middleware\ThrottleRequests::class,
        'verified' => \Illuminate\Auth\Middleware\EnsureEmailIsVerified::class,
    ];
}

其中 $middleware 属性中定义的是全局中间件,默认会作用到所有路由,而 $routeMiddleware 中定义的路由则一般按需应用到路由定义中,有时候,特定分组下的路由往往拥有共同的中间件,为了方便起见,我们通过中间件组 $middlewareGroups 为分组路由匹配设置中间件,关于路由中间件的分配我们在前面的文章和教程中已经有了详细介绍,这里不再赘述。

除此之外,还可以通过 $middlewarePriority 属性对中间件的优先级进行设置:

/**
 * The priority-sorted list of middleware.
 *
 * Forces the listed middleware to always be in the given order.
 *
 * @var array
 */
protected $middlewarePriority = [
    \Illuminate\Session\Middleware\StartSession::class,
    \Illuminate\View\Middleware\ShareErrorsFromSession::class,
    \Illuminate\Auth\Middleware\Authenticate::class,
    \Illuminate\Session\Middleware\AuthenticateSession::class,
    \Illuminate\Routing\Middleware\SubstituteBindings::class,
    \Illuminate\Auth\Middleware\Authorize::class,
];

中间件底层原理

关于中间件的创建和使用在中间件文档中已经介绍的比较详细了,下面我们重点来研究中间件的底层执行逻辑,我们从 vendor/laravel/framework/src/Illuminate/Foundation/Http/Kernel.phphandle 方法开始,关于中间件的执行逻辑则是从该方法中的 sendRequestThroughRouter 开始:

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());
}

1、首先构造 Illuminate\Routing\Pipeline 实例。 2、然后将当前请求实例 $request 赋值到 Pipeline$passable 属性。 3、如果应用启用中间件功能的话(默认启用),则将全局中间件数组 $middleware 赋值到 Pipeline$pipes 属性:

4、接下来调用 dispatchToRouter 方法将请求转发给路由,该方法返回一个闭包函数,这个闭包函数将在处理完全局中间件逻辑后执行:

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

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

5、接下来调用 Pipelinethen 方法:

public function then(Closure $destination)
{
    $pipeline = array_reduce(
        array_reverse($this->pipes), $this->carry(), $this->prepareDestination($destination)
    );

    return $pipeline($this->passable);
}

6、首先调用 array_reduce 函数进行预处理,该 PHP 内置函数的定义如下:

在这里,array_reverse($this->pipes) 是待处理的中间件数组,即倒序的全局中间件数组 $middleware$this->carry() 是回调函数,对应的函数体如下:

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));
            }
        };
    };
}

$this->prepareDestination($destination) 则是初始值:

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));
        }
    };
}

prepareDestination 返回的也是一个闭包函数,传入的参数 $destination 是之前 dispatchToRouter 返回的闭包函数,其中包含了路由中间件以及路由的匹配和执行相关逻辑。

7、遍历完所有全局中间件数组后,由于 carry 函数返回的还是闭包函数,所以直到此时都还没有执行任何中间件的 handle 方法,这段处理的意义是将所有全局中间件和路由处理通过统一的闭包函数进行迭代调用。最终返回的 $pipeline 是一个闭包函数:

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));
            }
        };
    };

其中 $pipe 值是第一个全局中间件 App\Http\Middleware\CheckForMaintenanceMode$stack 则包含了之前迭代的所有其它全局中间件和路由处理闭包:

到了下一行:

$pipeline($this->passable);

才开始正式执行 $pipeline 所指向的闭包函数:

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));
    }
}; 

其中 $passable 是当前请求实例,$stackpipe 则是前面 array_reduce 函数处理后的变量。这里的主要逻辑是调用父类的 carry 方法:

protected function carry()
{
    return function ($stack, $pipe) {
        return function ($passable) use ($stack, $pipe) {
            if (is_callable($pipe)) {
                // If the pipe is an instance of a Closure, we will just call it directly but
                // otherwise we'll resolve the pipes out of the container and call it with
                // the appropriate method and arguments, returning the results back out.
                return $pipe($passable, $stack);
            } elseif (! is_object($pipe)) {
                [$name, $parameters] = $this->parsePipeString($pipe);

                // If the pipe is a string we will parse the string and resolve the class out
                // of the dependency injection container. We can then build a callable and
                // execute the pipe function giving in the parameters that are required.
                $pipe = $this->getContainer()->make($name);

                $parameters = array_merge([$passable, $stack], $parameters);
            } else {
                // If the pipe is already an object we'll just make a callable and pass it to
                // the pipe as-is. There is no need to do any extra parsing and formatting
                // since the object we're given was already a fully instantiated object.
                $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;
        };
    };
}

如果当前迭代的 $pipe 是闭包的话,直接执行并返回;如果是对象的话,则通过容器实例化该对象,解析出中间件参数,然后调用对象实例的 handle 方法处理相应的业务逻辑。如果处理返回的结果是 Responsable 实例,则直接返回 HTTP 响应到客户端,否则继续往后处理后续中间件。

8、当执行完最后一个全局中间件 \App\Http\Middleware\TrustProxies::class,且请求未中断,控制流程进入到执行 $this->prepareDestination($destination) 返回的闭包函数,其中包含了应用到路由的中间件处理逻辑:

return $destination($passable);

具体执行的则是 dispatchToRouter 方法返回的闭包函数代码:

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

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

对应的 dispatch 方法最终执行的业务逻辑代码如下:

public function dispatchToRoute(Request $request)
{
    return $this->runRoute($request, $this->findRoute($request));
}

首先我们要通过 findRoute 方法根据当前请求匹配出对应的路由定义,路由定义中会包含分配给该路由的中间件信息,如果没有与之匹配的路由定义,则返回 404 异常,关于路由的匹配和参数解析,我们放到下一篇具体介绍,这里的主线是路由中间件的执行,现在只需了解路由中间件是在这里解析出来的即可。

获取到路由信息之后,就开始执行 runRoute 方法运行路由定义对应的闭包函数或控制器方法了:

protected function runRoute(Request $request, Route $route)
{
    $request->setRouteResolver(function () use ($route) {
        return $route;
    });

    $this->events->dispatch(new Events\RouteMatched($route, $request));

    return $this->prepareResponse($request,
        $this->runRouteWithinStack($route, $request)
    );
}

核心逻辑是最后一行的 runRouteWithinStack($route, $request) 这段函数调用:

protected function runRouteWithinStack(Route $route, Request $request)
{
    $shouldSkipMiddleware = $this->container->bound('middleware.disable') &&
                            $this->container->make('middleware.disable') === true;

    $middleware = $shouldSkipMiddleware ? [] : $this->gatherRouteMiddleware($route);

    return (new Pipeline($this->container))
                    ->send($request)
                    ->through($middleware)
                    ->then(function ($request) use ($route) {
                        return $this->prepareResponse(
                            $request, $route->run()
                        );
                    });
}

在这段代码中,如果应用禁用了 Laravel 中间件,则返回空数组,否则通过 gatherRouteMiddleware 方法从路由定义中获取对应的中间件信息,并对按照优先级对中间件进行排序,然后调用和 Kernel 中类似的代码对路由中间件进行处理,具体的逻辑和流程和前面全局中间件类似,这里就不再赘述了。如果所有路由中间件校验通过,则执行 then 方法传入的闭包函数中的逻辑:

$this->prepareResponse(
    $request, $route->run()
);

准备好响应数据返回给客户端。

9、最后,除了请求处理前执行的中间件,还有请求处理后执行的「终端中间件」,这些中间件在会在响应发送到客户端之后执行:

$response->send();

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

terminate 调用逻辑中,对应的底层执行代码如下:

protected function terminateMiddleware($request, $response)
{
    $middlewares = $this->app->shouldSkipMiddleware() ? [] : array_merge(
        $this->gatherRouteMiddleware($request),
        $this->middleware
    );

    foreach ($middlewares as $middleware) {
        if (! is_string($middleware)) {
            continue;
        }

        [$name] = $this->parseMiddleware($middleware);

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

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

对应的业务逻辑比较简单,Laravel 会检查所有全局中间件和应用到路由的路由中间件中是否包含 terminate 方法,如果包含的话则认为该中间件是「终端中间件」并执行对应的 terminate 方法。

中间件的实现实际上应用了管道设计模式,你可以参考这篇教程对其进行了解。


点赞 取消点赞 收藏 取消收藏

<< 上一篇: Laravel 响应类 Response 剖析

>> 下一篇: Laravel 路由匹配和执行底层源码剖析