Passed
Pull Request — master (#163)
by
unknown
02:35 queued 25s
created

Route::__construct()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 5
Code Lines 3

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 4
CRAP Score 1

Importance

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