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