Passed
Push — master ( b202e3...14e011 )
by Divine Niiquaye
02:35 queued 13s
created

Route::castRoute()   B

Complexity

Conditions 11
Paths 26

Size

Total Lines 24
Code Lines 12

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 12
CRAP Score 11

Importance

Changes 1
Bugs 0 Features 0
Metric Value
cc 11
eloc 12
nc 26
nop 1
dl 0
loc 24
ccs 12
cts 12
cp 1
crap 11
rs 7.3166
c 1
b 0
f 0

How to fix   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
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;
19
20
use Closure;
21
use Flight\Routing\Exceptions\InvalidControllerException;
22
use Flight\Routing\Interfaces\RouteInterface;
23
use Serializable;
24
25
/**
26
 * Value object representing a single route.
27
 *
28
 * Routes are a combination of path, middleware, and HTTP methods; two routes
29
 * representing the same path and overlapping HTTP methods are not allowed,
30
 * while two routes representing the same path and non-overlapping HTTP methods
31
 * can be used (and should typically resolve to different middleware).
32
 *
33
 * Internally, only those three properties are required. However, underlying
34
 * router implementations may allow or require additional information, such as
35
 * information defining how to generate a URL from the given route, qualifiers
36
 * for how segments of a route match, or even default values to use. These may
37
 * be provided after instantiation via the "defaults" property and related
38
 * addDefaults() method.
39
 *
40
 * @author Divine Niiquaye Ibok <[email protected]>
41
 */
42
class Route implements Serializable, RouteInterface
43
{
44
    /**
45
     * A Pattern to Locates appropriate route by name, support dynamic route allocation using following pattern:
46
     * Pattern route:   `pattern/*<controller@action>`
47
     * Default route: `*<controller@action>`
48
     * Only action:   `pattern/*<action>`.
49
     *
50
     * @var string
51
     */
52
    public const RCA_PATTERN = '/^(?:(?P<route>[^(.*)]+)\*<)?(?:(?P<controller>[^@]+)@+)?(?P<action>[a-z_\-]+)\>$/i';
53
54
    /** @var string[] */
55
    private $methods = [];
56
57
    /** @var string */
58
    private $path;
59
60
    /** @var null|string */
61
    private $domain;
62
63
    /** @var string */
64
    private $name;
65
66
    /** @var null|callable|object|string|string[] */
67
    private $controller;
68
69
    /** @var string[] */
70
    private $schemes = [];
71
72
    /** @var array<string,mixed> */
73
    private $arguments = [];
74
75
    /** @var array<string,mixed> */
76
    private $defaults = [];
77
78
    /** @var array<string,string|string[]> */
79
    private $patterns = [];
80
81
    /** @var string[] */
82
    private $middlewares = [];
83
84
    /**
85
     * Create a new Route constructor.
86
     *
87
     * @param string                               $name    The route name
88
     * @param string[]                             $methods The route HTTP methods
89
     * @param string                               $pattern The route pattern
90
     * @param null|callable|object|string|string[] $handler The route callable
91
     */
92 49
    public function __construct(string $name, array $methods, string $pattern, $handler)
93
    {
94 49
        $this->name       = $name;
95 49
        $this->controller = $handler;
96 49
        $this->methods    = \array_map('strtoupper', $methods);
97 49
        $this->path       = $this->castRoute($pattern);
98 48
    }
99
100
    /**
101
     * @internal
102
     *
103
     * @return array<string,mixed>
104
     */
105 1
    public function __serialize(): array
106
    {
107
        return [
108 1
            'name'          => $this->name,
109 1
            'path'          => $this->path,
110 1
            'host'          => $this->domain,
111 1
            'schemes'       => $this->schemes,
112 1
            'defaults'      => $this->defaults,
113 1
            'patterns'      => $this->patterns,
114 1
            'methods'       => $this->methods,
115 1
            'middlewares'   => $this->middlewares,
116 1
            'arguments'     => $this->arguments,
117 1
            'handler'       => $this->controller instanceof Closure ? [$this, 'getController'] : $this->controller,
118
        ];
119
    }
120
121
    /**
122
     * @internal
123
     *
124
     * @param array<string,mixed> $data
125
     */
126 1
    public function __unserialize(array $data): void
127
    {
128 1
        $this->name          = $data['name'];
129 1
        $this->path          = $data['path'];
130 1
        $this->domain        = $data['host'];
131 1
        $this->defaults      = $data['defaults'];
132 1
        $this->schemes       = $data['schemes'];
133 1
        $this->patterns      = $data['patterns'];
134 1
        $this->methods       = $data['methods'];
135 1
        $this->controller    = $data['handler'];
136 1
        $this->middlewares   = $data['middlewares'];
137 1
        $this->arguments     = $data['arguments'];
138 1
    }
139
140
    /**
141
     * {@inheritdoc}
142
     *
143
     * @internal
144
     */
145 1
    final public function serialize(): string
146
    {
147 1
        return \serialize($this->__serialize());
148
    }
149
150
    /**
151
     * {@inheritdoc}
152
     *
153
     * @internal
154
     */
155 1
    final public function unserialize($serialized): void
156
    {
157 1
        $this->__unserialize(\unserialize($serialized));
158 1
    }
159
160
    /**
161
     * {@inheritDoc}
162
     */
163 25
    public function getName(): string
164
    {
165 25
        return $this->name;
166
    }
167
168
    /**
169
     * {@inheritDoc}
170
     */
171 29
    public function getPath(): string
172
    {
173 29
        return $this->path;
174
    }
175
176
    /**
177
     * {@inheritDoc}
178
     */
179 27
    public function getMethods(): array
180
    {
181 27
        return $this->methods;
182
    }
183
184
    /**
185
     * {@inheritdoc}
186
     */
187 5
    public function getDomain(): string
188
    {
189 5
        return \str_replace(['http://', 'https://'], '', (string) $this->domain);
190
    }
191
192
    /**
193
     * {@inheritdoc}
194
     */
195 14
    public function getSchemes(): array
196
    {
197 14
        return $this->schemes;
198
    }
199
200
    /**
201
     * {@inheritDoc}
202
     */
203 26
    public function getController()
204
    {
205 26
        return $this->controller;
206
    }
207
208
    /**
209
     * {@inheritdoc}
210
     */
211 25
    public function getArguments(): array
212
    {
213 25
        $routeArguments = [];
214
215 25
        foreach ($this->arguments as $key => $value) {
216 14
            if (\is_int($key)) {
217 3
                continue;
218
            }
219
220 13
            $value                = \is_numeric($value) ? (int) $value : $value;
221 13
            $routeArguments[$key] = \is_string($value) ? \rawurldecode($value) : $value;
222
        }
223
224 25
        return $routeArguments;
225
    }
226
227
    /**
228
     * {@inheritdoc}
229
     */
230 13
    public function getDefaults(): array
231
    {
232 13
        return $this->defaults;
233
    }
234
235
    /**
236
     * {@inheritdoc}
237
     */
238 13
    public function getPatterns(): array
239
    {
240 13
        return $this->patterns;
0 ignored issues
show
introduced by
The expression return $this->patterns returns an array which contains values of type string[] which are incompatible with the return type string mandated by Flight\Routing\Interface...nterface::getPatterns().
Loading history...
241
    }
242
243
    /**
244
     * {@inheritdoc}
245
     */
246 25
    public function getMiddlewares(): array
247
    {
248 25
        return $this->middlewares;
249
    }
250
251
    /**
252
     * {@inheritDoc}
253
     */
254 2
    public function setName(string $name): RouteInterface
255
    {
256 2
        $this->name = $name;
257
258 2
        return $this;
259
    }
260
261
    /**
262
     * {@inheritdoc}
263
     */
264 5
    public function setDomain(string $domain): RouteInterface
265
    {
266 5
        if (false !== \preg_match('@^(?:(https?):)?(\/\/[^/]+)@i', $domain, $matches)) {
267 5
            [, $scheme, $domain] = $matches;
268
269 5
            if (!empty($scheme)) {
270 4
                $this->setScheme($scheme);
271
            }
272
        }
273 5
        $this->domain = \trim($domain, '//');
274
275 5
        return $this;
276
    }
277
278
    /**
279
     * {@inheritDoc}
280
     */
281 4
    public function setScheme(string ...$schemes): RouteInterface
282
    {
283 4
        foreach ($schemes as $scheme) {
284 4
            $this->schemes[] = $scheme;
285
        }
286
287 4
        return $this;
288
    }
289
290
    /**
291
     * {@inheritdoc}
292
     */
293 14
    public function setArguments(array $arguments): RouteInterface
294
    {
295 14
        foreach ($arguments as $key => $value) {
296 14
            $this->arguments[$key] = $value;
297
        }
298
299 14
        return $this;
300
    }
301
302
    /**
303
     * {@inheritdoc}
304
     */
305 22
    public function setDefaults(array $defaults): RouteInterface
306
    {
307 22
        foreach ($defaults as $key => $value) {
308 22
            $this->defaults[$key] = $value;
309
        }
310
311 22
        return $this;
312
    }
313
314
    /**
315
     * {@inheritdoc}
316
     */
317 2
    public function setPatterns(array $patterns): RouteInterface
318
    {
319 2
        foreach ($patterns as $key => $expression) {
320 2
            $this->addPattern($key, $expression);
321
        }
322
323 2
        return $this;
324
    }
325
326
    /**
327
     * {@inheritDoc}
328
     */
329 3
    public function addMethod(string ...$methods): RouteInterface
330
    {
331 3
        foreach ($methods as $method) {
332 3
            $this->methods[] = \strtoupper($method);
333
        }
334
335 3
        return $this;
336
    }
337
338
    /**
339
     * {@inheritdoc}
340
     */
341 2
    public function addPattern(string $name, $expression): RouteInterface
342
    {
343 2
        $this->patterns[$name] = $expression;
344
345 2
        return $this;
346
    }
347
348
    /**
349
     * {@inheritDoc}
350
     */
351 4
    public function addPrefix(string $prefix): RouteInterface
352
    {
353 4
        $this->path = $this->castPrefix($this->path, $prefix);
354
355 4
        return $this;
356
    }
357
358
    /**
359
     * {@inheritDoc}
360
     */
361 32
    public function addMiddleware(...$middlewares): RouteInterface
362
    {
363 32
        foreach ($middlewares as $middleware) {
364 32
            $this->middlewares[] = $middleware;
365
        }
366
367 32
        return $this;
368
    }
369
370
    /**
371
     * Locates appropriate route by name. Support dynamic route allocation using following pattern:
372
     * Pattern route:   `pattern/*<controller@action>`
373
     * Default route: `*<controller@action>`
374
     * Only action:   `pattern/*<action>`.
375
     *
376
     * @param string $route
377
     *
378
     * @throws InvalidControllerException
379
     *
380
     * @return string
381
     */
382 49
    private function castRoute(string $route): string
383
    {
384
        // Match domain + scheme from pattern...
385 49
        if (false !== \preg_match($regex = '@^(?:(https?):)?(//[^/]+)@i', $route)) {
386
            $route = \preg_replace_callback($regex, function (array $matches): string {
387 2
                $this->setDomain(isset($matches[1]) ? $matches[0] : $matches[2]);
388
389 2
                return '';
390 49
            }, $route);
391
        }
392
393 49
        if (false !== \strpbrk($route, '*') && false !== \preg_match(self::RCA_PATTERN, $route, $matches)) {
394 2
            if (!isset($matches['route']) || empty($matches['route'])) {
395 1
                throw new InvalidControllerException("Unable to locate route candidate on `{$route}`");
396
            }
397
398 1
            if (isset($matches['controller'], $matches['action'])) {
399 1
                $this->controller = [$matches['controller'] ?: $this->controller, $matches['action']];
400
            }
401
402 1
            $route = $matches['route'];
403
        }
404
405 48
        return (empty($route) || '/' === $route) ? '/' : $route;
406
    }
407
408
    /**
409
     * Ensures that the right-most slash is trimmed for prefixes of more than
410
     * one character, and that the prefix begins with a slash.
411
     *
412
     * @param string $uri
413
     * @param string $prefix
414
     *
415
     * @return string
416
     */
417 4
    private function castPrefix(string $uri, string $prefix)
418
    {
419
        // Allow homepage uri on prefix just like python django url style.
420 4
        if (\in_array($uri, ['', '/'], true)) {
421 1
            return \rtrim($prefix, '/') . $uri;
422
        }
423
424 4
        if (1 === \preg_match('/^([^\|\/|&|-|_|~|@]+)(&|-|_|~|@)/i', $prefix, $matches)) {
425 1
            $newPattern = \rtrim($prefix, $matches[2]) . $matches[2] . $uri;
426
        }
427
428 4
        return !empty($newPattern) ? $newPattern : \rtrim($prefix, '/') . '/' . \ltrim($uri, '/');
429
    }
430
}
431