Passed
Pull Request — master (#163)
by Alexander
02:31
created

Route::hosts()   A

Complexity

Conditions 4
Paths 3

Size

Total Lines 14
Code Lines 7

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 8
CRAP Score 4

Importance

Changes 0
Metric Value
cc 4
eloc 7
nc 3
nop 1
dl 0
loc 14
ccs 8
cts 8
cp 1
crap 4
rs 10
c 0
b 0
f 0
1
<?php
2
3
declare(strict_types=1);
4
5
namespace Yiisoft\Router;
6
7
use InvalidArgumentException;
8
use RuntimeException;
9
use Stringable;
10
use Yiisoft\Http\Method;
11
use Yiisoft\Middleware\Dispatcher\MiddlewareDispatcher;
12
13
use function in_array;
14
15
/**
16
 * Route defines a mapping from URL to callback / name and vice versa.
17
 */
18
final class Route
19
{
20
    private ?string $name = null;
21
22
    /**
23
     * @var string[]
24
     */
25
    private array $methods;
26
27
    private string $pattern;
28
29
    /**
30
     * @var string[]
31
     */
32
    private array $hosts = [];
33
    private bool $override = false;
34
    private ?MiddlewareDispatcher $dispatcher;
35
    private bool $actionAdded = false;
36
37
    /**
38
     * @var array[]|callable[]|string[]
39
     */
40
    private array $middlewareDefinitions = [];
41
42
    private array $disabledMiddlewareDefinitions = [];
43
44
    /**
45
     * @var string[]
46
     * @psalm-var array<string,string>
47
     */
48
    private array $defaults = [];
49
50
    /**
51
     * @param string[] $methods
52
     */
53 72
    private function __construct(array $methods, string $pattern, ?MiddlewareDispatcher $dispatcher = null)
54
    {
55 72
        $this->methods = $methods;
56 72
        $this->pattern = $pattern;
57 72
        $this->dispatcher = $dispatcher;
58
    }
59
60
    /**
61
     * @psalm-assert MiddlewareDispatcher $this->dispatcher
62
     */
63 13
    public function injectDispatcher(MiddlewareDispatcher $dispatcher): void
64
    {
65 13
        $this->dispatcher = $dispatcher;
66
    }
67
68
    /**
69
     * @return self
70
     */
71 5
    public function withDispatcher(MiddlewareDispatcher $dispatcher): self
72
    {
73 5
        $route = clone $this;
74 5
        $route->dispatcher = $dispatcher;
75 5
        return $route;
76
    }
77
78
    /**
79
     * @param string $pattern
80
     * @param MiddlewareDispatcher|null $dispatcher
81
     *
82
     * @return self
83
     */
84 61
    public static function get(string $pattern, ?MiddlewareDispatcher $dispatcher = null): self
85
    {
86 61
        return self::methods([Method::GET], $pattern, $dispatcher);
87
    }
88
89
    /**
90
     * @param string $pattern
91
     * @param MiddlewareDispatcher|null $dispatcher
92
     *
93
     * @return self
94
     */
95 9
    public static function post(string $pattern, ?MiddlewareDispatcher $dispatcher = null): self
96
    {
97 9
        return self::methods([Method::POST], $pattern, $dispatcher);
98
    }
99
100
    /**
101
     * @param string $pattern
102
     * @param MiddlewareDispatcher|null $dispatcher
103
     *
104
     * @return self
105
     */
106 4
    public static function put(string $pattern, ?MiddlewareDispatcher $dispatcher = null): self
107
    {
108 4
        return self::methods([Method::PUT], $pattern, $dispatcher);
109
    }
110
111
    /**
112
     * @param string $pattern
113
     * @param MiddlewareDispatcher|null $dispatcher
114
     *
115
     * @return self
116
     */
117 1
    public static function delete(string $pattern, ?MiddlewareDispatcher $dispatcher = null): self
118
    {
119 1
        return self::methods([Method::DELETE], $pattern, $dispatcher);
120
    }
121
122
    /**
123
     * @param string $pattern
124
     * @param MiddlewareDispatcher|null $dispatcher
125
     *
126
     * @return self
127
     */
128 1
    public static function patch(string $pattern, ?MiddlewareDispatcher $dispatcher = null): self
129
    {
130 1
        return self::methods([Method::PATCH], $pattern, $dispatcher);
131
    }
132
133
    /**
134
     * @param string $pattern
135
     * @param MiddlewareDispatcher|null $dispatcher
136
     *
137
     * @return self
138
     */
139 1
    public static function head(string $pattern, ?MiddlewareDispatcher $dispatcher = null): self
140
    {
141 1
        return self::methods([Method::HEAD], $pattern, $dispatcher);
142
    }
143
144
    /**
145
     * @param string $pattern
146
     * @param MiddlewareDispatcher|null $dispatcher
147
     *
148
     * @return self
149
     */
150 7
    public static function options(string $pattern, ?MiddlewareDispatcher $dispatcher = null): self
151
    {
152 7
        return self::methods([Method::OPTIONS], $pattern, $dispatcher);
153
    }
154
155
    /**
156
     * @param string[] $methods
157
     * @param string $pattern
158
     * @param MiddlewareDispatcher|null $dispatcher
159
     *
160
     * @return self
161
     */
162 72
    public static function methods(
163
        array $methods,
164
        string $pattern,
165
        ?MiddlewareDispatcher $dispatcher = null
166
    ): self {
167 72
        return new self($methods, $pattern, $dispatcher);
168
    }
169
170
    /**
171
     * @return self
172
     */
173 23
    public function name(string $name): self
174
    {
175 23
        $route = clone $this;
176 23
        $route->name = $name;
177 23
        return $route;
178
    }
179
180
    /**
181
     * @return self
182
     */
183 20
    public function pattern(string $pattern): self
184
    {
185 20
        $new = clone $this;
186 20
        $new->pattern = $pattern;
187 20
        return $new;
188
    }
189
190
    /**
191
     * @return self
192
     */
193 7
    public function host(string $host): self
194
    {
195 7
        return $this->hosts($host);
196
    }
197
198
    /**
199
     * @return self
200
     */
201 8
    public function hosts(string ...$hosts): self
202
    {
203 8
        $route = clone $this;
204 8
        $route->hosts = [];
205
206 8
        foreach ($hosts as $host) {
207 8
            $host = rtrim($host, '/');
208
209 8
            if ($host !== '' && !in_array($host, $route->hosts, true)) {
210 8
                $route->hosts[] = $host;
211
            }
212
        }
213
214 8
        return $route;
215
    }
216
217
    /**
218
     * Marks route as override. When added it will replace existing route with the same name.
219
     *
220
     * @return self
221
     */
222 3
    public function override(): self
223
    {
224 3
        $route = clone $this;
225 3
        $route->override = true;
226 3
        return $route;
227
    }
228
229
    /**
230
     * Parameter default values indexed by parameter names.
231
     *
232
     * @param array $defaults
233
     *
234
     * @psalm-param array<string,null|Stringable|scalar> $defaults
235
     *
236
     * @return self
237
     */
238 2
    public function defaults(array $defaults): self
239
    {
240 2
        $route = clone $this;
241 2
        $route->defaults = array_map('\strval', $defaults);
242 2
        return $route;
243
    }
244
245
    /**
246
     * Appends a handler middleware definition that should be invoked for a matched route.
247
     * First added handler will be executed first.
248
     *
249
     * @param array|callable|string ...$middlewareDefinition
250
     *
251
     * @return self
252
     */
253 17
    public function middleware(...$middlewareDefinition): self
254
    {
255 17
        if ($this->actionAdded) {
256 1
            throw new RuntimeException('middleware() can not be used after action().');
257
        }
258 16
        $route = clone $this;
259 16
        array_push(
260 16
            $route->middlewareDefinitions,
261 16
            ...array_values($middlewareDefinition)
262
        );
263 16
        return $route;
264
    }
265
266
    /**
267
     * Prepends a handler middleware definition that should be invoked for a matched route.
268
     * Last added handler will be executed first.
269
     *
270
     * @param array|callable|string ...$middlewareDefinition
271
     *
272
     * @return self
273
     */
274 16
    public function prependMiddleware(...$middlewareDefinition): self
275
    {
276 16
        if (!$this->actionAdded) {
277 1
            throw new RuntimeException('prependMiddleware() can not be used before action().');
278
        }
279 15
        $route = clone $this;
280 15
        array_unshift(
281 15
            $route->middlewareDefinitions,
282 15
            ...array_values($middlewareDefinition)
283
        );
284 15
        return $route;
285
    }
286
287
    /**
288
     * Appends action handler. It is a primary middleware definition that should be invoked last for a matched route.
289
     *
290
     * @param array|callable|string $middlewareDefinition
291
     *
292
     * @return self
293
     */
294 19
    public function action($middlewareDefinition): self
295
    {
296 19
        $route = clone $this;
297 19
        $route->middlewareDefinitions[] = $middlewareDefinition;
298 19
        $route->actionAdded = true;
299 19
        return $route;
300
    }
301
302
    /**
303
     * Excludes middleware from being invoked when action is handled.
304
     * It is useful to avoid invoking one of the parent group middleware for
305
     * a certain route.
306
     *
307
     * @param mixed ...$middlewareDefinition
308
     *
309
     * @return self
310
     */
311 2
    public function disableMiddleware(...$middlewareDefinition): self
312
    {
313 2
        $route = clone $this;
314 2
        array_push(
315 2
            $route->disabledMiddlewareDefinitions,
316 2
            ...array_values($middlewareDefinition)
317
        );
318 2
        return $route;
319
    }
320
321
    /**
322
     * @param string $key
323
     *
324
     * @return mixed
325
     *
326
     * @psalm-template T as string
327
     * @psalm-param T $key
328
     * @psalm-return (
329
     *   T is ('name'|'pattern') ? string :
330
     *       (T is 'host' ? string|null :
331
     *           (T is 'hosts' ? array<array-key, string> :
332
     *               (T is 'methods' ? array<array-key,string> :
333
     *                   (T is 'defaults' ? array<string,string> :
334
     *                       (T is ('override'|'hasMiddlewares'|'hasDispatcher') ? bool :
335
     *                           (T is 'dispatcherWithMiddlewares' ? MiddlewareDispatcher : mixed)
336
     *                       )
337
     *                   )
338
     *               )
339
     *           )
340
     *       )
341
     *    )
342
     */
343 54
    public function getData(string $key)
344
    {
345 54
        switch ($key) {
346 54
            case 'name':
347 26
                return $this->name ??
348 26
                    (implode(', ', $this->methods) . ' ' . implode('|', $this->hosts) . $this->pattern);
349 51
            case 'pattern':
350 21
                return $this->pattern;
351 49
            case 'host':
352
                /** @var string $this->hosts[0] */
353 8
                return $this->hosts[0] ?? null;
354 47
            case 'hosts':
355 2
                return $this->hosts;
356 45
            case 'methods':
357 28
                return $this->methods;
358 36
            case 'defaults':
359 1
                return $this->defaults;
360 35
            case 'override':
361 4
                return $this->override;
362 34
            case 'dispatcherWithMiddlewares':
363 19
                return $this->getDispatcherWithMiddlewares();
364 28
            case 'hasMiddlewares':
365 21
                return $this->middlewareDefinitions !== [];
366 12
            case 'hasDispatcher':
367 11
                return $this->dispatcher !== null;
368
            default:
369 1
                throw new InvalidArgumentException('Unknown data key: ' . $key);
370
        }
371
    }
372
373 3
    public function __toString(): string
374
    {
375 3
        $result = '';
376
377 3
        if ($this->name !== null) {
378 2
            $result .= '[' . $this->name . '] ';
379
        }
380
381 3
        if ($this->methods !== []) {
382 3
            $result .= implode(',', $this->methods) . ' ';
383
        }
384
385 3
        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...
386 1
            $quoted = array_map(static fn ($host) => preg_quote($host), $this->hosts);
387
388 1
            if (!preg_match('/' . implode('|', $quoted) . '/', $this->pattern)) {
389 1
                $result .= implode('|', $this->hosts);
390
            }
391
        }
392
393 3
        $result .= $this->pattern;
394
395 3
        return $result;
396
    }
397
398 1
    public function __debugInfo()
399
    {
400
        return [
401 1
            'name' => $this->name,
402 1
            'methods' => $this->methods,
403 1
            'pattern' => $this->pattern,
404 1
            'hosts' => $this->hosts,
405 1
            'defaults' => $this->defaults,
406 1
            'override' => $this->override,
407 1
            'actionAdded' => $this->actionAdded,
408 1
            'middlewareDefinitions' => $this->middlewareDefinitions,
409 1
            'disabledMiddlewareDefinitions' => $this->disabledMiddlewareDefinitions,
410 1
            'middlewareDispatcher' => $this->dispatcher,
411
        ];
412
    }
413
414 19
    private function getDispatcherWithMiddlewares(): MiddlewareDispatcher
415
    {
416 19
        if ($this->dispatcher === null) {
417 1
            throw new RuntimeException(sprintf('There is no dispatcher in the route %s.', $this->getData('name')));
418
        }
419
420
        // Don't add middlewares to dispatcher if we did it earlier.
421
        // This improves performance in event-loop applications.
422 18
        if ($this->dispatcher->hasMiddlewares()) {
423 1
            return $this->dispatcher;
424
        }
425
426
        /** @var mixed $definition */
427 17
        foreach ($this->middlewareDefinitions as $index => $definition) {
428 16
            if (in_array($definition, $this->disabledMiddlewareDefinitions, true)) {
429 1
                unset($this->middlewareDefinitions[$index]);
430
            }
431
        }
432
433 17
        return $this->dispatcher = $this->dispatcher->withMiddlewares($this->middlewareDefinitions);
434
    }
435
}
436