Passed
Push — gh-pages ( 22b0fe...eb2d91 )
by
unknown
12:27 queued 10:15
created

Uri   F

Complexity

Total Complexity 74

Size/Duplication

Total Lines 504
Duplicated Lines 0 %

Importance

Changes 1
Bugs 0 Features 0
Metric Value
eloc 155
c 1
b 0
f 0
dl 0
loc 504
rs 2.48
wmc 74

29 Methods

Rating   Name   Duplication   Size   Complexity  
A filterPort() 0 12 4
A filterPass() 0 7 2
B validateState() 0 21 10
A withScheme() 0 14 2
A filterFragment() 0 2 1
A withUserInfo() 0 18 4
A withFragment() 0 13 2
A withQuery() 0 13 2
A withPath() 0 13 2
A getScheme() 0 2 1
A replaceChars() 0 8 2
A parseUriParts() 0 12 3
A getPort() 0 2 1
A getAuthority() 0 13 3
A __construct() 0 10 4
A filterUser() 0 7 2
A getQuery() 0 2 1
A getUserInfo() 0 2 2
A filterPath() 0 7 2
A filterScheme() 0 7 2
A removeDefaultPort() 0 4 4
A __toString() 0 25 6
A withHost() 0 13 2
A getPath() 0 2 1
A getHost() 0 2 1
A filterHost() 0 11 3
A filterQuery() 0 7 2
A withPort() 0 14 2
A getFragment() 0 2 1

How to fix   Complexity   

Complex Class

Complex classes like Uri 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 Uri, and based on these observations, apply Extract Interface, too.

1
<?php
2
/**
3
 * Class Uri
4
 *
5
 * @created      10.08.2018
6
 * @author       smiley <[email protected]>
7
 * @copyright    2018 smiley
8
 * @license      MIT
9
 */
10
11
namespace chillerlan\HTTP\Psr7;
12
13
use InvalidArgumentException;
14
use Psr\Http\Message\UriInterface;
15
16
use function call_user_func_array, explode, filter_var, is_string, ltrim, parse_url,
17
	preg_replace_callback, rawurlencode, strpos, strtolower, ucfirst;
18
19
use const FILTER_FLAG_IPV6, FILTER_VALIDATE_IP;
20
21
class Uri implements UriInterface{
22
23
	protected const DEFAULT_PORTS = [
24
		'http'   => 80,
25
		'https'  => 443,
26
		'ftp'    => 21,
27
		'gopher' => 70,
28
		'nntp'   => 119,
29
		'news'   => 119,
30
		'telnet' => 23,
31
		'tn3270' => 23,
32
		'imap'   => 143,
33
		'pop'    => 110,
34
		'ldap'   => 389,
35
	];
36
37
	protected string $scheme = '';
38
39
	protected string $user = '';
40
41
	protected ?string $pass = null;
42
43
	protected string $host = '';
44
45
	protected ?int $port = null;
46
47
	protected string $path = '';
48
49
	protected string $query = '';
50
51
	protected string $fragment = '';
52
53
	/**
54
	 * Uri constructor.
55
	 *
56
	 * @throws \InvalidArgumentException
57
	 */
58
	public function __construct(string $uri = null){
59
60
		if($uri !== null && $uri !== ''){
61
			$parts = parse_url($uri);
62
63
			if($parts === false){
64
				throw new InvalidArgumentException('invalid URI: "'.$uri.'"');
65
			}
66
67
			$this->parseUriParts($parts);
68
		}
69
70
	}
71
72
	/**
73
	 * @inheritDoc
74
	 */
75
	public function __toString():string{
76
		$this->validateState();
77
78
		$uri       = '';
79
		$authority = $this->getAuthority();
80
81
		if($this->scheme !== ''){
82
			$uri .= $this->scheme.':';
83
		}
84
85
		if($authority !== '' || $this->scheme === 'file'){
86
			$uri .= '//'.$authority;
87
		}
88
89
		$uri .= $this->path;
90
91
		if($this->query !== ''){
92
			$uri .= '?'.$this->query;
93
		}
94
95
		if($this->fragment !== ''){
96
			$uri .= '#'.$this->fragment;
97
		}
98
99
		return $uri;
100
	}
101
102
	/**
103
	 * Scheme
104
	 */
105
106
	/**
107
	 * @param mixed $scheme
108
	 *
109
	 * @return string
110
	 * @throws \InvalidArgumentException
111
	 */
112
	protected function filterScheme($scheme):string{
113
114
		if(!is_string($scheme)){
115
			throw new InvalidArgumentException('scheme must be a string');
116
		}
117
118
		return strtolower($scheme);
119
	}
120
121
	/**
122
	 * @inheritDoc
123
	 */
124
	public function getScheme():string{
125
		return $this->scheme;
126
	}
127
128
	/**
129
	 * @inheritDoc
130
	 */
131
	public function withScheme($scheme):UriInterface{
132
		$scheme = $this->filterScheme($scheme);
133
134
		if($this->scheme === $scheme){
135
			return $this;
136
		}
137
138
		$clone         = clone $this;
139
		$clone->scheme = $scheme;
140
141
		$clone->removeDefaultPort();
142
		$clone->validateState();
143
144
		return $clone;
145
	}
146
147
	/**
148
	 * Authority
149
	 */
150
151
	/**
152
	 * @param mixed $user
153
	 *
154
	 * @return string
155
	 * @throws \InvalidArgumentException
156
	 */
157
	protected function filterUser($user):string{
158
159
		if(!is_string($user)){
160
			throw new InvalidArgumentException('user must be a string');
161
		}
162
163
		return $user;
164
	}
165
166
	/**
167
	 * @param mixed $pass
168
	 *
169
	 * @return string
170
	 * @throws \InvalidArgumentException
171
	 */
172
	protected function filterPass($pass):string{
173
174
		if(!is_string($pass)){
175
			throw new InvalidArgumentException('pass must be a string');
176
		}
177
178
		return $pass;
179
	}
180
181
	/**
182
	 * @inheritDoc
183
	 */
184
	public function getAuthority():string{
185
		$authority = $this->host;
186
		$userInfo  = $this->getUserInfo();
187
188
		if($userInfo !== ''){
189
			$authority = $userInfo.'@'.$authority;
190
		}
191
192
		if($this->port !== null){
193
			$authority .= ':'.$this->port;
194
		}
195
196
		return $authority;
197
	}
198
199
	/**
200
	 * @inheritDoc
201
	 */
202
	public function getUserInfo():string{
203
		return (string)$this->user.($this->pass != '' ? ':'.$this->pass : '');
204
	}
205
206
	/**
207
	 * @inheritDoc
208
	 */
209
	public function withUserInfo($user, $password = null):UriInterface{
210
		$info = $user;
211
212
		if($password !== null && $password !== ''){
213
			$info .= ':'.$password;
214
		}
215
216
		if($this->getUserInfo() === $info){
217
			return $this;
218
		}
219
220
		$clone       = clone $this;
221
		$clone->user = $user;
222
		$clone->pass = $password;
223
224
		$clone->validateState();
225
226
		return $clone;
227
	}
228
229
	/**
230
	 * Host
231
	 */
232
233
	/**
234
	 * @param mixed $host
235
	 *
236
	 * @return string
237
	 * @throws \InvalidArgumentException
238
	 */
239
	protected function filterHost($host):string{
240
241
		if(!is_string($host)){
242
			throw new InvalidArgumentException('host must be a string');
243
		}
244
245
		if(filter_var($host, FILTER_VALIDATE_IP, FILTER_FLAG_IPV6)){
246
			$host = '['.$host.']';
247
		}
248
249
		return strtolower($host);
250
	}
251
252
	/**
253
	 * @inheritDoc
254
	 */
255
	public function getHost():string{
256
		return $this->host;
257
	}
258
259
	/**
260
	 * @inheritDoc
261
	 */
262
	public function withHost($host):UriInterface{
263
		$host = $this->filterHost($host);
264
265
		if($this->host === $host){
266
			return $this;
267
		}
268
269
		$clone       = clone $this;
270
		$clone->host = $host;
271
272
		$clone->validateState();
273
274
		return $clone;
275
	}
276
277
	/**
278
	 * Port
279
	 */
280
281
	/**
282
	 * @param mixed $port
283
	 *
284
	 * @return int|null
285
	 * @throws \InvalidArgumentException
286
	 */
287
	protected function filterPort($port):?int{
288
289
		if($port === null){
290
			return null;
291
		}
292
293
		$port = (int)$port;
294
		if($port >= 1 && $port <= 0xffff){
295
			return $port;
296
		}
297
298
		throw new InvalidArgumentException('invalid port: '.$port);
299
	}
300
301
	/**
302
	 * @inheritDoc
303
	 */
304
	public function getPort():?int{
305
		return $this->port;
306
	}
307
308
	/**
309
	 * @inheritDoc
310
	 */
311
	public function withPort($port):UriInterface{
312
		$port = $this->filterPort($port);
313
314
		if($this->port === $port){
315
			return $this;
316
		}
317
318
		$clone       = clone $this;
319
		$clone->port = $port;
320
321
		$clone->removeDefaultPort();
322
		$clone->validateState();
323
324
		return $clone;
325
	}
326
327
	/**
328
	 * Path
329
	 */
330
331
	/**
332
	 * @param mixed $path
333
	 *
334
	 * @return string
335
	 * @throws \InvalidArgumentException
336
	 */
337
	protected function filterPath($path):string{
338
339
		if(!is_string($path)){
340
			throw new InvalidArgumentException('path must be a string');
341
		}
342
343
		return $this->replaceChars($path);
344
	}
345
346
	/**
347
	 * @inheritDoc
348
	 */
349
	public function getPath():string{
350
		return $this->path;
351
	}
352
353
	/**
354
	 * @inheritDoc
355
	 */
356
	public function withPath($path):UriInterface{
357
		$path = $this->filterPath($path);
358
359
		if($this->path === $path){
360
			return $this;
361
		}
362
363
		$clone       = clone $this;
364
		$clone->path = $path;
365
366
		$clone->validateState();
367
368
		return $clone;
369
	}
370
371
	/**
372
	 * Query
373
	 */
374
375
	/**
376
	 * @param mixed $query
377
	 *
378
	 * @return string
379
	 * @throws \InvalidArgumentException
380
	 */
381
	protected function filterQuery($query):string{
382
383
		if(!is_string($query)){
384
			throw new InvalidArgumentException('query and fragment must be a string');
385
		}
386
387
		return $this->replaceChars($query, true);
388
	}
389
390
	/**
391
	 * @inheritDoc
392
	 */
393
	public function getQuery():string{
394
		return $this->query;
395
	}
396
397
	/**
398
	 * @inheritDoc
399
	 */
400
	public function withQuery($query):UriInterface{
401
		$query = $this->filterQuery($query);
402
403
		if($this->query === $query){
404
			return $this;
405
		}
406
407
		$clone        = clone $this;
408
		$clone->query = $query;
409
410
		$clone->validateState();
411
412
		return $clone;
413
	}
414
415
	/**
416
	 * Fragment
417
	 */
418
419
	/**
420
	 * @param mixed $fragment
421
	 *
422
	 * @return string
423
	 */
424
	protected function filterFragment($fragment):string{
425
		return $this->filterQuery($fragment);
426
	}
427
428
	/**
429
	 * @inheritDoc
430
	 */
431
	public function getFragment():string{
432
		return $this->fragment;
433
	}
434
435
	/**
436
	 * @inheritDoc
437
	 */
438
	public function withFragment($fragment):UriInterface{
439
		$fragment = $this->filterFragment($fragment);
440
441
		if($this->fragment === $fragment){
442
			return $this;
443
		}
444
445
		$clone           = clone $this;
446
		$clone->fragment = $fragment;
447
448
		$clone->validateState();
449
450
		return $clone;
451
	}
452
453
	/**
454
	 * @param array $parts
455
	 *
456
	 * @return void
457
	 */
458
	protected function parseUriParts(array $parts):void{
459
460
		foreach(['scheme', 'user', 'pass', 'host', 'port', 'path', 'query', 'fragment'] as $part){
461
462
			if(!isset($parts[$part])){
463
				continue;
464
			}
465
466
			$this->{$part} = call_user_func_array([$this, 'filter'.ucfirst($part)], [$parts[$part]]);
467
		}
468
469
		$this->removeDefaultPort();
470
	}
471
472
	/**
473
	 * @param string    $str
474
	 * @param bool|null $query
475
	 *
476
	 * @return string
477
	 */
478
	protected function replaceChars(string $str, bool $query = null):string{
479
		/** @noinspection RegExpRedundantEscape */
480
		return preg_replace_callback(
481
			'/(?:[^a-z\d_\-\.~!\$&\'\(\)\*\+,;=%:@\/'.($query ? '\?' : '').']++|%(?![a-f\d]{2}))/i',
482
			function(array $match):string{
483
				return rawurlencode($match[0]);
484
			},
485
			$str
486
		);
487
488
	}
489
490
	/**
491
	 * @return void
492
	 */
493
	protected function removeDefaultPort():void{
494
495
		if($this->port !== null && (isset($this::DEFAULT_PORTS[$this->scheme]) && $this->port === $this::DEFAULT_PORTS[$this->scheme])){
496
			$this->port = null;
497
		}
498
499
	}
500
501
	/**
502
	 * @return void
503
	 */
504
	protected function validateState():void{
505
506
		if(empty($this->host) && ($this->scheme === 'http' || $this->scheme === 'https')){
507
			$this->host = 'localhost';
508
		}
509
510
		if($this->getAuthority() !== ''){
511
512
			if(isset($this->path[0]) && $this->path[0] !== '/'){
513
				$this->path = '/'.$this->path; // automagically fix the path, unlike Guzzle
514
			}
515
516
		}
517
		else{
518
519
			if(strpos($this->path, '//') === 0){
520
				$this->path = '/'.ltrim($this->path, '/'); // automagically fix the path, unlike Guzzle
521
			}
522
523
			if(empty($this->scheme) && strpos(explode('/', $this->path, 2)[0], ':') !== false){
524
				throw new InvalidArgumentException('A relative URI must not have a path beginning with a segment containing a colon');
525
			}
526
527
		}
528
529
	}
530
531
}
532