aboutsummaryrefslogtreecommitdiff
path: root/src/Routing/RouteMatcher.php
blob: f5b725f68b73ff95b680d1b470655a0e2de0cc72 (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
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;
    }
}