Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
100.00% covered (success)
100.00%
67 / 67
100.00% covered (success)
100.00%
26 / 26
CRAP
100.00% covered (success)
100.00%
1 / 1
Router
100.00% covered (success)
100.00%
67 / 67
100.00% covered (success)
100.00%
26 / 26
46
100.00% covered (success)
100.00%
1 / 1
 __construct
100.00% covered (success)
100.00%
7 / 7
100.00% covered (success)
100.00%
1 / 1
5
 addRoute
100.00% covered (success)
100.00%
2 / 2
100.00% covered (success)
100.00%
1 / 1
1
 addRoutes
100.00% covered (success)
100.00%
2 / 2
100.00% covered (success)
100.00%
1 / 1
1
 name
100.00% covered (success)
100.00%
2 / 2
100.00% covered (success)
100.00%
1 / 1
1
 hasName
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 getUrl
100.00% covered (success)
100.00%
3 / 3
100.00% covered (success)
100.00%
1 / 1
2
 addControllerParams
100.00% covered (success)
100.00%
2 / 2
100.00% covered (success)
100.00%
1 / 1
1
 appendControllerParams
100.00% covered (success)
100.00%
2 / 2
100.00% covered (success)
100.00%
1 / 1
1
 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%
2 / 2
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
 getRouteMatch
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 hasRoute
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 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
 getController
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 hasController
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 getAction
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 hasAction
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 getControllerClass
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 isCli
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 isHttp
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 prepare
100.00% covered (success)
100.00%
2 / 2
100.00% covered (success)
100.00%
1 / 1
1
 route
100.00% covered (success)
100.00%
27 / 27
100.00% covered (success)
100.00%
1 / 1
16
 noRouteFound
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
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-2026 NOLA Interactive, LLC.
8 * @license    https://www.popphp.org/license     New BSD License
9 */
10
11/**
12 * @namespace
13 */
14namespace Pop\Router;
15
16use Closure;
17use ReflectionException;
18use Pop\App;
19use Pop\Utils\Arr;
20
21/**
22 * Pop router class
23 *
24 * @category   Pop
25 * @package    Pop\Router
26 * @author     Nick Sagona, III <dev@noladev.com>
27 * @copyright  Copyright (c) 2009-2026 NOLA Interactive, LLC.
28 * @license    https://www.popphp.org/license     New BSD License
29 * @version    4.4.0
30 */
31class Router
32{
33
34    /**
35     * Route match object
36     * @var ?Match\MatchInterface
37     */
38    protected ?Match\MatchInterface $routeMatch = null;
39
40    /**
41     * Controller object
42     * @var mixed
43     */
44    protected mixed $controller = null;
45
46    /**
47     * Action
48     * @var mixed
49     */
50    protected mixed $action = null;
51
52    /**
53     * Controller class
54     * @var ?string
55     */
56    protected ?string $controllerClass = null;
57
58    /**
59     * Constructor
60     *
61     * Instantiate the router object
62     *
63     * @param  ?array               $routes
64     * @param  ?Match\AbstractMatch $match
65     */
66    public function __construct(?array $routes = null, ?Match\AbstractMatch $match = null)
67    {
68        if ($match !== null) {
69            $this->routeMatch = $match;
70        } else {
71            $this->routeMatch = ((stripos(php_sapi_name(), 'cli') !== false) &&
72                (stripos(php_sapi_name(), 'server') === false)) ?
73                new Match\Cli() : new Match\Http();
74        }
75
76        if ($routes !== null) {
77            $this->addRoutes($routes);
78        }
79    }
80
81    /**
82     * Add a route
83     *
84     * @param  string $route
85     * @param  mixed  $controller
86     * @return static
87     */
88    public function addRoute(string $route, mixed $controller): static
89    {
90        $this->routeMatch->addRoute($route, $controller);
91        return $this;
92    }
93
94    /**
95     * Add multiple controller routes
96     *
97     * @param  array $routes
98     * @return static
99     */
100    public function addRoutes(array $routes): static
101    {
102        $this->routeMatch->addRoutes($routes);
103        return $this;
104    }
105
106    /**
107     * Add a route name
108     *
109     * @param  string $routeName
110     * @return Router
111     */
112    public function name(string $routeName): static
113    {
114        $this->routeMatch->name($routeName);
115        return $this;
116    }
117
118    /**
119     * Has a route name
120     *
121     * @param  string $routeName
122     * @return bool
123     */
124    public function hasName(string $routeName): bool
125    {
126        return $this->routeMatch->hasName($routeName);
127    }
128
129    /**
130     * Get URL for the named route
131     *
132     * @param  string $routeName
133     * @param  mixed  $params
134     * @param  bool   $fqdn
135     * @throws Exception
136     * @return string
137     */
138    public function getUrl(string $routeName, mixed $params = null, bool $fqdn = false): string
139    {
140        if (!$this->isHttp()) {
141            throw new Exception('Error: The route is not HTTP.');
142        }
143        return $this->routeMatch->getUrl($routeName, $params, $fqdn);
144    }
145
146    /**
147     * Add controller params to be passed into a new controller instance
148     *
149     * @param  string $controller
150     * @param  mixed  $params
151     * @return static
152     */
153    public function addControllerParams(string $controller, mixed $params): static
154    {
155        $this->routeMatch->addControllerParams($controller, $params);
156        return $this;
157    }
158
159    /**
160     * Append controller params to be passed into a new controller instance
161     *
162     * @param  string $controller
163     * @param  mixed  $params
164     * @return static
165     */
166    public function appendControllerParams(string $controller, mixed $params): static
167    {
168        $this->routeMatch->appendControllerParams($controller, $params);
169        return $this;
170    }
171
172    /**
173     * Get the params assigned to the controller
174     *
175     * @param  string $controller
176     * @return mixed
177     */
178    public function getControllerParams(string $controller): mixed
179    {
180        return $this->routeMatch->getControllerParams($controller);
181    }
182
183    /**
184     * Determine if the controller has params
185     *
186     * @param  string $controller
187     * @return bool
188     */
189    public function hasControllerParams(string $controller): bool
190    {
191        return $this->routeMatch->hasControllerParams($controller);
192    }
193
194    /**
195     * Remove controller params
196     *
197     * @param  string $controller
198     * @return static
199     */
200    public function removeControllerParams(string $controller): static
201    {
202        $this->routeMatch->removeControllerParams($controller);
203        return $this;
204    }
205
206    /**
207     * Get routes
208     *
209     * @return array
210     */
211    public function getRoutes(): array
212    {
213        return $this->routeMatch->getRoutes();
214    }
215
216    /**
217     * Get route match object
218     *
219     * @return Match\MatchInterface
220     */
221    public function getRouteMatch(): Match\MatchInterface
222    {
223        return $this->routeMatch;
224    }
225
226    /**
227     * Determine if there is a route match
228     *
229     * @return bool
230     */
231    public function hasRoute(): bool
232    {
233        return $this->routeMatch->hasRoute();
234    }
235
236    /**
237     * Get the params discovered from the route
238     *
239     * @return array
240     */
241    public function getRouteParams(): array
242    {
243        return $this->routeMatch->getRouteParams();
244    }
245
246    /**
247     * Determine if the route has params
248     *
249     * @return bool
250     */
251    public function hasRouteParams(): bool
252    {
253        return $this->routeMatch->hasRouteParams();
254    }
255
256    /**
257     * Get the current controller object
258     *
259     * @return mixed
260     */
261    public function getController(): mixed
262    {
263        return $this->controller;
264    }
265
266    /**
267     * Determine if the router has a controller
268     *
269     * @return bool
270     */
271    public function hasController(): bool
272    {
273        return ($this->controller !== null);
274    }
275
276    /**
277     * Get the action
278     *
279     * @return mixed
280     */
281    public function getAction(): mixed
282    {
283        return $this->action;
284    }
285
286    /**
287     * Determine if the router has an action
288     *
289     * @return bool
290     */
291    public function hasAction(): bool
292    {
293        return ($this->action !== null);
294    }
295
296    /**
297     * Get the current controller class name
298     *
299     * @return string
300     */
301    public function getControllerClass(): string
302    {
303        return $this->controllerClass;
304    }
305
306    /**
307     * Determine if the route is CLI
308     *
309     * @return bool
310     */
311    public function isCli(): bool
312    {
313        return ($this->routeMatch instanceof Match\Cli);
314    }
315
316    /**
317     * Determine if the route is HTTP
318     *
319     * @return bool
320     */
321    public function isHttp(): bool
322    {
323        return ($this->routeMatch instanceof Match\Http);
324    }
325
326    /**
327     * Prepare routes
328     *
329     * @return static
330     */
331    public function prepare(): static
332    {
333        $this->routeMatch->prepare();
334        return $this;
335    }
336
337    /**
338     * Route to the correct controller
339     *
340     * @param  ?string $forceRoute
341     * @throws Exception|ReflectionException
342     * @return void
343     */
344    public function route(?string $forceRoute = null): void
345    {
346        if ($this->routeMatch->match($forceRoute)) {
347            if ($this->routeMatch->hasController()) {
348                $controller         = $this->routeMatch->getController();
349                $application        = App::get();
350                $middlewareDisabled = $application->env('MIDDLEWARE_DISABLED');
351
352                $routeConfig = $this->routeMatch->getRouteConfig();
353                if (!empty($routeConfig['middleware']) && ($middlewareDisabled != 'route') && ($middlewareDisabled != 'all')) {
354                    $application->middleware->addItems(Arr::make($routeConfig['middleware']));
355                }
356
357                if ($controller instanceof Closure) {
358                    $this->controllerClass = 'Closure';
359                    $this->controller      = $controller;
360                } else if (class_exists($controller)) {
361                    $this->controllerClass = $controller;
362                    $controllerParams      = null;
363
364                    if ($this->routeMatch->hasControllerParams($controller)) {
365                        $controllerParams = $this->routeMatch->getControllerParams($controller);
366                    } else if ($this->routeMatch->hasControllerParams('*')) {
367                        $controllerParams = $this->routeMatch->getControllerParams('*');
368                    }
369
370                    if ($controllerParams !== null) {
371                        $this->controller = (new \ReflectionClass($controller))->newInstanceArgs($controllerParams);
372                    } else {
373                        $this->controller = (class_uses($controller, 'Pop\Controller\HttpControllerTrait') ||
374                            class_uses($controller, 'Pop\Controller\ConsoleControllerTrait')) ?
375                            new $controller($application) : new $controller();
376                    }
377
378                    if (!($this->controller instanceof \Pop\Controller\ControllerInterface)) {
379                        throw new Exception('Error: The controller must be an instance of Pop\Controller\Interface');
380                    }
381
382                    $action       = $this->routeMatch->getAction();
383                    $this->action = (($action === null) && ($this->routeMatch->isDynamicRoute())) ? 'index' : $action;
384                }
385            }
386        }
387    }
388
389    /**
390     * Method to process if a route was not found
391     *
392     * @param  bool $exit
393     * @return void
394     */
395    public function noRouteFound(bool $exit = true): void
396    {
397        $this->routeMatch->noRouteFound($exit);
398    }
399
400}