Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
93.64% covered (success)
93.64%
103 / 110
90.91% covered (success)
90.91%
20 / 22
CRAP
0.00% covered (danger)
0.00%
0 / 1
Pop
93.64% covered (success)
93.64%
103 / 110
90.91% covered (success)
90.91%
20 / 22
67.12
0.00% covered (danger)
0.00%
0 / 1
 __construct
100.00% covered (success)
100.00%
42 / 42
100.00% covered (success)
100.00%
1 / 1
21
 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
 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) && isset($arg['routes'])) {
63                // Check for combined route matches
64                foreach ($arg['routes'] as $key => $value) {
65                    if ($key == '*') {
66                        foreach ($arg['routes'][$key] as $route => $controller) {
67                            $this->addToAll($route, $controller);
68                        }
69                        unset($arg['routes'][$key]);
70                    } else if (str_contains((string)$key, ',')) {
71                        foreach ($arg['routes'][$key] as $route => $controller) {
72                            $this->setRoutes($key, $route, $controller);
73                        }
74                        unset($arg['routes'][$key]);
75                    }
76                }
77
78                // Check for direct route method matches
79                $routeKeys = array_keys($this->routes);
80                foreach ($routeKeys as $key) {
81                    if (isset($arg['routes'][$key])) {
82                        foreach ($arg['routes'][$key] as $route => $controller) {
83                            $this->setRoute($key, $route, $controller);
84                        }
85                        unset($arg['routes'][$key]);
86                    }
87                }
88
89                // Check for static routes that are not assigned to a method,
90                // and auto-assign them to get,post for a fallback
91                if (count($arg['routes']) > 0) {
92                    foreach ($arg['routes'] as $route => $controller) {
93                        $this->setRoutes('get,post', $route, $controller);
94                    }
95                }
96
97                unset($args[$i]['routes']);
98            }
99        }
100
101        switch (count($args)) {
102            case 1:
103                parent::__construct($args[0]);
104                break;
105            case 2:
106                parent::__construct($args[0], $args[1]);
107                break;
108            case 3:
109                parent::__construct($args[0], $args[1], $args[2]);
110                break;
111            case 4:
112                parent::__construct($args[0], $args[1], $args[2], $args[3]);
113                break;
114            case 5:
115                parent::__construct($args[0], $args[1], $args[2], $args[3], $args[4]);
116                break;
117            case 6:
118                parent::__construct($args[0], $args[1], $args[2], $args[3], $args[4], $args[5]);
119                break;
120            default:
121                parent::__construct();
122        }
123
124    }
125
126    /**
127     * Add a GET route
128     *
129     * @param  string $route
130     * @param  mixed $controller
131     * @throws Exception
132     * @return Pop
133     */
134    public function get(string $route, mixed $controller): Pop
135    {
136        return $this->setRoute('get', $route, $controller);
137    }
138
139    /**
140     * Add a HEAD route
141     *
142     * @param  string $route
143     * @param  mixed  $controller
144     * @throws Exception
145     * @return Pop
146     */
147    public function head(string $route, mixed $controller): Pop
148    {
149        return $this->setRoute('head', $route, $controller);
150    }
151
152    /**
153     * Add a POST route
154     *
155     * @param  string $route
156     * @param  mixed  $controller
157     * @throws Exception
158     * @return Pop
159     */
160    public function post(string $route, mixed $controller): Pop
161    {
162        return $this->setRoute('post', $route, $controller);
163    }
164
165    /**
166     * Add a PUT route
167     *
168     * @param  string $route
169     * @param  mixed  $controller
170     * @throws Exception
171     * @return Pop
172     */
173    public function put(string $route, mixed $controller): Pop
174    {
175        return $this->setRoute('put', $route, $controller);
176    }
177
178    /**
179     * Add a DELETE route
180     *
181     * @param  string $route
182     * @param  mixed  $controller
183     * @throws Exception
184     * @return Pop
185     */
186    public function delete(string $route, mixed $controller): Pop
187    {
188        return $this->setRoute('delete', $route, $controller);
189    }
190
191    /**
192     * Add a TRACE route
193     *
194     * @param  string $route
195     * @param  mixed  $controller
196     * @throws Exception
197     * @return Pop
198     */
199    public function trace(string $route, mixed $controller): Pop
200    {
201        return $this->setRoute('trace', $route, $controller);
202    }
203
204    /**
205     * Add an OPTIONS route
206     *
207     * @param  string $route
208     * @param  mixed  $controller
209     * @throws Exception
210     * @return Pop
211     */
212    public function options(string $route, mixed $controller): Pop
213    {
214        return $this->setRoute('options', $route, $controller);
215    }
216
217    /**
218     * Add a CONNECT route
219     *
220     * @param  string $route
221     * @param  mixed  $controller
222     * @throws Exception
223     * @return Pop
224     */
225    public function connect(string $route, mixed $controller): Pop
226    {
227        return $this->setRoute('connect', $route,  $controller);
228    }
229
230    /**
231     * Add a PATCH route
232     *
233     * @param  string $route
234     * @param  mixed  $controller
235     * @throws Exception
236     * @return Pop
237     */
238    public function patch(string $route, mixed $controller): Pop
239    {
240        return $this->setRoute('patch', $route, $controller);
241    }
242
243    /**
244     * Add to any and all methods (alias method to addToAll)
245     *
246     * @param  string $route
247     * @param  mixed  $controller
248     * @return Pop
249     */
250    public function any(string $route, mixed $controller): Pop
251    {
252        return $this->addToAll($route, $controller);
253    }
254
255    /**
256     * Add a custom method
257     *
258     * @param  string $customMethod
259     * @return Pop
260     */
261    public function addCustomMethod(string $customMethod): Pop
262    {
263        $this->routes[strtolower($customMethod)] = [];
264        return $this;
265    }
266
267    /**
268     * Has a custom method
269     *
270     * @param  string $customMethod
271     * @return bool
272     */
273    public function hasCustomMethod(string $customMethod): bool
274    {
275        return isset($this->routes[strtolower($customMethod)]);
276    }
277
278    /**
279     * Add a route
280     *
281     * @param  string $method
282     * @param  string $route
283     * @param  mixed  $controller
284     * @throws Exception
285     * @return Pop
286     */
287    public function setRoute(string $method, string $route, mixed $controller): Pop
288    {
289        if (!array_key_exists(strtolower((string)$method), $this->routes)) {
290            throw new Exception("Error: The method '" . $method . "' is not allowed.");
291        }
292
293        if (is_callable($controller)) {
294            $controller = ['controller' => $controller];
295        }
296
297        if (isset($this->routes[$method][$route]) && is_array($this->routes[$method][$route])) {
298            $this->routes[$method][$route] = array_merge($this->routes[$method][$route], $controller);
299        } else {
300            $this->routes[$method][$route] = $controller;
301        }
302
303        return $this;
304    }
305
306    /**
307     * Add multiple routes
308     *
309     * @param  array|string $methods
310     * @param  string       $route
311     * @param  mixed        $controller
312     * @throws Exception
313     * @return Pop
314     */
315    public function setRoutes(array|string $methods, string $route, mixed $controller): Pop
316    {
317        if (is_string($methods)) {
318            $methods = array_map('trim', explode(',', strtolower($methods)));
319        }
320
321        foreach ($methods as $method) {
322            $this->setRoute($method, $route, $controller);
323        }
324        return $this;
325    }
326
327    /**
328     * Add to all methods
329     *
330     * @param  string $route
331     * @param  mixed  $controller
332     * @throws Exception
333     * @return Pop
334     */
335    public function addToAll(string $route, mixed $controller): Pop
336    {
337        foreach ($this->routes as $method => $value) {
338            $this->setRoute($method, $route, $controller);
339        }
340        return $this;
341    }
342
343    /**
344     * Method to get all routes
345     *
346     * @param  ?string $method
347     * @throws Exception
348     * @return array
349     */
350    public function getRoutes(?string $method = null): array
351    {
352        if (($method !== null) && !array_key_exists(strtolower($method), $this->routes)) {
353            throw new Exception("Error: The method '" . strtoupper($method) . "' is not allowed.");
354        }
355        return ($method !== null) ? $this->routes[$method] : $this->routes;
356    }
357
358    /**
359     * Method to get a route by method
360     *
361     * @param  string $method
362     * @param  string $route
363     * @return mixed
364     */
365    public function getRoute(string $method, string $route): mixed
366    {
367        return ($this->hasRoute($method, $route)) ? $this->routes[$method][$route] : null;
368    }
369
370    /**
371     * Method to determine if the application has a route
372     *
373     * @param  string $method
374     * @param  string $route
375     * @return bool
376     */
377    public function hasRoute(string $method, string $route): bool
378    {
379        return (isset($this->routes[$method]) && isset($this->routes[$method][$route]));
380    }
381
382    /**
383     * Determine if the route is allowed on for the method
384     *
385     * @param  ?string $route
386     * @return bool
387     */
388    public function isAllowed(?string $route = null): bool
389    {
390        $allowed = false;
391        $method  = strtolower($_SERVER['REQUEST_METHOD']);
392        $route   = (string)$route;
393
394        foreach ($this->routes[$method] as $rte => $ctrl) {
395            if (is_array($ctrl) && !isset($ctrl['controller'])) {
396                foreach ($ctrl as $r => $c) {
397                    if (str_starts_with($rte . $r, $route)) {
398                        $allowed = true;
399                        break;
400                    }
401                }
402            } else if (str_starts_with($rte, $route)) {
403                $allowed = true;
404                break;
405            }
406        }
407
408        return $allowed;
409    }
410
411    /**
412     * Run the application.
413     *
414     * @param bool $exit
415     * @param  ?string $forceRoute
416     * @throws Exception|\Pop\Event\Exception|\Pop\Router\Exception|\ReflectionException
417     * @return void
418     */
419    public function run(bool $exit = true, ?string $forceRoute = null): void
420    {
421        // If method is not allowed
422        if (!isset($this->routes[strtolower((string)$_SERVER['REQUEST_METHOD'])])) {
423            throw new Exception(
424                "Error: The method '" . strtoupper((string)$_SERVER['REQUEST_METHOD']) . "' is not allowed.", 405
425            );
426        }
427
428        // Route request
429        $this->router->addRoutes($this->routes[strtolower((string)$_SERVER['REQUEST_METHOD'])]);
430        $this->router->route();
431
432        // If route is allowed for this method
433        if ($this->router->hasRoute() && $this->isAllowed($this->router->getRouteMatch()->getOriginalRoute())) {
434            parent::run($exit, $forceRoute);
435        // Else, handle error
436        } else {
437            if ($this->router->hasRoute()) {
438                $message = "Error: The route '" . $_SERVER['REQUEST_URI'] .
439                    "' is not allowed on the '" . strtoupper((string)$_SERVER['REQUEST_METHOD']) . "' method";
440            } else {
441                $message = "Error: That route '" . $_SERVER['REQUEST_URI'] . "' was not found for the '" .
442                    strtoupper((string)$_SERVER['REQUEST_METHOD']) . "' method";
443            }
444
445            $this->trigger('app.error', ['exception' => new Exception($message, 404)]);
446            $this->router->getRouteMatch()->noRouteFound((bool)$exit);
447        }
448    }
449
450    /**
451     * Magic method to check for a custom method
452     *
453     * @param  string $methodName
454     * @param  array  $arguments
455     * @throws Exception
456     * @return void
457     */
458    public function __call(string $methodName, array $arguments): void
459    {
460        if (!isset($this->routes[strtolower($methodName)])) {
461            throw new Exception("Error: The custom method '" . strtoupper($methodName) . "' is not allowed.");
462        }
463
464        if (count($arguments) != 2) {
465            throw new Exception("Error: You must pass a route and a controller.");
466        }
467
468        [$route, $controller] = $arguments;
469
470        $this->setRoute(strtolower((string)$methodName), $route, $controller);
471    }
472
473}