路由是web服务不可或缺的一部分,一个好的web框架必须具备一整套灵活且丰富的路由系统。Laravel自然也不例外,通过配置文件中一两行代码就可以实现一个具有完整的参数、属性及约束的路由,甚至可以免去写专门的controller。如此强大的功能是如何实现的呢?下面仍然从laravel框架的启动过程出发,探究一下源码中是如何一步步实现路由服务的。
总体上,laravel的路由系统分为两个服务:RouteServiceProvider和RoutingServiceProvider。前者提供路由的配置解析与加载服务,主要由 Illuminate\Routing\Router 、Illuminate\Routing\Route 、Illuminate \Routing\RouteRegistrar这三个类在IOC容器初始化以及内核启动的过程中实现;后者提供请求的url匹配与参数绑定服务,主要由 Illuminate\Routing\RouteCollection、 Illuminate\Routing\Route、 Illuminate\Routing\Router、Symfony\Routing\RouteCompiler和Illuminate\Routing\RouteParameterBinder这几个类在内核处理请求的过程中实现。整个路由服务的框架大致如下:
在两个服务周期中都扮演者重要角色的Router路由器,是在laravel初始化的过程中由RoutingServiceProvider注册到IOC容器中的,注册形式为单例模式。laravel为何要把整个系统的路由服务分为RouteService和RoutingService两个部分呢?我的理解是为了便于更好的区分其作用或者说生命周期。我们在实际开发过程中,往往根据需求不同会隔离用户的使用场景,典型的例子就是CMS程序的管理端和用户端。这里可以做个类比,RouteService是路由服务的管理端,而RoutingService即是路由服务的用户端。在设计层面就把两者很好的区分开来,有助于我们在进一步扩展路由服务功能或使用路由服务进行业务开发的过程中,明确组件分工,写出高内聚的代码。
定义一条最基本的路由规则的语法很简单,调用Facade门面Route类的某个静态方法即可(本质上是调用了已经注册在服务容器中的路由器router实例api,不清楚Facade基本原理的同学可以看这里)。该静态方法对应于Reques请求的请求方式(GET/POST/HEAD/PUT/PUT/DELETE/OPTIONS),传入的参数为请求url及对应动作(一般是controller@method形式,也可是个闭包函数); 也可以在请求方式前添加一些路由的属性如domain\prefix\middleware等,称为前置属性;还可以在请求方式之后添加一些路由约束where或者属性name等。当然也可以在url中传入请求参数。如下是一些路由定义的例子:
//仅包含基础动作的路由
Route::get('foo','controller@method');
//添加前置属性的路由
Route::middleware('web')->namespace($this->namespace)->post('/foo/{id}', function ($id) { // });
//添加前置属性和后置约束的完整路由
Route::domain('route.domain.name')->get('foo','controller@method')->where('one','(.+)');
此外,可以用路由组的形式定义多条路由,路由组内共享路由属性,甚至还可嵌套新的路由组。实际上,所有 laravel 路由都定义在位于 routes 目录下的路由文件中,这些文件内的路由被laravel视为一个大的路由组,在RouteService启动的过程中通过Route门面加载出来(所以路由配置文件不需要声明对Route门面的引用):
class RouteServiceProvider
{
protected $namespace = 'App\Http\Controllers';
protected function mapWebRoutes()
{
Route::middleware('web')
->namespace($this->namespace)
->group(base_path('routes/web.php'));
}
}
那么RouteServiceProvider是在何时启动又如何加载路由缓存的呢?这里我们先放一下,来看看一条路由规则是如何被Router路由器解析的。
所谓路由解析,就是将路由定义中的一系列属性(包括约束和动作)等按一定规则解析并缓存起来,以待后用。这里的解析主要由前面提到的三个类负责,即Illuminate\Routing\Router 、Illuminate\Routing\Route 、Illuminate \Routing\RouteRegistrar。RouteRegistrar 主要负责位于group 、method 这些函数之前的属性注册,Route主要负责位于group 、method这些函数之后的属性注册,而Router则是解析过程中一个中转,将domain、prefix这些熟悉的注册处理转交给RouteRegistrar,并在自身处理method之后返回生成的路由实例Route,将where、name等约束的处理交给Route进行。路由解析的过程如下:
class Router implements RegistrarContract, BindingRegistrar
{
public function __call($method, $parameters)
{
if (static::hasMacro($method)) {
return $this->macroCall($method, $parameters);
}
return (new RouteRegistrar($this))->attribute($method, $parameters[0]);
}
}
class RouteRegistrar
{
protected $attributes = [];
protected $passthru = ['get', 'post', 'put', 'patch', 'delete', 'options', 'any',];
protected $allowedAttributes = ['as', 'domain', 'middleware', 'name', 'namespace', 'prefix',];
public function __call($method, $parameters)
{
if (in_array($method, $this->passthru)) {
return $this->registerRoute($method, ...$parameters);
}
if (in_array($method, $this->allowedAttributes)) {
return $this->attribute($method, $parameters[0]);
}
throw new BadMethodCallException("Method [{$method}] does not exist.");
}
public function attribute($key, $value)
{
if (! in_array($key, $this->allowedAttributes)) {
throw new InvalidArgumentException("Attribute [{$key}] does not exist.");
}
$this->attributes[array_get($this->aliases, $key, $key)] = $value;
return $this;
}
protected function registerRoute($method, $uri, $action = null)
{
if (! is_array($action)) {
$action = array_merge($this->attributes, $action ? ['uses' => $action] : []);
}
return $this->router->{$method}($uri, $this->compileAction($action));
}
class Router implements RegistrarContract, BindingRegistrar
{
protected $routes;
public function get($uri, $action = null)
{
return $this->addRoute(['GET', 'HEAD'], $uri, $action);
}
protected function addRoute($methods, $uri, $action)
{
return $this->routes->add($this->createRoute($methods, $uri, $action));
}
protected function createRoute($methods, $uri, $action)
{
if ($this->actionReferencesController($action)) {
$action = $this->convertToControllerAction($action);
}
$route = $this->newRoute(
$methods, $this->prefix($uri), $action
);
if ($this->hasGroupStack()) {
$this->mergeGroupAttributesIntoRoute($route);
}
$this->addWhereClausesToRoute($route);
return $route;
}
}
class RouteCollection implements Countable, IteratorAggregate
{
protected $routes = [];
protected $allRoutes = [];
protected $nameList = [];
protected $actionList = [];
public function add(Route $route)
{
$this->addToCollections($route);
$this->addLookups($route);
return $route;
}
protected function addToCollections($route)
{
$domainAndUri = $route->domain().$route->uri();
foreach ($route->methods() as $method) {
$this->routes[$method][$domainAndUri] = $route;
}
$this->allRoutes[$method.$domainAndUri] = $route;
}
protected function addLookups($route)
{
$action = $route->getAction();
if (isset($action['as'])) {
$this->nameList[$action['as']] = $route;
}
if (isset($action['controller'])) {
$this->addToActionList($action, $route);
}
}
protected function addToActionList($action, $route)
{
$this->actionList[trim($action['controller'], '\\')] = $route;
}
class Route
{
public $action;
public function __construct($methods, $uri, $action)
{
$this->uri = $uri;
$this->methods = (array) $methods;
$this->action = $this->parseAction($action);
if (in_array('GET', $this->methods) && ! in_array('HEAD', $this->methods)) {
$this->methods[] = 'HEAD';
}
if (isset($this->action['prefix'])) {
$this->prefix($this->action['prefix']);
}
}
protected function parseAction($action)
{
return RouteAction::parse($this->uri, $action);
}
}
现在让我们再回过头来看看,RouteServiceProvider是在什么时候启动map()进行路由解析的呢?当系统内核Kernel初始化结束后,就会调用 handle 函数,这个函数用于 laravel 各个功能服务的注册启动,还有request 的分发:
class Kernel implements KernelContract
{
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());
}
public function bootstrap()
{
if (! $this->app->hasBeenBootstrapped()) {
$this->app->bootstrapWith($this->bootstrappers());
}
}
}
这里boostrap()函数会调用一系列系统的启动器Bootstrapper进行系统基础服务的启动,这些基础服务就配置在config/service.php中,当然其中也包括RouteService。那么路由的解析需要每次启动服务的时候都进行吗?答案当然是否定的。因为对于开发者来说,route文件的配置其实是很少改动的,因此laravel在这里使用了静态文件缓存将解析好的路由规则缓存起来,缓存路径为/bootstrap/cache/routes.php。这样当每次需要加载路由的时候,先在缓存路径下查询解析好的静态路由文件,如果找到的话就直接加载;如果没有找到静态文件,就进行routes/web.php文件的动态解析并保存。
class RouteServiceProvider extends ServiceProvider
{
public function boot()
{
$this->setRootControllerNamespace();
if ($this->app->routesAreCached()) {
$this->loadCachedRoutes();
} else {
$this->loadRoutes();
$this->app->booted(function () {
$this->app['router']->getRoutes()->refreshNameLookups();
$this->app['router']->getRoutes()->refreshActionLookups();
});
}
}
protected function loadCachedRoutes()
{
$this->app->booted(function () {
require $this->app->getCachedRoutesPath();
});
}
protected function loadRoutes()
{
if (method_exists($this, 'map')) {
$this->app->call([$this, 'map']);
}
}
接下来就是路由与请求的匹配问题了。既然是请求的匹配,那么运行肯定也是在内核处理请求的过程中:
class Kernel implements KernelContract
{
protected function dispatchToRouter()
{
return function ($request) {
$this->app->instance('request', $request);
return $this->router->dispatch($request);
};
}
}
这里实际上是在Router的dispatch()方法中进行的,过程大致为:
class Router implements RegistrarContract, BindingRegistrar
{
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;
}
可以看到在findRoute()函数中寻找路由的任务主要由RouteCollection负责,这个集合提供一个match()函数负责匹配路由。在这个match()函数中,laravel先查找当前请求方式下存储的所有路由(前面按请求方式作为索引存储的数组还记得不?这里派上用场了),然后遍历这个集合,调用每个route的matches()接口,找到第一个返回true(即匹配)的路由就返回,并且把url中的请求参数保存到路由中。如果未在指定方法下找到route匹配,则遍历其它方法下的路由集合进行匹配,并将所有匹配的路由的对应methods记录,然后判断请求方式是否为OPTIONS:
class RouteCollection implements Countable, IteratorAggregate
{
public function match(Request $request)
{
$routes = $this->get($request->getMethod());
$route = $this->matchAgainstRoutes($routes, $request);
if (! is_null($route)) {
return $route->bind($request);
}
$others = $this->checkForAlternateVerbs($request);
if (count($others) > 0) {
return $this->getRouteForMethods($request, $others);
}
throw new NotFoundHttpException;
}
protected function matchAgainstRoutes(array $routes, $request, $includingMethod = true)
{
return Arr::first($routes, function ($value) use ($request, $includingMethod) {
return $value->matches($request, $includingMethod);
});
}
protected function checkForAlternateVerbs($request)
{
$methods = array_diff(Router::$verbs, [$request->getMethod()]);
$others = [];
foreach ($methods as $method) {
if (! is_null($this->matchAgainstRoutes($this->get($method), $request, false))) {
$others[] = $method;
}
}
return $others;
}
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);
}
那么一个Route实例具体是如何判断一个请求request实例与自己匹配的呢?接下来就是我们今天的主角——正则表达式大显身手的时候了! laravel 首先对路由进行正则编译,得到路由的正则匹配串regex,然后利用请求的参数url尝试去匹配,如果匹配成功,那么就会选定该路由:
class Route
{
public function matches(Request $request, $includingMethod = true)
{
$this->compileRoute(); //正则编译
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,
];
}
每个路由最终都分别调用了UrlValidator、MethodValidator、SchemaValidaor、HostValidator四个验证器对请求的参数进行了校验,除了请求方式外其余三个都必须匹配才算匹配成功。所谓校验,其实就是直接从request对象中获取相应参数进行判断。Laravel对于url和host的校验如下:
class UriValidator implements ValidatorInterface
{
public function matches(Route $route, Request $request)
{
$path = $request->path() == '/' ? '/' : '/'.$request->path();
return preg_match($route->getCompiled()->getRegex(), rawurldecode($path));
}
}
class HostValidator implements ValidatorInterface
{
public function matches(Route $route, Request $request)
{
if (is_null($route->getCompiled()->getHostRegex())) {
return true;
}
return preg_match($route->getCompiled()->getHostRegex(), $request->getHost());
}
}
就是一个正则匹配preg_match()搞定!所以问题关键在于进行正则匹配的regex是如何获得的。这里laravel发挥了不重复造轮子的精神,重用了Symfony库的RouteCompiler组件进行正则编译。
class Route
{
protected function compileRoute()
{
if (! $this->compiled) {
$this->compiled = (new RouteCompiler($this))->compile();
}
return $this->compiled;
}
public function getCompiled()
{
return $this->compiled;
}
}
class RouteCompiler
{
public function compile()
{
$optionals = $this->getOptionalParameters();
$uri = preg_replace('/\{(\w+?)\?\}/', '{$1}', $this->route->uri());
return (
new Symfony\Component\Routing\Route($uri, $optionals, $this->route->wheres, [], $this->route->domain() ?: '')
)->compile();
}
}
需要注意的是,在调用symfony的路由编译之前laravel自身的RouteCompiler先进行了一些特殊的正则处理,这是因为路由的url规则中可能还有形如
这一类的可选参数,但是对于 symfony 来说,'? '没有任何特殊意义,因此 laravel 需要把表示可选参数提取出来,另外传递给 SymfonyRoute 构造函数。在getOptionalParameters()里面的preg_match_all('/\{(\w+?)\?\}/', $this->route->uri(), $matches)这句话作用是把可选参数名值提取出来,并通过array_fill_keys()处理得到如下的命名数组:
而compile()中的preg_replace('/\{(\w+?)\?\}/', '{$1}', $this->route->uri())这句话的作用就是把可选参数中的‘?'符合给去掉,得到正常的带参数url
接下来就是symfony的RouteCompiler类的编译过程了。
class RouteCompiler implements RouteCompilerInterface
{
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'];
return new CompiledRoute(
$staticPrefix,
$regex,
$tokens,
$pathVariables,
$hostRegex,
$hostTokens,
$hostVariables,
array_unique($variables)
);
}
}
路由的正则编译由两个部分构成:主域的正则编译与 uri 的正则编译。这两个部分的编译功能由函数compilePattern 负责Host和path的匹配结果最终合并放入CompiledRoute对象中。所以整个正则编译的核心就在copilePattern()中:
class RouteCompiler implements RouteCompilerInterface
{
const REGEX_DELIMITER = '#';
const SEPARATORS = '/,;.:-_~+*=@|';
const VARIABLE_MAXIMUM_LENGTH = 32;
private static function compilePattern(Route $route, $pattern, $isHost)
{
$tokens = array();
$variables = array();
$matches = array();
$pos = 0;
$defaultSeparator = $isHost ? '.' : '/';
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 ($isSeparator && $precedingText !== $precedingChar) {
$tokens[] = array('text', substr($precedingText, 0, -strlen($precedingChar)));
} elseif (!$isSeparator && strlen($precedingText) > 0) {
$tokens[] = array('text', $precedingText);
}
$regexp = $route->getRequirement($varName);
if (null === $regexp) {
$followingPattern = (string) substr($pattern, $pos);
$nextSeparator = self::findNextSeparator($followingPattern, $useUtf8);
$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 .= '+';
}
}
$tokens[] = array('variable', $isSeparator ? $precedingChar : '', $regexp, $varName);
$variables[] = $varName;
}
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);
}
$regexp = self::REGEX_DELIMITER.'^'.$regexp.'$'.self::REGEX_DELIMITER.'sD'.($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' => self::determineStaticPrefix($route, $tokens),
'regex' => $regexp,
'tokens' => array_reverse($tokens),
'variables' => $variables,
);
}
private static function determineStaticPrefix(Route $route, array $tokens)
{
if ('text' !== $tokens[0][0]) {
return ($route->hasDefault($tokens[0][3]) || '/' === $tokens[0][1]) ? '' : $tokens[0][1];
}
$prefix = $tokens[0][1];
if (isset($tokens[1][1]) && '/' !== $tokens[1][1] && false === $route->hasDefault($tokens[1][3])) {
$prefix .= $tokens[1][1];
}
return $prefix;
}
private static function findNextSeparator($pattern, $useUtf8)
{
if ('' == $pattern) {
return '';
}
if ('' === $pattern = preg_replace('#\{\w+\}#', '', $pattern)) {
return '';
}
if ($useUtf8) {
preg_match('/^./u', $pattern, $pattern);
}
return false !== strpos(static::SEPARATORS, $pattern[0]) ? $pattern[0] : '';
}
}
这里虽然代码很多,但核心还是一条正则匹配:preg_match_all('#\{\w+\}#', $pattern, $matches, PREG_OFF SET_CAPTURE | PREG_SET_ORDER)。仔细研究一下这条语句,发现采用了PREG_SET_ORDER模式得到的是一个子匹配结果的顺序索引数组(便于接下来的遍历)。同时设置PREG_OFF_SET_CAPTURE标志以便于在匹配中定位字符串位置($pos = $match[0][1] + strlen($match[0][0])
)。此外,这里采用正则表达式采用‘#’作为分割符是为了和uri中的‘/’区分开来。清楚了这句话的作用,就可以根据上一步compile()函数中的思路,大致梳理一下compilePattern()的编译过程了:
此外,代码中还有一些关于字符编码的特殊处理,这里就不再赘述了。这里以路由‘prefix/{foo}/{baz?}.{ext?}/tail’为例,得到的最终tokens如下:
array(
'staticPrefix' => ‘prefix',
'regex' => '#^/prefix/(?P[^/]++)?(?:/(?P[^/\.]++)(?:\.(?P[^/]++))?)?/tai l$#s', //下一步再解释
'tokens' => array(
array('text', '/tail/'),
array('variable', '.', '[^\.\/]++', 'ext'),
array('variable', '/', '[^\/\.]+', 'bar'),
array('variable', '/', '[^\/]+', 'foo'),
array('text', 'prefix')
),
'variables' => array('foo', 'bar', 'ext')
);
最后看看computeRegexp()中tokens是如何被拼接的:
class RouteCompiler implements RouteCompilerInterface
{
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) {
// Close the optional subpatterns
$regexp .= str_repeat(')?', $nbTokens - $firstOptional - (0 === $firstOptional ? 1 : 0));
}
}
return $regexp;
}
}
}
这里有两个关键点。首先,拼接出的regex采用了子命名组语法,即(?P<参数>表达式)的形式。这里是为了后面与请求url进行参数绑定的时候方便取出变量名和变量值。其次,这里用到了上一步获取的第一个可选参数位置,因为在子命名组语法中规定:
通过第一个位置和遍历位置的计算,可以拼接出符合上述规则的正则表达式。
最后,还要添加开始符^,结束符$、最两侧分隔符#、单行修正符s,如果是主域的则表达式,还要添加不区分大 小写的修正符i。这里仍然以路由‘prefix/{foo}/{baz?}.{ext?}/tail’为例,其拼接过程如下:
/prefix /prefix/(?P[^/]++)
/prefix/(?P[^/]++)?(?:/(?P[^/\.]++)
/prefix/(?P[^/]++)?(?:/(?P[^/\.]++)(?:\.(?P[^/]++))?)?
/prefix/(?P[^/]++)?(?:/(?P[^/\.]++)(?:\.(?P[^/]++))?)?/tail
#^/prefix/(?P[^/]++)?(?:/(?P[^/\.]++)(?:\.(?P[^/]++))?)?/tai l$#s
得到一个路由的正则表达式regex之后,laravel就可以后续处理请求的时候使用它了:一是用来匹配url,二是用来获取url参数。前者我们已经在前面的步骤讲过,而后者的核心原理也大同小异。这里laravel采用RouteParameterBinder负责路由的参数绑定:
class RouteParameterBinder
{
protected $route;
public function parameters($request)
{
if (! is_null($this->route->compiled->getHostRegex())) {
$parameters = $this->bindHostParameters(
$request, $parameters
);
}
return $this->replaceDefaults($parameters);
}
protected function bindPathParameters($request)
{
$path = '/'.ltrim($request->decodedPath(), '/');
preg_match($this->route->compiled->getRegex(), $path, $matches);
return $this->matchToKeys(array_slice($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 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;
});
}
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;
}
}
绑定过程如下:
写到这里,大家应该都比较清楚laravel路由系统的工作原理了吧(可能对自己的讲解水平有地蜜汁自信了><)。概括一下本次的收获:路由系统的核心,其实就是url这个特殊的字符串的处理,而其中的关键问题是如何同时处理字符串的匹配和参数提取。如果今后遇到类似的问题,我们应该自然地想到程序员手中的这把尚方宝剑(其实是把双刃剑)——正则表达式,去斩杀字符串这个我们永远的共同敌人!
原创声明:本文系作者授权腾讯云开发者社区发表,未经许可,不得转载。
如有侵权,请联系 cloudcommunity@tencent.com 删除。
原创声明:本文系作者授权腾讯云开发者社区发表,未经许可,不得转载。
如有侵权,请联系 cloudcommunity@tencent.com 删除。