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

nette / routing / 22834910607

09 Mar 2026 01:42AM UTC coverage: 96.927%. Remained the same
22834910607

push

github

dg
added CLAUDE.md

410 of 423 relevant lines covered (96.93%)

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

59

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

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

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

83
                return $httpRequest;
1✔
84
        }
85

86

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

96

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

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

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

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

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

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

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

136
                return null;
1✔
137
        }
138

139

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

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

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

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

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

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

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

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

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

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

197

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

208

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

218

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

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

233

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

246

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

260

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

274

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

283

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

293

294
        /**
295
         * Returns the flags (e.g. ONE_WAY) for each router in this list.
296
         * @return list<int>
297
         */
298
        public function getFlags(): array
299
        {
300
                return array_column($this->list, 1);
1✔
301
        }
302

303

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

309

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

315

316
        private function expandDomain(string $host): string
1✔
317
        {
318
                assert($this->domain !== null);
319
                $parts = ip2long($host) ? [$host] : array_reverse(explode('.', $host));
1✔
320
                return strtr($this->domain, [
1✔
321
                        '%tld%' => $parts[0],
1✔
322
                        '%domain%' => isset($parts[1]) ? "$parts[1].$parts[0]" : $parts[0],
1✔
323
                        '%sld%' => $parts[1] ?? '',
1✔
324
                ]);
325
        }
326
}
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