资讯专栏INFORMATION COLUMN

Laravel 路由处理

Allen / 2767人阅读

摘要:请求绑定取得有效的对应的参数键值对取得有效的对应的主机键值对,并合并到参数键值对从中取出正则匹配的相应的值类似从上取出参数数组,并去掉小结本质就是对象参数属性的处理。

Laravel 路由处理 代码展示
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);
    };
}
public function dispatch(Request $request)
{
    $this->currentRequest = $request;

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

    $request->setRouteResolver(function () use ($route) {
        return $route;
    });

    $this->events->dispatch(new EventsRouteMatched($route, $request));
    // 至此,系统的流程走到了控制器相关的处理,后面待续
    $response = $this->runRouteWithinStack($route, $request);
    
    return $this->prepareResponse($request, $response);
}
路由查找
protected function findRoute($request)
{
    $this->current = $route = $this->routes->match($request);

    $this->container->instance(Route::class, $route);

    return $route;
}
public function match(Request $request)
{
    $routes = $this->get($request->getMethod());
    // 根据请求匹配到第一个相应的路由
    $route = $this->matchAgainstRoutes($routes, $request);

    if (! is_null($route)) {
        return $route->bind($request);
    }
    // 若没有找到,则按照["GET", "HEAD", "POST", "PUT", "PATCH", "DELETE", "OPTIONS"]顺序再找一遍
    $others = $this->checkForAlternateVerbs($request);

    if (count($others) > 0) {
        return $this->getRouteForMethods($request, $others);
    }

    throw new NotFoundHttpException;
}
public function get($method = null)
{
    return is_null($method) ? $this->getRoutes() : Arr::get($this->routes, $method, []);
}
protected function getRouteForMethods($request, array $methods)
{
    if ($request->method() == "OPTIONS") {
        return (new Route("OPTIONS", $request->path(), function () use ($methods) {
            return new Response("", 200, ["Allow" => implode(",", $methods)]);
        }))->bind($request);
    }

    $this->methodNotAllowed($methods);
}
路由匹配(核心)
protected function matchAgainstRoutes(array $routes, $request, $includingMethod = true)
{
    return Arr::first($routes, function ($value) use ($request, $includingMethod) {
        return $value->matches($request, $includingMethod);
    });
}
public function matches(Request $request, $includingMethod = true)
{
    // 解析路由
    $this->compileRoute();
    // 检验(uri正则匹配、方法是否存在、http(s)请求、主机正则匹配)均通过,则返回真
    foreach ($this->getValidators() as $validator) {
        if (! $includingMethod && $validator instanceof MethodValidator) {
            continue;
        }
        if (! $validator->matches($this, $request)) {
            return false;
        }
    }

    return true;
}
public static function getValidators()
{
    if (isset(static::$validators)) {
        return static::$validators;
    }

    return static::$validators = [
        new UriValidator, new MethodValidator,
        new SchemeValidator, new HostValidator,
    ];
}
protected function compileRoute()
{
    if (! $this->compiled) {
        $this->compiled = (new RouteCompiler($this))->compile();
    }

    return $this->compiled;
}
// 记录可选参数并统一 uri 形式,后面进行统一的处理
public function compile()
{
    $optionals = $this->getOptionalParameters();
    $uri = preg_replace("/{(w+?)?}/", "{$1}", $this->route->uri());
    // 构造成 symfony 的路由形式,再委托 SymfonyComponentRoutingRouteCompiler 处理
    return (
        new SymfonyRoute($uri, $optionals, $this->route->wheres, [], $this->route->domain() ?: "")
    )->compile();
}
protected function getOptionalParameters()
{
    preg_match_all("/{(w+?)?}/", $this->route->uri(), $matches);
    
    return isset($matches[1]) ? array_fill_keys($matches[1], null) : [];
}
// 初始化处理(路径和参数的规格化,合并 options,设置主机等)
public function __construct($path, array $defaults = array(), array $requirements = array(), array $options = array(), $host = "", $schemes = array(), $methods = array(), $condition = "")
{
    $this->setPath($path);
    $this->setDefaults($defaults);
    $this->setRequirements($requirements);
    $this->setOptions($options);            // 合并["compiler_class" => "SymfonyComponentRoutingRouteCompiler",] 和 $options
    $this->setHost($host);
    $this->setSchemes($schemes);
    $this->setMethods($methods);
    $this->setCondition($condition);
}
public function compile()
{
    if (null !== $this->compiled) {
        return $this->compiled;
    }

    $class = $this->getOption("compiler_class");
    // 委托 SymfonyComponentRoutingRouteCompiler 来处理,返回 SymfonyComponentRoutingCompiledRoute 对象
    return $this->compiled = $class::compile($this);
}
public static function compile(Route $route)
{
    $hostVariables = array();
    $variables = array();
    $hostRegex = null;
    $hostTokens = array();

    if ("" !== $host = $route->getHost()) {
        $result = self::compilePattern($route, $host, true);

        $hostVariables = $result["variables"];
        $variables = $hostVariables;

        $hostTokens = $result["tokens"];
        $hostRegex = $result["regex"];
    }

    $path = $route->getPath();

    $result = self::compilePattern($route, $path, false);

    $staticPrefix = $result["staticPrefix"];

    $pathVariables = $result["variables"];

    foreach ($pathVariables as $pathParam) {
        if ("_fragment" === $pathParam) {
            throw new InvalidArgumentException(sprintf("Route pattern "%s" cannot contain "_fragment" as a path parameter.", $route->getPath()));
        }
    }

    $variables = array_merge($variables, $pathVariables);

    $tokens = $result["tokens"];
    $regex = $result["regex"];
    // 委托 CompiledRoute 返回数据,数据没做什么处理,只是多提供了序列化和反序列化方法
    return new CompiledRoute(
        $staticPrefix,
        $regex,
        $tokens,
        $pathVariables,
        $hostRegex,
        $hostTokens,
        $hostVariables,
        array_unique($variables)
    );
}
// 核心方法
private static function compilePattern(Route $route, $pattern, $isHost)
{
    $tokens = array();
    $variables = array();
    $matches = array();
    $pos = 0;
    $defaultSeparator = $isHost ? "." : "/";    // 主机或路径默认分割符
    $useUtf8 = preg_match("//u", $pattern);
    $needsUtf8 = $route->getOption("utf8");
    
    if (!$needsUtf8 && $useUtf8 && preg_match("/[x80-xFF]/", $pattern)) {
        $needsUtf8 = true;
        @trigger_error(sprintf("Using UTF-8 route patterns without setting the "utf8" option is deprecated since Symfony 3.2 and will throw a LogicException in 4.0. Turn on the "utf8" route option for pattern "%s".", $pattern), E_USER_DEPRECATED);
    }
    if (!$useUtf8 && $needsUtf8) {
        throw new LogicException(sprintf("Cannot mix UTF-8 requirements with non-UTF-8 pattern "%s".", $pattern));
    }
    // 解析类似 $pattern("/posts/{post}/comments/{comment}") 里面的 {w+} 到 $matches ($matches 类似 [[["{post}",7]],[["{comment}",23]]],值和位置)
    preg_match_all("#{w+}#", $pattern, $matches, PREG_OFFSET_CAPTURE | PREG_SET_ORDER);
    foreach ($matches as $match) {
        $varName = substr($match[0][0], 1, -1);                         // 取方括号中间的值,即变量名称
        $precedingText = substr($pattern, $pos, $match[0][1] - $pos);   // 取前一个变量末尾位置后一位到({)的前一段文本
        $pos = $match[0][1] + strlen($match[0][0]);                     // 取(})后一段的(/)位置 
        // 尝试取前一段的最后一个字符
        if (!strlen($precedingText)) {
            $precedingChar = "";
        } elseif ($useUtf8) {
            preg_match("/.$/u", $precedingText, $precedingChar);
            $precedingChar = $precedingChar[0];
        } else {
            $precedingChar = substr($precedingText, -1);
        }
        $isSeparator = "" !== $precedingChar && false !== strpos(static::SEPARATORS, $precedingChar);   // 是否分割符
        if (preg_match("/^d/", $varName)) {
            throw new DomainException(sprintf("Variable name "%s" cannot start with a digit in route pattern "%s". Please use a different name.", $varName, $pattern));
        }
        if (in_array($varName, $variables)) {
            throw new LogicException(sprintf("Route pattern "%s" cannot reference variable name "%s" more than once.", $pattern, $varName));
        }
        if (strlen($varName) > self::VARIABLE_MAXIMUM_LENGTH) {
            throw new DomainException(sprintf("Variable name "%s" cannot be longer than %s characters in route pattern "%s". Please use a shorter name.", $varName, self::VARIABLE_MAXIMUM_LENGTH, $pattern));
        }
        // 若前一段有值,即文本形式。则入 $tokens 数组 类似: $tokens[] = ["text", "/posts",]
        if ($isSeparator && $precedingText !== $precedingChar) {
            $tokens[] = array("text", substr($precedingText, 0, -strlen($precedingChar)));
        } elseif (!$isSeparator && strlen($precedingText) > 0) {
            $tokens[] = array("text", $precedingText);
        }
        // 尝试获取 where 条件设置的正则
        $regexp = $route->getRequirement($varName);
        // 没有则用最宽松的方式处理
        if (null === $regexp) {
            // 取分割符后面的内容 类似: "/comments/{comment}"
            $followingPattern = (string) substr($pattern, $pos);
            // 找出下一个分割符 类似: "/"
            $nextSeparator = self::findNextSeparator($followingPattern, $useUtf8);
            // 构造 regexp 类似: "[^/]"
            $regexp = sprintf(
                "[^%s%s]+",
                preg_quote($defaultSeparator, self::REGEX_DELIMITER),
                $defaultSeparator !== $nextSeparator && "" !== $nextSeparator ? preg_quote($nextSeparator, self::REGEX_DELIMITER) : ""
            );
            // "[^/]+"
            if (("" !== $nextSeparator && !preg_match("#^{w+}#", $followingPattern)) || "" === $followingPattern) {
                $regexp .= "+";
            }
        } else {
            if (!preg_match("//u", $regexp)) {
                $useUtf8 = false;
            } elseif (!$needsUtf8 && preg_match("/[x80-xFF]|(?= 0; --$i) {
            $token = $tokens[$i];
            if ("variable" === $token[0] && $route->hasDefault($token[3])) {
                $firstOptional = $i;
            } else {
                break;
            }
        }
    }
    // 构造正则
    $regexp = "";
    for ($i = 0, $nbToken = count($tokens); $i < $nbToken; ++$i) {
        $regexp .= self::computeRegexp($tokens, $i, $firstOptional);
    }
    // 类似: "#^/posts/(?P[^/]+)/comments/(?P[^/]+)$#si"
    $regexp = self::REGEX_DELIMITER."^".$regexp."$".self::REGEX_DELIMITER."s".($isHost ? "i" : "");

    if ($needsUtf8) {
        $regexp .= "u";
        for ($i = 0, $nbToken = count($tokens); $i < $nbToken; ++$i) {
            if ("variable" === $tokens[$i][0]) {
                $tokens[$i][] = true;
            }
        }
    }

    return array(
        "staticPrefix" => "text" === $tokens[0][0] ? $tokens[0][1] : "",    // 设置静态的前缀
        "regex" => $regexp,
        "tokens" => array_reverse($tokens),
        "variables" => $variables,
    );
}
private static function computeRegexp(array $tokens, $index, $firstOptional)
{
    $token = $tokens[$index];
    // 文本形式,直接转义返回
    if ("text" === $token[0]) {
        return preg_quote($token[1], self::REGEX_DELIMITER);
    }
    // 参数形式
    else {
        // 第一个就是可选项的处理
        if (0 === $index && 0 === $firstOptional) {
            return sprintf("%s(?P<%s>%s)?", preg_quote($token[1], self::REGEX_DELIMITER), $token[3], $token[2]);
        } else {
            $regexp = sprintf("%s(?P<%s>%s)", preg_quote($token[1], self::REGEX_DELIMITER), $token[3], $token[2]);
            // 可选项后面的正则处理,均变为可选项。
            if ($index >= $firstOptional) {
                $regexp = "(?:$regexp";
                $nbTokens = count($tokens);
                if ($nbTokens - 1 == $index) {
                    $regexp .= str_repeat(")?", $nbTokens - $firstOptional - (0 === $firstOptional ? 1 : 0));
                }
            }

            return $regexp;
        }
    }
}

小结(路径 /posts/{post}/comments/{comment}):

路由解析主要就是解析路由的主机和路径部分(带参数部分),差别在于分割符不一样。并将解析结果放到 route 对象的 $compiled 属性供后续使用。

重点是先将其分割成对应的文本和变量部分( $tokens = [["text", "/posts",],["variable", "/", "1+", "post"],["text", "/comments",], $variables = ["post", "comment",])。

再根据上面分割的数组构造相应的正则表达式(会使用到 where 条件设置的正则)。

最后返回一个数组,表明此路由对应的静态前缀、正则表达式、tokens、variables。

请求绑定
$route->bind($request):
public function bind(Request $request)
{
    $this->compileRoute();

    $this->parameters = (new RouteParameterBinder($this))
                    ->parameters($request);

    return $this;
}

new RouteParameterBinder($this):
public function __construct($route)
{
    $this->route = $route;
}
public function parameters($request)
{
    // 取得有效的对应的参数键值对
    $parameters = $this->bindPathParameters($request);

    if (! is_null($this->route->compiled->getHostRegex())) {
        // 取得有效的对应的主机键值对,并合并到参数键值对
        $parameters = $this->bindHostParameters(
            $request, $parameters
        );
    }

    return $this->replaceDefaults($parameters);
}
protected function bindPathParameters($request)
{
    preg_match($this->route->compiled->getRegex(), "/".$request->decodedPath(), $matches);
    // 从 pathinfo 中取出正则匹配的相应的值 类似["post"=>1,"comment"=>2]
    return $this->matchToKeys(array_slice($matches, 1));
}
public function decodedPath()
{
    return rawurldecode($this->path());
}
protected function matchToKeys(array $matches)
{
    if (empty($parameterNames = $this->route->parameterNames())) {
        return [];
    }
    $parameters = array_intersect_key($matches, array_flip($parameterNames));

    return array_filter($parameters, function ($value) {
        return is_string($value) && strlen($value) > 0;
    });
}
public function parameterNames()
{
    if (isset($this->parameterNames)) {
        return $this->parameterNames;
    }

    return $this->parameterNames = $this->compileParameterNames();
}
protected function compileParameterNames()
{
    // 从 uri 上取出参数数组,并去掉 "?"
    preg_match_all("/{(.*?)}/", $this->domain().$this->uri, $matches);

    return array_map(function ($m) {
        return trim($m, "?");
    }, $matches[1]);
}
protected function bindHostParameters($request, $parameters)
{
    preg_match($this->route->compiled->getHostRegex(), $request->getHost(), $matches);

    return array_merge($this->matchToKeys(array_slice($matches, 1)), $parameters);
}
protected function replaceDefaults(array $parameters)
{
    foreach ($parameters as $key => $value) {
        $parameters[$key] = isset($value) ? $value : Arr::get($this->route->defaults, $key);
    }

    foreach ($this->route->defaults as $key => $value) {
        if (! isset($parameters[$key])) {
            $parameters[$key] = $value;
        }
    }

    return $parameters;
}

小结:

本质就是 route 对象参数属性 $parameters 的处理。根据路由解析得到的正则从实际请求的PATHINFO和HOST里面提取出相应参数对应的值,再进行合并处理(包括默认值的设置、追加默认参数及对值得过滤)。

路由分离器设置
public function setRouteResolver(Closure $callback)
{
    $this->routeResolver = $callback;

    return $this;
}
事件分发 参考 Kernel实例化后的处理 路由执行
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()
                        );
                    });
}
执行前的准备

获取经过优先级处理过的所有的非全局中间件

public function gatherRouteMiddleware(Route $route)
{
    // 返回 object(IlluminateSupportCollection),$items 属性类似 ["类名","类名:参数","匿名函数"]
    $middleware = collect($route->gatherMiddleware())->map(function ($name) {
        return (array) MiddlewareNameResolver::resolve($name, $this->middleware, $this->middlewareGroups);
    })->flatten();

    return $this->sortMiddleware($middleware);
}
// 获取本次请求设置的所有的非全局的 middleware (包括前置栈、middleware 方法、控制器 getMiddleware 方法)
public function gatherMiddleware()
{
    if (! is_null($this->computedMiddleware)) {
        return $this->computedMiddleware;
    }

    $this->computedMiddleware = [];

    return $this->computedMiddleware = array_unique(array_merge(
        $this->middleware(), $this->controllerMiddleware()
    ), SORT_REGULAR);
}
// 解析 middleware ,返回简单的形式(匿名函数或 middleware:parameters ),优先从 $middleware 和 $middlewareGroups 取,没有则直接返回 $name
public static function resolve($name, $map, $middlewareGroups)
{
    if ($name instanceof Closure) {
        return $name;
    } elseif (isset($map[$name]) && $map[$name] instanceof Closure) {
        return $map[$name];
    } elseif (isset($middlewareGroups[$name])) {
        return static::parseMiddlewareGroup(
            $name, $map, $middlewareGroups
        );
    } else {
        list($name, $parameters) = array_pad(explode(":", $name, 2), 2, null);

        return (isset($map[$name]) ? $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;
        }

        list($middleware, $parameters) = array_pad(
            explode(":", $middleware, 2), 2, null
        );
        if (isset($map[$middleware])) {
            $middleware = $map[$middleware];
        }

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

    return $results;
}
##################################### 排序块 BEGIN #######################################
protected function sortMiddleware(Collection $middlewares)
{
    return (new SortedMiddleware($this->middlewarePriority, $middlewares))->all();
}
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) {
        // 匿名函数直接 continue
        if (! is_string($middleware)) {
            continue;
        }

        $stripped = head(explode(":", $middleware));

        if (in_array($stripped, $priorityMap)) {
            $priorityIndex = array_search($stripped, $priorityMap);

            if (isset($lastPriorityIndex) && $priorityIndex < $lastPriorityIndex) {
                return $this->sortMiddleware(
                    $priorityMap, array_values(
                        $this->moveMiddleware($middlewares, $index, $lastIndex)
                    )
                );
            } else {
                $lastIndex = $index;
                $lastPriorityIndex = $priorityIndex;
            }
        }
    }

    return array_values(array_unique($middlewares, SORT_REGULAR));
}
protected function moveMiddleware($middlewares, $from, $to)
{
    array_splice($middlewares, $to, 0, $middlewares[$from]);

    unset($middlewares[$from + 1]);

    return $middlewares;
}
##################################### 排序块 END #######################################
执行

后续分解

public function run()
{
    $this->container = $this->container ?: new Container;

    try {
        // 控制器形式的处理(Controller@Method)
        if ($this->isControllerAction()) {
            return $this->runController();
        }
        // 匿名函数形式的处理
        return $this->runCallable();
    } catch (HttpResponseException $e) {
        return $e->getResponse();
    }
}
public function prepareResponse($request, $response)
{
    if ($response instanceof PsrResponseInterface) {
        $response = (new HttpFoundationFactory)->createResponse($response);
    } elseif (! $response instanceof SymfonyResponse) {
        $response = new Response($response);
    }

    return $response->prepare($request);
}

/ ↩

文章版权归作者所有,未经允许请勿转载,若此文章存在违规行为,您可以联系管理员删除。

转载请注明本文地址:https://www.ucloud.cn/yun/22618.html

相关文章

  • Laravel思维导图之Laravel HTTP路由、中间件、控制器

    摘要:又限于层的内容太多,我在这篇中将整理路由中间件控制器部分内容。前者定义页面路由,默认应用中间件组后者定义无状态路由,会应用中间件组。命名路由可以为指定路由或者控制器方法命名,也可以为已命名的路由生成。 showImg(https://segmentfault.com/img/remote/1460000010882838); 上图列出了 Laravel HTTP 层的相关知识大纲。由于...

    linkin 评论0 收藏0
  • 【日常填坑】之ajax请求laravel的api接口

    摘要:合适和够用是最完美的追求。比如从页面去请求的资源。它允许浏览器向跨源服务器,发出请求,从而克服了只能同源使用的限制。定义在中的路由都是无状态的,并且会应用中间件组。 关于作者 程序开发人员,不拘泥于语言与技术,目前主要从事PHP和前端开发,使用Laravel和VueJs,App端使用Apicloud混合式开发。合适和够用是最完美的追求。 个人网站:http://www.linganm...

    Arno 评论0 收藏0
  • 【日常填坑】之ajax请求laravel的api接口

    摘要:合适和够用是最完美的追求。比如从页面去请求的资源。它允许浏览器向跨源服务器,发出请求,从而克服了只能同源使用的限制。定义在中的路由都是无状态的,并且会应用中间件组。 关于作者 程序开发人员,不拘泥于语言与技术,目前主要从事PHP和前端开发,使用Laravel和VueJs,App端使用Apicloud混合式开发。合适和够用是最完美的追求。 个人网站:http://www.linganm...

    neu 评论0 收藏0
  • 【日常填坑】之ajax请求laravel的api接口

    摘要:合适和够用是最完美的追求。比如从页面去请求的资源。它允许浏览器向跨源服务器,发出请求,从而克服了只能同源使用的限制。定义在中的路由都是无状态的,并且会应用中间件组。 关于作者 程序开发人员,不拘泥于语言与技术,目前主要从事PHP和前端开发,使用Laravel和VueJs,App端使用Apicloud混合式开发。合适和够用是最完美的追求。 个人网站:http://www.linganm...

    fuyi501 评论0 收藏0
  • 「新轮子」PHP CORS (Cross-origin resource sharing),解决 P

    摘要:而我的新轮子也并不是专门解决它的问题的,而是顺便解决而已。概述这个包,支持在所有的项目中使用。一旦出现成员,代表允许全部。列出允许跨域请求的方法列表,默认是代表所有方法。信息地址嗯,新轮子,求一波。 showImg(https://segmentfault.com/img/bV5VxN?w=844&h=656); 是的,可能了解 Laravel 的都知道,在 Laravel 中简单的设...

    lbool 评论0 收藏0

发表评论

0条评论

最新活动
阅读需要支付1元查看
<