diff options
Diffstat (limited to 'src/Routing/RouteMatcher.php')
| -rw-r--r-- | src/Routing/RouteMatcher.php | 109 |
1 files changed, 109 insertions, 0 deletions
diff --git a/src/Routing/RouteMatcher.php b/src/Routing/RouteMatcher.php new file mode 100644 index 0000000..f5b725f --- /dev/null +++ b/src/Routing/RouteMatcher.php @@ -0,0 +1,109 @@ +<?php + +declare(strict_types=1); + +namespace BrettParson\MiniRoute\Routing; + +final class RouteMatcher +{ + public function matches(RouteDef $route, string $method, string $path): bool + { + if ($route->method !== strtoupper($method)) { + return false; + } + + return preg_match($this->compile($route->path), $path) === 1; + } + + /** + * Extract named route parameters from a matched path. + * + * @return array<string, string> + */ + public function extractParams(RouteDef $route, string $path): array + { + if (preg_match($this->compile($route->path), $path, $matches) !== 1) { + return []; + } + + $params = []; + + foreach ($matches as $key => $value) { + if (is_string($key)) { + $params[$key] = $value; + } + } + + return $params; + } + + /** + * Order two routes so the more specific one sorts first. + * + * Segments are compared left to right; a literal segment beats a + * {parameter} segment at the same position. So /entries/archive sorts + * before /entries/{slug} and /admin/entries/preview before + * /admin/entries/{id}, independent of registration order. + */ + public function compare(RouteDef $a, RouteDef $b): int + { + $segmentsA = $this->segments($a->path); + $segmentsB = $this->segments($b->path); + + $length = max(count($segmentsA), count($segmentsB)); + + for ($i = 0; $i < $length; $i++) { + $rankA = $this->rank($segmentsA[$i] ?? null); + $rankB = $this->rank($segmentsB[$i] ?? null); + + if ($rankA !== $rankB) { + return $rankA <=> $rankB; + } + } + + return 0; + } + + private function compile(string $path): string + { + $escaped = preg_quote($path, '#'); + + $pattern = preg_replace( + '#\\\\\{([a-zA-Z_]+)\\\\\}#', + '(?P<$1>[^/]+)', + $escaped, + ); + + return '#^' . $pattern . '$#'; + } + + /** + * @return list<string> + */ + private function segments(string $path): array + { + $trimmed = trim($path, '/'); + + if ($trimmed === '') { + return []; + } + + return explode('/', $trimmed); + } + + /** + * Lower rank = more specific: literal (0) < parameter (1) < missing (2). + */ + private function rank(?string $segment): int + { + if ($segment === null) { + return 2; + } + + if (str_starts_with($segment, '{') && str_ends_with($segment, '}')) { + return 1; + } + + return 0; + } +} |
