Passed
Pull Request — master (#246)
by Dmitriy
11:05
created

RouteCollection::isAliasRoute()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 3
Code Lines 1

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 0
CRAP Score 2

Importance

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