UrlGenerator::getUriPrefix()   A
last analyzed

Complexity

Conditions 1
Paths 1

Size

Total Lines 3
Code Lines 1

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 2
CRAP Score 1

Importance

Changes 1
Bugs 0 Features 0
Metric Value
cc 1
eloc 1
c 1
b 0
f 0
nc 1
nop 0
dl 0
loc 3
rs 10
ccs 2
cts 2
cp 1
crap 1
1
<?php
2
3
declare(strict_types=1);
4
5
namespace Yiisoft\Router\FastRoute;
6
7
use FastRoute\RouteParser;
8
use InvalidArgumentException;
9
use Psr\Http\Message\UriInterface;
10
use RuntimeException;
11
use Stringable;
12
use Yiisoft\Router\RouteCollectionInterface;
13
use Yiisoft\Router\RouteNotFoundException;
14
use Yiisoft\Router\CurrentRoute;
15
use Yiisoft\Router\UrlGeneratorInterface;
16
17
use function array_key_exists;
18
use function array_keys;
19
use function implode;
20
use function is_string;
21
use function preg_match;
22
23
final class UrlGenerator implements UrlGeneratorInterface
24
{
25
    private string $uriPrefix = '';
26
27
    /**
28
     * @var array<string,string>
29
     */
30
    private array $defaultArguments = [];
31
    private bool $encodeRaw = true;
32
    private RouteParser $routeParser;
33
34 53
    public function __construct(
35
        private RouteCollectionInterface $routeCollection,
36
        private ?CurrentRoute $currentRoute = null,
37
        RouteParser $parser = null
38
    ) {
39 53
        $this->routeParser = $parser ?? new RouteParser\Std();
40
    }
41
42
    /**
43
     * {@inheritDoc}
44
     *
45
     * Replacements in FastRoute are written as `{name}` or `{name:<pattern>}`;
46
     * this method uses {@see RouteParser\Std} to search for the best route
47
     * match based on the available substitutions and generates a URI.
48
     *
49
     * @throws RuntimeException If parameter value does not match its regex.
50
     */
51 48
    public function generate(string $name, array $arguments = [], array $queryParameters = []): string
52
    {
53 48
        $arguments = array_map('\strval', array_merge($this->defaultArguments, $arguments));
54 48
        $route = $this->routeCollection->getRoute($name);
55
        /** @var list<list<list<string>|string>> $parsedRoutes */
56 47
        $parsedRoutes = array_reverse($this->routeParser->parse($route->getData('pattern')));
57 47
        if ($parsedRoutes === []) {
0 ignored issues
show
introduced by
The condition $parsedRoutes === array() is always false.
Loading history...
58 1
            throw new RouteNotFoundException($name);
59
        }
60
61 46
        $missingArguments = [];
62
63
        // One route pattern can correspond to multiple routes if it has optional parts.
64 46
        foreach ($parsedRoutes as $parsedRouteParts) {
65
            // Check if all arguments can be substituted
66 46
            $missingArguments = $this->missingArguments($parsedRouteParts, $arguments);
67
68
            // If not all arguments can be substituted, try the next route.
69 46
            if (!empty($missingArguments)) {
70 3
                continue;
71
            }
72
73 44
            return $this->generatePath($arguments, $queryParameters, $parsedRouteParts);
74
        }
75
76
        // No valid route was found: list minimal required parameters.
77 2
        throw new RuntimeException(
78 2
            sprintf(
79 2
                'Route `%s` expects at least argument values for [%s], but received [%s]',
80 2
                $name,
81 2
                implode(',', $missingArguments),
82 2
                implode(',', array_keys($arguments))
83 2
            )
84 2
        );
85
    }
86
87 18
    public function generateAbsolute(
88
        string $name,
89
        array $arguments = [],
90
        array $queryParameters = [],
91
        string $scheme = null,
92
        string $host = null
93
    ): string {
94 18
        $url = $this->generate($name, $arguments, $queryParameters);
95 18
        $route = $this->routeCollection->getRoute($name);
96 18
        $uri = $this->currentRoute && $this->currentRoute->getUri() !== null ? $this->currentRoute->getUri() : null;
97 18
        $lastRequestScheme = $uri?->getScheme();
98
99 18
        if ($host !== null || ($host = $route->getData('host')) !== null) {
100 11
            if ($scheme === null && !$this->isRelative($host)) {
101 7
                return rtrim($host, '/') . $url;
102
            }
103
104 5
            if ((empty($scheme) || $lastRequestScheme === null) && $host !== '' && $this->isRelative($host)) {
105 3
                $host = '//' . $host;
106
            }
107
108 5
            return $this->ensureScheme(rtrim($host, '/') . $url, $scheme ?? $lastRequestScheme);
109
        }
110
111 7
        return $uri === null ? $url : $this->generateAbsoluteFromLastMatchedRequest($url, $uri, $scheme);
112
    }
113
114
    /**
115
     * {@inheritdoc}
116
     */
117 13
    public function generateFromCurrent(array $replacedArguments, array $queryParameters = [], string $fallbackRouteName = null): string
118
    {
119 13
        if ($this->currentRoute === null || $this->currentRoute->getName() === null) {
120 6
            if ($fallbackRouteName !== null) {
121 3
                return $this->generate($fallbackRouteName, $replacedArguments);
122
            }
123
124 3
            if ($this->currentRoute !== null && $this->currentRoute->getUri() !== null) {
125 1
                return $this->currentRoute->getUri()->getPath();
126
            }
127
128 2
            throw new RuntimeException('Current route is not detected.');
129
        }
130
131 7
        if ($this->currentRoute->getUri() !== null) {
132 7
            $currentQueryParameters = [];
133 7
            parse_str($this->currentRoute->getUri()->getQuery(), $currentQueryParameters);
134 7
            $queryParameters = array_merge($currentQueryParameters, $queryParameters);
135
        }
136
137
        /** @psalm-suppress PossiblyNullArgument Checked route name on null above. */
138 7
        return $this->generate(
139 7
            $this->currentRoute->getName(),
140 7
            array_merge($this->currentRoute->getArguments(), $replacedArguments),
141 7
            $queryParameters,
142 7
        );
143
    }
144
145
    /**
146
     * @psalm-param null|object|scalar $value
147
     */
148 15
    public function setDefaultArgument(string $name, $value): void
149
    {
150 15
        if (!is_scalar($value) && !$value instanceof Stringable && $value !== null) {
151
            throw new InvalidArgumentException('Default should be either scalar value or an instance of \Stringable.');
152
        }
153 15
        $this->defaultArguments[$name] = (string) $value;
154
    }
155
156 6
    private function generateAbsoluteFromLastMatchedRequest(string $url, UriInterface $uri, ?string $scheme): string
157
    {
158 6
        $port = '';
159 6
        $uriPort = $uri->getPort();
160 6
        if ($uriPort !== 80 && $uriPort !== null) {
161 1
            $port = ':' . $uriPort;
162
        }
163
164 6
        return $this->ensureScheme('://' . $uri->getHost() . $port . $url, $scheme ?? $uri->getScheme());
165
    }
166
167
    /**
168
     * Normalize URL by ensuring that it use specified scheme.
169
     *
170
     * If URL is relative or scheme is null, normalization is skipped.
171
     *
172
     * @param string $url The URL to process.
173
     * @param string|null $scheme The URI scheme used in URL (e.g. `http` or `https`). Use empty string to
174
     * create protocol-relative URL (e.g. `//example.com/path`).
175
     *
176
     * @return string The processed URL.
177
     */
178 11
    private function ensureScheme(string $url, ?string $scheme): string
179
    {
180 11
        if ($scheme === null || $this->isRelative($url)) {
181 1
            return $url;
182
        }
183
184 11
        if (str_starts_with($url, '//')) {
185
            // e.g. //example.com/path/to/resource
186 3
            return $scheme === '' ? $url : "$scheme:$url";
187
        }
188
189 10
        if (($pos = strpos($url, '://')) !== false) {
190 10
            if ($scheme === '') {
191 3
                $url = substr($url, $pos + 1);
192
            } else {
193 7
                $url = $scheme . substr($url, $pos);
194
            }
195
        }
196
197 10
        return $url;
198
    }
199
200
    /**
201
     * Returns a value indicating whether a URL is relative.
202
     * A relative URL does not have host info part.
203
     *
204
     * @param string $url The URL to be checked.
205
     *
206
     * @return bool Whether the URL is relative.
207
     */
208 17
    private function isRelative(string $url): bool
209
    {
210 17
        return strncmp($url, '//', 2) && !str_contains($url, '://');
211
    }
212
213 45
    public function getUriPrefix(): string
214
    {
215 45
        return $this->uriPrefix;
216
    }
217
218 2
    public function setEncodeRaw(bool $encodeRaw): void
219
    {
220 2
        $this->encodeRaw = $encodeRaw;
221
    }
222
223 3
    public function setUriPrefix(string $name): void
224
    {
225 3
        $this->uriPrefix = $name;
226
    }
227
228
    /**
229
     * Checks for any missing route parameters.
230
     *
231
     * @param list<list<string>|string> $parts
0 ignored issues
show
Bug introduced by
The type Yiisoft\Router\FastRoute\list was not found. Maybe you did not declare it correctly or list all dependencies?

The issue could also be caused by a filter entry in the build configuration. If the path has been excluded in your configuration, e.g. excluded_paths: ["lib/*"], you can move it to the dependency path list as follows:

filter:
    dependency_paths: ["lib/*"]

For further information see https://scrutinizer-ci.com/docs/tools/php/php-scrutinizer/#list-dependency-paths

Loading history...
232
     *
233
     * @return string[] Either an array containing missing required parameters or an empty array if none are missing.
234
     */
235 46
    private function missingArguments(array $parts, array $substitutions): array
236
    {
237 46
        $missingArguments = [];
238
239
        // Gather required arguments.
240 46
        foreach ($parts as $part) {
241 46
            if (is_string($part)) {
242 46
                continue;
243
            }
244
245 26
            $missingArguments[] = $part[0];
246
        }
247
248
        // Check if all arguments exist.
249 46
        foreach ($missingArguments as $argument) {
250 26
            if (!array_key_exists($argument, $substitutions)) {
251
                // Return the arguments, so they can be used in an
252
                // exception if needed.
253 3
                return $missingArguments;
254
            }
255
        }
256
257
        // All required arguments are available.
258 44
        return [];
259
    }
260
261
    /**
262
     * @param array<string,string> $arguments
263
     * @param list<list<string>|string> $parts
264
     */
265 44
    private function generatePath(array $arguments, array $queryParameters, array $parts): string
266
    {
267 44
        $path = $this->getUriPrefix();
268
269 44
        foreach ($parts as $part) {
270 44
            if (is_string($part)) {
271
                // Append the string.
272 44
                $path .= $part;
273 44
                continue;
274
            }
275
276 23
            if ($arguments[$part[0]] !== '') {
277
                // Check substitute value with regex.
278 23
                $pattern = str_replace('~', '\~', $part[1]);
279 23
                if (preg_match('~^' . $pattern . '$~', $arguments[$part[0]]) === 0) {
280 1
                    throw new RuntimeException(
281 1
                        sprintf(
282 1
                            'Argument value for [%s] did not match the regex `%s`',
283 1
                            $part[0],
284 1
                            $part[1]
285 1
                        )
286 1
                    );
287
                }
288
289
                // Append the substituted value.
290 22
                $path .= $this->encodeRaw
291 22
                    ? rawurlencode($arguments[$part[0]])
292 1
                    : urlencode($arguments[$part[0]]);
293
            }
294
        }
295
296 43
        $path = str_replace('//', '/', $path);
297
298 43
        $queryString = '';
299 43
        if (!empty($queryParameters)) {
300 7
            $queryString = http_build_query($queryParameters);
301
        }
302
303 43
        return $path . (!empty($queryString) ? '?' . $queryString : '');
304
    }
305
}
306