Passed
Pull Request — master (#192)
by Rustam
13:57 queued 01:33
created

RouteCollection::isStaticRoute()   A

Complexity

Conditions 2
Paths 2

Size

Total Lines 3
Code Lines 1

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 2
CRAP Score 2

Importance

Changes 0
Metric Value
cc 2
eloc 1
c 0
b 0
f 0
nc 2
nop 1
dl 0
loc 3
ccs 2
cts 2
cp 1
crap 2
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
    private string $uriPrefix = '';
21
22
    /**
23
     * @psalm-var Items
24
     */
25
    private array $items = [];
26
27
    /**
28
     * All attached routes as Route instances.
29
     *
30
     * @var Route[]
31
     */
32
    private array $routes = [];
33
34 22
    public function __construct(private RouteCollectorInterface $collector)
35
    {
36 22
    }
37
38 7
    public function getRoutes(): array
39
    {
40 7
        $this->ensureItemsInjected();
41 5
        return $this->routes;
42
    }
43
44 17
    public function getRoute(string $name): Route
45
    {
46 17
        $this->ensureItemsInjected();
47 17
        if (!array_key_exists($name, $this->routes)) {
48 1
            throw new RouteNotFoundException($name);
49
        }
50
51 16
        return $this->routes[$name];
52
    }
53
54 1
    public function getRouteTree(bool $routeAsString = true): array
55
    {
56 1
        $this->ensureItemsInjected();
57 1
        return $this->buildTree($this->items, $routeAsString);
58
    }
59
60
    public function getUriPrefix(): string
61
    {
62
        return $this->uriPrefix;
63
    }
64
65 1
    public function setUriPrefix(string $name): void
66
    {
67 1
        $this->uriPrefix = $name;
68
    }
69
70 22
    private function ensureItemsInjected(): void
71
    {
72 22
        if ($this->items === []) {
73 22
            $this->injectItems($this->collector->getItems());
74
        }
75
    }
76
77
    /**
78
     * Build routes array.
79
     *
80
     * @param Group[]|Route[] $items
81
     */
82 22
    private function injectItems(array $items): void
83
    {
84 22
        foreach ($items as $item) {
85 22
            if (!$this->isStaticRoute($item)) {
86 21
                $item = $item->prependMiddleware(...$this->collector->getMiddlewareDefinitions());
87
            }
88 22
            $this->injectItem($item);
89
        }
90
    }
91
92
    /**
93
     * Add an item into routes array.
94
     */
95 22
    private function injectItem(Group|Route $route): void
96
    {
97 22
        if ($route instanceof Group) {
98 20
            $this->injectGroup($route, $this->items, $this->uriPrefix);
99 19
            return;
100
        }
101
102 5
        $route = $route->pattern($this->uriPrefix . $route->getData('pattern'));
103 5
        $routeName = $route->getData('name');
104 5
        $this->items[] = $routeName;
105 5
        if (isset($this->routes[$routeName]) && !$route->getData('override')) {
106 1
            throw new InvalidArgumentException("A route with name '$routeName' already exists.");
107
        }
108 4
        $this->routes[$routeName] = $route;
109
    }
110
111
    /**
112
     * Inject a Group instance into route and item arrays.
113
     *
114
     * @psalm-param Items $tree
115
     */
116 20
    private function injectGroup(Group $group, array &$tree, string $prefix = '', string $namePrefix = ''): void
117
    {
118 20
        $prefix .= (string) $group->getData('prefix');
119 20
        $namePrefix .= (string) $group->getData('namePrefix');
120 20
        $items = $group->getData('items');
121 20
        $pattern = null;
122 20
        $hosts = [];
123 20
        foreach ($items as $item) {
124 20
            if (!$this->isStaticRoute($item)) {
125 15
                $item = $item->prependMiddleware(...$group->getData('middlewareDefinitions'));
126
            }
127
128 20
            if (!empty($group->getData('hosts')) && empty($item->getData('hosts'))) {
129 1
                $item = $item->hosts(...$group->getData('hosts'));
130
            }
131
132 20
            if ($item instanceof Group) {
133 6
                if ($group->getData('hasCorsMiddleware')) {
134 2
                    $item = $item->withCors($group->getData('corsMiddleware'));
135
                }
136 6
                if (empty($item->getData('prefix'))) {
137 2
                    $this->injectGroup($item, $tree, $prefix, $namePrefix);
138 2
                    continue;
139
                }
140
                /** @psalm-suppress PossiblyNullArrayOffset Checked group prefix on not empty above. */
141 5
                if (!isset($tree[$item->getData('prefix')])) {
142 5
                    $tree[$item->getData('prefix')] = [];
143
                }
144
                /**
145
                 * @psalm-suppress MixedArgumentTypeCoercion
146
                 * @psalm-suppress MixedArgument,PossiblyNullArrayOffset
147
                 * Checked group prefix on not empty above.
148
                 */
149 5
                $this->injectGroup($item, $tree[$item->getData('prefix')], $prefix, $namePrefix);
150 5
                continue;
151
            }
152
153 20
            $modifiedItem = $item->pattern($prefix . $item->getData('pattern'));
154
155 20
            if (!str_contains($modifiedItem->getData('name'), implode(', ', $modifiedItem->getData('methods')))) {
156 15
                $modifiedItem = $modifiedItem->name($namePrefix . $modifiedItem->getData('name'));
157
            }
158
159 20
            if ($group->getData('hasCorsMiddleware')) {
160 5
                $this->processCors($group, $hosts, $pattern, $modifiedItem, $tree);
161
            }
162
163 20
            $routeName = $modifiedItem->getData('name');
164 20
            $tree[] = $routeName;
165 20
            if (isset($this->routes[$routeName]) && !$modifiedItem->getData('override')) {
166 1
                throw new InvalidArgumentException("A route with name '$routeName' already exists.");
167
            }
168 20
            $this->routes[$routeName] = $modifiedItem;
169
        }
170
    }
171
172
    /**
173
     * @psalm-param Items $tree
174
     */
175 5
    private function processCors(
176
        Group $group,
177
        array &$hosts,
178
        ?string &$pattern,
179
        Route &$modifiedItem,
180
        array &$tree
181
    ): void {
182
        /** @var array|callable|string $middleware */
183 5
        $middleware = $group->getData('corsMiddleware');
184 5
        $isNotDuplicate = !in_array(Method::OPTIONS, $modifiedItem->getData('methods'), true)
185 5
            && ($pattern !== $modifiedItem->getData('pattern') || $hosts !== $modifiedItem->getData('hosts'));
186
187 5
        $pattern = $modifiedItem->getData('pattern');
188 5
        $hosts = $modifiedItem->getData('hosts');
189 5
        $optionsRoute = Route::options($pattern);
190 5
        if (!empty($hosts)) {
191 1
            $optionsRoute = $optionsRoute->hosts(...$hosts);
192
        }
193 5
        if ($isNotDuplicate) {
194 5
            $optionsRoute = $optionsRoute->middleware($middleware);
195
196 5
            $routeName = $optionsRoute->getData('name');
197 5
            $tree[] = $routeName;
198 5
            $this->routes[$routeName] = $optionsRoute->action(
199 5
                static fn (ResponseFactoryInterface $responseFactory) => $responseFactory->createResponse(204)
200 5
            );
201
        }
202 5
        $modifiedItem = $modifiedItem->prependMiddleware($middleware);
203
    }
204
205
    /**
206
     * Builds route tree from items.
207
     *
208
     * @psalm-param Items $items
209
     */
210 1
    private function buildTree(array $items, bool $routeAsString): array
211
    {
212 1
        $tree = [];
213 1
        foreach ($items as $key => $item) {
214 1
            if (is_array($item)) {
215
                /** @psalm-var Items $item */
216 1
                $tree[$key] = $this->buildTree($item, $routeAsString);
217
            } else {
218 1
                $tree[] = $routeAsString ? (string) $this->getRoute($item) : $this->getRoute($item);
219
            }
220
        }
221 1
        return $tree;
222
    }
223
224 22
    private function isStaticRoute(Group|Route $item): bool
225
    {
226 22
        return $item instanceof Route && !$item->getData('hasMiddlewares');
227
    }
228
}
229