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

nette / routing / 21943387777

12 Feb 2026 10:44AM UTC coverage: 96.919% (+0.03%) from 96.89%
21943387777

push

github

dg
added CLAUDE.md

409 of 422 relevant lines covered (96.92%)

0.97 hits per line

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

93.33
/src/Routing/RouteList.php
1
<?php
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
declare(strict_types=1);
9

10
namespace Nette\Routing;
11

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

15

16
/**
17
 * The router broker.
18
 */
19
class RouteList implements Router
20
{
21
        protected ?self $parent;
22

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

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

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

35

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

41

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

62

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

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

83
                        $httpRequest = $httpRequest->withUrl($url);
1✔
84
                }
85

86
                return $httpRequest;
1✔
87
        }
88

89

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

99

100
        /**
101
         * Constructs absolute URL from array.
102
         * @param array<string, mixed>  $params
103
         */
104
        public function constructUrl(array $params, Nette\Http\UrlScript $refUrl): ?string
1✔
105
        {
106
                if ($this->domain) {
1✔
107
                        if (!$this->refUrlCache->offsetExists($refUrl)) {
1✔
108
                                $this->refUrlCache->offsetSet($refUrl, $refUrl->withHost(
1✔
109
                                        $this->expandDomain($refUrl->getHost()),
1✔
110
                                ));
111
                        }
112

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

116
                if ($this->path) {
1✔
117
                        if (!$this->refUrlCache->offsetExists($refUrl)) {
1✔
118
                                $this->refUrlCache->offsetSet($refUrl, $refUrl->withPath($refUrl->getBasePath() . $this->path));
1✔
119
                        }
120

121
                        $refUrl = $this->refUrlCache->offsetGet($refUrl);
1✔
122
                }
123

124
                if ($this->ranks === null) {
1✔
125
                        $this->warmupCache();
1✔
126
                }
127

128
                assert($this->ranks !== null);
129
                $key = $params[$this->cacheKey ?? ''] ?? null;
1✔
130
                $key = is_scalar($key) ? (string) $key : '*';
1✔
131
                if (!isset($this->ranks[$key])) {
1✔
132
                        $key = '*';
1✔
133
                }
134

135
                foreach ($this->ranks[$key] as $router) {
1✔
136
                        $url = $router->constructUrl($params, $refUrl);
1✔
137
                        if ($url !== null) {
1✔
138
                                return $url;
1✔
139
                        }
140
                }
141

142
                return null;
1✔
143
        }
144

145

146
        public function warmupCache(): void
147
        {
148
                // find best key
149
                $candidates = [];
1✔
150
                $routers = [];
1✔
151
                foreach ($this->list as [$router, $oneWay]) {
1✔
152
                        if ($oneWay) {
1✔
153
                                continue;
1✔
154
                        } elseif ($router instanceof self) {
1✔
155
                                $router->warmupCache();
1✔
156
                        }
157

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

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

166
                        $routers[] = [$router, $params];
1✔
167
                }
168

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

177
                // classify routers
178
                $ranks = ['*' => []];
1✔
179

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

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

192
                                $ranks[$value][] = $router;
1✔
193
                        }
194
                }
195

196
                $this->ranks = $ranks;
1✔
197
        }
1✔
198

199

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

210

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

220

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

232
                $this->ranks = null;
×
233
        }
234

235

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

247

248
        /**
249
         * Returns an iterator over all routers.
250
         */
251
        public function withDomain(string $domain): static
1✔
252
        {
253
                $router = new static;
1✔
254
                $router->domain = $domain;
1✔
255
                $router->refUrlCache = new \SplObjectStorage;
1✔
256
                $router->parent = $this;
1✔
257
                $this->add($router);
1✔
258
                return $router;
1✔
259
        }
260

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
        public function end(): ?self
274
        {
275
                return $this->parent;
1✔
276
        }
277

278

279
        /**
280
         * @return list<Router>
281
         */
282
        public function getRouters(): array
283
        {
284
                return array_column($this->list, 0);
1✔
285
        }
286

287

288
        /**
289
         * @return list<int>
290
         */
291
        public function getFlags(): array
292
        {
293
                return array_column($this->list, 1);
1✔
294
        }
295

296

297
        public function getDomain(): ?string
298
        {
299
                return $this->domain;
×
300
        }
301

302

303
        public function getPath(): ?string
304
        {
305
                return $this->path;
×
306
        }
307

308

309
        private function expandDomain(string $host): string
1✔
310
        {
311
                assert($this->domain !== null);
312
                $parts = ip2long($host) ? [$host] : array_reverse(explode('.', $host));
1✔
313
                return strtr($this->domain, [
1✔
314
                        '%tld%' => $parts[0],
1✔
315
                        '%domain%' => isset($parts[1]) ? "$parts[1].$parts[0]" : $parts[0],
1✔
316
                        '%sld%' => $parts[1] ?? '',
1✔
317
                ]);
318
        }
319
}
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