Passed
Pull Request — master (#163)
by
unknown
02:42
created

Route::methods()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 6
Code Lines 1

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 2
CRAP Score 1

Importance

Changes 3
Bugs 1 Features 0
Metric Value
cc 1
eloc 1
nc 1
nop 3
dl 0
loc 6
ccs 2
cts 2
cp 1
crap 1
rs 10
c 3
b 1
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 = [];
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 ...$hosts): self
190
    {
191 8
        $route = clone $this;
192 8
        $route->hosts = [];
193
194 8
        foreach ($hosts as $host) {
195 8
            $host = rtrim($host, '/');
196
197 8
            if (!in_array($host, $route->hosts, true)) {
198 8
                $route->hosts[] = $host;
199
            }
200
        }
201
202 8
        return $route;
203
    }
204
205 1
    public function isMultiHost(): bool
206
    {
207 1
        return isset($this->hosts[1]);
208
    }
209
210
    /**
211
     * Marks route as override. When added it will replace existing route with the same name.
212
     *
213
     * @return self
214
     */
215 3
    public function override(): self
216
    {
217 3
        $route = clone $this;
218 3
        $route->override = true;
219 3
        return $route;
220
    }
221
222
    /**
223
     * Parameter default values indexed by parameter names.
224
     *
225
     * @param array $defaults
226
     *
227
     * @psalm-param array<string,null|Stringable|scalar> $defaults
228
     *
229
     * @return self
230
     */
231 2
    public function defaults(array $defaults): self
232
    {
233 2
        $route = clone $this;
234 2
        $route->defaults = array_map('\strval', $defaults);
235 2
        return $route;
236
    }
237
238
    /**
239
     * Appends a handler middleware definition that should be invoked for a matched route.
240
     * First added handler will be executed first.
241
     *
242
     * @param array|callable|string ...$middlewareDefinition
243
     *
244
     * @return self
245
     */
246 17
    public function middleware(...$middlewareDefinition): self
247
    {
248 17
        if ($this->actionAdded) {
249 1
            throw new RuntimeException('middleware() can not be used after action().');
250
        }
251 16
        $route = clone $this;
252 16
        array_push(
253 16
            $route->middlewareDefinitions,
254 16
            ...array_values($middlewareDefinition)
255
        );
256 16
        return $route;
257
    }
258
259
    /**
260
     * Prepends a handler middleware definition that should be invoked for a matched route.
261
     * Last added handler will be executed first.
262
     *
263
     * @param array|callable|string ...$middlewareDefinition
264
     *
265
     * @return self
266
     */
267 16
    public function prependMiddleware(...$middlewareDefinition): self
268
    {
269 16
        if (!$this->actionAdded) {
270 1
            throw new RuntimeException('prependMiddleware() can not be used before action().');
271
        }
272 15
        $route = clone $this;
273 15
        array_unshift(
274 15
            $route->middlewareDefinitions,
275 15
            ...array_values($middlewareDefinition)
276
        );
277 15
        return $route;
278
    }
279
280
    /**
281
     * Appends action handler. It is a primary middleware definition that should be invoked last for a matched route.
282
     *
283
     * @param array|callable|string $middlewareDefinition
284
     *
285
     * @return self
286
     */
287 19
    public function action($middlewareDefinition): self
288
    {
289 19
        $route = clone $this;
290 19
        $route->middlewareDefinitions[] = $middlewareDefinition;
291 19
        $route->actionAdded = true;
292 19
        return $route;
293
    }
294
295
    /**
296
     * Excludes middleware from being invoked when action is handled.
297
     * It is useful to avoid invoking one of the parent group middleware for
298
     * a certain route.
299
     *
300
     * @param mixed ...$middlewareDefinition
301
     *
302
     * @return self
303
     */
304 2
    public function disableMiddleware(...$middlewareDefinition): self
305
    {
306 2
        $route = clone $this;
307 2
        array_push(
308 2
            $route->disabledMiddlewareDefinitions,
309 2
            ...array_values($middlewareDefinition)
310
        );
311 2
        return $route;
312
    }
313
314
    /**
315
     * @param string $key
316
     *
317
     * @return mixed
318
     *
319
     * @psalm-template T as string
320
     * @psalm-param T $key
321
     * @psalm-return (
322
     *   T is ('name'|'pattern') ? string :
323
     *     (T is 'host' ? string|null :
324
     *        (T is 'hosts' ? string|null :
325
     *           (T is 'methods' ? array<array-key,string> :
326
     *              (T is 'defaults' ? array<string,string> :
327
     *                 (T is ('override'|'hasMiddlewares'|'hasDispatcher') ? bool :
328
     *                    (T is 'dispatcherWithMiddlewares' ? MiddlewareDispatcher : mixed)
329
     *                 )
330
     *              )
331
     *           )
332
     *        )
333
     *     )
334
     * )
335
     */
336 56
    public function getData(string $key)
337
    {
338 56
        switch ($key) {
339 56
            case 'name':
340 26
                return $this->name ??
341 26
                    (implode(', ', $this->methods) . ' ' . (string) $this->getData('hosts') . $this->pattern);
342 54
            case 'pattern':
343 21
                return $this->pattern;
344 52
            case 'host':
345
                /** @var string $this->hosts[0] */
346 9
                return $this->hosts[0] ?? null;
347 49
            case 'hosts':
348
                /** @var array<array-key, string> $this->hosts */
349 12
                return $this->hosts ? implode('|', $this->hosts) : null;
350 45
            case 'methods':
351 28
                return $this->methods;
352 36
            case 'defaults':
353 1
                return $this->defaults;
354 35
            case 'override':
355 4
                return $this->override;
356 34
            case 'dispatcherWithMiddlewares':
357 19
                return $this->getDispatcherWithMiddlewares();
358 28
            case 'hasMiddlewares':
359 21
                return $this->middlewareDefinitions !== [];
360 12
            case 'hasDispatcher':
361 11
                return $this->dispatcher !== null;
362
            default:
363 1
                throw new InvalidArgumentException('Unknown data key: ' . $key);
364
        }
365
    }
366
367 3
    public function __toString(): string
368
    {
369 3
        $result = '';
370 3
        $hosts = $this->getData('hosts');
371
372 3
        if ($this->name !== null) {
373 2
            $result .= '[' . $this->name . '] ';
374
        }
375
376 3
        if ($this->methods !== []) {
377 3
            $result .= implode(',', $this->methods) . ' ';
378
        }
379 3
        if ($hosts !== null && !preg_match('/' . $hosts . '/', $this->pattern)) {
380 1
            $result .= $hosts;
381
        }
382 3
        $result .= $this->pattern;
383
384 3
        return $result;
385
    }
386
387 1
    public function __debugInfo()
388
    {
389
        return [
390 1
            'name' => $this->name,
391 1
            'methods' => $this->methods,
392 1
            'pattern' => $this->pattern,
393 1
            'host' => $this->getData('host'),
394 1
            'hosts' => $this->hosts,
395 1
            'defaults' => $this->defaults,
396 1
            'override' => $this->override,
397 1
            'actionAdded' => $this->actionAdded,
398 1
            'middlewareDefinitions' => $this->middlewareDefinitions,
399 1
            'disabledMiddlewareDefinitions' => $this->disabledMiddlewareDefinitions,
400 1
            'middlewareDispatcher' => $this->dispatcher,
401
        ];
402
    }
403
404 19
    private function getDispatcherWithMiddlewares(): MiddlewareDispatcher
405
    {
406 19
        if ($this->dispatcher === null) {
407 1
            throw new RuntimeException(sprintf('There is no dispatcher in the route %s.', $this->getData('name')));
408
        }
409
410
        // Don't add middlewares to dispatcher if we did it earlier.
411
        // This improves performance in event-loop applications.
412 18
        if ($this->dispatcher->hasMiddlewares()) {
413 1
            return $this->dispatcher;
414
        }
415
416
        /** @var mixed $definition */
417 17
        foreach ($this->middlewareDefinitions as $index => $definition) {
418 16
            if (in_array($definition, $this->disabledMiddlewareDefinitions, true)) {
419 1
                unset($this->middlewareDefinitions[$index]);
420
            }
421
        }
422
423 17
        return $this->dispatcher = $this->dispatcher->withMiddlewares($this->middlewareDefinitions);
424
    }
425
}
426