Passed
Pull Request — master (#196)
by Alexander
04:57 queued 02:26
created

Group   A

Complexity

Total Complexity 28

Size/Duplication

Total Lines 232
Duplicated Lines 0 %

Test Coverage

Coverage 100%

Importance

Changes 7
Bugs 1 Features 0
Metric Value
eloc 78
dl 0
loc 232
ccs 89
cts 89
cp 1
rs 10
c 7
b 1
f 0
wmc 28

14 Methods

Rating   Name   Duplication   Size   Complexity  
A __construct() 0 13 1
A assertMiddlewares() 0 10 5
A assertHosts() 0 5 3
A middleware() 0 12 2
A withCors() 0 6 1
A host() 0 3 1
A routes() 0 10 2
A getBuiltMiddlewares() 0 16 4
A create() 0 3 1
A getData() 0 12 1
A disableMiddleware() 0 9 1
A prependMiddleware() 0 10 1
A hosts() 0 13 4
A namePrefix() 0 5 1
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 38
    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 38
        $this->assertMiddlewares($middlewares);
50 37
        $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(?string $prefix = null): self
62
    {
63 35
        return new self($prefix);
64
    }
65
66 27
    public function routes(self|Route ...$routes): self
67
    {
68 27
        if ($this->middlewareAdded) {
69 1
            throw new RuntimeException('routes() can not be used after prependMiddleware().');
70
        }
71 26
        $new = clone $this;
72 26
        $new->routes = $routes;
73 26
        $new->routesAdded = true;
74
75 26
        return $new;
76
    }
77
78
    /**
79
     * Adds a middleware definition that handles CORS requests.
80
     * If set, routes for {@see Method::OPTIONS} request will be added automatically.
81
     *
82
     * @param array|callable|string|null $middlewareDefinition Middleware definition for CORS requests.
83
     */
84 8
    public function withCors(array|callable|string|null $middlewareDefinition): self
85
    {
86 8
        $group = clone $this;
87 8
        $group->corsMiddleware = $middlewareDefinition;
88
89 8
        return $group;
90
    }
91
92
    /**
93
     * Appends a handler middleware definition that should be invoked for a matched route.
94
     * First added handler will be executed first.
95
     */
96 12
    public function middleware(array|callable|string ...$middlewareDefinition): self
97
    {
98 12
        if ($this->routesAdded) {
99 1
            throw new RuntimeException('middleware() can not be used after routes().');
100
        }
101 11
        $new = clone $this;
102 11
        array_push(
103 11
            $new->middlewares,
104 11
            ...array_values($middlewareDefinition)
105 11
        );
106 11
        $new->builtMiddlewares = [];
107 11
        return $new;
108
    }
109
110
    /**
111
     * Prepends a handler middleware definition that should be invoked for a matched route.
112
     * First added handler will be executed last.
113
     */
114 26
    public function prependMiddleware(array|callable|string ...$middlewareDefinition): self
115
    {
116 26
        $new = clone $this;
117 26
        array_unshift(
118 26
            $new->middlewares,
119 26
            ...array_values($middlewareDefinition)
120 26
        );
121 26
        $new->middlewareAdded = true;
122 26
        $new->builtMiddlewares = [];
123 26
        return $new;
124
    }
125
126 4
    public function namePrefix(string $namePrefix): self
127
    {
128 4
        $new = clone $this;
129 4
        $new->namePrefix = $namePrefix;
130 4
        return $new;
131
    }
132
133 2
    public function host(string $host): self
134
    {
135 2
        return $this->hosts($host);
136
    }
137
138 5
    public function hosts(string ...$hosts): self
139
    {
140 5
        $new = clone $this;
141
142 5
        foreach ($hosts as $host) {
143 4
            $host = rtrim($host, '/');
144
145 4
            if ($host !== '' && !in_array($host, $new->hosts, true)) {
146 4
                $new->hosts[] = $host;
147
            }
148
        }
149
150 5
        return $new;
151
    }
152
153
    /**
154
     * Excludes middleware from being invoked when action is handled.
155
     * It is useful to avoid invoking one of the parent group middleware for
156
     * a certain route.
157
     */
158 3
    public function disableMiddleware(mixed ...$middlewareDefinition): self
159
    {
160 3
        $new = clone $this;
161 3
        array_push(
162 3
            $new->disabledMiddlewares,
163 3
            ...array_values($middlewareDefinition),
164 3
        );
165 3
        $new->builtMiddlewares = [];
166 3
        return $new;
167
    }
168
169
    /**
170
     * @psalm-template T as string
171
     *
172
     * @psalm-param T $key
173
     *
174
     * @psalm-return (
175
     *   T is ('prefix'|'namePrefix'|'host') ? string|null :
176
     *   (T is 'routes' ? Group[]|Route[] :
177
     *     (T is 'hosts' ? array<array-key, string> :
178
     *       (T is 'hasCorsMiddleware' ? bool :
179
     *         (T is 'middlewares' ? list<array|callable|string> :
180
     *           (T is 'corsMiddleware' ? array|callable|string|null : mixed)
181
     *         )
182
     *       )
183
     *     )
184
     *   )
185
     * )
186
     */
187 32
    public function getData(string $key): mixed
188
    {
189 32
        return match ($key) {
190 32
            'prefix' => $this->prefix,
191 32
            'namePrefix' => $this->namePrefix,
192 32
            'host' => $this->hosts[0] ?? null,
193 32
            'hosts' => $this->hosts,
194 32
            'corsMiddleware' => $this->corsMiddleware,
195 32
            'routes' => $this->routes,
196 32
            'hasCorsMiddleware' => $this->corsMiddleware !== null,
197 32
            'middlewares' => $this->getBuiltMiddlewares(),
198 32
            default => throw new InvalidArgumentException('Unknown data key: ' . $key),
199 32
        };
200
    }
201
202 22
    private function getBuiltMiddlewares(): array
203
    {
204 22
        if (!empty($this->builtMiddlewares)) {
205 5
            return $this->builtMiddlewares;
206
        }
207
208 22
        $builtMiddlewares = $this->middlewares;
209
210
        /** @var mixed $definition */
211 22
        foreach ($builtMiddlewares as $index => $definition) {
212 12
            if (in_array($definition, $this->disabledMiddlewares, true)) {
213 2
                unset($builtMiddlewares[$index]);
214
            }
215
        }
216
217 22
        return $this->builtMiddlewares = array_values($builtMiddlewares);
218
    }
219
220
    /**
221
     * @psalm-assert array<string> $hosts
222
     */
223 37
    private function assertHosts(array $hosts): void
224
    {
225 37
        foreach ($hosts as $host) {
226 1
            if (!is_string($host)) {
227 1
                throw new \InvalidArgumentException('Invalid $hosts provided, list of string expected.');
228
            }
229
        }
230
    }
231
232
    /**
233
     * @psalm-assert array<array|callable|string> $middlewares
234
     */
235 38
    private function assertMiddlewares(array $middlewares): void
236
    {
237
        /** @var mixed $middleware */
238 38
        foreach ($middlewares as $middleware) {
239 1
            if (is_string($middleware) || is_callable($middleware) || is_array($middleware)) {
240 1
                continue;
241
            }
242
243 1
            throw new \InvalidArgumentException(
244 1
                'Invalid $middlewares provided, list of string or array or callable expected.'
245 1
            );
246
        }
247
    }
248
}
249