Passed
Push — develop ( 2bc06f...07d547 )
by nguereza
02:52
created

Route   A

Complexity

Total Complexity 33

Size/Duplication

Total Lines 339
Duplicated Lines 0 %

Importance

Changes 1
Bugs 0 Features 0
Metric Value
eloc 96
c 1
b 0
f 0
dl 0
loc 339
rs 9.76
wmc 33

15 Methods

Rating   Name   Duplication   Size   Complexity  
A setName() 0 5 1
A getAttribute() 0 3 1
A __construct() 0 22 3
A getParameters() 0 3 1
B match() 0 45 9
A setAttribute() 0 5 1
A getMethods() 0 3 1
A path() 0 3 1
A getName() 0 3 1
A hasAttribute() 0 3 1
B getUri() 0 38 8
A isAllowedMethod() 0 7 2
A getHandler() 0 3 1
A removeAttribute() 0 5 1
A getPattern() 0 3 1
1
<?php
2
3
/**
4
 * Platine Router
5
 *
6
 * Platine Router is the a lightweight and simple router using middleware
7
 *  to match and dispatch the request.
8
 *
9
 * This content is released under the MIT License (MIT)
10
 *
11
 * Copyright (c) 2020 Platine Router
12
 * Copyright (c) 2020 Evgeniy Zyubin
13
 *
14
 * Permission is hereby granted, free of charge, to any person obtaining a copy
15
 * of this software and associated documentation files (the "Software"), to deal
16
 * in the Software without restriction, including without limitation the rights
17
 * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
18
 * copies of the Software, and to permit persons to whom the Software is
19
 * furnished to do so, subject to the following conditions:
20
 *
21
 * The above copyright notice and this permission notice shall be included in all
22
 * copies or substantial portions of the Software.
23
 *
24
 * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
25
 * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
26
 * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
27
 * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
28
 * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
29
 * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
30
 * SOFTWARE.
31
 */
32
33
/**
34
 *  @file Route.php
35
 *
36
 *  The Route class used to describe each route data
37
 *
38
 *  @package    Platine\Route
39
 *  @author Platine Developers Team
40
 *  @copyright  Copyright (c) 2020
41
 *  @license    http://opensource.org/licenses/MIT  MIT License
42
 *  @link   https://www.platine-php.com
43
 *  @version 1.0.0
44
 *  @filesource
45
 */
46
47
declare(strict_types=1);
48
49
namespace Platine\Route;
50
51
use InvalidArgumentException;
52
use Platine\Http\ServerRequestInterface;
53
use Platine\Http\Uri;
54
use Platine\Http\UriInterface;
55
use Platine\Route\Exception\InvalidRouteParameterException;
56
57
class Route
58
{
59
    /**
60
     * Search through the given route looking for dynamic portions.
61
     *
62
     * Using ~ as the regex delimiter.
63
     *
64
     * We start by looking for a literal '{' character followed by any amount
65
     * of whitespace. The next portion inside the parentheses looks for a parameter name
66
     * containing alphanumeric characters or underscore.
67
     *
68
     * After this we look for the ':\d+' and ':[0-9]+'
69
     * style portion ending with a closing '}' character.
70
     *
71
     * Finally we look for an optional '?' which is used to signify
72
     * an optional route parameter.
73
     */
74
    private const PARAMETERS_PLACEHOLDER = '~\{\s*([a-zA-Z_][a-zA-Z0-9_-]*)\s*(?::\s*([^{}]*(?:\{(?-1)\}[^{}]*)*))?\}~';
75
76
    /**
77
     * The default parameter character restriction
78
     * (One or more characters that is not a '/').
79
     */
80
    private const DEFAULT_PARAMETERS_REGEX = '[^\/]+';
81
82
    /**
83
     * The route name
84
     * @var string
85
     */
86
    protected string $name;
87
88
    /**
89
     * The path pattern with parameters
90
     * @var string
91
     */
92
    protected string $pattern;
93
94
    /**
95
     * The route handler
96
     * action, controller, callable, closure, etc.
97
     * @var mixed
98
     */
99
    protected $handler;
100
101
    /**
102
     * The route allowed request methods
103
     * @var array<string>
104
     */
105
    protected array $methods = [];
106
107
    /**
108
     * The instance ParameterCollection.
109
     * @var ParameterCollection
110
     */
111
    protected ParameterCollection $parameters;
112
113
    /**
114
     * The list of routes parameters shortcuts
115
     * @var array<string, string>
116
     */
117
    protected array $parameterShortcuts = [
118
        ':i}' => ':[0-9]+}',
119
        ':a}' => ':[0-9A-Za-z]+}',
120
        ':al}' => ':[a-zA-Z0-9+_\-\.]+}',
121
        ':any}' => ':.*}',
122
    ];
123
124
    /**
125
     * The route attributes in order to add some
126
     * additional information
127
     * @var array<string, mixed>
128
     */
129
    protected array $attributes = [];
130
131
    /**
132
     * Create the new instance
133
     * @param string $pattern       path pattern with parameters.
134
     * @param mixed $handler       action, controller, callable, closure, etc.
135
     * @param string|null $name       the route name
136
     * @param array<string>  $methods the route allowed methods
137
     * @param array<string, mixed>  $attributes the route attributes
138
     */
139
    public function __construct(
140
        string $pattern,
141
        $handler,
142
        ?string $name = null,
143
        array $methods = [],
144
        array $attributes = []
145
    ) {
146
        $this->pattern = $pattern;
147
        $this->handler = $handler;
148
        $this->parameters = new ParameterCollection();
149
        $this->name = $name ?? '';
150
        $this->attributes = $attributes;
151
152
        foreach ($methods as $method) {
153
            if (!is_string($method)) {
154
                throw new InvalidRouteParameterException(sprintf(
155
                    'Invalid request method [%s], must be a string',
156
                    $method
157
                ));
158
            }
159
160
            $this->methods[] = strtoupper($method);
161
        }
162
    }
163
164
    /**
165
     * Whether the route has the given attribute
166
     * @param string $name
167
     * @return bool
168
     */
169
    public function hasAttribute(string $name): bool
170
    {
171
        return array_key_exists($name, $this->attributes);
172
    }
173
174
    /**
175
     * Return the value of the given attribute
176
     * @param string $name
177
     * @return mixed|null
178
     */
179
    public function getAttribute(string $name)
180
    {
181
        return $this->attributes[$name] ?? null;
182
    }
183
184
    /**
185
     * Set attribute value
186
     * @param string $name
187
     * @param mixed $value
188
     * @return $this
189
     */
190
    public function setAttribute(string $name, $value): self
191
    {
192
        $this->attributes[$name] = $value;
193
194
        return $this;
195
    }
196
197
    /**
198
     * Remove the given attribute
199
     * @param string $name
200
     * @return $this
201
     */
202
    public function removeAttribute(string $name): self
203
    {
204
        unset($this->attributes[$name]);
205
206
        return $this;
207
    }
208
209
    /**
210
     * Return the route name
211
     * @return string
212
     */
213
    public function getName(): string
214
    {
215
        return $this->name;
216
    }
217
218
    /**
219
     * Set the route name.
220
     *
221
     * @param string $name the new route name
222
     *
223
     * @return self
224
     */
225
    public function setName(string $name): self
226
    {
227
        $this->name = $name;
228
229
        return $this;
230
    }
231
232
    /**
233
     * Return the route pattern
234
     * @return string
235
     */
236
    public function getPattern(): string
237
    {
238
        return $this->pattern;
239
    }
240
241
    /**
242
     * Return the route handler
243
     * @return mixed
244
     */
245
    public function getHandler()
246
    {
247
        return $this->handler;
248
    }
249
250
    /**
251
     * Return the route request methods
252
     * @return string[]
253
     */
254
    public function getMethods(): array
255
    {
256
        return $this->methods;
257
    }
258
259
    /**
260
     * Return the ParameterCollection for this route.
261
     *
262
     * @return ParameterCollection
263
     */
264
    public function getParameters(): ParameterCollection
265
    {
266
        return $this->parameters;
267
    }
268
269
    /**
270
     * Checks whether the request method is allowed for the current route.
271
     * @param  string  $method
272
     * @return bool
273
     */
274
    public function isAllowedMethod(string $method): bool
275
    {
276
        return (empty($this->methods)
277
                || in_array(
278
                    strtoupper($method),
279
                    $this->methods,
280
                    true
281
                ));
282
    }
283
284
    /**
285
     * Checks whether the request URI matches the current route.
286
     *
287
     * If there is a match and the route has matched parameters, they will
288
     * be saved and available via the `Route::getParameters()` method.
289
     *
290
     * @param  ServerRequestInterface $request
291
     * @param  string $basePath
292
     * @return bool
293
     */
294
    public function match(ServerRequestInterface $request, string $basePath = '/'): bool
295
    {
296
        $routePattern = $this->pattern;
297
        $pattern = strtr($routePattern, $this->parameterShortcuts);
298
299
        preg_match_all(self::PARAMETERS_PLACEHOLDER, $pattern, $matches);
300
301
        foreach ($matches[0] as $key => $value) {
302
            $parameterName = ($matches[1][$key] !== '')
303
                    ? $matches[1][$key]
304
                    : $matches[2][$key];
305
306
            $parameterPattern = sprintf('(%s)', self::DEFAULT_PARAMETERS_REGEX);
307
            if ($matches[1][$key] !== '' && $matches[2][$key] !== '') {
308
                $parameterPattern = sprintf('(%s)', $matches[2][$key]);
309
            }
310
            $this->parameters->add(new Parameter($parameterName, null));
311
            $pattern = str_replace($value, $parameterPattern, $pattern);
312
        }
313
314
        $requestPath = $request->getUri()->getPath();
315
        if ($basePath !== '/') {
316
            $basePathLength = strlen($basePath);
317
            if (substr($requestPath, 0, $basePathLength) === $basePath) {
318
                $requestPath = substr($requestPath, $basePathLength);
319
            }
320
        }
321
322
        if (
323
            preg_match(
324
                '~^' . $pattern . '$~i',
325
                rawurldecode($requestPath),
326
                $matches
327
            )
328
        ) {
329
            array_shift($matches);
330
331
            foreach ($this->parameters->all() as $parameter) {
332
                $parameter->setValue(array_shift($matches));
333
            }
334
335
            return true;
336
        }
337
338
        return false;
339
    }
340
341
    /**
342
     * Return the URI for this route
343
     * @param  array<string, mixed>  $parameters the route parameters
344
     * @param string $basePath the base path
345
     * @return UriInterface
346
     */
347
    public function getUri(array $parameters = [], $basePath = '/'): UriInterface
348
    {
349
        $pattern = $this->pattern;
350
        if ($basePath !== '/') {
351
            $pattern = rtrim($basePath, '/') . $pattern;
352
        }
353
        $uri = strtr($pattern, $this->parameterShortcuts);
354
355
        $matches = [];
356
        preg_match_all(self::PARAMETERS_PLACEHOLDER, $uri, $matches);
357
358
        foreach ($matches[0] as $key => $value) {
359
            $parameterName = ($matches[1][$key] !== '')
360
                    ? $matches[1][$key]
361
                    : $matches[2][$key];
362
363
            $parameterPattern = sprintf('(%s)', self::DEFAULT_PARAMETERS_REGEX);
364
            if ($matches[1][$key] !== '' && $matches[2][$key] !== '') {
365
                $parameterPattern = sprintf('(%s)', $matches[2][$key]);
366
            }
367
368
            if (
369
                    isset($parameters[$parameterName])
370
                    && preg_match(
371
                        '/^' . $parameterPattern . '$/',
372
                        (string) $parameters[$parameterName]
373
                    )
374
            ) {
375
                $uri = str_replace($value, (string) $parameters[$parameterName], $uri);
376
            } else {
377
                throw new InvalidArgumentException(sprintf(
378
                    'Parameter [%s] is not passed',
379
                    $parameterName
380
                ));
381
            }
382
        }
383
384
        return new Uri($uri);
385
    }
386
387
    /**
388
     * Generates the URL path from the route parameters.
389
     * @param  array<string, mixed>  $parameters parameter-value set.
390
     * @param string $basePath the base path
391
     * @return string URL path generated.
392
     */
393
    public function path(array $parameters = [], $basePath = '/'): string
394
    {
395
        return $this->getUri($parameters, $basePath)->getPath();
396
    }
397
}
398