Test Failed
Push — master ( 08de7a...d45f24 )
by Divine Niiquaye
11:57
created

RouteCollection::options()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 3
Code Lines 1

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 2
CRAP Score 1

Importance

Changes 0
Metric Value
eloc 1
c 0
b 0
f 0
dl 0
loc 3
ccs 2
cts 2
cp 1
rs 10
cc 1
nc 1
nop 2
crap 1
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
 * @method RouteCollection assert(string $variable, string|string[] $regexp)
40
 * @method RouteCollection default(string $variable, mixed $default)
41
 * @method RouteCollection argument(string $variable, mixed $value)
42
 * @method RouteCollection method(string ...$methods)
43
 * @method RouteCollection scheme(string ...$schemes)
44
 * @method RouteCollection domain(string ...$hosts)
45
 * @method RouteCollection prefix(string $path)
46
 * @method RouteCollection defaults(array $values)
47
 * @method RouteCollection asserts(array $patterns)
48
 * @method RouteCollection arguments(array $parameters)
49
 * @method RouteCollection namespace(string $namespace)
50
 * @method RouteCollection piped(string ...$to)
51
 *
52
 * @author Divine Niiquaye Ibok <[email protected]>
53
 */
54
final class RouteCollection
55
{
56
    private ?self $parent = null;
57
58
    /** @var array<string,mixed[]>|null */
59
    private ?array $prototypes = [];
60
61
    /** @var array<string,bool> */
62
    private array $prototyped = [];
63
64
    /** @var Routes\FastRoute[] */
65
    private iterable $routes = [];
66
67
    /** @var self[] */
68
    private array $groups = [];
69
70
    private ?string $uniqueId;
71
72
    private string $namedPrefix;
73
74
    /**
75
     * @param string $namedPrefix The unqiue name for this group
76
     */
77
    public function __construct(string $namedPrefix = '')
78
    {
79 13
        $this->namedPrefix = $namedPrefix;
80
        $this->uniqueId = (string) \uniqid($namedPrefix);
81 13
    }
82 13
83
    /**
84 13
     * Nested collection and routes should be cloned.
85 1
     */
86 1
    public function __clone()
87
    {
88
        foreach ($this->routes as $offset => $route) {
89 1
            $this->routes[$offset] = clone $route;
90
        }
91 12
    }
92 12
93
    /**
94
     * @param string[] $arguments
95
     */
96 11
    public function __call(string $routeMethod, array $arguments): self
97
    {
98
        $routeMethod = \strtolower($routeMethod);
99
100
        if (isset($this->prototyped[$this->uniqueId])) {
101
            $this->prototypes[$routeMethod] = \array_merge($this->prototypes[$routeMethod] ?? [], $arguments);
102
        } else {
103
            foreach ($this->routes as $route) {
104 5
                \call_user_func_array([$route, $routeMethod], $arguments);
105
            }
106 5
107
            if (\array_key_exists($routeMethod, $this->prototypes ?? [])) {
108
                unset($this->prototypes[$routeMethod]);
109
            }
110
111
            foreach ($this->groups as $group) {
112
                \call_user_func_array([$group, $routeMethod], $arguments);
113
            }
114
        }
115
116 81
        return $this;
117
    }
118 81
119
    /**
120
     * @return iterable<Routes\FastRoute>
121
     */
122
    public function getRoutes(): iterable
123
    {
124
        $routes = $this->routes;
0 ignored issues
show
Documentation Bug introduced by
It seems like $this->routes of type iterable is incompatible with the declared type Flight\Routing\Routes\FastRoute[] of property $routes.

Our type inference engine has found an assignment to a property that is incompatible with the declared type of that property.

Either this assignment is in error or the assigned type should be added to the documentation/type hint for that property..

Loading history...
125
126
        if ($routes instanceof \SplFixedArray) {
127
            return $routes;
128
        }
129
130 70
        if (!empty($this->groups)) {
131
            $this->injectGroups('', $routes);
132 70
        }
133 70
134
        $this->uniqueId = null; // Lock grouping and prototyping
135 70
136
        return $this->routes = self::sortRoutes($routes);
137
    }
138 70
139
    /**
140
     * Merge a collection into base.
141
     *
142
     * @throws \RuntimeException if locked
143
     */
144
    public function populate(self $collection, bool $asGroup = false): void
145
    {
146
        if ($asGroup) {
147
            if (null === $this->uniqueId) {
148
                throw new \RuntimeException('Populating a route collection as group must be done before calling the getRoutes() method.');
149
            }
150 29
151
            $this->groups[] = $collection;
152 29
        } else {
153
            $routes = $collection->routes;
0 ignored issues
show
Documentation Bug introduced by
It seems like $collection->routes of type iterable is incompatible with the declared type Flight\Routing\Routes\FastRoute[] of property $routes.

Our type inference engine has found an assignment to a property that is incompatible with the declared type of that property.

Either this assignment is in error or the assigned type should be added to the documentation/type hint for that property..

Loading history...
154 29
155
            if (!empty($collection->groups)) {
156 29
                $collection->injectGroups($collection->namedPrefix, $routes);
157
            }
158
159
            foreach ($routes as $route) {
160
                $this->routes[] = $this->injectRoute($route);
161
            }
162
163
            $collection->routes = $collection->prototypes = [];
164
        }
165
    }
166
167 10
    /**
168
     * Add route to the collection.
169 10
     */
170 1
    public function add(Routes\FastRoute $route): self
171 1
    {
172
        $this->routes[] = $this->injectRoute($route);
173
174 10
        return $this;
175 3
    }
176 3
177
    /**
178 3
     * Maps a pattern to a handler.
179 3
     *
180 7
     * You can must specify HTTP methods that should be matched.
181 1
     *
182
     * @param string   $pattern Matched route pattern
183
     * @param string[] $methods Matched HTTP methods
184 9
     * @param mixed    $handler Handler that returns the response when matched
185
     */
186 9
    public function addRoute(string $pattern, array $methods, $handler = null): Routes\Route
187 9
    {
188
        return $this->routes[] = $this->injectRoute(new Routes\Route($pattern, $methods, $handler));
189 9
    }
190
191
    /**
192
     * Same as addRoute method, except uses Routes\FastRoute class.
193
     *
194
     * @param string   $pattern Matched route pattern
195 1
     * @param string[] $methods Matched HTTP methods
196
     * @param mixed    $handler Handler that returns the response when matched
197
     */
198 1
    public function fastRoute(string $pattern, array $methods, $handler = null): Routes\FastRoute
199 1
    {
200
        return $this->routes[] = $this->injectRoute(new Routes\FastRoute($pattern, $methods, $handler));
201
    }
202 1
203
    /**
204
     * Add routes to the collection.
205
     *
206
     * @param Routes\FastRoute[] $routes
207
     *
208
     * @throws \TypeError if $routes doesn't contain a fast route instance
209
     * @throws \RuntimeException if locked
210
     */
211 5
    public function routes(array $routes): self
212
    {
213 5
        foreach ($routes as $route) {
214
            $this->routes[] = $this->injectRoute($route);
215
        }
216
217
        return $this;
218
    }
219
220
    /**
221
     * Mounts controllers under the given route prefix.
222 10
     *
223
     * @param string                   $name        The route group prefixed name
224 10
     * @param callable|RouteCollection $controllers A RouteCollection instance or a callable for defining routes
225
     *
226
     * @throws \TypeError        if $controllers not instance of route collection's class
227
     * @throws \RuntimeException if locked
228
     */
229
    public function group(string $name, $controllers = null): self
230
    {
231
        if (null === $this->uniqueId) {
232
            throw new \RuntimeException('Grouping index invalid or out of range, add group before calling the getRoutes() method.');
233 2
        }
234
235 2
        if (\is_callable($controllers)) {
236
            $routes = new static($name);
237
            $routes->prototypes = $this->prototypes ?? [];
238
            $controllers($routes);
239
240
            return $this->groups[] = $routes;
241
        }
242
243
        return $this->groups[] = $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

243
        return $this->groups[] = $this->injectGroup($name, /** @scrutinizer ignore-type */ $controllers ?? new static($name));
Loading history...
244 1
    }
245
246 1
    /**
247
     * Allows a proxied method call to route's.
248
     *
249
     * @throws \RuntimeException if locked
250
     */
251
    public function prototype(): self
252
    {
253
        if (null === $uniqueId = $this->uniqueId) {
254
            throw new \RuntimeException('Routes method prototyping must be done before calling the getRoutes() method.');
255 2
        }
256
257 2
        $this->prototypes = (null !== $this->parent) ? ($this->parent->prototypes ?? []) : [];
258
        $this->prototyped[$uniqueId] = true; // Prototyping calls to routes ...
259
260
        return $this;
261
    }
262
263
    /**
264
     * Unmounts a group collection to continue routes stalk.
265
     */
266 1
    public function end(): self
267
    {
268 1
        if (isset($this->prototyped[$this->uniqueId])) {
269
            unset($this->prototyped[$this->uniqueId]);
270
271
            // Remove last element from stack.
272
            if (null !== $stack = $this->prototypes) {
273
                \array_pop($stack);
274
            }
275
276
            return $this;
277 1
        }
278
279 1
        return $this->parent ?? $this;
280
    }
281
282
    /**
283
     * Maps a HEAD request to a handler.
284
     *
285
     * @param string $pattern Matched route pattern
286
     * @param mixed  $handler Handler that returns the response when matched
287
     */
288 3
    public function head(string $pattern, $handler = null): Routes\Route
289
    {
290 3
        return $this->addRoute($pattern, [Router::METHOD_HEAD], $handler);
291
    }
292
293
    /**
294
     * Maps a GET and HEAD request to a handler.
295
     *
296
     * @param string $pattern Matched route pattern
297
     * @param mixed  $handler Handler that returns the response when matched
298
     */
299
    public function get(string $pattern, $handler = null): Routes\Route
300
    {
301
        return $this->addRoute($pattern, [Router::METHOD_GET, Router::METHOD_HEAD], $handler);
302 2
    }
303
304 2
    /**
305
     * Maps a POST request to a handler.
306
     *
307
     * @param string $pattern Matched route pattern
308
     * @param mixed  $handler Handler that returns the response when matched
309
     */
310
    public function post(string $pattern, $handler = null): Routes\Route
311
    {
312
        return $this->addRoute($pattern, [Router::METHOD_POST], $handler);
313
    }
314 71
315
    /**
316
     * Maps a PUT request to a handler.
317 71
     *
318 26
     * @param string $pattern Matched route pattern
319 6
     * @param mixed  $handler Handler that returns the response when matched
320
     */
321
    public function put(string $pattern, $handler = null): Routes\Route
322
    {
323 71
        return $this->addRoute($pattern, [Router::METHOD_PUT], $handler);
324
    }
325
326
    /**
327
     * Maps a PATCH request to a handler.
328
     *
329 92
     * @param string $pattern Matched route pattern
330
     * @param mixed  $handler Handler that returns the response when matched
331 92
     */
332 1
    public function patch(string $pattern, $handler = null): Routes\Route
333
    {
334 1
        return $this->addRoute($pattern, [Router::METHOD_PATCH], $handler);
335 1
    }
336
337
    /**
338
     * Maps a DELETE request to a handler.
339 92
     *
340 2
     * @param string $pattern Matched route pattern
341
     * @param mixed  $handler Handler that returns the response when matched
342 92
     */
343
    public function delete(string $pattern, $handler = null): Routes\Route
344
    {
345
        return $this->addRoute($pattern, [Router::METHOD_DELETE], $handler);
346
    }
347 81
348
    /**
349 81
     * Maps a OPTIONS request to a handler.
350
     *
351
     * @param string $pattern Matched route pattern
352 81
     * @param mixed  $handler Handler that returns the response when matched
353 78
     */
354 9
    public function options(string $pattern, $handler = null): Routes\Route
355
    {
356 9
        return $this->addRoute($pattern, [Router::METHOD_OPTIONS], $handler);
357
    }
358
359 78
    /**
360 18
     * Maps any request to a handler.
361
     *
362 18
     * @param string $pattern Matched route pattern
363 2
     * @param mixed  $handler Handler that returns the response when matched
364
     */
365
    public function any(string $pattern, $handler = null): Routes\Route
366
    {
367 78
        return $this->addRoute($pattern, Router::HTTP_METHODS_STANDARD, $handler);
368
    }
369 81
370
    /**
371 81
     * Maps any Router::HTTP_METHODS_STANDARD request to a resource handler prefixed to $action's method name.
372
     *
373
     * E.g: Having pattern as "/accounts/{userId}", all request made from supported request methods
374
     * are to have the same url.
375
     *
376
     * @param string              $action   The prefixed name attached to request method
377
     * @param string              $pattern  matched path where request should be sent to
378
     * @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...
379
     */
380
    public function resource(string $pattern, $resource, string $action = 'action'): Routes\Route
381
    {
382
        return $this->any($pattern, new Handlers\ResourceHandler($resource, $action));
383
    }
384
385
    /**
386
     * Rearranges routes, sorting static paths before dynamic paths.
387
     *
388
     * @param Routes\FastRoute[] $routes
389
     *
390
     * @return \SplFixedArray<Routes\FastRoute>
391
     */
392
    private static function sortRoutes(array $routes): \SplFixedArray
393
    {
394
        $sortRegex = '#^\\/[\\/\\w]+$#';
395
396
        \usort($routes, static function (Routes\FastRoute $a, Routes\FastRoute $b) use ($sortRegex): int {
397
            $aB = \preg_match($sortRegex, $aP = $a->getPath());
398
            $bB = \preg_match($sortRegex, $bP = $b->getPath());
399
400
            return $aB && $bB ? 0 : ($aB < $bB ? +1 : ($aB > $bB ? -1 : \strcmp($aP, $bP)));
401
        });
402
403
        return \SplFixedArray::fromArray($routes);
404
    }
405
406
    /**
407
     * @throws \RuntimeException if locked
408
     */
409
    private function injectRoute(Routes\FastRoute $route): Routes\FastRoute
410
    {
411
        foreach ($this->prototypes ?? [] as $routeMethod => $arguments) {
412
            if (empty($arguments)) {
413
                continue;
414
            }
415
416
            \call_user_func_array([$route, $routeMethod], 'prefix' === $routeMethod ? [\implode('', $arguments)] : $arguments);
417
        }
418
419
        if (null !== $this->parent) {
420
            $route->belong($this); // Attach grouping to route.
421
        }
422
423
        return $route;
424
    }
425
426
    private function injectGroup(string $prefix, self $controllers): self
427
    {
428
        $controllers->prototypes = $this->prototypes ?? [];
429
        $controllers->parent = $this;
430
431
        if (empty($controllers->namedPrefix)) {
432
            $controllers->namedPrefix = $prefix;
433
        }
434
435
        return $controllers;
436
    }
437
438
    /**
439
     * @param iterable<int,Routes\FastRoute> $collection
440
     */
441
    private function injectGroups(string $prefix, iterable &$collection): void
442
    {
443
        $unnamedRoutes = [];
444
445
        foreach ($this->groups as $group) {
446
            foreach ($group->routes as $route) {
447
                if (empty($name = $route->getName())) {
448
                    $name = $route->generateRouteName('');
449
450
                    if (isset($unnamedRoutes[$name])) {
451
                        $name .= ('_' !== $name[-1] ? '_' : '') . ++$unnamedRoutes[$name];
452
                    } else {
453
                        $unnamedRoutes[$name] = 0;
454
                    }
455
                }
456
457
                $collection[] = $route->bind($prefix . $group->namedPrefix . $name);
458
            }
459
460
            if (!empty($group->groups)) {
461
                $group->injectGroups($prefix . $group->namedPrefix, $collection);
462
            }
463
        }
464
465
        $this->groups = [];
466
    }
467
}
468