Laravel路由中间件的实现原理

Laravel 中间件提供了一种方便的机制来过滤进入应用的 HTTP 请求。例如,Laravel 内置了一个中间件来验证用户的身份认证。如果用户没有通过身份认证,中间件会将用户重定向到登录界面。但是,如果用户被认证,中间件将允许该请求进一步进入该应用。

当然,除了身份认证以外,还可以编写另外的中间件来执行各种任务。例如:CORS 中间件可以负责为所有离开应用的响应添加合适的头部信息;日志中间件可以记录所有传入应用的请求。

Laravel 自带了一些中间件,包括身份验证、CSRF 保护等。所有这些中间件都位于 app/Http/Middleware 目录。

简单来说就是请求在不去修改自身的逻辑,通过中间件扩展或者处理一些功能。

1、先来了解一下 array_reduce 函数
地址:https://blog.csdn.net/raoxiaoya/article/details/103447483

2、源码解读
index.php中

$kernel = $app->make(Illuminate\Contracts\Http\Kernel::class);

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

$response->send();

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

然后
Illuminate\Foundation\Http\Kernel 的handle方法

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

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());
}
// 通过管道之后进入路由,控制器
protected function dispatchToRouter()
{
    return function ($request) {
        $this->app->instance('request', $request);

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

然后是 Illuminate\Pipeline\Pipeline

send 方法设置通过管道的数据 即request
through 方法设置要通过的管道数组
then 方法是执行,传入的参数是一个闭包,也就是如果顺利通过管道后进入的路由层。

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

    // 执行闭包,并传入一个参数,也就是request对象
    return $pipeline($this->passable);
}

protected function prepareDestination(Closure $destination)
{
    return function ($passable) use ($destination) {
        return $destination($passable);
    };
}

根据 array_reduce(元素数组, 回调函数, 初始化值) 的了解,如果传入的是闭包嵌套,那么最终解包后的执行顺序是反的,也就是说最里层的闭包最后执行,而在封装的时候,第一个是在最里面的,最后一个是最外面的,所以要使用array_reverse来反转middlewares,并且最后的路由层反而应该设置在初始化的位置.

那么问题来了,我们定义的路由中间件都是单独的类,实现了 public function handle($request, Closure $next) 方法,如何将其封装为闭包呢?实际上在外层套一个闭包函数就可以了

return function ($passable) use ($stack, $pipe) {
	return (new $pipe)->handle($passable, $stack);
}

为了实现在carry方法中统一的调用方式,将 $destination 在嵌套一层闭包,也就是 prepareDestination 方法的作用,只需要再次 use 自己就行。实际上不加这一层也没毛病。

protected function carry()
{
    return function ($stack, $pipe) {
        return function ($passable) use ($stack, $pipe) {
            if (is_callable($pipe)) {
                return $pipe($passable, $stack);
            } elseif (! is_object($pipe)) {
                [$name, $parameters] = $this->parsePipeString($pipe);
                $pipe = $this->getContainer()->make($name);
                $parameters = array_merge([$passable, $stack], $parameters);
            } else {
                $parameters = [$passable, $stack];
            }

            $response = method_exists($pipe, $this->method)
                            ? $pipe->{$this->method}(...$parameters)
                            : $pipe(...$parameters);

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

要注意,array_reduce 的作用只是形成一个递归闭包,要使其能够一层层执行下去,需要 $pipeline($this->passable) 触发,然后还需要内部建立关联关系,即 $next($request) ,而刚好最里层的闭包不需要 next,设计得还算很巧妙。由这种嵌套的方式注定了你不能设置太多的中间件,因为层次越深性能将急剧下降。

一个示例

<?php
/**
 * ----------------------------------------------------------
 * date: 2019/12/8 14:39
 * ----------------------------------------------------------
 * author: Raoxiaoya
 * ----------------------------------------------------------
 * describe:
 * ----------------------------------------------------------
 */

class VerfiyCsrfToken
{
    public function handle($request, Closure $next)
    {
        print_r($request);
        $request['token'] = 'xxxxxxxxxxx';
        $response         = $next($request);
        return $response;
    }
}

class VerfiyAuth
{
    public function handle($request, Closure $next)
    {
        print_r($request);
        $request['auth'] = 'aaaaaaaaaaa';
        $response        = $next($request);
        return $response;
    }
}

class SetCookieInfo
{
    public function handle($request, Closure $next)
    {
        print_r($request);
        $request['cookie'] = 'ccccccccccccc';
        $response = $next($request);
        return $response;
    }
}

$middlewareArr = [
    'VerfiyCsrfToken',
    'VerfiyAuth',
    'SetCookieInfo',
];

$request = [];

$destination = function ($request) {
    print_r($request);
    return '通过管道之后';
};
$handle = function ($passable) use ($destination) {
    return $destination($passable);
};

$pipeline = array_reduce(
    array_reverse($middlewareArr),
    function ($stack, $pipe) {
        return function ($passable) use ($stack, $pipe) {
            if (is_callable($pipe)) {
                return $pipe($passable, $stack);
            } elseif (! is_object($pipe)) {
                $pipe = new $pipe();
            }

            $response = $pipe->handle($passable, $stack);

            return $response;
        };
    },
    $handle
);

print_r($pipeline);
var_dump($pipeline($request));

最终得到的大闭包
在这里插入图片描述
执行结果:
在这里插入图片描述

我们打印出解包过程:
在这里插入图片描述
第一次解包时的打印
在这里插入图片描述
第二次解包时打印
在这里插入图片描述
第三次解包时打印
在这里插入图片描述
只打印了三次,那还有一次呢?因为经过这个回调函数的只有我们定义的三个中间件,最后一个不经过这里,所以不会在这里执行。这样的话 is_callable 也是多余的。

发布了412 篇原创文章 · 获赞 25 · 访问量 5万+

猜你喜欢

转载自blog.csdn.net/raoxiaoya/article/details/103462286
今日推荐