Test Failed
Branch master (effa58)
by Divine Niiquaye
02:13
created

FastRoute::resolveNamespace()   B

Complexity

Conditions 8
Paths 4

Size

Total Lines 15
Code Lines 7

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 8
eloc 7
nc 4
nop 2
dl 0
loc 15
rs 8.4444
c 0
b 0
f 0
1
<?php
2
3
declare(strict_types=1);
4
5
/*
6
 * This file is part of Flight Routing.
7
 *
8
 * PHP version 7.4 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
    /** @var array<int,string> 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 array $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 array $data = [];
67
68
    /** @var array<int,string> */
69
    protected array $middlewares = [];
70
71
    private ?RouteCollection $collection = null;
72
73
    /**
74
     * Create a new Route constructor.
75
     *
76
     * @param string          $pattern The route pattern
77
     * @param string|string[] $methods the route HTTP methods
78
     * @param mixed           $handler The PHP class, object or callable that returns the response when matched
79
     */
80
    public function __construct(string $pattern, $methods = self::DEFAULT_METHODS, $handler = null)
81
    {
82
        $this->data = [
83
            'path' => $pattern,
84
            'handler' => $handler,
85
            'methods' => !empty($methods) ? \array_map('strtoupper', (array) $methods) : [],
86
        ];
87
    }
88
89
    /**
90
     * @param string[] $arguments
91
     *
92
     * @throws \BadMethodCallException
93
     *
94
     * @return mixed
95
     */
96
    public function __call(string $method, array $arguments)
97
    {
98
        if (\str_starts_with($method = \strtolower($method), 'get')) {
99
            $method = \substr($method, 3);
100
        }
101
102
        if (!empty($arguments)) {
103
            throw new \BadMethodCallException(\sprintf('Arguments passed into "%s::%s(...)" not supported, method invalid.', __CLASS__, $method));
104
        }
105
106
        return $this->get($method);
107
    }
108
109
    /**
110
     * @internal
111
     */
112
    public function __serialize(): array
113
    {
114
        return $this->data;
115
    }
116
117
    /**
118
     * @internal
119
     *
120
     * @param array<string,mixed> $data
121
     */
122
    public function __unserialize(array $data): void
123
    {
124
        $this->data = $data;
125
    }
126
127
    /**
128
     * @internal
129
     *
130
     * @param array<string,mixed> $properties The route data properties
131
     *
132
     * @return static
133
     */
134
    public static function __set_state(array $properties)
135
    {
136
        $route = new static($properties['path'] ?? '', $properties['methods'] ?? [], $properties['handler'] ?? null);
137
        $route->data += \array_diff_key($properties, ['path' => null, 'methods' => [], 'handler' => null]);
138
139
        return $route;
140
    }
141
142
    /**
143
     * Create a new Route statically.
144
     *
145
     * @param string          $pattern The route pattern
146
     * @param string|string[] $methods the route HTTP methods
147
     * @param mixed           $handler The PHP class, object or callable that returns the response when matched
148
     *
149
     * @return static
150
     */
151
    public static function to(string $pattern, $methods = self::DEFAULT_METHODS, $handler = null)
152
    {
153
        return new static($pattern, $methods, $handler);
154
    }
155
156
    /**
157
     * Asserts route.
158
     *
159
     * @throws MethodNotAllowedException
160
     *
161
     * @return static
162
     */
163
    public function match(string $method, UriInterface $uri)
164
    {
165
        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

165
        if (!\in_array($method, /** @scrutinizer ignore-type */ $methods = $this->get('methods'), true)) {
Loading history...
166
            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

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