UriResolver   F
last analyzed

Complexity

Total Complexity 60

Size/Duplication

Total Lines 343
Duplicated Lines 0 %

Test Coverage

Coverage 100%

Importance

Changes 1
Bugs 0 Features 0
Metric Value
eloc 119
c 1
b 0
f 0
dl 0
loc 343
ccs 126
cts 126
cp 1
rs 3.6
wmc 60

15 Methods

Rating   Name   Duplication   Size   Complexity  
A resolve() 0 33 5
C resolvePathAndQuery() 0 41 12
A formatPath() 0 16 6
A relativize() 0 26 6
A getSegments() 0 7 3
A removeDotSegments() 0 20 5
A __construct() 0 2 1
A formatPathWithEmptyBaseQuery() 0 7 2
A reducer() 0 13 3
A filterUri() 0 4 3
A isRelativizable() 0 5 3
A relativizePath() 0 17 4
A componentEquals() 0 3 1
A getComponent() 0 8 3
A formatHost() 0 12 3

How to fix   Complexity   

Complex Class

Complex classes like UriResolver often do a lot of different things. To break such a class down, we need to identify a cohesive component within that class. A common approach to find such a component is to look for fields/methods that share the same prefixes, or suffixes.

Once you have determined the fields that belong together, you can apply the Extract Class refactoring. If the component makes sense as a sub-class, Extract Subclass is also a candidate, and is often faster.

While breaking up the class, it is a good idea to analyze how other classes use UriResolver, and based on these observations, apply Extract Interface, too.

1
<?php
2
3
/**
4
 * League.Uri (https://uri.thephpleague.com)
5
 *
6
 * (c) Ignace Nyamagana Butera <[email protected]>
7
 *
8
 * For the full copyright and license information, please view the LICENSE
9
 * file that was distributed with this source code.
10
 */
11
12
declare(strict_types=1);
13
14
namespace League\Uri;
15
16
use League\Uri\Contracts\UriInterface;
17
use Psr\Http\Message\UriInterface as Psr7UriInterface;
18
use function array_pop;
19
use function array_reduce;
20
use function count;
21
use function end;
22
use function explode;
23
use function gettype;
24
use function implode;
25
use function in_array;
26
use function sprintf;
27
use function str_repeat;
28
use function strpos;
29
use function substr;
30
31
final class UriResolver
32
{
33
    /**
34
     * @var array<string,int>
35
     */
36
    const DOT_SEGMENTS = ['.' => 1, '..' => 1];
37
38
    /**
39
     * @codeCoverageIgnore
40
     */
41
    private function __construct()
42
    {
43
    }
44
45
    /**
46
     * Resolve an URI against a base URI using RFC3986 rules.
47
     *
48
     * If the first argument is a UriInterface the method returns a UriInterface object
49
     * If the first argument is a Psr7UriInterface the method returns a Psr7UriInterface object
50
     *
51
     * @param Psr7UriInterface|UriInterface $uri
52
     * @param Psr7UriInterface|UriInterface $base_uri
53
     *
54
     * @return Psr7UriInterface|UriInterface
55
     */
56 98
    public static function resolve($uri, $base_uri)
57
    {
58 98
        self::filterUri($uri);
59 96
        self::filterUri($base_uri);
60 96
        $null = $uri instanceof Psr7UriInterface ? '' : null;
61
62 96
        if ($null !== $uri->getScheme()) {
63
            return $uri
64 20
                ->withPath(self::removeDotSegments($uri->getPath()));
65
        }
66
67 76
        if ($null !== $uri->getAuthority()) {
68
            return $uri
69 4
                ->withScheme($base_uri->getScheme())
70 4
                ->withPath(self::removeDotSegments($uri->getPath()));
71
        }
72
73 72
        $user = $null;
74 72
        $pass = null;
75 72
        $userInfo = $base_uri->getUserInfo();
76 72
        if (null !== $userInfo) {
77 72
            [$user, $pass] = explode(':', $userInfo, 2) + [1 => null];
78
        }
79
80 72
        [$uri_path, $uri_query] = self::resolvePathAndQuery($uri, $base_uri);
81
82
        return $uri
83 72
            ->withPath(self::removeDotSegments($uri_path))
84 72
            ->withQuery($uri_query)
85 72
            ->withHost($base_uri->getHost())
86 72
            ->withPort($base_uri->getPort())
87 72
            ->withUserInfo((string) $user, $pass)
88 72
            ->withScheme($base_uri->getScheme())
89
        ;
90
    }
91
92
    /**
93
     * Filter the URI object.
94
     *
95
     * @param mixed $uri an URI object
96
     *
97
     * @throws \TypeError if the URI object does not implements the supported interfaces.
98
     */
99 200
    private static function filterUri($uri): void
100
    {
101 200
        if (!$uri instanceof UriInterface && !$uri instanceof Psr7UriInterface) {
102 4
            throw new \TypeError(sprintf('The uri must be a valid URI object received `%s`', gettype($uri)));
103
        }
104 196
    }
105
106
    /**
107
     * Remove dot segments from the URI path.
108
     */
109 96
    private static function removeDotSegments(string $path): string
110
    {
111 96
        if (false === strpos($path, '.')) {
112 50
            return $path;
113
        }
114
115 48
        $old_segments = explode('/', $path);
116 48
        $new_path = implode('/', array_reduce($old_segments, [UriResolver::class, 'reducer'], []));
117 48
        if (isset(self::DOT_SEGMENTS[end($old_segments)])) {
118 8
            $new_path .= '/';
119
        }
120
121
        // @codeCoverageIgnoreStart
122
        // added because some PSR-7 implementations do not respect RFC3986
123
        if (0 === strpos($path, '/') && 0 !== strpos($new_path, '/')) {
124
            return '/'.$new_path;
125
        }
126
        // @codeCoverageIgnoreEnd
127
128 42
        return $new_path;
129
    }
130
131
    /**
132
     * Remove dot segments.
133
     *
134
     * @return array<int, string>
135
     */
136 48
    private static function reducer(array $carry, string $segment): array
137
    {
138 48
        if ('..' === $segment) {
139 26
            array_pop($carry);
140
141 26
            return $carry;
142
        }
143
144 48
        if (!isset(self::DOT_SEGMENTS[$segment])) {
145 48
            $carry[] = $segment;
146
        }
147
148 48
        return $carry;
149
    }
150
151
    /**
152
     * Resolve an URI path and query component.
153
     *
154
     * @param Psr7UriInterface|UriInterface $uri
155
     * @param Psr7UriInterface|UriInterface $base_uri
156
     *
157
     * @return array{0:string, 1:string|null}
158
     */
159 72
    private static function resolvePathAndQuery($uri, $base_uri): array
160
    {
161 72
        $target_path = $uri->getPath();
162 72
        $target_query = $uri->getQuery();
163 72
        $null = $uri instanceof Psr7UriInterface ? '' : null;
164 72
        $baseNull = $base_uri instanceof Psr7UriInterface ? '' : null;
165
166 72
        if (0 === strpos($target_path, '/')) {
167 6
            return [$target_path, $target_query];
168
        }
169
170 66
        if ('' === $target_path) {
171 6
            if ($null === $target_query) {
172 4
                $target_query = $base_uri->getQuery();
173
            }
174
175 6
            $target_path = $base_uri->getPath();
176
            //@codeCoverageIgnoreStart
177
            //because some PSR-7 Uri implementations allow this RFC3986 forbidden construction
178
            if ($baseNull !== $base_uri->getAuthority() && 0 !== strpos($target_path, '/')) {
179
                $target_path = '/'.$target_path;
180
            }
181
            //@codeCoverageIgnoreEnd
182
183 6
            return [$target_path, $target_query];
184
        }
185
186 60
        $base_path = $base_uri->getPath();
187 60
        if ($baseNull !== $base_uri->getAuthority() && '' === $base_path) {
188 2
            $target_path = '/'.$target_path;
189
        }
190
191 60
        if ('' !== $base_path) {
192 58
            $segments = explode('/', $base_path);
193 58
            array_pop($segments);
194 58
            if ([] !== $segments) {
195 58
                $target_path = implode('/', $segments).'/'.$target_path;
196
            }
197
        }
198
199 60
        return [$target_path, $target_query];
200
    }
201
202
    /**
203
     * Relativize an URI according to a base URI.
204
     *
205
     * This method MUST retain the state of the submitted URI instance, and return
206
     * an URI instance of the same type that contains the applied modifications.
207
     *
208
     * This method MUST be transparent when dealing with error and exceptions.
209
     * It MUST not alter of silence them apart from validating its own parameters.
210
     *
211
     * @param Psr7UriInterface|UriInterface $uri
212
     * @param Psr7UriInterface|UriInterface $base_uri
213
     *
214
     * @return Psr7UriInterface|UriInterface
215
     */
216 102
    public static function relativize($uri, $base_uri)
217
    {
218 102
        self::filterUri($uri);
219 100
        self::filterUri($base_uri);
220 100
        $uri = self::formatHost($uri);
221 100
        $base_uri = self::formatHost($base_uri);
222 100
        if (!self::isRelativizable($uri, $base_uri)) {
223 44
            return $uri;
224
        }
225
226 56
        $null = $uri instanceof Psr7UriInterface ? '' : null;
227 56
        $uri = $uri->withScheme($null)->withPort(null)->withUserInfo($null)->withHost($null);
228 56
        $target_path = $uri->getPath();
229 56
        if ($target_path !== $base_uri->getPath()) {
230 40
            return $uri->withPath(self::relativizePath($target_path, $base_uri->getPath()));
231
        }
232
233 16
        if (self::componentEquals('getQuery', $uri, $base_uri)) {
234 8
            return $uri->withPath('')->withQuery($null);
235
        }
236
237 8
        if ($null === $uri->getQuery()) {
238 4
            return $uri->withPath(self::formatPathWithEmptyBaseQuery($target_path));
239
        }
240
241 4
        return $uri->withPath('');
242
    }
243
244
    /**
245
     * Tells whether the component value from both URI object equals.
246
     *
247
     * @param Psr7UriInterface|UriInterface $uri
248
     * @param Psr7UriInterface|UriInterface $base_uri
249
     */
250 62
    private static function componentEquals(string $method, $uri, $base_uri): bool
251
    {
252 62
        return self::getComponent($method, $uri) === self::getComponent($method, $base_uri);
253
    }
254
255
    /**
256
     * Returns the component value from the submitted URI object.
257
     *
258
     * @param Psr7UriInterface|UriInterface $uri
259
     */
260 62
    private static function getComponent(string $method, $uri): ?string
261
    {
262 62
        $component = $uri->$method();
263 62
        if ($uri instanceof Psr7UriInterface && '' === $component) {
264 8
            return null;
265
        }
266
267 62
        return $component;
268
    }
269
270
    /**
271
     * Filter the URI object.
272
     *
273
     * @param null|mixed $uri
274
     *
275
     * @throws \TypeError if the URI object does not implements the supported interfaces.
276
     *
277
     * @return Psr7UriInterface|UriInterface
278
     */
279 100
    private static function formatHost($uri)
280
    {
281 100
        if (!$uri instanceof Psr7UriInterface) {
282 100
            return $uri;
283
        }
284
285 100
        $host = $uri->getHost();
286 100
        if ('' === $host) {
287 34
            return $uri;
288
        }
289
290 66
        return $uri->withHost((string) Uri::createFromComponents(['host' => $host])->getHost());
291
    }
292
293
    /**
294
     * Tell whether the submitted URI object can be relativize.
295
     *
296
     * @param Psr7UriInterface|UriInterface $uri
297
     * @param Psr7UriInterface|UriInterface $base_uri
298
     */
299 100
    private static function isRelativizable($uri, $base_uri): bool
300
    {
301 100
        return !UriInfo::isRelativePath($uri)
302 100
            && self::componentEquals('getScheme', $uri, $base_uri)
303 100
            &&  self::componentEquals('getAuthority', $uri, $base_uri);
304
    }
305
306
    /**
307
     * Relative the URI for a authority-less target URI.
308
     */
309 40
    private static function relativizePath(string $path, string $basepath): string
310
    {
311 40
        $base_segments = self::getSegments($basepath);
312 40
        $target_segments = self::getSegments($path);
313 40
        $target_basename = array_pop($target_segments);
314 40
        array_pop($base_segments);
315 40
        foreach ($base_segments as $offset => $segment) {
316 32
            if (!isset($target_segments[$offset]) || $segment !== $target_segments[$offset]) {
317 16
                break;
318
            }
319 22
            unset($base_segments[$offset], $target_segments[$offset]);
320
        }
321 40
        $target_segments[] = $target_basename;
322
323 40
        return self::formatPath(
324 40
            str_repeat('../', count($base_segments)).implode('/', $target_segments),
325 20
            $basepath
326
        );
327
    }
328
329
    /**
330
     * returns the path segments.
331
     *
332
     * @return string[]
333
     */
334 44
    private static function getSegments(string $path): array
335
    {
336 44
        if ('' !== $path && '/' === $path[0]) {
337 44
            $path = substr($path, 1);
338
        }
339
340 44
        return explode('/', $path);
341
    }
342
343
    /**
344
     * Formatting the path to keep a valid URI.
345
     */
346 40
    private static function formatPath(string $path, string $basepath): string
347
    {
348 40
        if ('' === $path) {
349 6
            return in_array($basepath, ['', '/'], true) ? $basepath : './';
350
        }
351
352 34
        if (false === ($colon_pos = strpos($path, ':'))) {
353 28
            return $path;
354
        }
355
356 6
        $slash_pos = strpos($path, '/');
357 6
        if (false === $slash_pos || $colon_pos < $slash_pos) {
358 4
            return "./$path";
359
        }
360
361 2
        return $path;
362
    }
363
364
    /**
365
     * Formatting the path to keep a resolvable URI.
366
     */
367 4
    private static function formatPathWithEmptyBaseQuery(string $path): string
368
    {
369 4
        $target_segments = self::getSegments($path);
370
        /** @var string $basename */
371 4
        $basename = end($target_segments);
372
373 4
        return '' === $basename ? './' : $basename;
374
    }
375
}
376