Code Coverage |
||||||||||
Lines |
Functions and Methods |
Classes and Traits |
||||||||
Total | |
91.41% |
181 / 198 |
|
73.33% |
11 / 15 |
CRAP | |
0.00% |
0 / 1 |
Cli | |
91.41% |
181 / 198 |
|
73.33% |
11 / 15 |
106.33 | |
0.00% |
0 / 1 |
__construct | |
100.00% |
5 / 5 |
|
100.00% |
1 / 1 |
1 | |||
prepare | |
100.00% |
2 / 2 |
|
100.00% |
1 / 1 |
1 | |||
match | |
100.00% |
10 / 10 |
|
100.00% |
1 / 1 |
7 | |||
hasRoute | |
100.00% |
3 / 3 |
|
100.00% |
1 / 1 |
6 | |||
getCommands | |
100.00% |
1 / 1 |
|
100.00% |
1 / 1 |
1 | |||
getCommandParameters | |
100.00% |
1 / 1 |
|
100.00% |
1 / 1 |
1 | |||
getCommandOptions | |
100.00% |
1 / 1 |
|
100.00% |
1 / 1 |
1 | |||
getParameters | |
100.00% |
3 / 3 |
|
100.00% |
1 / 1 |
1 | |||
getParameter | |
100.00% |
1 / 1 |
|
100.00% |
1 / 1 |
1 | |||
getOptions | |
100.00% |
1 / 1 |
|
100.00% |
1 / 1 |
1 | |||
getOption | |
100.00% |
1 / 1 |
|
100.00% |
1 / 1 |
1 | |||
noRouteFound | |
75.00% |
6 / 8 |
|
0.00% |
0 / 1 |
4.25 | |||
flattenRoutes | |
76.92% |
10 / 13 |
|
0.00% |
0 / 1 |
8.79 | |||
getRouteRegex | |
90.59% |
77 / 85 |
|
0.00% |
0 / 1 |
27.61 | |||
parseRouteParams | |
93.65% |
59 / 63 |
|
0.00% |
0 / 1 |
39.39 |
1 | <?php |
2 | /** |
3 | * Pop PHP Framework (https://www.popphp.org/) |
4 | * |
5 | * @link https://github.com/popphp/popphp-framework |
6 | * @author Nick Sagona, III <dev@noladev.com> |
7 | * @copyright Copyright (c) 2009-2025 NOLA Interactive, LLC. |
8 | * @license https://www.popphp.org/license New BSD License |
9 | */ |
10 | |
11 | /** |
12 | * @namespace |
13 | */ |
14 | namespace Pop\Router\Match; |
15 | |
16 | /** |
17 | * Pop router CLI match class |
18 | * |
19 | * @category Pop |
20 | * @package Pop\Router |
21 | * @author Nick Sagona, III <dev@noladev.com> |
22 | * @copyright Copyright (c) 2009-2025 NOLA Interactive, LLC. |
23 | * @license https://www.popphp.org/license New BSD License |
24 | * @version 4.3.7 |
25 | */ |
26 | class Cli extends AbstractMatch |
27 | { |
28 | |
29 | /** |
30 | * Route commands |
31 | * @var array |
32 | */ |
33 | protected array $commands = []; |
34 | |
35 | /** |
36 | * Allowed route options |
37 | * @var array |
38 | */ |
39 | protected array $options = [ |
40 | 'options' => [], // [-v|--verbose] |
41 | 'values' => [], // [-n|--name=] |
42 | 'arrays' => [] // [-i|--id=*] |
43 | ]; |
44 | |
45 | /** |
46 | * Allowed route parameters |
47 | * @var array |
48 | */ |
49 | protected array $parameters = []; |
50 | |
51 | /** |
52 | * Flag for all required parameters |
53 | * @var bool |
54 | */ |
55 | protected bool $hasAllRequired = true; |
56 | |
57 | /** |
58 | * Constructor |
59 | * |
60 | * cmd required command |
61 | * [cmd] optional command |
62 | * cmd1|cmd2 required command with alternate options |
63 | * [cmd1|cmd2] optional command with alternate options |
64 | * |
65 | * [-o|--option] option flag |
66 | * [-o|--option=] option value |
67 | * [-o|--option=*] multiple option values |
68 | * |
69 | * <param> required parameter |
70 | * [<param>] optional parameter |
71 | * |
72 | * |
73 | * Instantiate the CLI match object |
74 | */ |
75 | public function __construct() |
76 | { |
77 | $argv = $_SERVER['argv']; |
78 | |
79 | // Trim the script name out of the arguments array |
80 | array_shift($argv); |
81 | |
82 | $this->segments = $argv; |
83 | $this->routeString = implode(' ', $argv); |
84 | |
85 | return $this; |
86 | } |
87 | |
88 | /** |
89 | * Prepare the routes |
90 | * |
91 | * @return static |
92 | */ |
93 | public function prepare(): static |
94 | { |
95 | $this->flattenRoutes($this->routes); |
96 | return $this; |
97 | } |
98 | |
99 | /** |
100 | * Match the route |
101 | * |
102 | * @param mixed $forceRoute |
103 | * @return bool |
104 | */ |
105 | public function match(mixed $forceRoute = null): bool |
106 | { |
107 | if (count($this->preparedRoutes) == 0) { |
108 | $this->prepare(); |
109 | } |
110 | |
111 | $routeToMatch = ($forceRoute !== null) ? $forceRoute : $this->routeString; |
112 | |
113 | foreach ($this->preparedRoutes as $regex => $controller) { |
114 | if (preg_match($regex, $routeToMatch) != 0) { |
115 | $this->route = $regex; |
116 | break; |
117 | } |
118 | } |
119 | |
120 | if (($this->route !== null) || ($this->dynamicRoute !== null)) { |
121 | $this->parseRouteParams(); |
122 | } |
123 | |
124 | return $this->hasRoute(); |
125 | } |
126 | |
127 | /** |
128 | * Determine if the route has been matched |
129 | * |
130 | * @return bool |
131 | */ |
132 | public function hasRoute(): bool |
133 | { |
134 | if (($this->route !== null) && !($this->hasAllRequired)) { |
135 | return false; |
136 | } |
137 | |
138 | return ((($this->route !== null) && ($this->hasAllRequired)) || (($this->dynamicRoute !== null) || ($this->defaultRoute !== null))); |
139 | } |
140 | |
141 | /** |
142 | * Get the route commands |
143 | * |
144 | * @return array |
145 | */ |
146 | public function getCommands(): array |
147 | { |
148 | return $this->commands; |
149 | } |
150 | |
151 | /** |
152 | * Get the command parameters |
153 | * |
154 | * @return array |
155 | */ |
156 | public function getCommandParameters(): array |
157 | { |
158 | return $this->parameters; |
159 | } |
160 | |
161 | /** |
162 | * Get the command options |
163 | * |
164 | * @return array |
165 | */ |
166 | public function getCommandOptions(): array |
167 | { |
168 | return $this->options; |
169 | } |
170 | |
171 | /** |
172 | * Get the parsed route params |
173 | * |
174 | * @return array |
175 | */ |
176 | public function getParameters(): array |
177 | { |
178 | $params = $this->routeParams; |
179 | unset($params['options']); |
180 | return $params; |
181 | } |
182 | |
183 | /** |
184 | * Get a parsed route param |
185 | * |
186 | * @param string $name |
187 | * @return mixed |
188 | */ |
189 | public function getParameter(string $name): mixed |
190 | { |
191 | return $this->routeParams[$name] ?? null; |
192 | } |
193 | |
194 | /** |
195 | * Get the parsed route options |
196 | * |
197 | * @return array |
198 | */ |
199 | public function getOptions(): array |
200 | { |
201 | return $this->routeParams['options'] ?? []; |
202 | } |
203 | |
204 | /** |
205 | * Get a parsed route option |
206 | * |
207 | * @param string $name |
208 | * @return mixed |
209 | */ |
210 | public function getOption(string $name): mixed |
211 | { |
212 | return $this->routeParams['options'][$name] ?? null; |
213 | } |
214 | |
215 | /** |
216 | * Method to process if a route was not found |
217 | * |
218 | * @param bool $exit |
219 | * @return void |
220 | */ |
221 | public function noRouteFound(bool $exit = true): void |
222 | { |
223 | if ((stripos(PHP_OS, 'darwin') === false) && (stripos(PHP_OS, 'win') !== false)) { |
224 | $string = 'Command Not Found.'; |
225 | } else { |
226 | $string = " \x1b[1;37m\x1b[41m \x1b[0m" . PHP_EOL; |
227 | $string .= " \x1b[1;37m\x1b[41m Command Not Found. \x1b[0m" . PHP_EOL; |
228 | $string .= " \x1b[1;37m\x1b[41m \x1b[0m"; |
229 | } |
230 | |
231 | echo PHP_EOL . $string . PHP_EOL . PHP_EOL; |
232 | |
233 | if ($exit) { |
234 | exit(127); |
235 | } |
236 | } |
237 | |
238 | /** |
239 | * Flatten the nested routes |
240 | * |
241 | * @param array|string $route |
242 | * @param mixed $controller |
243 | * @return void |
244 | */ |
245 | protected function flattenRoutes(array|string $route, mixed $controller = null): void |
246 | { |
247 | if (is_array($route)) { |
248 | foreach ($route as $r => $c) { |
249 | $this->flattenRoutes($r, $c); |
250 | } |
251 | } else if ($controller !== null) { |
252 | if (!isset($controller['controller'])) { |
253 | foreach ($controller as $r => $c) { |
254 | $this->flattenRoutes($route . $r, $c); |
255 | } |
256 | } else { |
257 | $routeRegex = $this->getRouteRegex($route); |
258 | if (isset($controller['default']) && ($controller['default'])) { |
259 | $this->defaultRoute['*'] = $controller; |
260 | } |
261 | $this->preparedRoutes[$routeRegex['regex']] = array_merge($controller, [ |
262 | 'route' => $route |
263 | ]); |
264 | } |
265 | } |
266 | } |
267 | |
268 | /** |
269 | * Get the REGEX pattern for the route string |
270 | * |
271 | * @param string $route |
272 | * @return array |
273 | */ |
274 | protected function getRouteRegex(string $route): array |
275 | { |
276 | $routeRegex = '^'; |
277 | $commands = []; |
278 | $options = []; |
279 | $optionValues = []; |
280 | $optionValueArray = []; |
281 | $requiredParameters = []; |
282 | $optionalParameters = []; |
283 | |
284 | if (!isset($this->commands[$route])) { |
285 | $this->commands[$route] = []; |
286 | } |
287 | |
288 | // Get route commands |
289 | if (str_contains($route, '<') || str_contains($route, '[')) { |
290 | $regexCommands = []; |
291 | preg_match_all('/[a-zA-Z0-9-_:|\p{L}]*(?=\s)/u', $route, $commands, PREG_OFFSET_CAPTURE); |
292 | foreach ($commands[0] as $i => $command) { |
293 | if (!empty($command[0])) { |
294 | $regexCommands[] = $command[0]; |
295 | $this->commands[$route][] = $command[0]; |
296 | } |
297 | } |
298 | if (count($regexCommands) > 0) { |
299 | $routeRegex .= implode(' ', $regexCommands); |
300 | } |
301 | } else { |
302 | $this->commands[$route] = explode(' ', $route); |
303 | $routeRegex .= $route . '$'; |
304 | } |
305 | |
306 | // Get route options |
307 | // [-o] |
308 | // [--option] |
309 | // [-o|--option] |
310 | // [--option|-o] |
311 | preg_match_all('/\[(\-[a-zA-Z0-9]|\-\-[a-zA-Z0-9:\-_]*)(\|(\-\-[a-zA-Z0-9:\-_]*|\-[a-zA-Z0-9]))*\]/', $route, $options, PREG_OFFSET_CAPTURE); |
312 | |
313 | // Get route option values |
314 | // [-o|--option=] |
315 | // [--option=|-o] |
316 | // [--option=] |
317 | preg_match_all('/\[(\-[a-zA-z0-9]\|)*\-\-[a-zA-Z0-9:\-_]*=(\|\-[a-zA-z0-9])*\]/', $route, $optionValues, PREG_OFFSET_CAPTURE); |
318 | |
319 | // Get route option value arrays |
320 | // [-o|--option=*] |
321 | // [--option=*|-o] |
322 | // [--option=*] |
323 | preg_match_all('/\[(\-[a-zA-z0-9]\|)*\-\-[a-zA-Z0-9:\-_]*=\*(\|\-[a-zA-z0-9])*\]/', $route, $optionValueArray, PREG_OFFSET_CAPTURE); |
324 | |
325 | // Get route required parameters <param> |
326 | preg_match_all('/(?<!\[)<[a-zA-Z0-9-_:|]*>/', $route, $requiredParameters, PREG_OFFSET_CAPTURE); |
327 | |
328 | // Get route optional parameters [<param>] |
329 | preg_match_all('/\[<[a-zA-Z0-9-_:|]*>\]/', $route, $optionalParameters, PREG_OFFSET_CAPTURE); |
330 | |
331 | $routeRegex .= (isset($requiredParameters[0]) && isset($requiredParameters[0][0])) ? ' (.*)$' : '(.*)$'; |
332 | |
333 | foreach ($options[0] as $option) { |
334 | if (str_contains($option[0], '--')) { |
335 | $name = substr($option[0], (strpos($option[0], '--') + 2)); |
336 | $name = substr($name, 0, strpos($name, ']')); |
337 | if (str_contains($name, '|')) { |
338 | $name = substr($name, 0, strpos($name, '|')); |
339 | } |
340 | } else { |
341 | $name = substr($option[0], (strpos($option[0], '-') + 1)); |
342 | $name = (str_contains($name, '|')) ? substr($name, 0, strpos($name, '|')) : substr($name, 0, strpos($name, ']')); |
343 | } |
344 | if (!isset($this->options['options'][$route])) { |
345 | $this->options['options'][$route] = []; |
346 | } |
347 | $this->options['options'][$route][$name] = '/' . str_replace(['[', ']'], ['(', ')'], $option[0]) . '/'; |
348 | } |
349 | |
350 | foreach ($optionValues[0] as $option) { |
351 | $opt = str_replace(['[', ']'], ['', ''], $option[0]); |
352 | if (str_contains($option[0], '--')) { |
353 | $name = substr($option[0], (strpos($option[0], '--') + 2)); |
354 | $name = substr($name, 0, strpos($name, '=')); |
355 | } else { |
356 | $name = substr($option[0], (strpos($option[0], '-') + 1)); |
357 | $name = substr($name, 0, 1); |
358 | } |
359 | if (str_contains($opt, '|')) { |
360 | [$opt1, $opt2] = explode('|', $opt); |
361 | $optionRegex = '(' . $opt1 . '[a-zA-Z0-9-_:|.@,\/]+|' . $opt1 . '"(.*)"|' . $opt2 . |
362 | '[a-zA-Z0-9-_:|.@,\/]+|' . $opt2 . '"(.*)")'; |
363 | } else { |
364 | $optionRegex = '(' . $opt . '[a-zA-Z0-9-_:|.@,\/]+|' . $opt . '"(.*)")'; |
365 | } |
366 | if (!isset($this->options['values'][$route])) { |
367 | $this->options['values'][$route] = []; |
368 | } |
369 | $this->options['values'][$route][$name] = '/' . $optionRegex . '/'; |
370 | } |
371 | |
372 | foreach ($optionValueArray[0] as $option) { |
373 | $opt = str_replace(['[', ']', '*'], ['', '', ''], $option[0]); |
374 | if (str_contains($option[0], '--')) { |
375 | $name = substr($option[0], (strpos($option[0], '--') + 2)); |
376 | $name = substr($name, 0, strpos($name, '=')); |
377 | } else { |
378 | $name = substr($option[0], (strpos($option[0], '-') + 1)); |
379 | $name = substr($name, 0, 1); |
380 | } |
381 | if (str_contains($opt, '|')) { |
382 | [$opt1, $opt2] = explode('|', $opt); |
383 | $optionRegex = '(' . $opt1 . '[a-zA-Z0-9-_:|.@,\/]+|' . $opt1 . '"(.*)"|' . $opt2 . |
384 | '[a-zA-Z0-9-_:|.@,\/]+|' . $opt2 . '"(.*)")'; |
385 | } else { |
386 | $optionRegex = '(' . $opt . '[a-zA-Z0-9-_:|.@,\/]+|' . $opt . '"(.*)")'; |
387 | } |
388 | if (!isset($this->options['arrays'][$route])) { |
389 | $this->options['arrays'][$route] = []; |
390 | } |
391 | $this->options['arrays'][$route][$name] = '/' . $optionRegex . '/'; |
392 | } |
393 | |
394 | foreach ($requiredParameters[0] as $i => $parameter) { |
395 | if (!isset($this->parameters[$route])) { |
396 | $this->parameters[$route] = []; |
397 | } |
398 | $this->parameters[$route][substr($parameter[0], 1, -1)] = [ |
399 | 'position' => ($i + 1), |
400 | 'required' => true |
401 | ]; |
402 | } |
403 | |
404 | $cur = (isset($this->parameters[$route])) ? count($this->parameters[$route]) : 0; |
405 | |
406 | foreach ($optionalParameters[0] as $j => $parameter) { |
407 | if (!isset($this->parameters[$route])) { |
408 | $this->parameters[$route] = []; |
409 | } |
410 | $this->parameters[$route][substr($parameter[0], 2, -2)] = [ |
411 | 'position' => ($j + 1 + $cur), |
412 | 'required' => false |
413 | ]; |
414 | } |
415 | |
416 | return [ |
417 | 'regex' => '/' . $routeRegex . '/' |
418 | ]; |
419 | } |
420 | |
421 | /** |
422 | * Parse route dispatch parameters |
423 | * |
424 | * @return void |
425 | */ |
426 | protected function parseRouteParams(): void |
427 | { |
428 | if (($this->dynamicRoute !== null) && (count($this->segments) >= 3)) { |
429 | $this->routeParams = (str_contains($this->dynamicRoute, 'param*')) ? |
430 | [array_slice($this->segments, 2)] : array_slice($this->segments, 2); |
431 | } else { |
432 | $options = []; |
433 | $start = 0; |
434 | $route = $this->preparedRoutes[$this->route]['route']; |
435 | if (isset($this->options['options'][$route])) { |
436 | foreach ($this->options['options'][$route] as $option => $regex) { |
437 | $match = []; |
438 | preg_match($regex, $this->routeString, $match); |
439 | if (isset($match[0]) && !empty($match[0])) { |
440 | $options[$option] = true; |
441 | if (array_search($match[0], $this->segments) > $start) { |
442 | $start = array_search($match[0], $this->segments); |
443 | } |
444 | } |
445 | } |
446 | } |
447 | |
448 | if (isset($this->options['values'][$route])) { |
449 | foreach ($this->options['values'][$route] as $option => $regex) { |
450 | $match = []; |
451 | $value = null; |
452 | preg_match($regex, $this->routeString, $match); |
453 | if (isset($match[0]) && !empty($match[0])) { |
454 | if (str_contains($match[0], '=')) { |
455 | $value = substr($match[0], (strpos($match[0], '=') + 1)); |
456 | } else if ((str_starts_with($match[0], '-')) && (substr($match[0], 1, 1) != '-') && !str_contains($match[0], $option)) { |
457 | $value = substr($match[0], 2); |
458 | } |
459 | $options[$option] = $value; |
460 | if (array_search($match[0], $this->segments) > $start) { |
461 | $start = array_search($match[0], $this->segments); |
462 | } |
463 | } |
464 | } |
465 | } |
466 | |
467 | if (isset($this->options['arrays'][$route])) { |
468 | foreach ($this->options['arrays'][$route] as $option => $regex) { |
469 | $matches = []; |
470 | $values = []; |
471 | preg_match_all($regex, $this->routeString, $matches); |
472 | if (isset($matches[0]) && !empty($matches[0])) { |
473 | foreach ($matches[0] as $match) { |
474 | $value = null; |
475 | if (str_contains($match, '=')) { |
476 | $value = substr($match, (strpos($match, '=') + 1)); |
477 | } else if ((str_starts_with($match, '-')) && (substr($match, 1, 1) != '-') && !str_contains($match, $option)) { |
478 | $value = substr($match, 2); |
479 | } |
480 | $values[] = $value; |
481 | if (array_search($match, $this->segments) > $start) { |
482 | $start = array_search($match, $this->segments); |
483 | } |
484 | } |
485 | } |
486 | if (count($values) > 0) { |
487 | $options[$option] = $values; |
488 | } |
489 | } |
490 | } |
491 | |
492 | if (isset($this->parameters[$route])) { |
493 | // Filter out commands and options from route segments, leaving only potential parameters |
494 | $paramSegments = $this->segments; |
495 | if (isset($this->commands[$route]) && is_array($this->commands[$route])) { |
496 | foreach ($this->commands[$route] as $command) { |
497 | if (in_array($command, $paramSegments)) { |
498 | unset($paramSegments[array_search($command, $paramSegments)]); |
499 | } |
500 | } |
501 | } |
502 | $paramSegments = array_values(array_filter($paramSegments, function($value) { |
503 | return !str_starts_with($value, '-'); |
504 | })); |
505 | |
506 | $i = 0; |
507 | |
508 | foreach ($this->parameters[$route] as $name => $parameter) { |
509 | if (isset($paramSegments[$i])) { |
510 | $this->routeParams[$name] = $paramSegments[$i]; |
511 | $i++; |
512 | } else { |
513 | $this->routeParams[$name] = null; |
514 | } |
515 | |
516 | if (($parameter['required']) && ($this->routeParams[$name] === null)) { |
517 | $this->hasAllRequired = false; |
518 | } |
519 | } |
520 | } |
521 | |
522 | if (!empty($options)) { |
523 | $this->routeParams['options'] = $options; |
524 | } |
525 | } |
526 | } |
527 | |
528 | } |