Test Failed
Pull Request — master (#13)
by Divine Niiquaye
14:05
created

RouteCollection::add()   A

Complexity

Conditions 3
Paths 3

Size

Total Lines 13
Code Lines 6

Duplication

Lines 0
Ratio 0 %

Importance

Changes 2
Bugs 0 Features 0
Metric Value
eloc 6
c 2
b 0
f 0
dl 0
loc 13
rs 10
cc 3
nc 3
nop 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
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 Route|null $defaultRoute
67
     * @param mixed $defaultHandler
68
     */
69
    public function __construct(?Route $defaultRoute = null, $defaultHandler = null)
70
    {
71
        $this->defaultRoute = $defaultRoute ?? new Route('/', '', $defaultHandler);
72
    }
73
74
    public function __call($method, $arguments)
75
    {
76
        $routeMethod = \strtolower(\preg_replace('~^with([A-Z]{1}[a-z]+)$~', '\1', $method, 1));
77
78
        if (!\method_exists($this->defaultRoute, $routeMethod)) {
79
            throw new \BadMethodCallException(
80
                \sprintf(
81
                    'Method "%s::%s" does not exist. %2$s method should have a \'with\' prefix',
82
                    Route::class,
83
                    $method
84
                )
85
            );
86
        }
87
88
        \call_user_func_array([$this->defaultRoute, $routeMethod], $arguments);
89
90
        foreach ($this as $route) {
91
            \call_user_func_array([$route, $routeMethod], $arguments);
92
        }
93
94
        return $this;
95
    }
96
97
    /**
98
     * Gets the current RouteCollection as an array that includes all routes.
99
     *
100
     * Use this method to fetch routes instead of getIterator().
101
     *
102
     * @return Route[] The filtered merged routes
103
     */
104
    public function getRoutes(): array
105
    {
106
        return $this->doMerge('', new static());
107
    }
108
109
    /**
110
     * Add route(s) to the collection
111
     *
112
     * @param Route ...$routes
113
     *
114
     * @return self
115
     */
116
    public function add(Route ...$routes): self
117
    {
118
        foreach ($routes as $route) {
119
            $default = clone $this->defaultRoute;
120
121
            if (null === $route->getController()) {
122
                $route->run($default->getController());
123
            }
124
125
            $this[]  = $default::__set_state($route->getAll());
126
        }
127
128
        return $this;
129
    }
130
131
    /**
132
     * Maps a pattern to a handler.
133
     *
134
     * You can must specify HTTP methods that should be matched.
135
     *
136
     * @param string   $pattern Matched route pattern
137
     * @param string[] $methods Matched HTTP methods
138
     * @param mixed    $handler Handler that returns the response when matched
139
     *
140
     * @return Route
141
     */
142
    public function addRoute(string $pattern, array $methods, $handler = null): Route
143
    {
144
        $route      = clone $this->defaultRoute;
145
        $controller = null === $handler ? $route->getController() : $handler;
146
        
147
        $route->path($pattern)->method(...$methods);
148
149
        $this[] = $route;
150
        $route->run($controller);
151
152
        return $route;
153
    }
154
155
    /**
156
     * Mounts controllers under the given route prefix.
157
     *
158
     * @param string                   $named       the route group prefixed name
159
     * @param callable|RouteCollection $controllers A RouteCollection instance or a callable for defining routes
160
     *
161
     * @throws LogicException
162
     */
163
    public function group(string $name, $controllers): self
164
    {
165
        if (\is_callable($controllers)) {
166
            $collection = new static();
167
            \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

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