Passed
Pull Request — master (#143)
by Sergei
03:01
created

RouteCollection::processCors()   A

Complexity

Conditions 5
Paths 12

Size

Total Lines 28
Code Lines 15

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 17
CRAP Score 5

Importance

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