Skip to content

Commit

Permalink
Improved route match performance
Browse files Browse the repository at this point in the history
  • Loading branch information
divineniiquaye committed Apr 13, 2022
1 parent d21da0f commit d5e0aed
Show file tree
Hide file tree
Showing 2 changed files with 84 additions and 102 deletions.
39 changes: 11 additions & 28 deletions src/RouteCompiler.php
Expand Up @@ -116,10 +116,10 @@ public function build(RouteCollection $routes): array

foreach ($routes->getRoutes() as $i => $route) {
[$pathRegex, $hostsRegex, $compiledVars] = $this->compile($route);
$pathRegex = self::resolvePathRegex($pathRegex);
$pathRegex = self::resolveRegex($pathRegex);

if (!empty($hostsRegex)) {
$variables[$i] = [$hostsRegex, []];
$variables[$i] = [self::resolveRegex($hostsRegex), []];
}

if (!empty($compiledVars)) {
Expand Down Expand Up @@ -294,8 +294,14 @@ private static function compilePattern(string $uriPattern, bool $reversed = fals
\preg_match_all(self::COMPILER_REGEX, $uriPattern, $matches, \PREG_SET_ORDER | \PREG_UNMATCHED_AS_NULL);

foreach ($matches as [$placeholder, $varName, $segment, $default]) {
// Filter variable name to meet requirement
self::filterVariableName($varName, $uriPattern);
// A PCRE subpattern name must start with a non-digit.
if (1 === \preg_match('/\d/A', $varName)) {
throw new UriHandlerException(\sprintf('Variable name "%s" cannot start with a digit in route pattern "%s". Use a different name.', $varName, $uriPattern));
}

if (\strlen($varName) > self::VARIABLE_MAXIMUM_LENGTH) {
throw new UriHandlerException(\sprintf('Variable name "%s" cannot be longer than %s characters in route pattern "%s".', $varName, self::VARIABLE_MAXIMUM_LENGTH, $uriPattern));
}

if (\array_key_exists($varName, $variables)) {
throw new UriHandlerException(\sprintf('Route pattern "%s" cannot reference variable name "%s" more than once.', $uriPattern, $varName));
Expand All @@ -308,29 +314,6 @@ private static function compilePattern(string $uriPattern, bool $reversed = fals
return [\strtr($uriPattern, $replaces), $variables];
}

/**
* Filter variable name to meet requirements.
*/
private static function filterVariableName(string $varName, string $pattern): void
{
// A PCRE subpattern name must start with a non-digit. Also a PHP variable cannot start with a digit so the
// variable would not be usable as a Controller action argument.
if (1 === \preg_match('/\d/A', $varName)) {
throw new UriHandlerException(\sprintf('Variable name "%s" cannot start with a digit in route pattern "%s". Use a different name.', $varName, $pattern));
}

if (\strlen($varName) > self::VARIABLE_MAXIMUM_LENGTH) {
throw new UriHandlerException(
\sprintf(
'Variable name "%s" cannot be longer than %s characters in route pattern "%s".',
$varName,
self::VARIABLE_MAXIMUM_LENGTH,
$pattern
)
);
}
}

/**
* Prepares segment pattern with given constrains.
*
Expand All @@ -352,7 +335,7 @@ private static function prepareSegment(string $name, array $requirements): strin
/**
* Strips starting and ending modifiers from a path regex.
*/
private static function resolvePathRegex(string $pathRegex): string
private static function resolveRegex(string $pathRegex): string
{
$pos = (int) \strrpos($pathRegex, '$');
$pathRegex = \substr($pathRegex, 1 + \strpos($pathRegex, '^'), -(\strlen($pathRegex) - $pos));
Expand Down
147 changes: 73 additions & 74 deletions src/RouteMatcher.php
Expand Up @@ -30,9 +30,11 @@
*/
class RouteMatcher implements RouteMatcherInterface
{
private RouteCollection $routes;
private RouteCompilerInterface $compiler;

/** @var RouteCollection|array<int,Route> */
private $routes;

/** @var array<int,mixed> */
private ?array $compiledData = null;

Expand Down Expand Up @@ -60,8 +62,7 @@ public function __serialize(): array
*/
public function __unserialize(array $data): void
{
[$this->compiledData, $routes, $this->compiler] = $data;
$this->routes = RouteCollection::create($routes);
[$this->compiledData, $this->routes, $this->compiler] = $data;
}

/**
Expand Down Expand Up @@ -93,18 +94,18 @@ public function match(string $method, UriInterface $uri): ?Route
public function generateUri(string $routeName, array $parameters = []): GeneratedUri
{
if (null === $optimized = &$this->optimized[$routeName] ?? null) {
foreach ($this->routes->getRoutes() as $offset => $route) {
foreach ($this->getRoutes() as $offset => $route) {
if ($routeName === $route->getName()) {
$optimized = $offset;
goto generate_uri;

return $this->compiler->generateUri($route, $parameters);
}
}

throw new UrlGenerationException(\sprintf('Unable to generate a URL for the named route "%s" as such route does not exist.', $routeName), 404);
}

generate_uri:
return $this->compiler->generateUri($this->routes->getRoutes()[$optimized], $parameters);
return $this->compiler->generateUri($this->getRoutes()[$optimized], $parameters);
}

/**
Expand All @@ -122,55 +123,57 @@ public function getCompiler(): RouteCompilerInterface
*/
public function getRoutes(): array
{
return $this->routes->getRoutes();
if (\is_array($routes = $this->routes)) {
return $routes;
}

return $routes->getRoutes();
}

/**
* Tries to match a route from a set of routes.
*/
protected function matchCollection(string $method, UriInterface $uri, RouteCollection $routes): ?Route
{
$requirements = [[], [], []];
$requestPath = $uri->getPath();
$requirements = [];
$requestPath = \rawurldecode($uri->getPath()) ?: '/';
$requestScheme = $uri->getScheme();

foreach ($routes->getRoutes() as $offset => $route) {
if (!empty($staticPrefix = $route->getStaticPrefix()) && !\str_starts_with($requestPath, $staticPrefix)) {
continue;
}

[$pathRegex, $hostsRegex, $variables] = $this->optimized[$offset] ??= $this->compiler->compile($route);

if (!\preg_match($pathRegex, $requestPath, $matches, \PREG_UNMATCHED_AS_NULL)) {
if (!$route->hasMethod($method)) {
$requirements[0] = \array_merge($requirements[0] ?? [], $route->getMethods());
continue;
}

$hostsVar = [];
$requiredSchemes = $route->getSchemes();

if (!empty($hostsRegex) && !$this->matchHost($hostsRegex, $uri, $hostsVar)) {
$requirements[1][] = $hostsRegex;
if (!$route->hasScheme($requestScheme)) {
$requirements[1] = \array_merge($requirements[1] ?? [], $route->getSchemes());
continue;
}

if (!\in_array($method, $route->getMethods(), true)) {
$requirements[0] = \array_merge($requirements[0], $route->getMethods());
continue;
}
[$pathRegex, $hostsRegex, $variables] = $this->optimized[$offset] ??= $this->compiler->compile($route);
$hostsVar = [];

if ($requiredSchemes && !\in_array($uri->getScheme(), $requiredSchemes, true)) {
$requirements[2] = \array_merge($requirements[2], $route->getSchemes());
if (!\preg_match($pathRegex, $requestPath, $matches, \PREG_UNMATCHED_AS_NULL)) {
continue;
}

if (!empty($variables)) {
$matchInt = 0;
if (empty($hostsRegex) || $this->matchHost($hostsRegex, $uri, $hostsVar)) {
if (!empty($variables)) {
$matchInt = 0;

foreach ($variables as $key => $value) {
$route->argument($key, $matches[++$matchInt] ?? $matches[$key] ?? $hostsVar[$key] ?? $value);
foreach ($variables as $key => $value) {
$route->argument($key, $matches[++$matchInt] ?? $matches[$key] ?? $hostsVar[$key] ?? $value);
}
}

return $route;
}

return $route;
$requirements[2][] = $hostsRegex;
}

return $this->assertMatch($method, $uri, $requirements);
Expand All @@ -181,62 +184,50 @@ protected function matchCollection(string $method, UriInterface $uri, RouteColle
*/
public function matchCached(string $method, UriInterface $uri, array $optimized): ?Route
{
[$requestPath, $matches, $requirements] = [$uri->getPath(), [], [[], [], []]];

if (null !== $handler = $optimized['handler'] ?? null) {
$matchedIds = $handler($method, $uri, $optimized, fn (int $id) => $this->routes->getRoutes()[$id] ?? null);

if (\is_array($matchedIds)) {
goto found_a_route_match;
}

return $matchedIds;
}

[$staticRoutes, $regexList, $variables] = $optimized;
$requestPath = \rawurldecode($uri->getPath()) ?: '/';
$requestScheme = $uri->getScheme();
$requirements = $matches = [];
$index = 0;

if (empty($matchedIds = $staticRoutes[$requestPath] ?? [])) {
if (null === $regexList || !\preg_match($regexList, $requestPath, $matches, \PREG_UNMATCHED_AS_NULL)) {
return null;
}

$matchedIds = [(int) $matches['MARK']];
if (null === $matchedIds = $staticRoutes[$requestPath] ?? ($regexList && \preg_match($regexList, $requestPath, $matches, \PREG_UNMATCHED_AS_NULL) ? [(int) $matches['MARK']] : null)) {
return null;
}

found_a_route_match:
foreach ($matchedIds as $matchedId) {
$requiredSchemes = ($route = $this->routes->getRoutes()[$matchedId])->getSchemes();
do {
$route = $this->routes[$i = $matchedIds[$index]];

if (!\in_array($method, $route->getMethods(), true)) {
$requirements[0] = \array_merge($requirements[0], $route->getMethods());
if (!$route->hasMethod($method)) {
$requirements[0] = \array_merge($requirements[0] ?? [], $route->getMethods());
continue;
}

if ($requiredSchemes && !\in_array($uri->getScheme(), $requiredSchemes, true)) {
$requirements[2] = \array_merge($requirements[2], $route->getSchemes());
if (!$route->hasScheme($requestScheme)) {
$requirements[1] = \array_merge($requirements[1] ?? [], $route->getSchemes());
continue;
}

if (\array_key_exists($matchedId, $variables)) {
[$hostsRegex, $routeVar] = $variables[$matchedId];
$hostsVar = [];
if (!\array_key_exists($i, $variables)) {
return $route;
}

if ($hostsRegex && !$this->matchHost($hostsRegex, $uri, $hostsVar)) {
$requirements[1][] = $hostsRegex;
continue;
}
[$hostsRegex, $routeVar] = $variables[$i];
$hostsVar = [];

if (empty($hostsRegex) || $this->matchHost($hostsRegex, $uri, $hostsVar)) {
if (!empty($routeVar)) {
$matchInt = 0;

foreach ($routeVar as $key => $value) {
$route->argument($key, $matches[++$matchInt] ?? $matches[$key] ?? $hostsVar[$key] ?? $value);
}
}

return $route;
}

return $route;
}
$requirements[2][] = $hostsRegex;
} while (isset($matchedIds[++$index]));

return $this->assertMatch($method, $uri, $requirements);
}
Expand All @@ -245,26 +236,34 @@ protected function matchHost(string $hostsRegex, UriInterface $uri, array &$host
{
$hostAndPost = $uri->getHost() . (null !== $uri->getPort() ? ':' . $uri->getPort() : '');

return (bool) \preg_match($hostsRegex, $hostAndPost, $hostsVar, \PREG_UNMATCHED_AS_NULL);
if ($hostsRegex === $hostAndPost) {
return true;
}

if (!\str_contains($hostsRegex, '^')) {
$hostsRegex = '#^' . $hostsRegex . '$#ui';
}

return 1 === \preg_match($hostsRegex, $hostAndPost, $hostsVar, \PREG_UNMATCHED_AS_NULL);
}

/**
* @param array<int,mixed> $requirements
*/
protected function assertMatch(string $method, UriInterface $uri, array $requirements)
{
[$requiredMethods, $requiredHosts, $requiredSchemes] = $requirements;

if (!empty($requiredMethods)) {
$this->assertMethods($method, $uri->getPath(), $requiredMethods);
}
if (!empty($requirements)) {
if (isset($requirements[0])) {
$this->assertMethods($method, $uri->getPath(), $requirements[0]);
}

if (!empty($requiredSchemes)) {
$this->assertSchemes($uri, $requiredSchemes);
}
if (isset($requirements[1])) {
$this->assertSchemes($uri, $requirements[1]);
}

if (!empty($requiredHosts)) {
$this->assertHosts($uri, $requiredHosts);
if (isset($requirements[2])) {
$this->assertHosts($uri, $requirements[2]);
}
}

return null;
Expand Down

0 comments on commit d5e0aed

Please sign in to comment.