Code Coverage |
||||||||||
Lines |
Functions and Methods |
Classes and Traits |
||||||||
Total | |
91.13% |
113 / 124 |
|
86.96% |
20 / 23 |
CRAP | |
0.00% |
0 / 1 |
Pop | |
91.13% |
113 / 124 |
|
86.96% |
20 / 23 |
77.82 | |
0.00% |
0 / 1 |
__construct | |
92.45% |
49 / 53 |
|
0.00% |
0 / 1 |
27.31 | |||
get | |
100.00% |
1 / 1 |
|
100.00% |
1 / 1 |
1 | |||
head | |
100.00% |
1 / 1 |
|
100.00% |
1 / 1 |
1 | |||
post | |
100.00% |
1 / 1 |
|
100.00% |
1 / 1 |
1 | |||
put | |
100.00% |
1 / 1 |
|
100.00% |
1 / 1 |
1 | |||
delete | |
100.00% |
1 / 1 |
|
100.00% |
1 / 1 |
1 | |||
trace | |
100.00% |
1 / 1 |
|
100.00% |
1 / 1 |
1 | |||
options | |
100.00% |
1 / 1 |
|
100.00% |
1 / 1 |
1 | |||
connect | |
100.00% |
1 / 1 |
|
100.00% |
1 / 1 |
1 | |||
patch | |
100.00% |
1 / 1 |
|
100.00% |
1 / 1 |
1 | |||
any | |
100.00% |
1 / 1 |
|
100.00% |
1 / 1 |
1 | |||
addCustomMethod | |
100.00% |
2 / 2 |
|
100.00% |
1 / 1 |
1 | |||
addCustomMethods | |
100.00% |
3 / 3 |
|
100.00% |
1 / 1 |
2 | |||
hasCustomMethod | |
100.00% |
1 / 1 |
|
100.00% |
1 / 1 |
1 | |||
setRoute | |
100.00% |
8 / 8 |
|
100.00% |
1 / 1 |
5 | |||
setRoutes | |
100.00% |
5 / 5 |
|
100.00% |
1 / 1 |
3 | |||
addToAll | |
100.00% |
3 / 3 |
|
100.00% |
1 / 1 |
2 | |||
getRoutes | |
100.00% |
3 / 3 |
|
100.00% |
1 / 1 |
4 | |||
getRoute | |
100.00% |
1 / 1 |
|
100.00% |
1 / 1 |
2 | |||
hasRoute | |
100.00% |
1 / 1 |
|
100.00% |
1 / 1 |
2 | |||
isAllowed | |
69.23% |
9 / 13 |
|
0.00% |
0 / 1 |
8.43 | |||
run | |
80.00% |
12 / 15 |
|
0.00% |
0 / 1 |
5.20 | |||
__call | |
100.00% |
6 / 6 |
|
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 | */ |
14 | namespace Popcorn; |
15 | |
16 | use 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 | */ |
28 | class 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 | } |