Completed
Push — master ( 2b1385...7c6a84 )
by Thomas
07:21
created

Url   D

Complexity

Total Complexity 88

Size/Duplication

Total Lines 587
Duplicated Lines 0 %

Coupling/Cohesion

Components 1
Dependencies 1

Importance

Changes 1
Bugs 0 Features 0
Metric Value
wmc 88
c 1
b 0
f 0
lcom 1
cbo 1
dl 0
loc 587
rs 4.8718

How to fix   Complexity   

Complex Class

Complex classes like Url 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. You can also have a look at the cohesion graph to spot any un-connected, or weakly-connected components.

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 Url, and based on these observations, apply Extract Interface, too.

1
<?php
2
namespace GuzzleHttp;
3
4
use GuzzleHttp\Ring\Core;
5
6
/**
7
 * Parses and generates URLs based on URL parts
8
 */
9
class Url
10
{
11
    private $scheme;
12
    private $host;
13
    private $port;
14
    private $username;
15
    private $password;
16
    private $path = '';
17
    private $fragment;
18
    private static $defaultPorts = ['http' => 80, 'https' => 443, 'ftp' => 21];
19
    private static $pathPattern = '/[^a-zA-Z0-9\-\._~!\$&\'\(\)\*\+,;=%:@\/]+|%(?![A-Fa-f0-9]{2})/';
20
    private static $queryPattern = '/[^a-zA-Z0-9\-\._~!\$\'\(\)\*\+,;%:@\/\?=&]+|%(?![A-Fa-f0-9]{2})/';
21
    /** @var Query|string Query part of the URL */
22
    private $query;
23
24
    /**
25
     * Factory method to create a new URL from a URL string
26
     *
27
     * @param string $url Full URL used to create a Url object
28
     *
29
     * @return Url
30
     * @throws \InvalidArgumentException
31
     */
32
    public static function fromString($url)
33
    {
34
        static $defaults = ['scheme' => null, 'host' => null,
35
            'path' => null, 'port' => null, 'query' => null,
36
            'user' => null, 'pass' => null, 'fragment' => null];
37
38
        if (false === ($parts = parse_url($url))) {
39
            throw new \InvalidArgumentException('Unable to parse malformed '
40
                . 'url: ' . $url);
41
        }
42
43
        $parts += $defaults;
44
45
        // Convert the query string into a Query object
46
        if ($parts['query'] || 0 !== strlen($parts['query'])) {
47
            $parts['query'] = Query::fromString($parts['query']);
48
        }
49
50
        return new static($parts['scheme'], $parts['host'], $parts['user'],
51
            $parts['pass'], $parts['port'], $parts['path'], $parts['query'],
52
            $parts['fragment']);
53
    }
54
55
    /**
56
     * Build a URL from parse_url parts. The generated URL will be a relative
57
     * URL if a scheme or host are not provided.
58
     *
59
     * @param array $parts Array of parse_url parts
60
     *
61
     * @return string
62
     */
63
    public static function buildUrl(array $parts)
64
    {
65
        $url = $scheme = '';
66
67
        if (!empty($parts['scheme'])) {
68
            $scheme = $parts['scheme'];
69
            $url .= $scheme . ':';
70
        }
71
72
        if (!empty($parts['host'])) {
73
            $url .= '//';
74
            if (isset($parts['user'])) {
75
                $url .= $parts['user'];
76
                if (isset($parts['pass'])) {
77
                    $url .= ':' . $parts['pass'];
78
                }
79
                $url .=  '@';
80
            }
81
82
            $url .= $parts['host'];
83
84
            // Only include the port if it is not the default port of the scheme
85
            if (isset($parts['port']) &&
86
                (!isset(self::$defaultPorts[$scheme]) ||
87
                 $parts['port'] != self::$defaultPorts[$scheme])
88
            ) {
89
                $url .= ':' . $parts['port'];
90
            }
91
        }
92
93
        // Add the path component if present
94
        if (isset($parts['path']) && strlen($parts['path'])) {
95
            // Always ensure that the path begins with '/' if set and something
96
            // is before the path
97
            if (!empty($parts['host']) && $parts['path'][0] != '/') {
98
                $url .= '/';
99
            }
100
            $url .= $parts['path'];
101
        }
102
103
        // Add the query string if present
104
        if (isset($parts['query'])) {
105
            $queryStr = (string) $parts['query'];
106
            if ($queryStr || $queryStr === '0') {
107
                $url .= '?' . $queryStr;
108
            }
109
        }
110
111
        // Ensure that # is only added to the url if fragment contains anything.
112
        if (isset($parts['fragment'])) {
113
            $url .= '#' . $parts['fragment'];
114
        }
115
116
        return $url;
117
    }
118
119
    /**
120
     * Create a new URL from URL parts
121
     *
122
     * @param string             $scheme   Scheme of the URL
123
     * @param string             $host     Host of the URL
124
     * @param string             $username Username of the URL
125
     * @param string             $password Password of the URL
126
     * @param int                $port     Port of the URL
127
     * @param string             $path     Path of the URL
128
     * @param Query|array|string $query    Query string of the URL
129
     * @param string             $fragment Fragment of the URL
130
     */
131
    public function __construct(
132
        $scheme,
133
        $host,
134
        $username = null,
135
        $password = null,
136
        $port = null,
137
        $path = null,
138
        $query = null,
139
        $fragment = null
140
    ) {
141
        $this->scheme = strtolower($scheme);
142
        $this->host = $host;
143
        $this->port = $port;
144
        $this->username = $username;
145
        $this->password = $password;
146
        $this->fragment = $fragment;
147
148
        if ($query) {
149
            $this->setQuery($query);
150
        }
151
152
        $this->setPath($path);
153
    }
154
155
    /**
156
     * Clone the URL
157
     */
158
    public function __clone()
159
    {
160
        if ($this->query instanceof Query) {
161
            $this->query = clone $this->query;
162
        }
163
    }
164
165
    /**
166
     * Returns the URL as a URL string
167
     *
168
     * @return string
169
     */
170
    public function __toString()
171
    {
172
        return static::buildUrl($this->getParts());
173
    }
174
175
    /**
176
     * Get the parts of the URL as an array
177
     *
178
     * @return array
179
     */
180
    public function getParts()
181
    {
182
        return array(
183
            'scheme'   => $this->scheme,
184
            'user'     => $this->username,
185
            'pass'     => $this->password,
186
            'host'     => $this->host,
187
            'port'     => $this->port,
188
            'path'     => $this->path,
189
            'query'    => $this->query,
190
            'fragment' => $this->fragment,
191
        );
192
    }
193
194
    /**
195
     * Set the host of the request.
196
     *
197
     * @param string $host Host to set (e.g. www.yahoo.com, yahoo.com)
198
     *
199
     * @return Url
200
     */
201
    public function setHost($host)
202
    {
203
        if (strpos($host, ':') === false) {
204
            $this->host = $host;
205
        } else {
206
            list($host, $port) = explode(':', $host);
207
            $this->host = $host;
208
            $this->setPort($port);
209
        }
210
    }
211
212
    /**
213
     * Get the host part of the URL
214
     *
215
     * @return string
216
     */
217
    public function getHost()
218
    {
219
        return $this->host;
220
    }
221
222
    /**
223
     * Set the scheme part of the URL (http, https, ftp, etc.)
224
     *
225
     * @param string $scheme Scheme to set
226
     */
227
    public function setScheme($scheme)
228
    {
229
        // Remove the default port if one is specified
230
        if ($this->port
231
            && isset(self::$defaultPorts[$this->scheme])
232
            && self::$defaultPorts[$this->scheme] == $this->port
233
        ) {
234
            $this->port = null;
235
        }
236
237
        $this->scheme = strtolower($scheme);
238
    }
239
240
    /**
241
     * Get the scheme part of the URL
242
     *
243
     * @return string
244
     */
245
    public function getScheme()
246
    {
247
        return $this->scheme;
248
    }
249
250
    /**
251
     * Set the port part of the URL
252
     *
253
     * @param int $port Port to set
254
     */
255
    public function setPort($port)
256
    {
257
        $this->port = $port;
258
    }
259
260
    /**
261
     * Get the port part of the URl.
262
     *
263
     * If no port was set, this method will return the default port for the
264
     * scheme of the URI.
265
     *
266
     * @return int|null
267
     */
268
    public function getPort()
269
    {
270
        if ($this->port) {
271
            return $this->port;
272
        } elseif (isset(self::$defaultPorts[$this->scheme])) {
273
            return self::$defaultPorts[$this->scheme];
274
        }
275
276
        return null;
277
    }
278
279
    /**
280
     * Set the path part of the URL.
281
     *
282
     * The provided URL is URL encoded as necessary.
283
     *
284
     * @param string $path Path string to set
285
     */
286
    public function setPath($path)
287
    {
288
        $this->path = self::encodePath($path);
289
    }
290
291
    /**
292
     * Removes dot segments from a URL
293
     * @link http://tools.ietf.org/html/rfc3986#section-5.2.4
294
     */
295
    public function removeDotSegments()
296
    {
297
        static $noopPaths = ['' => true, '/' => true, '*' => true];
298
        static $ignoreSegments = ['.' => true, '..' => true];
299
300
        if (isset($noopPaths[$this->path])) {
301
            return;
302
        }
303
304
        $results = [];
305
        $segments = $this->getPathSegments();
306
        foreach ($segments as $segment) {
307
            if ($segment == '..') {
308
                array_pop($results);
309
            } elseif (!isset($ignoreSegments[$segment])) {
310
                $results[] = $segment;
311
            }
312
        }
313
314
        $newPath = implode('/', $results);
315
316
        // Add the leading slash if necessary
317
        if (substr($this->path, 0, 1) === '/' &&
318
            substr($newPath, 0, 1) !== '/'
319
        ) {
320
            $newPath = '/' . $newPath;
321
        }
322
323
        // Add the trailing slash if necessary
324
        if ($newPath != '/' && isset($ignoreSegments[end($segments)])) {
325
            $newPath .= '/';
326
        }
327
328
        $this->path = $newPath;
329
    }
330
331
    /**
332
     * Add a relative path to the currently set path.
333
     *
334
     * @param string $relativePath Relative path to add
335
     */
336
    public function addPath($relativePath)
337
    {
338
        if ($relativePath != '/' &&
339
            is_string($relativePath) &&
340
            strlen($relativePath) > 0
341
        ) {
342
            // Add a leading slash if needed
343
            if ($relativePath[0] !== '/' &&
344
                substr($this->path, -1, 1) !== '/'
345
            ) {
346
                $relativePath = '/' . $relativePath;
347
            }
348
349
            $this->setPath($this->path . $relativePath);
350
        }
351
    }
352
353
    /**
354
     * Get the path part of the URL
355
     *
356
     * @return string
357
     */
358
    public function getPath()
359
    {
360
        return $this->path;
361
    }
362
363
    /**
364
     * Get the path segments of the URL as an array
365
     *
366
     * @return array
367
     */
368
    public function getPathSegments()
369
    {
370
        return explode('/', $this->path);
371
    }
372
373
    /**
374
     * Set the password part of the URL
375
     *
376
     * @param string $password Password to set
377
     */
378
    public function setPassword($password)
379
    {
380
        $this->password = $password;
381
    }
382
383
    /**
384
     * Get the password part of the URL
385
     *
386
     * @return null|string
387
     */
388
    public function getPassword()
389
    {
390
        return $this->password;
391
    }
392
393
    /**
394
     * Set the username part of the URL
395
     *
396
     * @param string $username Username to set
397
     */
398
    public function setUsername($username)
399
    {
400
        $this->username = $username;
401
    }
402
403
    /**
404
     * Get the username part of the URl
405
     *
406
     * @return null|string
407
     */
408
    public function getUsername()
409
    {
410
        return $this->username;
411
    }
412
413
    /**
414
     * Get the query part of the URL as a Query object
415
     *
416
     * @return Query
417
     */
418
    public function getQuery()
419
    {
420
        // Convert the query string to a query object if not already done.
421
        if (!$this->query instanceof Query) {
422
            $this->query = $this->query === null
423
                ? new Query()
424
                : Query::fromString($this->query);
425
        }
426
427
        return $this->query;
428
    }
429
430
    /**
431
     * Set the query part of the URL.
432
     *
433
     * You may provide a query string as a string and pass $rawString as true
434
     * to provide a query string that is not parsed until a call to getQuery()
435
     * is made. Setting a raw query string will still encode invalid characters
436
     * in a query string.
437
     *
438
     * @param Query|string|array $query Query string value to set. Can
439
     *     be a string that will be parsed into a Query object, an array
440
     *     of key value pairs, or a Query object.
441
     * @param bool $rawString Set to true when providing a raw query string.
442
     *
443
     * @throws \InvalidArgumentException
444
     */
445
    public function setQuery($query, $rawString = false)
446
    {
447
        if ($query instanceof Query) {
448
            $this->query = $query;
449
        } elseif (is_string($query)) {
450
            if (!$rawString) {
451
                $this->query = Query::fromString($query);
452
            } else {
453
                // Ensure the query does not have illegal characters.
454
                $this->query = preg_replace_callback(
455
                    self::$queryPattern,
456
                    [__CLASS__, 'encodeMatch'],
457
                    $query
458
                );
459
            }
460
461
        } elseif (is_array($query)) {
462
            $this->query = new Query($query);
463
        } else {
464
            throw new \InvalidArgumentException('Query must be a Query, '
465
                . 'array, or string. Got ' . Core::describeType($query));
466
        }
467
    }
468
469
    /**
470
     * Get the fragment part of the URL
471
     *
472
     * @return null|string
473
     */
474
    public function getFragment()
475
    {
476
        return $this->fragment;
477
    }
478
479
    /**
480
     * Set the fragment part of the URL
481
     *
482
     * @param string $fragment Fragment to set
483
     */
484
    public function setFragment($fragment)
485
    {
486
        $this->fragment = $fragment;
487
    }
488
489
    /**
490
     * Check if this is an absolute URL
491
     *
492
     * @return bool
493
     */
494
    public function isAbsolute()
495
    {
496
        return $this->scheme && $this->host;
497
    }
498
499
    /**
500
     * Combine the URL with another URL and return a new URL instance.
501
     *
502
     * Follows the rules specific in RFC 3986 section 5.4.
503
     *
504
     * @param string $url Relative URL to combine with
505
     *
506
     * @return Url
507
     * @throws \InvalidArgumentException
508
     * @link http://tools.ietf.org/html/rfc3986#section-5.4
509
     */
510
    public function combine($url)
511
    {
512
        $url = static::fromString($url);
513
514
        // Use the more absolute URL as the base URL
515
        if (!$this->isAbsolute() && $url->isAbsolute()) {
516
            $url = $url->combine($this);
517
        }
518
519
        $parts = $url->getParts();
520
521
        // Passing a URL with a scheme overrides everything
522
        if ($parts['scheme']) {
523
            return clone $url;
524
        }
525
526
        // Setting a host overrides the entire rest of the URL
527
        if ($parts['host']) {
528
            return new static(
529
                $this->scheme,
530
                $parts['host'],
531
                $parts['user'],
532
                $parts['pass'],
533
                $parts['port'],
534
                $parts['path'],
535
                $parts['query'] instanceof Query
536
                    ? clone $parts['query']
537
                    : $parts['query'],
538
                $parts['fragment']
539
            );
540
        }
541
542
        if (!$parts['path'] && $parts['path'] !== '0') {
543
            // The relative URL has no path, so check if it is just a query
544
            $path = $this->path ?: '';
545
            $query = $parts['query'] ?: $this->query;
546
        } else {
547
            $query = $parts['query'];
548
            if ($parts['path'][0] == '/' || !$this->path) {
549
                // Overwrite the existing path if the rel path starts with "/"
550
                $path = $parts['path'];
551
            } else {
552
                // If the relative URL does not have a path or the base URL
553
                // path does not end in a "/" then overwrite the existing path
554
                // up to the last "/"
555
                $path = substr($this->path, 0, strrpos($this->path, '/') + 1) . $parts['path'];
556
            }
557
        }
558
559
        $result = new self(
560
            $this->scheme,
561
            $this->host,
562
            $this->username,
563
            $this->password,
564
            $this->port,
565
            $path,
566
            $query instanceof Query ? clone $query : $query,
567
            $parts['fragment']
568
        );
569
570
        if ($path) {
571
            $result->removeDotSegments();
572
        }
573
574
        return $result;
575
    }
576
577
    /**
578
     * Encodes the path part of a URL without double-encoding percent-encoded
579
     * key value pairs.
580
     *
581
     * @param string $path Path to encode
582
     *
583
     * @return string
584
     */
585
    public static function encodePath($path)
586
    {
587
        static $cb = [__CLASS__, 'encodeMatch'];
588
        return preg_replace_callback(self::$pathPattern, $cb, $path);
589
    }
590
591
    private static function encodeMatch(array $match)
592
    {
593
        return rawurlencode($match[0]);
594
    }
595
}
596