preMatchCollection($pathinfo, $routes); $matches = count($matchedRoutes); if ($matches === 0) { return []; } if ($matches === 1) { return $matchedRoutes[0]->getRouteResult(); } usort($matchedRoutes, [$this, 'sortMatchedRoutes']); return array_shift($matchedRoutes)->getRouteResult(); } /** * Tries to match a URL with a set of routes. * Basically all code has been duplicated from `UrlMatcher::matchCollection`, the difference is * it does not just return the first match, but return all possible matches for further reduction. * * @param string $pathinfo The path info to be parsed * @return list */ protected function preMatchCollection(string $pathinfo, SymfonyRouteCollection $routes): array { $matchedRoutes = []; // HEAD and GET are equivalent as per RFC $method = $this->context->getMethod(); if ($method === 'HEAD') { $method = 'GET'; } $supportsTrailingSlash = $method === 'GET' && $this instanceof RedirectableUrlMatcherInterface; $trimmedPathinfo = rtrim($pathinfo, '/') ?: '/'; foreach ($routes as $name => $route) { $compiledRoute = $route->compile(); $staticPrefix = rtrim($compiledRoute->getStaticPrefix(), '/'); $requiredMethods = $route->getMethods(); // check the static prefix of the URL first. Only use the more expensive preg_match when it matches if ($staticPrefix !== '' && !str_starts_with($trimmedPathinfo, $staticPrefix)) { continue; } $regex = $compiledRoute->getRegex(); $pos = strrpos($regex, '$'); $hasTrailingSlash = $regex[$pos - 1] === '/'; $regex = substr_replace($regex, '/?$', $pos - $hasTrailingSlash, 1 + $hasTrailingSlash); if (!preg_match($regex, $pathinfo, $matches)) { continue; } $hasTrailingVar = $trimmedPathinfo !== $pathinfo && preg_match('#\{[\w\x80-\xFF]+\}/?$#', $route->getPath()); if ($hasTrailingVar && ($hasTrailingSlash || (null === $m = $matches[count($compiledRoute->getPathVariables())] ?? null) || '/' !== ($m[-1] ?? '/')) && preg_match($regex, $trimmedPathinfo, $m)) { if ($hasTrailingSlash) { $matches = $m; } else { $hasTrailingVar = false; } } $hostMatches = []; if ($compiledRoute->getHostRegex() && !preg_match($compiledRoute->getHostRegex(), $this->context->getHost(), $hostMatches)) { continue; } $attributes = $this->getAttributes($route, $name, array_replace($matches, $hostMatches)); $status = $this->handleRouteRequirements($pathinfo, $name, $route, $attributes); if ($status[0] === self::REQUIREMENT_MISMATCH) { continue; } if ($pathinfo !== '/' && !$hasTrailingVar && $hasTrailingSlash === ($trimmedPathinfo === $pathinfo)) { if ($supportsTrailingSlash && (!$requiredMethods || in_array('GET', $requiredMethods))) { return $this->allow = $this->allowSchemes = []; } continue; } if ($route->getSchemes() && !$route->hasScheme($this->context->getScheme())) { $this->allowSchemes = array_merge($this->allowSchemes, $route->getSchemes()); continue; } if ($requiredMethods && !in_array($method, $requiredMethods)) { $this->allow = array_merge($this->allow, $requiredMethods); continue; } $matchedRoute = GeneralUtility::makeInstance( MatchedRoute::class, $route, array_replace($attributes, $status[1] ?? []) ); $matchedRoutes[] = $matchedRoute->withPathMatches($matches)->withHostMatches($hostMatches); } return $matchedRoutes; } /** * Sorts the best matching route result to the beginning */ protected function sortMatchedRoutes(MatchedRoute $a, MatchedRoute $b): int { if ($a->getFallbackScore() !== $b->getFallbackScore()) { // sort fallbacks to the end return $a->getFallbackScore() <=> $b->getFallbackScore(); } if ($b->getHostMatchScore() !== $a->getHostMatchScore()) { // sort more specific host matches to the beginning return $b->getHostMatchScore() <=> $a->getHostMatchScore(); } // index `1` refers to the array index containing the corresponding `tail` match // @todo not sure, whether `tail` can be defined generic, it's hard coded in `SiteMatcher` if ($b->getPathMatchScore(1) !== $a->getPathMatchScore(1)) { return $b->getPathMatchScore(1) <=> $a->getPathMatchScore(1); } // fallback for behavior prior to issue #93240, using reverse sorted site identifier // (side note: site identifier did not contain any URL relevant information) return $b->getSiteIdentifier() <=> $a->getSiteIdentifier(); } }