Passed
Push — master ( 76d475...f4623c )
by Divine Niiquaye
02:29 queued 01:44
created

RouteCollection::create()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 7
Code Lines 4

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 5
CRAP Score 1

Importance

Changes 1
Bugs 0 Features 0
Metric Value
cc 1
eloc 4
nc 1
nop 2
dl 0
loc 7
ccs 5
cts 5
cp 1
crap 1
rs 10
c 1
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;
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
    use Traits\GroupingTrait;
45
46
    /** @var array<int,Route> */
47
    private array $routes = [];
48
49
    private ?Route $route = null;
50
51
    private bool $locked = false;
52
53
    /**
54
     * @param string $namedPrefix The unqiue name for this group
55
     */
56 143
    public function __construct(string $namedPrefix = null)
57
    {
58 143
        $this->namedPrefix = $namedPrefix;
59
    }
60
61
    /**
62
     * Nested collection and routes should be cloned.
63
     */
64 1
    public function __clone()
65
    {
66 1
        $this->includeRoute(); // Incase of missing end method call on route.
67
68 1
        foreach ($this->routes as $offset => $route) {
69 1
            $this->routes[$offset] = clone $route;
70
        }
71
    }
72
73
    /**
74
     * Create an instance of resolved routes.
75
     *
76
     * @return static
77
     */
78 13
    final public static function create(array $routes, bool $locked = true)
79
    {
80 13
        $collection = new static();
81 13
        $collection->routes = $routes;
82 13
        $collection->locked = $locked;
83
84 13
        return $collection;
85
    }
86
87
    /**
88
     * Inject Groups and sort routes in a natural order.
89
     */
90 136
    final public function buildRoutes(bool $lock = true): void
91
    {
92 136
        $this->includeRoute(); // Incase of missing end method call on route.
93 136
        $routes = $this->routes;
94
95 136
        if (!empty($this->groups)) {
96 12
            $this->injectGroups('', $routes);
97
        }
98
99 136
        \usort($routes, static function (Route $a, Route $b): int {
100 45
            return !$a->getStaticPrefix() <=> !$b->getStaticPrefix() ?: \strnatcmp($a->getPath(), $b->getPath());
101
        });
102
103 136
        $this->locked = $lock; // Lock grouping and prototyping
104 136
        $this->routes = $routes;
105
    }
106
107
    /**
108
     * Get all the routes.
109
     *
110
     * @return array<int,Route>
111
     */
112 138
    public function getRoutes(): array
113
    {
114 138
        if (!$this->locked) {
115 136
            $this->buildRoutes();
116
        }
117
118 138
        return $this->routes;
119
    }
120
121
    /**
122
     * Get the current route in stack.
123
     */
124 6
    public function getRoute(): ?Route
125
    {
126 6
        return $this->route;
127
    }
128
129
    /**
130
     * Add route to the collection.
131
     *
132
     * @return $this
133
     */
134 15
    public function add(Route $route)
135
    {
136 15
        $this->route = $this->injectRoute($route);
137
138 15
        return $this;
139
    }
140
141
    /**
142
     * Maps a pattern to a handler.
143
     *
144
     * You can must specify HTTP methods that should be matched.
145
     *
146
     * @param string   $pattern Matched route pattern
147
     * @param string[] $methods Matched HTTP methods
148
     * @param mixed    $handler Handler that returns the response when matched
149
     *
150
     * @return $this
151
     */
152 32
    public function addRoute(string $pattern, array $methods, $handler = null)
153
    {
154 32
        $this->route = $this->injectRoute(new Route($pattern, $methods, $handler));
155
156 31
        return $this;
157
    }
158
159
    /**
160
     * Add routes to the collection.
161
     *
162
     * @param Route[] $routes
163
     *
164
     * @throws \TypeError        if $routes doesn't contain a route instance
165
     * @throws \RuntimeException if locked
166
     *
167
     * @return $this
168
     */
169 89
    public function routes(array $routes)
170
    {
171 89
        foreach ($routes as $route) {
172 89
            $this->routes[] = $this->injectRoute($route);
173
        }
174
175 89
        return $this;
176
    }
177
178
    /**
179
     * Maps a HEAD request to a handler.
180
     *
181
     * @param string $pattern Matched route pattern
182
     * @param mixed  $handler Handler that returns the response when matched
183
     *
184
     * @return $this
185
     */
186 4
    public function head(string $pattern, $handler = null)
187
    {
188 4
        return $this->addRoute($pattern, [Router::METHOD_HEAD], $handler);
189
    }
190
191
    /**
192
     * Maps a GET and HEAD request to a handler.
193
     *
194
     * @param string $pattern Matched route pattern
195
     * @param mixed  $handler Handler that returns the response when matched
196
     *
197
     * @return $this
198
     */
199 11
    public function get(string $pattern, $handler = null)
200
    {
201 11
        return $this->addRoute($pattern, [Router::METHOD_GET, Router::METHOD_HEAD], $handler);
202
    }
203
204
    /**
205
     * Maps a POST request to a handler.
206
     *
207
     * @param string $pattern Matched route pattern
208
     * @param mixed  $handler Handler that returns the response when matched
209
     *
210
     * @return $this
211
     */
212 2
    public function post(string $pattern, $handler = null)
213
    {
214 2
        return $this->addRoute($pattern, [Router::METHOD_POST], $handler);
215
    }
216
217
    /**
218
     * Maps a PUT request to a handler.
219
     *
220
     * @param string $pattern Matched route pattern
221
     * @param mixed  $handler Handler that returns the response when matched
222
     *
223
     * @return $this
224
     */
225 1
    public function put(string $pattern, $handler = null)
226
    {
227 1
        return $this->addRoute($pattern, [Router::METHOD_PUT], $handler);
228
    }
229
230
    /**
231
     * Maps a PATCH request to a handler.
232
     *
233
     * @param string $pattern Matched route pattern
234
     * @param mixed  $handler Handler that returns the response when matched
235
     *
236
     * @return $this
237
     */
238 2
    public function patch(string $pattern, $handler = null)
239
    {
240 2
        return $this->addRoute($pattern, [Router::METHOD_PATCH], $handler);
241
    }
242
243
    /**
244
     * Maps a DELETE request to a handler.
245
     *
246
     * @param string $pattern Matched route pattern
247
     * @param mixed  $handler Handler that returns the response when matched
248
     *
249
     * @return $this
250
     */
251 1
    public function delete(string $pattern, $handler = null)
252
    {
253 1
        return $this->addRoute($pattern, [Router::METHOD_DELETE], $handler);
254
    }
255
256
    /**
257
     * Maps a OPTIONS request to a handler.
258
     *
259
     * @param string $pattern Matched route pattern
260
     * @param mixed  $handler Handler that returns the response when matched
261
     *
262
     * @return $this
263
     */
264 1
    public function options(string $pattern, $handler = null)
265
    {
266 1
        return $this->addRoute($pattern, [Router::METHOD_OPTIONS], $handler);
267
    }
268
269
    /**
270
     * Maps any request to a handler.
271
     *
272
     * @param string $pattern Matched route pattern
273
     * @param mixed  $handler Handler that returns the response when matched
274
     *
275
     * @return $this
276
     */
277 1
    public function any(string $pattern, $handler = null)
278
    {
279 1
        return $this->addRoute($pattern, Router::HTTP_METHODS_STANDARD, $handler);
280
    }
281
282
    /**
283
     * Maps any Router::HTTP_METHODS_STANDARD request to a resource handler prefixed to $action's method name.
284
     *
285
     * E.g: Having pattern as "/accounts/{userId}", all request made from supported request methods
286
     * are to have the same url.
287
     *
288
     * @param string              $action   The prefixed name attached to request method
289
     * @param string              $pattern  matched path where request should be sent to
290
     * @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...
291
     *
292
     * @return $this
293
     */
294 1
    public function resource(string $pattern, $resource, string $action = 'action')
295
    {
296 1
        return $this->any($pattern, new Handlers\ResourceHandler($resource, $action));
297
    }
298
299
    /**
300
     * @throws \RuntimeException if locked
301
     */
302 132
    protected function injectRoute(Route $route): Route
303
    {
304 132
        if ($this->locked) {
305 1
            throw new \RuntimeException('Cannot add a route to a frozen routes collection.');
306
        }
307
308 131
        $this->includeRoute(); // Incase of missing end method call on route.
309
310 131
        if (!empty($defaultsStack = $this->prototypes)) {
311 10
            foreach ($defaultsStack as $routeMethod => $arguments) {
312 10
                if ('prefix' === $routeMethod) {
313 6
                    $route->prefix(\implode('', \array_merge(...$arguments)));
314
315 6
                    continue;
316
                }
317
318 9
                if (\count($arguments) > 1) {
319 2
                    foreach ($arguments as $argument) {
320 2
                        $route->{$routeMethod}(...$argument);
321
                    }
322
323 2
                    continue;
324
                }
325
326 8
                $route->{$routeMethod}(...$arguments[0]);
327
            }
328
        }
329
330 131
        return $route;
331
    }
332
333
    /**
334
     * Include route to stack if not done.
335
     */
336 136
    protected function includeRoute(self $collection = null): void
337
    {
338 136
        if (null !== $this->route) {
339 41
            $this->defaultIndex = -1;
340
341 41
            $this->routes[] = $this->route; // Incase an end method is missing at the end of a route call.
342 41
            $this->route = null;
343
        }
344
345 136
        if (null !== $collection) {
346 15
            $collection->includeRoute();
347
348 15
            if (empty($collection->routes)) {
349 7
                $collection->defaultIndex = 1;
350
            }
351
352 15
            $collection->prototypes = \array_merge($this->prototypes, $collection->prototypes);
353 15
            $collection->parent = $this;
354
        }
355
    }
356
}
357