Test Failed
Pull Request — master (#16)
by Divine Niiquaye
03:21
created

Route   B

Complexity

Total Complexity 44

Size/Duplication

Total Lines 386
Duplicated Lines 0 %

Test Coverage

Coverage 87%

Importance

Changes 16
Bugs 0 Features 1
Metric Value
eloc 103
c 16
b 0
f 1
dl 0
loc 386
ccs 87
cts 100
cp 0.87
rs 8.8798
wmc 44

22 Methods

Rating   Name   Duplication   Size   Complexity  
A default() 0 5 1
A argument() 0 9 2
A __call() 0 10 2
A method() 0 7 2
A run() 0 5 1
A middleware() 0 7 2
A defaults() 0 7 2
A assert() 0 5 1
A arguments() 0 7 2
A __invoke() 0 17 4
A namespace() 0 11 3
A path() 0 5 1
A scheme() 0 7 2
A get() 0 25 4
A end() 0 10 2
A generateRouteName() 0 9 1
A asserts() 0 7 2
A __set_state() 0 10 2
A bind() 0 5 1
A prefix() 0 5 1
A __construct() 0 7 2
A domain() 0 15 4

How to fix   Complexity   

Complex Class

Complex classes like Route 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 Route, 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;
19
20
use Flight\Routing\Exceptions\InvalidControllerException;
21
use Psr\Http\Message\{ResponseInterface, ServerRequestInterface};
22
use Psr\Http\Server\{MiddlewareInterface, RequestHandlerInterface};
23
24
/**
25
 * Value object representing a single route.
26
 *
27
 * @method string getPath() Gets the route path.
28
 * @method string|null getName() Gets the route name.
29
 * @method string[] getMethods() Gets the route methods.
30
 * @method string[] getSchemes() Gets the route domain schemes.
31
 * @method string[] getDomain() Gets the route host.
32
 * @method mixed getController() Gets the route handler.
33
 * @method array getMiddlewares() Gets the route middlewares.
34
 * @method array getPatterns() Gets the route pattern placeholder assert.
35
 * @method array getDefaults() Gets the route default settings.
36
 * @method array getArguments() Gets the arguments passed to route handler as parameters.
37
 * @method array getAll() Gets all the routes properties.
38
 *
39
 * @author Divine Niiquaye Ibok <[email protected]>
40
 */
41
class Route
42
{
43
    use Traits\CastingTrait;
0 ignored issues
show
Bug introduced by
The trait Flight\Routing\Traits\CastingTrait requires the property $arguments which is not provided by Flight\Routing\Route.
Loading history...
44
45
    /**
46
     * A Pattern to Locates appropriate route by name, support dynamic route allocation using following pattern:
47
     * Pattern route:   `pattern/*<controller@action>`
48
     * Default route: `*<controller@action>`
49
     * Only action:   `pattern/*<action>`.
50
     *
51
     * @var string
52
     */
53
    public const RCA_PATTERN = '#^(?:([a-z]+)\:)?(?:\/{2}([^\/]+))?(.*?)(?:\*\<(?:([\w\\\\]+)\@)?(\w+)\>)?$#u';
54
55
    /**
56
     * A Pattern to match protocol, host and port from a url.
57
     *
58
     * Examples of urls that can be matched: http://en.example.domain, {sub}.example.domain, https://example.com:34, example.com, etc.
59
     *
60
     * @var string
61
     */
62
    public const URL_PATTERN = '#^(?:([a-z]+)\:\/{2})?([^\/]+)?$#u';
63
64
    /**
65
     * Slashes supported on broswer when used.
66
     *
67
     * @var string[]
68
     */
69
    public const URL_PREFIX_SLASHES = ['/' => '/', ':' => ':', '-' => '-', '_' => '_', '~' => '~', '@' => '@'];
70
71
    /**
72
     * Default methods for route.
73
     */
74
    public const DEFAULT_METHODS = [Router::METHOD_GET, Router::METHOD_HEAD];
75
76
    /** @var RouteCollection|null */
77
    private $collection = null;
78
79
    /**
80
     * Create a new Route constructor.
81
     *
82
     * @param string          $pattern The route pattern
83
     * @param string|string[] $methods The route HTTP methods. Multiple methods can be supplied as an array
84
     * @param mixed           $handler The PHP class, object or callable that returns the response when matched
85
     */
86
    public function __construct(string $pattern, $methods = self::DEFAULT_METHODS, $handler = null)
87
    {
88
        $this->controller = $handler;
89
        $this->path = $this->castRoute($pattern);
90
91
        if (!empty($methods)) {
92
            $this->methods = \array_change_key_case(\array_fill_keys((array) $methods, true), \CASE_UPPER);
93 144
        }
94
    }
95 144
96 144
    /**
97
     * @internal This is handled different by router
98 144
     *
99 142
     * @return self
100
     */
101 144
    public static function __set_state(array $properties)
102
    {
103
        $recovered = new self($properties['path'], $properties['methods'], $properties['controller']);
104
        unset($properties['path'], $properties['controller'], $properties['methods']);
105
106
        foreach ($properties as $name => $property) {
107
            $recovered->{$name} = $property;
108
        }
109
110 4
        return $recovered;
111
    }
112 4
113
    /**
114 4
     * @param string  $method
115
     * @param mixed[] $arguments
116 4
     *
117 4
     * @throws \BadMethodCallException
118
     *
119
     * @return mixed
120 4
     */
121
    public function __call($method, $arguments)
122
    {
123
        $routeMethod = (string) \preg_replace('/^get([A-Z]{1}[a-z]+)$/', '\1', $method, 1);
124
        $routeMethod = \strtolower($routeMethod);
125
126
        if (!empty($arguments)) {
127
            throw new \BadMethodCallException(\sprintf('Arguments passed into "%s" method not supported, as method does not exist.', $routeMethod));
128
        }
129
130
        return $this->get($routeMethod);
131 102
    }
132
133 102
    /**
134 102
     * Invoke the response from route handler.
135
     *
136 102
     * @param null|callable(mixed,array) $handlerResolver
137 1
     *
138
     * @return ResponseInterface|string
139
     */
140 101
    public function __invoke(ServerRequestInterface $request, ?callable $handlerResolver = null)
141
    {
142
        $handler = $this->controller;
143
144
        if ($handler instanceof RequestHandlerInterface) {
145
            return $handler->handle($request);
146
        }
147
148
        if (!$handler instanceof ResponseInterface) {
149
            if ($handler instanceof Handlers\ResourceHandler) {
150 15
                $handler = $handler(\strtolower($request->getMethod()));
151
            }
152 15
153
            $handler = $this->castHandler($request, $handlerResolver, $handler);
154 15
        }
155
156
        return $handler;
157
    }
158
159
    /**
160
     * Sets the route path prefix.
161
     */
162
    public function prefix(string $path): self
163
    {
164
        $this->path = $this->castPrefix($this->path, $path);
165
166
        return $this;
167
    }
168
169
    /**
170
     * Sets the route path pattern.
171
     */
172
    public function path(string $pattern): self
173
    {
174
        $this->path = $this->castRoute($pattern);
175
176
        return $this;
177
    }
178 111
179
    /**
180 111
     * Sets the route name.
181
     */
182 111
    public function bind(string $routeName): self
183
    {
184
        $this->name = $routeName;
185
186
        return $this;
187
    }
188
189
    /**
190
     * Sets the route code that should be executed when matched.
191
     *
192 2
     * @param mixed $to PHP class, object or callable that returns the response when matched
193
     */
194 2
    public function run($to): self
195
    {
196 2
        $this->controller = $to;
197
198
        return $this;
199
    }
200
201
    /**
202
     * Sets the missing namespace on route's handler.
203
     *
204
     * @throws InvalidControllerException if $namespace is invalid
205
     */
206
    public function namespace(string $namespace): self
207 12
    {
208
        if ('' !== $namespace) {
209 12
            if ('\\' === $namespace[-1]) {
210
                throw new InvalidControllerException(\sprintf('Namespace "%s" provided for routes must not end with a "\\".', $namespace));
211 12
            }
212
213
            $this->controller = $this->castNamespace($namespace, $this->controller);
214
        }
215
216
        return $this;
217
    }
218
219
    /**
220
     * Sets the requirement for a route variable.
221
     *
222
     * @param string          $variable The variable name
223
     * @param string|string[] $regexp   The regexp to apply
224
     */
225
    public function assert(string $variable, $regexp): self
226
    {
227
        $this->patterns[$variable] = $regexp;
228
229
        return $this;
230
    }
231
232
    /**
233
     * Sets the requirements for a route variable.
234
     *
235
     * @param array<string,string|string[]> $regexps The regexps to apply
236
     */
237
    public function asserts(array $regexps): self
238 49
    {
239
        foreach ($regexps as $variable => $regexp) {
240 49
            $this->assert($variable, $regexp);
241
        }
242 49
243
        return $this;
244
    }
245
246
    /**
247
     * Sets the default value for a route variable.
248
     *
249
     * @param string $variable The variable name
250
     * @param mixed  $default  The default value
251
     */
252
    public function default(string $variable, $default): self
253
    {
254
        $this->defaults[$variable] = $default;
255
256
        return $this;
257
    }
258
259
    /**
260
     * Sets the default values for a route variables.
261
     *
262
     * @param array<string,mixed> $values
263
     */
264
    public function defaults(array $values): self
265
    {
266
        foreach ($values as $variable => $default) {
267
            $this->default($variable, $default);
268
        }
269 28
270
        return $this;
271 28
    }
272 25
273 7
    /**
274 22
     * Sets the parameter value for a route handler.
275 22
     *
276
     * @param string $variable The parameter name
277
     * @param mixed  $value    The parameter value
278 25
     */
279
    public function argument(string $variable, $value): self
280
    {
281 28
        if (\is_string($value)) {
282
            $value = \rawurldecode($value);
283
        }
284
285
        $this->defaults['_arguments'][$variable] = $value;
286
287
        return $this;
288
    }
289
290
    /**
291 49
     * Sets the parameter values for a route handler.
292
     *
293 49
     * @param array<int|string> $variables The route handler parameters
294 13
     */
295
    public function arguments(array $variables): self
296
    {
297 49
        foreach ($variables as $variable => $value) {
298
            $this->argument($variable, $value);
299
        }
300
301
        return $this;
302
    }
303
304
    /**
305
     * Sets the requirement for the HTTP method.
306
     *
307 7
     * @param string $methods the HTTP method(s) name
308
     */
309 7
    public function method(string ...$methods): self
310 7
    {
311
        foreach ($methods as $method) {
312
            $this->methods[\strtoupper($method)] = true;
313 7
        }
314
315
        return $this;
316
    }
317
318
    /**
319
     * Sets the requirement of host on this Route.
320
     *
321
     * @param string $hosts The host for which this route should be enabled
322
     */
323 13
    public function domain(string ...$hosts): self
324
    {
325 13
        foreach ($hosts as $host) {
326 12
            \preg_match(Route::URL_PATTERN, $host, $matches, \PREG_UNMATCHED_AS_NULL);
327
328 12
            if (isset($matches[1])) {
329
                $this->schemes[$matches[1]] = true;
330 12
            }
331
332
            if (isset($matches[2])) {
333
                $this->domain[] = $matches[2];
334
            }
335
        }
336 12
337 10
        return $this;
338
    }
339
340 12
    /**
341 12
     * Sets the requirement of domain scheme on this Route.
342
     *
343
     * @param string ...$schemes
344
     */
345 13
    public function scheme(string ...$schemes): self
346
    {
347
        foreach ($schemes as $scheme) {
348
            $this->schemes[$scheme] = true;
349
        }
350
351
        return $this;
352
    }
353
354
    /**
355 2
     * Sets the middleware(s) to handle before triggering the route handler.
356
     *
357 2
     * @param MiddlewareInterface ...$middlewares
358 2
     */
359
    public function middleware(MiddlewareInterface ...$middlewares): self
360
    {
361 2
        foreach ($middlewares as $middleware) {
362
            $this->middlewares[] = $middleware;
363
        }
364
365
        return $this;
366
    }
367
368
    /**
369
     * Get any of (name, path, domain, defaults, schemes, domain, controller, patterns, middlewares).
370
     * And also accepts "all" and "arguments".
371 46
     *
372
     * @throws \BadMethodCallException if $name does not exist as property
373
     *
374 46
     * @return mixed
375 46
     */
376 1
    public function get(string $name)
377
    {
378 1
        if (\property_exists(__CLASS__, $name)) {
379
            return $this->{$name};
380
        }
381 46
382
        if ('all' === $name) {
383
            return [
384 46
                'controller' => $this->controller,
385
                'methods' => $this->methods,
386
                'schemes' => $this->schemes,
387
                'domain' => $this->domain,
388
                'name' => $this->name,
389
                'path' => $this->path,
390
                'patterns' => $this->patterns,
391
                'middlewares' => $this->middlewares,
392
                'defaults' => $this->defaults,
393
            ];
394
        }
395 147
396
        if ('arguments' === $name) {
397 147
            return $this->defaults['_arguments'] ?? [];
398 145
        }
399
400
        throw new \BadMethodCallException(\sprintf('Invalid call for "%s" as method, %s(\'%1$s\') not supported.', $name, __METHOD__));
401 52
    }
402
403 4
    /**
404 4
     * End a group stack or return self.
405 4
     */
406 4
    public function end(RouteCollection $collection = null): ?RouteCollection
407 4
    {
408 4
        if (null !== $collection) {
409 4
            return $this->collection = $collection;
410 4
        }
411 4
412
        $stack = $this->collection;
413
        $this->collection = null; // Just remove it.
414
415 52
        return $stack ?? $collection;
416 50
    }
417
418
    public function generateRouteName(string $prefix): string
419 2
    {
420
        $methods = \implode('_', \array_keys($this->methods)) . '_';
421
422
        $routeName = $methods . $prefix . $this->path;
423
        $routeName = \str_replace(['/', ':', '|', '-'], '_', $routeName);
424
        $routeName = (string) \preg_replace('/[^a-z0-9A-Z_.]+/', '', $routeName);
425 2
426
        return (string) \preg_replace(['/\_+/', '/\.+/'], ['_', '.'], $routeName);
427 2
    }
428
}
429