Issues (3)

src/Route.php (2 issues)

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
/**
58
 * @class Route
59
 * @package Platine\Route
60
 */
61
class Route
62
{
63
    /**
64
     * Search through the given route looking for dynamic portions.
65
     *
66
     * Using ~ as the regex delimiter.
67
     *
68
     * We start by looking for a literal '{' character followed by any amount
69
     * of whitespace. The next portion inside the parentheses looks for a parameter name
70
     * containing alphanumeric characters or underscore.
71
     *
72
     * After this we look for the ':\d+' and ':[0-9]+'
73
     * style portion ending with a closing '}' character.
74
     *
75
     * Finally we look for an optional '?' which is used to signify
76
     * an optional route parameter.
77
     */
78
    private const PARAMETERS_PLACEHOLDER = '~\{\s*([a-zA-Z_][a-zA-Z0-9_-]*)\s*(?::\s*([^{}]*(?:\{(?-1)\}[^{}]*)*))?\}~';
79
80
    /**
81
     * The default parameter character restriction
82
     * (One or more characters that is not a '/').
83
     */
84
    private const DEFAULT_PARAMETERS_REGEX = '[^\/]+';
85
86
    /**
87
     * The route name
88
     * @var string
89
     */
90
    protected string $name;
91
92
    /**
93
     * The path pattern with parameters
94
     * @var string
95
     */
96
    protected string $pattern;
97
98
    /**
99
     * The route handler
100
     * action, controller, callable, closure, etc.
101
     * @var mixed
102
     */
103
    protected mixed $handler;
104
105
    /**
106
     * The route allowed request methods
107
     * @var array<string>
108
     */
109
    protected array $methods = [];
110
111
    /**
112
     * The instance ParameterCollection.
113
     * @var ParameterCollection
114
     */
115
    protected ParameterCollection $parameters;
116
117
    /**
118
     * The list of routes parameters shortcuts
119
     * @var array<string, string>
120
     */
121
    protected array $parameterShortcuts = [
122
        ':i}' => ':[0-9]+}',
123
        ':a}' => ':[0-9A-Za-z]+}',
124
        ':al}' => ':[a-zA-Z0-9+_\-\.]+}',
125
        ':any}' => ':.*}',
126
    ];
127
128
    /**
129
     * The route attributes in order to add some
130
     * additional information
131
     * @var array<string, mixed>
132
     */
133
    protected array $attributes = [];
134
135
    /**
136
     * Create the new instance
137
     * @param string $pattern       path pattern with parameters.
138
     * @param mixed $handler       action, controller, callable, closure, etc.
139
     * @param string|null $name       the route name
140
     * @param string[]|string  $methods the route allowed methods
141
     * @param array<string, mixed>  $attributes the route attributes
142
     */
143
    public function __construct(
144
        string $pattern,
145
        mixed $handler,
146
        ?string $name = null,
147
        array|string $methods = [],
148
        array $attributes = []
149
    ) {
150
        $this->pattern = $pattern;
151
        $this->handler = $handler;
152
        $this->parameters = new ParameterCollection();
153
        $this->name = $name ?? '';
154
        $this->attributes = $attributes;
155
        
156
        if(is_string($methods)){
0 ignored issues
show
The condition is_string($methods) is always false.
Loading history...
Expected 1 space after IF keyword; 0 found
Loading history...
157
            $methods = [$methods];
158
        }
159
160
        foreach ($methods as $method) {
161
            if (!is_string($method)) {
162
                throw new InvalidRouteParameterException(sprintf(
163
                    'Invalid request method [%s], must be a string',
164
                    $method
165
                ));
166
            }
167
168
            $this->methods[] = strtoupper($method);
169
        }
170
    }
171
172
    /**
173
     * Whether the route has the given attribute
174
     * @param string $name
175
     * @return bool
176
     */
177
    public function hasAttribute(string $name): bool
178
    {
179
        return array_key_exists($name, $this->attributes);
180
    }
181
182
    /**
183
     * Return the value of the given attribute
184
     * @param string $name
185
     * @return mixed
186
     */
187
    public function getAttribute(string $name): mixed
188
    {
189
        return $this->attributes[$name] ?? null;
190
    }
191
192
    /**
193
     * Set attribute value
194
     * @param string $name
195
     * @param mixed $value
196
     * @return $this
197
     */
198
    public function setAttribute(string $name, mixed $value): self
199
    {
200
        $this->attributes[$name] = $value;
201
202
        return $this;
203
    }
204
205
    /**
206
     * Remove the given attribute
207
     * @param string $name
208
     * @return $this
209
     */
210
    public function removeAttribute(string $name): self
211
    {
212
        unset($this->attributes[$name]);
213
214
        return $this;
215
    }
216
217
    /**
218
     * Return the route name
219
     * @return string
220
     */
221
    public function getName(): string
222
    {
223
        return $this->name;
224
    }
225
226
    /**
227
     * Set the route name.
228
     *
229
     * @param string $name the new route name
230
     *
231
     * @return $this
232
     */
233
    public function setName(string $name): self
234
    {
235
        $this->name = $name;
236
237
        return $this;
238
    }
239
240
    /**
241
     * Return the route pattern
242
     * @return string
243
     */
244
    public function getPattern(): string
245
    {
246
        return $this->pattern;
247
    }
248
249
    /**
250
     * Return the route handler
251
     * @return mixed
252
     */
253
    public function getHandler(): mixed
254
    {
255
        return $this->handler;
256
    }
257
258
    /**
259
     * Return the route request methods
260
     * @return string[]
261
     */
262
    public function getMethods(): array
263
    {
264
        return $this->methods;
265
    }
266
267
    /**
268
     * Return the ParameterCollection for this route.
269
     *
270
     * @return ParameterCollection
271
     */
272
    public function getParameters(): ParameterCollection
273
    {
274
        return $this->parameters;
275
    }
276
277
    /**
278
     * Checks whether the request method is allowed for the current route.
279
     * @param  string  $method
280
     * @return bool
281
     */
282
    public function isAllowedMethod(string $method): bool
283
    {
284
        return (empty($this->methods)
285
                || in_array(
286
                    strtoupper($method),
287
                    $this->methods,
288
                    true
289
                ));
290
    }
291
292
    /**
293
     * Checks whether the request URI matches the current route.
294
     *
295
     * If there is a match and the route has matched parameters, they will
296
     * be saved and available via the `Route::getParameters()` method.
297
     *
298
     * @param  ServerRequestInterface $request
299
     * @param  string $basePath
300
     * @return bool
301
     */
302
    public function match(ServerRequestInterface $request, string $basePath = '/'): bool
303
    {
304
        $routePattern = $this->pattern;
305
        $pattern = strtr($routePattern, $this->parameterShortcuts);
306
        $matches = [];
307
308
        preg_match_all(self::PARAMETERS_PLACEHOLDER, $pattern, $matches);
309
310
        foreach ($matches[0] as $key => $value) {
311
            $parameterName = ($matches[1][$key] !== '')
312
                    ? $matches[1][$key]
313
                    : $matches[2][$key];
314
315
            $parameterPattern = sprintf('(%s)', self::DEFAULT_PARAMETERS_REGEX);
316
            if ($matches[1][$key] !== '' && $matches[2][$key] !== '') {
317
                $parameterPattern = sprintf('(%s)', $matches[2][$key]);
318
            }
319
            $this->parameters->add(new Parameter($parameterName, null));
320
            $pattern = str_replace($value, $parameterPattern, $pattern);
321
        }
322
323
        $requestPath = $request->getUri()->getPath();
324
        if ($basePath !== '/') {
325
            $basePathLength = strlen($basePath);
326
            if (substr($requestPath, 0, $basePathLength) === $basePath) {
327
                $requestPath = substr($requestPath, $basePathLength);
328
            }
329
        }
330
331
        if (
332
            preg_match(
333
                '~^' . $pattern . '$~i',
334
                rawurldecode($requestPath),
335
                $matches
336
            )
337
        ) {
338
            array_shift($matches);
339
340
            foreach ($this->parameters->all() as $parameter) {
341
                $parameter->setValue(array_shift($matches));
342
            }
343
344
            return true;
345
        }
346
347
        return false;
348
    }
349
350
    /**
351
     * Return the URI for this route
352
     * @param  array<string, mixed>  $parameters the route parameters
353
     * @param string $basePath the base path
354
     * @return UriInterface
355
     */
356
    public function getUri(array $parameters = [], string $basePath = '/'): UriInterface
357
    {
358
        $pattern = $this->pattern;
359
        if ($basePath !== '/') {
360
            $pattern = rtrim($basePath, '/') . $pattern;
361
        }
362
        $uri = strtr($pattern, $this->parameterShortcuts);
363
364
        $matches = [];
365
        preg_match_all(self::PARAMETERS_PLACEHOLDER, $uri, $matches);
366
367
        foreach ($matches[0] as $key => $value) {
368
            $parameterName = ($matches[1][$key] !== '')
369
                    ? $matches[1][$key]
370
                    : $matches[2][$key];
371
372
            $parameterPattern = sprintf('(%s)', self::DEFAULT_PARAMETERS_REGEX);
373
            if ($matches[1][$key] !== '' && $matches[2][$key] !== '') {
374
                $parameterPattern = sprintf('(%s)', $matches[2][$key]);
375
            }
376
377
            if (
378
                isset($parameters[$parameterName])
379
                && preg_match(
380
                    '/^' . $parameterPattern . '$/',
381
                    (string) $parameters[$parameterName]
382
                )
383
            ) {
384
                $uri = str_replace($value, (string) $parameters[$parameterName], $uri);
385
            } else {
386
                throw new InvalidArgumentException(sprintf(
387
                    'Parameter [%s] is not passed',
388
                    $parameterName
389
                ));
390
            }
391
        }
392
393
        return new Uri($uri);
394
    }
395
396
    /**
397
     * Generates the URL path from the route parameters.
398
     * @param  array<string, mixed>  $parameters parameter-value set.
399
     * @param string $basePath the base path
400
     * @return string URL path generated.
401
     */
402
    public function path(array $parameters = [], string $basePath = '/'): string
403
    {
404
        return $this->getUri($parameters, $basePath)->getPath();
405
    }
406
}
407