Test Failed
Pull Request — master (#94)
by Dmitriy
01:56
created

UrlGenerator::getUriPrefix()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 3
Code Lines 1

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 1
CRAP Score 1

Importance

Changes 1
Bugs 0 Features 0
Metric Value
cc 1
eloc 1
nc 1
nop 0
dl 0
loc 3
ccs 1
cts 1
cp 1
crap 1
rs 10
c 1
b 0
f 0
1
<?php
2
3
declare(strict_types=1);
4
5
namespace Yiisoft\Router\FastRoute;
6
7
use FastRoute\RouteParser;
8
use Psr\Http\Message\UriInterface;
9
use RuntimeException;
10
use Yiisoft\Router\RouteCollectionInterface;
11
use Yiisoft\Router\RouteNotFoundException;
12
use Yiisoft\Router\CurrentRoute;
13
use Yiisoft\Router\UrlGeneratorInterface;
14
15
use function array_key_exists;
16
use function array_keys;
17
use function implode;
18
use function is_string;
19
use function preg_match;
20
21
final class UrlGenerator implements UrlGeneratorInterface
22
{
23
    private string $uriPrefix = '';
24
    private array $defaults = [];
25
    private bool $encodeRaw = true;
26
    private RouteCollectionInterface $routeCollection;
27
    private ?CurrentRoute $currentRoute;
28
    private RouteParser $routeParser;
29
30
    public function __construct(
31
        RouteCollectionInterface $routeCollection,
32 36
        CurrentRoute $currentRoute = null,
33
        RouteParser $parser = null
34
    ) {
35
        $this->currentRoute = $currentRoute;
36
        $this->routeCollection = $routeCollection;
37 36
        $this->routeParser = $parser ?? new RouteParser\Std();
38 36
    }
39 36
40 36
    /**
41
     * {@inheritDoc}
42
     *
43
     * Replacements in FastRoute are written as `{name}` or `{name:<pattern>}`;
44
     * this method uses {@see RouteParser\Std} to search for the best route
45
     * match based on the available substitutions and generates a URI.
46
     *
47
     * @throws RuntimeException If parameter value does not match its regex.
48
     */
49
    public function generate(string $name, array $parameters = []): string
50
    {
51 34
        $parameters = array_map('\strval', array_merge($this->defaults, $parameters));
52
53 34
        $route = $this->routeCollection->getRoute($name);
54
        /** @psalm-var list<list<string|list<string>>> $parsedRoutes */
55
        $parsedRoutes = array_reverse($this->routeParser->parse($route->getData('pattern')));
56 34
        if ($parsedRoutes === []) {
57 34
            throw new RouteNotFoundException($name);
58 34
        }
59
60 3
        $missingParameters = [];
61 3
62 3
        // One route pattern can correspond to multiple routes if it has optional parts.
63 3
        foreach ($parsedRoutes as $parsedRouteParts) {
64
            // Check if all parameters can be substituted
65
            $missingParameters = $this->missingParameters($parsedRouteParts, $parameters);
66 34
67
            // If not all parameters can be substituted, try the next route.
68 33
            if (!empty($missingParameters)) {
69 33
                continue;
70 1
            }
71
72
            return $this->generatePath($parameters, $parsedRouteParts);
73 32
        }
74
75
        // No valid route was found: list minimal required parameters.
76 32
        throw new RuntimeException(
77
            sprintf(
78 32
                'Route `%s` expects at least parameter values for [%s], but received [%s]',
79
                $name,
80
                implode(',', $missingParameters),
81 32
                implode(',', array_keys($parameters))
82 3
            )
83
        );
84
    }
85 30
86
    public function generateAbsolute(
87
        string $name,
88
        array $parameters = [],
89 2
        string $scheme = null,
90 2
        string $host = null
91 2
    ): string {
92
        $parameters = array_map('\strval', $parameters);
93 2
94 2
        $url = $this->generate($name, $parameters);
95
        $route = $this->routeCollection->getRoute($name);
96
        $uri = $this->currentRoute && $this->currentRoute->getUri() !== null ? $this->currentRoute->getUri() : null;
97
        $lastRequestScheme = $uri !== null ? $uri->getScheme() : null;
98
99 17
        if ($host !== null || ($host = $route->getData('host')) !== null) {
100
            if ($scheme === null && !$this->isRelative($host)) {
101
                return rtrim($host, '/') . $url;
102
            }
103
104
            if ((empty($scheme) || $lastRequestScheme === null) && $host !== '' && $this->isRelative($host)) {
105 17
                $host = '//' . $host;
106
            }
107 17
108 17
            return $this->ensureScheme(rtrim($host, '/') . $url, $scheme ?? $lastRequestScheme);
109 17
        }
110 17
111
        return $uri === null ? $url : $this->generateAbsoluteFromLastMatchedRequest($url, $uri, $scheme);
112 17
    }
113 11
114 7
    public function generateCurrent(array $replacedParams, string $fallbackRouteName = null): string
115
    {
116
        if ($this->currentRoute === null || $this->currentRoute->getName() === null) {
117 5
            if ($fallbackRouteName !== null) {
118 3
                $fallbackRoute = $this->routeCollection->getRoute($fallbackRouteName);
119
                return $this->generate(
120
                    $fallbackRouteName,
121 5
                    array_merge($fallbackRoute->getData('defaults'), $replacedParams)
122
                );
123
            }
124 6
125
            if ($this->currentRoute->getUri() !== null) {
0 ignored issues
show
Bug introduced by
The method getUri() does not exist on null. ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-call  annotation

125
            if ($this->currentRoute->/** @scrutinizer ignore-call */ getUri() !== null) {

This check looks for calls to methods that do not seem to exist on a given type. It looks for the method on the type itself as well as in inherited classes or implemented interfaces.

This is most likely a typographical error or the method has been renamed.

Loading history...
126
                return $this->currentRoute->getUri()->getPath();
127 5
            }
128
129 5
            throw new RuntimeException('Current route is not detected.');
130 5
        }
131 5
132 1
        return $this->generate(
133
            $this->currentRoute->getName(),
134
            array_merge($this->currentRoute->getArguments(), $replacedParams)
135 5
        );
136
    }
137
138
    public function setDefault(string $name, $value): void
139
    {
140
        $this->defaults[$name] = $value;
141
    }
142
143
    private function generateAbsoluteFromLastMatchedRequest(string $url, UriInterface $uri, ?string $scheme): string
144
    {
145
        $port = '';
146
        $uriPort = $uri->getPort();
147
        if ($uriPort !== 80 && $uriPort !== null) {
148
            $port = ':' . $uriPort;
149 10
        }
150
151 10
        return $this->ensureScheme('://' . $uri->getHost() . $port . $url, $scheme ?? $uri->getScheme());
152 1
    }
153
154
    /**
155 10
     * Normalize URL by ensuring that it use specified scheme.
156
     *
157 3
     * If URL is relative or scheme is null, normalization is skipped.
158
     *
159
     * @param string $url The URL to process.
160 9
     * @param string|null $scheme The URI scheme used in URL (e.g. `http` or `https`). Use empty string to
161 9
     * create protocol-relative URL (e.g. `//example.com/path`).
162 3
     *
163
     * @return string The processed URL.
164 6
     */
165
    private function ensureScheme(string $url, ?string $scheme): string
166
    {
167
        if ($scheme === null || $this->isRelative($url)) {
168 9
            return $url;
169
        }
170
171
        if (strpos($url, '//') === 0) {
172
            // e.g. //example.com/path/to/resource
173
            return $scheme === '' ? $url : "$scheme:$url";
174
        }
175
176
        if (($pos = strpos($url, '://')) !== false) {
177
            if ($scheme === '') {
178
                $url = substr($url, $pos + 1);
179 16
            } else {
180
                $url = $scheme . substr($url, $pos);
181 16
            }
182
        }
183
184 31
        return $url;
185
    }
186 31
187
    /**
188
     * Returns a value indicating whether a URL is relative.
189 1
     * A relative URL does not have host info part.
190
     *
191 1
     * @param string $url The URL to be checked.
192 1
     *
193
     * @return bool Whether the URL is relative.
194 3
     */
195
    private function isRelative(string $url): bool
196 3
    {
197 3
        return strncmp($url, '//', 2) && strpos($url, '://') === false;
198
    }
199 1
200
    public function getUriPrefix(): string
201 1
    {
202
        return $this->uriPrefix;
203
    }
204 4
205
    public function setEncodeRaw(bool $encodeRaw): void
206 4
    {
207 4
        $this->encodeRaw = $encodeRaw;
208
    }
209 3
210
    public function setUriPrefix(string $name): void
211 3
    {
212 3
        $this->uriPrefix = $name;
213
    }
214
215
    /**
216
     * Checks for any missing route parameters.
217
     *
218
     * @param array $parts
219
     * @param array $substitutions
220
     *
221
     * @return string[] Either an array containing missing required parameters or an empty array if none are missing.
222
     *
223
     * @psalm-param list<string|list<string>> $parts
224 32
     */
225
    private function missingParameters(array $parts, array $substitutions): array
226 32
    {
227
        $missingParameters = [];
228
229 32
        // Gather required parameters.
230 32
        foreach ($parts as $part) {
231 32
            if (is_string($part)) {
232
                continue;
233
            }
234 10
235
            $missingParameters[] = $part[0];
236
        }
237
238 32
        // Check if all parameters exist.
239 10
        foreach ($missingParameters as $parameter) {
240
            if (!array_key_exists($parameter, $substitutions)) {
241
                // Return the parameters, so they can be used in an
242 3
                // exception if needed.
243
                return $missingParameters;
244
            }
245
        }
246
247 30
        // All required parameters are available.
248
        return [];
249
    }
250
251
    /**
252
     * @psalm-param array<string,string> $parameters
253
     * @psalm-param list<string|list<string>> $parts
254 30
     */
255
    private function generatePath(array $parameters, array $parts): string
256 30
    {
257 30
        $notSubstitutedParams = $parameters;
258
        $path = $this->getUriPrefix();
259 30
260 30
        foreach ($parts as $part) {
261
            if (is_string($part)) {
262 30
                // Append the string.
263 30
                $path .= $part;
264
                continue;
265
            }
266
267 7
            // Check substitute value with regex.
268 7
            $pattern = str_replace('~', '\~', $part[1]);
269 1
            if (preg_match('~^' . $pattern . '$~', $parameters[$part[0]]) === 0) {
270 1
                throw new RuntimeException(
271 1
                    sprintf(
272 1
                        'Parameter value for [%s] did not match the regex `%s`',
273 1
                        $part[0],
274
                        $part[1]
275
                    )
276
                );
277
            }
278
279 6
            // Append the substituted value.
280 6
            $path .= $this->encodeRaw
281 1
                ? rawurlencode($parameters[$part[0]])
282 6
                : urlencode($parameters[$part[0]]);
283
            unset($notSubstitutedParams[$part[0]]);
284
        }
285 29
286 3
        return $path . ($notSubstitutedParams !== [] ? '?' . http_build_query($notSubstitutedParams) : '');
287
    }
288
}
289