Code Coverage |
||||||||||||||||
Lines |
Branches |
Paths |
Functions and Methods |
Classes and Traits |
||||||||||||
| Total | |
100.00% |
52 / 52 |
|
93.33% |
56 / 60 |
|
47.14% |
33 / 70 |
|
90.91% |
20 / 22 |
CRAP | |
0.00% |
0 / 1 |
| Route | |
100.00% |
52 / 52 |
|
93.33% |
56 / 60 |
|
47.14% |
33 / 70 |
|
100.00% |
22 / 22 |
215.90 | |
100.00% |
1 / 1 |
| __construct | |
100.00% |
3 / 3 |
|
100.00% |
4 / 4 |
|
100.00% |
2 / 2 |
|
100.00% |
1 / 1 |
2 | |||
| any | |
100.00% |
1 / 1 |
|
100.00% |
1 / 1 |
|
100.00% |
1 / 1 |
|
100.00% |
1 / 1 |
1 | |||
| map | |
100.00% |
3 / 3 |
|
100.00% |
3 / 3 |
|
100.00% |
2 / 2 |
|
100.00% |
1 / 1 |
2 | |||
| get | |
100.00% |
1 / 1 |
|
100.00% |
1 / 1 |
|
100.00% |
1 / 1 |
|
100.00% |
1 / 1 |
1 | |||
| post | |
100.00% |
1 / 1 |
|
100.00% |
1 / 1 |
|
100.00% |
1 / 1 |
|
100.00% |
1 / 1 |
1 | |||
| put | |
100.00% |
1 / 1 |
|
100.00% |
1 / 1 |
|
100.00% |
1 / 1 |
|
100.00% |
1 / 1 |
1 | |||
| patch | |
100.00% |
1 / 1 |
|
100.00% |
1 / 1 |
|
100.00% |
1 / 1 |
|
100.00% |
1 / 1 |
1 | |||
| delete | |
100.00% |
1 / 1 |
|
100.00% |
1 / 1 |
|
100.00% |
1 / 1 |
|
100.00% |
1 / 1 |
1 | |||
| head | |
100.00% |
1 / 1 |
|
100.00% |
1 / 1 |
|
100.00% |
1 / 1 |
|
100.00% |
1 / 1 |
1 | |||
| options | |
100.00% |
1 / 1 |
|
100.00% |
1 / 1 |
|
100.00% |
1 / 1 |
|
100.00% |
1 / 1 |
1 | |||
| withMethods | |
100.00% |
3 / 3 |
|
100.00% |
1 / 1 |
|
100.00% |
1 / 1 |
|
100.00% |
1 / 1 |
1 | |||
| methods | |
100.00% |
1 / 1 |
|
100.00% |
1 / 1 |
|
100.00% |
1 / 1 |
|
100.00% |
1 / 1 |
1 | |||
| prefix | |
100.00% |
6 / 6 |
|
100.00% |
5 / 5 |
|
75.00% |
3 / 4 |
|
100.00% |
1 / 1 |
3.14 | |||
| controller | |
100.00% |
6 / 6 |
|
100.00% |
3 / 3 |
|
100.00% |
2 / 2 |
|
100.00% |
1 / 1 |
2 | |||
| name | |
100.00% |
1 / 1 |
|
100.00% |
1 / 1 |
|
100.00% |
1 / 1 |
|
100.00% |
1 / 1 |
1 | |||
| url | |
100.00% |
1 / 1 |
|
100.00% |
1 / 1 |
|
100.00% |
1 / 1 |
|
100.00% |
1 / 1 |
1 | |||
| view | |
100.00% |
1 / 1 |
|
100.00% |
1 / 1 |
|
100.00% |
1 / 1 |
|
100.00% |
1 / 1 |
1 | |||
| pattern | |
100.00% |
1 / 1 |
|
100.00% |
1 / 1 |
|
100.00% |
1 / 1 |
|
100.00% |
1 / 1 |
1 | |||
| match | |
100.00% |
4 / 4 |
|
100.00% |
3 / 3 |
|
100.00% |
2 / 2 |
|
100.00% |
1 / 1 |
2 | |||
| routePattern | |
100.00% |
1 / 1 |
|
100.00% |
1 / 1 |
|
100.00% |
1 / 1 |
|
100.00% |
1 / 1 |
1 | |||
| pathWithoutPrefix | |
100.00% |
12 / 12 |
|
85.00% |
17 / 20 |
|
12.50% |
5 / 40 |
|
100.00% |
1 / 1 |
39.83 | |||
| normalizePrefix | |
100.00% |
1 / 1 |
|
85.71% |
6 / 7 |
|
66.67% |
2 / 3 |
|
100.00% |
1 / 1 |
2.15 | |||
| 1 | <?php |
| 2 | |
| 3 | declare(strict_types=1); |
| 4 | |
| 5 | namespace Celemas\Router; |
| 6 | |
| 7 | use Celemas\Router\Exception\ValueError; |
| 8 | use Closure; |
| 9 | |
| 10 | /** |
| 11 | * @psalm-api |
| 12 | * |
| 13 | * @psalm-type View = callable|list{string, string}|non-empty-string |
| 14 | */ |
| 15 | class Route |
| 16 | { |
| 17 | use AddsBeforeAfter; |
| 18 | use AddsMiddleware; |
| 19 | |
| 20 | /** @var null|list<string> */ |
| 21 | protected ?array $methods = null; |
| 22 | |
| 23 | /** @var Closure|list{string, string}|string */ |
| 24 | protected Closure|array|string $view; |
| 25 | |
| 26 | protected ?RoutePattern $routePattern = null; |
| 27 | |
| 28 | /** |
| 29 | * @param string $pattern The URL pattern of the route |
| 30 | * |
| 31 | * @param callable|list{string, string}|non-empty-string $view The callable view. Can be a closure, an invokable object or any other callable |
| 32 | * |
| 33 | * @param string $name The name of the route. If not given the pattern will be hashed and used as name. |
| 34 | */ |
| 35 | public function __construct( |
| 36 | protected string $pattern, |
| 37 | callable|array|string $view, |
| 38 | protected string $name = '', |
| 39 | ) { |
| 40 | if (is_callable($view)) { |
| 41 | $this->view = Closure::fromCallable($view); |
| 42 | } else { |
| 43 | $this->view = $view; |
| 44 | } |
| 45 | } |
| 46 | |
| 47 | /** @param callable|list{string, string}|non-empty-string $view */ |
| 48 | public static function any(string $pattern, callable|array|string $view, string $name = ''): self |
| 49 | { |
| 50 | return new self($pattern, $view, $name); |
| 51 | } |
| 52 | |
| 53 | /** |
| 54 | * @param array<array-key, string> $methods |
| 55 | * @param callable|list{string, string}|non-empty-string $view |
| 56 | */ |
| 57 | public static function map( |
| 58 | array $methods, |
| 59 | string $pattern, |
| 60 | callable|array|string $view, |
| 61 | string $name = '', |
| 62 | ): self { |
| 63 | if ($methods === []) { |
| 64 | throw new ValueError('Route method list cannot be empty.'); |
| 65 | } |
| 66 | |
| 67 | return new self($pattern, $view, $name)->withMethods(...array_values($methods)); |
| 68 | } |
| 69 | |
| 70 | /** @param callable|list{string, string}|non-empty-string $view */ |
| 71 | public static function get(string $pattern, callable|array|string $view, string $name = ''): self |
| 72 | { |
| 73 | return new self($pattern, $view, $name)->withMethods('GET'); |
| 74 | } |
| 75 | |
| 76 | /** @param callable|list{string, string}|non-empty-string $view */ |
| 77 | public static function post(string $pattern, callable|array|string $view, string $name = ''): self |
| 78 | { |
| 79 | return new self($pattern, $view, $name)->withMethods('POST'); |
| 80 | } |
| 81 | |
| 82 | /** @param callable|list{string, string}|non-empty-string $view */ |
| 83 | public static function put(string $pattern, callable|array|string $view, string $name = ''): self |
| 84 | { |
| 85 | return new self($pattern, $view, $name)->withMethods('PUT'); |
| 86 | } |
| 87 | |
| 88 | /** @param callable|list{string, string}|non-empty-string $view */ |
| 89 | public static function patch(string $pattern, callable|array|string $view, string $name = ''): self |
| 90 | { |
| 91 | return new self($pattern, $view, $name)->withMethods('PATCH'); |
| 92 | } |
| 93 | |
| 94 | /** @param callable|list{string, string}|non-empty-string $view */ |
| 95 | public static function delete( |
| 96 | string $pattern, |
| 97 | callable|array|string $view, |
| 98 | string $name = '', |
| 99 | ): self { |
| 100 | return new self($pattern, $view, $name)->withMethods('DELETE'); |
| 101 | } |
| 102 | |
| 103 | /** @param callable|list{string, string}|non-empty-string $view */ |
| 104 | public static function head(string $pattern, callable|array|string $view, string $name = ''): self |
| 105 | { |
| 106 | return new self($pattern, $view, $name)->withMethods('HEAD'); |
| 107 | } |
| 108 | |
| 109 | /** @param callable|list{string, string}|non-empty-string $view */ |
| 110 | public static function options( |
| 111 | string $pattern, |
| 112 | callable|array|string $view, |
| 113 | string $name = '', |
| 114 | ): self { |
| 115 | return new self($pattern, $view, $name)->withMethods('OPTIONS'); |
| 116 | } |
| 117 | |
| 118 | private function withMethods(string ...$args): self |
| 119 | { |
| 120 | /** @var list<string> $methods */ |
| 121 | $methods = array_map(static fn(string $method): string => strtoupper($method), $args); |
| 122 | $this->methods = [...($this->methods ?? []), ...$methods]; |
| 123 | |
| 124 | return $this; |
| 125 | } |
| 126 | |
| 127 | /** @return list<string> */ |
| 128 | public function methods(): array |
| 129 | { |
| 130 | return $this->methods ?? []; |
| 131 | } |
| 132 | |
| 133 | /** @internal */ |
| 134 | public function prefix(string $pattern = '', string $name = ''): static |
| 135 | { |
| 136 | if ($pattern !== '') { |
| 137 | $this->pattern = $pattern . $this->pattern; |
| 138 | $this->routePattern = null; |
| 139 | } |
| 140 | |
| 141 | if ($name !== '') { |
| 142 | $this->name = $name . $this->name; |
| 143 | } |
| 144 | |
| 145 | return $this; |
| 146 | } |
| 147 | |
| 148 | /** @internal */ |
| 149 | public function controller(string $controller): static |
| 150 | { |
| 151 | if (is_string($this->view)) { |
| 152 | $this->view = [$controller, $this->view]; |
| 153 | |
| 154 | return $this; |
| 155 | } |
| 156 | |
| 157 | throw new ValueError( |
| 158 | 'Cannot add controller to route action. Controller groups require bare method names.', |
| 159 | ); |
| 160 | } |
| 161 | |
| 162 | public function name(): string |
| 163 | { |
| 164 | return $this->name; |
| 165 | } |
| 166 | |
| 167 | /** @param array<array-key, mixed> $params */ |
| 168 | public function url(array $params = []): string |
| 169 | { |
| 170 | return $this->routePattern()->generate($params); |
| 171 | } |
| 172 | |
| 173 | /** @return Closure|list{string, string}|string */ |
| 174 | public function view(): Closure|array|string |
| 175 | { |
| 176 | return $this->view; |
| 177 | } |
| 178 | |
| 179 | public function pattern(): string |
| 180 | { |
| 181 | return $this->pattern; |
| 182 | } |
| 183 | |
| 184 | /** @return null|array<string, string> */ |
| 185 | public function match(string $url, string $prefix = ''): ?array |
| 186 | { |
| 187 | $path = $this->pathWithoutPrefix($url, $prefix); |
| 188 | |
| 189 | if ($path === null) { |
| 190 | return null; |
| 191 | } |
| 192 | |
| 193 | return $this->routePattern()->match($path); |
| 194 | } |
| 195 | |
| 196 | private function routePattern(): RoutePattern |
| 197 | { |
| 198 | return $this->routePattern ??= new RoutePattern($this->pattern); |
| 199 | } |
| 200 | |
| 201 | private function pathWithoutPrefix(string $url, string $prefix): ?string |
| 202 | { |
| 203 | $path = $url === '' ? '/' : $url; |
| 204 | $prefix = self::normalizePrefix($prefix); |
| 205 | |
| 206 | if ($prefix === '') { |
| 207 | return $path; |
| 208 | } |
| 209 | |
| 210 | if ($path === $prefix) { |
| 211 | return '/'; |
| 212 | } |
| 213 | |
| 214 | if (!str_starts_with($path, $prefix . '/')) { |
| 215 | return null; |
| 216 | } |
| 217 | |
| 218 | $path = substr($path, strlen($prefix)); |
| 219 | |
| 220 | if ($path === '/' && $this->routePattern()->pattern() === '/') { |
| 221 | return null; |
| 222 | } |
| 223 | |
| 224 | return $path; |
| 225 | } |
| 226 | |
| 227 | private static function normalizePrefix(string $prefix): string |
| 228 | { |
| 229 | return $prefix === '' ? '' : '/' . trim($prefix, '/'); |
| 230 | } |
| 231 | } |
Below are the source code lines that represent each code branch as identified by Xdebug. Please note a branch is not
necessarily coterminous with a line, a line may contain multiple branches and therefore show up more than once.
Please also be aware that some branches may be implicit rather than explicit, e.g. an if statement
always has an else as part of its logical flow even if you didn't write one.
| 36 | protected string $pattern, |
| 37 | callable|array|string $view, |
| 38 | protected string $name = '', |
| 39 | ) { |
| 40 | if (is_callable($view)) { |
| 40 | if (is_callable($view)) { |
| 41 | $this->view = Closure::fromCallable($view); |
| 43 | $this->view = $view; |
| 44 | } |
| 45 | } |
| 45 | } |
| 48 | public static function any(string $pattern, callable|array|string $view, string $name = ''): self |
| 49 | { |
| 50 | return new self($pattern, $view, $name); |
| 51 | } |
| 149 | public function controller(string $controller): static |
| 150 | { |
| 151 | if (is_string($this->view)) { |
| 152 | $this->view = [$controller, $this->view]; |
| 153 | |
| 154 | return $this; |
| 157 | throw new ValueError( |
| 158 | 'Cannot add controller to route action. Controller groups require bare method names.', |
| 159 | ); |
| 160 | } |
| 96 | string $pattern, |
| 97 | callable|array|string $view, |
| 98 | string $name = '', |
| 99 | ): self { |
| 100 | return new self($pattern, $view, $name)->withMethods('DELETE'); |
| 101 | } |
| 71 | public static function get(string $pattern, callable|array|string $view, string $name = ''): self |
| 72 | { |
| 73 | return new self($pattern, $view, $name)->withMethods('GET'); |
| 74 | } |
| 104 | public static function head(string $pattern, callable|array|string $view, string $name = ''): self |
| 105 | { |
| 106 | return new self($pattern, $view, $name)->withMethods('HEAD'); |
| 107 | } |
| 58 | array $methods, |
| 59 | string $pattern, |
| 60 | callable|array|string $view, |
| 61 | string $name = '', |
| 62 | ): self { |
| 63 | if ($methods === []) { |
| 64 | throw new ValueError('Route method list cannot be empty.'); |
| 67 | return new self($pattern, $view, $name)->withMethods(...array_values($methods)); |
| 68 | } |
| 185 | public function match(string $url, string $prefix = ''): ?array |
| 186 | { |
| 187 | $path = $this->pathWithoutPrefix($url, $prefix); |
| 188 | |
| 189 | if ($path === null) { |
| 190 | return null; |
| 193 | return $this->routePattern()->match($path); |
| 194 | } |
| 130 | return $this->methods ?? []; |
| 131 | } |
| 164 | return $this->name; |
| 165 | } |
| 227 | private static function normalizePrefix(string $prefix): string |
| 228 | { |
| 229 | return $prefix === '' ? '' : '/' . trim($prefix, '/'); |
| 229 | return $prefix === '' ? '' : '/' . trim($prefix, '/'); |
| 229 | return $prefix === '' ? '' : '/' . trim($prefix, '/'); |
| 229 | return $prefix === '' ? '' : '/' . trim($prefix, '/'); |
| 229 | return $prefix === '' ? '' : '/' . trim($prefix, '/'); |
| 229 | return $prefix === '' ? '' : '/' . trim($prefix, '/'); |
| 229 | return $prefix === '' ? '' : '/' . trim($prefix, '/'); |
| 230 | } |
| 111 | string $pattern, |
| 112 | callable|array|string $view, |
| 113 | string $name = '', |
| 114 | ): self { |
| 115 | return new self($pattern, $view, $name)->withMethods('OPTIONS'); |
| 116 | } |
| 89 | public static function patch(string $pattern, callable|array|string $view, string $name = ''): self |
| 90 | { |
| 91 | return new self($pattern, $view, $name)->withMethods('PATCH'); |
| 92 | } |
| 201 | private function pathWithoutPrefix(string $url, string $prefix): ?string |
| 202 | { |
| 203 | $path = $url === '' ? '/' : $url; |
| 203 | $path = $url === '' ? '/' : $url; |
| 203 | $path = $url === '' ? '/' : $url; |
| 203 | $path = $url === '' ? '/' : $url; |
| 204 | $prefix = self::normalizePrefix($prefix); |
| 205 | |
| 206 | if ($prefix === '') { |
| 207 | return $path; |
| 210 | if ($path === $prefix) { |
| 211 | return '/'; |
| 214 | if (!str_starts_with($path, $prefix . '/')) { |
| 214 | if (!str_starts_with($path, $prefix . '/')) { |
| 214 | if (!str_starts_with($path, $prefix . '/')) { |
| 214 | if (!str_starts_with($path, $prefix . '/')) { |
| 215 | return null; |
| 218 | $path = substr($path, strlen($prefix)); |
| 218 | $path = substr($path, strlen($prefix)); |
| 218 | $path = substr($path, strlen($prefix)); |
| 218 | $path = substr($path, strlen($prefix)); |
| 219 | |
| 220 | if ($path === '/' && $this->routePattern()->pattern() === '/') { |
| 220 | if ($path === '/' && $this->routePattern()->pattern() === '/') { |
| 220 | if ($path === '/' && $this->routePattern()->pattern() === '/') { |
| 221 | return null; |
| 224 | return $path; |
| 225 | } |
| 181 | return $this->pattern; |
| 182 | } |
| 77 | public static function post(string $pattern, callable|array|string $view, string $name = ''): self |
| 78 | { |
| 79 | return new self($pattern, $view, $name)->withMethods('POST'); |
| 80 | } |
| 134 | public function prefix(string $pattern = '', string $name = ''): static |
| 135 | { |
| 136 | if ($pattern !== '') { |
| 137 | $this->pattern = $pattern . $this->pattern; |
| 138 | $this->routePattern = null; |
| 139 | } |
| 140 | |
| 141 | if ($name !== '') { |
| 141 | if ($name !== '') { |
| 142 | $this->name = $name . $this->name; |
| 143 | } |
| 144 | |
| 145 | return $this; |
| 145 | return $this; |
| 146 | } |
| 83 | public static function put(string $pattern, callable|array|string $view, string $name = ''): self |
| 84 | { |
| 85 | return new self($pattern, $view, $name)->withMethods('PUT'); |
| 86 | } |
| 198 | return $this->routePattern ??= new RoutePattern($this->pattern); |
| 199 | } |
| 168 | public function url(array $params = []): string |
| 169 | { |
| 170 | return $this->routePattern()->generate($params); |
| 171 | } |
| 176 | return $this->view; |
| 177 | } |
| 118 | private function withMethods(string ...$args): self |
| 119 | { |
| 120 | /** @var list<string> $methods */ |
| 121 | $methods = array_map(static fn(string $method): string => strtoupper($method), $args); |
| 122 | $this->methods = [...($this->methods ?? []), ...$methods]; |
| 123 | |
| 124 | return $this; |
| 125 | } |
| 121 | $methods = array_map(static fn(string $method): string => strtoupper($method), $args); |