aboutsummaryrefslogtreecommitdiff
path: root/src/Routing/RouteMatcher.php
diff options
context:
space:
mode:
Diffstat (limited to 'src/Routing/RouteMatcher.php')
-rw-r--r--src/Routing/RouteMatcher.php109
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;
+ }
+}