Test Failed
Push — master ( 95d817...af60fe )
by Divine Niiquaye
02:49
created

RouteCollection   B

Complexity

Total Complexity 52

Size/Duplication

Total Lines 402
Duplicated Lines 0 %

Test Coverage

Coverage 99.11%

Importance

Changes 9
Bugs 1 Features 0
Metric Value
eloc 99
dl 0
loc 402
ccs 111
cts 112
cp 0.9911
rs 7.44
c 9
b 1
f 0
wmc 52

23 Methods

Rating   Name   Duplication   Size   Complexity  
A addRoute() 0 3 1
A populate() 0 16 4
A post() 0 3 1
A head() 0 3 1
A routes() 0 13 4
A resource() 0 3 1
A patch() 0 3 1
A options() 0 3 1
A buildRoutes() 0 16 4
A add() 0 10 3
A get() 0 3 1
A getRoute() 0 3 1
A group() 0 9 2
A delete() 0 3 1
A __clone() 0 6 2
A put() 0 3 1
A any() 0 3 1
A __construct() 0 4 1
A getRoutes() 0 8 2
A injectRoute() 0 16 5
B injectGroups() 0 27 7
A includeRoute() 0 7 2
A injectGroup() 0 24 5

How to fix   Complexity   

Complex Class

Complex classes like RouteCollection 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 RouteCollection, 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.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;
19
20
/**
21
 * A RouteCollection represents a set of Route instances.
22
 *
23
 * This class provides all(*) methods for creating path+HTTP method-based routes and
24
 * injecting them into the router:
25
 *
26
 * - head
27
 * - get
28
 * - post
29
 * - put
30
 * - patch
31
 * - delete
32
 * - options
33
 * - any
34
 * - resource
35
 *
36
 * A general `addRoute()` method allows specifying multiple request methods and/or
37
 * arbitrary request methods when creating a path-based route.
38
 *
39
 * @author Divine Niiquaye Ibok <[email protected]>
40
 */
41
class RouteCollection
42
{
43
    use Traits\PrototypeTrait;
44
45
    private ?string $namedPrefix;
46
    private ?Route $route = null;
47
    private ?self $parent = null;
48
    private bool $sortRoutes, $locked = false;
49
50
    /** @var array<int,Route> */
51
    private array $routes = [];
52
53
    /** @var array<int,self> */
54
    private array $groups = [];
55
56
    /**
57
     * @param string $namedPrefix The unqiue name for this group
58
     */
59 148
    public function __construct(string $namedPrefix = null, bool $sortRoutes = false)
60
    {
61 148
        $this->namedPrefix = $namedPrefix;
62 148
        $this->sortRoutes = $sortRoutes;
63
    }
64
65
    /**
66
     * Nested collection and routes should be cloned.
67
     */
68 1
    public function __clone()
69
    {
70 1
        $this->includeRoute(); // Incase of missing end method call on route.
71
72 1
        foreach ($this->routes as $offset => $route) {
73 1
            $this->routes[$offset] = clone $route;
74
        }
75
    }
76
77
    /**
78
     * Inject Groups and sort routes in a natural order.
79
     */
80 141
    final public function buildRoutes(): void
81
    {
82 141
        $this->includeRoute(); // Incase of missing end method call on route.
83 141
        $routes = $this->routes;
84
85 141
        if (!empty($this->groups)) {
86 12
            $this->injectGroups('', $routes);
87
        }
88
89 141
        if ($this->sortRoutes) {
90 1
            \usort($routes, static function (Route $a, Route $b): int {
91 1
                return !$a->getStaticPrefix() <=> !$b->getStaticPrefix() ?: \strnatcmp($a->getPath(), $b->getPath());
92
            });
93
        }
94
95 141
        $this->routes = $routes;
96
    }
97
98
    /**
99
     * Get all the routes.
100
     *
101
     * @return array<int,Route>
102
     */
103 141
    public function getRoutes(): array
104
    {
105 141
        if (!$this->locked) {
106 141
            $this->buildRoutes();
107 141
            $this->locked = true; // Lock grouping and prototyping
108
        }
109
110 141
        return $this->routes;
111
    }
112
113
    /**
114
     * Get the current route in stack.
115
     */
116 6
    public function getRoute(): ?Route
117
    {
118 6
        return $this->route;
119
    }
120
121
    /**
122
     * Add route to the collection.
123
     *
124
     * @param bool $inject Whether to call injectRoute() on route
125
     *
126
     * @return $this
127
     */
128 47
    public function add(Route $route, bool $inject = true)
129
    {
130 47
        if ($this->locked) {
131 1
            throw new \RuntimeException('Cannot add a route to a frozen routes collection.');
132
        }
133
134 46
        $this->includeRoute(); // Incase of missing end method call on route.
135 46
        $this->route = $inject ? $this->injectRoute($route) : $route;
136
137 46
        return $this;
138
    }
139
140
    /**
141
     * Maps a pattern to a handler.
142
     *
143
     * You can must specify HTTP methods that should be matched.
144
     *
145
     * @param string   $pattern Matched route pattern
146
     * @param string[] $methods Matched HTTP methods
147
     * @param mixed    $handler Handler that returns the response when matched
148
     *
149
     * @return $this
150
     */
151 36
    public function addRoute(string $pattern, array $methods, $handler = null)
152
    {
153 36
        return $this->add(new Route($pattern, $methods, $handler));
154
    }
155
156
    /**
157
     * Add routes to the collection.
158
     *
159
     * @param array<int,Route> $routes
160
     * @param bool             $inject Whether to call injectRoute() on each route
161
     *
162
     * @throws \TypeError        if $routes doesn't contain a route instance
163
     * @throws \RuntimeException if locked
164
     *
165
     * @return $this
166
     */
167 91
    public function routes(array $routes, bool $inject = true)
168
    {
169 91
        if ($this->locked) {
170
            throw new \RuntimeException('Cannot add a route to a frozen routes collection.');
171
        }
172
173 91
        $this->includeRoute(); // Incase of missing end method call on route.
174
175 91
        foreach ($routes as $route) {
176 91
            $this->routes[] = $inject ? $this->injectRoute($route) : $route;
177
        }
178
179 91
        return $this;
180
    }
181
182
    /**
183
     * Mounts controllers under the given route prefix.
184
     *
185
     * @param string|null                   $name        The route group prefixed name
186
     * @param callable|RouteCollection|null $controllers A RouteCollection instance or a callable for defining routes
187
     *
188
     * @throws \TypeError        if $controllers not instance of route collection's class
189
     * @throws \RuntimeException if locked
190
     *
191
     * @return $this
192
     */
193 13
    public function group(string $name = null, $controllers = null)
194
    {
195 13
        $this->includeRoute(); // Incase of missing end method call on route.
196
197 13
        if (\is_callable($controllers)) {
198 3
            $controllers($routes = $this->injectGroup($name, new static($name)));
199
        }
200
201 13
        return $this->groups[] = $routes ?? $this->injectGroup($name, $controllers ?? new static($name));
0 ignored issues
show
Bug introduced by
It seems like $controllers ?? new static($name) can also be of type callable and null; however, parameter $controllers of Flight\Routing\RouteCollection::injectGroup() does only seem to accept Flight\Routing\RouteCollection, 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

201
        return $this->groups[] = $routes ?? $this->injectGroup($name, /** @scrutinizer ignore-type */ $controllers ?? new static($name));
Loading history...
202
    }
203
204
    /**
205
     * Merge a collection into base.
206
     *
207
     * @throws \RuntimeException if locked
208
     */
209 6
    public function populate(self $collection, bool $asGroup = false): void
210
    {
211 6
        $this->includeRoute();
212
213 6
        if ($asGroup) {
214 4
            $this->groups[] = $this->injectGroup($collection->namedPrefix, $collection);
215
        } else {
216 4
            $collection->includeRoute(); // Incase of missing end method call on route.
217 4
            $routes = $collection->routes;
218
219 4
            if (!empty($collection->groups)) {
220 1
                $collection->injectGroups($collection->namedPrefix ?? '', $routes);
221
            }
222
223 4
            foreach ($routes as $route) {
224 4
                $this->routes[] = $this->injectRoute($route);
225
            }
226
        }
227
    }
228
229
    /**
230
     * Maps a HEAD request to a handler.
231
     *
232
     * @param string $pattern Matched route pattern
233
     * @param mixed  $handler Handler that returns the response when matched
234
     *
235
     * @return $this
236
     */
237 4
    public function head(string $pattern, $handler = null)
238
    {
239 4
        return $this->addRoute($pattern, [Router::METHOD_HEAD], $handler);
240
    }
241
242
    /**
243
     * Maps a GET and HEAD request to a handler.
244
     *
245
     * @param string $pattern Matched route pattern
246
     * @param mixed  $handler Handler that returns the response when matched
247
     *
248
     * @return $this
249
     */
250 11
    public function get(string $pattern, $handler = null)
251
    {
252 11
        return $this->addRoute($pattern, [Router::METHOD_GET, Router::METHOD_HEAD], $handler);
253
    }
254
255
    /**
256
     * Maps a POST request to a handler.
257
     *
258
     * @param string $pattern Matched route pattern
259
     * @param mixed  $handler Handler that returns the response when matched
260
     *
261
     * @return $this
262
     */
263 2
    public function post(string $pattern, $handler = null)
264
    {
265 2
        return $this->addRoute($pattern, [Router::METHOD_POST], $handler);
266
    }
267
268
    /**
269
     * Maps a PUT request to a handler.
270
     *
271
     * @param string $pattern Matched route pattern
272
     * @param mixed  $handler Handler that returns the response when matched
273
     *
274
     * @return $this
275
     */
276 1
    public function put(string $pattern, $handler = null)
277
    {
278 1
        return $this->addRoute($pattern, [Router::METHOD_PUT], $handler);
279
    }
280
281
    /**
282
     * Maps a PATCH request to a handler.
283
     *
284
     * @param string $pattern Matched route pattern
285
     * @param mixed  $handler Handler that returns the response when matched
286
     *
287
     * @return $this
288
     */
289 2
    public function patch(string $pattern, $handler = null)
290
    {
291 2
        return $this->addRoute($pattern, [Router::METHOD_PATCH], $handler);
292
    }
293
294
    /**
295
     * Maps a DELETE request to a handler.
296
     *
297
     * @param string $pattern Matched route pattern
298
     * @param mixed  $handler Handler that returns the response when matched
299
     *
300
     * @return $this
301
     */
302 1
    public function delete(string $pattern, $handler = null)
303
    {
304 1
        return $this->addRoute($pattern, [Router::METHOD_DELETE], $handler);
305
    }
306
307
    /**
308
     * Maps a OPTIONS request to a handler.
309
     *
310
     * @param string $pattern Matched route pattern
311
     * @param mixed  $handler Handler that returns the response when matched
312
     *
313
     * @return $this
314
     */
315 1
    public function options(string $pattern, $handler = null)
316
    {
317 1
        return $this->addRoute($pattern, [Router::METHOD_OPTIONS], $handler);
318
    }
319
320
    /**
321
     * Maps any request to a handler.
322
     *
323
     * @param string $pattern Matched route pattern
324
     * @param mixed  $handler Handler that returns the response when matched
325
     *
326
     * @return $this
327
     */
328 1
    public function any(string $pattern, $handler = null)
329
    {
330 1
        return $this->addRoute($pattern, Router::HTTP_METHODS_STANDARD, $handler);
331
    }
332
333
    /**
334
     * Maps any Router::HTTP_METHODS_STANDARD request to a resource handler prefixed to $action's method name.
335
     *
336
     * E.g: Having pattern as "/accounts/{userId}", all request made from supported request methods
337
     * are to have the same url.
338
     *
339
     * @param string              $action   The prefixed name attached to request method
340
     * @param string              $pattern  matched path where request should be sent to
341
     * @param class-string|object $resource Handler that returns the response
0 ignored issues
show
Documentation Bug introduced by
The doc comment class-string|object at position 0 could not be parsed: Unknown type name 'class-string' at position 0 in class-string|object.
Loading history...
342
     *
343
     * @return $this
344
     */
345 1
    public function resource(string $pattern, $resource, string $action = 'action')
346
    {
347 1
        return $this->any($pattern, new Handlers\ResourceHandler($resource, $action));
348
    }
349
350
    /**
351
     * @throws \RuntimeException if locked
352
     */
353 58
    protected function injectRoute(Route $route): Route
354
    {
355 58
        if (!empty($defaultsStack = $this->prototypes)) {
356 10
            foreach ($defaultsStack as $routeMethod => $arguments) {
357 10
                if ('prefix' === $routeMethod) {
358 6
                    $route->prefix(\implode('', \array_merge(...$arguments)));
359
                    continue;
360 6
                }
361
362
                foreach ($arguments as $parameters) {
363 9
                    \call_user_func_array([$route, $routeMethod], $parameters);
364 2
                }
365 2
            }
366
        }
367
368 2
        return $route;
369
    }
370
371 8
    /**
372
     * Include route to stack if not done.
373
     */
374
    protected function includeRoute(): void
375 58
    {
376
        if (null !== $this->route) {
377
            $this->defaultIndex = -1;
378
379
            $this->routes[] = $this->route; // Incase an end method is missing at the end of a route call.
380
            $this->route = null;
381 143
        }
382
    }
383 143
384 44
    /**
385
     * @return self|$this
386 44
     */
387 44
    protected function injectGroup(?string $prefix, self $controllers)
388
    {
389
        if ($this->locked) {
390
            throw new \RuntimeException('Cannot add a nested routes collection to a frozen routes collection.');
391
        }
392
393
        if ($controllers->sortRoutes) {
394 16
            throw new \RuntimeException('Cannot sort routes in a nested collection.');
395
        }
396 16
397 2
        $controllers->includeRoute(); // Incase of missing end method call on route.
398
399
        if (empty($controllers->routes)) {
400 14
            $controllers->defaultIndex = 1;
401 1
        }
402
403
        $controllers->prototypes = \array_merge($this->prototypes, $controllers->prototypes);
404 13
        $controllers->parent = $this;
405
406 13
        if (empty($controllers->namedPrefix)) {
407 7
            $controllers->namedPrefix = $prefix;
408
        }
409
410 13
        return $controllers;
411 13
    }
412
413 13
    /**
414 13
     * @param array<int,Route> $collection
415
     */
416
    private function injectGroups(string $prefix, array &$collection): void
417 13
    {
418
        $unnamedRoutes = [];
419
420
        foreach ($this->groups as $group) {
421
            $group->includeRoute(); // Incase of missing end method call on route.
422
423 13
            foreach ($group->routes as $route) {
424
                if (empty($name = $route->getName())) {
425 13
                    $name = $route->generateRouteName('');
426
427 13
                    if (isset($unnamedRoutes[$name])) {
428 13
                        $name .= ('_' !== $name[-1] ? '_' : '') . ++$unnamedRoutes[$name];
429
                    } else {
430 13
                        $unnamedRoutes[$name] = 0;
431 13
                    }
432 12
                }
433
434 12
                $collection[] = $route->bind($prefix . $group->namedPrefix . $name);
435 2
            }
436
437 12
            if (!empty($group->groups)) {
438
                $group->injectGroups($prefix . $group->namedPrefix, $collection);
439
            }
440
        }
441 13
442
        $this->groups = [];
443
    }
444
}
445