• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

nette / routing / 22835010463

09 Mar 2026 01:47AM UTC coverage: 96.956% (+0.04%) from 96.919%
22835010463

push

github

dg
typo

414 of 427 relevant lines covered (96.96%)

0.97 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

93.39
/src/Routing/RouteList.php
1
<?php declare(strict_types=1);
1✔
2

3
/**
4
 * This file is part of the Nette Framework (https://nette.org)
5
 * Copyright (c) 2004 David Grudl (https://davidgrudl.com)
6
 */
7

8
namespace Nette\Routing;
9

10
use Nette;
11
use function array_column, array_filter, array_keys, array_reverse, array_splice, count, explode, ip2long, is_scalar, rtrim, strtr;
12

13

14
/**
15
 * Router collection that tries each router in sequence and caches URL construction lookups.
16
 */
17
class RouteList implements Router
18
{
19
        protected ?self $parent;
20

21
        /** @var list<array{Router, int}> */
22
        private array $list = [];
23

24
        /** @var array<string, list<Router>>|null */
25
        private ?array $ranks = null;
26
        private ?string $cacheKey;
27
        private ?string $domain = null;
28
        private ?string $path = null;
29

30
        /** @var \SplObjectStorage<Nette\Http\UrlScript, Nette\Http\UrlScript> */
31
        private \SplObjectStorage $refUrlCache;
32

33

34
        public function __construct()
35
        {
36
                $this->refUrlCache = new \SplObjectStorage;
1✔
37
        }
1✔
38

39

40
        /**
41
         * @return ?array<string, mixed>
42
         */
43
        final public function match(Nette\Http\IRequest $httpRequest): ?array
1✔
44
        {
45
                if ($httpRequest = $this->prepareRequest($httpRequest)) {
1✔
46
                        foreach ($this->list as [$router]) {
1✔
47
                                if (
48
                                        ($params = $router->match($httpRequest)) !== null
1✔
49
                                        && ($params = $this->completeParameters($params)) !== null
1✔
50
                                ) {
51
                                        return $params;
1✔
52
                                }
53
                        }
54
                }
55
                return null;
1✔
56
        }
57

58

59
        protected function prepareRequest(Nette\Http\IRequest $httpRequest): ?Nette\Http\IRequest
1✔
60
        {
61
                if ($this->domain) {
1✔
62
                        $host = $httpRequest->getUrl()->getHost();
1✔
63
                        if ($host !== $this->expandDomain($host)) {
1✔
64
                                return null;
1✔
65
                        }
66
                }
67

68
                if ($this->path) {
1✔
69
                        $url = $httpRequest->getUrl();
1✔
70
                        $relativePath = $url->getRelativePath();
1✔
71
                        if (str_starts_with($relativePath, $this->path)) {
1✔
72
                                $url = $url->withPath($url->getPath(), $url->getBasePath() . $this->path);
1✔
73
                        } elseif ($relativePath . '/' === $this->path) {
1✔
74
                                $url = $url->withPath($url->getPath() . '/');
1✔
75
                        } else {
76
                                return null;
1✔
77
                        }
78

79
                        $httpRequest = $httpRequest->withUrl($url);
1✔
80
                }
81

82
                return $httpRequest;
1✔
83
        }
84

85

86
        /**
87
         * @param array<string, mixed>  $params
88
         * @return ?array<string, mixed>
89
         */
90
        protected function completeParameters(array $params): ?array
1✔
91
        {
92
                return $params;
1✔
93
        }
94

95

96
        /** @param array<string, mixed>  $params */
97
        public function constructUrl(array $params, Nette\Http\UrlScript $refUrl): ?string
1✔
98
        {
99
                if ($this->domain) {
1✔
100
                        if (!$this->refUrlCache->offsetExists($refUrl)) {
1✔
101
                                $this->refUrlCache->offsetSet($refUrl, $refUrl->withHost(
1✔
102
                                        $this->expandDomain($refUrl->getHost()),
1✔
103
                                ));
104
                        }
105

106
                        $refUrl = $this->refUrlCache->offsetGet($refUrl);
1✔
107
                }
108

109
                if ($this->path) {
1✔
110
                        if (!$this->refUrlCache->offsetExists($refUrl)) {
1✔
111
                                $this->refUrlCache->offsetSet($refUrl, $refUrl->withPath($refUrl->getBasePath() . $this->path));
1✔
112
                        }
113

114
                        $refUrl = $this->refUrlCache->offsetGet($refUrl);
1✔
115
                }
116

117
                if ($this->ranks === null) {
1✔
118
                        $this->warmupCache();
1✔
119
                }
120

121
                assert($this->ranks !== null);
122
                $key = $params[$this->cacheKey ?? ''] ?? null;
1✔
123
                $key = is_scalar($key) ? (string) $key : '*';
1✔
124
                if (!isset($this->ranks[$key])) {
1✔
125
                        $key = '*';
1✔
126
                }
127

128
                foreach ($this->ranks[$key] as $router) {
1✔
129
                        $url = $router->constructUrl($params, $refUrl);
1✔
130
                        if ($url !== null) {
1✔
131
                                return $url;
1✔
132
                        }
133
                }
134

135
                return null;
1✔
136
        }
137

138

139
        /**
140
         * Builds an internal lookup index of routers grouped by their most discriminating constant parameter.
141
         * Call this before URL generation to improve performance; called automatically on first use.
142
         */
143
        public function warmupCache(): void
144
        {
145
                // find best key
146
                $candidates = [];
1✔
147
                $routers = [];
1✔
148
                foreach ($this->list as [$router, $oneWay]) {
1✔
149
                        if ($oneWay) {
1✔
150
                                continue;
1✔
151
                        } elseif ($router instanceof self) {
1✔
152
                                $router->warmupCache();
1✔
153
                        }
154

155
                        $params = $router instanceof Route
1✔
156
                                ? $router->getConstantParameters()
1✔
157
                                : [];
1✔
158

159
                        foreach (array_filter($params, is_scalar(...)) as $name => $value) {
1✔
160
                                $candidates[$name][(string) $value] = true;
1✔
161
                        }
162

163
                        $routers[] = [$router, $params];
1✔
164
                }
165

166
                $this->cacheKey = $count = null;
1✔
167
                foreach ($candidates as $name => $items) {
1✔
168
                        if (count($items) > $count) {
1✔
169
                                $count = count($items);
1✔
170
                                $this->cacheKey = $name;
1✔
171
                        }
172
                }
173

174
                // classify routers
175
                $ranks = ['*' => []];
1✔
176

177
                foreach ($routers as [$router, $params]) {
1✔
178
                        $value = $params[$this->cacheKey ?? ''] ?? null;
1✔
179
                        $values = $value === null
1✔
180
                                ? array_keys($ranks)
1✔
181
                                : [is_scalar($value) ? (string) $value : '*'];
1✔
182

183
                        foreach ($values as $value) {
1✔
184
                                $value = (string) $value;
1✔
185
                                if (!isset($ranks[$value])) {
1✔
186
                                        $ranks[$value] = $ranks['*'];
1✔
187
                                }
188

189
                                $ranks[$value][] = $router;
1✔
190
                        }
191
                }
192

193
                $this->ranks = $ranks;
1✔
194
        }
1✔
195

196

197
        /**
198
         * Adds a router.
199
         */
200
        public function add(Router $router, bool $oneWay = false): static
1✔
201
        {
202
                $this->list[] = [$router, $oneWay];
1✔
203
                $this->ranks = null;
1✔
204
                return $this;
1✔
205
        }
206

207

208
        /**
209
         * Prepends a router.
210
         */
211
        public function prepend(Router $router, bool $oneWay = false): void
1✔
212
        {
213
                array_splice($this->list, 0, 0, [[$router, $oneWay]]);
1✔
214
                $this->ranks = null;
1✔
215
        }
1✔
216

217

218
        /** @internal */
219
        protected function modify(int $index, ?Router $router): void
220
        {
221
                if (!isset($this->list[$index])) {
×
222
                        throw new Nette\OutOfRangeException('Offset invalid or out of range');
×
223
                } elseif ($router) {
×
224
                        $this->list[$index] = [$router, 0];
×
225
                } else {
226
                        array_splice($this->list, $index, 1);
×
227
                }
228

229
                $this->ranks = null;
×
230
        }
231

232

233
        /**
234
         * Creates a Route from the mask and adds it to the list.
235
         * @param string  $mask e.g. '<presenter>/<action>/<id \d{1,3}>'
236
         * @param array<string, mixed>  $metadata default values or metadata
237
         */
238
        public function addRoute(string $mask, array $metadata = [], bool $oneWay = false): static
1✔
239
        {
240
                $this->add(new Route($mask, $metadata), $oneWay);
1✔
241
                return $this;
1✔
242
        }
243

244

245
        /**
246
         * Creates a child RouteList scoped to the given domain and adds it to this list.
247
         */
248
        public function withDomain(string $domain): static
1✔
249
        {
250
                $router = new static;
1✔
251
                $router->domain = $domain;
1✔
252
                $router->refUrlCache = new \SplObjectStorage;
1✔
253
                $router->parent = $this;
1✔
254
                $this->add($router);
1✔
255
                return $router;
1✔
256
        }
257

258

259
        /**
260
         * Creates a child RouteList scoped to the given path prefix and adds it to this list.
261
         */
262
        public function withPath(string $path): static
1✔
263
        {
264
                $router = new static;
1✔
265
                $router->path = rtrim($path, '/') . '/';
1✔
266
                $router->refUrlCache = new \SplObjectStorage;
1✔
267
                $router->parent = $this;
1✔
268
                $this->add($router);
1✔
269
                return $router;
1✔
270
        }
271

272

273
        /**
274
         * Returns the parent RouteList, used to end a withDomain()/withPath() chain.
275
         */
276
        public function end(): ?self
277
        {
278
                return $this->parent;
1✔
279
        }
280

281

282
        /**
283
         * Returns all routers in this list.
284
         * @return list<Router>
285
         */
286
        public function getRouters(): array
287
        {
288
                return array_column($this->list, 0);
1✔
289
        }
290

291

292
        /**
293
         * Returns the flags (e.g. oneWay) for each router in this list.
294
         * @return list<array{oneWay: bool}>
295
         */
296
        public function getFlags(): array
297
        {
298
                return array_map(fn($info) => ['oneWay' => (bool) $info[1]], $this->list);
1✔
299
        }
300

301

302
        public function getDomain(): ?string
303
        {
304
                return $this->domain;
×
305
        }
306

307

308
        public function getPath(): ?string
309
        {
310
                return $this->path;
×
311
        }
312

313

314
        private function expandDomain(string $host): string
1✔
315
        {
316
                assert($this->domain !== null);
317
                $parts = ip2long($host) ? [$host] : array_reverse(explode('.', $host));
1✔
318
                return strtr($this->domain, [
1✔
319
                        '%tld%' => $parts[0],
1✔
320
                        '%domain%' => isset($parts[1]) ? "$parts[1].$parts[0]" : $parts[0],
1✔
321
                        '%sld%' => $parts[1] ?? '',
1✔
322
                ]);
323
        }
324
}
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2026 Coveralls, Inc