Passed
Push — master ( c66b4e...42eae5 )
by Alexander
02:13
created

UrlGenerator::setEncodeRaw()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 3
Code Lines 1

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 2
CRAP Score 1

Importance

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