Test Failed
Pull Request — master (#16)
by Divine Niiquaye
02:47
created

Route::__construct()   A

Complexity

Conditions 2
Paths 2

Size

Total Lines 7
Code Lines 4

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 1
CRAP Score 2

Importance

Changes 3
Bugs 0 Features 0
Metric Value
cc 2
eloc 4
c 3
b 0
f 0
nc 2
nop 3
dl 0
loc 7
ccs 1
cts 1
cp 1
crap 2
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.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
use Psr\Http\Message\{ResponseInterface, ServerRequestInterface};
22
use Psr\Http\Server\{MiddlewareInterface, RequestHandlerInterface};
23
24
/**
25
 * Value object representing a single route.
26
 *
27
 * @method string getPath() Gets the route path.
28
 * @method string|null getName() Gets the route name.
29
 * @method string[] getMethods() Gets the route methods.
30
 * @method string[] getSchemes() Gets the route domain schemes.
31
 * @method string[] getDomain() Gets the route host.
32
 * @method mixed getController() Gets the route handler.
33
 * @method array getMiddlewares() Gets the route middlewares.
34
 * @method array getPatterns() Gets the route pattern placeholder assert.
35
 * @method array getDefaults() Gets the route default settings.
36
 * @method array getArguments() Gets the arguments passed to route handler as parameters.
37
 * @method array getAll() Gets all the routes properties.
38
 *
39
 * @author Divine Niiquaye Ibok <[email protected]>
40
 */
41
class Route
42
{
43
    use Traits\CastingTrait;
44
45
    /**
46
     * A Pattern to Locates appropriate route by name, support dynamic route allocation using following pattern:
47
     * Pattern route:   `pattern/*<controller@action>`
48
     * Default route: `*<controller@action>`
49
     * Only action:   `pattern/*<action>`.
50
     *
51
     * @var string
52
     */
53
    public const RCA_PATTERN = '#^(?:([a-z]+)\:)?(?:\/{2}([^\/]+))?([^*]+?)(?:\*\<(?:([\w\\\\]+)\@)?(\w+)\>)?$#u';
54
55
    /**
56
     * A Pattern to match protocol, host and port from a url.
57
     *
58
     * Examples of urls that can be matched: http://en.example.domain, {sub}.example.domain, https://example.com:34, example.com, etc.
59
     *
60
     * @var string
61
     */
62
    public const URL_PATTERN = '#^(?:([a-z]+)\:\/{2})?([^\/]+)?$#u';
63
64
    /**
65
     * Slashes supported on browser when used.
66
     *
67
     * @var string[]
68
     */
69
    public const URL_PREFIX_SLASHES = ['/' => '/', ':' => ':', '-' => '-', '_' => '_', '~' => '~', '@' => '@'];
70
71
    /**
72
     * Default methods for route.
73
     */
74
    public const DEFAULT_METHODS = [Router::METHOD_GET, Router::METHOD_HEAD];
75
76
    /** @var RouteCollection|null */
77
    private $collection = null;
78
79
    /**
80
     * Create a new Route constructor.
81
     *
82
     * @param string          $pattern The route pattern
83
     * @param string|string[] $methods The route HTTP methods. Multiple methods can be supplied as an array
84
     *                                 or string combined with `|`.
85
     * @param mixed           $handler The PHP class, object or callable that returns the response when matched
86
     */
87
    public function __construct(string $pattern, $methods = self::DEFAULT_METHODS, $handler = null)
88
    {
89
        $this->controller = $handler;
90
        $this->path = $this->castRoute($pattern);
91
92
        if (!empty($methods)) {
93 144
            $this->methods = \array_map('strtoupper', (array) $methods);
94
        }
95 144
    }
96 144
97
    /**
98 144
     * @internal This is handled different by router
99 142
     *
100
     * @return self
101 144
     */
102
    public static function __set_state(array $properties)
103
    {
104
        $recovered = new self($properties['path'], $properties['methods'], $properties['controller']);
105
        unset($properties['path'], $properties['controller'], $properties['methods']);
106
107
        foreach ($properties as $name => $property) {
108
            $recovered->{$name} = $property;
109
        }
110 4
111
        return $recovered;
112 4
    }
113
114 4
    /**
115
     * @param string  $method
116 4
     * @param mixed[] $arguments
117 4
     *
118
     * @throws \BadMethodCallException
119
     *
120 4
     * @return mixed
121
     */
122
    public function __call($method, $arguments)
123
    {
124
        $routeMethod = (string) \preg_replace('/^get([A-Z]{1}[a-z]+)$/', '\1', $method, 1);
125
        $routeMethod = \strtolower($routeMethod);
126
127
        if (!empty($arguments)) {
128
            throw new \BadMethodCallException(\sprintf('Arguments passed into "%s" method not supported, as method does not exist.', $routeMethod));
129
        }
130
131 102
        return $this->get($routeMethod);
132
    }
133 102
134 102
    /**
135
     * Invoke the response from route handler.
136 102
     *
137 1
     * @param null|callable(mixed,array) $handlerResolver
138
     *
139
     * @return ResponseInterface|string
140 101
     */
141
    public function __invoke(ServerRequestInterface $request, ?callable $handlerResolver = null)
142
    {
143
        $handler = $this->controller;
144
145
        if ($handler instanceof ResponseInterface) {
146
            return $handler;
147
        }
148
149
        if ($handler instanceof RequestHandlerInterface) {
150 15
            return $handler->handle($request);
151
        }
152 15
153
        if ($handler instanceof Handlers\ResourceHandler) {
154 15
            $handler = $handler(\strtolower($request->getMethod()));
155
        }
156
157
        $handler = $this->castHandler($request, $handlerResolver, $handler);
158
159
        if (!$handler) {
160
            throw new InvalidControllerException(
161
                \sprintf('Response type of "%s" for route "%s" is not allowed in PSR7 response body stream.', \get_debug_type($handler), $this->name)
162
            );
163
        }
164
165
        return $handler;
166
    }
167
168
    /**
169
     * Sets the route path prefix.
170
     */
171
    public function prefix(string $path): self
172
    {
173
        $this->path = $this->castPrefix($this->path, $path);
174
175
        return $this;
176
    }
177
178 111
    /**
179
     * Sets the route path pattern.
180 111
     */
181
    public function path(string $pattern): self
182 111
    {
183
        $this->path = $this->castRoute($pattern);
184
185
        return $this;
186
    }
187
188
    /**
189
     * Sets the route name.
190
     */
191
    public function bind(string $routeName): self
192 2
    {
193
        $this->name = $routeName;
194 2
195
        return $this;
196 2
    }
197
198
    /**
199
     * Sets the route code that should be executed when matched.
200
     *
201
     * @param mixed $to PHP class, object or callable that returns the response when matched
202
     */
203
    public function run($to): self
204
    {
205
        $this->controller = $to;
206
207 12
        return $this;
208
    }
209 12
210
    /**
211 12
     * Sets the missing namespace on route's handler.
212
     *
213
     * @throws InvalidControllerException if $namespace is invalid
214
     */
215
    public function namespace(string $namespace): self
216
    {
217
        if ('' !== $namespace) {
218
            if ('\\' === $namespace[-1]) {
219
                throw new InvalidControllerException(\sprintf('Namespace "%s" provided for routes must not end with a "\\".', $namespace));
220
            }
221
222
            $this->controller = $this->castNamespace($namespace, $this->controller);
223
        }
224
225
        return $this;
226
    }
227
228
    /**
229
     * Sets the requirement for a route variable.
230
     *
231
     * @param string          $variable The variable name
232
     * @param string|string[] $regexp   The regexp to apply
233
     */
234
    public function assert(string $variable, $regexp): self
235
    {
236
        $this->patterns[$variable] = $regexp;
237
238 49
        return $this;
239
    }
240 49
241
    /**
242 49
     * Sets the requirements for a route variable.
243
     *
244
     * @param array<string,string|string[]> $regexps The regexps to apply
245
     */
246
    public function asserts(array $regexps): self
247
    {
248
        foreach ($regexps as $variable => $regexp) {
249
            $this->assert($variable, $regexp);
250
        }
251
252
        return $this;
253
    }
254
255
    /**
256
     * Sets the default value for a route variable.
257
     *
258
     * @param string $variable The variable name
259
     * @param mixed  $default  The default value
260
     */
261
    public function default(string $variable, $default): self
262
    {
263
        $this->defaults[$variable] = $default;
264
265
        return $this;
266
    }
267
268
    /**
269 28
     * Sets the default values for a route variables.
270
     *
271 28
     * @param array<string,mixed> $values
272 25
     */
273 7
    public function defaults(array $values): self
274 22
    {
275 22
        foreach ($values as $variable => $default) {
276
            $this->default($variable, $default);
277
        }
278 25
279
        return $this;
280
    }
281 28
282
    /**
283
     * Sets the parameter value for a route handler.
284
     *
285
     * @param string $variable The parameter name
286
     * @param mixed  $value    The parameter value
287
     */
288
    public function argument(string $variable, $value): self
289
    {
290
        if (\is_numeric($value)) {
291 49
            $value = (int) $value;
292
        } elseif (\is_string($value)) {
293 49
            $value = \rawurldecode($value);
294 13
        }
295
296
        $this->defaults['_arguments'][$variable] = $value;
297 49
298
        return $this;
299
    }
300
301
    /**
302
     * Sets the parameter values for a route handler.
303
     *
304
     * @param array<int|string> $variables The route handler parameters
305
     */
306
    public function arguments(array $variables): self
307 7
    {
308
        foreach ($variables as $variable => $value) {
309 7
            $this->argument($variable, $value);
310 7
        }
311
312
        return $this;
313 7
    }
314
315
    /**
316
     * Sets the requirement for the HTTP method.
317
     *
318
     * @param string $methods the HTTP method(s) name
319
     */
320
    public function method(string ...$methods): self
321
    {
322
        foreach ($methods as $method) {
323 13
            $this->methods[] = \strtoupper($method);
324
        }
325 13
326 12
        return $this;
327
    }
328 12
329
    /**
330 12
     * Sets the requirement of host on this Route.
331
     *
332
     * @param string $hosts The host for which this route should be enabled
333
     */
334
    public function domain(string ...$hosts): self
335
    {
336 12
        foreach ($hosts as $host) {
337 10
            \preg_match(Route::URL_PATTERN, $host, $matches, \PREG_UNMATCHED_AS_NULL);
338
339
            if (isset($matches[1])) {
340 12
                $this->schemes[$matches[1]] = true;
341 12
            }
342
343
            if (isset($matches[2])) {
344
                $this->domain[] = $matches[2];
345 13
            }
346
        }
347
348
        return $this;
349
    }
350
351
    /**
352
     * Sets the requirement of domain scheme on this Route.
353
     *
354
     * @param string ...$schemes
355 2
     */
356
    public function scheme(string ...$schemes): self
357 2
    {
358 2
        foreach ($schemes as $scheme) {
359
            $this->schemes[\strtolower($scheme)] = true;
360
        }
361 2
362
        return $this;
363
    }
364
365
    /**
366
     * Sets the middleware(s) to handle before triggering the route handler.
367
     *
368
     * @param MiddlewareInterface ...$middlewares
369
     */
370
    public function middleware(MiddlewareInterface ...$middlewares): self
371 46
    {
372
        foreach ($middlewares as $middleware) {
373
            $this->middlewares[] = $middleware;
374 46
        }
375 46
376 1
        return $this;
377
    }
378 1
379
    /**
380
     * Get any of (name, path, domain, defaults, schemes, domain, controller, patterns, middlewares).
381 46
     * And also accepts "all" and "arguments".
382
     *
383
     * @throws \BadMethodCallException if $name does not exist as property
384 46
     *
385
     * @return mixed
386
     */
387
    public function get(string $name)
388
    {
389
        if (\property_exists(__CLASS__, $name)) {
390
            return $this->{$name};
391
        }
392
393
        if ('all' === $name) {
394
            return [
395 147
                'controller' => $this->controller,
396
                'methods' => $this->methods,
397 147
                'schemes' => $this->schemes,
398 145
                'domain' => $this->domain,
399
                'name' => $this->name,
400
                'path' => $this->path,
401 52
                'patterns' => $this->patterns,
402
                'middlewares' => $this->middlewares,
403 4
                'defaults' => $this->defaults,
404 4
            ];
405 4
        }
406 4
407 4
        if ('arguments' === $name) {
408 4
            return $this->defaults['_arguments'] ?? [];
409 4
        }
410 4
411 4
        throw new \BadMethodCallException(\sprintf('Invalid call for "%s" as method, %s(\'%1$s\') not supported.', $name, __METHOD__));
412
    }
413
414
    /**
415 52
     * End a group stack or return self.
416 50
     */
417
    public function end(RouteCollection $collection = null): ?RouteCollection
418
    {
419 2
        if (null !== $collection) {
420
            return $this->collection = $collection;
421
        }
422
423
        $stack = $this->collection;
424
        $this->collection = null; // Just remove it.
425 2
426
        return $stack ?? $collection;
427 2
    }
428 2
429
    public function generateRouteName(string $prefix): string
430
    {
431 1
        $routeName = \implode('_', $this->methods) . '_' . $prefix . $this->path;
432 1
        $routeName = \str_replace(['/', ':', '|', '-'], '_', $routeName);
433
        $routeName = (string) \preg_replace('/[^a-z0-9A-Z_.]+/', '', $routeName);
434 1
435
        return (string) \preg_replace(['/\_+/', '/\.+/'], ['_', '.'], $routeName);
436
    }
437
}
438