Passed
Pull Request — master (#13)
by Divine Niiquaye
02:26
created

RouteCollection::addRoute()   A

Complexity

Conditions 2
Paths 2

Size

Total Lines 11
Code Lines 6

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 7
CRAP Score 2

Importance

Changes 0
Metric Value
eloc 6
c 0
b 0
f 0
dl 0
loc 11
ccs 7
cts 7
cp 1
rs 10
cc 2
nc 2
nop 3
crap 2
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
use Flight\Routing\Exceptions\InvalidControllerException;
21
22
/**
23
 * A RouteCollection represents a set of Route instances.
24
 *
25
 * This class provides all(*) methods for creating path+HTTP method-based routes and
26
 * injecting them into the router:
27
 *
28
 * - head
29
 * - get
30
 * - post
31
 * - put
32
 * - patch
33
 * - delete
34
 * - options
35
 * - any
36
 * - resource
37
 *
38
 * A general `addRoute()` method allows specifying multiple request methods and/or
39
 * arbitrary request methods when creating a path-based route.
40
 *
41
 * __call() forwards method-calls to Route, but returns instance of RouteCollection
42
 * listing Route's methods below, so that IDEs know they are valid
43
 *
44
 * @method RouteCollection withAssert(string $variable, string $regexp)
45
 * @method RouteCollection withDefault(string $variable, mixed $default)
46
 * @method RouteCollection withArgument($variable, mixed $value)
47
 * @method RouteCollection withMethod(string ...$methods)
48
 * @method RouteCollection withScheme(string ...$schemes)
49
 * @method RouteCollection withMiddleware(mixed ...$middlewares)
50
 * @method RouteCollection withDomain(string ...$hosts)
51
 * @method RouteCollection withPrefix(string $path)
52
 *
53
 * @author Fabien Potencier <[email protected]>
54
 * @author Tobias Schultze <http://tobion.de>
55
 * @author Divine Niiquaye Ibok <[email protected]>
56
 */
57
final class RouteCollection extends \ArrayIterator
58
{
59
    /** @var null|string */
60
    private $namePrefix;
61
62
    /** @var Route */
63
    private $defaultRoute;
64
65
    /**
66
     * @param null|Route $defaultRoute
67
     * @param mixed      $defaultHandler
68
     */
69 111
    public function __construct(?Route $defaultRoute = null, $defaultHandler = null)
70
    {
71 111
        $this->defaultRoute = $defaultRoute ?? new Route('/', '', $defaultHandler);
72 111
        parent::__construct();
73 111
    }
74
75
    /**
76
     * @param string   $method
77
     * @param string[] $arguments
78
     *
79
     * @return mixed
80
     */
81 9
    public function __call($method, $arguments)
82
    {
83 9
        $routeMethod = \strtolower((string) \preg_replace('~^with([A-Z]{1}[a-z]+)$~', '\1', $method, 1));
84
85 9
        if (!\method_exists($this->defaultRoute, $routeMethod)) {
86 1
            throw new \BadMethodCallException(
87 1
                \sprintf(
88 1
                    'Method "%s::%s" does not exist. %2$s method should have a \'with\' prefix',
89 1
                    Route::class,
90 1
                    $method
91
                )
92
            );
93
        }
94
95 8
        \call_user_func_array([$this->defaultRoute, $routeMethod], $arguments);
96
97 8
        foreach ($this as $route) {
98 8
            \call_user_func_array([$route, $routeMethod], $arguments);
99
        }
100
101 8
        return $this;
102
    }
103
104
    /**
105
     * Gets the current RouteCollection as an array that includes all routes.
106
     *
107
     * Use this method to fetch routes instead of getIterator().
108
     *
109
     * @return Route[] The filtered merged routes
110
     */
111 83
    public function getRoutes(): array
112
    {
113 83
        return $this->doMerge('', new static());
114
    }
115
116
    /**
117
     * Add route(s) to the collection
118
     *
119
     * @param Route ...$routes
120
     *
121
     * @return self
122
     */
123 71
    public function add(Route ...$routes): self
124
    {
125 71
        foreach ($routes as $route) {
126 71
            $default = clone $this->defaultRoute;
127
128 71
            if (null === $route->getController()) {
129 9
                $route->run($default->getController());
130
            }
131
132 71
            $this[]  = $default::__set_state($route->getAll());
133
        }
134
135 71
        return $this;
136
    }
137
138
    /**
139
     * Maps a pattern to a handler.
140
     *
141
     * You can must specify HTTP methods that should be matched.
142
     *
143
     * @param string   $pattern Matched route pattern
144
     * @param string[] $methods Matched HTTP methods
145
     * @param mixed    $handler Handler that returns the response when matched
146
     *
147
     * @return Route
148
     */
149 22
    public function addRoute(string $pattern, array $methods, $handler = null): Route
150
    {
151 22
        $route      = clone $this->defaultRoute;
152 22
        $controller = null === $handler ? $route->getController() : $handler;
153
154 22
        $route->path($pattern)->method(...$methods);
155
156 22
        $this[] = $route;
157 22
        $route->run($controller);
158
159 22
        return $route;
160
    }
161
162
    /**
163
     * Mounts controllers under the given route prefix.
164
     *
165
     * @param string                   $name        The route group prefixed name
166
     * @param callable|RouteCollection $controllers A RouteCollection instance or a callable for defining routes
167
     *
168
     * @throws \LogicException
169
     */
170 10
    public function group(string $name, $controllers): self
171
    {
172 10
        if (\is_callable($controllers)) {
173 4
            $collection = new static();
174 4
            \call_user_func($controllers, $collection);
0 ignored issues
show
Bug introduced by
It seems like $controllers can also be of type Flight\Routing\RouteCollection; however, parameter $callback of call_user_func() does only seem to accept callable, 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

174
            \call_user_func(/** @scrutinizer ignore-type */ $controllers, $collection);
Loading history...
175 4
            $controllers = clone $collection;
176 6
        } elseif (!$controllers instanceof self) {
177 1
            throw new \LogicException('The "mount" method takes either a "RouteCollection" instance or callable.');
178
        }
179
180 9
        $controllers->namePrefix = $name;
181
182 9
        $this[] = $controllers;
183
184 9
        return $controllers;
185
    }
186
187
    /**
188
     * Maps a HEAD request to a handler.
189
     *
190
     * @param string $pattern Matched route pattern
191
     * @param mixed  $handler Handler that returns the response when matched
192
     *
193
     * @return Route
194
     */
195 5
    public function head(string $pattern, $handler = null): Route
196
    {
197 5
        return $this->addRoute($pattern, [Router::METHOD_HEAD], $handler);
198
    }
199
200
    /**
201
     * Maps a GET and HEAD request to a handler.
202
     *
203
     * @param string $pattern Matched route pattern
204
     * @param mixed  $handler Handler that returns the response when matched
205
     *
206
     * @return Route
207
     */
208 10
    public function get(string $pattern, $handler = null): Route
209
    {
210 10
        return $this->addRoute($pattern, [Router::METHOD_GET, Router::METHOD_HEAD], $handler);
211
    }
212
213
    /**
214
     * Maps a POST request to a handler.
215
     *
216
     * @param string $pattern Matched route pattern
217
     * @param mixed  $handler Handler that returns the response when matched
218
     *
219
     * @return Route
220
     */
221 2
    public function post(string $pattern, $handler = null): Route
222
    {
223 2
        return $this->addRoute($pattern, [Router::METHOD_POST], $handler);
224
    }
225
226
    /**
227
     * Maps a PUT request to a handler.
228
     *
229
     * @param string $pattern Matched route pattern
230
     * @param mixed  $handler Handler that returns the response when matched
231
     *
232
     * @return Route
233
     */
234 1
    public function put(string $pattern, $handler = null): Route
235
    {
236 1
        return $this->addRoute($pattern, [Router::METHOD_PUT], $handler);
237
    }
238
239
    /**
240
     * Maps a PATCH request to a handler.
241
     *
242
     * @param string $pattern Matched route pattern
243
     * @param mixed  $handler Handler that returns the response when matched
244
     *
245
     * @return Route
246
     */
247 2
    public function patch(string $pattern, $handler = null): Route
248
    {
249 2
        return $this->addRoute($pattern, [Router::METHOD_PATCH], $handler);
250
    }
251
252
    /**
253
     * Maps a DELETE request to a handler.
254
     *
255
     * @param string $pattern Matched route pattern
256
     * @param mixed  $handler Handler that returns the response when matched
257
     *
258
     * @return Route
259
     */
260 1
    public function delete(string $pattern, $handler = null): Route
261
    {
262 1
        return $this->addRoute($pattern, [Router::METHOD_DELETE], $handler);
263
    }
264
265
    /**
266
     * Maps a OPTIONS request to a handler.
267
     *
268
     * @param string $pattern Matched route pattern
269
     * @param mixed  $handler Handler that returns the response when matched
270
     *
271
     * @return Route
272
     */
273 1
    public function options(string $pattern, $handler = null): Route
274
    {
275 1
        return $this->addRoute($pattern, [Router::METHOD_OPTIONS], $handler);
276
    }
277
278
    /**
279
     * Maps any request to a handler.
280
     *
281
     * @param string $pattern Matched route pattern
282
     * @param mixed  $handler Handler that returns the response when matched
283
     *
284
     * @return Route
285
     */
286 2
    public function any(string $pattern, $handler = null): Route
287
    {
288 2
        return $this->addRoute($pattern, Router::HTTP_METHODS_STANDARD, $handler);
289
    }
290
291
    /**
292
     * Maps any request to a resource handler and prefix class method by request method.
293
     * If you request on "/account" path with a GET method, prefixed by the name
294
     * parameter eg: 'user', class method will match `getUser`.
295
     *
296
     * @param string              $name     The prefixed name attached to request method
297
     * @param string              $pattern  matched path where request should be sent to
298
     * @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...
299
     *
300
     * @return Route
301
     */
302 2
    public function resource(string $name, string $pattern, $resource): Route
303
    {
304 2
        if (!\is_object($resource) || (\is_string($resource) && \class_exists($resource))) {
305 1
            throw new InvalidControllerException(
306 1
                'Resource handler type should be a class string or class object, but not a callable or array'
307
            );
308
        }
309
310 1
        $route = $this->any($pattern, [$resource, $name]);
311
312 1
        return $route->bind($route->generateRouteName($name) . '__restful');
313
    }
314
315
    /**
316
     * Find a route by name.
317
     *
318
     * @param string $name The route name
319
     *
320
     * @return null|Route A Route instance or null when not found
321
     */
322 73
    public function find(string $name): ?Route
323
    {
324 73
        foreach ($this as $route) {
325 34
            if ($name === $route->getName()) {
326 7
                return $route;
327
            }
328
        }
329
330 73
        return null;
331
    }
332
333
    /**
334
     * @param string $prefix
335
     * @param self   $routes
336
     *
337
     * @return Route[]
338
     */
339 83
    private function doMerge(string $prefix, self $routes): array
340
    {
341
        /** @var Route|RouteCollection $route */
342 83
        foreach ($this as $route) {
343 74
            if ($route instanceof Route) {
344 74
                if (null === $name = $route->getName()) {
345 14
                    $name = $base = $route->generateRouteName('');
346 14
                    $i    = 0;
347
348 14
                    while ($routes->find($name)) {
349 2
                        $name = $base . '_' . ++$i;
350
                    }
351
                }
352
353 74
                $routes[] = $route->bind($prefix . $name);
354
            } else {
355 9
                $route->doMerge($prefix . $route->namePrefix, $routes);
356
            }
357
        }
358
359 83
        return $routes->getArrayCopy();
360
    }
361
}
362