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}):
路由解析主要就是解析路由的主机和路径部分(带参数部分),差别在于分割符不一样。并将解析结果放到 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(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);
}
- / ↩