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

UrlGenerator::setUriPrefix()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 3
Code Lines 1

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 0
CRAP Score 2

Importance

Changes 1
Bugs 0 Features 0
Metric Value
cc 1
eloc 1
c 1
b 0
f 0
nc 1
nop 1
dl 0
loc 3
rs 10
ccs 0
cts 2
cp 0
crap 2
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
     * Generate a URI based on a given route.
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
     * @param string $name Route name.
44
     * @param array $parameters Key/value option pairs to pass to the router for
45
     * purposes of generating a URI; takes precedence over options present
46
     * in route used to generate URI.
47
     *
48
     * @return string URI path generated.
49
     * @throws \RuntimeException if the route name is not known or a parameter value does not match its regex.
50
     */
51 26
    public function generate(string $name, array $parameters = []): string
52
    {
53 26
        $route = $this->routeCollection->getRoute($name);
54
55 25
        $parsedRoutes = array_reverse($this->routeParser->parse($route->getPattern()));
56 25
        if ($parsedRoutes === []) {
57
            throw new RouteNotFoundException($name);
58
        }
59
60 25
        $missingParameters = [];
61
62
        // One route pattern can correspond to multiple routes if it has optional parts
63 25
        foreach ($parsedRoutes as $parsedRouteParts) {
64
            // Check if all parameters can be substituted
65 25
            $missingParameters = $this->missingParameters($parsedRouteParts, $parameters);
66
67
            // If not all parameters can be substituted, try the next route
68 25
            if (!empty($missingParameters)) {
69 3
                continue;
70
            }
71
72 23
            return $this->generatePath($parameters, $parsedRouteParts);
73
        }
74
75
        // No valid route was found: list minimal required parameters
76 2
        throw new \RuntimeException(sprintf(
77 2
            'Route `%s` expects at least parameter values for [%s], but received [%s]',
78
            $name,
79 2
            implode(',', $missingParameters),
80 2
            implode(',', array_keys($parameters))
81
        ));
82
    }
83
84
    /**
85
     * Generates absolute URL from named route and parameters
86
     *
87
     * @param string $name name of the route
88
     * @param array $parameters parameter-value set
89
     * @param string|null $scheme host scheme
90
     * @param string|null $host host for manual setup
91
     * @return string URL generated
92
     * @throws RouteNotFoundException in case there is no route with the name specified
93
     */
94 15
    public function generateAbsolute(string $name, array $parameters = [], string $scheme = null, string $host = null): string
95
    {
96 15
        $url = $this->generate($name, $parameters);
97 15
        $route = $this->routeCollection->getRoute($name);
98
        /** @var Uri $uri */
99 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

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