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