Route::hasAttribute()   A
last analyzed

Complexity

Conditions 1
Paths 1

Size

Total Lines 3
Code Lines 1

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 1
eloc 1
c 0
b 0
f 0
nc 1
nop 1
dl 0
loc 3
rs 10
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
56
/**
57
 * @class Route
58
 * @package Platine\Route
59
 */
60
class Route
61
{
62
    /**
63
     * Search through the given route looking for dynamic portions.
64
     *
65
     * Using ~ as the regex delimiter.
66
     *
67
     * We start by looking for a literal '{' character followed by any amount
68
     * of whitespace. The next portion inside the parentheses looks for a parameter name
69
     * containing alphanumeric characters or underscore.
70
     *
71
     * After this we look for the ':\d+' and ':[0-9]+'
72
     * style portion ending with a closing '}' character.
73
     *
74
     * Finally we look for an optional '?' which is used to signify
75
     * an optional route parameter.
76
     */
77
    private const PARAMETERS_PLACEHOLDER = '~\{\s*([a-zA-Z_][a-zA-Z0-9_-]*)\s*(?::\s*([^{}]*(?:\{(?-1)\}[^{}]*)*))?\}~';
78
79
    /**
80
     * The default parameter character restriction
81
     * (One or more characters that is not a '/').
82
     */
83
    private const DEFAULT_PARAMETERS_REGEX = '[^\/]+';
84
85
    /**
86
     * The route name
87
     * @var string
88
     */
89
    protected string $name;
90
91
    /**
92
     * The path pattern with parameters
93
     * @var string
94
     */
95
    protected string $pattern;
96
97
    /**
98
     * The route handler
99
     * action, controller, callable, closure, etc.
100
     * @var mixed
101
     */
102
    protected mixed $handler;
103
104
    /**
105
     * The route allowed request methods
106
     * @var array<string>
107
     */
108
    protected array $methods = [];
109
110
    /**
111
     * The instance ParameterCollection.
112
     * @var ParameterCollection
113
     */
114
    protected ParameterCollection $parameters;
115
116
    /**
117
     * The list of routes parameters shortcuts
118
     * @var array<string, string>
119
     */
120
    protected array $parameterShortcuts = [
121
        ':i}' => ':[0-9]+}',
122
        ':a}' => ':[0-9A-Za-z]+}',
123
        ':al}' => ':[a-zA-Z0-9+_\-\.]+}',
124
        ':any}' => ':.*}',
125
    ];
126
127
    /**
128
     * The route attributes in order to add some
129
     * additional information
130
     * @var array<string, mixed>
131
     */
132
    protected array $attributes = [];
133
134
    /**
135
     * Create the new instance
136
     * @param string $pattern       path pattern with parameters.
137
     * @param mixed $handler       action, controller, callable, closure, etc.
138
     * @param string|null $name       the route name
139
     * @param string[]|string  $methods the route allowed methods
140
     * @param array<string, mixed>  $attributes the route attributes
141
     */
142
    public function __construct(
143
        string $pattern,
144
        mixed $handler,
145
        ?string $name = null,
146
        array|string $methods = [],
147
        array $attributes = []
148
    ) {
149
        $this->pattern = $pattern;
150
        $this->handler = $handler;
151
        $this->parameters = new ParameterCollection();
152
        $this->name = $name ?? '';
153
        $this->attributes = $attributes;
154
155
        if (is_string($methods)) {
0 ignored issues
show
introduced by
The condition is_string($methods) is always false.
Loading history...
156
            $methods = [$methods];
157
        }
158
159
        foreach ($methods as $method) {
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
178
     */
179
    public function getAttribute(string $name): mixed
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, mixed $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 $this
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(): mixed
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
        $matches = [];
299
300
        preg_match_all(self::PARAMETERS_PLACEHOLDER, $pattern, $matches);
301
302
        foreach ($matches[0] as $key => $value) {
303
            $parameterName = ($matches[1][$key] !== '')
304
                    ? $matches[1][$key]
305
                    : $matches[2][$key];
306
307
            $parameterPattern = sprintf('(%s)', self::DEFAULT_PARAMETERS_REGEX);
308
            if ($matches[1][$key] !== '' && $matches[2][$key] !== '') {
309
                $parameterPattern = sprintf('(%s)', $matches[2][$key]);
310
            }
311
            $this->parameters->add(new Parameter($parameterName, null));
312
            $pattern = str_replace($value, $parameterPattern, $pattern);
313
        }
314
315
        $requestPath = $request->getUri()->getPath();
316
        if ($basePath !== '/') {
317
            $basePathLength = strlen($basePath);
318
            if (substr($requestPath, 0, $basePathLength) === $basePath) {
319
                $requestPath = substr($requestPath, $basePathLength);
320
            }
321
        }
322
323
        if (
324
            preg_match(
325
                '~^' . $pattern . '$~i',
326
                rawurldecode($requestPath),
327
                $matches
328
            )
329
        ) {
330
            array_shift($matches);
331
332
            foreach ($this->parameters->all() as $parameter) {
333
                $parameter->setValue(array_shift($matches));
334
            }
335
336
            return true;
337
        }
338
339
        return false;
340
    }
341
342
    /**
343
     * Return the URI for this route
344
     * @param  array<string, mixed>  $parameters the route parameters
345
     * @param string $basePath the base path
346
     * @return UriInterface
347
     */
348
    public function getUri(array $parameters = [], string $basePath = '/'): UriInterface
349
    {
350
        $pattern = $this->pattern;
351
        if ($basePath !== '/') {
352
            $pattern = rtrim($basePath, '/') . $pattern;
353
        }
354
        $uri = strtr($pattern, $this->parameterShortcuts);
355
356
        $matches = [];
357
        preg_match_all(self::PARAMETERS_PLACEHOLDER, $uri, $matches);
358
359
        foreach ($matches[0] as $key => $value) {
360
            $parameterName = ($matches[1][$key] !== '')
361
                    ? $matches[1][$key]
362
                    : $matches[2][$key];
363
364
            $parameterPattern = sprintf('(%s)', self::DEFAULT_PARAMETERS_REGEX);
365
            if ($matches[1][$key] !== '' && $matches[2][$key] !== '') {
366
                $parameterPattern = sprintf('(%s)', $matches[2][$key]);
367
            }
368
369
            if (
370
                isset($parameters[$parameterName])
371
                && preg_match(
372
                    '/^' . $parameterPattern . '$/',
373
                    (string) $parameters[$parameterName]
374
                )
375
            ) {
376
                $uri = str_replace($value, (string) $parameters[$parameterName], $uri);
377
            } else {
378
                throw new InvalidArgumentException(sprintf(
379
                    'Parameter [%s] is not passed',
380
                    $parameterName
381
                ));
382
            }
383
        }
384
385
        return new Uri($uri);
386
    }
387
388
    /**
389
     * Generates the URL path from the route parameters.
390
     * @param  array<string, mixed>  $parameters parameter-value set.
391
     * @param string $basePath the base path
392
     * @return string URL path generated.
393
     */
394
    public function path(array $parameters = [], string $basePath = '/'): string
395
    {
396
        return $this->getUri($parameters, $basePath)->getPath();
397
    }
398
}
399