Laravel 路由处理

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 Events\RouteMatched($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 的路由形式,再委托 Symfony\\Component\\Routing\\RouteCompiler 处理
    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' => 'Symfony\\Component\\Routing\\RouteCompiler',] 和 $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');
    // 委托 Symfony\\Component\\Routing\\RouteCompiler 来处理,返回 \Symfony\Component\Routing\CompiledRoute 对象
    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]|(?<!\\\\)\\\\(?:\\\\\\\\)*+(?-i:X|[pP][\{CLMNPSZ]|x\{[A-Fa-f0-9]{3})/', $regexp)) {
                $needsUtf8 = true;
                @trigger_error(sprintf('Using UTF-8 route requirements 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 variable "%s" in pattern "%s".', $varName, $pattern), E_USER_DEPRECATED);
            }
            if (!$useUtf8 && $needsUtf8) {
                throw new \LogicException(sprintf('Cannot mix UTF-8 requirement with non-UTF-8 charset for variable "%s" in pattern "%s".', $varName, $pattern));
            }
        }
        // $tokens[] = ['variable', '/', '[^\/]+', 'post']
        $tokens[] = array('variable', $isSeparator ? $precedingChar : '', $regexp, $varName);
        // $variables[] = 'post'
        $variables[] = $varName;
    }
    // 循环过后的结果类似:
    // $tokens = [['text', '/posts',],['variable', '/', '[^\/]+', 'post'],['text', '/comments',],['variable', '/', '[^\/]+', 'comment'],]
    // $variables = ['post', 'comment',]
    
    if ($pos < strlen($pattern)) {
        $tokens[] = array('text', substr($pattern, $pos));  // 将剩下的部分当做是文本形式处理,构造正则时,直接转义输出
    }
    // 取第一个可选项位置,此位置表明后面的所有参数都需要以可选的方式处理,即对应的正则都是零宽可选形式
    $firstOptional = PHP_INT_MAX;
    if (!$isHost) {
        for ($i = count($tokens) - 1; $i >= 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<post>[^\/]+)\/comments\/(?P<comment>[^\/]+)$#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}):

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

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

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

  4. 最后返回一个数组,表明此路由对应的静态前缀、正则表达式、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(Illuminate\Support\Collection),$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);
}
  1. /
    原文作者:TylerZou
    原文地址: https://segmentfault.com/a/1190000008901379
    本文转自网络文章,转载此文章仅为分享知识,如有侵权,请联系博主进行删除。
点赞