Code Coverage |
||||||||||
Lines |
Functions and Methods |
Classes and Traits |
||||||||
Total | |
93.64% |
103 / 110 |
|
90.91% |
20 / 22 |
CRAP | |
0.00% |
0 / 1 |
Pop | |
93.64% |
103 / 110 |
|
90.91% |
20 / 22 |
67.12 | |
0.00% |
0 / 1 |
__construct | |
100.00% |
42 / 42 |
|
100.00% |
1 / 1 |
21 | |||
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 | |||
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) && 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 | } |