Passed
Push — main ( f2efe3...53dc0a )
by smiley
10:15
created

Uri::validateState()   B

Complexity

Conditions 10
Paths 12

Size

Total Lines 21
Code Lines 10

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 10
eloc 10
nc 12
nop 0
dl 0
loc 21
rs 7.6666
c 0
b 0
f 0

How to fix   Complexity   

Long Method

Small methods make your code easier to understand, in particular if combined with a good name. Besides, if your method is small, finding a good name is usually much easier.

For example, if you find yourself adding comments to a method's body, this is usually a good sign to extract the commented part to a new method, and use the comment as a starting point when coming up with a good name for this new method.

Commonly applied refactorings include:

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