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

willy68 / pg-router / 16055434026

03 Jul 2025 10:48AM UTC coverage: 81.095% (+4.1%) from 77.011%
16055434026

push

github

willy68
First commit

8 of 8 new or added lines in 1 file covered. (100.0%)

10 existing lines in 1 file now uncovered.

785 of 968 relevant lines covered (81.1%)

7.43 hits per line

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

86.84
/src/Generator/UrlGenerator.php
1
<?php
2

3
declare(strict_types=1);
4

5
namespace Pg\Router\Generator;
6

7
use Pg\Router\Exception\MissingAttributeException;
8
use Pg\Router\Exception\RouteNotFoundException;
9
use Pg\Router\Exception\RuntimeException;
10
use Pg\Router\Regex\Regex;
11
use Pg\Router\Route;
12
use Pg\Router\RouteCollectionInterface;
13
use Pg\Router\RouterInterface;
14

15
use function explode;
16
use function implode;
17
use function is_array;
18
use function preg_match;
19
use function preg_match_all;
20
use function preg_split;
21
use function rawurlencode;
22
use function sprintf;
23
use function str_replace;
24
use function str_starts_with;
25
use function strtr;
26

27
class UrlGenerator implements GeneratorInterface
28
{
29
    protected Route $route;
30
    protected array $data = [];
31
    protected array $repl = [];
32
    protected RouterInterface|RouteCollectionInterface $router;
33

34

35
    public function __construct(RouteCollectionInterface|RouterInterface $router)
10✔
36
    {
37
        $this->router = $router;
10✔
38
    }
39

40
    public function generate(string $name, array $attributes = []): string
10✔
41
    {
42
        $route = $this->router->getRouteName($name);
10✔
43

44
        if (null === $route) {
10✔
45
            throw new RouteNotFoundException(
2✔
46
                sprintf('Route with name [%s] not found', $name)
2✔
47
            );
2✔
48
        }
49

50
        $this->route = $route;
9✔
51
        $url = $this->route->getPath();
9✔
52
        $this->data = $attributes;
9✔
53
        $urlStartWithBracket = str_starts_with($url, '[');
9✔
54

55
        if ($urlStartWithBracket && empty($this->data)) {
9✔
56
            return '/';
2✔
57
        }
58

59
        $urlWithoutClosingBracket = rtrim($url, ']');
9✔
60
        $parts = preg_split('~' . Regex::REGEX . '(*SKIP)(*F)|\[~x', $urlWithoutClosingBracket);
9✔
61
        $base = (trim($parts[0]) ?? '') ?: '/';
9✔
62
        $optionalSegments = isset($parts[1]) ? '[' . $parts[1] . ']' : '';
9✔
63

64
        if ($base !== '/') {
9✔
65
            $this->buildTokenReplacements($base);
7✔
66
        }
67

68
        if ($optionalSegments !== '') {
8✔
69
            $optionalParts = explode(';', $parts[1]);
3✔
70
            if ($urlStartWithBracket && !str_starts_with(ltrim($optionalParts[0]), '/')) {
3✔
71
                $optionalParts[0] = '/' . $optionalParts[0];
1✔
72
            }
73
            $this->buildOptionalReplacements($optionalSegments, $optionalParts);
3✔
74
        }
75

76
        return strtr($url, $this->repl);
8✔
77
    }
78

79
    /**
80
     *
81
     * Builds urlencoded data for token replacements.
82
     *
83
     * @param string $base
84
     * @return void
85
     */
86
    protected function buildTokenReplacements(string $base): void
7✔
87
    {
88
        if (preg_match_all('~' . Regex::REGEX . '~x', $base, $matches, PREG_SET_ORDER) > 0) {
7✔
89
            foreach ($matches as $match) {
6✔
90
                $name = $match[1];
6✔
91
                $token = $match[2] ?? "([^/]+)";
6✔
92

93
                if (!isset($this->data[$name])) {
6✔
UNCOV
94
                    throw new MissingAttributeException(sprintf(
×
UNCOV
95
                        'Parameter value for [%s] is missing for route [%s]',
×
UNCOV
96
                        $name,
×
UNCOV
97
                        $this->route->getName()
×
UNCOV
98
                    ));
×
99
                }
100

101
                $value = (string)$this->data[$name];
6✔
102

103
                if (!preg_match('~^' . $token . '$~x', $value)) {
6✔
104
                    throw new RuntimeException(sprintf(
1✔
105
                        'Parameter value for [%s] did not match the regex `%s` in route [%s]',
1✔
106
                        $name,
1✔
107
                        $token,
1✔
108
                        $this->route->getName()
1✔
109
                    ));
1✔
110
                }
111

112
                $this->repl[$match[0]] = rawurlencode($value);
5✔
113
            }
114
        }
115
    }
116

117
    /**
118
     *
119
     * Builds replacements for attributes in the generated path.
120
     *
121
     * @param string $optionalSegment
122
     * @param array $optionalParts
123
     * @return void
124
     */
125
    protected function buildOptionalReplacements(string $optionalSegment = '', array $optionalParts = []): void
3✔
126
    {
127
        $replacements = [];
3✔
128

129
        foreach ($optionalParts as $part) {
3✔
130
            if (preg_match_all('~' . Regex::REGEX . '~x', $part, $exMatches, PREG_SET_ORDER) > 0) {
3✔
131
                $tokenStr = [];
3✔
132
                $names = [];
3✔
133

134
                foreach ($exMatches as $match) {
3✔
135
                    $tokenStr[] = $match[0];
3✔
136
                    $names[] = isset($match[2]) ? [$match[1], $match[2]] : $match[1];
3✔
137
                }
138

139
                $replacement = $this->buildOptionalReplacement($names, $tokenStr, trim($part));
3✔
140
                if ($replacement !== '') {
3✔
141
                    $replacements[] = $replacement;
3✔
142
                }
143
            }
144
        }
145

146
        if ($replacements !== []) {
3✔
147
            $this->repl[$optionalSegment] = implode('', $replacements);
3✔
148
        }
149
    }
150

151
    /**
152
     *
153
     * Builds the optional replacement for attribute names.
154
     *
155
     * @param array $names The optional replacement names.
156
     * @param array $tokenStr
157
     * @param string $subject
158
     * @return string
159
     */
160
    protected function buildOptionalReplacement(array $names, array $tokenStr, string $subject): string
3✔
161
    {
162
        $replacements = [];
3✔
163

164
        foreach ($names as $name) {
3✔
165
            $token = is_array($name) ? $name[1] : '([^/]+)';
3✔
166
            $paramName = is_array($name) ? $name[0] : $name;
3✔
167

168
            if (!isset($this->data[$paramName])) {
3✔
169
                return ''; // Options are sequentially optional
3✔
170
            }
171

172
            $value = (string)$this->data[$paramName];
3✔
173

174
            if (!preg_match('~^' . $token . '$~x', $value)) {
3✔
UNCOV
175
                throw new RuntimeException(sprintf(
×
UNCOV
176
                    'Parameter value for [%s] did not match the regex `%s`',
×
UNCOV
177
                    $paramName,
×
UNCOV
178
                    $token
×
UNCOV
179
                ));
×
180
            }
181

182
            $replacements[] = rawurlencode($value);
3✔
183
        }
184

185
        return str_replace($tokenStr, $replacements, $subject);
3✔
186
    }
187
}
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