Passed
Pull Request — master (#157)
by
unknown
02:27
created

Route::__toString()   A

Complexity

Conditions 5
Paths 8

Size

Total Lines 17
Code Lines 9

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 10
CRAP Score 5

Importance

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