Passed
Pull Request — master (#143)
by Sergei
02:55
created

RouteCollection::ensureItemsInjected()   A

Complexity

Conditions 2
Paths 2

Size

Total Lines 4
Code Lines 2

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 3
CRAP Score 2

Importance

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