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

Route::end()   A

Complexity

Conditions 2
Paths 2

Size

Total Lines 7
Code Lines 3

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 0
CRAP Score 6

Importance

Changes 0
Metric Value
cc 2
eloc 3
c 0
b 0
f 0
nc 2
nop 0
dl 0
loc 7
ccs 0
cts 0
cp 0
crap 6
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, MethodNotAllowedException, UriHandlerException};
21
use Psr\Http\Message\{ResponseInterface, ServerRequestInterface, UriInterface};
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
    protected const SUPPORTED_GETTER_METHODS = [
77
        'controller' => 'controller',
78
        'methods' => 'methods',
79
        'schemes' => 'schemes',
80
        'domain' => 'domain',
81
        'name' => 'name',
82
        'path' => 'path',
83
        'patterns' => 'patterns',
84
        'middlewares' => 'middlewares',
85
        'defaults' => 'defaults',
86
    ];
87
88
    /** @var RouteCollection|null */
89
    private $collection = null;
90
91
    /**
92
     * Create a new Route constructor.
93 144
     *
94
     * @param string          $pattern The route pattern
95 144
     * @param string|string[] $methods The route HTTP methods. Multiple methods can be supplied as an array
96 144
     *                                 or string combined with `|`.
97
     * @param mixed           $handler The PHP class, object or callable that returns the response when matched
98 144
     */
99 142
    public function __construct(string $pattern, $methods = self::DEFAULT_METHODS, $handler = null)
100
    {
101 144
        $this->controller = $handler;
102
        $this->path = $this->castRoute($pattern);
103
104
        if (!empty($methods)) {
105
            $this->methods = \array_map('strtoupper', (array) $methods);
106
        }
107
    }
108
109
    /**
110 4
     * @internal This is handled different by router
111
     *
112 4
     * @return self
113
     */
114 4
    public static function __set_state(array $properties)
115
    {
116 4
        $recovered = new self($properties['path'], $properties['methods'], $properties['controller']);
117 4
        unset($properties['path'], $properties['controller'], $properties['methods']);
118
119
        foreach ($properties as $name => $property) {
120 4
            $recovered->{$name} = $property;
121
        }
122
123
        return $recovered;
124
    }
125
126
    /**
127
     * @internal
128
     */
129
    public function __serialize(): array
130
    {
131 102
        return $this->get('all');
132
    }
133 102
134 102
    /**
135
     * @internal
136 102
     *
137 1
     * @param array<string,mixed>
138
     */
139
    public function __unserialize(array $data): void
140 101
    {
141
        foreach ($data as $parameter => $value) {
142
            $this->{$parameter} = $value;
143
        }
144
    }
145
146
    /**
147
     * @param string  $method
148
     * @param mixed[] $arguments
149
     *
150 15
     * @throws \BadMethodCallException
151
     *
152 15
     * @return mixed
153
     */
154 15
    public function __call($method, $arguments)
155
    {
156
        $routeMethod = (string) \preg_replace('/^get([A-Z]{1}[a-z]+)$/', '\1', $method, 1);
157
        $routeMethod = \strtolower($routeMethod);
158
159
        if (!empty($arguments)) {
160
            throw new \BadMethodCallException(\sprintf('Arguments passed into "%s" method not supported, as method does not exist.', $routeMethod));
161
        }
162
163
        return $this->get($routeMethod);
164
    }
165
166
    /**
167
     * Invoke the response from route handler.
168
     *
169
     * @param null|callable(mixed,array) $handlerResolver
170
     *
171
     * @return ResponseInterface|string
172
     */
173
    public function __invoke(ServerRequestInterface $request, ?callable $handlerResolver = null)
174
    {
175
        $handler = $this->controller;
176
177
        if ($handler instanceof ResponseInterface) {
178 111
            return $handler;
179
        }
180 111
181
        if ($handler instanceof RequestHandlerInterface) {
182 111
            return $handler->handle($request);
183
        }
184
185
        if ($handler instanceof Handlers\ResourceHandler) {
186
            $handler = $handler(\strtolower($request->getMethod()));
187
        }
188
189
        $handler = $this->castHandler($request, $handlerResolver, $handler);
190
191
        if (!$handler) {
192 2
            throw new InvalidControllerException(
193
                \sprintf('Response type of "%s" for route "%s" is not allowed in PSR7 response body stream.', \get_debug_type($handler), $this->name)
194 2
            );
195
        }
196 2
197
        return $handler;
198
    }
199
200
    /**
201
     * Sets the route path prefix.
202
     */
203
    public function prefix(string $path): self
204
    {
205
        $this->path = $this->castPrefix($this->path, $path);
206
207 12
        return $this;
208
    }
209 12
210
    /**
211 12
     * Sets the route path pattern.
212
     */
213
    public function path(string $pattern): self
214
    {
215
        $this->path = $this->castRoute($pattern);
216
217
        return $this;
218
    }
219
220
    /**
221
     * Sets the route name.
222
     */
223
    public function bind(string $routeName): self
224
    {
225
        $this->name = $routeName;
226
227
        return $this;
228
    }
229
230
    /**
231
     * Sets the route code that should be executed when matched.
232
     *
233
     * @param mixed $to PHP class, object or callable that returns the response when matched
234
     */
235
    public function run($to): self
236
    {
237
        $this->controller = $to;
238 49
239
        return $this;
240 49
    }
241
242 49
    /**
243
     * Sets the missing namespace on route's handler.
244
     *
245
     * @throws InvalidControllerException if $namespace is invalid
246
     */
247
    public function namespace(string $namespace): self
248
    {
249
        if ('' !== $namespace) {
250
            if ('\\' === $namespace[-1]) {
251
                throw new InvalidControllerException(\sprintf('Namespace "%s" provided for routes must not end with a "\\".', $namespace));
252
            }
253
254
            $this->controller = $this->castNamespace($namespace, $this->controller);
255
        }
256
257
        return $this;
258
    }
259
260
    /**
261
     * Sets the requirement for a route variable.
262
     *
263
     * @param string          $variable The variable name
264
     * @param string|string[] $regexp   The regexp to apply
265
     */
266
    public function assert(string $variable, $regexp): self
267
    {
268
        $this->patterns[$variable] = $regexp;
269 28
270
        return $this;
271 28
    }
272 25
273 7
    /**
274 22
     * Sets the requirements for a route variable.
275 22
     *
276
     * @param array<string,string|string[]> $regexps The regexps to apply
277
     */
278 25
    public function asserts(array $regexps): self
279
    {
280
        foreach ($regexps as $variable => $regexp) {
281 28
            $this->assert($variable, $regexp);
282
        }
283
284
        return $this;
285
    }
286
287
    /**
288
     * Sets the default value for a route variable.
289
     *
290
     * @param string $variable The variable name
291 49
     * @param mixed  $default  The default value
292
     */
293 49
    public function default(string $variable, $default): self
294 13
    {
295
        $this->defaults[$variable] = $default;
296
297 49
        return $this;
298
    }
299
300
    /**
301
     * Sets the default values for a route variables.
302
     *
303
     * @param array<string,mixed> $values
304
     */
305
    public function defaults(array $values): self
306
    {
307 7
        foreach ($values as $variable => $default) {
308
            $this->default($variable, $default);
309 7
        }
310 7
311
        return $this;
312
    }
313 7
314
    /**
315
     * Sets the parameter value for a route handler.
316
     *
317
     * @param string $variable The parameter name
318
     * @param mixed  $value    The parameter value
319
     */
320
    public function argument(string $variable, $value): self
321
    {
322
        if (\is_numeric($value)) {
323 13
            $value = (int) $value;
324
        } elseif (\is_string($value)) {
325 13
            $value = \rawurldecode($value);
326 12
        }
327
328 12
        $this->defaults['_arguments'][$variable] = $value;
329
330 12
        return $this;
331
    }
332
333
    /**
334
     * Sets the parameter values for a route handler.
335
     *
336 12
     * @param array<int|string> $variables The route handler parameters
337 10
     */
338
    public function arguments(array $variables): self
339
    {
340 12
        foreach ($variables as $variable => $value) {
341 12
            $this->argument($variable, $value);
342
        }
343
344
        return $this;
345 13
    }
346
347
    /**
348
     * Sets the requirement for the HTTP method.
349
     *
350
     * @param string $methods the HTTP method(s) name
351
     */
352
    public function method(string ...$methods): self
353
    {
354
        foreach ($methods as $method) {
355 2
            $this->methods[] = \strtoupper($method);
356
        }
357 2
358 2
        return $this;
359
    }
360
361 2
    /**
362
     * Sets the requirement of host on this Route.
363
     *
364
     * @param string $hosts The host for which this route should be enabled
365
     */
366
    public function domain(string ...$hosts): self
367
    {
368
        foreach ($hosts as $host) {
369
            \preg_match(Route::URL_PATTERN, $host, $matches, \PREG_UNMATCHED_AS_NULL);
370
371 46
            if (isset($matches[1])) {
372
                $this->schemes[$matches[1]] = true;
373
            }
374 46
375 46
            if (isset($matches[2])) {
376 1
                $this->domain[] = $matches[2];
377
            }
378 1
        }
379
380
        return $this;
381 46
    }
382
383
    /**
384 46
     * Sets the requirement of domain scheme on this Route.
385
     *
386
     * @param string ...$schemes
387
     */
388
    public function scheme(string ...$schemes): self
389
    {
390
        foreach ($schemes as $scheme) {
391
            $this->schemes[] = \strtolower($scheme);
392
        }
393
394
        return $this;
395 147
    }
396
397 147
    /**
398 145
     * Sets the middleware(s) to handle before triggering the route handler.
399
     *
400
     * @param MiddlewareInterface ...$middlewares
401 52
     */
402
    public function middleware(MiddlewareInterface ...$middlewares): self
403 4
    {
404 4
        foreach ($middlewares as $middleware) {
405 4
            $this->middlewares[] = $middleware;
406 4
        }
407 4
408 4
        return $this;
409 4
    }
410 4
411 4
    /**
412
     * Asserts methods and host's scheme.
413
     *
414
     * @throws MethodNotAllowedException|UriHandlerException
415 52
     */
416 50
    public function match(string $method, UriInterface $uri): self
417
    {
418
        if (!\in_array($method, $this->methods)) {
419 2
            throw new MethodNotAllowedException($this->methods, $uri->getPath(), $method);
420
        }
421
422
        if (!empty($schemes = $this->schemes)) {
423
            if (\in_array($uri->getScheme(), $schemes, true)) {
424
                return $this;
425 2
            }
426
427 2
            throw new UriHandlerException(\sprintf('Unfortunately current scheme "%s" is not allowed on requested uri [%s].', $uri->getScheme(), $uri->getPath()), 400);
428 2
        }
429
430
        return $this;
431 1
    }
432 1
433
    /**
434 1
     * Get any of (name, path, domain, defaults, schemes, domain, controller, patterns, middlewares).
435
     * And also accepts "all" and "arguments".
436
     *
437 56
     * @throws \BadMethodCallException if $name does not exist as property
438
     *
439 56
     * @return mixed
440
     */
441 56
    public function get(string $name)
442 56
    {
443 56
        if (isset(static::SUPPORTED_GETTER_METHODS[$name])) {
444
            return $this->{$name};
445
        }
446 56
447
        if ('all' === $name) {
448 56
            return \array_map([$this, 'get'], static::SUPPORTED_GETTER_METHODS);
449
        }
450
451
        if ('arguments' === $name) {
452
            return $this->defaults['_arguments'] ?? [];
453
        }
454
455
        throw new \BadMethodCallException(\sprintf('Invalid call for "%s" as method, %s(\'%1$s\') not supported.', $name, __METHOD__));
456
    }
457
458
    /**
459
     * Sets the route belonging to a particular collection.
460
     *
461
     * This method is kinda internal, only used in RouteCollection class,
462
     * and retrieved using this class end method.
463
     *
464
     * @internal used by RouteCollection class
465
     */
466
    public function belong(RouteCollection $to): void
467
    {
468
        $this->collection = $to;
469
    }
470
471
    /**
472
     * End a group stack or return self.
473
     */
474
    public function end(): ?RouteCollection
475
    {
476
        if (null !== $stack = $this->collection) {
477
            $this->collection = null; // Just remove it.
478
        }
479
480
        return $stack;
481
    }
482
483
    public function generateRouteName(string $prefix): string
484
    {
485
        $routeName = \implode('_', $this->methods) . '_' . $prefix . $this->path;
486
        $routeName = \str_replace(['/', ':', '|', '-'], '_', $routeName);
487
        $routeName = (string) \preg_replace('/[^a-z0-9A-Z_.]+/', '', $routeName);
488
489
        return (string) \preg_replace(['/\_+/', '/\.+/'], ['_', '.'], $routeName);
490
    }
491
}
492