Code Coverage |
||||||||||||||||
Lines |
Branches |
Paths |
Functions and Methods |
Classes and Traits |
||||||||||||
| Total | |
100.00% |
57 / 57 |
|
93.65% |
59 / 63 |
|
50.68% |
37 / 73 |
|
92.00% |
23 / 25 |
CRAP | |
0.00% |
0 / 1 |
| Route | |
100.00% |
57 / 57 |
|
93.65% |
59 / 63 |
|
50.68% |
37 / 73 |
|
100.00% |
25 / 25 |
211.18 | |
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% |
2 / 2 |
|
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 | |||
| replaceMiddleware | |
100.00% |
2 / 2 |
|
100.00% |
1 / 1 |
|
100.00% |
1 / 1 |
|
100.00% |
1 / 1 |
1 | |||
| setBeforeHandlers | |
100.00% |
2 / 2 |
|
100.00% |
1 / 1 |
|
100.00% |
1 / 1 |
|
100.00% |
1 / 1 |
1 | |||
| setAfterHandlers | |
100.00% |
2 / 2 |
|
100.00% |
1 / 1 |
|
100.00% |
1 / 1 |
|
100.00% |
1 / 1 |
1 | |||
| 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 |
|
15.00% |
6 / 40 |
|
100.00% |
1 / 1 |
37.09 | |||
| 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 Celema\Router; |
| 6 | |
| 7 | use Celema\Router\Exception\ValueError; |
| 8 | use Closure; |
| 9 | use Psr\Http\Server\MiddlewareInterface as Middleware; |
| 10 | |
| 11 | /** @psalm-api */ |
| 12 | class Route |
| 13 | { |
| 14 | use AddsBeforeAfter; |
| 15 | use AddsMiddleware; |
| 16 | |
| 17 | /** @var null|list<string> */ |
| 18 | protected ?array $methods = null; |
| 19 | |
| 20 | /** @var Closure|list{string, string}|string */ |
| 21 | protected Closure|array|string $view; |
| 22 | |
| 23 | protected ?RoutePattern $routePattern = null; |
| 24 | |
| 25 | /** |
| 26 | * @param string $pattern The URL pattern of the route |
| 27 | * |
| 28 | * @param callable|list{string, string}|non-empty-string $view The callable view. Can be a closure, an invokable object or any other callable |
| 29 | * |
| 30 | * @param string $name The name of the route, used to generate its URL. Routes without a name cannot be looked up. |
| 31 | */ |
| 32 | public function __construct( |
| 33 | protected string $pattern, |
| 34 | callable|array|string $view, |
| 35 | protected string $name = '', |
| 36 | ) { |
| 37 | if (is_callable($view)) { |
| 38 | $this->view = Closure::fromCallable($view); |
| 39 | } else { |
| 40 | $this->view = $view; |
| 41 | } |
| 42 | } |
| 43 | |
| 44 | /** @param callable|list{string, string}|non-empty-string $view */ |
| 45 | public static function any(string $pattern, callable|array|string $view, string $name = ''): self |
| 46 | { |
| 47 | return new self($pattern, $view, $name); |
| 48 | } |
| 49 | |
| 50 | /** |
| 51 | * @param array<array-key, string> $methods |
| 52 | * @param callable|list{string, string}|non-empty-string $view |
| 53 | */ |
| 54 | public static function map( |
| 55 | array $methods, |
| 56 | string $pattern, |
| 57 | callable|array|string $view, |
| 58 | string $name = '', |
| 59 | ): self { |
| 60 | if ($methods === []) { |
| 61 | throw new ValueError('Route method list cannot be empty.'); |
| 62 | } |
| 63 | |
| 64 | return new self($pattern, $view, $name)->withMethods(array_values($methods)); |
| 65 | } |
| 66 | |
| 67 | /** @param callable|list{string, string}|non-empty-string $view */ |
| 68 | public static function get(string $pattern, callable|array|string $view, string $name = ''): self |
| 69 | { |
| 70 | return new self($pattern, $view, $name)->withMethods(['GET']); |
| 71 | } |
| 72 | |
| 73 | /** @param callable|list{string, string}|non-empty-string $view */ |
| 74 | public static function post(string $pattern, callable|array|string $view, string $name = ''): self |
| 75 | { |
| 76 | return new self($pattern, $view, $name)->withMethods(['POST']); |
| 77 | } |
| 78 | |
| 79 | /** @param callable|list{string, string}|non-empty-string $view */ |
| 80 | public static function put(string $pattern, callable|array|string $view, string $name = ''): self |
| 81 | { |
| 82 | return new self($pattern, $view, $name)->withMethods(['PUT']); |
| 83 | } |
| 84 | |
| 85 | /** @param callable|list{string, string}|non-empty-string $view */ |
| 86 | public static function patch(string $pattern, callable|array|string $view, string $name = ''): self |
| 87 | { |
| 88 | return new self($pattern, $view, $name)->withMethods(['PATCH']); |
| 89 | } |
| 90 | |
| 91 | /** @param callable|list{string, string}|non-empty-string $view */ |
| 92 | public static function delete( |
| 93 | string $pattern, |
| 94 | callable|array|string $view, |
| 95 | string $name = '', |
| 96 | ): self { |
| 97 | return new self($pattern, $view, $name)->withMethods(['DELETE']); |
| 98 | } |
| 99 | |
| 100 | /** @param callable|list{string, string}|non-empty-string $view */ |
| 101 | public static function head(string $pattern, callable|array|string $view, string $name = ''): self |
| 102 | { |
| 103 | return new self($pattern, $view, $name)->withMethods(['HEAD']); |
| 104 | } |
| 105 | |
| 106 | /** @param callable|list{string, string}|non-empty-string $view */ |
| 107 | public static function options( |
| 108 | string $pattern, |
| 109 | callable|array|string $view, |
| 110 | string $name = '', |
| 111 | ): self { |
| 112 | return new self($pattern, $view, $name)->withMethods(['OPTIONS']); |
| 113 | } |
| 114 | |
| 115 | /** @param list<string> $methods */ |
| 116 | private function withMethods(array $methods): self |
| 117 | { |
| 118 | $this->methods = array_map(static fn(string $method): string => strtoupper($method), $methods); |
| 119 | |
| 120 | return $this; |
| 121 | } |
| 122 | |
| 123 | /** @return list<string> */ |
| 124 | public function methods(): array |
| 125 | { |
| 126 | return $this->methods ?? []; |
| 127 | } |
| 128 | |
| 129 | /** @internal */ |
| 130 | public function prefix(string $pattern = '', string $name = ''): static |
| 131 | { |
| 132 | if ($pattern !== '') { |
| 133 | $this->pattern = $pattern . $this->pattern; |
| 134 | $this->routePattern = null; |
| 135 | } |
| 136 | |
| 137 | if ($name !== '') { |
| 138 | $this->name = $name . $this->name; |
| 139 | } |
| 140 | |
| 141 | return $this; |
| 142 | } |
| 143 | |
| 144 | /** @internal */ |
| 145 | public function controller(string $controller): static |
| 146 | { |
| 147 | if (is_string($this->view)) { |
| 148 | $this->view = [$controller, $this->view]; |
| 149 | |
| 150 | return $this; |
| 151 | } |
| 152 | |
| 153 | throw new ValueError( |
| 154 | 'Cannot add controller to route action. Controller groups require bare method names.', |
| 155 | ); |
| 156 | } |
| 157 | |
| 158 | /** |
| 159 | * @internal |
| 160 | * @param list<Middleware> $middleware |
| 161 | */ |
| 162 | public function replaceMiddleware(array $middleware): static |
| 163 | { |
| 164 | $this->middleware = $middleware; |
| 165 | |
| 166 | return $this; |
| 167 | } |
| 168 | |
| 169 | /** |
| 170 | * @internal |
| 171 | * @param list<Before> $beforeHandlers |
| 172 | */ |
| 173 | public function setBeforeHandlers(array $beforeHandlers): static |
| 174 | { |
| 175 | $this->beforeHandlers = $beforeHandlers; |
| 176 | |
| 177 | return $this; |
| 178 | } |
| 179 | |
| 180 | /** |
| 181 | * @internal |
| 182 | * @param list<After> $afterHandlers |
| 183 | */ |
| 184 | public function setAfterHandlers(array $afterHandlers): static |
| 185 | { |
| 186 | $this->afterHandlers = $afterHandlers; |
| 187 | |
| 188 | return $this; |
| 189 | } |
| 190 | |
| 191 | public function name(): string |
| 192 | { |
| 193 | return $this->name; |
| 194 | } |
| 195 | |
| 196 | /** @param array<array-key, mixed> $params */ |
| 197 | public function url(array $params = []): string |
| 198 | { |
| 199 | return $this->routePattern()->generate($params); |
| 200 | } |
| 201 | |
| 202 | /** @return Closure|list{string, string}|string */ |
| 203 | public function view(): Closure|array|string |
| 204 | { |
| 205 | return $this->view; |
| 206 | } |
| 207 | |
| 208 | public function pattern(): string |
| 209 | { |
| 210 | return $this->pattern; |
| 211 | } |
| 212 | |
| 213 | /** @return null|array<string, string> */ |
| 214 | public function match(string $url, string $prefix = ''): ?array |
| 215 | { |
| 216 | $path = $this->pathWithoutPrefix($url, $prefix); |
| 217 | |
| 218 | if ($path === null) { |
| 219 | return null; |
| 220 | } |
| 221 | |
| 222 | return $this->routePattern()->match($path); |
| 223 | } |
| 224 | |
| 225 | private function routePattern(): RoutePattern |
| 226 | { |
| 227 | return $this->routePattern ??= new RoutePattern($this->pattern); |
| 228 | } |
| 229 | |
| 230 | private function pathWithoutPrefix(string $url, string $prefix): ?string |
| 231 | { |
| 232 | $path = $url === '' ? '/' : $url; |
| 233 | $prefix = self::normalizePrefix($prefix); |
| 234 | |
| 235 | if ($prefix === '') { |
| 236 | return $path; |
| 237 | } |
| 238 | |
| 239 | if ($path === $prefix) { |
| 240 | return '/'; |
| 241 | } |
| 242 | |
| 243 | if (!str_starts_with($path, $prefix . '/')) { |
| 244 | return null; |
| 245 | } |
| 246 | |
| 247 | $path = substr($path, strlen($prefix)); |
| 248 | |
| 249 | if ($path === '/' && $this->routePattern()->pattern() === '/') { |
| 250 | return null; |
| 251 | } |
| 252 | |
| 253 | return $path; |
| 254 | } |
| 255 | |
| 256 | private static function normalizePrefix(string $prefix): string |
| 257 | { |
| 258 | return $prefix === '' ? '' : '/' . trim($prefix, '/'); |
| 259 | } |
| 260 | } |
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.
| 33 | protected string $pattern, |
| 34 | callable|array|string $view, |
| 35 | protected string $name = '', |
| 36 | ) { |
| 37 | if (is_callable($view)) { |
| 37 | if (is_callable($view)) { |
| 38 | $this->view = Closure::fromCallable($view); |
| 40 | $this->view = $view; |
| 41 | } |
| 42 | } |
| 42 | } |
| 45 | public static function any(string $pattern, callable|array|string $view, string $name = ''): self |
| 46 | { |
| 47 | return new self($pattern, $view, $name); |
| 48 | } |
| 145 | public function controller(string $controller): static |
| 146 | { |
| 147 | if (is_string($this->view)) { |
| 148 | $this->view = [$controller, $this->view]; |
| 149 | |
| 150 | return $this; |
| 153 | throw new ValueError( |
| 154 | 'Cannot add controller to route action. Controller groups require bare method names.', |
| 155 | ); |
| 156 | } |
| 93 | string $pattern, |
| 94 | callable|array|string $view, |
| 95 | string $name = '', |
| 96 | ): self { |
| 97 | return new self($pattern, $view, $name)->withMethods(['DELETE']); |
| 98 | } |
| 68 | public static function get(string $pattern, callable|array|string $view, string $name = ''): self |
| 69 | { |
| 70 | return new self($pattern, $view, $name)->withMethods(['GET']); |
| 71 | } |
| 101 | public static function head(string $pattern, callable|array|string $view, string $name = ''): self |
| 102 | { |
| 103 | return new self($pattern, $view, $name)->withMethods(['HEAD']); |
| 104 | } |
| 55 | array $methods, |
| 56 | string $pattern, |
| 57 | callable|array|string $view, |
| 58 | string $name = '', |
| 59 | ): self { |
| 60 | if ($methods === []) { |
| 61 | throw new ValueError('Route method list cannot be empty.'); |
| 64 | return new self($pattern, $view, $name)->withMethods(array_values($methods)); |
| 65 | } |
| 214 | public function match(string $url, string $prefix = ''): ?array |
| 215 | { |
| 216 | $path = $this->pathWithoutPrefix($url, $prefix); |
| 217 | |
| 218 | if ($path === null) { |
| 219 | return null; |
| 222 | return $this->routePattern()->match($path); |
| 223 | } |
| 126 | return $this->methods ?? []; |
| 127 | } |
| 193 | return $this->name; |
| 194 | } |
| 256 | private static function normalizePrefix(string $prefix): string |
| 257 | { |
| 258 | return $prefix === '' ? '' : '/' . trim($prefix, '/'); |
| 258 | return $prefix === '' ? '' : '/' . trim($prefix, '/'); |
| 258 | return $prefix === '' ? '' : '/' . trim($prefix, '/'); |
| 258 | return $prefix === '' ? '' : '/' . trim($prefix, '/'); |
| 258 | return $prefix === '' ? '' : '/' . trim($prefix, '/'); |
| 258 | return $prefix === '' ? '' : '/' . trim($prefix, '/'); |
| 258 | return $prefix === '' ? '' : '/' . trim($prefix, '/'); |
| 259 | } |
| 108 | string $pattern, |
| 109 | callable|array|string $view, |
| 110 | string $name = '', |
| 111 | ): self { |
| 112 | return new self($pattern, $view, $name)->withMethods(['OPTIONS']); |
| 113 | } |
| 86 | public static function patch(string $pattern, callable|array|string $view, string $name = ''): self |
| 87 | { |
| 88 | return new self($pattern, $view, $name)->withMethods(['PATCH']); |
| 89 | } |
| 230 | private function pathWithoutPrefix(string $url, string $prefix): ?string |
| 231 | { |
| 232 | $path = $url === '' ? '/' : $url; |
| 232 | $path = $url === '' ? '/' : $url; |
| 232 | $path = $url === '' ? '/' : $url; |
| 232 | $path = $url === '' ? '/' : $url; |
| 233 | $prefix = self::normalizePrefix($prefix); |
| 234 | |
| 235 | if ($prefix === '') { |
| 236 | return $path; |
| 239 | if ($path === $prefix) { |
| 240 | return '/'; |
| 243 | if (!str_starts_with($path, $prefix . '/')) { |
| 243 | if (!str_starts_with($path, $prefix . '/')) { |
| 243 | if (!str_starts_with($path, $prefix . '/')) { |
| 243 | if (!str_starts_with($path, $prefix . '/')) { |
| 244 | return null; |
| 247 | $path = substr($path, strlen($prefix)); |
| 247 | $path = substr($path, strlen($prefix)); |
| 247 | $path = substr($path, strlen($prefix)); |
| 247 | $path = substr($path, strlen($prefix)); |
| 248 | |
| 249 | if ($path === '/' && $this->routePattern()->pattern() === '/') { |
| 249 | if ($path === '/' && $this->routePattern()->pattern() === '/') { |
| 249 | if ($path === '/' && $this->routePattern()->pattern() === '/') { |
| 250 | return null; |
| 253 | return $path; |
| 254 | } |
| 210 | return $this->pattern; |
| 211 | } |
| 74 | public static function post(string $pattern, callable|array|string $view, string $name = ''): self |
| 75 | { |
| 76 | return new self($pattern, $view, $name)->withMethods(['POST']); |
| 77 | } |
| 130 | public function prefix(string $pattern = '', string $name = ''): static |
| 131 | { |
| 132 | if ($pattern !== '') { |
| 133 | $this->pattern = $pattern . $this->pattern; |
| 134 | $this->routePattern = null; |
| 135 | } |
| 136 | |
| 137 | if ($name !== '') { |
| 137 | if ($name !== '') { |
| 138 | $this->name = $name . $this->name; |
| 139 | } |
| 140 | |
| 141 | return $this; |
| 141 | return $this; |
| 142 | } |
| 80 | public static function put(string $pattern, callable|array|string $view, string $name = ''): self |
| 81 | { |
| 82 | return new self($pattern, $view, $name)->withMethods(['PUT']); |
| 83 | } |
| 162 | public function replaceMiddleware(array $middleware): static |
| 163 | { |
| 164 | $this->middleware = $middleware; |
| 165 | |
| 166 | return $this; |
| 167 | } |
| 227 | return $this->routePattern ??= new RoutePattern($this->pattern); |
| 228 | } |
| 184 | public function setAfterHandlers(array $afterHandlers): static |
| 185 | { |
| 186 | $this->afterHandlers = $afterHandlers; |
| 187 | |
| 188 | return $this; |
| 189 | } |
| 173 | public function setBeforeHandlers(array $beforeHandlers): static |
| 174 | { |
| 175 | $this->beforeHandlers = $beforeHandlers; |
| 176 | |
| 177 | return $this; |
| 178 | } |
| 197 | public function url(array $params = []): string |
| 198 | { |
| 199 | return $this->routePattern()->generate($params); |
| 200 | } |
| 205 | return $this->view; |
| 206 | } |
| 116 | private function withMethods(array $methods): self |
| 117 | { |
| 118 | $this->methods = array_map(static fn(string $method): string => strtoupper($method), $methods); |
| 119 | |
| 120 | return $this; |
| 121 | } |
| 118 | $this->methods = array_map(static fn(string $method): string => strtoupper($method), $methods); |