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