Test Failed
Push — master ( 79397a...a27c44 )
by Divine Niiquaye
15:30
created

RouteCollection::create()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 7
Code Lines 4

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 4
CRAP Score 1

Importance

Changes 1
Bugs 0 Features 0
Metric Value
cc 1
eloc 4
c 1
b 0
f 0
nc 1
nop 2
dl 0
loc 7
ccs 4
cts 4
cp 1
crap 1
rs 10
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
    public function __construct(string $namedPrefix = null)
57
    {
58
        $this->namedPrefix = $namedPrefix;
59
    }
60
61
    /**
62
     * Nested collection and routes should be cloned.
63
     */
64
    public function __clone()
65
    {
66
        $this->includeRoute(); // Incase of missing end method call on route.
67
68
        foreach ($this->routes as $offset => $route) {
69
            $this->routes[$offset] = clone $route;
70
        }
71
    }
72
73
    /**
74
     * Create an instance of resolved routes.
75
     *
76
     * @return static
77
     */
78
    final public static function create(array $routes, bool $locked = true)
79 13
    {
80
        $collection = new static();
81 13
        $collection->routes = $routes;
82 13
        $collection->locked = $locked;
83
84 13
        return $collection;
85 1
    }
86 1
87
    /**
88
     * Inject Groups and sort routes in a natural order.
89 1
     */
90
    final public function buildRoutes(bool $lock = true): void
91 12
    {
92 12
        $this->includeRoute(); // Incase of missing end method call on route.
93
        $routes = $this->routes;
94
95
        if (!empty($this->groups)) {
96 11
            $this->injectGroups('', $routes);
97
        }
98
99
        \usort($routes, static function (Route $a, Route $b): int {
100
            return !$a->getStaticPrefix() <=> !$b->getStaticPrefix() ?: \strnatcmp($a->getPath(), $b->getPath());
101
        });
102
103
        $this->locked = $lock; // Lock grouping and prototyping
104 5
        $this->routes = $routes;
105
    }
106 5
107
    /**
108
     * Get all the routes.
109
     *
110
     * @return array<int,Route>
111
     */
112
    public function getRoutes(): array
113
    {
114
        if (!$this->locked) {
115
            $this->buildRoutes();
116 81
        }
117
118 81
        return $this->routes;
119
    }
120
121
    /**
122
     * Get the current route in stack.
123
     */
124
    public function getRoute(): ?Route
125
    {
126
        return $this->route;
127
    }
128
129
    /**
130 70
     * Add route to the collection.
131
     *
132 70
     * @return $this
133 70
     */
134
    public function add(Route $route)
135 70
    {
136
        $this->route = $this->injectRoute($route);
137
138 70
        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 29
     * @return $this
151
     */
152 29
    public function addRoute(string $pattern, array $methods, $handler = null)
153
    {
154 29
        $this->route = $this->injectRoute(new Route($pattern, $methods, $handler));
155
156 29
        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 10
     * @return $this
168
     */
169 10
    public function routes(array $routes)
170 1
    {
171 1
        foreach ($routes as $route) {
172
            $this->routes[] = $this->injectRoute($route);
173
        }
174 10
175 3
        return $this;
176 3
    }
177
178 3
    /**
179 3
     * Maps a HEAD request to a handler.
180 7
     *
181 1
     * @param string $pattern Matched route pattern
182
     * @param mixed  $handler Handler that returns the response when matched
183
     *
184 9
     * @return $this
185
     */
186 9
    public function head(string $pattern, $handler = null)
187 9
    {
188
        return $this->addRoute($pattern, [Router::METHOD_HEAD], $handler);
189 9
    }
190
191
    /**
192
     * Maps a GET and HEAD request to a handler.
193
     *
194
     * @param string $pattern Matched route pattern
195 1
     * @param mixed  $handler Handler that returns the response when matched
196
     *
197
     * @return $this
198 1
     */
199 1
    public function get(string $pattern, $handler = null)
200
    {
201
        return $this->addRoute($pattern, [Router::METHOD_GET, Router::METHOD_HEAD], $handler);
202 1
    }
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 5
     */
212
    public function post(string $pattern, $handler = null)
213 5
    {
214
        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 10
     *
223
     * @return $this
224 10
     */
225
    public function put(string $pattern, $handler = null)
226
    {
227
        return $this->addRoute($pattern, [Router::METHOD_PUT], $handler);
228
    }
229
230
    /**
231
     * Maps a PATCH request to a handler.
232
     *
233 2
     * @param string $pattern Matched route pattern
234
     * @param mixed  $handler Handler that returns the response when matched
235 2
     *
236
     * @return $this
237
     */
238
    public function patch(string $pattern, $handler = null)
239
    {
240
        return $this->addRoute($pattern, [Router::METHOD_PATCH], $handler);
241
    }
242
243
    /**
244 1
     * Maps a DELETE request to a handler.
245
     *
246 1
     * @param string $pattern Matched route pattern
247
     * @param mixed  $handler Handler that returns the response when matched
248
     *
249
     * @return $this
250
     */
251
    public function delete(string $pattern, $handler = null)
252
    {
253
        return $this->addRoute($pattern, [Router::METHOD_DELETE], $handler);
254
    }
255 2
256
    /**
257 2
     * 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
    public function options(string $pattern, $handler = null)
265
    {
266 1
        return $this->addRoute($pattern, [Router::METHOD_OPTIONS], $handler);
267
    }
268 1
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 3
     * @param string              $action   The prefixed name attached to request method
289
     * @param string              $pattern  matched path where request should be sent to
290 3
     * @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
    public function resource(string $pattern, $resource, string $action = 'action')
295
    {
296
        return $this->any($pattern, new Handlers\ResourceHandler($resource, $action));
297
    }
298
299
    /**
300
     * @throws \RuntimeException if locked
301
     */
302 2
    protected function injectRoute(Route $route): Route
303
    {
304 2
        if ($this->locked) {
305
            throw new \RuntimeException('Cannot add a route to a frozen routes collection.');
306
        }
307
308
        $this->includeRoute(); // Incase of missing end method call on route.
309
310
        if (!empty($defaultsStack = $this->prototypes)) {
311
            foreach ($defaultsStack as $routeMethod => $arguments) {
312
                if ('prefix' === $routeMethod) {
313
                    $route->prefix(\implode('', \array_merge(...$arguments)));
314 71
315
                    continue;
316
                }
317 71
318 26
                if (\count($arguments) > 1) {
319 6
                    foreach ($arguments as $argument) {
320
                        $route->{$routeMethod}(...$argument);
321
                    }
322
323 71
                    continue;
324
                }
325
326
                $route->{$routeMethod}(...$arguments[0]);
327
            }
328
        }
329 92
330
        return $route;
331 92
    }
332 1
333
    /**
334 1
     * Include route to stack if not done.
335 1
     */
336
    protected function includeRoute(self $collection = null): void
337
    {
338
        if (null !== $this->route) {
339 92
            $this->defaultIndex = -1;
340 2
341
            $this->routes[] = $this->route; // Incase an end method is missing at the end of a route call.
342 92
            $this->route = null;
343
        }
344
345
        if (null !== $collection) {
346
            $collection->includeRoute();
347 81
348
            if (empty($collection->routes)) {
349 81
                $collection->defaultIndex = 1;
350
            }
351
352 81
            $collection->prototypes = \array_merge($this->prototypes, $collection->prototypes);
353 78
            $collection->parent = $this;
354 9
        }
355
    }
356
}
357