Test Failed
Pull Request — master (#16)
by Divine Niiquaye
02:34
created

FastRoute   B

Complexity

Total Complexity 52

Size/Duplication

Total Lines 424
Duplicated Lines 0 %

Importance

Changes 3
Bugs 0 Features 0
Metric Value
wmc 52
eloc 99
c 3
b 0
f 0
dl 0
loc 424
rs 7.44

27 Methods

Rating   Name   Duplication   Size   Complexity  
A getPiped() 0 3 1
A path() 0 5 1
A defaults() 0 7 2
B resolveNamespace() 0 15 8
A asserts() 0 7 2
A __call() 0 11 3
A end() 0 7 2
A bind() 0 5 1
A generateRouteName() 0 7 1
A default() 0 5 1
A __unserialize() 0 3 1
A to() 0 3 1
A prefix() 0 5 1
A namespace() 0 13 4
A __construct() 0 6 2
A belong() 0 3 1
A piped() 0 7 2
A __serialize() 0 3 1
A assert() 0 5 1
A getData() 0 9 2
A __set_state() 0 6 1
A get() 0 11 3
A method() 0 7 2
A run() 0 5 1
A arguments() 0 7 2
A argument() 0 11 3
A match() 0 7 2

How to fix   Complexity   

Complex Class

Complex classes like FastRoute often do a lot of different things. To break such a class down, we need to identify a cohesive component within that class. A common approach to find such a component is to look for fields/methods that share the same prefixes, or suffixes.

Once you have determined the fields that belong together, you can apply the Extract Class refactoring. If the component makes sense as a sub-class, Extract Subclass is also a candidate, and is often faster.

While breaking up the class, it is a good idea to analyze how other classes use FastRoute, and based on these observations, apply Extract Interface, too.

1
<?php
2
3
declare(strict_types=1);
4
5
/*
6
 * This file is part of Flight Routing.
7
 *
8
 * PHP version 7.1 and above required
9
 *
10
 * @author    Divine Niiquaye Ibok <[email protected]>
11
 * @copyright 2019 Biurad Group (https://biurad.com/)
12
 * @license   https://opensource.org/licenses/BSD-3-Clause License
13
 *
14
 * For the full copyright and license information, please view the LICENSE
15
 * file that was distributed with this source code.
16
 */
17
18
namespace Flight\Routing\Routes;
19
20
use Flight\Routing\Exceptions\{MethodNotAllowedException, InvalidControllerException};
21
use Flight\Routing\{Router, RouteCollection};
22
use Flight\Routing\Handlers\ResourceHandler;
23
use Psr\Http\Message\UriInterface;
24
25
/**
26
 * Value object representing a single route.
27
 *
28
 * Route path and prefixing a path are not casted. This class is meant to be
29
 * extendable for addition support on route(s).
30
 *
31
 * The default support for this route class:
32
 * - name binding
33
 * - methods binding
34
 * - handler & namespacing
35
 * - arguments binding to handler
36
 * - pattern placeholders assert binding
37
 * - add defaults binding
38
 *
39
 * @method string      getPath()      Gets the route path.
40
 * @method string|null getName()      Gets the route name.
41
 * @method string[]    getMethods()   Gets the route methods.
42
 * @method mixed       getHandler()   Gets the route handler.
43
 * @method array       getArguments() Gets the arguments passed to route handler as parameters.
44
 * @method array       getDefaults()  Gets the route default settings.
45
 * @method array       getPatterns()  Gets the route pattern placeholder assert.
46
 *
47
 * @author Divine Niiquaye Ibok <[email protected]>
48
 */
49
class FastRoute
50
{
51
    /** Default methods for route. */
52
    public const DEFAULT_METHODS = [Router::METHOD_GET, Router::METHOD_HEAD];
53
54
    /** @var array<string,string> Getter methods supported by route */
55
    protected static $getter = [
56
        'name' => 'name',
57
        'path' => 'path',
58
        'methods' => 'methods*',
59
        'handler' => 'handler',
60
        'arguments' => 'arguments*',
61
        'defaults' => 'defaults*',
62
        'patterns' => 'patterns*',
63
    ];
64
65
    /** @var array<string,mixed> */
66
    protected $data = [];
67
68
    /** @var array<int,string> */
69
    protected $middlewares = [];
70
71
    /** @var RouteCollection|null */
72
    private $collection;
73
74
    /**
75
     * Create a new Route constructor.
76
     *
77
     * @param string          $pattern The route pattern
78
     * @param string|string[] $methods the route HTTP methods
79
     * @param mixed           $handler The PHP class, object or callable that returns the response when matched
80
     */
81
    public function __construct(string $pattern, $methods = self::DEFAULT_METHODS, $handler = null)
82
    {
83
        $this->data = [
84
            'path' => $pattern,
85
            'handler' => $handler,
86
            'methods' => !empty($methods) ? \array_map('strtoupper', (array) $methods) : [],
87
        ];
88
    }
89
90
    /**
91
     * @param string[] $arguments
92
     *
93
     * @throws \BadMethodCallException
94
     *
95
     * @return mixed
96
     */
97
    public function __call(string $method, array $arguments)
98
    {
99
        if (\str_starts_with($method = \strtolower($method), 'get')) {
100
            $method = \substr($method, 3);
101
        }
102
103
        if (!empty($arguments)) {
104
            throw new \BadMethodCallException(\sprintf('Arguments passed into "%s::%s(...)" not supported, method invalid.', __CLASS__, $method));
105
        }
106
107
        return $this->get($method);
108
    }
109
110
    /**
111
     * @internal
112
     */
113
    public function __serialize(): array
114
    {
115
        return $this->data;
116
    }
117
118
    /**
119
     * @internal
120
     *
121
     * @param array<string,mixed> $data
122
     */
123
    public function __unserialize(array $data): void
124
    {
125
        $this->data = $data;
126
    }
127
128
    /**
129
     * @internal
130
     *
131
     * @param array<string,mixed> $properties The route data properties
132
     *
133
     * @return static
134
     */
135
    public static function __set_state(array $properties)
136
    {
137
        $route = new static($properties['path'] ?? '', $properties['methods'] ?? [], $properties['handler'] ?? null);
138
        $route->data += \array_diff_key($properties, ['path' => null, 'methods' => [], 'handler' => null]);
139
140
        return $route;
141
    }
142
143
    /**
144
     * Create a new Route statically.
145
     *
146
     * @param string          $pattern The route pattern
147
     * @param string|string[] $methods the route HTTP methods
148
     * @param mixed           $handler The PHP class, object or callable that returns the response when matched
149
     *
150
     * @return static
151
     */
152
    public static function to(string $pattern, $methods = self::DEFAULT_METHODS, $handler = null)
153
    {
154
        return new static($pattern, $methods, $handler);
155
    }
156
157
    /**
158
     * Asserts route.
159
     *
160
     * @throws MethodNotAllowedException
161
     *
162
     * @return static
163
     */
164
    public function match(string $method, UriInterface $uri)
165
    {
166
        if (!\in_array($method, $methods = $this->get('methods'), true)) {
0 ignored issues
show
Bug introduced by
It seems like $methods = $this->get('methods') can also be of type null; however, parameter $haystack of in_array() does only seem to accept array, maybe add an additional type check? ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-type  annotation

166
        if (!\in_array($method, /** @scrutinizer ignore-type */ $methods = $this->get('methods'), true)) {
Loading history...
167
            throw new MethodNotAllowedException($methods, $uri->getPath(), $method);
0 ignored issues
show
Bug introduced by
It seems like $methods can also be of type null; however, parameter $methods of Flight\Routing\Exception...xception::__construct() does only seem to accept array, maybe add an additional type check? ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-type  annotation

167
            throw new MethodNotAllowedException(/** @scrutinizer ignore-type */ $methods, $uri->getPath(), $method);
Loading history...
168
        }
169
170
        return $this;
171
    }
172
173
    /**
174
     * Sets the route path prefix.
175
     *
176
     * @return static
177
     */
178
    public function prefix(string $path)
179
    {
180
        $this->data['path'] = $path . $this->data['path'] ?? '';
181
182
        return $this;
183
    }
184
185
    /**
186
     * Sets the route path pattern.
187
     *
188
     * @return static
189
     */
190
    public function path(string $pattern)
191
    {
192
        $this->data['path'] = $pattern;
193
194
        return $this;
195
    }
196
197
    /**
198
     * Sets the requirement for the HTTP method.
199
     *
200
     * @param string $methods the HTTP method(s) name
201
     *
202
     * @return static
203
     */
204
    public function method(string ...$methods)
205
    {
206
        foreach ($methods as $method) {
207
            $this->data['methods'][] = \strtoupper($method);
208
        }
209
210
        return $this;
211
    }
212
213
    /**
214
     * Sets the route name.
215
     *
216
     * @return static
217
     */
218
    public function bind(string $routeName)
219
    {
220
        $this->data['name'] = $routeName;
221
222
        return $this;
223
    }
224
225
    /**
226
     * Sets the parameter value for a route handler.
227
     *
228
     * @param mixed $value The parameter value
229
     *
230
     * @return static
231
     */
232
    public function argument(string $parameter, $value)
233
    {
234
        if (\is_numeric($value)) {
235
            $value = (int) $value;
236
        } elseif (\is_string($value)) {
237
            $value = \rawurldecode($value);
238
        }
239
240
        $this->data['arguments'][$parameter] = $value;
241
242
        return $this;
243
    }
244
245
    /**
246
     * Sets the parameter values for a route handler.
247
     *
248
     * @param array<int|string> $parameters The route handler parameters
249
     *
250
     * @return static
251
     */
252
    public function arguments(array $parameters)
253
    {
254
        foreach ($parameters as $variable => $value) {
255
            $this->argument($variable, $value);
256
        }
257
258
        return $this;
259
    }
260
261
    /**
262
     * Sets the route code that should be executed when matched.
263
     *
264
     * @param mixed $to PHP class, object or callable that returns the response when matched
265
     *
266
     * @return static
267
     */
268
    public function run($to)
269
    {
270
        $this->data['handler'] = $to;
271
272
        return $this;
273
    }
274
275
    /**
276
     * Sets the missing namespace on route's handler.
277
     *
278
     * @throws InvalidControllerException if $namespace is invalid
279
     *
280
     * @return static
281
     */
282
    public function namespace(string $namespace)
283
    {
284
        if ('' !== $namespace) {
285
            if ('\\' === $namespace[-1]) {
286
                throw new InvalidControllerException(\sprintf('Namespace "%s" provided for routes must not end with a "\\".', $namespace));
287
            }
288
289
            if (isset($this->data['handler'])) {
290
                $this->data['handler'] = self::resolveNamespace($namespace, $this->data['handler']);
291
            }
292
        }
293
294
        return $this;
295
    }
296
297
    /**
298
     * Attach a named middleware group(s) to route.
299
     *
300
     * @return static
301
     */
302
    public function piped(string ...$to)
303
    {
304
        foreach ($to as $namedMiddleware) {
305
            $this->middlewares[] = $namedMiddleware;
306
        }
307
308
        return $this;
309
    }
310
311
    /**
312
     * Sets the requirement for a route variable.
313
     *
314
     * @param string|string[] $regexp The regexp to apply
315
     *
316
     * @return static
317
     */
318
    public function assert(string $variable, $regexp)
319
    {
320
        $this->data['patterns'][$variable] = $regexp;
321
322
        return $this;
323
    }
324
325
    /**
326
     * Sets the requirements for a route variable.
327
     *
328
     * @param array<string,string|string[]> $regexps The regexps to apply
329
     *
330
     * @return static
331
     */
332
    public function asserts(array $regexps)
333
    {
334
        foreach ($regexps as $variable => $regexp) {
335
            $this->assert($variable, $regexp);
336
        }
337
338
        return $this;
339
    }
340
341
    /**
342
     * Sets the default value for a route variable.
343
     *
344
     * @param mixed $default The default value
345
     *
346
     * @return static
347
     */
348
    public function default(string $variable, $default)
349
    {
350
        $this->data['defaults'][$variable] = $default;
351
352
        return $this;
353
    }
354
355
    /**
356
     * Sets the default values for a route variables.
357
     *
358
     * @param array<string,mixed> $values
359
     *
360
     * @return static
361
     */
362
    public function defaults(array $values)
363
    {
364
        foreach ($values as $variable => $default) {
365
            $this->default($variable, $default);
366
        }
367
368
        return $this;
369
    }
370
371
    /**
372
     * Sets the route belonging to a particular collection.
373
     *
374
     * This method is kinda internal, only used in RouteCollection class,
375
     * and retrieved using this class end method.
376
     *
377
     * @internal used by RouteCollection class
378
     */
379
    public function belong(RouteCollection $to): void
380
    {
381
        $this->collection = $to;
382
    }
383
384
    /**
385
     * End a group stack or return self.
386
     */
387
    public function end(): ?RouteCollection
388
    {
389
        if (null !== $stack = $this->collection) {
390
            $this->collection = null; // Just remove it.
391
        }
392
393
        return $stack;
394
    }
395
396
    /**
397
     * Get a return from any valid key name of this class $getter static property.
398
     *
399
     * @throws \InvalidArgumentException if $name does not exist as property
400
     *
401
     * @return mixed
402
     */
403
    public function get(string $name)
404
    {
405
        if (null === $key = static::$getter[$name] ?? null) {
406
            throw new \InvalidArgumentException(\sprintf('Invalid call for "%s" in %s(\'%1$s\'), try any of [%s].', $name, __METHOD__, \implode(',', \array_keys(static::$getter))));
407
        }
408
409
        if ('*' === $key[-1]) {
410
            return \array_unique($this->data[\substr($key, 0, -1)] ?? []);
411
        }
412
413
        return $this->data[$key] ?? null;
414
    }
415
416
    /**
417
     * Return the list of attached grouped middlewares.
418
     *
419
     * @return array<int,string>
420
     */
421
    public function getPiped(): array
422
    {
423
        return $this->middlewares;
424
    }
425
426
    /**
427
     * Get the route's data.
428
     *
429
     * @return array<string,mixed>
430
     */
431
    public function getData(): array
432
    {
433
        return \array_map(function (string $property) {
434
            if ('*' === $property[-1]) {
435
                $property = \substr($property, 0, -1);
436
            }
437
438
            return $this->get($property);
439
        }, static::$getter);
440
    }
441
442
    public function generateRouteName(string $prefix): string
443
    {
444
        $routeName = \implode('_', $this->data['methods'] ?? []) . '_' . $prefix . $this->data['path'] ?? '';
445
        $routeName = \str_replace(['/', ':', '|', '-'], '_', $routeName);
446
        $routeName = (string) \preg_replace('/[^a-z0-9A-Z_.]+/', '', $routeName);
447
448
        return (string) \preg_replace(['/\_+/', '/\.+/'], ['_', '.'], $routeName);
449
    }
450
451
    /**
452
     * @internal skip throwing an exception and return existing $controller
453
     *
454
     * @param callable|object|string|string[] $controller
455
     *
456
     * @return mixed
457
     */
458
    private static function resolveNamespace(string $namespace, $controller)
459
    {
460
        if ($controller instanceof ResourceHandler) {
461
            return $controller->namespace($namespace);
462
        }
463
464
        if (\is_string($controller) && (!\str_starts_with($controller, $namespace) && '\\' === $controller[0])) {
465
            return $namespace . $controller;
466
        }
467
468
        if ((\is_array($controller) && \array_keys($controller) === [0, 1]) && \is_string($controller[0])) {
469
            $controller[0] = self::resolveNamespace($namespace, $controller[0]);
470
        }
471
472
        return $controller;
473
    }
474
}
475