Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
93.02% covered (success)
93.02%
120 / 129
87.10% covered (success)
87.10%
27 / 31
CRAP
0.00% covered (danger)
0.00%
0 / 1
AbstractMatch
93.02% covered (success)
93.02%
120 / 129
87.10% covered (success)
87.10%
27 / 31
117.34
0.00% covered (danger)
0.00%
0 / 1
 addRoute
100.00% covered (success)
100.00%
24 / 24
100.00% covered (success)
100.00%
1 / 1
18
 addRoutes
100.00% covered (success)
100.00%
3 / 3
100.00% covered (success)
100.00%
1 / 1
2
 name
100.00% covered (success)
100.00%
4 / 4
100.00% covered (success)
100.00%
1 / 1
2
 hasName
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 addControllerParams
100.00% covered (success)
100.00%
4 / 4
100.00% covered (success)
100.00%
1 / 1
2
 appendControllerParams
100.00% covered (success)
100.00%
5 / 5
100.00% covered (success)
100.00%
1 / 1
3
 getControllerParams
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 hasControllerParams
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 removeControllerParams
100.00% covered (success)
100.00%
3 / 3
100.00% covered (success)
100.00%
1 / 1
2
 getRouteString
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 getSegments
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 getSegment
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 getOriginalRoute
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 getRoute
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 getRoutes
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 getPreparedRoutes
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 hasRouteConfig
100.00% covered (success)
100.00%
4 / 4
100.00% covered (success)
100.00%
1 / 1
6
 getRouteConfig
100.00% covered (success)
100.00%
5 / 5
100.00% covered (success)
100.00%
1 / 1
4
 getFlattenedRoutes
100.00% covered (success)
100.00%
6 / 6
100.00% covered (success)
100.00%
1 / 1
3
 getRouteParams
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 hasRouteParams
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 getDefaultRoute
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 hasDefaultRoute
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 getDynamicRoute
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 getDynamicRoutePrefix
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 hasDynamicRoute
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 isDynamicRoute
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 getController
77.78% covered (success)
77.78%
14 / 18
0.00% covered (danger)
0.00%
0 / 1
20.17
 hasController
86.67% covered (success)
86.67%
13 / 15
0.00% covered (danger)
0.00%
0 / 1
16.61
 getAction
80.00% covered (success)
80.00%
8 / 10
0.00% covered (danger)
0.00%
0 / 1
10.80
 hasAction
90.00% covered (success)
90.00%
9 / 10
0.00% covered (danger)
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 */
14namespace 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 */
26abstract 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}