Completed
Push — master ( 988ea9...868c80 )
by smiley
07:26
created

Uri::validateState()   B

Complexity

Conditions 10
Paths 12

Size

Total Lines 26

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 10
nc 12
nop 0
dl 0
loc 26
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 = '';
0 ignored issues
show
Bug introduced by
This code did not parse for me. Apparently, there is an error somewhere around this line:

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