Passed
Pull Request — master (#81)
by Rustam
10:57 queued 03:11
created

UrlGenerator   B

Complexity

Total Complexity 47

Size/Duplication

Total Lines 227
Duplicated Lines 0 %

Test Coverage

Coverage 94.32%

Importance

Changes 12
Bugs 0 Features 0
Metric Value
eloc 82
c 12
b 0
f 0
dl 0
loc 227
ccs 83
cts 88
cp 0.9432
rs 8.64
wmc 47

11 Methods

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

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