Passed
Pull Request — master (#14)
by Divine Niiquaye
02:38
created

RouteCollection::head()   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.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
/**
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
 * __call() forwards method-calls to Route, but returns instance of RouteCollection
40
 * listing Route's methods below, so that IDEs know they are valid
41
 *
42
 * @method RouteCollection withAssert(string $variable, string|string[] $regexp)
43
 * @method RouteCollection withDefault(string $variable, mixed $default)
44
 * @method RouteCollection withArgument($variable, mixed $value)
45
 * @method RouteCollection withMethod(string ...$methods)
46
 * @method RouteCollection withScheme(string ...$schemes)
47
 * @method RouteCollection withMiddleware(mixed ...$middlewares)
48
 * @method RouteCollection withDomain(string ...$hosts)
49
 * @method RouteCollection withPrefix(string $path)
50
 * @method RouteCollection withDefaults(array $values)
51
 * @method RouteCollection withAsserts(array $patterns)
52
 * @method RouteCollection withArguments(array $patterns)
53
 *
54
 * @author Divine Niiquaye Ibok <[email protected]>
55
 */
56
final class RouteCollection implements \IteratorAggregate, \Countable
57
{
58
    /** @var string|null */
59
    private $namePrefix;
60
61
    /** @var Route[] */
62
    private $routes = [];
63
64
    /** @var self|null */
65
    private $parent = null;
66
67
    /** @var array<string,mixed[]>|null */
68
    private $stack = null;
69
70
    /** @var \ArrayIterator|null */
71
    private $iterable = null;
72
73
    /**
74
     * @param string   $method
75
     * @param string[] $arguments
76
     *
77
     * @return mixed
78
     */
79 13
    public function __call($method, $arguments)
80
    {
81 13
        $routeMethod = (string) \preg_replace('/^with([A-Z]{1}[a-z]+)$/', '\1', $method, 1);
82 13
        $routeMethod = \strtolower($routeMethod);
83
84 13
        if (null !== $this->stack) {
85 1
            if ([] !== $stack = $this->stack[$routeMethod] ?? []) {
86 1
                $arguments = \array_merge(\end($stack), $arguments);
87
            }
88
89 1
            $this->stack[$routeMethod][] = $arguments;
90
        } else {
91 12
            foreach ($this->routes as $route) {
92 12
                \call_user_func_array([$route, $routeMethod], $arguments);
93
            }
94
        }
95
96 11
        return $this;
97
    }
98
99
    /**
100
     * Gets the number of Routes in this collection.
101
     *
102
     * @return int The number of routes
103
     */
104 5
    public function count(): int
105
    {
106 5
        return $this->getIterator()->count();
107
    }
108
109
    /**
110
     * Gets the filtered RouteCollection as an ArrayIterator that includes all routes.
111
     *
112
     * @see doMerge() method
113
     *
114
     * @return \ArrayIterator<int,Route> The filtered routes
115
     */
116 81
    public function getIterator(): \ArrayIterator
117
    {
118 81
        return $this->iterable ?? $this->iterable = new \ArrayIterator($this->doMerge('', new static()));
119
    }
120
121
    /**
122
     * Add route(s) to the collection.
123
     *
124
     * This method unset all setting from default route and use new settings
125
     * from new the route(s). If you want the default settings to be merged
126
     * into routes, use `addRoute` method instead.
127
     *
128
     * @param Route ...$routes
129
     */
130 70
    public function add(Route ...$routes): self
131
    {
132 70
        foreach ($routes as $route) {
133 70
            $this->routes[] = $route;
134
135 70
            $this->resolveWith($route);
136
        }
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
    public function addRoute(string $pattern, array $methods, $handler = null): Route
151
    {
152 29
        $this->routes[] = $route = new Route($pattern, $methods, $handler);
153
154 29
        $this->resolveWith($route);
155
156 29
        return $route;
157
    }
158
159
    /**
160
     * Mounts controllers under the given route prefix.
161
     *
162
     * @param string                   $name        The route group prefixed name
163
     * @param callable|RouteCollection $controllers A RouteCollection instance or a callable for defining routes
164
     *
165
     * @throws \LogicException
166
     */
167 10
    public function group(string $name, $controllers = null): self
168
    {
169 10
        if (null === $controllers) {
170 1
            $controllers = new static();
171 1
            $controllers->stack = $this->stack ?? [];
172
        }
173
174 10
        if (\is_callable($controllers)) {
175 3
            $deprecated = 'Since %s v1.1, usage of %s() method\'s second parameter for callable is deprecated. Will be dropped in v2.0';
176 3
            @\trigger_error(\sprintf($deprecated, 'divineniiquaye/flight-routing', __METHOD__), \E_USER_DEPRECATED);
177
178 3
            $controllers($collection = new static());
179 3
            $controllers = clone $collection;
180 7
        } elseif (!$controllers instanceof self) {
181 1
            throw new \LogicException('The "group" method takes either a "RouteCollection" instance or callable.');
182
        }
183
184 9
        $this->routes[] = $controllers;
185
186 9
        $controllers->namePrefix = $name;
187 9
        $controllers->parent = $this;
188
189 9
        return $controllers;
190
    }
191
192
    /**
193
     * Unmounts a group collection to continue routes stalk.
194
     */
195 1
    public function end(): self
196
    {
197
        // Remove last element from stack.
198 1
        foreach ($this->stack as $stack) {
199 1
            \array_pop($stack);
200
        }
201
202 1
        return $this->parent ?? $this;
203
    }
204
205
    /**
206
     * Maps a HEAD request to a handler.
207
     *
208
     * @param string $pattern Matched route pattern
209
     * @param mixed  $handler Handler that returns the response when matched
210
     */
211 5
    public function head(string $pattern, $handler = null): Route
212
    {
213 5
        return $this->addRoute($pattern, [Router::METHOD_HEAD], $handler);
214
    }
215
216
    /**
217
     * Maps a GET and HEAD request to a handler.
218
     *
219
     * @param string $pattern Matched route pattern
220
     * @param mixed  $handler Handler that returns the response when matched
221
     */
222 10
    public function get(string $pattern, $handler = null): Route
223
    {
224 10
        return $this->addRoute($pattern, [Router::METHOD_GET, Router::METHOD_HEAD], $handler);
225
    }
226
227
    /**
228
     * Maps a POST request to a handler.
229
     *
230
     * @param string $pattern Matched route pattern
231
     * @param mixed  $handler Handler that returns the response when matched
232
     */
233 2
    public function post(string $pattern, $handler = null): Route
234
    {
235 2
        return $this->addRoute($pattern, [Router::METHOD_POST], $handler);
236
    }
237
238
    /**
239
     * Maps a PUT request to a handler.
240
     *
241
     * @param string $pattern Matched route pattern
242
     * @param mixed  $handler Handler that returns the response when matched
243
     */
244 1
    public function put(string $pattern, $handler = null): Route
245
    {
246 1
        return $this->addRoute($pattern, [Router::METHOD_PUT], $handler);
247
    }
248
249
    /**
250
     * Maps a PATCH request to a handler.
251
     *
252
     * @param string $pattern Matched route pattern
253
     * @param mixed  $handler Handler that returns the response when matched
254
     */
255 2
    public function patch(string $pattern, $handler = null): Route
256
    {
257 2
        return $this->addRoute($pattern, [Router::METHOD_PATCH], $handler);
258
    }
259
260
    /**
261
     * Maps a DELETE request to a handler.
262
     *
263
     * @param string $pattern Matched route pattern
264
     * @param mixed  $handler Handler that returns the response when matched
265
     */
266 1
    public function delete(string $pattern, $handler = null): Route
267
    {
268 1
        return $this->addRoute($pattern, [Router::METHOD_DELETE], $handler);
269
    }
270
271
    /**
272
     * Maps a OPTIONS request to a handler.
273
     *
274
     * @param string $pattern Matched route pattern
275
     * @param mixed  $handler Handler that returns the response when matched
276
     */
277 1
    public function options(string $pattern, $handler = null): Route
278
    {
279 1
        return $this->addRoute($pattern, [Router::METHOD_OPTIONS], $handler);
280
    }
281
282
    /**
283
     * Maps any 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 any(string $pattern, $handler = null): Route
289
    {
290 3
        return $this->addRoute($pattern, Router::HTTP_METHODS_STANDARD, $handler);
291
    }
292
293
    /**
294
     * Maps any request to a resource handler and prefix class method by request method.
295
     * If you request on "/account" path with a GET method, prefixed by the name
296
     * parameter eg: 'user', class method will match `getUser`.
297
     *
298
     * @param string              $name     The prefixed name attached to request method
299
     * @param string              $pattern  matched path where request should be sent to
300
     * @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...
301
     */
302 2
    public function resource(string $name, string $pattern, $resource): Route
303
    {
304 2
        return $this->any(\sprintf('api://%s/%s', $name, $pattern), $resource);
305
    }
306
307
    /**
308
     * Find a route by name.
309
     *
310
     * @param string $name The route name
311
     *
312
     * @return Route|null A Route instance or null when not found
313
     */
314 71
    public function find(string $name): ?Route
315
    {
316
        /** @var Route|RouteCollection $route */
317 71
        foreach ($this->iterable ?? $this->routes as $route) {
318 26
            if ($route instanceof Route && $name === $route->get('name')) {
319 6
                return $route;
320
            }
321
        }
322
323 71
        return null;
324
    }
325
326
    /**
327
     * Bind route with collection.
328
     */
329 92
    private function resolveWith(Route $route): void
330
    {
331 92
        foreach ($this->stack ?? [] as $routeMethod => $arguments) {
332 1
            $stack = \end($arguments);
333
334 1
            if (false !== $stack) {
335 1
                \call_user_func_array([$route, $routeMethod], 'prefix' === $routeMethod ? [\implode('', $stack)] : $stack);
336
            }
337
        }
338
339 92
        if (null !== $this->parent) {
340 2
            $route->end($this);
341
        }
342 92
    }
343
344
    /**
345
     * @return Route[]
346
     */
347 81
    private function doMerge(string $prefix, self $routes): array
348
    {
349 81
        $defaultUnnamedIndex = 0;
350
351
        /** @var Route|RouteCollection $route */
352 81
        foreach ($this->routes as $route) {
353 78
            if ($route instanceof self) {
354 9
                $route->doMerge($prefix . $route->namePrefix, $routes);
0 ignored issues
show
Bug introduced by
The method doMerge() does not exist on Flight\Routing\Route. Since you implemented __call, consider adding a @method annotation. ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-call  annotation

354
                $route->/** @scrutinizer ignore-call */ 
355
                        doMerge($prefix . $route->namePrefix, $routes);
Loading history...
355
356 9
                continue;
357
            }
358
359 78
            if (null === $name = $route->get('name')) {
360 18
                $name = $base = $route->generateRouteName('');
361
362 18
                while ($routes->find($name)) {
363 2
                    $name = $base . '_' . ++$defaultUnnamedIndex;
364
                }
365
            }
366
367 78
            $routes->routes[] = $route->bind($prefix . $name);
368
        }
369 81
        $this->routes = [];
370
371 81
        return $routes->routes;
372
    }
373
}
374