Passed
Pull Request — master (#24)
by Alexander
01:18
created

UrlGenerator::generateAbsolute()   B

Complexity

Conditions 11
Paths 20

Size

Total Lines 21
Code Lines 11

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 12
CRAP Score 11

Importance

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

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 Nyholm\Psr7\Uri;
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
        UrlMatcherInterface $matcher,
29
        RouteParser $parser = null
30
    ) {
31 26
        $this->matcher = $matcher;
32 26
        $this->routeCollection = $matcher->getRouteCollection();
0 ignored issues
show
Bug introduced by
The method getRouteCollection() does not exist on Yiisoft\Router\UrlMatcherInterface. It seems like you code against a sub-type of Yiisoft\Router\UrlMatcherInterface such as Yiisoft\Router\FastRoute\UrlMatcher. ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-call  annotation

32
        /** @scrutinizer ignore-call */ 
33
        $this->routeCollection = $matcher->getRouteCollection();
Loading history...
33 26
        $this->routeParser = $parser ?? new RouteParser\Std();
34
    }
35
36
    /**
37
     * {@inheritDoc}
38
     *
39
     * Replacements in FastRoute are written as `{name}` or `{name:<pattern>}`;
40
     * this method uses `FastRoute\RouteParser\Std` to search for the best route
41
     * match based on the available substitutions and generates a uri.
42
     *
43
     * @throws \RuntimeException if parameter value does not match its regex.
44
     */
45 26
    public function generate(string $name, array $parameters = []): string
46
    {
47 26
        $route = $this->routeCollection->getRoute($name);
48
49 25
        $parsedRoutes = array_reverse($this->routeParser->parse($route->getPattern()));
50 25
        if ($parsedRoutes === []) {
51
            throw new RouteNotFoundException($name);
52
        }
53
54 25
        $missingParameters = [];
55
56
        // One route pattern can correspond to multiple routes if it has optional parts
57 25
        foreach ($parsedRoutes as $parsedRouteParts) {
58
            // Check if all parameters can be substituted
59 25
            $missingParameters = $this->missingParameters($parsedRouteParts, $parameters);
60
61
            // If not all parameters can be substituted, try the next route
62 25
            if (!empty($missingParameters)) {
63 3
                continue;
64
            }
65
66 23
            return $this->generatePath($parameters, $parsedRouteParts);
67
        }
68
69
        // No valid route was found: list minimal required parameters
70 2
        throw new \RuntimeException(sprintf(
71 2
            'Route `%s` expects at least parameter values for [%s], but received [%s]',
72
            $name,
73 2
            implode(',', $missingParameters),
74 2
            implode(',', array_keys($parameters))
75
        ));
76
    }
77
78
    /**
79
     * Generates absolute URL from named route and parameters
80
     *
81
     * @param string $name name of the route
82
     * @param array $parameters parameter-value set
83
     * @param string|null $scheme host scheme
84
     * @param string|null $host host for manual setup
85
     * @return string URL generated
86
     * @throws RouteNotFoundException in case there is no route with the name specified
87
     */
88 15
    public function generateAbsolute(string $name, array $parameters = [], string $scheme = null, string $host = null): string
89
    {
90 15
        $url = $this->generate($name, $parameters);
91 15
        $route = $this->routeCollection->getRoute($name);
92
        /** @var Uri $uri */
93 15
        $uri = $this->matcher->getLastMatchedRequest() !== null ? $this->matcher->getLastMatchedRequest()->getUri() : null;
0 ignored issues
show
Bug introduced by
The method getLastMatchedRequest() does not exist on Yiisoft\Router\UrlMatcherInterface. It seems like you code against a sub-type of Yiisoft\Router\UrlMatcherInterface such as Yiisoft\Router\FastRoute\UrlMatcher. ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-call  annotation

93
        $uri = $this->matcher->/** @scrutinizer ignore-call */ getLastMatchedRequest() !== null ? $this->matcher->getLastMatchedRequest()->getUri() : null;
Loading history...
94 15
        $lastRequestScheme = $uri !== null ? $uri->getScheme() : null;
95
96 15
        if ($host !== null || ($host = $route->getHost()) !== null) {
97 10
            if ($scheme === null && !$this->isRelative($host)) {
98 6
                return rtrim($host, '/') . $url;
99
            }
100
101 4
            if ($scheme === '' && $host !== '' && $this->isRelative($host)) {
102 2
                $host = '//' . $host;
103
            }
104
105 4
            return $this->ensureScheme(rtrim($host, '/') . $url, $scheme ?? $lastRequestScheme);
106
        }
107
108 5
        return $uri === null ? $url : $this->generateAbsoluteFromLastMatchedRequest($url, $uri, $scheme);
109
    }
110
111 4
    private function generateAbsoluteFromLastMatchedRequest(string $url, Uri $uri, ?string $scheme): string
112
    {
113 4
        $port = $uri->getPort() === 80 || $uri->getPort() === null ? '' : ':' . $uri->getPort();
114 4
        return  $this->ensureScheme('://' . $uri->getHost() . $port . $url, $scheme ?? $uri->getScheme());
115
    }
116
117
    /**
118
     * Normalize URL by ensuring that it use specified scheme.
119
     *
120
     * If URL is relative or scheme is null, normalization is skipped.
121
     *
122
     * @param string $url the URL to process
123
     * @param string|null $scheme the URI scheme used in URL (e.g. `http` or `https`). Use empty string to
124
     * create protocol-relative URL (e.g. `//example.com/path`)
125
     * @return string the processed URL
126
     */
127 8
    private function ensureScheme(string $url, ?string $scheme): string
128
    {
129 8
        if ($scheme === null || $this->isRelative($url)) {
130
            return $url;
131
        }
132
133 8
        if (strpos($url, '//') === 0) {
134
            // e.g. //example.com/path/to/resource
135 2
            return $scheme === '' ? $url : "$scheme:$url";
136
        }
137
138 8
        if (($pos = strpos($url, '://')) !== false) {
139 8
            if ($scheme === '') {
140 3
                $url = substr($url, $pos + 1);
141
            } else {
142 5
                $url = $scheme . substr($url, $pos);
143
            }
144
        }
145
146 8
        return $url;
147
    }
148
149
    /**
150
     * Returns a value indicating whether a URL is relative.
151
     * A relative URL does not have host info part.
152
     * @param string $url the URL to be checked
153
     * @return bool whether the URL is relative
154
     */
155 14
    private function isRelative(string $url): bool
156
    {
157 14
        return strncmp($url, '//', 2) && strpos($url, '://') === false;
158
    }
159
160
161 23
    public function getUriPrefix(): string
162
    {
163 23
        return $this->uriPrefix;
164
    }
165
166
    public function setUriPrefix(string $prefix): void
167
    {
168
        $this->uriPrefix = $prefix;
169
    }
170
171
    /**
172
     * Checks for any missing route parameters
173
     * @param array $parts
174
     * @param array $substitutions
175
     * @return array with minimum required parameters if any are missing or an empty array if none are missing
176
     */
177 25
    private function missingParameters(array $parts, array $substitutions): array
178
    {
179 25
        $missingParameters = [];
180
181
        // Gather required parameters
182 25
        foreach ($parts as $part) {
183 25
            if (is_string($part)) {
184 25
                continue;
185
            }
186
187 9
            $missingParameters[] = $part[0];
188
        }
189
190
        // Check if all parameters exist
191 25
        foreach ($missingParameters as $parameter) {
192 9
            if (!array_key_exists($parameter, $substitutions)) {
193
                // Return the parameters so they can be used in an
194
                // exception if needed
195 3
                return $missingParameters;
196
            }
197
        }
198
199
        // All required parameters are available
200 23
        return [];
201
    }
202
203 23
    private function generatePath(array $parameters, array $parts): string
204
    {
205 23
        $notSubstitutedParams = $parameters;
206 23
        $path = $this->getUriPrefix();
207
208 23
        foreach ($parts as $part) {
209 23
            if (is_string($part)) {
210
                // Append the string
211 23
                $path .= $part;
212 23
                continue;
213
            }
214
215
            // Check substitute value with regex
216 6
            $pattern = str_replace('~', '\~', $part[1]);
217 6
            if (preg_match('~^' . $pattern . '$~', (string)$parameters[$part[0]]) === 0) {
218 1
                throw new \RuntimeException(
219 1
                    sprintf(
220 1
                        'Parameter value for [%s] did not match the regex `%s`',
221 1
                        $part[0],
222 1
                        $part[1]
223
                    )
224
                );
225
            }
226
227
            // Append the substituted value
228 5
            $path .= $parameters[$part[0]];
229 5
            unset($notSubstitutedParams[$part[0]]);
230
        }
231
232 22
        return $path . ($notSubstitutedParams !== [] ? '?' . http_build_query($notSubstitutedParams) : '');
233
    }
234
}
235