RouteParser::parseRoute()   F
last analyzed

Complexity

Conditions 42
Paths 67

Size

Total Lines 254
Code Lines 171

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 188
CRAP Score 42

Importance

Changes 2
Bugs 0 Features 0
Metric Value
cc 42
eloc 171
c 2
b 0
f 0
nc 67
nop 1
dl 0
loc 254
ccs 188
cts 188
cp 1
crap 42
rs 3.3333

How to fix   Long Method    Complexity   

Long Method

Small methods make your code easier to understand, in particular if combined with a good name. Besides, if your method is small, finding a good name is usually much easier.

For example, if you find yourself adding comments to a method's body, this is usually a good sign to extract the commented part to a new method, and use the comment as a starting point when coming up with a good name for this new method.

Commonly applied refactorings include:

1
<?php
2
3
/**
4
 * It's free open-source software released under the MIT License.
5
 *
6
 * @author Anatoly Nekhay <[email protected]>
7
 * @copyright Copyright (c) 2018, Anatoly Nekhay
8
 * @license https://github.com/sunrise-php/http-router/blob/master/LICENSE
9
 * @link https://github.com/sunrise-php/http-router
10
 */
11
12
declare(strict_types=1);
13
14
namespace Sunrise\Http\Router\Helper;
15
16
use InvalidArgumentException;
17
18
use function sprintf;
19
20
/**
21
 * @since 3.0.0
22
 */
23
final class RouteParser
24
{
25
    private const IN_VARIABLE = 1;
26
    private const IN_VARIABLE_NAME = 2;
27
    private const IN_VARIABLE_PATTERN = 8;
28
    private const IN_OPTIONAL_PART = 16;
29
    private const IN_OCCUPIED_PART = 32;
30
31
    /**
32
     * @link https://www.pcre.org/original/doc/html/pcrepattern.html#SEC16
33
     */
34
    private const PCRE_SUBPATTERN_NAME_CHARSET = [
35
        "\x30" => 1, "\x31" => 1, "\x32" => 1, "\x33" => 1, "\x34" => 1, "\x35" => 1, "\x36" => 1, "\x37" => 1,
36
        "\x38" => 1, "\x39" => 1, "\x41" => 1, "\x42" => 1, "\x43" => 1, "\x44" => 1, "\x45" => 1, "\x46" => 1,
37
        "\x47" => 1, "\x48" => 1, "\x49" => 1, "\x4a" => 1, "\x4b" => 1, "\x4c" => 1, "\x4d" => 1, "\x4e" => 1,
38
        "\x4f" => 1, "\x50" => 1, "\x51" => 1, "\x52" => 1, "\x53" => 1, "\x54" => 1, "\x55" => 1, "\x56" => 1,
39
        "\x57" => 1, "\x58" => 1, "\x59" => 1, "\x5a" => 1, "\x5f" => 1, "\x61" => 1, "\x62" => 1, "\x63" => 1,
40
        "\x64" => 1, "\x65" => 1, "\x66" => 1, "\x67" => 1, "\x68" => 1, "\x69" => 1, "\x6a" => 1, "\x6b" => 1,
41
        "\x6c" => 1, "\x6d" => 1, "\x6e" => 1, "\x6f" => 1, "\x70" => 1, "\x71" => 1, "\x72" => 1, "\x73" => 1,
42
        "\x74" => 1, "\x75" => 1, "\x76" => 1, "\x77" => 1, "\x78" => 1, "\x79" => 1, "\x7a" => 1,
43
    ];
44
45
    /**
46
     * Parses the given route and returns its variables
47
     *
48
     * @return list<array{statement: string, name: string, pattern?: string, optional_part?: string}>
0 ignored issues
show
Bug introduced by
The type Sunrise\Http\Router\Helper\list was not found. Maybe you did not declare it correctly or list all dependencies?

The issue could also be caused by a filter entry in the build configuration. If the path has been excluded in your configuration, e.g. excluded_paths: ["lib/*"], you can move it to the dependency path list as follows:

filter:
    dependency_paths: ["lib/*"]

For further information see https://scrutinizer-ci.com/docs/tools/php/php-scrutinizer/#list-dependency-paths

Loading history...
49
     *
50
     * @throws InvalidArgumentException
51
     */
52 109
    public static function parseRoute(string $route): array
53
    {
54 109
        $cursor = 0;
55 109
        $variable = -1;
56
57
        /** @var list<array{statement?: string, name?: string, pattern?: string, optional_part?: string}> $variables */
58 109
        $variables = [];
59
60
        /** @var array<string, true> $names */
61 109
        $names = [];
62
63 109
        $left = $right = '';
64
65 109
        for ($offset = 0; isset($route[$offset]); $offset++) {
66 62
            if ($route[$offset] === '(' && !($cursor & self::IN_VARIABLE)) {
67 10
                if (($cursor & self::IN_OPTIONAL_PART)) {
68 1
                    throw new InvalidArgumentException(sprintf(
69 1
                        'The route "%s" could not be parsed due to a syntax error. ' .
70 1
                        'The attempt to open an optional part at position %d failed ' .
71 1
                        'because nested optional parts are not supported.',
72 1
                        $route,
73 1
                        $offset,
74 1
                    ));
75
                }
76
77 10
                $cursor |= self::IN_OPTIONAL_PART;
78 10
                continue;
79
            }
80 60
            if ($route[$offset] === ')' && !($cursor & self::IN_VARIABLE)) {
81 8
                if (!($cursor & self::IN_OPTIONAL_PART)) {
82 1
                    throw new InvalidArgumentException(sprintf(
83 1
                        'The route "%s" could not be parsed due to a syntax error. ' .
84 1
                        'The attempt to close an optional part at position %d failed ' .
85 1
                        'because an open optional part was not found.',
86 1
                        $route,
87 1
                        $offset,
88 1
                    ));
89
                }
90
91 7
                if (($cursor & self::IN_OCCUPIED_PART)) {
92 6
                    $cursor &= ~self::IN_OCCUPIED_PART;
93
                    // phpcs:ignore Generic.Files.LineLength.TooLong
94 6
                    $variables[$variable]['optional_part'] = '(' . $left . ($variables[$variable]['statement'] ?? '') . $right . ')';
95
                }
96
97 7
                $cursor &= ~self::IN_OPTIONAL_PART;
98 7
                $left = $right = '';
99 7
                continue;
100
            }
101
102 59
            if ($route[$offset] === '{' && !($cursor & self::IN_VARIABLE_PATTERN)) {
103 48
                if (($cursor & self::IN_VARIABLE)) {
104 1
                    throw new InvalidArgumentException(sprintf(
105 1
                        'The route "%s" could not be parsed due to a syntax error. ' .
106 1
                        'The attempt to open a variable at position %d failed ' .
107 1
                        'because nested variables are not supported.',
108 1
                        $route,
109 1
                        $offset,
110 1
                    ));
111
                }
112 48
                if (($cursor & self::IN_OCCUPIED_PART)) {
113 1
                    throw new InvalidArgumentException(sprintf(
114 1
                        'The route "%s" could not be parsed due to a syntax error. ' .
115 1
                        'The attempt to open a variable at position %d failed ' .
116 1
                        'because more than one variable inside an optional part is not supported.',
117 1
                        $route,
118 1
                        $offset,
119 1
                    ));
120
                }
121
122 48
                if (($cursor & self::IN_OPTIONAL_PART)) {
123 7
                    $cursor |= self::IN_OCCUPIED_PART;
124
                }
125
126 48
                $cursor |= self::IN_VARIABLE | self::IN_VARIABLE_NAME;
127 48
                $variable++;
128 48
                continue;
129
            }
130 57
            if ($route[$offset] === '}' && !($cursor & self::IN_VARIABLE_PATTERN)) {
131 38
                if (!($cursor & self::IN_VARIABLE)) {
132 1
                    throw new InvalidArgumentException(sprintf(
133 1
                        'The route "%s" could not be parsed due to a syntax error. ' .
134 1
                        'The attempt to close a variable at position %d failed ' .
135 1
                        'because an open variable was not found.',
136 1
                        $route,
137 1
                        $offset,
138 1
                    ));
139
                }
140 37
                if (!isset($variables[$variable]['name'])) {
141 1
                    throw new InvalidArgumentException(sprintf(
142 1
                        'The route "%s" could not be parsed due to a syntax error. ' .
143 1
                        'The attempt to close a variable at position %d failed ' .
144 1
                        'because its name is required for its declaration.',
145 1
                        $route,
146 1
                        $offset,
147 1
                    ));
148
                }
149 36
                if (isset($names[$variables[$variable]['name']])) {
150 1
                    throw new InvalidArgumentException(sprintf(
151 1
                        'The route "%s" at position %d could not be parsed ' .
152 1
                        'because the variable name "%s" is already in use.',
153 1
                        $route,
154 1
                        $offset,
155 1
                        $variables[$variable]['name'],
156 1
                    ));
157
                }
158
159 36
                $cursor &= ~(self::IN_VARIABLE | self::IN_VARIABLE_NAME);
160 36
                $variables[$variable]['statement'] = '{' . ($variables[$variable]['statement'] ?? '') . '}';
161 36
                $names[$variables[$variable]['name']] = true; // @phpstan-ignore-line
162 36
                continue;
163
            }
164
165 55
            if (($cursor & self::IN_VARIABLE)) {
166 45
                $variables[$variable]['statement'] ??= '';
167 45
                $variables[$variable]['statement'] .= $route[$offset];
168
            }
169
170 55
            if ($route[$offset] === '<' && ($cursor & self::IN_VARIABLE)) {
171 16
                if (($cursor & self::IN_VARIABLE_PATTERN)) {
172 1
                    throw new InvalidArgumentException(sprintf(
173 1
                        'The route "%s" could not be parsed due to a syntax error. ' .
174 1
                        'The attempt to open a variable pattern at position %d failed ' .
175 1
                        'because nested patterns are not supported.',
176 1
                        $route,
177 1
                        $offset,
178 1
                    ));
179
                }
180 16
                if (!($cursor & self::IN_VARIABLE_NAME)) {
181 1
                    throw new InvalidArgumentException(sprintf(
182 1
                        'The route "%s" could not be parsed due to a syntax error. ' .
183 1
                        'The attempt to open a variable pattern at position %d failed ' .
184 1
                        'because the pattern must be preceded by the variable name.',
185 1
                        $route,
186 1
                        $offset,
187 1
                    ));
188
                }
189
190 16
                $cursor = $cursor & ~self::IN_VARIABLE_NAME | self::IN_VARIABLE_PATTERN;
191 16
                continue;
192
            }
193 54
            if ($route[$offset] === '>' && ($cursor & self::IN_VARIABLE)) {
194 15
                if (!($cursor & self::IN_VARIABLE_PATTERN)) {
195 1
                    throw new InvalidArgumentException(sprintf(
196 1
                        'The route "%s" could not be parsed due to a syntax error. ' .
197 1
                        'The attempt to close a variable pattern at position %d failed ' .
198 1
                        'because an open pattern was not found.',
199 1
                        $route,
200 1
                        $offset,
201 1
                    ));
202
                }
203 14
                if (!isset($variables[$variable]['pattern'])) {
204 1
                    throw new InvalidArgumentException(sprintf(
205 1
                        'The route "%s" could not be parsed due to a syntax error. ' .
206 1
                        'The attempt to close a variable pattern at position %d failed ' .
207 1
                        'because its content is required for its declaration.',
208 1
                        $route,
209 1
                        $offset,
210 1
                    ));
211
                }
212
213 13
                $cursor &= ~self::IN_VARIABLE_PATTERN;
214 13
                continue;
215
            }
216
217
            // (left{var}right)
218
            // ~^^^^~~~~~^^^^^~
219 52
            if (($cursor & self::IN_OPTIONAL_PART) && !($cursor & self::IN_VARIABLE)) {
220 7
                if (!($cursor & self::IN_OCCUPIED_PART)) {
221 7
                    $left .= $route[$offset];
222
                } else {
223 1
                    $right .= $route[$offset];
224
                }
225
226 7
                continue;
227
            }
228
229
            // https://www.pcre.org/original/doc/html/pcrepattern.html#SEC16
230 52
            if (($cursor & self::IN_VARIABLE_NAME)) {
231 40
                if (!isset($variables[$variable]['name']) && $route[$offset] >= '0' && $route[$offset] <= '9') {
232 1
                    throw new InvalidArgumentException(sprintf(
233 1
                        'The route "%s" could not be parsed due to a syntax error. ' .
234 1
                        'An invalid character was found at position %d. ' .
235 1
                        'Please note that variable names cannot start with digits.',
236 1
                        $route,
237 1
                        $offset,
238 1
                    ));
239
                }
240 39
                if (!isset(self::PCRE_SUBPATTERN_NAME_CHARSET[$route[$offset]])) {
241 1
                    throw new InvalidArgumentException(sprintf(
242 1
                        'The route "%s" could not be parsed due to a syntax error. ' .
243 1
                        'An invalid character was found at position %d. ' .
244 1
                        'Please note that variable names must consist only of digits, letters and underscores.',
245 1
                        $route,
246 1
                        $offset,
247 1
                    ));
248
                }
249 38
                if (isset($variables[$variable]['name'][31])) {
250 1
                    throw new InvalidArgumentException(sprintf(
251 1
                        'The route "%s" could not be parsed due to a syntax error. ' .
252 1
                        'An extra character was found at position %d. ' .
253 1
                        'Please note that variable names must not exceed 32 characters.',
254 1
                        $route,
255 1
                        $offset,
256 1
                    ));
257
                }
258
259 38
                $variables[$variable]['name'] ??= '';
260 38
                $variables[$variable]['name'] .= $route[$offset];
261 38
                continue;
262
            }
263
264 47
            if (($cursor & self::IN_VARIABLE_PATTERN)) {
265 14
                if ($route[$offset] === RouteCompiler::EXPRESSION_DELIMITER) {
266 1
                    throw new InvalidArgumentException(sprintf(
267 1
                        'The route "%s" could not be parsed due to a syntax error. ' .
268 1
                        'An invalid character was found at position %d. ' .
269 1
                        'Please note that variable patterns cannot contain the character "%s"; ' .
270 1
                        'use an octal or hexadecimal sequence instead.',
271 1
                        $route,
272 1
                        $offset,
273 1
                        RouteCompiler::EXPRESSION_DELIMITER,
274 1
                    ));
275
                }
276
277 13
                $variables[$variable]['pattern'] ??= '';
278 13
                $variables[$variable]['pattern'] .= $route[$offset];
279 13
                continue;
280
            }
281
282
            // {var<\w+>xxx}
283
            // ~~~~~~~~~^^^~
284 46
            if (($cursor & self::IN_VARIABLE)) {
285 1
                throw new InvalidArgumentException(sprintf(
286 1
                    'The route "%s" could not be parsed due to a syntax error. ' .
287 1
                    'An unexpected character was found at position %d; ' .
288 1
                    'a variable at this position must be closed.',
289 1
                    $route,
290 1
                    $offset,
291 1
                ));
292
            }
293
        }
294
295 93
        if (($cursor & self::IN_VARIABLE) || ($cursor & self::IN_OPTIONAL_PART)) {
296 2
            throw new InvalidArgumentException(sprintf(
297 2
                'The route "%s" could not be parsed due to a syntax error. ' .
298 2
                'The attempt to parse the route failed ' .
299 2
                'because it contains an unclosed variable or optional part.',
300 2
                $route,
301 2
            ));
302
        }
303
304
        /** @var list<array{statement: string, name: string, pattern?: string, optional_part?: string}> */
305 91
        return $variables;
0 ignored issues
show
Bug Best Practice introduced by
The expression return $variables returns the type Sunrise\Http\Router\Helper\list which is incompatible with the type-hinted return array.
Loading history...
306
    }
307
}
308