Passed
Pull Request — master (#196)
by Alexander
05:57 queued 03:03
created

Group::assertHosts()   A

Complexity

Conditions 3
Paths 3

Size

Total Lines 5
Code Lines 3

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 2
CRAP Score 4.125

Importance

Changes 0
Metric Value
cc 3
eloc 3
nc 3
nop 1
dl 0
loc 5
ccs 2
cts 4
cp 0.5
crap 4.125
rs 10
c 0
b 0
f 0
1
<?php
2
3
declare(strict_types=1);
4
5
namespace Yiisoft\Router;
6
7
use Attribute;
8
use InvalidArgumentException;
9
use RuntimeException;
10
11
use function in_array;
12
13
#[Attribute(Attribute::TARGET_CLASS)]
14
final class Group
15
{
16
    /**
17
     * @var Group[]|Route[]
18
     */
19
    private array $routes = [];
20
    private bool $routesAdded = false;
21
    private bool $middlewareAdded = false;
22
    private array $builtMiddlewares = [];
23
    /**
24
     * @var array|callable|string|null Middleware definition for CORS requests.
25
     */
26
    private $corsMiddleware;
27
    /**
28
     * @var string[]
29
     */
30
    private array $hosts = [];
31
    /**
32
     * @var array[]|callable[]|string[]
33
     */
34
    private array $middlewares = [];
35
36
    /**
37
     * @param array $disabledMiddlewares Excludes middleware from being invoked when action is handled.
38
     * It is useful to avoid invoking one of the parent group middleware for
39
     * a certain route.
40
     */
41 36
    public function __construct(
42
        private ?string $prefix = null,
43
        array $middlewares = [],
44
        array $hosts = [],
45
        private ?string $namePrefix = null,
46
        private array $disabledMiddlewares = [],
47
        array|callable|string|null $corsMiddleware = null
48
    ) {
49 36
        $this->assertMiddlewares($middlewares);
50 36
        $this->assertHosts($hosts);
51 36
        $this->middlewares = $middlewares;
52 36
        $this->hosts = $hosts;
53 36
        $this->corsMiddleware = $corsMiddleware;
54
    }
55
56
    /**
57
     * Create a new group instance.
58
     *
59
     * @param string|null $prefix URL prefix to prepend to all routes of the group.
60
     */
61 35
    public static function create(
62
        ?string $prefix = null,
63
        array $middlewares = [],
64
        array $hosts = [],
65
        ?string $namePrefix = null,
66
        array $disabledMiddlewares = [],
67
        array|callable|string|null $corsMiddleware = null
68
    ): self {
69 35
        return new self(
70 35
            prefix: $prefix,
71 35
            middlewares: $middlewares,
72 35
            hosts: $hosts,
73 35
            namePrefix: $namePrefix,
74 35
            disabledMiddlewares: $disabledMiddlewares,
75 35
            corsMiddleware: $corsMiddleware
76 35
        );
77
    }
78
79 27
    public function routes(self|Route ...$routes): self
80
    {
81 27
        if ($this->middlewareAdded) {
82 1
            throw new RuntimeException('routes() can not be used after prependMiddleware().');
83
        }
84 26
        $new = clone $this;
85 26
        $new->routes = $routes;
86 26
        $new->routesAdded = true;
87
88 26
        return $new;
89
    }
90
91
    /**
92
     * Adds a middleware definition that handles CORS requests.
93
     * If set, routes for {@see Method::OPTIONS} request will be added automatically.
94
     *
95
     * @param array|callable|string|null $middlewareDefinition Middleware definition for CORS requests.
96
     */
97 8
    public function withCors(array|callable|string|null $middlewareDefinition): self
98
    {
99 8
        $group = clone $this;
100 8
        $group->corsMiddleware = $middlewareDefinition;
101
102 8
        return $group;
103
    }
104
105
    /**
106
     * Appends a handler middleware definition that should be invoked for a matched route.
107
     * First added handler will be executed first.
108
     */
109 12
    public function middleware(array|callable|string ...$middlewareDefinition): self
110
    {
111 12
        if ($this->routesAdded) {
112 1
            throw new RuntimeException('middleware() can not be used after routes().');
113
        }
114 11
        $new = clone $this;
115 11
        array_push(
116 11
            $new->middlewares,
117 11
            ...array_values($middlewareDefinition)
118 11
        );
119 11
        $new->builtMiddlewares = [];
120 11
        return $new;
121
    }
122
123
    /**
124
     * Prepends a handler middleware definition that should be invoked for a matched route.
125
     * First added handler will be executed last.
126
     */
127 26
    public function prependMiddleware(array|callable|string ...$middlewareDefinition): self
128
    {
129 26
        $new = clone $this;
130 26
        array_unshift(
131 26
            $new->middlewares,
132 26
            ...array_values($middlewareDefinition)
133 26
        );
134 26
        $new->middlewareAdded = true;
135 26
        $new->builtMiddlewares = [];
136 26
        return $new;
137
    }
138
139 4
    public function namePrefix(string $namePrefix): self
140
    {
141 4
        $new = clone $this;
142 4
        $new->namePrefix = $namePrefix;
143 4
        return $new;
144
    }
145
146 2
    public function host(string $host): self
147
    {
148 2
        return $this->hosts($host);
149
    }
150
151 5
    public function hosts(string ...$hosts): self
152
    {
153 5
        $new = clone $this;
154
155 5
        foreach ($hosts as $host) {
156 4
            $host = rtrim($host, '/');
157
158 4
            if ($host !== '' && !in_array($host, $new->hosts, true)) {
159 4
                $new->hosts[] = $host;
160
            }
161
        }
162
163 5
        return $new;
164
    }
165
166
    /**
167
     * Excludes middleware from being invoked when action is handled.
168
     * It is useful to avoid invoking one of the parent group middleware for
169
     * a certain route.
170
     */
171 3
    public function disableMiddleware(mixed ...$middlewareDefinition): self
172
    {
173 3
        $new = clone $this;
174 3
        array_push(
175 3
            $new->disabledMiddlewares,
176 3
            ...array_values($middlewareDefinition),
177 3
        );
178 3
        $new->builtMiddlewares = [];
179 3
        return $new;
180
    }
181
182
    /**
183
     * @psalm-template T as string
184
     *
185
     * @psalm-param T $key
186
     *
187
     * @psalm-return (
188
     *   T is ('prefix'|'namePrefix'|'host') ? string|null :
189
     *   (T is 'routes' ? Group[]|Route[] :
190
     *     (T is 'hosts' ? array<array-key, string> :
191
     *       (T is 'hasCorsMiddleware' ? bool :
192
     *         (T is 'middlewares' ? list<array|callable|string> :
193
     *           (T is 'corsMiddleware' ? array|callable|string|null : mixed)
194
     *         )
195
     *       )
196
     *     )
197
     *   )
198
     * )
199
     */
200 32
    public function getData(string $key): mixed
201
    {
202 32
        return match ($key) {
203 32
            'prefix' => $this->prefix,
204 32
            'namePrefix' => $this->namePrefix,
205 32
            'host' => $this->hosts[0] ?? null,
206 32
            'hosts' => $this->hosts,
207 32
            'corsMiddleware' => $this->corsMiddleware,
208 32
            'routes' => $this->routes,
209 32
            'hasCorsMiddleware' => $this->corsMiddleware !== null,
210 32
            'middlewares' => $this->getBuiltMiddlewares(),
211 32
            default => throw new InvalidArgumentException('Unknown data key: ' . $key),
212 32
        };
213
    }
214
215 22
    private function getBuiltMiddlewares(): array
216
    {
217 22
        if (!empty($this->builtMiddlewares)) {
218 5
            return $this->builtMiddlewares;
219
        }
220
221 22
        $builtMiddlewares = $this->middlewares;
222
223
        /** @var mixed $definition */
224 22
        foreach ($builtMiddlewares as $index => $definition) {
225 12
            if (in_array($definition, $this->disabledMiddlewares, true)) {
226 2
                unset($builtMiddlewares[$index]);
227
            }
228
        }
229
230 22
        return $this->builtMiddlewares = array_values($builtMiddlewares);
231
    }
232
233
    /**
234
     * @psalm-assert array<string> $hosts
235
     */
236 36
    private function assertHosts(array $hosts): void
237
    {
238 36
        foreach ($hosts as $host) {
239
            if (!is_string($host)) {
240
                throw new \InvalidArgumentException('Invalid hosts provided, list of string expected.');
241
            }
242
        }
243
    }
244
245
    /**
246
     * @psalm-assert array<array|callable|string> $middlewares
247
     */
248 36
    private function assertMiddlewares(array $middlewares): void
249
    {
250
        /** @var mixed $middleware */
251 36
        foreach ($middlewares as $middleware) {
252
            if (is_string($middleware)) {
253
                continue;
254
            }
255
256
            if (is_callable($middleware) || is_array($middleware)) {
257
                continue;
258
            }
259
260
            throw new \InvalidArgumentException(
261
                'Invalid middlewares provided, list of string or array or callable expected.'
262
            );
263
        }
264
    }
265
}
266