Passed
Pull Request — master (#81)
by Rustam
02:36
created

UrlGenerator::generate()   B

Complexity

Conditions 10
Paths 21

Size

Total Lines 43
Code Lines 26

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 24
CRAP Score 10.0064

Importance

Changes 6
Bugs 1 Features 0
Metric Value
cc 10
eloc 26
c 6
b 1
f 0
nc 21
nop 2
dl 0
loc 43
ccs 24
cts 25
cp 0.96
crap 10.0064
rs 7.6666

How to fix   Complexity   

Long Method

Small methods make your code easier to understand, in particular if combined with a good name. Besides, if your method is small, finding a good name is usually much easier.

For example, if you find yourself adding comments to a method's body, this is usually a good sign to extract the commented part to a new method, and use the comment as a starting point when coming up with a good name for this new method.

Commonly applied refactorings include:

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