Passed
Pull Request — master (#194)
by Alexander
02:33
created

Route::getMiddlewares()   A

Complexity

Conditions 3
Paths 3

Size

Total Lines 12
Code Lines 6

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 7
CRAP Score 3

Importance

Changes 0
Metric Value
cc 3
eloc 6
c 0
b 0
f 0
nc 3
nop 0
dl 0
loc 12
ccs 7
cts 7
cp 1
crap 3
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 const POSSIBLE_METHODS = [
22
        Method::GET,
23
        Method::POST,
24
        Method::PUT,
25
        Method::DELETE,
26
        Method::PATCH,
27
        Method::HEAD,
28
        Method::OPTIONS,
29
    ];
30
31
    private bool $actionAdded = false;
32
    private array $builtMiddlewares = [];
33
34
    /**
35
     * @param string[] $methods
36
     * @param string[] $hosts
37
     * @param array[]|callable[]|string[] $middlewares
38
     * @param array<string,string> $defaults
39
     */
40 73
    public function __construct(
41
        private array $methods,
42
        private string $pattern,
43
        private ?string $name = null,
44
        private array $middlewares = [],
45
        /**
46
         * Excludes middleware from being invoked when action is handled.
47
         * It is useful to avoid invoking one of the parent group middleware for
48
         * a certain route.
49
         */
50
        private array $disabledMiddlewareDefinitions = [],
51
        private array $hosts = [],
52
        /**
53
         * Marks route as override. When added it will replace existing route with the same name.
54
         */
55
        private bool $override = false,
56
        /**
57
         * Parameter default values indexed by parameter names.
58
         *
59
         * @psalm-param array<string,null|Stringable|scalar> $defaults
60
         */
61
        private array $defaults = [],
62
    ) {
63 73
        $this->methods = $this->processMethods($this->methods);
64 73
        $this->hosts = $this->processHosts($this->hosts);
65 73
        $this->defaults = $this->processDefaults($this->defaults);
66
    }
67
68 60
    public static function get(string $pattern): self
69
    {
70 60
        return self::methods([Method::GET], $pattern);
71
    }
72
73 9
    public static function post(string $pattern): self
74
    {
75 9
        return self::methods([Method::POST], $pattern);
76
    }
77
78 4
    public static function put(string $pattern): self
79
    {
80 4
        return self::methods([Method::PUT], $pattern);
81
    }
82
83 1
    public static function delete(string $pattern): self
84
    {
85 1
        return self::methods([Method::DELETE], $pattern);
86
    }
87
88 1
    public static function patch(string $pattern): self
89
    {
90 1
        return self::methods([Method::PATCH], $pattern);
91
    }
92
93 1
    public static function head(string $pattern): self
94
    {
95 1
        return self::methods([Method::HEAD], $pattern);
96
    }
97
98 7
    public static function options(string $pattern): self
99
    {
100 7
        return self::methods([Method::OPTIONS], $pattern);
101
    }
102
103
    /**
104
     * @param string[] $methods
105
     */
106 72
    public static function methods(
107
        array $methods,
108
        string $pattern,
109
    ): self {
110 72
        return new self($methods, $pattern);
111
    }
112
113 23
    public function name(string $name): self
114
    {
115 23
        $route = clone $this;
116 23
        $route->name = $name;
117 23
        return $route;
118
    }
119
120 21
    public function pattern(string $pattern): self
121
    {
122 21
        $new = clone $this;
123 21
        $new->pattern = $pattern;
124 21
        return $new;
125
    }
126
127 8
    public function host(string $host): self
128
    {
129 8
        return $this->hosts($host);
130
    }
131
132 12
    public function hosts(string ...$hosts): self
133
    {
134 12
        $route = clone $this;
135
136 12
        $route->hosts = $this->processHosts($hosts);
137
138 12
        return $route;
139
    }
140
141
    /**
142
     * Marks route as override. When added it will replace existing route with the same name.
143
     */
144 4
    public function override(): self
145
    {
146 4
        $route = clone $this;
147 4
        $route->override = true;
148 4
        return $route;
149
    }
150
151
    /**
152
     * Parameter default values indexed by parameter names.
153
     *
154
     * @psalm-param array<string,null|Stringable|scalar> $defaults
155
     */
156 3
    public function defaults(array $defaults): self
157
    {
158 3
        $route = clone $this;
159 3
        $route->defaults = $this->processDefaults($defaults);
160 3
        return $route;
161
    }
162
163
    /**
164
     * Appends a handler middleware definition that should be invoked for a matched route.
165
     * First added handler will be executed first.
166
     */
167 18
    public function middleware(array|callable|string ...$middlewareDefinition): self
168
    {
169 18
        if ($this->actionAdded) {
170 1
            throw new RuntimeException('middleware() can not be used after action().');
171
        }
172 17
        $route = clone $this;
173 17
        array_push(
174 17
            $route->middlewares,
175 17
            ...array_values($middlewareDefinition)
176 17
        );
177 17
        $route->builtMiddlewares = [];
178 17
        return $route;
179
    }
180
181
    /**
182
     * Prepends a handler middleware definition that should be invoked for a matched route.
183
     * Last added handler will be executed first.
184
     */
185 18
    public function prependMiddleware(array|callable|string ...$middlewareDefinition): self
186
    {
187 18
        if (!$this->actionAdded) {
188 1
            throw new RuntimeException('prependMiddleware() can not be used before action().');
189
        }
190 17
        $route = clone $this;
191 17
        array_unshift(
192 17
            $route->middlewares,
193 17
            ...array_values($middlewareDefinition)
194 17
        );
195 17
        $route->builtMiddlewares = [];
196 17
        return $route;
197
    }
198
199
    /**
200
     * Appends action handler. It is a primary middleware definition that should be invoked last for a matched route.
201
     */
202 20
    public function action(array|callable|string $middlewareDefinition): self
203
    {
204 20
        $route = clone $this;
205 20
        $route->middlewares[] = $middlewareDefinition;
206 20
        $route->builtMiddlewares = [];
207 20
        $route->actionAdded = true;
208 20
        return $route;
209
    }
210
211
    /**
212
     * Excludes middleware from being invoked when action is handled.
213
     * It is useful to avoid invoking one of the parent group middleware for
214
     * a certain route.
215
     */
216 3
    public function disableMiddleware(mixed ...$middlewareDefinition): self
217
    {
218 3
        $route = clone $this;
219 3
        array_push(
220 3
            $route->disabledMiddlewareDefinitions,
221 3
            ...array_values($middlewareDefinition)
222 3
        );
223 3
        $route->builtMiddlewares = [];
224 3
        return $route;
225
    }
226
227
    /**
228
     * @psalm-template T as string
229
     *
230
     * @psalm-param T $key
231
     *
232
     * @psalm-return (
233
     *   T is ('name'|'pattern') ? string :
234
     *       (T is 'host' ? string|null :
235
     *           (T is 'hosts' ? array<array-key, string> :
236
     *               (T is 'methods' ? array<array-key,string> :
237
     *                   (T is 'defaults' ? array<string,string> :
238
     *                       (T is ('override'|'hasMiddlewares') ? bool : mixed)
239
     *                   )
240
     *               )
241
     *           )
242
     *       )
243
     *    )
244
     */
245 46
    public function getData(string $key): mixed
246
    {
247 46
        return match ($key) {
248 46
            'name' => $this->name ??
249 27
                (implode(', ', $this->methods) . ' ' . implode('|', $this->hosts) . $this->pattern),
250 46
            'pattern' => $this->pattern,
251 46
            'host' => $this->hosts[0] ?? null,
252 46
            'hosts' => $this->hosts,
253 46
            'methods' => $this->methods,
254 46
            'defaults' => $this->defaults,
255 46
            'override' => $this->override,
256 46
            'hasMiddlewares' => $this->middlewares !== [],
257 46
            default => throw new InvalidArgumentException('Unknown data key: ' . $key),
258 46
        };
259
    }
260
261 4
    public function __toString(): string
262
    {
263 4
        $result = $this->name === null
264 1
            ? ''
265 3
            : '[' . $this->name . '] ';
266
267 4
        if ($this->methods !== []) {
268 4
            $result .= implode(',', $this->methods) . ' ';
269
        }
270
271 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...
272 2
            $quoted = array_map(static fn ($host) => preg_quote($host, '/'), $this->hosts);
273
274 2
            if (!preg_match('/' . implode('|', $quoted) . '/', $this->pattern)) {
275 2
                $result .= implode('|', $this->hosts);
276
            }
277
        }
278
279 4
        $result .= $this->pattern;
280
281 4
        return $result;
282
    }
283
284 1
    public function __debugInfo()
285
    {
286 1
        return [
287 1
            'name' => $this->name,
288 1
            'methods' => $this->methods,
289 1
            'pattern' => $this->pattern,
290 1
            'hosts' => $this->hosts,
291 1
            'defaults' => $this->defaults,
292 1
            'override' => $this->override,
293 1
            'actionAdded' => $this->actionAdded,
294 1
            'middlewareDefinitions' => $this->middlewares,
295 1
            'builtMiddlewares' => $this->builtMiddlewares,
296 1
            'disabledMiddlewareDefinitions' => $this->disabledMiddlewareDefinitions,
297 1
        ];
298
    }
299
300 16
    public function getMiddlewares(): array
301
    {
302 16
        $result = $this->middlewares;
303
        /** @var mixed $definition */
304 16
        foreach ($result as $index => $definition) {
305 15
            if (in_array($definition, $this->disabledMiddlewareDefinitions, true)) {
306 1
                unset($result[$index]);
307
            }
308
        }
309 16
        $this->builtMiddlewares = $result;
310
311 16
        return $result;
312
    }
313
314 73
    private function processDefaults(array $defaults): array
315
    {
316 73
        return array_map('\strval', $defaults);
317
    }
318
319 73
    private function processHosts(array $hosts): array
320
    {
321 73
        $result = [];
322 73
        foreach ($hosts as $host) {
323 13
            $host = rtrim($host, '/');
324
325 13
            if ($host !== '' && !in_array($host, $result, true)) {
326 12
                $result[] = $host;
327
            }
328
        }
329 73
        return $result;
330
    }
331
332 73
    private function processMethods(array $methods): array
333
    {
334 73
        $result = [];
335 73
        foreach ($methods as $method) {
336 73
            $method = strtoupper($method);
337 73
            if (!in_array($method, self::POSSIBLE_METHODS, true)) {
338
                throw new \InvalidArgumentException(
339
                    sprintf(
340
                        'Method "%s" is not supported. Possible methods: "%s".',
341
                        $method,
342
                        implode('", "', self::POSSIBLE_METHODS),
343
                    )
344
                );
345
            }
346 73
            $result[] = $method;
347
        }
348 73
        return $result;
349
    }
350
}
351