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

generateAbsoluteFromLastMatchedRequest()   A

Complexity

Conditions 3
Paths 4

Size

Total Lines 4
Code Lines 2

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 3
CRAP Score 3

Importance

Changes 0
Metric Value
cc 3
eloc 2
nc 4
nop 3
dl 0
loc 4
ccs 3
cts 3
cp 1
crap 3
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 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 15
    public function generateAbsolute(string $name, array $parameters = [], string $scheme = null, string $host = null): string
79
    {
80 15
        $url = $this->generate($name, $parameters);
81 15
        $route = $this->routeCollection->getRoute($name);
82
        /** @var Uri $uri */
83 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

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