Passed
Pull Request — master (#81)
by Rustam
05:44 queued 03:36
created

UrlGenerator::setLocales()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 3
Code Lines 1

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 0
CRAP Score 2

Importance

Changes 1
Bugs 0 Features 0
Metric Value
cc 1
eloc 1
c 1
b 0
f 0
nc 1
nop 1
dl 0
loc 3
ccs 0
cts 2
cp 0
crap 2
rs 10
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\CurrentRoute;
11
use Yiisoft\Router\RouteCollectionInterface;
12
use Yiisoft\Router\RouteNotFoundException;
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 bool $encodeRaw = true;
25
    private array $locales = [];
26
    private string $localeParameterName = '_locale';
27
    private RouteCollectionInterface $routeCollection;
28
    private ?CurrentRoute $currentRoute;
29
    private RouteParser $routeParser;
30
31 28
    public function __construct(
32
        RouteCollectionInterface $routeCollection,
33
        CurrentRoute $currentRoute = null,
34
        RouteParser $parser = null
35
    ) {
36 28
        $this->currentRoute = $currentRoute;
37 28
        $this->routeCollection = $routeCollection;
38 28
        $this->routeParser = $parser ?? new RouteParser\Std();
39 28
    }
40
41
    /**
42
     * {@inheritDoc}
43
     *
44
     * Replacements in FastRoute are written as `{name}` or `{name:<pattern>}`;
45
     * this method uses {@see RouteParser\Std} to search for the best route
46
     * match based on the available substitutions and generates a uri.
47
     *
48
     * @throws RuntimeException if parameter value does not match its regex.
49
     */
50 28
    public function generate(string $name, array $parameters = []): string
51
    {
52
        if (
53 28
            isset($parameters[$this->localeParameterName])
54 28
            && $this->locales !== []
55
        ) {
56
            $locale = $parameters[$this->localeParameterName];
57
            if (isset($this->locales[$locale])) {
58
                return '/' . $locale . $this->currentRoute->getUri()->getPath();
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

58
                return '/' . $locale . $this->currentRoute->/** @scrutinizer ignore-call */ getUri()->getPath();

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...
59
            }
60
        }
61 28
        $route = $this->routeCollection->getRoute($name);
62 27
        $parsedRoutes = array_reverse($this->routeParser->parse($route->getPattern()));
63 27
        if ($parsedRoutes === []) {
64
            throw new RouteNotFoundException($name);
65
        }
66
67 27
        $missingParameters = [];
68
69
        // One route pattern can correspond to multiple routes if it has optional parts
70 27
        foreach ($parsedRoutes as $parsedRouteParts) {
71
            // Check if all parameters can be substituted
72 27
            $missingParameters = $this->missingParameters($parsedRouteParts, $parameters);
73
74
            // If not all parameters can be substituted, try the next route
75 27
            if (!empty($missingParameters)) {
76 3
                continue;
77
            }
78
79 25
            return $this->generatePath($parameters, $parsedRouteParts);
80
        }
81
82
        // No valid route was found: list minimal required parameters
83 2
        throw new RuntimeException(
84 2
            sprintf(
85 2
                'Route `%s` expects at least parameter values for [%s], but received [%s]',
86
                $name,
87 2
                implode(',', $missingParameters),
88 2
                implode(',', array_keys($parameters))
89
            )
90
        );
91
    }
92
93 16
    public function generateAbsolute(
94
        string $name,
95
        array $parameters = [],
96
        string $scheme = null,
97
        string $host = null
98
    ): string {
99 16
        $url = $this->generate($name, $parameters);
100 16
        $route = $this->routeCollection->getRoute($name);
101
        /** @var UriInterface $uri */
102 16
        $uri = $this->currentRoute && $this->currentRoute->getUri() !== null ? $this->currentRoute->getUri() : null;
103 16
        $lastRequestScheme = $uri !== null ? $uri->getScheme() : null;
104
105 16
        if ($host !== null || ($host = $route->getHost()) !== null) {
106 11
            if ($scheme === null && !$this->isRelative($host)) {
107 7
                return rtrim($host, '/') . $url;
108
            }
109
110 5
            if ((empty($scheme) || $lastRequestScheme === null) && $host !== '' && $this->isRelative($host)) {
111 3
                $host = '//' . $host;
112
            }
113
114 5
            return $this->ensureScheme(rtrim($host, '/') . $url, $scheme ?? $lastRequestScheme);
115
        }
116
117 5
        return $uri === null ? $url : $this->generateAbsoluteFromLastMatchedRequest($url, $uri, $scheme);
118
    }
119
120 4
    private function generateAbsoluteFromLastMatchedRequest(string $url, UriInterface $uri, ?string $scheme): string
121
    {
122 4
        $port = $uri->getPort() === 80 || $uri->getPort() === null ? '' : ':' . $uri->getPort();
123 4
        return $this->ensureScheme('://' . $uri->getHost() . $port . $url, $scheme ?? $uri->getScheme());
124
    }
125
126
    /**
127
     * Normalize URL by ensuring that it use specified scheme.
128
     *
129
     * If URL is relative or scheme is null, normalization is skipped.
130
     *
131
     * @param string $url the URL to process
132
     * @param string|null $scheme the URI scheme used in URL (e.g. `http` or `https`). Use empty string to
133
     * create protocol-relative URL (e.g. `//example.com/path`)
134
     *
135
     * @return string the processed URL
136
     */
137 9
    private function ensureScheme(string $url, ?string $scheme): string
138
    {
139 9
        if ($scheme === null || $this->isRelative($url)) {
140 1
            return $url;
141
        }
142
143 9
        if (strpos($url, '//') === 0) {
144
            // e.g. //example.com/path/to/resource
145 3
            return $scheme === '' ? $url : "$scheme:$url";
146
        }
147
148 8
        if (($pos = strpos($url, '://')) !== false) {
149 8
            if ($scheme === '') {
150 3
                $url = substr($url, $pos + 1);
151
            } else {
152 5
                $url = $scheme . substr($url, $pos);
153
            }
154
        }
155
156 8
        return $url;
157
    }
158
159
    /**
160
     * Returns a value indicating whether a URL is relative.
161
     * A relative URL does not have host info part.
162
     *
163
     * @param string $url the URL to be checked
164
     *
165
     * @return bool whether the URL is relative
166
     */
167 15
    private function isRelative(string $url): bool
168
    {
169 15
        return strncmp($url, '//', 2) && strpos($url, '://') === false;
170
    }
171
172 25
    public function getUriPrefix(): string
173
    {
174 25
        return $this->uriPrefix;
175
    }
176
177 1
    public function setEncodeRaw(bool $encodeRaw): void
178
    {
179 1
        $this->encodeRaw = $encodeRaw;
180 1
    }
181
182
    public function setUriPrefix(string $name): void
183
    {
184
        $this->uriPrefix = $name;
185
    }
186
187
    public function setLocales(array $locales)
188
    {
189
        $this->locales = $locales;
190
    }
191
192
    public function localeParameterName(string $localParameterName)
193
    {
194
        $this->localeParameterName = $localParameterName;
195
    }
196
197
    /**
198
     * Checks for any missing route parameters
199
     *
200
     * @param array $parts
201
     * @param array $substitutions
202
     *
203
     * @return array with minimum required parameters if any are missing or an empty array if none are missing
204
     */
205 27
    private function missingParameters(array $parts, array $substitutions): array
206
    {
207 27
        $missingParameters = [];
208
209
        // Gather required parameters
210 27
        foreach ($parts as $part) {
211 27
            if (is_string($part)) {
212 27
                continue;
213
            }
214
215 10
            $missingParameters[] = $part[0];
216
        }
217
218
        // Check if all parameters exist
219 27
        foreach ($missingParameters as $parameter) {
220 10
            if (!array_key_exists($parameter, $substitutions)) {
221
                // Return the parameters so they can be used in an
222
                // exception if needed
223 3
                return $missingParameters;
224
            }
225
        }
226
227
        // All required parameters are available
228 25
        return [];
229
    }
230
231 25
    private function generatePath(array $parameters, array $parts): string
232
    {
233 25
        $notSubstitutedParams = $parameters;
234 25
        $path = $this->getUriPrefix();
235
236 25
        foreach ($parts as $part) {
237 25
            if (is_string($part)) {
238
                // Append the string
239 25
                $path .= $part;
240 25
                continue;
241
            }
242
243
            // Check substitute value with regex
244 7
            $pattern = str_replace('~', '\~', $part[1]);
245 7
            if (preg_match('~^' . $pattern . '$~', (string)$parameters[$part[0]]) === 0) {
246 1
                throw new RuntimeException(
247 1
                    sprintf(
248 1
                        'Parameter value for [%s] did not match the regex `%s`',
249 1
                        $part[0],
250 1
                        $part[1]
251
                    )
252
                );
253
            }
254
255
            // Append the substituted value
256 6
            $path .= $this->encodeRaw
257 6
                ? rawurlencode((string)$parameters[$part[0]])
258 1
                : urlencode((string)$parameters[$part[0]]);
259 6
            unset($notSubstitutedParams[$part[0]]);
260
        }
261
262 24
        return $path . ($notSubstitutedParams !== [] ? '?' . http_build_query($notSubstitutedParams) : '');
263
    }
264
}
265