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

Route::bind()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 5
Code Lines 2

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 3
CRAP Score 1

Importance

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