Passed
Pull Request — master (#173)
by Rustam
02:30
created

RouteCollection::__construct()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 2
Code Lines 0

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 1
CRAP Score 1

Importance

Changes 2
Bugs 0 Features 0
Metric Value
cc 1
eloc 0
c 2
b 0
f 0
nc 1
nop 1
dl 0
loc 2
ccs 1
cts 1
cp 1
crap 1
rs 10
1
<?php
2
3
declare(strict_types=1);
4
5
namespace Yiisoft\Router;
6
7
use InvalidArgumentException;
8
use Psr\Http\Message\ResponseFactoryInterface;
9
use Yiisoft\Http\Method;
10
11
use function array_key_exists;
12
use function in_array;
13
use function is_array;
14
15
/**
16
 * @psalm-type Items = array<array-key,array|string>
17
 */
18
final class RouteCollection implements RouteCollectionInterface
19
{
20
    /**
21
     * @psalm-var Items
22
     */
23
    private array $items = [];
24
25
    /**
26
     * All attached routes as Route instances.
27
     *
28
     * @var Route[]
29
     */
30
    private array $routes = [];
31
32 21
    public function __construct(private RouteCollectorInterface $collector)
33
    {
34
    }
35
36 7
    public function getRoutes(): array
37
    {
38 7
        $this->ensureItemsInjected();
39 5
        return $this->routes;
40
    }
41
42 16
    public function getRoute(string $name): Route
43
    {
44 16
        $this->ensureItemsInjected();
45 16
        if (!array_key_exists($name, $this->routes)) {
46 1
            throw new RouteNotFoundException($name);
47
        }
48
49 15
        return $this->routes[$name];
50
    }
51
52 1
    public function getRouteTree(bool $routeAsString = true): array
53
    {
54 1
        $this->ensureItemsInjected();
55 1
        return $this->buildTree($this->items, $routeAsString);
56
    }
57
58 21
    private function ensureItemsInjected(): void
59
    {
60 21
        if ($this->items === []) {
61 21
            $this->injectItems($this->collector->getItems());
62
        }
63
    }
64
65
    /**
66
     * Build routes array.
67
     *
68
     * @param Group[]|Route[] $items
69
     */
70 21
    private function injectItems(array $items): void
71
    {
72 21
        foreach ($items as $item) {
73 21
            if (!$this->isStaticRoute($item)) {
74 20
                $item = $item->prependMiddleware(...$this->collector->getMiddlewareDefinitions());
75
            }
76 21
            $this->injectItem($item);
77
        }
78
    }
79
80
    /**
81
     * Add an item into routes array.
82
     */
83 21
    private function injectItem(Group|Route $route): void
84
    {
85 21
        if ($route instanceof Group) {
86 19
            $this->injectGroup($route, $this->items);
87 18
            return;
88
        }
89
90 4
        $routeName = $route->getData('name');
91 4
        $this->items[] = $routeName;
92 4
        if (isset($this->routes[$routeName]) && !$route->getData('override')) {
93 1
            throw new InvalidArgumentException("A route with name '$routeName' already exists.");
94
        }
95 3
        $this->routes[$routeName] = $route;
96
    }
97
98
    /**
99
     * Inject a Group instance into route and item arrays.
100
     *
101
     * @psalm-param Items $tree
102
     */
103 19
    private function injectGroup(Group $group, array &$tree, string $prefix = '', string $namePrefix = ''): void
104
    {
105 19
        $prefix .= (string) $group->getData('prefix');
106 19
        $namePrefix .= (string) $group->getData('namePrefix');
107 19
        $items = $group->getData('items');
108 19
        $pattern = null;
109 19
        $hosts = [];
110 19
        foreach ($items as $item) {
111 19
            if (!$this->isStaticRoute($item)) {
112 15
                $item = $item->prependMiddleware(...$group->getData('middlewareDefinitions'));
113
            }
114
115 19
            if (!empty($group->getData('hosts')) && empty($item->getData('hosts'))) {
116 1
                $item = $item->hosts(...$group->getData('hosts'));
117
            }
118
119 19
            if ($item instanceof Group) {
120 6
                if ($group->getData('hasCorsMiddleware')) {
121 2
                    $item = $item->withCors($group->getData('corsMiddleware'));
122
                }
123 6
                if (empty($item->getData('prefix'))) {
124 2
                    $this->injectGroup($item, $tree, $prefix, $namePrefix);
125 2
                    continue;
126
                }
127
                /** @psalm-suppress PossiblyNullArrayOffset Checked group prefix on not empty above. */
128 5
                if (!isset($tree[$item->getData('prefix')])) {
129 5
                    $tree[$item->getData('prefix')] = [];
130
                }
131
                /**
132
                 * @psalm-suppress MixedArgumentTypeCoercion
133
                 * @psalm-suppress MixedArgument,PossiblyNullArrayOffset
134
                 * Checked group prefix on not empty above.
135
                 */
136 5
                $this->injectGroup($item, $tree[$item->getData('prefix')], $prefix, $namePrefix);
137 5
                continue;
138
            }
139
140 19
            $modifiedItem = $item->pattern($prefix . $item->getData('pattern'));
141
142 19
            if (!str_contains($modifiedItem->getData('name'), implode(', ', $modifiedItem->getData('methods')))) {
143 14
                $modifiedItem = $modifiedItem->name($namePrefix . $modifiedItem->getData('name'));
144
            }
145
146 19
            if ($group->getData('hasCorsMiddleware')) {
147 5
                $this->processCors($group, $hosts, $pattern, $modifiedItem, $tree);
148
            }
149
150 19
            $routeName = $modifiedItem->getData('name');
151 19
            $tree[] = $routeName;
152 19
            if (isset($this->routes[$routeName]) && !$modifiedItem->getData('override')) {
153 1
                throw new InvalidArgumentException("A route with name '$routeName' already exists.");
154
            }
155 19
            $this->routes[$routeName] = $modifiedItem;
156
        }
157
    }
158
159
    /**
160
     * @psalm-param Items $tree
161
     */
162 5
    private function processCors(
163
        Group $group,
164
        array &$hosts,
165
        ?string &$pattern,
166
        Route &$modifiedItem,
167
        array &$tree
168
    ): void {
169
        /** @var array|callable|string $middleware */
170 5
        $middleware = $group->getData('corsMiddleware');
171 5
        $isNotDuplicate = !in_array(Method::OPTIONS, $modifiedItem->getData('methods'), true)
172 5
            && ($pattern !== $modifiedItem->getData('pattern') || $hosts !== $modifiedItem->getData('hosts'));
173
174 5
        $pattern = $modifiedItem->getData('pattern');
175 5
        $hosts = $modifiedItem->getData('hosts');
176 5
        $optionsRoute = Route::options($pattern);
0 ignored issues
show
Bug introduced by
It seems like $pattern can also be of type null; however, parameter $pattern of Yiisoft\Router\Route::options() does only seem to accept string, maybe add an additional type check? ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-type  annotation

176
        $optionsRoute = Route::options(/** @scrutinizer ignore-type */ $pattern);
Loading history...
177 5
        if (!empty($hosts)) {
178 1
            $optionsRoute = $optionsRoute->hosts(...$hosts);
179
        }
180 5
        if ($isNotDuplicate) {
181 5
            $optionsRoute = $optionsRoute->middleware($middleware);
182
183 5
            $routeName = $optionsRoute->getData('name');
184 5
            $tree[] = $routeName;
185 5
            $this->routes[$routeName] = $optionsRoute->action(
186 5
                static fn (ResponseFactoryInterface $responseFactory) => $responseFactory->createResponse(204)
187
            );
188
        }
189 5
        $modifiedItem = $modifiedItem->prependMiddleware($middleware);
190
    }
191
192
    /**
193
     * Builds route tree from items.
194
     *
195
     * @psalm-param Items $items
196
     */
197 1
    private function buildTree(array $items, bool $routeAsString): array
198
    {
199 1
        $tree = [];
200 1
        foreach ($items as $key => $item) {
201 1
            if (is_array($item)) {
202
                /** @psalm-var Items $item */
203 1
                $tree[$key] = $this->buildTree($item, $routeAsString);
204
            } else {
205 1
                $tree[] = $routeAsString ? (string) $this->getRoute($item) : $this->getRoute($item);
206
            }
207
        }
208 1
        return $tree;
209
    }
210
211 21
    private function isStaticRoute(Group|Route $item): bool
212
    {
213 21
        return $item instanceof Route && !$item->getData('hasMiddlewares');
214
    }
215
}
216