Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
96.43% covered (success)
96.43%
216 / 224
85.19% covered (success)
85.19%
23 / 27
CRAP
0.00% covered (danger)
0.00%
0 / 1
Cli
96.43% covered (success)
96.43%
216 / 224
85.19% covered (success)
85.19%
23 / 27
114
0.00% covered (danger)
0.00%
0 / 1
 __construct
100.00% covered (success)
100.00%
3 / 3
100.00% covered (success)
100.00%
1 / 1
1
 seed
100.00% covered (success)
100.00%
5 / 5
100.00% covered (success)
100.00%
1 / 1
2
 prepare
100.00% covered (success)
100.00%
7 / 7
100.00% covered (success)
100.00%
1 / 1
1
 match
100.00% covered (success)
100.00%
16 / 16
100.00% covered (success)
100.00%
1 / 1
7
 hasRoute
100.00% covered (success)
100.00%
3 / 3
100.00% covered (success)
100.00%
1 / 1
5
 getCommands
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 getCommandParameters
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 getCommandOptions
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 getParameters
100.00% covered (success)
100.00%
3 / 3
100.00% covered (success)
100.00%
1 / 1
1
 getParameter
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 getOptions
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 getOption
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 noRouteFound
75.00% covered (success)
75.00%
6 / 8
0.00% covered (danger)
0.00%
0 / 1
4.25
 flattenRoutes
85.71% covered (success)
85.71%
12 / 14
0.00% covered (danger)
0.00%
0 / 1
8.19
 getRouteRegex
100.00% covered (success)
100.00%
16 / 16
100.00% covered (success)
100.00%
1 / 1
3
 getRouteSpecificity
100.00% covered (success)
100.00%
9 / 9
100.00% covered (success)
100.00%
1 / 1
7
 extractRouteCommandsRegex
100.00% covered (success)
100.00%
10 / 10
100.00% covered (success)
100.00%
1 / 1
6
 indexRouteOptions
100.00% covered (success)
100.00%
11 / 11
100.00% covered (success)
100.00%
1 / 1
6
 indexRouteOptionValues
80.00% covered (success)
80.00%
8 / 10
0.00% covered (danger)
0.00%
0 / 1
4.13
 indexRouteOptionValueArrays
80.00% covered (success)
80.00%
8 / 10
0.00% covered (danger)
0.00%
0 / 1
4.13
 buildOptionValueRegex
100.00% covered (success)
100.00%
5 / 5
100.00% covered (success)
100.00%
1 / 1
2
 indexRouteParameters
100.00% covered (success)
100.00%
15 / 15
100.00% covered (success)
100.00%
1 / 1
6
 parseRouteParams
100.00% covered (success)
100.00%
17 / 17
100.00% covered (success)
100.00%
1 / 1
6
 parseOptionFlags
100.00% covered (success)
100.00%
8 / 8
100.00% covered (success)
100.00%
1 / 1
5
 parseOptionValues
100.00% covered (success)
100.00%
13 / 13
100.00% covered (success)
100.00%
1 / 1
9
 parseOptionValueArrays
100.00% covered (success)
100.00%
17 / 17
100.00% covered (success)
100.00%
1 / 1
11
 parsePositionalParameters
100.00% covered (success)
100.00%
18 / 18
100.00% covered (success)
100.00%
1 / 1
10
1<?php
2declare(strict_types=1);
3/**
4 * Pop PHP Framework (https://www.popphp.org/)
5 *
6 * @link       https://github.com/popphp/popphp-framework
7 * @author     Nick Sagona, III <nick@popphp.org>
8 * @copyright  Copyright (c) 2009-2026 Nick Sagona, III
9 * @license    https://www.popphp.org/license     New BSD License
10 */
11
12/**
13 * @namespace
14 */
15namespace Pop\Router\Match;
16
17/**
18 * Pop router CLI match class
19 *
20 * @category   Pop
21 * @package    Pop\Router
22 * @author     Nick Sagona, III <nick@popphp.org>
23 * @copyright  Copyright (c) 2009-2026 Nick Sagona, III
24 * @license    https://www.popphp.org/license     New BSD License
25 * @version    5.0.0
26 */
27class Cli extends AbstractMatch
28{
29
30    /**
31     * Route commands
32     * @var array
33     */
34    protected array $commands = [];
35
36    /**
37     * Allowed route options
38     * @var array
39     */
40    protected array $options = [
41        'options' => [], // [-v|--verbose]
42        'values'  => [], // [-n|--name=]
43        'arrays'  => []  // [-i|--id=*]
44    ];
45
46    /**
47     * Allowed route parameters
48     * @var array
49     */
50    protected array $parameters = [];
51
52    /**
53     * Flag for all required parameters
54     * @var bool
55     */
56    protected bool $hasAllRequired = true;
57
58    /**
59     * Specificity score per prepared route, keyed the same as preparedRoutes
60     * @var array
61     */
62    protected array $routeSpecificity = [];
63
64    /**
65     * Constructor
66     *
67     *   cmd              required command
68     *   [cmd]            optional command
69     *   cmd1|cmd2        required command with alternate options
70     *   [cmd1|cmd2]      optional command with alternate options
71     *
72     *   [-o|--option]    option flag
73     *   [-o|--option=]   option value
74     *   [-o|--option=*]  multiple option values
75     *
76     *   <param>          required parameter
77     *   [<param>]        optional parameter
78     *
79     *
80     * Instantiate the CLI match object
81     */
82    public function __construct()
83    {
84        $argv = $_SERVER['argv'];
85
86        // Trim the script name out of the arguments array
87        array_shift($argv);
88
89        $this->seed($argv);
90    }
91
92    /**
93     * Seed the parsing inputs (segments and route string) from either
94     * pre-split argv-style segments or a raw command string
95     *
96     * @param  string|array $input
97     * @return void
98     */
99    protected function seed(string|array $input): void
100    {
101        $segments = is_array($input)
102            ? array_values($input)
103            : preg_split('/\s+/', trim($input), -1, PREG_SPLIT_NO_EMPTY);
104
105        $this->segments    = $segments;
106        $this->routeString = implode(' ', $segments);
107    }
108
109    /**
110     * Prepare the routes
111     *
112     * @return static
113     */
114    public function prepare(): static
115    {
116        $this->flattenRoutes($this->routes);
117
118        uksort($this->preparedRoutes, function($keyA, $keyB) {
119            $scoreA = $this->routeSpecificity[$keyA] ?? '';
120            $scoreB = $this->routeSpecificity[$keyB] ?? '';
121            return strcmp($scoreB, $scoreA);
122        });
123
124        return $this;
125    }
126
127    /**
128     * Match the route
129     *
130     * @param  string|array|null $forceRoute
131     * @return bool
132     */
133    public function match(mixed $forceRoute = null): bool
134    {
135        if (count($this->preparedRoutes) == 0) {
136            $this->prepare();
137        }
138
139        $this->route                = null;
140        $this->hasAllRequired       = true;
141        $this->routeParams          = [];
142        $this->dispatchableResolved = false;
143
144        if ($forceRoute !== null) {
145            $this->seed($forceRoute);
146        }
147
148        $routeToMatch = $this->routeString;
149
150        foreach (array_keys($this->preparedRoutes) as $regex) {
151            if (preg_match($regex, $routeToMatch) != 0) {
152                $this->route = $regex;
153                break;
154            }
155        }
156
157        if (($this->route !== null) || ($this->dynamicRoute !== null)) {
158            $this->parseRouteParams();
159        }
160
161        return $this->hasRoute();
162    }
163
164    /**
165     * Determine if the route has been matched
166     *
167     * @return bool
168     */
169    public function hasRoute(): bool
170    {
171        if (($this->route !== null) && !($this->hasAllRequired)) {
172            return false;
173        }
174
175        return (($this->route !== null) || $this->matchesDynamicRoute() || ($this->defaultRoute !== null));
176    }
177
178    /**
179     * Get the route commands
180     *
181     * @return array
182     */
183    public function getCommands(): array
184    {
185        return $this->commands;
186    }
187
188    /**
189     * Get the command parameters
190     *
191     * @return array
192     */
193    public function getCommandParameters(): array
194    {
195        return $this->parameters;
196    }
197
198    /**
199     * Get the command options
200     *
201     * @return array
202     */
203    public function getCommandOptions(): array
204    {
205        return $this->options;
206    }
207
208    /**
209     * Get the parsed route params
210     *
211     * @return array
212     */
213    public function getParameters(): array
214    {
215        $params = $this->routeParams;
216        unset($params['options']);
217        return $params;
218    }
219
220    /**
221     * Get a parsed route param
222     *
223     * @param  string $name
224     * @return mixed
225     */
226    public function getParameter(string $name): mixed
227    {
228        return $this->routeParams[$name] ?? null;
229    }
230
231    /**
232     * Get the parsed route options
233     *
234     * @return array
235     */
236    public function getOptions(): array
237    {
238        return $this->routeParams['options'] ?? [];
239    }
240
241    /**
242     * Get a parsed route option
243     *
244     * @param  string $name
245     * @return mixed
246     */
247    public function getOption(string $name): mixed
248    {
249        return $this->routeParams['options'][$name] ?? null;
250    }
251
252    /**
253     * Method to process if a route was not found
254     *
255     * @param  bool $exit
256     * @return void
257     */
258    public function noRouteFound(bool $exit = true): void
259    {
260        if ((stripos(PHP_OS, 'darwin') === false) && (stripos(PHP_OS, 'win') !== false)) {
261            $string = 'Command Not Found.';
262        } else {
263            $string  = "    \x1b[1;37m\x1b[41m                          \x1b[0m" . PHP_EOL;
264            $string .= "    \x1b[1;37m\x1b[41m    Command Not Found.    \x1b[0m" . PHP_EOL;
265            $string .= "    \x1b[1;37m\x1b[41m                          \x1b[0m";
266        }
267
268        echo PHP_EOL . $string . PHP_EOL . PHP_EOL;
269
270        if ($exit) {
271            exit(127);
272        }
273    }
274
275    /**
276     * Flatten the nested routes
277     *
278     * @param  array|string $route
279     * @param  mixed        $controller
280     * @return void
281     */
282    protected function flattenRoutes(array|string $route, mixed $controller = null): void
283    {
284        if (is_array($route)) {
285            foreach ($route as $r => $c) {
286                $this->flattenRoutes($r, $c);
287            }
288        } else if ($controller !== null) {
289            if (!isset($controller['controller'])) {
290                foreach ($controller as $r => $c) {
291                    $this->flattenRoutes($route . $r, $c);
292                }
293            } else {
294                $routeRegex = $this->getRouteRegex($route);
295
296                $this->routeSpecificity[$routeRegex['regex']] = $this->getRouteSpecificity($route);
297
298                if (isset($controller['default']) && ($controller['default'])) {
299                    $this->defaultRoute['*'] = $controller;
300                }
301                $this->preparedRoutes[$routeRegex['regex']] = array_merge($controller, [
302                    'route' => $route
303                ]);
304            }
305        }
306    }
307
308    /**
309     * Get the REGEX pattern for the route string
310     *
311     * @param  string $route
312     * @return array
313     */
314    protected function getRouteRegex(string $route): array
315    {
316        if (!isset($this->commands[$route])) {
317            $this->commands[$route] = [];
318        }
319
320        $routeRegex = '^' . $this->extractRouteCommandsRegex($route);
321
322        // Get route options
323        //   [-o]
324        //   [--option]
325        //   [-o|--option]
326        //   [--option|-o]
327        preg_match_all('/\[(\-[a-zA-Z0-9]|\-\-[a-zA-Z0-9:\-_]*)(\|(\-\-[a-zA-Z0-9:\-_]*|\-[a-zA-Z0-9]))*\]/', $route, $options, PREG_OFFSET_CAPTURE);
328
329        // Get route option values
330        //   [-o|--option=]
331        //   [--option=|-o]
332        //   [--option=]
333        preg_match_all('/\[(\-[a-zA-z0-9]\|)*\-\-[a-zA-Z0-9:\-_]*=(\|\-[a-zA-z0-9])*\]/', $route, $optionValues, PREG_OFFSET_CAPTURE);
334
335        // Get route option value arrays
336        //   [-o|--option=*]
337        //   [--option=*|-o]
338        //   [--option=*]
339        preg_match_all('/\[(\-[a-zA-z0-9]\|)*\-\-[a-zA-Z0-9:\-_]*=\*(\|\-[a-zA-z0-9])*\]/', $route, $optionValueArray, PREG_OFFSET_CAPTURE);
340
341        // Get route required parameters <param>
342        preg_match_all('/(?<!\[)<[a-zA-Z0-9-_:|]*>/', $route, $requiredParameters, PREG_OFFSET_CAPTURE);
343
344        // Get route optional parameters [<param>]
345        preg_match_all('/\[<[a-zA-Z0-9-_:|]*>\]/', $route, $optionalParameters, PREG_OFFSET_CAPTURE);
346
347        $routeRegex .= (isset($requiredParameters[0][0])) ? ' (.*)$' : '(.*)$';
348
349        $this->indexRouteOptions($route, $options[0]);
350        $this->indexRouteOptionValues($route, $optionValues[0]);
351        $this->indexRouteOptionValueArrays($route, $optionValueArray[0]);
352        $this->indexRouteParameters($route, $requiredParameters[0], $optionalParameters[0]);
353
354        return [
355            'regex' => '/' . str_replace('/', '\/', $routeRegex) . '/'
356        ];
357    }
358
359    /**
360     * Score a route's specificity for match-order sorting
361     *
362     * Ranks each whitespace-separated token left to right - a literal command
363     * beats a required <param> beats an optional [<param>] - as digits in a
364     * fixed-width string, so a plain string comparison gives leftmost-token
365     * precedence. Option tokens ([-o], [--option], [--option=], [--option=*])
366     * don't affect positional specificity, so they're skipped.
367     *
368     * @param  string $route
369     * @return string
370     */
371    protected function getRouteSpecificity(string $route): string
372    {
373        $digits = '';
374
375        foreach (preg_split('/\s+/', trim($route), -1, PREG_SPLIT_NO_EMPTY) as $segment) {
376            if (str_starts_with($segment, '[<') && str_ends_with($segment, '>]')) {
377                $digits .= '1';
378            } elseif (str_starts_with($segment, '<') && str_ends_with($segment, '>')) {
379                $digits .= '2';
380            } elseif (!str_starts_with($segment, '[')) {
381                $digits .= '3';
382            }
383        }
384
385        return str_pad($digits, 24, '0');
386    }
387
388    /**
389     * Extract the leading command-segment regex fragment from a route string,
390     * seeding $this->commands[$route] along the way
391     *
392     * @param  string $route
393     * @return string
394     */
395    protected function extractRouteCommandsRegex(string $route): string
396    {
397        if (str_contains($route, '<') || str_contains($route, '[')) {
398            $regexCommands = [];
399            preg_match_all('/[a-zA-Z0-9-_:|\p{L}]*(?=\s)/u', $route, $commands, PREG_OFFSET_CAPTURE);
400            foreach ($commands[0] as $command) {
401                if (!empty($command[0])) {
402                    $regexCommands[] = $command[0];
403                    $this->commands[$route][] = $command[0];
404                }
405            }
406
407            return (count($regexCommands) > 0) ? implode(' ', $regexCommands) : '';
408        }
409
410        $this->commands[$route] = explode(' ', $route);
411
412        return $route . '$';
413    }
414
415    /**
416     * Index the [-o]/[--option] flag matches for a route
417     *
418     * @param  string $route
419     * @param  array  $options
420     * @return void
421     */
422    protected function indexRouteOptions(string $route, array $options): void
423    {
424        foreach ($options as $option) {
425            if (str_contains($option[0], '--')) {
426                $name = substr($option[0], (strpos($option[0], '--') + 2));
427                $name = substr($name, 0, strpos($name, ']'));
428                if (str_contains($name, '|')) {
429                    $name = substr($name, 0, strpos($name, '|'));
430                }
431            } else {
432                $name = substr($option[0], (strpos($option[0], '-') + 1));
433                $name = (str_contains($name, '|')) ? substr($name, 0, strpos($name, '|')) : substr($name, 0, strpos($name, ']'));
434            }
435            if (!isset($this->options['options'][$route])) {
436                $this->options['options'][$route] = [];
437            }
438            $this->options['options'][$route][$name] = '/' . str_replace(['[', ']'], ['(', ')'], $option[0]) . '/';
439        }
440    }
441
442    /**
443     * Index the [--option=]-style value-option matches for a route
444     *
445     * @param  string $route
446     * @param  array  $optionValues
447     * @return void
448     */
449    protected function indexRouteOptionValues(string $route, array $optionValues): void
450    {
451        foreach ($optionValues as $option) {
452            $opt = str_replace(['[', ']'], ['', ''], $option[0]);
453            if (str_contains($option[0], '--')) {
454                $name = substr($option[0], (strpos($option[0], '--') + 2));
455                $name = substr($name, 0, strpos($name, '='));
456            } else {
457                $name = substr($option[0], (strpos($option[0], '-') + 1));
458                $name = substr($name, 0, 1);
459            }
460            if (!isset($this->options['values'][$route])) {
461                $this->options['values'][$route] = [];
462            }
463            $this->options['values'][$route][$name] = '/' . $this->buildOptionValueRegex($opt) . '/';
464        }
465    }
466
467    /**
468     * Index the [--option=*]-style array-value-option matches for a route
469     *
470     * @param  string $route
471     * @param  array  $optionValueArray
472     * @return void
473     */
474    protected function indexRouteOptionValueArrays(string $route, array $optionValueArray): void
475    {
476        foreach ($optionValueArray as $option) {
477            $opt = str_replace(['[', ']', '*'], ['', '', ''], $option[0]);
478            if (str_contains($option[0], '--')) {
479                $name = substr($option[0], (strpos($option[0], '--') + 2));
480                $name = substr($name, 0, strpos($name, '='));
481            } else {
482                $name = substr($option[0], (strpos($option[0], '-') + 1));
483                $name = substr($name, 0, 1);
484            }
485            if (!isset($this->options['arrays'][$route])) {
486                $this->options['arrays'][$route] = [];
487            }
488            $this->options['arrays'][$route][$name] = '/' . $this->buildOptionValueRegex($opt) . '/';
489        }
490    }
491
492    /**
493     * Build the value-matching regex fragment shared by option-value and
494     * option-value-array matches
495     *
496     * @param  string $opt
497     * @return string
498     */
499    protected function buildOptionValueRegex(string $opt): string
500    {
501        if (str_contains($opt, '|')) {
502            [$opt1, $opt2] = explode('|', $opt);
503            return '(' . $opt1 . '[a-zA-Z0-9-_:|.@,\/]+|' . $opt1 . '"(.*)"|' . $opt2 .
504                '[a-zA-Z0-9-_:|.@,\/]+|' . $opt2 . '"(.*)")';
505        }
506
507        return '(' . $opt . '[a-zA-Z0-9-_:|.@,\/]+|' . $opt . '"(.*)")';
508    }
509
510    /**
511     * Index the <param>/[<param>] required and optional parameter positions for a route
512     *
513     * @param  string $route
514     * @param  array  $requiredParameters
515     * @param  array  $optionalParameters
516     * @return void
517     */
518    protected function indexRouteParameters(string $route, array $requiredParameters, array $optionalParameters): void
519    {
520        foreach ($requiredParameters as $i => $parameter) {
521            if (!isset($this->parameters[$route])) {
522                $this->parameters[$route] = [];
523            }
524            $this->parameters[$route][substr($parameter[0], 1, -1)] = [
525                'position' => ($i + 1),
526                'required' => true
527            ];
528        }
529
530        $cur = (isset($this->parameters[$route])) ? count($this->parameters[$route]) : 0;
531
532        foreach ($optionalParameters as $j => $parameter) {
533            if (!isset($this->parameters[$route])) {
534                $this->parameters[$route] = [];
535            }
536            $this->parameters[$route][substr($parameter[0], 2, -2)] = [
537                'position' => ($j + 1 + $cur),
538                'required' => false
539            ];
540        }
541    }
542
543    /**
544     * Parse route dispatch parameters
545     *
546     * @return void
547     */
548    protected function parseRouteParams(): void
549    {
550        if ($this->matchesDynamicRoute()) {
551            $offset = $this->getDynamicRouteParamOffset();
552            if (count($this->segments) > $offset) {
553                $this->routeParams = (str_contains((string)$this->dynamicRoute, 'param*')) ?
554                    [array_slice($this->segments, $offset)] : array_slice($this->segments, $offset);
555            }
556            return;
557        }
558
559        if ($this->route === null) {
560            return;
561        }
562
563        $route = $this->preparedRoutes[$this->route]['route'];
564
565        // Later categories win on a name collision, matching the original
566        // sequential options -> values -> arrays overwrite order.
567        $options = array_merge(
568            $this->parseOptionFlags($route),
569            $this->parseOptionValues($route),
570            $this->parseOptionValueArrays($route)
571        );
572
573        $this->parsePositionalParameters($route);
574
575        if (!empty($options)) {
576            $this->routeParams['options'] = $options;
577        }
578    }
579
580    /**
581     * Parse [-o]/[--option] boolean flags present in the route string
582     *
583     * @param  string $route
584     * @return array
585     */
586    protected function parseOptionFlags(string $route): array
587    {
588        $options = [];
589
590        if (isset($this->options['options'][$route])) {
591            foreach ($this->options['options'][$route] as $option => $regex) {
592                $match = [];
593                preg_match($regex, $this->routeString, $match);
594                if (isset($match[0]) && !empty($match[0])) {
595                    $options[$option] = true;
596                }
597            }
598        }
599
600        return $options;
601    }
602
603    /**
604     * Parse [--option=]-style value options present in the route string
605     *
606     * @param  string $route
607     * @return array
608     */
609    protected function parseOptionValues(string $route): array
610    {
611        $options = [];
612
613        if (isset($this->options['values'][$route])) {
614            foreach ($this->options['values'][$route] as $option => $regex) {
615                $match = [];
616                $value = null;
617                preg_match($regex, $this->routeString, $match);
618                if (isset($match[0]) && !empty($match[0])) {
619                    if (str_contains($match[0], '=')) {
620                        $value = substr($match[0], (strpos($match[0], '=') + 1));
621                    } else if ((str_starts_with($match[0], '-')) && (substr($match[0], 1, 1) != '-') && !str_contains($match[0], $option)) {
622                        $value = substr($match[0], 2);
623                    }
624                    $options[$option] = $value;
625                }
626            }
627        }
628
629        return $options;
630    }
631
632    /**
633     * Parse [--option=*]-style array-value options present in the route string
634     *
635     * @param  string $route
636     * @return array
637     */
638    protected function parseOptionValueArrays(string $route): array
639    {
640        $options = [];
641
642        if (isset($this->options['arrays'][$route])) {
643            foreach ($this->options['arrays'][$route] as $option => $regex) {
644                $matches = [];
645                $values  = [];
646                preg_match_all($regex, $this->routeString, $matches);
647                if (isset($matches[0]) && !empty($matches[0])) {
648                    foreach ($matches[0] as $match) {
649                        $value = null;
650                        if (str_contains($match, '=')) {
651                            $value = substr($match, (strpos($match, '=') + 1));
652                        } else if ((str_starts_with($match, '-')) && (substr($match, 1, 1) != '-') && !str_contains($match, $option)) {
653                            $value = substr($match, 2);
654                        }
655                        $values[] = $value;
656                    }
657                }
658                if (count($values) > 0) {
659                    $options[$option] = $values;
660                }
661            }
662        }
663
664        return $options;
665    }
666
667    /**
668     * Match remaining route segments (after commands/options are filtered
669     * out) positionally against the route's required/optional parameters
670     *
671     * @param  string $route
672     * @return void
673     */
674    protected function parsePositionalParameters(string $route): void
675    {
676        if (!isset($this->parameters[$route])) {
677            return;
678        }
679
680        // Filter out commands and options from route segments, leaving only potential parameters
681        $paramSegments = $this->segments;
682        if (isset($this->commands[$route]) && is_array($this->commands[$route])) {
683            foreach ($this->commands[$route] as $command) {
684                if (in_array($command, $paramSegments)) {
685                    unset($paramSegments[array_search($command, $paramSegments)]);
686                }
687            }
688        }
689        $paramSegments = array_values(array_filter($paramSegments, function($value) {
690            return !str_starts_with($value, '-');
691        }));
692
693        $i = 0;
694
695        foreach ($this->parameters[$route] as $name => $parameter) {
696            if (isset($paramSegments[$i])) {
697                $this->routeParams[$name] = $paramSegments[$i];
698                $i++;
699            } else {
700                $this->routeParams[$name] = null;
701            }
702
703            if (($parameter['required']) && ($this->routeParams[$name] === null)) {
704                $this->hasAllRequired = false;
705            }
706        }
707    }
708
709}