Passed
Push — master ( 38258d...d09845 )
by Alexander
02:38
created

UrlGenerator   C

Complexity

Total Complexity 54

Size/Duplication

Total Lines 260
Duplicated Lines 0 %

Test Coverage

Coverage 100%

Importance

Changes 12
Bugs 0 Features 0
Metric Value
eloc 96
c 12
b 0
f 0
dl 0
loc 260
ccs 105
cts 105
cp 1
rs 6.4799
wmc 54

15 Methods

Rating   Name   Duplication   Size   Complexity  
A getUriPrefix() 0 3 1
A setEncodeRaw() 0 3 1
B ensureScheme() 0 20 7
A isRelative() 0 3 2
A missingParameters() 0 24 5
A generateAbsoluteFromLastMatchedRequest() 0 4 3
C generateAbsolute() 0 25 13
A addLocaleToPath() 0 4 2
A setLocales() 0 3 1
A __construct() 0 8 1
A setLocaleParameterName() 0 3 1
B generatePath() 0 36 7
A setUriPrefix() 0 3 1
A getLocales() 0 3 1
B generate() 0 41 8

How to fix   Complexity   

Complex Class

Complex classes like UrlGenerator often do a lot of different things. To break such a class down, we need to identify a cohesive component within that class. A common approach to find such a component is to look for fields/methods that share the same prefixes, or suffixes.

Once you have determined the fields that belong together, you can apply the Extract Class refactoring. If the component makes sense as a sub-class, Extract Subclass is also a candidate, and is often faster.

While breaking up the class, it is a good idea to analyze how other classes use UrlGenerator, and based on these observations, apply Extract Interface, too.

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