Code Coverage |
||||||||||
Lines |
Functions and Methods |
Classes and Traits |
||||||||
Total | |
93.02% |
120 / 129 |
|
87.10% |
27 / 31 |
CRAP | |
0.00% |
0 / 1 |
AbstractMatch | |
93.02% |
120 / 129 |
|
87.10% |
27 / 31 |
117.34 | |
0.00% |
0 / 1 |
addRoute | |
100.00% |
24 / 24 |
|
100.00% |
1 / 1 |
18 | |||
addRoutes | |
100.00% |
3 / 3 |
|
100.00% |
1 / 1 |
2 | |||
name | |
100.00% |
4 / 4 |
|
100.00% |
1 / 1 |
2 | |||
hasName | |
100.00% |
1 / 1 |
|
100.00% |
1 / 1 |
1 | |||
addControllerParams | |
100.00% |
4 / 4 |
|
100.00% |
1 / 1 |
2 | |||
appendControllerParams | |
100.00% |
5 / 5 |
|
100.00% |
1 / 1 |
3 | |||
getControllerParams | |
100.00% |
1 / 1 |
|
100.00% |
1 / 1 |
1 | |||
hasControllerParams | |
100.00% |
1 / 1 |
|
100.00% |
1 / 1 |
1 | |||
removeControllerParams | |
100.00% |
3 / 3 |
|
100.00% |
1 / 1 |
2 | |||
getRouteString | |
100.00% |
1 / 1 |
|
100.00% |
1 / 1 |
1 | |||
getSegments | |
100.00% |
1 / 1 |
|
100.00% |
1 / 1 |
1 | |||
getSegment | |
100.00% |
1 / 1 |
|
100.00% |
1 / 1 |
1 | |||
getOriginalRoute | |
100.00% |
1 / 1 |
|
100.00% |
1 / 1 |
1 | |||
getRoute | |
100.00% |
1 / 1 |
|
100.00% |
1 / 1 |
1 | |||
getRoutes | |
100.00% |
1 / 1 |
|
100.00% |
1 / 1 |
1 | |||
getPreparedRoutes | |
100.00% |
1 / 1 |
|
100.00% |
1 / 1 |
1 | |||
hasRouteConfig | |
100.00% |
4 / 4 |
|
100.00% |
1 / 1 |
6 | |||
getRouteConfig | |
100.00% |
5 / 5 |
|
100.00% |
1 / 1 |
4 | |||
getFlattenedRoutes | |
100.00% |
6 / 6 |
|
100.00% |
1 / 1 |
3 | |||
getRouteParams | |
100.00% |
1 / 1 |
|
100.00% |
1 / 1 |
1 | |||
hasRouteParams | |
100.00% |
1 / 1 |
|
100.00% |
1 / 1 |
1 | |||
getDefaultRoute | |
100.00% |
1 / 1 |
|
100.00% |
1 / 1 |
1 | |||
hasDefaultRoute | |
100.00% |
1 / 1 |
|
100.00% |
1 / 1 |
1 | |||
getDynamicRoute | |
100.00% |
1 / 1 |
|
100.00% |
1 / 1 |
1 | |||
getDynamicRoutePrefix | |
100.00% |
1 / 1 |
|
100.00% |
1 / 1 |
1 | |||
hasDynamicRoute | |
100.00% |
1 / 1 |
|
100.00% |
1 / 1 |
1 | |||
isDynamicRoute | |
100.00% |
1 / 1 |
|
100.00% |
1 / 1 |
1 | |||
getController | |
77.78% |
14 / 18 |
|
0.00% |
0 / 1 |
20.17 | |||
hasController | |
86.67% |
13 / 15 |
|
0.00% |
0 / 1 |
16.61 | |||
getAction | |
80.00% |
8 / 10 |
|
0.00% |
0 / 1 |
10.80 | |||
hasAction | |
90.00% |
9 / 10 |
|
0.00% |
0 / 1 |
10.10 | |||
hasRoute | n/a |
0 / 0 |
n/a |
0 / 0 |
0 | |||||
prepare | n/a |
0 / 0 |
n/a |
0 / 0 |
0 | |||||
match | n/a |
0 / 0 |
n/a |
0 / 0 |
0 | |||||
noRouteFound | n/a |
0 / 0 |
n/a |
0 / 0 |
0 |
1 | <?php |
2 | /** |
3 | * Pop PHP Framework (https://www.popphp.org/) |
4 | * |
5 | * @link https://github.com/popphp/popphp-framework |
6 | * @author Nick Sagona, III <dev@noladev.com> |
7 | * @copyright Copyright (c) 2009-2025 NOLA Interactive, LLC. |
8 | * @license https://www.popphp.org/license New BSD License |
9 | */ |
10 | |
11 | /** |
12 | * @namespace |
13 | */ |
14 | namespace Pop\Router\Match; |
15 | |
16 | /** |
17 | * Pop router match abstract class |
18 | * |
19 | * @category Pop |
20 | * @package Pop\Router |
21 | * @author Nick Sagona, III <dev@noladev.com> |
22 | * @copyright Copyright (c) 2009-2025 NOLA Interactive, LLC. |
23 | * @license https://www.popphp.org/license New BSD License |
24 | * @version 4.3.5 |
25 | */ |
26 | abstract class AbstractMatch implements MatchInterface |
27 | { |
28 | |
29 | /** |
30 | * Route string |
31 | * @var ?string |
32 | */ |
33 | protected ?string $routeString = null; |
34 | |
35 | /** |
36 | * Segments of route string |
37 | * @var array |
38 | */ |
39 | protected array $segments = []; |
40 | |
41 | /** |
42 | * Matched route |
43 | * @var ?string |
44 | */ |
45 | protected ?string $route = null; |
46 | |
47 | /** |
48 | * Default route |
49 | * @var ?array |
50 | */ |
51 | protected ?array $defaultRoute = null; |
52 | |
53 | /** |
54 | * Dynamic route |
55 | * @var mixed |
56 | */ |
57 | protected mixed $dynamicRoute = null; |
58 | |
59 | /** |
60 | * Dynamic route prefix |
61 | * @var mixed |
62 | */ |
63 | protected mixed $dynamicRoutePrefix = null; |
64 | |
65 | /** |
66 | * Flag for dynamic route |
67 | * @var bool |
68 | */ |
69 | protected bool $isDynamicRoute = false; |
70 | |
71 | /** |
72 | * Routes |
73 | * @var array |
74 | */ |
75 | protected array $routes = []; |
76 | |
77 | /** |
78 | * Prepared routes |
79 | * @var array |
80 | */ |
81 | protected array $preparedRoutes = []; |
82 | |
83 | /** |
84 | * Controller parameters |
85 | * @var array |
86 | */ |
87 | protected array $controllerParams = []; |
88 | |
89 | /** |
90 | * Route parameters |
91 | * @var array |
92 | */ |
93 | protected array $routeParams = []; |
94 | |
95 | /** |
96 | * Route names |
97 | * @var array |
98 | */ |
99 | protected array $routeNames = []; |
100 | |
101 | /** |
102 | * Add a route |
103 | * |
104 | * @param string $route |
105 | * @param mixed $controller |
106 | * @return AbstractMatch |
107 | */ |
108 | public function addRoute(string $route, mixed $controller): AbstractMatch |
109 | { |
110 | // If is dynamic route |
111 | if ((($this instanceof Http) && (str_contains($route, ':controller'))) || |
112 | (($this instanceof Cli) && (str_contains($route, '<controller')))) { |
113 | $this->dynamicRoute = $route; |
114 | if (isset($controller['prefix'])) { |
115 | $this->dynamicRoutePrefix = $controller['prefix']; |
116 | } |
117 | // Else, if wildcard route |
118 | } else if (($route == '*') || (str_ends_with($route, '/*'))) { |
119 | $routeKey = (str_ends_with($route, '/*')) ? substr($route, 0, -2) : $route; |
120 | if (is_callable($controller)) { |
121 | $controller = ['controller' => $controller]; |
122 | } |
123 | $this->defaultRoute[$routeKey] = $controller; |
124 | // Else, regular route |
125 | } else { |
126 | $this->routeString = urldecode($this->routeString); |
127 | // Handle nested routes |
128 | if (is_array($controller) && !isset($controller['controller'])) { |
129 | foreach ($controller as $r => $c) { |
130 | $fullRoute = ($r == '*') ? $route . '/*' : $route . $r; |
131 | $this->addRoute($fullRoute, $c); |
132 | } |
133 | } else { |
134 | if (is_callable($controller)) { |
135 | $controller = ['controller' => $controller]; |
136 | } |
137 | |
138 | $this->routes[$route] = (isset($this->routes[$route])) ? |
139 | array_merge($this->routes[$route], $controller) : $controller; |
140 | } |
141 | } |
142 | |
143 | if (isset($controller['name'])) { |
144 | $this->name($controller['name']); |
145 | } |
146 | |
147 | if (isset($controller['params'])) { |
148 | $this->addControllerParams($controller['controller'], $controller['params']); |
149 | } |
150 | |
151 | return $this; |
152 | } |
153 | |
154 | /** |
155 | * Add multiple controller routes |
156 | * |
157 | * @param array $routes |
158 | * @return AbstractMatch |
159 | */ |
160 | public function addRoutes(array $routes): AbstractMatch |
161 | { |
162 | foreach ($routes as $route => $controller) { |
163 | $this->addRoute($route, $controller); |
164 | } |
165 | |
166 | return $this; |
167 | } |
168 | |
169 | /** |
170 | * Add a route name |
171 | * |
172 | * @param string $routeName |
173 | * @throws Exception |
174 | * @return AbstractMatch |
175 | */ |
176 | public function name(string $routeName): AbstractMatch |
177 | { |
178 | if (empty($this->routes)) { |
179 | throw new Exception('Error: No routes have been added to name.'); |
180 | } |
181 | |
182 | $this->routeNames[$routeName] = key(array_slice($this->routes, -1)); |
183 | return $this; |
184 | } |
185 | |
186 | /** |
187 | * Has a route name |
188 | * |
189 | * @param string $routeName |
190 | * @return bool |
191 | */ |
192 | public function hasName(string $routeName): bool |
193 | { |
194 | return (isset($this->routeNames[$routeName])); |
195 | } |
196 | |
197 | /** |
198 | * Add controller params to be passed into a new controller instance |
199 | * |
200 | * @param string $controller |
201 | * @param mixed $params |
202 | * @return AbstractMatch |
203 | */ |
204 | public function addControllerParams(string $controller, mixed $params): AbstractMatch |
205 | { |
206 | if (!is_array($params)) { |
207 | $params = [$params]; |
208 | } |
209 | $this->controllerParams[$controller] = $params; |
210 | |
211 | return $this; |
212 | } |
213 | |
214 | /** |
215 | * Append controller params to be passed into a new controller instance |
216 | * |
217 | * @param string $controller |
218 | * @param mixed $params |
219 | * @return AbstractMatch |
220 | */ |
221 | public function appendControllerParams(string $controller, mixed $params): AbstractMatch |
222 | { |
223 | if (!is_array($params)) { |
224 | $params = [$params]; |
225 | } |
226 | $this->controllerParams[$controller] = (isset($this->controllerParams[$controller])) ? |
227 | array_merge($this->controllerParams[$controller], $params) : $params; |
228 | |
229 | return $this; |
230 | } |
231 | |
232 | /** |
233 | * Get the params assigned to the controller |
234 | * |
235 | * @param string $controller |
236 | * @return mixed |
237 | */ |
238 | public function getControllerParams(string $controller): mixed |
239 | { |
240 | return $this->controllerParams[$controller] ?? null; |
241 | } |
242 | |
243 | /** |
244 | * Determine if the controller has params |
245 | * |
246 | * @param string $controller |
247 | * @return bool |
248 | */ |
249 | public function hasControllerParams(string $controller): bool |
250 | { |
251 | return (isset($this->controllerParams[$controller])); |
252 | } |
253 | |
254 | /** |
255 | * Remove controller params |
256 | * |
257 | * @param string $controller |
258 | * @return AbstractMatch |
259 | */ |
260 | public function removeControllerParams(string $controller): AbstractMatch |
261 | { |
262 | if (isset($this->controllerParams[$controller])) { |
263 | unset($this->controllerParams[$controller]); |
264 | } |
265 | return $this; |
266 | } |
267 | |
268 | /** |
269 | * Get the route string |
270 | * |
271 | * @return string |
272 | */ |
273 | public function getRouteString(): string |
274 | { |
275 | return $this->routeString; |
276 | } |
277 | |
278 | /** |
279 | * Get the route string segments |
280 | * |
281 | * @return array |
282 | */ |
283 | public function getSegments(): array |
284 | { |
285 | return $this->segments; |
286 | } |
287 | |
288 | /** |
289 | * Get a route string segment |
290 | * |
291 | * @param int $i |
292 | * @return ?string |
293 | */ |
294 | public function getSegment(int $i): ?string |
295 | { |
296 | return $this->segments[$i] ?? null; |
297 | } |
298 | |
299 | /** |
300 | * Get original route string |
301 | * |
302 | * @return ?string |
303 | */ |
304 | public function getOriginalRoute(): ?string |
305 | { |
306 | return $this->preparedRoutes[$this->route]['route'] ?? null; |
307 | } |
308 | |
309 | /** |
310 | * Get route regex |
311 | * |
312 | * @return string |
313 | */ |
314 | public function getRoute(): string |
315 | { |
316 | return $this->route; |
317 | } |
318 | |
319 | /** |
320 | * Get routes |
321 | * |
322 | * @return array |
323 | */ |
324 | public function getRoutes(): array |
325 | { |
326 | return $this->routes; |
327 | } |
328 | |
329 | /** |
330 | * Get prepared routes |
331 | * |
332 | * @return array |
333 | */ |
334 | public function getPreparedRoutes(): array |
335 | { |
336 | return $this->preparedRoutes; |
337 | } |
338 | |
339 | /** |
340 | * Has route config |
341 | * |
342 | * @param ?string $key |
343 | * @return bool |
344 | */ |
345 | public function hasRouteConfig(?string $key = null): bool |
346 | { |
347 | if (($this->route !== null) && isset($this->preparedRoutes[$this->route])) { |
348 | return ((($key !== null) && isset($this->preparedRoutes[$this->route][$key])) || |
349 | (($key === null) && !empty($this->preparedRoutes[$this->route]))); |
350 | } else { |
351 | return false; |
352 | } |
353 | } |
354 | |
355 | /** |
356 | * Get route config |
357 | * |
358 | * @param ?string $key |
359 | * @return mixed |
360 | */ |
361 | public function getRouteConfig(?string $key = null): mixed |
362 | { |
363 | if (($this->route !== null) && isset($this->preparedRoutes[$this->route])) { |
364 | if ($key === null) { |
365 | return $this->preparedRoutes[$this->route]; |
366 | } else { |
367 | return $this->preparedRoutes[$this->route][$key] ?? null; |
368 | } |
369 | } else { |
370 | return null; |
371 | } |
372 | } |
373 | |
374 | /** |
375 | * Get flattened routes |
376 | * |
377 | * @return array |
378 | */ |
379 | public function getFlattenedRoutes(): array |
380 | { |
381 | $routes = []; |
382 | foreach ($this->preparedRoutes as $value) { |
383 | if (isset($value['route'])) { |
384 | $routes[$value['route']] = $value; |
385 | unset($routes[$value['route']]['route']); |
386 | } |
387 | } |
388 | return $routes; |
389 | } |
390 | |
391 | /** |
392 | * Get the params discovered from the route |
393 | * |
394 | * @return array |
395 | */ |
396 | public function getRouteParams(): array |
397 | { |
398 | return $this->routeParams; |
399 | } |
400 | |
401 | /** |
402 | * Determine if the route has params |
403 | * |
404 | * @return bool |
405 | */ |
406 | public function hasRouteParams(): bool |
407 | { |
408 | return (count($this->routeParams) > 0); |
409 | } |
410 | |
411 | /** |
412 | * Get the default route |
413 | * |
414 | * @return array |
415 | */ |
416 | public function getDefaultRoute(): array |
417 | { |
418 | return $this->defaultRoute; |
419 | } |
420 | |
421 | /** |
422 | * Determine if there is a default route |
423 | * |
424 | * @return bool |
425 | */ |
426 | public function hasDefaultRoute(): bool |
427 | { |
428 | return ($this->defaultRoute !== null); |
429 | } |
430 | |
431 | /** |
432 | * Get the dynamic route |
433 | * |
434 | * @return mixed |
435 | */ |
436 | public function getDynamicRoute(): mixed |
437 | { |
438 | return $this->dynamicRoute; |
439 | } |
440 | |
441 | /** |
442 | * Get the dynamic route prefix |
443 | * |
444 | * @return mixed |
445 | */ |
446 | public function getDynamicRoutePrefix(): mixed |
447 | { |
448 | return $this->dynamicRoutePrefix; |
449 | } |
450 | |
451 | /** |
452 | * Determine if there is a dynamic route |
453 | * |
454 | * @return bool |
455 | */ |
456 | public function hasDynamicRoute(): bool |
457 | { |
458 | return ($this->dynamicRoute !== null); |
459 | } |
460 | |
461 | /** |
462 | * Determine if it is a dynamic route |
463 | * |
464 | * @return bool |
465 | */ |
466 | public function isDynamicRoute(): bool |
467 | { |
468 | return $this->isDynamicRoute; |
469 | } |
470 | |
471 | /** |
472 | * Get the controller |
473 | * |
474 | * @return mixed |
475 | */ |
476 | public function getController(): mixed |
477 | { |
478 | $routeController = null; |
479 | |
480 | if (($this->route !== null) && isset($this->preparedRoutes[$this->route]) && |
481 | isset($this->preparedRoutes[$this->route]['controller'])) { |
482 | $routeController = $this->preparedRoutes[$this->route]['controller']; |
483 | } else { |
484 | if (($this->dynamicRoute !== null) && ($this->dynamicRoutePrefix !== null) && (count($this->segments) >= 1)) { |
485 | $routeController = $this->dynamicRoutePrefix . ucfirst(strtolower($this->segments[0])) . 'Controller'; |
486 | if (!class_exists($routeController)) { |
487 | $routeController = null; |
488 | $this->isDynamicRoute = false; |
489 | } else { |
490 | $this->isDynamicRoute = true; |
491 | } |
492 | } |
493 | if (($routeController === null) && !empty($this->defaultRoute)) { |
494 | foreach ($this->defaultRoute as $routeKey => $controller) { |
495 | if ($routeKey != '*') { |
496 | if (str_starts_with($this->routeString, $routeKey) && isset($controller['controller'])) { |
497 | $routeController = $controller['controller']; |
498 | } |
499 | } |
500 | } |
501 | if (($routeController === null) && isset($this->defaultRoute['*']) && isset($this->defaultRoute['*']['controller'])) { |
502 | $routeController = $this->defaultRoute['*']['controller']; |
503 | } |
504 | } |
505 | } |
506 | |
507 | return $routeController; |
508 | } |
509 | |
510 | /** |
511 | * Determine if there is a controller |
512 | * |
513 | * @return bool |
514 | */ |
515 | public function hasController(): bool |
516 | { |
517 | $result = false; |
518 | |
519 | if (($this->route !== null) && isset($this->preparedRoutes[$this->route]) && |
520 | isset($this->preparedRoutes[$this->route]['controller'])) { |
521 | $result = true; |
522 | } else if (($this->dynamicRoute !== null) && ($this->getController() !== null) && |
523 | ($this->dynamicRoutePrefix !== null) && (count($this->segments) >= 1)) { |
524 | $result = class_exists($this->getController()); |
525 | } else if (!empty($this->defaultRoute)) { |
526 | foreach ($this->defaultRoute as $routeKey => $controller) { |
527 | if (($routeKey != '*') && str_starts_with($this->routeString, $routeKey) && isset($controller['controller'])) { |
528 | $result = true; |
529 | break; |
530 | } |
531 | } |
532 | if ((!$result) && isset($this->defaultRoute['*']) && isset($this->defaultRoute['*']['controller'])) { |
533 | $result = true; |
534 | } |
535 | } |
536 | |
537 | return $result; |
538 | } |
539 | |
540 | /** |
541 | * Get the action |
542 | * |
543 | * @return mixed |
544 | */ |
545 | public function getAction(): mixed |
546 | { |
547 | $action = null; |
548 | |
549 | if (($this->route !== null) && isset($this->preparedRoutes[$this->route]) && |
550 | isset($this->preparedRoutes[$this->route]['action'])) { |
551 | $action = $this->preparedRoutes[$this->route]['action']; |
552 | } else if (($this->dynamicRoute !== null) && ($this->dynamicRoutePrefix !== null) && |
553 | (count($this->segments) >= 1)) { |
554 | $action = (isset($this->segments[1])) ? $this->segments[1] : null; |
555 | } else if (($this->defaultRoute !== null) && isset($this->defaultRoute['action'])) { |
556 | $action = $this->defaultRoute['action']; |
557 | } |
558 | |
559 | return $action; |
560 | } |
561 | |
562 | /** |
563 | * Determine if there is an action |
564 | * |
565 | * @return bool |
566 | */ |
567 | public function hasAction(): bool |
568 | { |
569 | $result = false; |
570 | |
571 | if (($this->route !== null) && isset($this->preparedRoutes[$this->route]) && |
572 | isset($this->preparedRoutes[$this->route]['action'])) { |
573 | $result = true; |
574 | } else { |
575 | if (($this->dynamicRoute !== null) && ($this->dynamicRoutePrefix !== null) && |
576 | (count($this->segments) >= 2)) { |
577 | $result = method_exists($this->getController(), $this->getAction()); |
578 | } |
579 | if (!($result) && ($this->defaultRoute !== null) && isset($this->defaultRoute['action'])) { |
580 | $result = true; |
581 | } |
582 | } |
583 | |
584 | return $result; |
585 | } |
586 | |
587 | /** |
588 | * Determine if the route has been matched |
589 | * |
590 | * @return bool |
591 | */ |
592 | abstract public function hasRoute(): bool; |
593 | |
594 | /** |
595 | * Prepare the routes |
596 | * |
597 | * @return AbstractMatch |
598 | */ |
599 | abstract public function prepare(): AbstractMatch; |
600 | |
601 | /** |
602 | * Match the route |
603 | * |
604 | * @param mixed $forceRoute |
605 | * @return bool |
606 | */ |
607 | abstract public function match(mixed $forceRoute = null): bool; |
608 | |
609 | /** |
610 | * Method to process if a route was not found |
611 | * |
612 | * @param bool $exit |
613 | * @return void |
614 | */ |
615 | abstract public function noRouteFound(bool $exit = true): void; |
616 | |
617 | } |