Passed
Pull Request — master (#51)
by Wilmer
07:00
created

UrlGenerator::generateAbsolute()   C

Complexity

Conditions 12
Paths 40

Size

Total Lines 21
Code Lines 11

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 12
CRAP Score 12

Importance

Changes 3
Bugs 0 Features 0
Metric Value
cc 12
eloc 11
c 3
b 0
f 0
nc 40
nop 4
dl 0
loc 21
ccs 12
cts 12
cp 1
crap 12
rs 6.9666

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