Test Failed
Pull Request — master (#13)
by Divine Niiquaye
02:21
created

Route::default()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 5
Code Lines 3

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 0
CRAP Score 2

Importance

Changes 0
Metric Value
eloc 3
c 0
b 0
f 0
dl 0
loc 5
rs 10
ccs 0
cts 0
cp 0
cc 1
nc 1
nop 2
crap 2
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
     * @var string
73
     */
74
    public const URL_PATTERN = '/^(?:(?P<scheme>https?)\:)?(?P<domain>(?:\/\/)?(?P<host>[^\/\*]+)?(\:\d+)?)\/*?$/u';
75
76
    /**
77
     * Create a new Route constructor.
78
     *
79 151
     * @param string $pattern The route pattern
80
     * @param string $methods The route HTTP methods. Multiple methods can be supplied,
81 151
     *                        delimited by a pipe character '|', eg. 'GET|POST'
82 151
     * @param mixed  $handler The PHP class, object or callable that returns the response when matched
83 151
     */
84 151
    public function __construct(string $pattern, string $methods = 'GET|HEAD', $handler = null)
85 150
    {
86
        $this->controller = $handler;
87
        $this->path       = $this->castRoute($pattern);
88
89
        if (!empty($methods)) {
90
            $this->method(...\explode('|', $methods));
91
        }
92 1
    }
93
94 1
    /**
95
     * @internal This is handled different by router
96 1
     *
97 1
     * @param array $properties
98 1
     */
99 1
    public static function __set_state(array $properties)
100 1
    {
101 1
        $recovered = new self($properties['path'], '', $properties['controller']);
102 1
103
        unset($properties['path'], $properties['controller']);
104 1
105
        foreach ($properties as $name => $property) {
106
            $recovered->{$name} = $property;
107
        }
108
109
        return $recovered;
110
    }
111
112
    /**
113
     * @param string   $method
114
     * @param string[] $arguments
115
     *
116
     * @return mixed
117
     */
118
    public function __call($method, $arguments)
119
    {
120
        $routeMethod = \strtolower((string) \preg_replace('~^get([A-Z]{1}[a-z]+)$~', '\1', $method, 1));
121
122
        if ('all' === $routeMethod || 'arguments' === $routeMethod) {
123
            return \call_user_func([$this, 'get'], $routeMethod);
124
        }
125
126
        if (!\property_exists(__CLASS__, $routeMethod)) {
127
            throw new \BadMethodCallException(
128
                \sprintf(
129
                    'Property "%s->%s" does not exist. should be one of [%s],' .
130
                    ' or arguments, prefixed with a \'get\' name; eg: getName().',
131
                    Route::class,
132
                    $method,
133
                    \join(', ', \array_keys($this->get('all')))
134
                )
135
            );
136
        }
137
138
        return \call_user_func([$this, 'get'], $routeMethod);
139
    }
140
141
    /**
142
     * Sets the route path prefix.
143
     *
144
     * @param string $path
145
     *
146
     * @return Route $this The current Route instance
147
     */
148
    public function prefix(string $path): self
149
    {
150
        $this->path = $this->castPrefix($this->path, $path);
151
152
        return $this;
153
    }
154
155
    /**
156
     * Sets the route path pattern.
157
     *
158
     * @param string $pattern
159
     *
160
     * @return Route $this The current Route instance
161
     */
162
    public function path(string $pattern): self
163
    {
164
        $this->path = $this->castRoute($pattern);
165
166
        return $this;
167
    }
168
169
    /**
170
     * Sets the route name.
171
     *
172
     * @param string $routeName
173
     *
174
     * @return Route $this The current Route instance
175
     */
176
    public function bind(string $routeName): self
177
    {
178
        $this->name = $routeName;
179
180
        return $this;
181
    }
182
183
    /**
184
     * Sets the route code that should be executed when matched.
185
     *
186
     * @param mixed $to PHP class, object or callable that returns the response when matched
187
     *
188
     * @return Route $this The current Route instance
189
     */
190
    public function run($to): self
191
    {
192
        $this->controller = $to;
193
194
        return $this;
195
    }
196
197
    /**
198
     * Sets the requirement for a route variable.
199
     *
200
     * @param string          $variable The variable name
201
     * @param string|string[] $regexp   The regexp to apply
202
     *
203
     * @return Route $this The current route instance
204
     */
205
    public function assert(string $variable, $regexp): self
206
    {
207
        $this->patterns[$variable] = $regexp;
208
209
        return $this;
210
    }
211
212
    /**
213
     * Sets the default value for a route variable.
214
     *
215
     * @param string $variable The variable name
216
     * @param mixed  $default  The default value
217
     *
218
     * @return Route $this The current Route instance
219
     */
220
    public function default(string $variable, $default): self
221
    {
222
        $this->defaults[$variable] = $default;
223
224
        return $this;
225
    }
226
227
    /**
228
     * Sets the parameter value for a route handler.
229
     *
230
     * @param int|string $variable The parameter name
231
     * @param mixed      $value    The parameter value
232
     *
233
     * @return Route $this The current Route instance
234
     */
235
    public function argument($variable, $value): self
236
    {
237
        if (!\is_int($variable)) {
238
            if (\is_numeric($value)) {
239
                $value = (int) $value;
240
            } elseif (\is_string($value)) {
241
                $value = \rawurldecode($value);
242
            }
243
244
            $this->defaults['_arguments'][$variable] = $value;
245
        }
246
247
        return $this;
248
    }
249
250
    /**
251
     * Sets the requirement for the HTTP method.
252
     *
253
     * @param string $methods the HTTP method(s) name
254
     *
255
     * @return Route $this The current Route instance
256
     */
257
    public function method(string ...$methods): self
258
    {
259
        foreach ($methods as $method) {
260
            $this->methods[\strtoupper($method)] = true;
261
        }
262
263
        return $this;
264
    }
265
266
    /**
267
     * Sets the requirement of host on this Route.
268
     *
269
     * @param string $hosts The host for which this route should be enabled
270
     *
271
     * @return Route $this The current Route instance
272
     */
273
    public function domain(string ...$hosts): self
274
    {
275
        foreach ($hosts as $host) {
276
            \preg_match(Route::URL_PATTERN, $host, $matches);
277
278
            if (isset($matches['scheme']) && !empty($scheme = $matches['scheme'])) {
279
                $this->schemes[$scheme] = true;
280
            }
281
282
            $this->domain[$matches['host'] ?? $host] = true;
283
        }
284
285
        return $this;
286
    }
287
288
    /**
289
     * Sets the requirement of domain scheme on this Route.
290
     *
291
     * @param string ...$schemes
292
     *
293
     * @return Route $this The current Route instance
294
     */
295
    public function scheme(string ...$schemes): self
296
    {
297
        foreach ($schemes as $scheme) {
298
            $this->schemes[$scheme] = true;
299
        }
300
301
        return $this;
302
    }
303
304
    /**
305
     * Sets the middleware(s) to handle before triggering the route handler
306
     *
307
     * @param mixed ...$middlewares
308
     *
309
     * @return Route $this The current Route instance
310
     */
311
    public function middleware(...$middlewares): self
312
    {
313
        /** @var int|string $index */
314
        foreach ($middlewares as $index => $middleware) {
315
            if (!\is_callable($middleware) && (\is_int($index) && \is_array($middleware))) {
316
                $this->middleware(...$middleware);
317
318
                continue;
319
            }
320
321
            $this->middlewares[] = $middleware;
322
        }
323
324
        return $this;
325
    }
326
327
    /**
328
     * Get any of (name, path, domain, defaults, schemes, domain, controller, patterns, middlewares).
329
     * And also accepts "all" and "arguments".
330
     *
331
     * @param string $name
332
     *
333
     * @return mixed
334
     */
335
    public function get(string $name)
336
    {
337
        if (\property_exists(__CLASS__, $name)) {
338
            return $this->{$name};
339
        }
340
341
        if ('all' === $name) {
342
            return [
343
                'controller'  => $this->controller,
344
                'methods'     => $this->methods,
345
                'schemes'     => $this->schemes,
346
                'domain'      => $this->domain,
347
                'name'        => $this->name,
348
                'path'        => $this->path,
349
                'patterns'    => $this->patterns,
350
                'middlewares' => $this->middlewares,
351
                'defaults'    => $this->defaults,
352
            ];
353
        }
354
355
        if ('arguments' === $name) {
356
            return $this->defaults['_arguments'] ?? [];
357
        }
358
359
        return null;
360
    }
361
362
    public function generateRouteName(string $prefix): string
363
    {
364
        $methods = \implode('_', \array_keys($this->methods)) . '_';
365
366
        $routeName = $methods . $prefix . $this->path;
367
        $routeName = \str_replace(['/', ':', '|', '-'], '_', $routeName);
368
        $routeName = (string) \preg_replace('/[^a-z0-9A-Z_.]+/', '', $routeName);
369
370
        // Collapse consecutive underscores down into a single underscore.
371
        $routeName = (string) \preg_replace('/_+/', '_', $routeName);
372
373
        return $routeName;
374
    }
375
}
376