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

FastRoute::bind()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 5
Code Lines 2

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 1
eloc 2
nc 1
nop 1
dl 0
loc 5
rs 10
c 0
b 0
f 0
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\Routes;
19
20
use Flight\Routing\Exceptions\{MethodNotAllowedException, InvalidControllerException};
21
use Flight\Routing\{Router, RouteCollection};
22
use Flight\Routing\Handlers\ResourceHandler;
23
use Psr\Http\Message\UriInterface;
24
25
/**
26
 * Value object representing a single route.
27
 *
28
 * Route path and prefixing a path are not casted. This class is meant to be
29
 * extendable for addition support on route(s).
30
 *
31
 * The default support for this route class:
32
 * - name binding
33
 * - methods binding
34
 * - handler & namespacing
35
 * - arguments binding to handler
36
 * - pattern placeholders assert binding
37
 * - add defaults binding
38
 *
39
 * @method string      getPath()      Gets the route path.
40
 * @method string|null getName()      Gets the route name.
41
 * @method string[]    getMethods()   Gets the route methods.
42
 * @method mixed       getHandler()   Gets the route handler.
43
 * @method array       getArguments() Gets the arguments passed to route handler as parameters.
44
 * @method array       getDefaults()  Gets the route default settings.
45
 * @method array       getPatterns()  Gets the route pattern placeholder assert.
46
 *
47
 * @author Divine Niiquaye Ibok <[email protected]>
48
 */
49
class FastRoute
50
{
51
    /** Default methods for route. */
52
    public const DEFAULT_METHODS = [Router::METHOD_GET, Router::METHOD_HEAD];
53
54
    /** @var array<string,string> Getter methods supported by route */
55
    protected static $getter = [
56
        'name' => 'name',
57
        'path' => 'path',
58
        'methods' => 'methods*',
59
        'handler' => 'handler',
60
        'arguments' => 'arguments*',
61
        'defaults' => 'defaults*',
62
        'patterns' => 'patterns*',
63
    ];
64
65
    /** @var array<string,mixed> */
66
    protected $data = [];
67
68
    /** @var RouteCollection|null */
69
    private $collection;
70
71
    /**
72
     * Create a new Route constructor.
73
     *
74
     * @param string          $pattern The route pattern
75
     * @param string|string[] $methods the route HTTP methods
76
     * @param mixed           $handler The PHP class, object or callable that returns the response when matched
77
     */
78
    public function __construct(string $pattern, $methods = self::DEFAULT_METHODS, $handler = null)
79
    {
80
        $this->data = [
81
            'path' => $pattern,
82
            'handler' => $handler,
83
            'methods' => !empty($methods) ? \array_map('strtoupper', (array) $methods) : [],
84
        ];
85
    }
86
87
    /**
88
     * @param string[] $arguments
89
     *
90
     * @throws \BadMethodCallException
91
     */
92
    public function __call(string $method, array $arguments)
93
    {
94
        if (\str_starts_with($method = \strtolower($method), 'get')) {
95
            $method = \substr($method, 3);
96
        }
97
98
        if (!empty($arguments)) {
99
            throw new \BadMethodCallException(\sprintf('Arguments passed into "%s::%s(...)" not supported, method invalid.', __CLASS__, $method));
100
        }
101
102
        return $this->get($method);
103
    }
104
105
    /**
106
     * @internal
107
     */
108
    public function __serialize(): array
109
    {
110
        return $this->data;
111
    }
112
113
    /**
114
     * @internal
115
     *
116
     * @param array<string,mixed>
117
     */
118
    public function __unserialize(array $data): void
119
    {
120
        $this->data = $data;
121
    }
122
123
    /**
124
     * @internal
125
     *
126
     * @param array<string,mixed> $properties The route data properties
127
     *
128
     * @return self
129
     */
130
    public static function __set_state(array $properties)
131
    {
132
        $route = new static($properties['path'] ?? '', $properties['methods'] ?? [], $properties['handler'] ?? null);
133
        $route->data += \array_diff_key($properties, ['path' => null, 'methods' => [], 'handler' => null]);
134
135
        return $route;
136
    }
137
138
    /**
139
     * Create a new Route statically.
140
     *
141
     * @param string          $pattern The route pattern
142
     * @param string|string[] $methods the route HTTP methods
143
     * @param mixed           $handler The PHP class, object or callable that returns the response when matched
144
     */
145
    public static function to(string $pattern, $methods = self::DEFAULT_METHODS, $handler = null): self
146
    {
147
        return new static($pattern, $methods, $handler);
148
    }
149
150
    /**
151
     * Asserts route.
152
     *
153
     * @throws MethodNotAllowedException
154
     */
155
    public function match(string $method, UriInterface $uri): self
156
    {
157
        if (!\in_array($method, $methods = $this->get('methods'), true)) {
0 ignored issues
show
Bug introduced by
It seems like $methods = $this->get('methods') can also be of type null; however, parameter $haystack of in_array() does only seem to accept array, maybe add an additional type check? ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-type  annotation

157
        if (!\in_array($method, /** @scrutinizer ignore-type */ $methods = $this->get('methods'), true)) {
Loading history...
158
            throw new MethodNotAllowedException($methods, $uri->getPath(), $method);
0 ignored issues
show
Bug introduced by
It seems like $methods can also be of type null; however, parameter $methods of Flight\Routing\Exception...xception::__construct() does only seem to accept array, maybe add an additional type check? ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-type  annotation

158
            throw new MethodNotAllowedException(/** @scrutinizer ignore-type */ $methods, $uri->getPath(), $method);
Loading history...
159
        }
160
161
        return $this;
162
    }
163
164
    /**
165
     * Sets the route path prefix.
166
     */
167
    public function prefix(string $path): self
168
    {
169
        $this->data['path'] = $path . $this->data['path'] ?? '';
170
171
        return $this;
172
    }
173
174
    /**
175
     * Sets the route path pattern.
176
     */
177
    public function path(string $pattern): self
178
    {
179
        $this->data['path'] = $pattern;
180
181
        return $this;
182
    }
183
184
    /**
185
     * Sets the requirement for the HTTP method.
186
     *
187
     * @param string $methods the HTTP method(s) name
188
     */
189
    public function method(string ...$methods): self
190
    {
191
        foreach ($methods as $method) {
192
            $this->data['methods'][] = \strtoupper($method);
193
        }
194
195
        return $this;
196
    }
197
198
    /**
199
     * Sets the route name.
200
     */
201
    public function bind(string $routeName): self
202
    {
203
        $this->data['name'] = $routeName;
204
205
        return $this;
206
    }
207
208
    /**
209
     * Sets the parameter value for a route handler.
210
     *
211
     * @param mixed $value The parameter value
212
     */
213
    public function argument(string $parameter, $value): self
214
    {
215
        if (\is_numeric($value)) {
216
            $value = (int) $value;
217
        } elseif (\is_string($value)) {
218
            $value = \rawurldecode($value);
219
        }
220
221
        $this->data['arguments'][$parameter] = $value;
222
223
        return $this;
224
    }
225
226
    /**
227
     * Sets the parameter values for a route handler.
228
     *
229
     * @param array<int|string> $parameters The route handler parameters
230
     */
231
    public function arguments(array $parameters): self
232
    {
233
        foreach ($parameters as $variable => $value) {
234
            $this->argument($variable, $value);
235
        }
236
237
        return $this;
238
    }
239
240
    /**
241
     * Sets the route code that should be executed when matched.
242
     *
243
     * @param mixed $to PHP class, object or callable that returns the response when matched
244
     */
245
    public function run($to): self
246
    {
247
        $this->data['handler'] = $to;
248
249
        return $this;
250
    }
251
252
    /**
253
     * Sets the missing namespace on route's handler.
254
     *
255
     * @throws InvalidControllerException if $namespace is invalid
256
     */
257
    public function namespace(string $namespace): self
258
    {
259
        if ('' !== $namespace) {
260
            if ('\\' === $namespace[-1]) {
261
                throw new InvalidControllerException(\sprintf('Namespace "%s" provided for routes must not end with a "\\".', $namespace));
262
            }
263
264
            if (isset($this->data['handler'])) {
265
                $this->data['handler'] = self::resolveNamespace($namespace, $this->data['handler']);
266
            }
267
        }
268
269
        return $this;
270
    }
271
272
    /**
273
     * Sets the requirement for a route variable.
274
     *
275
     * @param string|string[] $regexp The regexp to apply
276
     */
277
    public function assert(string $variable, $regexp): self
278
    {
279
        $this->data['patterns'][$variable] = $regexp;
280
281
        return $this;
282
    }
283
284
    /**
285
     * Sets the requirements for a route variable.
286
     *
287
     * @param array<string,string|string[]> $regexps The regexps to apply
288
     */
289
    public function asserts(array $regexps): self
290
    {
291
        foreach ($regexps as $variable => $regexp) {
292
            $this->assert($variable, $regexp);
293
        }
294
295
        return $this;
296
    }
297
298
    /**
299
     * Sets the default value for a route variable.
300
     *
301
     * @param mixed $default The default value
302
     */
303
    public function default(string $variable, $default): self
304
    {
305
        $this->data['defaults'][$variable] = $default;
306
307
        return $this;
308
    }
309
310
    /**
311
     * Sets the default values for a route variables.
312
     *
313
     * @param array<string,mixed> $values
314
     */
315
    public function defaults(array $values): self
316
    {
317
        foreach ($values as $variable => $default) {
318
            $this->default($variable, $default);
319
        }
320
321
        return $this;
322
    }
323
324
    /**
325
     * Sets the route belonging to a particular collection.
326
     *
327
     * This method is kinda internal, only used in RouteCollection class,
328
     * and retrieved using this class end method.
329
     *
330
     * @internal used by RouteCollection class
331
     */
332
    public function belong(RouteCollection $to): void
333
    {
334
        $this->collection = $to;
335
    }
336
337
    /**
338
     * End a group stack or return self.
339
     */
340
    public function end(): ?RouteCollection
341
    {
342
        if (null !== $stack = $this->collection) {
343
            $this->collection = null; // Just remove it.
344
        }
345
346
        return $stack;
347
    }
348
349
    /**
350
     * Get a return from any valid key name of this class $getter static property.
351
     *
352
     * @throws \InvalidArgumentException if $name does not exist as property
353
     *
354
     * @return mixed
355
     */
356
    public function get(string $name)
357
    {
358
        $name = static::$getter[$name] ?? throw new \InvalidArgumentException(
359
            \sprintf('Invalid call for "%s" in %s(\'%1$s\'), try any of [%s].', $name, __METHOD__, \implode(',', \array_keys(static::$getter)))
360
        );
361
362
        if ('*' === $name[-1]) {
363
            return \array_unique($this->data[\substr($name, 0, -1)] ?? []);
364
        }
365
366
        return $this->data[$name] ?? null;
367
    }
368
369
    /**
370
     * Get the route's data.
371
     *
372
     * @return array<string,mixed>
373
     */
374
    public function getData(): array
375
    {
376
        return \array_map(function (string $property) {
377
            if ('*' === $property[-1]) {
378
                $property = \substr($property, 0, -1);
379
            }
380
381
            return $this->get($property);
382
        }, static::$getter);
383
    }
384
385
    public function generateRouteName(string $prefix): string
386
    {
387
        $routeName = \implode('_', $this->data['methods'] ?? []) . '_' . $prefix . $this->data['path'] ?? '';
388
        $routeName = \str_replace(['/', ':', '|', '-'], '_', $routeName);
389
        $routeName = (string) \preg_replace('/[^a-z0-9A-Z_.]+/', '', $routeName);
390
391
        return (string) \preg_replace(['/\_+/', '/\.+/'], ['_', '.'], $routeName);
392
    }
393
394
    /**
395
     * @internal skip throwing an exception and return existing $controller
396
     *
397
     * @param callable|object|string|string[] $controller
398
     *
399
     * @return mixed
400
     */
401
    private static function resolveNamespace(string $namespace, $controller)
402
    {
403
        if ($controller instanceof ResourceHandler) {
404
            return $controller->namespace($namespace);
405
        }
406
407
        if (\is_string($controller) && (!\str_starts_with($controller, $namespace) && '\\' === $controller[0])) {
408
            return $namespace . $controller;
409
        }
410
411
        if ((\is_array($controller) && \array_keys($controller) === [0, 1]) && \is_string($controller[0])) {
412
            $controller[0] = self::resolveNamespace($namespace, $controller[0]);
413
        }
414
415
        return $controller;
416
    }
417
}
418