Passed
Pull Request — master (#24)
by Dmitriy
01:21
created

UrlGenerator::ensureScheme()   B

Complexity

Conditions 7
Paths 6

Size

Total Lines 20
Code Lines 10

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 9
CRAP Score 7.049

Importance

Changes 1
Bugs 0 Features 0
Metric Value
cc 7
eloc 10
c 1
b 0
f 0
nc 6
nop 2
dl 0
loc 20
rs 8.8333
ccs 9
cts 10
cp 0.9
crap 7.049
1
<?php
2
3
declare(strict_types=1);
4
5
namespace Yiisoft\Router\FastRoute;
6
7
use Nyholm\Psr7\Uri;
8
use Yiisoft\Router\Group;
9
use Yiisoft\Router\Route;
10
use Yiisoft\Router\RouteCollectionInterface;
11
use Yiisoft\Router\RouteCollectorInterface;
12
use Yiisoft\Router\RouteNotFoundException;
13
use Yiisoft\Router\UrlMatcherInterface;
14
use Yiisoft\Router\UrlGeneratorInterface;
15
use FastRoute\RouteParser;
16
17
use function array_key_exists;
18
use function array_keys;
19
use function implode;
20
use function is_string;
21
use function preg_match;
22
23
final class UrlGenerator implements UrlGeneratorInterface
24
{
25
    /** @var string */
26
    private string $uriPrefix = '';
27
28
    /**
29
     * Route collection
30
     *
31
     * @var RouteCollectionInterface
32
     */
33
    private RouteCollectionInterface $routeCollection;
34
35
    /**
36
     * @var UrlMatcherInterface $matcher
37
     */
38
    private UrlMatcherInterface $matcher;
39
40
    /**
41
     * @var RouteParser
42
     */
43
    private RouteParser $routeParser;
44
45
    /**
46
     * Constructor
47
     *
48
     * @param UrlMatcherInterface $matcher url matcher
49
     * @param RouteParser|null $parser
50
     */
51 26
    public function __construct(
52
        UrlMatcherInterface $matcher,
53
        RouteParser $parser = null
54
    ) {
55 26
        $this->matcher = $matcher;
56 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

56
        /** @scrutinizer ignore-call */ 
57
        $this->routeCollection = $matcher->getRouteCollection();
Loading history...
57 26
        $this->routeParser = $parser ?? new RouteParser\Std();
58
    }
59
60
    /**
61
     * Generate a URI based on a given route.
62
     *
63
     * Replacements in FastRoute are written as `{name}` or `{name:<pattern>}`;
64
     * this method uses `FastRoute\RouteParser\Std` to search for the best route
65
     * match based on the available substitutions and generates a uri.
66
     *
67
     * @param string $name Route name.
68
     * @param array $parameters Key/value option pairs to pass to the router for
69
     * purposes of generating a URI; takes precedence over options present
70
     * in route used to generate URI.
71
     *
72
     * @return string URI path generated.
73
     * @throws \RuntimeException if the route name is not known or a parameter value does not match its regex.
74
     */
75 26
    public function generate(string $name, array $parameters = []): string
76
    {
77 26
        $route = $this->routeCollection->getRoute($name);
78
79 25
        $parsedRoutes = array_reverse($this->routeParser->parse($route->getPattern()));
80 25
        if ($parsedRoutes === []) {
81
            throw new RouteNotFoundException($name);
82
        }
83
84 25
        $missingParameters = [];
85
86
        // One route pattern can correspond to multiple routes if it has optional parts
87 25
        foreach ($parsedRoutes as $parsedRouteParts) {
88
            // Check if all parameters can be substituted
89 25
            $missingParameters = $this->missingParameters($parsedRouteParts, $parameters);
90
91
            // If not all parameters can be substituted, try the next route
92 25
            if (!empty($missingParameters)) {
93 3
                continue;
94
            }
95
96 23
            return $this->generatePath($parameters, $parsedRouteParts);
97
        }
98
99
        // No valid route was found: list minimal required parameters
100 2
        throw new \RuntimeException(sprintf(
101 2
            'Route `%s` expects at least parameter values for [%s], but received [%s]',
102
            $name,
103 2
            implode(',', $missingParameters),
104 2
            implode(',', array_keys($parameters))
105
        ));
106
    }
107
108
    /**
109
     * Generates absolute URL from named route and parameters
110
     *
111
     * @param string $name name of the route
112
     * @param array $parameters parameter-value set
113
     * @param string|null $scheme host scheme
114
     * @param string|null $host host for manual setup
115
     * @return string URL generated
116
     * @throws RouteNotFoundException in case there is no route with the name specified
117
     */
118 15
    public function generateAbsolute(string $name, array $parameters = [], string $scheme = null, string $host = null): string
119
    {
120 15
        $url = $this->generate($name, $parameters);
121 15
        $route = $this->routeCollection->getRoute($name);
122
        /** @var Uri $uri */
123 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

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