Passed
Pull Request — master (#194)
by Dmitriy
11:40 queued 09:09
created

Route::get()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 3
Code Lines 1

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 2
CRAP Score 1

Importance

Changes 2
Bugs 0 Features 0
Metric Value
cc 1
eloc 1
c 2
b 0
f 0
nc 1
nop 1
dl 0
loc 3
ccs 2
cts 2
cp 1
crap 1
rs 10
1
<?php
2
3
declare(strict_types=1);
4
5
namespace Yiisoft\Router;
6
7
use Attribute;
8
use InvalidArgumentException;
9
use RuntimeException;
10
use Stringable;
11
use Yiisoft\Http\Method;
12
13
use function in_array;
14
15
/**
16
 * Route defines a mapping from URL to callback / name and vice versa.
17
 */
18
#[Attribute(Attribute::TARGET_METHOD | Attribute::TARGET_CLASS | Attribute::IS_REPEATABLE)]
19
final class Route implements Stringable
20
{
21
    private bool $actionAdded = false;
22
    private array $builtMiddlewares = [];
23
24
    /**
25
     * @param string[] $methods HTTP method names
26
     * @param string[] $hosts
27
     * @param array[]|callable[]|string[] $middlewares
28
     * @param array $disabledMiddlewares Excludes middleware from being invoked when action is handled.
29
     * It is useful to avoid invoking one of the parent group middleware for
30
     * a certain route.
31
     * @param bool $override Marks route as override. When added it will replace existing route with the same name.
32
     * @param array<string,string> $defaults  Parameter default values indexed by parameter names.
33
     *
34
     * @psalm-param array<string,null|Stringable|scalar> $defaults
35
     */
36 78
    public function __construct(
37
        private array $methods,
38
        private string $pattern,
39
        private ?string $name = null,
40
        private array $middlewares = [],
41
        private array $disabledMiddlewares = [],
42
        private array $hosts = [],
43
        private bool $override = false,
44
        private array $defaults = [],
45
    ) {
46 78
        $this->methods = $this->processMethods($this->methods);
47 78
        $this->hosts = $this->processHosts($this->hosts);
48 78
        $this->defaults = $this->processDefaults($this->defaults);
49
    }
50
51 64
    public static function get(string $pattern): self
52
    {
53 64
        return self::methods([Method::GET], $pattern);
54
    }
55
56 10
    public static function post(string $pattern): self
57
    {
58 10
        return self::methods([Method::POST], $pattern);
59
    }
60
61 4
    public static function put(string $pattern): self
62
    {
63 4
        return self::methods([Method::PUT], $pattern);
64
    }
65
66 1
    public static function delete(string $pattern): self
67
    {
68 1
        return self::methods([Method::DELETE], $pattern);
69
    }
70
71 1
    public static function patch(string $pattern): self
72
    {
73 1
        return self::methods([Method::PATCH], $pattern);
74
    }
75
76 1
    public static function head(string $pattern): self
77
    {
78 1
        return self::methods([Method::HEAD], $pattern);
79
    }
80
81 9
    public static function options(string $pattern): self
82
    {
83 9
        return self::methods([Method::OPTIONS], $pattern);
84
    }
85
86
    /**
87
     * @param string[] $methods
88
     */
89 77
    public static function methods(
90
        array $methods,
91
        string $pattern,
92
    ): self {
93 77
        return new self($methods, $pattern);
94
    }
95
96 23
    public function name(string $name): self
97
    {
98 23
        $route = clone $this;
99 23
        $route->name = $name;
100 23
        return $route;
101
    }
102
103 23
    public function pattern(string $pattern): self
104
    {
105 23
        $new = clone $this;
106 23
        $new->pattern = $pattern;
107 23
        return $new;
108
    }
109
110 9
    public function host(string $host): self
111
    {
112 9
        return $this->hosts($host);
113
    }
114
115 13
    public function hosts(string ...$hosts): self
116
    {
117 13
        $route = clone $this;
118
119 13
        $route->hosts = $this->processHosts($hosts);
120
121 13
        return $route;
122
    }
123
124
    /**
125
     * Marks route as override. When added it will replace existing route with the same name.
126
     */
127 4
    public function override(): self
128
    {
129 4
        $route = clone $this;
130 4
        $route->override = true;
131 4
        return $route;
132
    }
133
134
    /**
135
     * Parameter default values indexed by parameter names.
136
     *
137
     * @psalm-param array<string,null|Stringable|scalar> $defaults
138
     */
139 3
    public function defaults(array $defaults): self
140
    {
141 3
        $route = clone $this;
142 3
        $route->defaults = $this->processDefaults($defaults);
143 3
        return $route;
144
    }
145
146
    /**
147
     * Appends a handler middleware definition that should be invoked for a matched route.
148
     * First added handler will be executed first.
149
     */
150 23
    public function middleware(array|callable|string ...$middlewareDefinition): self
151
    {
152 23
        if ($this->actionAdded) {
153 1
            throw new RuntimeException('middleware() can not be used after action().');
154
        }
155 22
        $route = clone $this;
156 22
        array_push(
157 22
            $route->middlewares,
158 22
            ...array_values($middlewareDefinition)
159 22
        );
160 22
        $route->builtMiddlewares = [];
161 22
        return $route;
162
    }
163
164
    /**
165
     * Prepends a handler middleware definition that should be invoked for a matched route.
166
     * Last added handler will be executed first.
167
     */
168 23
    public function prependMiddleware(array|callable|string ...$middlewareDefinition): self
169
    {
170 23
        if (!$this->actionAdded) {
171 1
            throw new RuntimeException('prependMiddleware() can not be used before action().');
172
        }
173 22
        $route = clone $this;
174 22
        array_unshift(
175 22
            $route->middlewares,
176 22
            ...array_values($middlewareDefinition)
177 22
        );
178 22
        $route->builtMiddlewares = [];
179 22
        return $route;
180
    }
181
182
    /**
183
     * Appends action handler. It is a primary middleware definition that should be invoked last for a matched route.
184
     */
185 25
    public function action(array|callable|string $middlewareDefinition): self
186
    {
187 25
        $route = clone $this;
188 25
        $route->middlewares[] = $middlewareDefinition;
189 25
        $route->builtMiddlewares = [];
190 25
        $route->actionAdded = true;
191 25
        return $route;
192
    }
193
194
    /**
195
     * Excludes middleware from being invoked when action is handled.
196
     * It is useful to avoid invoking one of the parent group middleware for
197
     * a certain route.
198
     */
199 3
    public function disableMiddleware(mixed ...$middlewareDefinition): self
200
    {
201 3
        $route = clone $this;
202 3
        array_push(
203 3
            $route->disabledMiddlewares,
204 3
            ...array_values($middlewareDefinition)
205 3
        );
206 3
        $route->builtMiddlewares = [];
207 3
        return $route;
208
    }
209
210
    /**
211
     * @psalm-template T as string
212
     *
213
     * @psalm-param T $key
214
     *
215
     * @psalm-return (
216
     *   T is ('name'|'pattern') ? string :
217
     *       (T is 'host' ? string|null :
218
     *           (T is 'hosts' ? array<array-key, string> :
219
     *               (T is 'methods' ? array<array-key,string> :
220
     *                   (T is 'defaults' ? array<string,string> :
221
     *                       (T is ('override'|'hasMiddlewares') ? bool : mixed)
222
     *                   )
223
     *               )
224
     *           )
225
     *       )
226
     *    )
227
     */
228 51
    public function getData(string $key): mixed
229
    {
230 51
        return match ($key) {
231 51
            'name' => $this->name ??
232 32
                (implode(', ', $this->methods) . ' ' . implode('|', $this->hosts) . $this->pattern),
233 51
            'pattern' => $this->pattern,
234 51
            'host' => $this->hosts[0] ?? null,
235 51
            'hosts' => $this->hosts,
236 51
            'methods' => $this->methods,
237 51
            'defaults' => $this->defaults,
238 51
            'override' => $this->override,
239 51
            'hasMiddlewares' => $this->middlewares !== [],
240 51
            default => throw new InvalidArgumentException('Unknown data key: ' . $key),
241 51
        };
242
    }
243
244 4
    public function __toString(): string
245
    {
246 4
        $result = $this->name === null
247 1
            ? ''
248 3
            : '[' . $this->name . '] ';
249
250 4
        if ($this->methods !== []) {
251 4
            $result .= implode(',', $this->methods) . ' ';
252
        }
253
254 4
        if ($this->hosts) {
0 ignored issues
show
Bug Best Practice introduced by
The expression $this->hosts of type array is implicitly converted to a boolean; are you sure this is intended? If so, consider using ! empty($expr) instead to make it clear that you intend to check for an array without elements.

This check marks implicit conversions of arrays to boolean values in a comparison. While in PHP an empty array is considered to be equal (but not identical) to false, this is not always apparent.

Consider making the comparison explicit by using empty(..) or ! empty(...) instead.

Loading history...
255 2
            $quoted = array_map(static fn ($host) => preg_quote($host, '/'), $this->hosts);
256
257 2
            if (!preg_match('/' . implode('|', $quoted) . '/', $this->pattern)) {
258 2
                $result .= implode('|', $this->hosts);
259
            }
260
        }
261
262 4
        $result .= $this->pattern;
263
264 4
        return $result;
265
    }
266
267 1
    public function __debugInfo()
268
    {
269 1
        return [
270 1
            'name' => $this->name,
271 1
            'methods' => $this->methods,
272 1
            'pattern' => $this->pattern,
273 1
            'hosts' => $this->hosts,
274 1
            'defaults' => $this->defaults,
275 1
            'override' => $this->override,
276 1
            'actionAdded' => $this->actionAdded,
277 1
            'middlewareDefinitions' => $this->middlewares,
278 1
            'builtMiddlewares' => $this->builtMiddlewares,
279 1
            'disabledMiddlewareDefinitions' => $this->disabledMiddlewares,
280 1
        ];
281
    }
282
283 20
    public function getMiddlewares(): array
284
    {
285 20
        if ($this->builtMiddlewares !== []) {
286
            return $this->builtMiddlewares;
287
        }
288 20
        $result = $this->middlewares;
289
        /** @var mixed $definition */
290 20
        foreach ($result as $index => $definition) {
291 19
            if (in_array($definition, $this->disabledMiddlewares, true)) {
292 1
                unset($result[$index]);
293
            }
294
        }
295 20
        $this->builtMiddlewares = $result;
296
297 20
        return $result;
298
    }
299
300 78
    private function processDefaults(array $defaults): array
301
    {
302 78
        return array_map('\strval', $defaults);
303
    }
304
305 78
    private function processHosts(array $hosts): array
306
    {
307 78
        $result = [];
308 78
        foreach ($hosts as $host) {
309 14
            $host = rtrim($host, '/');
310
311 14
            if ($host !== '' && !in_array($host, $result, true)) {
312 13
                $result[] = $host;
313
            }
314
        }
315 78
        return $result;
316
    }
317
318 78
    private function processMethods(array $methods): array
319
    {
320 78
        $result = [];
321 78
        foreach ($methods as $method) {
322 78
            $method = strtoupper($method);
323 78
            if (!in_array($method, Method::ALL, true)) {
324
                throw new \InvalidArgumentException(
325
                    sprintf(
326
                        'Method "%s" is not supported. Possible methods: "%s".',
327
                        $method,
328
                        implode('", "', Method::ALL),
329
                    )
330
                );
331
            }
332 78
            $result[] = $method;
333
        }
334 78
        return $result;
335
    }
336
}
337