• 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

98.59
/src/Routing/Route.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 Nette\Utils\Strings;
14
use function array_flip, array_key_exists, array_pop, array_reverse, array_unshift, count, explode, http_build_query, ini_get, ip2long, is_array, is_scalar, is_string, ltrim, preg_match, preg_quote, preg_replace_callback, rawurldecode, rawurlencode, str_starts_with, strlen, strpbrk, strtr, substr, trim;
15

16

17
/**
18
 * The bidirectional route is responsible for mapping
19
 * HTTP request to an array for dispatch and vice-versa.
20
 */
21
class Route implements Router
22
{
23
        /** key used in metadata */
24
        public const
25
                Value = 'value',
26
                Pattern = 'pattern',
27
                FilterIn = 'filterIn',
28
                FilterOut = 'filterOut',
29
                FilterTable = 'filterTable',
30
                FilterStrict = 'filterStrict';
31

32
        /** @deprecated use Route::Value */
33
        public const VALUE = self::Value;
34

35
        /** @deprecated use Route::Pattern */
36
        public const PATTERN = self::Pattern;
37

38
        /** @deprecated use Route::FilterIn */
39
        public const FILTER_IN = self::FilterIn;
40

41
        /** @deprecated use Route::FilterOut */
42
        public const FILTER_OUT = self::FilterOut;
43

44
        /** @deprecated use Route::FilterTable */
45
        public const FILTER_TABLE = self::FilterTable;
46

47
        /** @deprecated use Route::FilterStrict */
48
        public const FILTER_STRICT = self::FilterStrict;
49

50
        /** key used in metadata */
51
        private const
52
                Default = 'defOut',
53
                Fixity = 'fixity',
54
                FilterTableOut = 'filterTO';
55

56
        /** url type */
57
        private const
58
                Host = 1,
59
                Path = 2,
60
                Relative = 3;
61

62
        /** fixity types - has default value and is: */
63
        private const
64
                InQuery = 0,
65
                InPath = 1, // in brackets is default value = null
66
                Constant = 2;
67

68
        /** @var array<string, array<string, mixed>> */
69
        protected array $defaultMeta = [
70
                '#' => [ // default style for path parameters
71
                        self::Pattern => '[^/]+',
72
                        self::FilterOut => [self::class, 'param2path'],
73
                ],
74
        ];
75

76
        private string $mask;
77

78
        /** @var list<string> */
79
        private array $sequence;
80

81
        /** regular expression pattern */
82
        private string $re;
83

84
        /** @var array<string, string> parameter aliases in regular expression */
85
        private array $aliases = [];
86

87
        /** @var array<string, array<string, mixed>> of [value & fixity, filterIn, filterOut] */
88
        private array $metadata = [];
89

90
        /** @var array<string, string> */
91
        private array $xlat = [];
92

93
        /** Host, Path, Relative */
94
        private int $type;
95

96
        /** http | https */
97
        private string $scheme = '';
98

99

100
        /**
101
         * @param string  $mask e.g. '<presenter>/<action>/<id \d{1,3}>'
102
         * @param array<string, mixed>  $metadata default values or metadata
103
         */
104
        public function __construct(string $mask, array $metadata = [])
1✔
105
        {
106
                $this->mask = $mask;
1✔
107
                $this->metadata = $this->normalizeMetadata($metadata);
1✔
108
                $this->parseMask($this->detectMaskType());
1✔
109
        }
1✔
110

111

112
        /**
113
         * Returns mask.
114
         */
115
        public function getMask(): string
116
        {
117
                return $this->mask;
1✔
118
        }
119

120

121
        /**
122
         * @internal
123
         * @return array<string, array<string, mixed>>
124
         */
125
        protected function getMetadata(): array
126
        {
127
                return $this->metadata;
×
128
        }
129

130

131
        /**
132
         * Returns default values.
133
         * @return array<string, mixed>
134
         */
135
        public function getDefaults(): array
136
        {
137
                $defaults = [];
1✔
138
                foreach ($this->metadata as $name => $meta) {
1✔
139
                        if (isset($meta[self::Fixity])) {
1✔
140
                                $defaults[$name] = $meta[self::Value];
1✔
141
                        }
142
                }
143

144
                return $defaults;
1✔
145
        }
146

147

148
        /**
149
         * @internal
150
         * @return array<string, mixed>
151
         */
152
        public function getConstantParameters(): array
153
        {
154
                $res = [];
1✔
155
                foreach ($this->metadata as $name => $meta) {
1✔
156
                        if (isset($meta[self::Fixity]) && $meta[self::Fixity] === self::Constant) {
1✔
157
                                $res[$name] = $meta[self::Value];
1✔
158
                        }
159
                }
160

161
                return $res;
1✔
162
        }
163

164

165
        /**
166
         * Maps HTTP request to an array.
167
         * @return ?array<string, mixed>
168
         */
169
        public function match(Nette\Http\IRequest $httpRequest): ?array
1✔
170
        {
171
                // combine with precedence: mask (params in URL-path), fixity, query, (post,) defaults
172

173
                // 1) URL MASK
174
                $url = $httpRequest->getUrl();
1✔
175
                $re = $this->re;
1✔
176

177
                if ($this->type === self::Host) {
1✔
178
                        $host = $url->getHost();
1✔
179
                        $path = '//' . $host . $url->getPath();
1✔
180
                        $parts = ip2long($host)
1✔
181
                                ? [$host]
1✔
182
                                : array_reverse(explode('.', $host));
1✔
183
                        $re = strtr($re, [
1✔
184
                                '/%basePath%/' => preg_quote($url->getBasePath(), '#'),
1✔
185
                                '%tld%' => preg_quote($parts[0], '#'),
1✔
186
                                '%domain%' => preg_quote(isset($parts[1]) ? "$parts[1].$parts[0]" : $parts[0], '#'),
1✔
187
                                '%sld%' => preg_quote($parts[1] ?? '', '#'),
1✔
188
                                '%host%' => preg_quote($host, '#'),
1✔
189
                        ]);
190

191
                } elseif ($this->type === self::Relative) {
1✔
192
                        $basePath = $url->getBasePath();
1✔
193
                        if (!str_starts_with($url->getPath(), $basePath)) {
1✔
194
                                return null;
×
195
                        }
196

197
                        $path = substr($url->getPath(), strlen($basePath));
1✔
198

199
                } else {
200
                        $path = $url->getPath();
1✔
201
                }
202

203
                $path = rawurldecode($path);
1✔
204
                if ($path !== '' && $path[-1] !== '/') {
1✔
205
                        $path .= '/';
1✔
206
                }
207

208
                if (!$matches = Strings::match($path, $re)) {
1✔
209
                        return null; // stop, not matched
1✔
210
                }
211

212
                // assigns matched values to parameters
213
                $params = [];
1✔
214
                foreach ($matches as $k => $v) {
1✔
215
                        if (is_string($k) && $v !== '') {
1✔
216
                                $params[$this->aliases[$k]] = $v;
1✔
217
                        }
218
                }
219

220
                // 2) CONSTANT FIXITY
221
                foreach ($this->metadata as $name => $meta) {
1✔
222
                        if (!isset($params[$name]) && isset($meta[self::Fixity]) && $meta[self::Fixity] !== self::InQuery) {
1✔
223
                                $params[$name] = null; // cannot be overwriten in 3) and detected by isset() in 4)
1✔
224
                        }
225
                }
226

227
                // 3) QUERY
228
                $params += self::renameKeys($httpRequest->getQuery(), array_flip($this->xlat));
1✔
229

230
                // 4) APPLY FILTERS & FIXITY
231
                foreach ($this->metadata as $name => $meta) {
1✔
232
                        if (isset($params[$name])) {
1✔
233
                                if (!is_scalar($params[$name])) {
1✔
234
                                        // do nothing
235
                                } elseif (isset($meta[self::FilterTable][$params[$name]])) { // applies filterTable only to scalar parameters
1✔
236
                                        $params[$name] = $meta[self::FilterTable][$params[$name]];
1✔
237

238
                                } elseif (isset($meta[self::FilterTable]) && !empty($meta[self::FilterStrict])) {
1✔
239
                                        return null; // rejected by filterTable
1✔
240

241
                                } elseif (isset($meta[self::FilterIn])) { // applies filterIn only to scalar parameters
1✔
242
                                        $params[$name] = $meta[self::FilterIn]((string) $params[$name]);
1✔
243
                                        if ($params[$name] === null && !isset($meta[self::Fixity])) {
1✔
244
                                                return null; // rejected by filter
1✔
245
                                        }
246
                                }
247
                        } elseif (isset($meta[self::Fixity])) {
1✔
248
                                $params[$name] = $meta[self::Value];
1✔
249
                        }
250
                }
251

252
                if (isset($this->metadata[''][self::FilterIn])) {
1✔
253
                        $params = $this->metadata[''][self::FilterIn]($params);
1✔
254
                        if ($params === null) {
1✔
255
                                return null;
1✔
256
                        }
257
                }
258

259
                return $params;
1✔
260
        }
261

262

263
        /**
264
         * Constructs absolute URL from array.
265
         * @param array<string, mixed>  $params
266
         */
267
        public function constructUrl(array $params, Nette\Http\UrlScript $refUrl): ?string
1✔
268
        {
269
                if (!$this->preprocessParams($params)) {
1✔
270
                        return null;
1✔
271
                }
272

273
                $url = $this->compileUrl($params);
1✔
274
                if ($url === null) {
1✔
275
                        return null;
1✔
276
                }
277

278
                // absolutize
279
                if ($this->type === self::Relative) {
1✔
280
                        $url = (($tmp = $refUrl->getAuthority()) ? "//$tmp" : '') . $refUrl->getBasePath() . $url;
1✔
281

282
                } elseif ($this->type === self::Path) {
1✔
283
                        $url = (($tmp = $refUrl->getAuthority()) ? "//$tmp" : '') . $url;
1✔
284

285
                } else {
286
                        $host = $refUrl->getHost();
1✔
287
                        $parts = ip2long($host)
1✔
288
                                ? [$host]
1✔
289
                                : array_reverse(explode('.', $host));
1✔
290
                        $url = strtr($url, [
1✔
291
                                '/%basePath%/' => $refUrl->getBasePath(),
1✔
292
                                '%tld%' => $parts[0],
1✔
293
                                '%domain%' => isset($parts[1]) ? "$parts[1].$parts[0]" : $parts[0],
1✔
294
                                '%sld%' => $parts[1] ?? '',
1✔
295
                                '%host%' => $host,
1✔
296
                        ]);
297
                }
298

299
                $url = ($this->scheme ?: $refUrl->getScheme()) . ':' . $url;
1✔
300

301
                // build query string
302
                $params = self::renameKeys($params, $this->xlat);
1✔
303
                $sep = ini_get('arg_separator.input');
1✔
304
                $query = http_build_query($params, '', $sep ? $sep[0] : '&');
1✔
305
                if ($query !== '') {
1✔
306
                        $url .= '?' . $query;
1✔
307
                }
308

309
                return $url;
1✔
310
        }
311

312

313
        /**
314
         * @param array<string, mixed>  $params
315
         */
316
        private function preprocessParams(array &$params): bool
1✔
317
        {
318
                $filter = $this->metadata[''][self::FilterOut] ?? null;
1✔
319
                if ($filter) {
1✔
320
                        $params = $filter($params);
1✔
321
                        if ($params === null) {
1✔
322
                                return false; // rejected by global filter
1✔
323
                        }
324
                }
325

326
                foreach ($this->metadata as $name => $meta) {
1✔
327
                        $fixity = $meta[self::Fixity] ?? null;
1✔
328

329
                        if (!isset($params[$name])) {
1✔
330
                                continue; // retains null values
1✔
331
                        }
332

333
                        if (is_scalar($params[$name])) {
1✔
334
                                $params[$name] = $params[$name] === false
1✔
335
                                        ? '0'
1✔
336
                                        : (string) $params[$name];
1✔
337
                        }
338

339
                        if ($fixity !== null) {
1✔
340
                                if ($params[$name] === $meta[self::Value]) { // remove default values; null values are retain
1✔
341
                                        unset($params[$name]);
1✔
342
                                        continue;
1✔
343

344
                                } elseif ($fixity === self::Constant) {
1✔
345
                                        return false; // wrong parameter value
1✔
346
                                }
347
                        }
348

349
                        if (is_scalar($params[$name]) && isset($meta[self::FilterTableOut][$params[$name]])) {
1✔
350
                                $params[$name] = $meta[self::FilterTableOut][$params[$name]];
1✔
351

352
                        } elseif (isset($meta[self::FilterTableOut]) && !empty($meta[self::FilterStrict])) {
1✔
353
                                return false;
×
354

355
                        } elseif (isset($meta[self::FilterOut])) {
1✔
356
                                $params[$name] = $meta[self::FilterOut]($params[$name]);
1✔
357
                        }
358

359
                        if (
360
                                isset($meta[self::Pattern])
1✔
361
                                && !preg_match("#(?:{$meta[self::Pattern]})$#DA", rawurldecode((string) $params[$name]))
1✔
362
                        ) {
363
                                return false; // pattern not match
1✔
364
                        }
365
                }
366

367
                return true;
1✔
368
        }
369

370

371
        /**
372
         * @param array<string, mixed>  $params
373
         */
374
        private function compileUrl(array &$params): ?string
1✔
375
        {
376
                $brackets = [];
1✔
377
                $required = null; // null for auto-optional
1✔
378
                $path = '';
1✔
379
                $i = count($this->sequence) - 1;
1✔
380

381
                do {
382
                        $path = $this->sequence[$i] . $path;
1✔
383
                        if ($i === 0) {
1✔
384
                                return $path;
1✔
385
                        }
386

387
                        $i--;
1✔
388

389
                        $name = $this->sequence[$i--]; // parameter name
1✔
390

391
                        if ($name === ']') { // opening optional part
1✔
392
                                $brackets[] = $path;
1✔
393

394
                        } elseif ($name[0] === '[') { // closing optional part
1✔
395
                                $tmp = array_pop($brackets);
1✔
396
                                if ($required < count($brackets) + 1) { // is this level optional?
1✔
397
                                        if ($name !== '[!') { // and not "required"-optional
1✔
398
                                                $path = $tmp;
1✔
399
                                        }
400
                                } else {
401
                                        $required = count($brackets);
1✔
402
                                }
403
                        } elseif ($name[0] === '?') { // "foo" parameter
1✔
404
                                continue;
1✔
405

406
                        } elseif (isset($params[$name]) && $params[$name] !== '') {
1✔
407
                                $required = count($brackets); // make this level required
1✔
408
                                $path = $params[$name] . $path;
1✔
409
                                unset($params[$name]);
1✔
410

411
                        } elseif (isset($this->metadata[$name][self::Fixity])) { // has default value?
1✔
412
                                $path = $required === null && !$brackets // auto-optional
1✔
413
                                        ? ''
1✔
414
                                        : $this->metadata[$name][self::Default] . $path;
1✔
415

416
                        } else {
417
                                return null; // missing parameter '$name'
1✔
418
                        }
419
                } while (true);
1✔
420
        }
421

422

423
        private function detectMaskType(): string
424
        {
425
                // '//host/path' vs. '/abs. path' vs. 'relative path'
426
                if (preg_match('#(?:(https?):)?(//.*)#A', $this->mask, $m)) {
1✔
427
                        $this->type = self::Host;
1✔
428
                        [, $this->scheme, $path] = $m;
1✔
429
                        return $path;
1✔
430

431
                } elseif (str_starts_with($this->mask, '/')) {
1✔
432
                        $this->type = self::Path;
1✔
433

434
                } else {
435
                        $this->type = self::Relative;
1✔
436
                }
437

438
                return $this->mask;
1✔
439
        }
440

441

442
        /**
443
         * @param array<string, mixed>  $metadata
444
         * @return array<string, array<string, mixed>>
445
         */
446
        private function normalizeMetadata(array $metadata): array
1✔
447
        {
448
                foreach ($metadata as $name => $meta) {
1✔
449
                        if (!is_array($meta)) {
1✔
450
                                $metadata[$name] = $meta = [self::Value => $meta];
1✔
451
                        }
452

453
                        if (array_key_exists(self::Value, $meta)) {
1✔
454
                                if (is_scalar($meta[self::Value])) {
1✔
455
                                        $metadata[$name][self::Value] = $meta[self::Value] === false
1✔
456
                                                ? '0'
1✔
457
                                                : (string) $meta[self::Value];
1✔
458
                                }
459

460
                                $metadata[$name]['fixity'] = self::Constant;
1✔
461
                        }
462
                }
463

464
                return $metadata;
1✔
465
        }
466

467

468
        private function parseMask(string $path): void
1✔
469
        {
470
                // <parameter-name[=default] [pattern]> or [ or ] or ?...
471
                $parts = Strings::split($path, '/<([^<>= ]+)(=[^<> ]*)? *([^<>]*)>|(\[!?|\]|\s*\?.*)/');
1✔
472

473
                $i = count($parts) - 1;
1✔
474
                if ($i === 0) {
1✔
475
                        $this->re = '#' . preg_quote($parts[0], '#') . '/?$#DA';
1✔
476
                        $this->sequence = [$parts[0]];
1✔
477
                        return;
1✔
478
                }
479

480
                if ($this->parseQuery($parts)) {
1✔
481
                        $i -= 5;
1✔
482
                }
483

484
                $brackets = 0; // optional level
1✔
485
                $re = '';
1✔
486
                $sequence = [];
1✔
487
                $autoOptional = true;
1✔
488

489
                do {
490
                        $part = $parts[$i]; // part of path
1✔
491
                        if (strpbrk($part, '<>') !== false) {
1✔
492
                                throw new Nette\InvalidArgumentException("Unexpected '$part' in mask '$this->mask'.");
1✔
493
                        }
494

495
                        array_unshift($sequence, $part);
1✔
496
                        $re = preg_quote($part, '#') . $re;
1✔
497
                        if ($i === 0) {
1✔
498
                                break;
1✔
499
                        }
500

501
                        $i--;
1✔
502

503
                        $part = $parts[$i]; // [ or ]
1✔
504
                        if ($part === '[' || $part === ']' || $part === '[!') {
1✔
505
                                $brackets += $part[0] === '[' ? -1 : 1;
1✔
506
                                if ($brackets < 0) {
1✔
507
                                        throw new Nette\InvalidArgumentException("Unexpected '$part' in mask '$this->mask'.");
1✔
508
                                }
509

510
                                array_unshift($sequence, $part);
1✔
511
                                $re = ($part[0] === '[' ? '(?:' : ')?') . $re;
1✔
512
                                $i -= 4;
1✔
513
                                continue;
1✔
514
                        }
515

516
                        $pattern = trim($parts[$i--]); // validation condition (as regexp)
1✔
517
                        $default = $parts[$i--]; // default value
1✔
518
                        $name = $parts[$i--]; // parameter name
1✔
519
                        array_unshift($sequence, $name);
1✔
520

521
                        if ($name[0] === '?') { // "foo" parameter
1✔
522
                                $name = substr($name, 1);
1✔
523
                                $re = $pattern
1✔
524
                                        ? '(?:' . preg_quote($name, '#') . "|$pattern)$re"
1✔
525
                                        : preg_quote($name, '#') . $re;
1✔
526
                                $sequence[1] = $name . $sequence[1];
1✔
527
                                continue;
1✔
528
                        }
529

530
                        // pattern, condition & metadata
531
                        $meta = ($this->metadata[$name] ?? []) + ($this->defaultMeta[$name] ?? $this->defaultMeta['#']);
1✔
532

533
                        if ($pattern === '' && isset($meta[self::Pattern])) {
1✔
534
                                $pattern = $meta[self::Pattern];
1✔
535
                        }
536

537
                        if ($default !== '') {
1✔
538
                                $meta[self::Value] = substr($default, 1);
1✔
539
                                $meta[self::Fixity] = self::InPath;
1✔
540
                        }
541

542
                        $meta[self::FilterTableOut] = empty($meta[self::FilterTable])
1✔
543
                                ? null
1✔
544
                                : array_flip($meta[self::FilterTable]);
1✔
545
                        if (array_key_exists(self::Value, $meta)) {
1✔
546
                                if (isset($meta[self::FilterTableOut][$meta[self::Value]])) {
1✔
547
                                        $meta[self::Default] = $meta[self::FilterTableOut][$meta[self::Value]];
×
548

549
                                } elseif (isset($meta[self::Value], $meta[self::FilterOut])) {
1✔
550
                                        $meta[self::Default] = $meta[self::FilterOut]($meta[self::Value]);
1✔
551

552
                                } else {
553
                                        $meta[self::Default] = $meta[self::Value];
1✔
554
                                }
555
                        }
556

557
                        $meta[self::Pattern] = $pattern;
1✔
558

559
                        // include in expression
560
                        $this->aliases['p' . $i] = $name;
1✔
561
                        $re = '(?P<p' . $i . '>(?U)' . $pattern . ')' . $re;
1✔
562
                        if ($brackets) { // is in brackets?
1✔
563
                                if (!isset($meta[self::Value])) {
1✔
564
                                        $meta[self::Value] = $meta[self::Default] = null;
1✔
565
                                }
566

567
                                $meta[self::Fixity] = self::InPath;
1✔
568

569
                        } elseif (isset($meta[self::Fixity])) {
1✔
570
                                if ($autoOptional) {
1✔
571
                                        $re = '(?:' . $re . ')?';
1✔
572
                                }
573

574
                                $meta[self::Fixity] = self::InPath;
1✔
575

576
                        } else {
577
                                $autoOptional = false;
1✔
578
                        }
579

580
                        $this->metadata[$name] = $meta;
1✔
581
                } while (true);
1✔
582

583
                if ($brackets) {
1✔
584
                        throw new Nette\InvalidArgumentException("Missing '[' in mask '$this->mask'.");
1✔
585
                }
586

587
                $this->re = '#' . $re . '/?$#DA';
1✔
588
                $this->sequence = $sequence;
1✔
589
        }
1✔
590

591

592
        /**
593
         * @param string[]  $parts
594
         */
595
        private function parseQuery(array $parts): bool
1✔
596
        {
597
                $query = $parts[count($parts) - 2] ?? '';
1✔
598
                if (!str_starts_with(ltrim($query), '?')) {
1✔
599
                        return false;
1✔
600
                }
601

602
                // name=<parameter-name [pattern]>
603
                $matches = Strings::matchAll($query, '/(?:([a-zA-Z0-9_.-]+)=)?<([^> ]+) *([^>]*)>/');
1✔
604

605
                foreach ($matches as [, $param, $name, $pattern]) { // $pattern is not used
1✔
606
                        $meta = ($this->metadata[$name] ?? []) + ($this->defaultMeta['?' . $name] ?? []);
1✔
607

608
                        if (array_key_exists(self::Value, $meta)) {
1✔
609
                                $meta[self::Fixity] = self::InQuery;
1✔
610
                        }
611

612
                        unset($meta[self::Pattern]);
1✔
613
                        $meta[self::FilterTableOut] = empty($meta[self::FilterTable])
1✔
614
                                ? null
1✔
615
                                : array_flip($meta[self::FilterTable]);
1✔
616

617
                        $this->metadata[$name] = $meta;
1✔
618
                        if ($param !== '') {
1✔
619
                                $this->xlat[$name] = $param;
1✔
620
                        }
621
                }
622

623
                return true;
1✔
624
        }
625

626

627
        /********************* Utilities ****************d*g**/
628

629

630
        /**
631
         * Rename keys in array.
632
         * @param array<string, mixed>  $arr
633
         * @param array<string, string>  $xlat
634
         * @return array<string, mixed>
635
         */
636
        private static function renameKeys(array $arr, array $xlat): array
1✔
637
        {
638
                if (!$xlat) {
1✔
639
                        return $arr;
1✔
640
                }
641

642
                $res = [];
1✔
643
                $occupied = array_flip($xlat);
1✔
644
                foreach ($arr as $k => $v) {
1✔
645
                        if (isset($xlat[$k])) {
1✔
646
                                $res[$xlat[$k]] = $v;
1✔
647

648
                        } elseif (!isset($occupied[$k])) {
1✔
649
                                $res[$k] = $v;
1✔
650
                        }
651
                }
652

653
                return $res;
1✔
654
        }
655

656

657
        /**
658
         * Url encode.
659
         */
660
        public static function param2path(string $s): string
1✔
661
        {
662
                // segment + "/", see https://datatracker.ietf.org/doc/html/rfc3986#appendix-A
663
                return (string) preg_replace_callback(
1✔
664
                        '#[^\w.~!$&\'()*+,;=:@"/-]#',
1✔
665
                        fn($m) => rawurlencode($m[0]),
1✔
666
                        $s,
1✔
667
                );
668
        }
669
}
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