Test Failed
Push — master ( d3660e...c7a4a9 )
by Divine Niiquaye
10:08
created

Route::__call()   B

Complexity

Conditions 8
Paths 5

Size

Total Lines 31
Code Lines 17

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 0
CRAP Score 72

Importance

Changes 1
Bugs 0 Features 0
Metric Value
eloc 17
c 1
b 0
f 0
dl 0
loc 31
rs 8.4444
ccs 0
cts 0
cp 0
cc 8
nc 5
nop 2
crap 72
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
 * @method Route asserts(array $patterns) Add an array of route named patterns.
44
 * @method Route defaults(array $values) Add an array of default values.
45
 * @method Route arguments(array $properties) Add an array of handler's arguments.
46
 *
47
 * @author Divine Niiquaye Ibok <[email protected]>
48
 */
49
class Route
50
{
51
    use Traits\CastingTrait;
52
53
    /**
54
     * A Pattern to Locates appropriate route by name, support dynamic route allocation using following pattern:
55
     * Pattern route:   `pattern/*<controller@action>`
56
     * Default route: `*<controller@action>`
57
     * Only action:   `pattern/*<action>`.
58
     *
59
     * @var string
60
     */
61
    public const RCA_PATTERN = '/^(?P<route>.*?)?(?P<handler>\*\<(?:(?<c>[a-zA-Z0-9\\\\]+)\@)?(?<a>[a-zA-Z0-9_\-]+)\>)?$/u';
62
63
    /**
64
     * A Pattern to match protocol, host and port from a url
65
     *
66
     * Examples of urls that can be matched:
67
     * http://en.example.domain
68
     * //example.domain
69
     * //example.com
70
     * https://example.com:34
71
     * //example.com
72
     * example.com
73
     * localhost:8000
74
     * {foo}.domain.com
75
     *
76
     * Also supports api resource routing, eg: api://user/path
77
     *
78
     * @var string
79 151
     */
80
    public const URL_PATTERN = '/^(?:(?P<scheme>api|https?)\:)?(\/\/)?(?P<host>[^\/\*]+)\/*?$/u';
81 151
82 151
    /**
83 151
     * Create a new Route constructor.
84 151
     *
85 150
     * @param string $pattern The route pattern
86
     * @param string $methods The route HTTP methods. Multiple methods can be supplied,
87
     *                        delimited by a pipe character '|', eg. 'GET|POST'
88
     * @param mixed  $handler The PHP class, object or callable that returns the response when matched
89
     */
90
    public function __construct(string $pattern, string $methods = 'GET|HEAD', $handler = null)
91
    {
92 1
        $this->controller = $handler;
93
        $this->path       = $this->castRoute($pattern);
94 1
95
        if (!empty($methods)) {
96 1
            $this->method(...\explode('|', $methods));
97 1
        }
98 1
    }
99 1
100 1
    /**
101 1
     * @internal This is handled different by router
102 1
     *
103
     * @param array $properties
104 1
     *
105
     * @return self
106
     */
107
    public static function __set_state(array $properties)
108
    {
109
        $recovered = new self($properties['path'], '', $properties['controller']);
110
111
        unset($properties['path'], $properties['controller']);
112
113
        foreach ($properties as $name => $property) {
114
            $recovered->{$name} = $property;
115
        }
116
117
        return $recovered;
118
    }
119
120
    /**
121
     * @param string   $method
122
     * @param mixed[] $arguments
123
     *
124
     * @return mixed
125
     */
126
    public function __call($method, $arguments)
127
    {
128
        $routeMethod = (string) \preg_replace('/^(default|assert)(s)|get([A-Z]{1}[a-z]+)$/', '\1\3', $method, 1);
129
130
        if (\in_array($routeMethod = \strtolower($routeMethod), ['all', 'arguments'], true)) {
131
            return $this->get($routeMethod);
132
        }
133
134
        if (!\property_exists($this, $routeMethod)) {
135
            if (method_exists($this, $routeMethod) || 'arguments' === $method) {
136
                $arguments = (array) \current($arguments) ?: [];
137
138
                foreach ($arguments as $variable => $value) {
139
                    $this->{$routeMethod}($variable, $value);
140
                }
141
142
                return $this;
143
            }
144
145
            throw new \BadMethodCallException(
146
                \sprintf(
147
                    'Method "%s->%s" does not exist. should be one of [%s], all, or arguments. ' .
148
                    '%2$s method should start with a \'get\' prefix.',
149
                    Route::class,
150
                    $routeMethod ?: $method,
151
                    \join(', ', \array_keys($this->get('all')))
152
                )
153
            );
154
        }
155
156
        return $this->get($routeMethod);
157
    }
158
159
    /**
160
     * Sets the route path prefix.
161
     *
162
     * @param string $path
163
     *
164
     * @return Route $this The current Route instance
165
     */
166
    public function prefix(string $path): self
167
    {
168
        $this->path = $this->castPrefix($this->path, $path);
169
170
        return $this;
171
    }
172
173
    /**
174
     * Sets the route path pattern.
175
     *
176
     * @param string $pattern
177
     *
178
     * @return Route $this The current Route instance
179
     */
180
    public function path(string $pattern): self
181
    {
182
        $this->path = $this->castRoute($pattern);
183
184
        return $this;
185
    }
186
187
    /**
188
     * Sets the route name.
189
     *
190
     * @param string $routeName
191
     *
192
     * @return Route $this The current Route instance
193
     */
194
    public function bind(string $routeName): self
195
    {
196
        $this->name = $routeName;
197
198
        return $this;
199
    }
200
201
    /**
202
     * Sets the route code that should be executed when matched.
203
     *
204
     * @param mixed $to PHP class, object or callable that returns the response when matched
205
     *
206
     * @return Route $this The current Route instance
207
     */
208
    public function run($to): self
209
    {
210
        $this->controller = $to;
211
212
        return $this;
213
    }
214
215
    /**
216
     * Sets the requirement for a route variable.
217
     *
218
     * @param string          $variable The variable name
219
     * @param string|string[] $regexp   The regexp to apply
220
     *
221
     * @return Route $this The current route instance
222
     */
223
    public function assert(string $variable, $regexp): self
224
    {
225
        $this->patterns[$variable] = $regexp;
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
    public function default(string $variable, $default): self
239
    {
240
        $this->defaults[$variable] = $default;
241
242
        return $this;
243
    }
244
245
    /**
246
     * Sets the parameter value for a route handler.
247
     *
248
     * @param int|string $variable The parameter name
249
     * @param mixed      $value    The parameter value
250
     *
251
     * @return Route $this The current Route instance
252
     */
253
    public function argument($variable, $value): self
254
    {
255
        if (!\is_int($variable)) {
256
            if (\is_numeric($value)) {
257
                $value = (int) $value;
258
            } elseif (\is_string($value)) {
259
                $value = \rawurldecode($value);
260
            }
261
262
            $this->defaults['_arguments'][$variable] = $value;
263
        }
264
265
        return $this;
266
    }
267
268
    /**
269
     * Sets the requirement for the HTTP method.
270
     *
271
     * @param string $methods the HTTP method(s) name
272
     *
273
     * @return Route $this The current Route instance
274
     */
275
    public function method(string ...$methods): self
276
    {
277
        foreach ($methods as $method) {
278
            $this->methods[\strtoupper($method)] = true;
279
        }
280
281
        return $this;
282
    }
283
284
    /**
285
     * Sets the requirement of host on this Route.
286
     *
287
     * @param string $hosts The host for which this route should be enabled
288
     *
289
     * @return Route $this The current Route instance
290
     */
291
    public function domain(string ...$hosts): self
292
    {
293
        foreach ($hosts as $host) {
294
            \preg_match(Route::URL_PATTERN, $host, $matches);
295
296
            $scheme = $matches['scheme'] ?? null;
297
298
            if ('api' === $scheme && isset($matches['host'])) {
299
                $this->defaults['_api'] = \ucfirst($matches['host']);
300
301
                continue;
302
            }
303
304
            if (!empty($scheme)) {
305
                $this->schemes[$scheme] = true;
306
            }
307
308
            if (!empty($matches['host'])) {
309
                $this->domain[$matches['host']] = true;
310
            }
311
        }
312
313
        return $this;
314
    }
315
316
    /**
317
     * Sets the requirement of domain scheme on this Route.
318
     *
319
     * @param string ...$schemes
320
     *
321
     * @return Route $this The current Route instance
322
     */
323
    public function scheme(string ...$schemes): self
324
    {
325
        foreach ($schemes as $scheme) {
326
            $this->schemes[$scheme] = true;
327
        }
328
329
        return $this;
330
    }
331
332
    /**
333
     * Sets the middleware(s) to handle before triggering the route handler
334
     *
335
     * @param mixed ...$middlewares
336
     *
337
     * @return Route $this The current Route instance
338
     */
339
    public function middleware(...$middlewares): self
340
    {
341
        /** @var int|string $index */
342
        foreach ($middlewares as $index => $middleware) {
343
            if (!\is_callable($middleware) && (\is_int($index) && \is_array($middleware))) {
344
                $this->middleware(...$middleware);
345
346
                continue;
347
            }
348
349
            $this->middlewares[] = $middleware;
350
        }
351
352
        return $this;
353
    }
354
355
    /**
356
     * Get any of (name, path, domain, defaults, schemes, domain, controller, patterns, middlewares).
357
     * And also accepts "all" and "arguments".
358
     *
359
     * @param string $name
360
     *
361
     * @return mixed
362
     */
363
    public function get(string $name)
364
    {
365
        if (\property_exists($this, $name)) {
366
            return $this->{$name};
367
        }
368
369
        if ('all' === $name) {
370
            return [
371
                'controller'  => $this->controller,
372
                'methods'     => $this->methods,
373
                'schemes'     => $this->schemes,
374
                'domain'      => $this->domain,
375
                'name'        => $this->name,
376
                'path'        => $this->path,
377
                'patterns'    => $this->patterns,
378
                'middlewares' => $this->middlewares,
379
                'defaults'    => $this->defaults,
380
            ];
381
        }
382
383
        if ('arguments' === $name) {
384
            return $this->defaults['_arguments'] ?? [];
385
        }
386
387
        return null;
388
    }
389
390
    public function generateRouteName(string $prefix): string
391
    {
392
        $methods = \implode('_', \array_keys($this->methods)) . '_';
393
394
        $routeName = $methods . $prefix . $this->path;
395
        $routeName = \str_replace(['/', ':', '|', '-'], '_', $routeName);
396
        $routeName = (string) \preg_replace('/[^a-z0-9A-Z_.]+/', '', $routeName);
397
398
        // Collapse consecutive underscores down into a single underscore.
399
        $routeName = (string) \preg_replace('/_+/', '_', $routeName);
400
401
        return $routeName;
402
    }
403
}
404