Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
91.13% covered (success)
91.13%
113 / 124
86.96% covered (success)
86.96%
20 / 23
CRAP
0.00% covered (danger)
0.00%
0 / 1
Pop
91.13% covered (success)
91.13%
113 / 124
86.96% covered (success)
86.96%
20 / 23
77.82
0.00% covered (danger)
0.00%
0 / 1
 __construct
92.45% covered (success)
92.45%
49 / 53
0.00% covered (danger)
0.00%
0 / 1
27.31
 get
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 head
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 post
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 put
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 delete
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 trace
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 options
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 connect
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 patch
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 any
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 addCustomMethod
100.00% covered (success)
100.00%
2 / 2
100.00% covered (success)
100.00%
1 / 1
1
 addCustomMethods
100.00% covered (success)
100.00%
3 / 3
100.00% covered (success)
100.00%
1 / 1
2
 hasCustomMethod
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 setRoute
100.00% covered (success)
100.00%
8 / 8
100.00% covered (success)
100.00%
1 / 1
5
 setRoutes
100.00% covered (success)
100.00%
5 / 5
100.00% covered (success)
100.00%
1 / 1
3
 addToAll
100.00% covered (success)
100.00%
3 / 3
100.00% covered (success)
100.00%
1 / 1
2
 getRoutes
100.00% covered (success)
100.00%
3 / 3
100.00% covered (success)
100.00%
1 / 1
4
 getRoute
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
2
 hasRoute
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
2
 isAllowed
69.23% covered (warning)
69.23%
9 / 13
0.00% covered (danger)
0.00%
0 / 1
8.43
 run
80.00% covered (success)
80.00%
12 / 15
0.00% covered (danger)
0.00%
0 / 1
5.20
 __call
100.00% covered (success)
100.00%
6 / 6
100.00% covered (success)
100.00%
1 / 1
3
1<?php
2/**
3 * Popcorn Micro-Framework (http://popcorn.popphp.org/)
4 *
5 * @link       https://github.com/popphp/popcorn
6 * @author     Nick Sagona, III <dev@nolainteractive.com>
7 * @copyright  Copyright (c) 2009-2024 NOLA Interactive, LLC. (http://www.nolainteractive.com)
8 * @license    http://popcorn.popphp.org/license     New BSD License
9 */
10
11/**
12 * @namespace
13 */
14namespace Popcorn;
15
16use Pop\Application;
17
18/**
19 * This is the main class for the Popcorn Micro-Framework.
20 *
21 * @category   Popcorn
22 * @package    Popcorn
23 * @author     Nick Sagona, III <dev@nolainteractive.com>
24 * @copyright  Copyright (c) 2009-2024 NOLA Interactive, LLC. (http://www.nolainteractive.com)
25 * @license    http://popcorn.popphp.org/license     New BSD License
26 * @version    4.0.0
27 */
28class Pop extends Application
29{
30
31    /**
32     * Routes array
33     * @var array
34     */
35    protected array $routes = [
36        'get'     => [],
37        'head'    => [],
38        'post'    => [],
39        'put'     => [],
40        'delete'  => [],
41        'trace'   => [],
42        'options' => [],
43        'connect' => [],
44        'patch'   => []
45    ];
46
47    /**
48     * Constructor
49     *
50     * Instantiate an application object
51     *
52     * Optional parameters are a service locator instance, a router instance,
53     * an event manager instance or a configuration object or array
54     *
55     * @throws Exception
56     */
57    public function __construct()
58    {
59        $args = func_get_args();
60
61        foreach ($args as $i => $arg) {
62            if (is_array($arg)) {
63                // Handle custom methods config
64                if (isset($arg['custom_methods'])) {
65                    if (is_array($arg['custom_methods'])) {
66                        $this->addCustomMethods($arg['custom_methods']);
67                    } else {
68                        $this->addCustomMethod($arg['custom_methods']);
69                    }
70                    unset($args[$i]['custom_methods']);
71                }
72
73                // Handle routes config
74                if (isset($arg['routes'])) {
75                    // Check for combined route matches
76                    foreach ($arg['routes'] as $key => $value) {
77                        // Handle route wildcard
78                        if ($key == '*') {
79                            foreach ($arg['routes'][$key] as $route => $controller) {
80                                $this->addToAll($route, $controller);
81                            }
82                            unset($arg['routes'][$key]);
83                        // Handle multiple route methods
84                        } else if (str_contains((string)$key, ',')) {
85                            foreach ($arg['routes'][$key] as $route => $controller) {
86                                $this->setRoutes($key, $route, $controller);
87                            }
88                            unset($arg['routes'][$key]);
89                        // Handle route prefixes
90                        } else if (str_starts_with($key, '/') && is_array($value)) {
91                            foreach ($value as $methods => $methodRoutes) {
92                                foreach ($methodRoutes as $route => $controller) {
93                                    $this->setRoutes($methods, $key . $route, $controller);
94                                }
95                            }
96                            unset($arg['routes'][$key]);
97                        }
98                    }
99
100                    // Check for direct route method matches
101                    $routeKeys = array_keys($this->routes);
102                    foreach ($routeKeys as $key) {
103                        if (isset($arg['routes'][$key])) {
104                            foreach ($arg['routes'][$key] as $route => $controller) {
105                                $this->setRoute($key, $route, $controller);
106                            }
107                            unset($arg['routes'][$key]);
108                        }
109                    }
110
111                    // Check for static routes that are not assigned to a method,
112                    // and auto-assign them to get,post for a fallback
113                    if (count($arg['routes']) > 0) {
114                        foreach ($arg['routes'] as $route => $controller) {
115                            $this->setRoutes('get,post', $route, $controller);
116                        }
117                    }
118
119                    unset($args[$i]['routes']);
120                }
121            }
122        }
123
124        switch (count($args)) {
125            case 1:
126                parent::__construct($args[0]);
127                break;
128            case 2:
129                parent::__construct($args[0], $args[1]);
130                break;
131            case 3:
132                parent::__construct($args[0], $args[1], $args[2]);
133                break;
134            case 4:
135                parent::__construct($args[0], $args[1], $args[2], $args[3]);
136                break;
137            case 5:
138                parent::__construct($args[0], $args[1], $args[2], $args[3], $args[4]);
139                break;
140            case 6:
141                parent::__construct($args[0], $args[1], $args[2], $args[3], $args[4], $args[5]);
142                break;
143            default:
144                parent::__construct();
145        }
146
147    }
148
149    /**
150     * Add a GET route
151     *
152     * @param  string $route
153     * @param  mixed $controller
154     * @throws Exception
155     * @return Pop
156     */
157    public function get(string $route, mixed $controller): Pop
158    {
159        return $this->setRoute('get', $route, $controller);
160    }
161
162    /**
163     * Add a HEAD route
164     *
165     * @param  string $route
166     * @param  mixed  $controller
167     * @throws Exception
168     * @return Pop
169     */
170    public function head(string $route, mixed $controller): Pop
171    {
172        return $this->setRoute('head', $route, $controller);
173    }
174
175    /**
176     * Add a POST route
177     *
178     * @param  string $route
179     * @param  mixed  $controller
180     * @throws Exception
181     * @return Pop
182     */
183    public function post(string $route, mixed $controller): Pop
184    {
185        return $this->setRoute('post', $route, $controller);
186    }
187
188    /**
189     * Add a PUT route
190     *
191     * @param  string $route
192     * @param  mixed  $controller
193     * @throws Exception
194     * @return Pop
195     */
196    public function put(string $route, mixed $controller): Pop
197    {
198        return $this->setRoute('put', $route, $controller);
199    }
200
201    /**
202     * Add a DELETE route
203     *
204     * @param  string $route
205     * @param  mixed  $controller
206     * @throws Exception
207     * @return Pop
208     */
209    public function delete(string $route, mixed $controller): Pop
210    {
211        return $this->setRoute('delete', $route, $controller);
212    }
213
214    /**
215     * Add a TRACE route
216     *
217     * @param  string $route
218     * @param  mixed  $controller
219     * @throws Exception
220     * @return Pop
221     */
222    public function trace(string $route, mixed $controller): Pop
223    {
224        return $this->setRoute('trace', $route, $controller);
225    }
226
227    /**
228     * Add an OPTIONS route
229     *
230     * @param  string $route
231     * @param  mixed  $controller
232     * @throws Exception
233     * @return Pop
234     */
235    public function options(string $route, mixed $controller): Pop
236    {
237        return $this->setRoute('options', $route, $controller);
238    }
239
240    /**
241     * Add a CONNECT route
242     *
243     * @param  string $route
244     * @param  mixed  $controller
245     * @throws Exception
246     * @return Pop
247     */
248    public function connect(string $route, mixed $controller): Pop
249    {
250        return $this->setRoute('connect', $route,  $controller);
251    }
252
253    /**
254     * Add a PATCH route
255     *
256     * @param  string $route
257     * @param  mixed  $controller
258     * @throws Exception
259     * @return Pop
260     */
261    public function patch(string $route, mixed $controller): Pop
262    {
263        return $this->setRoute('patch', $route, $controller);
264    }
265
266    /**
267     * Add to any and all methods (alias method to addToAll)
268     *
269     * @param  string $route
270     * @param  mixed  $controller
271     * @return Pop
272     */
273    public function any(string $route, mixed $controller): Pop
274    {
275        return $this->addToAll($route, $controller);
276    }
277
278    /**
279     * Add a custom method
280     *
281     * @param  string $customMethod
282     * @return Pop
283     */
284    public function addCustomMethod(string $customMethod): Pop
285    {
286        $this->routes[strtolower($customMethod)] = [];
287        return $this;
288    }
289
290    /**
291     * Add custom methods
292     *
293     * @param  array $customMethods
294     * @return Pop
295     */
296    public function addCustomMethods(array $customMethods): Pop
297    {
298        foreach ($customMethods as $customMethod) {
299            $this->addCustomMethod($customMethod);
300        }
301        return $this;
302    }
303
304    /**
305     * Has a custom method
306     *
307     * @param  string $customMethod
308     * @return bool
309     */
310    public function hasCustomMethod(string $customMethod): bool
311    {
312        return isset($this->routes[strtolower($customMethod)]);
313    }
314
315    /**
316     * Add a route
317     *
318     * @param  string $method
319     * @param  string $route
320     * @param  mixed  $controller
321     * @throws Exception
322     * @return Pop
323     */
324    public function setRoute(string $method, string $route, mixed $controller): Pop
325    {
326        if (!array_key_exists(strtolower((string)$method), $this->routes)) {
327            throw new Exception("Error: The method '" . $method . "' is not allowed.");
328        }
329
330        if (is_callable($controller)) {
331            $controller = ['controller' => $controller];
332        }
333
334        if (isset($this->routes[$method][$route]) && is_array($this->routes[$method][$route])) {
335            $this->routes[$method][$route] = array_merge($this->routes[$method][$route], $controller);
336        } else {
337            $this->routes[$method][$route] = $controller;
338        }
339
340        return $this;
341    }
342
343    /**
344     * Add multiple routes
345     *
346     * @param  array|string $methods
347     * @param  string       $route
348     * @param  mixed        $controller
349     * @throws Exception
350     * @return Pop
351     */
352    public function setRoutes(array|string $methods, string $route, mixed $controller): Pop
353    {
354        if (is_string($methods)) {
355            $methods = array_map('trim', explode(',', strtolower($methods)));
356        }
357
358        foreach ($methods as $method) {
359            $this->setRoute($method, $route, $controller);
360        }
361        return $this;
362    }
363
364    /**
365     * Add to all methods
366     *
367     * @param  string $route
368     * @param  mixed  $controller
369     * @throws Exception
370     * @return Pop
371     */
372    public function addToAll(string $route, mixed $controller): Pop
373    {
374        foreach ($this->routes as $method => $value) {
375            $this->setRoute($method, $route, $controller);
376        }
377        return $this;
378    }
379
380    /**
381     * Method to get all routes
382     *
383     * @param  ?string $method
384     * @throws Exception
385     * @return array
386     */
387    public function getRoutes(?string $method = null): array
388    {
389        if (($method !== null) && !array_key_exists(strtolower($method), $this->routes)) {
390            throw new Exception("Error: The method '" . strtoupper($method) . "' is not allowed.");
391        }
392        return ($method !== null) ? $this->routes[$method] : $this->routes;
393    }
394
395    /**
396     * Method to get a route by method
397     *
398     * @param  string $method
399     * @param  string $route
400     * @return mixed
401     */
402    public function getRoute(string $method, string $route): mixed
403    {
404        return ($this->hasRoute($method, $route)) ? $this->routes[$method][$route] : null;
405    }
406
407    /**
408     * Method to determine if the application has a route
409     *
410     * @param  string $method
411     * @param  string $route
412     * @return bool
413     */
414    public function hasRoute(string $method, string $route): bool
415    {
416        return (isset($this->routes[$method]) && isset($this->routes[$method][$route]));
417    }
418
419    /**
420     * Determine if the route is allowed on for the method
421     *
422     * @param  ?string $route
423     * @return bool
424     */
425    public function isAllowed(?string $route = null): bool
426    {
427        $allowed = false;
428        $method  = strtolower($_SERVER['REQUEST_METHOD']);
429        $route   = (string)$route;
430
431        foreach ($this->routes[$method] as $rte => $ctrl) {
432            if (is_array($ctrl) && !isset($ctrl['controller'])) {
433                foreach ($ctrl as $r => $c) {
434                    if (str_starts_with($rte . $r, $route)) {
435                        $allowed = true;
436                        break;
437                    }
438                }
439            } else if (str_starts_with($rte, $route)) {
440                $allowed = true;
441                break;
442            }
443        }
444
445        return $allowed;
446    }
447
448    /**
449     * Run the application.
450     *
451     * @param bool $exit
452     * @param  ?string $forceRoute
453     * @throws Exception|\Pop\Event\Exception|\Pop\Router\Exception|\ReflectionException
454     * @return void
455     */
456    public function run(bool $exit = true, ?string $forceRoute = null): void
457    {
458        // If method is not allowed
459        if (!isset($this->routes[strtolower((string)$_SERVER['REQUEST_METHOD'])])) {
460            throw new Exception(
461                "Error: The method '" . strtoupper((string)$_SERVER['REQUEST_METHOD']) . "' is not allowed.", 405
462            );
463        }
464
465        // Route request
466        $this->router->addRoutes($this->routes[strtolower((string)$_SERVER['REQUEST_METHOD'])]);
467        $this->router->route();
468
469        // If route is allowed for this method
470        if ($this->router->hasRoute() && $this->isAllowed($this->router->getRouteMatch()->getOriginalRoute())) {
471            parent::run($exit, $forceRoute);
472        // Else, handle error
473        } else {
474            if ($this->router->hasRoute()) {
475                $message = "Error: The route '" . $_SERVER['REQUEST_URI'] .
476                    "' is not allowed on the '" . strtoupper((string)$_SERVER['REQUEST_METHOD']) . "' method";
477            } else {
478                $message = "Error: That route '" . $_SERVER['REQUEST_URI'] . "' was not found for the '" .
479                    strtoupper((string)$_SERVER['REQUEST_METHOD']) . "' method";
480            }
481
482            $this->trigger('app.error', ['exception' => new Exception($message, 404)]);
483            $this->router->getRouteMatch()->noRouteFound((bool)$exit);
484        }
485    }
486
487    /**
488     * Magic method to check for a custom method
489     *
490     * @param  string $methodName
491     * @param  array  $arguments
492     * @throws Exception
493     * @return void
494     */
495    public function __call(string $methodName, array $arguments): void
496    {
497        if (!isset($this->routes[strtolower($methodName)])) {
498            throw new Exception("Error: The custom method '" . strtoupper($methodName) . "' is not allowed.");
499        }
500
501        if (count($arguments) != 2) {
502            throw new Exception("Error: You must pass a route and a controller.");
503        }
504
505        [$route, $controller] = $arguments;
506
507        $this->setRoute(strtolower((string)$methodName), $route, $controller);
508    }
509
510}