Laravel Middleware(中间件)执行流程源码分析

基于laravel10分析
完整的执行流程我会放到后面的文章,这里只针对中间件的执行部分

先来说一下中间件的实现吧,在lavavel中是通过Pipeline(管道)+array_reduce+array_reverse内置函数来实现的
代码是这样子的

(new Pipeline($this->container))
    ->send($request)
    ->through($middleware)
    ->then(fn($request) => xxx);

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

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

    return $this;
}

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

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

protected function carry()
 {
     return function ($stack, $pipe) {
         return function ($passable) use ($stack, $pipe) {
             try {
                 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];
                }

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

                return $this->handleCarry($carry);
        } catch (Throwable $e) {
            return $this->handleException($passable, $e);
        }
     };
  };
}

主要是来看一下then方法,先是把中间件数组通过array_reverse顺序翻转(目的是为了配合array_reduce),因为array_reduce是迭代数组元素传入回调函数里面,第一次迭代就用第三个参数作为初始值,看carry方法就能知道,每一次迭代就是闭包套闭包,最开始的就在最里面,所以需要翻转数组
上面array_reduce执行完后得到的差不多是下面的效果,只是里面闭包是use进去的,我这里举例是固定的

$f = function () {
    $next2 = function () {
        $next1 = function () {
        };
        //前置
        $next1();
        //后置
    };
    //前置
    $next2();
    //后置
};
$f();

回到正文

在laravel框架中,中间件是分两步执行的,第一步是全局中间件,第二步是路由中间件

全局中间件

全局中间件是对应App\Http\Kernel类的$middleware属性
全局中间件是在Illuminate\Foundation\Http\Kernel类的sendRequestThroughRouter方法中执行的,这里也没有做什么特别的处理,就是拿$middleware属性数组传入进去了

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

路由中间件

主要是看这里,路由中间件做了很多处理,让我们来看一下做了哪些处理
先看Illuminate\Foundation\Http\Kernel类的syncMiddlewareToRouter方法

protected function syncMiddlewareToRouter()
 {
         //这里就是把中间件优先级排序列表赋值到路由单例里面
        $this->router->middlewarePriority = $this->middlewarePriority;
        //这里是把中间件组赋值到路由单例里面
        foreach ($this->middlewareGroups as $key => $middleware) {
            $this->router->middlewareGroup($key, $middleware);
        }
        //这里是把中间件别名赋值到路由单例里面
        //$routeMiddleware这个属性好像在10版本里面没有直接用到,不知道是不是以前的版本是用这个属性来定义别名的
        foreach (array_merge($this->routeMiddleware, $this->middlewareAliases) as $key => $middleware) {
            $this->router->aliasMiddleware($key, $middleware);
        }
}

再直接看Illuminate\Routing\Router类的runRouteWithinStack方法

protected function runRouteWithinStack(Route $route, Request $request)
{
        //这里是判断是否禁用中间件,这里不清楚为啥不像全局中间件那样调用 $this->app->shouldSkipMiddleware()
        $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(fn ($request) => $this->prepareResponse(
                            $request, $route->run()
                        ));
}

①看一下Illuminate\Routing\Router类的gatherRouteMiddleware方法做了什么处理

public function gatherRouteMiddleware(Route $route)
{
        //这里是拿到定义的中间件和需要排除的中间件传入进去
        return ②$this->resolveMiddleware($route->gatherMiddleware(), $route->excludedMiddleware());
}

②看一下Illuminate\Routing\Router类的resolveMiddleware方法做了什么处理

public function resolveMiddleware(array $middleware, array $excluded = [])
    {
        //排除中间件
        $excluded = collect($excluded)->map(function ($name) {
            //这里是去解析中间件
            return (array) ③MiddlewareNameResolver::resolve($name, $this->middleware, $this->middlewareGroups);
        })->flatten()->values()->all();
        //当前路由需要执行的中间件(去掉排除的中间件)
        $middleware = collect($middleware)->map(function ($name) {
            return (array) MiddlewareNameResolver::resolve($name, $this->middleware, $this->middlewareGroups);
        })->flatten()->reject(function ($name) use ($excluded) {
            if (empty($excluded)) {
                return false;
            }

            if ($name instanceof Closure) {
                return false;
            }

            if (in_array($name, $excluded, true)) {
                return true;
            }

            if (! class_exists($name)) {
                return false;
            }

            $reflection = new ReflectionClass($name);
            //这里是判断是否是排除中间件中某一个中间件的子类
            return collect($excluded)->contains(
                fn ($exclude) => class_exists($exclude) && $reflection->isSubclassOf($exclude)
            );
        })->values();
        //排序
        return ④$this->sortMiddleware($middleware);
    }

③看一下Illuminate\Routing\MiddlewareNameResolver类的resolve方法做了什么处理

public static function resolve($name, $map, $middlewareGroups)
{
        //判断是否是闭包
        if ($name instanceof Closure) {
            return $name;
        }
        //判断是否别名 且别名对应的也是闭包
        if (isset($map[$name]) && $map[$name] instanceof Closure) {
            return $map[$name];
        }
        //判断是否是中间件组
        if (isset($middlewareGroups[$name])) {
            //解析中间件组
            return static::parseMiddlewareGroup($name, $map, $middlewareGroups);
        }
        //分离名称和参数
        [$name, $parameters] = array_pad(explode(':', $name, 2), 2, null);
        //把别名替换类名拼接参数
        return ($map[$name] ?? $name).(! is_null($parameters) ? ':'.$parameters : '');
}

protected static function parseMiddlewareGroup($name, $map, $middlewareGroups)
{
    $results = [];

    foreach ($middlewareGroups[$name] as $middleware) {
        //嵌套中间组
        if (isset($middlewareGroups[$middleware])) {
            //递归处理
            $results = array_merge($results, static::parseMiddlewareGroup(
                $middleware, $map, $middlewareGroups
            ));

            continue;
        }
        //拿到中间件名和参数
        [$middleware, $parameters] = array_pad(
            explode(':', $middleware, 2), 2, null
        );
        //判断是否是别名
        if (isset($map[$middleware])) {
            $middleware = $map[$middleware];
        }

        $results[] = $middleware.($parameters ? ':'.$parameters : '');
    }

    return $results;
}

上面就是把中间件组和别名处理找到对应的类名,后续就可以直接去实例化调用
④看一下Illuminate\Routing\Router类的sortMiddleware方法做了什么处理
就是拿$middlewarePriority属性数组中间的顺序去排序当前请求路由上的中间件

 protected function sortMiddleware(Collection $middlewares)
 {
     return (new SortedMiddleware($this->middlewarePriority, $middlewares))->all();
}

class SortedMiddleware extends Collection
{
    public function __construct(array $priorityMap, $middlewares)
    {
        if ($middlewares instanceof Collection) {
            $middlewares = $middlewares->all();
        }

        $this->items = $this->sortMiddleware($priorityMap, $middlewares);
    }

    protected function sortMiddleware($priorityMap, $middlewares)
    {
        $lastIndex = 0;

        foreach ($middlewares as $index => $middleware) {
            //闭包
            if (! is_string($middleware)) {
                continue;
            }
            //看是否能拿到优先级数组的下标
              $priorityIndex = ⑤$this->priorityMapIndex($priorityMap, $middleware);
            //表示能拿到下标
            if (! is_null($priorityIndex)) {
                //如果存在上一次的下标 且 这次的下标小于上一次的下标 则需要进行排序,把这次的放到上次下标的前面,然后删除这次下标+1的元素(就是未排序前的自己)
                if (isset($lastPriorityIndex) && $priorityIndex < $lastPriorityIndex) {
                    //这里需要重新进行新的排序  递归处理
                    return $this->sortMiddleware(
                        $priorityMap, array_values($this->moveMiddleware($middlewares, $index, $lastIndex))
                    );
                }

                $lastIndex = $index;

                $lastPriorityIndex = $priorityIndex;
            }
        }
        //去重
        return ⑥Router::uniqueMiddleware($middlewares);
    }
}

⑤看一下Illuminate\Routing\SortedMiddleware类的priorityMapIndex方法做了什么处理

protected function priorityMapIndex($priorityMap, $middleware)
{
    foreach ($this->middlewareNames($middleware) as $name) {
        $priorityIndex = array_search($name, $priorityMap);

        if ($priorityIndex !== false) {
            return $priorityIndex;
        }
    }
}
//这里返回一个迭代
protected function middlewareNames($middleware)
{
    //拿到中间件类名
    $stripped = head(explode(':', $middleware));

    yield $stripped;
    //拿到所有接口包括父类的接口
    $interfaces = @class_implements($stripped);

    if ($interfaces !== false) {
        foreach ($interfaces as $interface) {
            yield $interface;
        }
    }
    //拿到所有继承的父类
    $parents = @class_parents($stripped);

    if ($parents !== false) {
        foreach ($parents as $parent) {
            yield $parent;
        }
    }
}

⑥看一下Illuminate\Routing\Router类的uniqueMiddleware方法做了什么处理

public static function uniqueMiddleware(array $middleware)
{
        $seen = [];
        $result = [];

        foreach ($middleware as $value) {
            //这里如果是对象(闭包)  则拿到对象的唯一标识符
            $key = \is_object($value) ? \spl_object_id($value) : $value;

            if (! isset($seen[$key])) {
                $seen[$key] = true;
                $result[] = $value;
            }
        }

        return $result;
}

排序去重完中间件之后就会传入到管道里面去执行后续的流程了

以上就是中间件执行大概流程了,如果有写的有误的地方,请大佬们指正

本作品采用《CC 协议》,转载必须注明作者和本文链接
本帖由系统于 1年前 自动加精
《L03 构架 API 服务器》
你将学到如 RESTFul 设计风格、PostMan 的使用、OAuth 流程,JWT 概念及使用 和 API 开发相关的进阶知识。
《L01 基础入门》
我们将带你从零开发一个项目并部署到线上,本课程教授 Web 开发中专业、实用的技能,如 Git 工作流、Laravel Mix 前端工作流等。
讨论数量: 0
(= ̄ω ̄=)··· 暂无内容!

讨论应以学习和精进为目的。请勿发布不友善或者负能量的内容,与人为善,比聪明更重要!