Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
94.51% covered (success)
94.51%
155 / 164
97.92% covered (success)
97.92%
47 / 48
CRAP
0.00% covered (danger)
0.00%
0 / 1
Uri
94.51% covered (success)
94.51%
155 / 164
97.92% covered (success)
97.92%
47 / 48
100.62
0.00% covered (danger)
0.00%
0 / 1
 __construct
100.00% covered (success)
100.00%
22 / 22
100.00% covered (success)
100.00%
1 / 1
11
 create
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 getBasePath
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 getScheme
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 getHost
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 getFullHost
100.00% covered (success)
100.00%
4 / 4
100.00% covered (success)
100.00%
1 / 1
2
 getUsername
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 getPassword
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 getAuthority
100.00% covered (success)
100.00%
10 / 10
100.00% covered (success)
100.00%
1 / 1
4
 getUserInfo
100.00% covered (success)
100.00%
6 / 6
100.00% covered (success)
100.00%
1 / 1
3
 getPath
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 getPort
100.00% covered (success)
100.00%
7 / 7
100.00% covered (success)
100.00%
1 / 1
4
 getQuery
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 getQueryAsArray
100.00% covered (success)
100.00%
4 / 4
100.00% covered (success)
100.00%
1 / 1
2
 getFragment
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 getUri
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 getFullUri
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 getSegment
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 getSegments
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 hasBasePath
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 hasScheme
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 hasHost
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 hasUsername
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 hasPassword
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 hasUri
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 hasPort
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 hasQuery
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 hasFragment
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 hasSegments
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 hasSegment
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 setBasePath
100.00% covered (success)
100.00%
2 / 2
100.00% covered (success)
100.00%
1 / 1
1
 setScheme
100.00% covered (success)
100.00%
2 / 2
100.00% covered (success)
100.00%
1 / 1
1
 setHost
100.00% covered (success)
100.00%
2 / 2
100.00% covered (success)
100.00%
1 / 1
1
 setUsername
100.00% covered (success)
100.00%
2 / 2
100.00% covered (success)
100.00%
1 / 1
1
 setPassword
100.00% covered (success)
100.00%
2 / 2
100.00% covered (success)
100.00%
1 / 1
1
 setPort
100.00% covered (success)
100.00%
2 / 2
100.00% covered (success)
100.00%
1 / 1
1
 setQuery
100.00% covered (success)
100.00%
4 / 4
100.00% covered (success)
100.00%
1 / 1
2
 setFragment
100.00% covered (success)
100.00%
2 / 2
100.00% covered (success)
100.00%
1 / 1
1
 withScheme
100.00% covered (success)
100.00%
3 / 3
100.00% covered (success)
100.00%
1 / 1
2
 withUserInfo
100.00% covered (success)
100.00%
4 / 4
100.00% covered (success)
100.00%
1 / 1
2
 withHost
100.00% covered (success)
100.00%
3 / 3
100.00% covered (success)
100.00%
1 / 1
2
 withPort
100.00% covered (success)
100.00%
3 / 3
100.00% covered (success)
100.00%
1 / 1
1
 withPath
100.00% covered (success)
100.00%
3 / 3
100.00% covered (success)
100.00%
1 / 1
1
 withQuery
100.00% covered (success)
100.00%
3 / 3
100.00% covered (success)
100.00%
1 / 1
2
 withFragment
100.00% covered (success)
100.00%
3 / 3
100.00% covered (success)
100.00%
1 / 1
2
 setUri
70.97% covered (success)
70.97%
22 / 31
0.00% covered (danger)
0.00%
0 / 1
27.83
 render
100.00% covered (success)
100.00%
15 / 15
100.00% covered (success)
100.00%
1 / 1
8
 __toString
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
1<?php
2declare(strict_types=1);
3/**
4 * Pop PHP Framework (https://www.popphp.org/)
5 *
6 * @link       https://github.com/popphp/popphp-framework
7 * @author     Nick Sagona, III <nick@popphp.org>
8 * @copyright  Copyright (c) 2009-2026 Nick Sagona, III
9 * @license    https://www.popphp.org/license     New BSD License
10 */
11
12/**
13 * @namespace
14 */
15namespace Pop\Http;
16
17use Psr\Http\Message\UriInterface;
18
19/**
20 * HTTP URI class
21 *
22 * @category   Pop
23 * @package    Pop\Http
24 * @author     Nick Sagona, III <nick@popphp.org>
25 * @copyright  Copyright (c) 2009-2026 Nick Sagona, III
26 * @license    https://www.popphp.org/license     New BSD License
27 * @version    6.0.0
28 */
29class Uri implements UriInterface
30{
31
32    /**
33     * Scheme
34     * @var ?string
35     */
36    protected ?string $scheme = null;
37
38    /**
39     * Host
40     * @var ?string
41     */
42    protected ?string $host = null;
43
44    /**
45     * Username
46     * @var ?string
47     */
48    protected ?string $username = null;
49
50    /**
51     * Password
52     * @var ?string
53     */
54    protected ?string $password = null;
55
56    /**
57     * URI
58     * @var ?string
59     */
60    protected ?string $uri = null;
61
62    /**
63     * Port
64     * @var string|int|null
65     */
66    protected string|int|null $port = null;
67
68    /**
69     * Query
70     * @var ?string
71     */
72    protected ?string $query = null;
73
74    /**
75     * Fragment
76     * @var ?string
77     */
78    protected ?string $fragment = null;
79
80    /**
81     * Base path
82     * @var ?string
83     */
84    protected ?string $basePath = null;
85
86    /**
87     * Path segments
88     * @var array
89     */
90    protected array $segments = [];
91
92    /**
93     * Constructor
94     *
95     * Instantiate the URI object
96     *
97     * @param  ?string $uri
98     * @param  ?string $basePath
99     * @throws Exception
100     */
101    public function __construct(?string $uri = null, ?string $basePath = null)
102    {
103        $path = null;
104        if ($uri !== null) {
105            $uriInfo = parse_url($uri);
106
107            if ($uriInfo === false) {
108                throw new Exception('Error: Unable to parse the URI value.');
109            }
110
111            if (!empty($uriInfo['scheme'])) {
112                $this->setScheme($uriInfo['scheme']);
113            }
114            if (!empty($uriInfo['host'])) {
115                $this->setHost($uriInfo['host']);
116            }
117            if (!empty($uriInfo['user'])) {
118                $this->setUsername($uriInfo['user']);
119            }
120            if (!empty($uriInfo['pass'])) {
121                $this->setPassword($uriInfo['pass']);
122            }
123            if (!empty($uriInfo['port'])) {
124                $this->setPort($uriInfo['port']);
125            }
126            if (!empty($uriInfo['query'])) {
127                $this->setQuery($uriInfo['query']);
128            }
129            if (!empty($uriInfo['fragment'])) {
130                $this->setFragment($uriInfo['fragment']);
131            }
132            if (!empty($uriInfo['path'])) {
133                $path = $uriInfo['path'];
134            }
135        }
136
137        $this->setUri($path, $basePath);
138    }
139
140    /**
141     * Create URI object
142     *
143     * @param  ?string $uri
144     * @param  ?string $basePath
145     * @throws Exception
146     * @return Uri
147     */
148    public static function create(?string $uri = null, ?string $basePath = null): Uri
149    {
150        return new self($uri, $basePath);
151    }
152
153    /**
154     * Get the base path
155     *
156     * @return string
157     */
158    public function getBasePath(): string
159    {
160        return $this->basePath;
161    }
162
163    /**
164     * Get the scheme
165     *
166     * @return string
167     */
168    public function getScheme(): string
169    {
170        return $this->scheme ?? '';
171    }
172
173    /**
174     * Get the host
175     *
176     * @return string
177     */
178    public function getHost(): string
179    {
180        return $this->host ?? '';
181    }
182
183    /**
184     * Get the host with the port
185     *
186     * @return string
187     */
188    public function getFullHost(): string
189    {
190        $host = $this->host;
191        if ($this->hasPort()) {
192            $host .= ':' . $this->port;
193        }
194
195        return $host;
196    }
197
198    /**
199     * Get the username
200     *
201     * @return string
202     */
203    public function getUsername(): string
204    {
205        return $this->username;
206    }
207
208    /**
209     * Get the password
210     *
211     * @return string
212     */
213    public function getPassword(): string
214    {
215        return $this->password;
216    }
217
218    /**
219     * Get the URI authority (userinfo@host:port)
220     *
221     * @return string
222     */
223    public function getAuthority(): string
224    {
225        if (!$this->hasHost()) {
226            return '';
227        }
228
229        $authority = $this->getHost();
230        $userInfo  = $this->getUserInfo();
231
232        if ($userInfo !== '') {
233            $authority = $userInfo . '@' . $authority;
234        }
235
236        $port = $this->getPort();
237        if ($port !== null) {
238            $authority .= ':' . $port;
239        }
240
241        return $authority;
242    }
243
244    /**
245     * Get the URI user info (user[:password])
246     *
247     * @return string
248     */
249    public function getUserInfo(): string
250    {
251        if (!$this->hasUsername()) {
252            return '';
253        }
254
255        $userInfo = $this->username;
256
257        if ($this->hasPassword()) {
258            $userInfo .= ':' . $this->password;
259        }
260
261        return $userInfo;
262    }
263
264    /**
265     * Get the URI path
266     *
267     * @return string
268     */
269    public function getPath(): string
270    {
271        return $this->uri ?? '';
272    }
273
274    /**
275     * Get the port
276     *
277     * @return ?int
278     */
279    public function getPort(): ?int
280    {
281        if ($this->port === null) {
282            return null;
283        }
284
285        $port          = (int)$this->port;
286        $standardPorts = ['http' => 80, 'https' => 443];
287
288        if (isset($standardPorts[$this->scheme]) && ($port === $standardPorts[$this->scheme])) {
289            return null;
290        }
291
292        return $port;
293    }
294
295    /**
296     * Get the query
297     *
298     * @return string
299     */
300    public function getQuery(): string
301    {
302        return $this->query ?? '';
303    }
304
305    /**
306     * Get the query
307     *
308     * @return array
309     */
310    public function getQueryAsArray(): array
311    {
312        $result = [];
313
314        if ($this->query !== null) {
315            parse_str($this->query, $result);
316        }
317
318        return $result;
319    }
320
321    /**
322     * Get the fragment
323     *
324     * @return string
325     */
326    public function getFragment(): string
327    {
328        return $this->fragment ?? '';
329    }
330
331    /**
332     * Get the URI
333     *
334     * @return string
335     */
336    public function getUri(): string
337    {
338        return $this->uri;
339    }
340
341    /**
342     * Get the full URI, including base path
343     *
344     * @return string
345     */
346    public function getFullUri(): string
347    {
348        return $this->basePath . $this->uri;
349    }
350
351    /**
352     * Get a path segment, divided by the forward slash,
353     * where $i refers to the array key index, i.e.,
354     *    0     1     2
355     * /hello/world/page
356     *
357     * @param  int $i
358     * @return string|null
359     */
360    public function getSegment(int $i): string|null
361    {
362        return $this->segments[(int)$i] ?? null;
363    }
364
365    /**
366     * Get all path segments
367     *
368     * @return array
369     */
370    public function getSegments(): array
371    {
372        return $this->segments;
373    }
374
375    /**
376     * Has a base path
377     *
378     * @return bool
379     */
380    public function hasBasePath(): bool
381    {
382        return ($this->basePath !== null);
383    }
384
385    /**
386     * Has a scheme
387     *
388     * @return bool
389     */
390    public function hasScheme(): bool
391    {
392        return ($this->scheme !== null);
393    }
394
395    /**
396     * Has a host
397     *
398     * @return bool
399     */
400    public function hasHost(): bool
401    {
402        return ($this->host !== null);
403    }
404
405    /**
406     * Has a username
407     *
408     * @return bool
409     */
410    public function hasUsername(): bool
411    {
412        return ($this->username !== null);
413    }
414
415    /**
416     * Has a password
417     *
418     * @return bool
419     */
420    public function hasPassword(): bool
421    {
422        return ($this->password !== null);
423    }
424
425    /**
426     * Has a uri
427     *
428     * @return bool
429     */
430    public function hasUri(): bool
431    {
432        return ($this->uri !== null);
433    }
434
435    /**
436     * Has a port
437     *
438     * @return bool
439     */
440    public function hasPort(): bool
441    {
442        return ($this->port !== null);
443    }
444
445    /**
446     * Has a query
447     *
448     * @return bool
449     */
450    public function hasQuery(): bool
451    {
452        return ($this->query !== null);
453    }
454
455    /**
456     * Has a fragment
457     *
458     * @return bool
459     */
460    public function hasFragment(): bool
461    {
462        return ($this->fragment !== null);
463    }
464
465    /**
466     * Has segments
467     *
468     * @return bool
469     */
470    public function hasSegments(): bool
471    {
472        return !empty($this->segments);
473    }
474
475    /**
476     * Has segment
477     *
478     * @return bool
479     */
480    public function hasSegment($i): bool
481    {
482        return isset($this->segments[$i]);
483    }
484
485    /**
486     * Set the base path
487     *
488     * @param  ?string $path
489     * @return Uri
490     */
491    public function setBasePath(?string $path = null): Uri
492    {
493        $this->basePath = $path;
494        return $this;
495    }
496
497    /**
498     * Set the scheme
499     *
500     * @param  string $scheme
501     * @return Uri
502     */
503    public function setScheme(string $scheme): Uri
504    {
505        $this->scheme = $scheme;
506        return $this;
507    }
508
509    /**
510     * Set the host
511     *
512     * @param  string $host
513     * @return Uri
514     */
515    public function setHost(string $host): Uri
516    {
517        $this->host = $host;
518        return $this;
519    }
520
521    /**
522     * Set the username
523     *
524     * @param  string $username
525     * @return Uri
526     */
527    public function setUsername(string $username): Uri
528    {
529        $this->username = $username;
530        return $this;
531    }
532
533    /**
534     * Set the password
535     *
536     * @param  string $password
537     * @return Uri
538     */
539    public function setPassword(string $password): Uri
540    {
541        $this->password = $password;
542        return $this;
543    }
544
545    /**
546     * Set the port
547     *
548     * @param  string|int $port
549     * @return Uri
550     */
551    public function setPort(string|int $port): Uri
552    {
553        $this->port = $port;
554        return $this;
555    }
556
557    /**
558     * Set the query
559     *
560     * @param  string|array $query
561     * @return Uri
562     */
563    public function setQuery(string|array $query): Uri
564    {
565        if (is_array($query)) {
566            $query = http_build_query($query);
567        }
568        $this->query = $query;
569        return $this;
570    }
571
572    /**
573     * Set the fragment
574     *
575     * @param  string $fragment
576     * @return Uri
577     */
578    public function setFragment(string $fragment): Uri
579    {
580        $this->fragment = $fragment;
581        return $this;
582    }
583
584    /**
585     * Return an instance with the specified scheme
586     *
587     * @param  string $scheme
588     * @return static
589     */
590    public function withScheme(string $scheme): static
591    {
592        $clone         = clone $this;
593        $clone->scheme = ($scheme !== '') ? $scheme : null;
594        return $clone;
595    }
596
597    /**
598     * Return an instance with the specified user info
599     *
600     * @param  string  $user
601     * @param  ?string $password
602     * @return static
603     */
604    public function withUserInfo(string $user, ?string $password = null): static
605    {
606        $clone           = clone $this;
607        $clone->username = ($user !== '') ? $user : null;
608        $clone->password = $password;
609        return $clone;
610    }
611
612    /**
613     * Return an instance with the specified host
614     *
615     * @param  string $host
616     * @return static
617     */
618    public function withHost(string $host): static
619    {
620        $clone       = clone $this;
621        $clone->host = ($host !== '') ? $host : null;
622        return $clone;
623    }
624
625    /**
626     * Return an instance with the specified port
627     *
628     * @param  ?int $port
629     * @return static
630     */
631    public function withPort(?int $port): static
632    {
633        $clone       = clone $this;
634        $clone->port = $port;
635        return $clone;
636    }
637
638    /**
639     * Return an instance with the specified path
640     *
641     * @param  string $path
642     * @return static
643     */
644    public function withPath(string $path): static
645    {
646        $clone      = clone $this;
647        $clone->uri = $path;
648        return $clone;
649    }
650
651    /**
652     * Return an instance with the specified query string
653     *
654     * @param  string $query
655     * @return static
656     */
657    public function withQuery(string $query): static
658    {
659        $clone        = clone $this;
660        $clone->query = ($query !== '') ? $query : null;
661        return $clone;
662    }
663
664    /**
665     * Return an instance with the specified fragment
666     *
667     * @param  string $fragment
668     * @return static
669     */
670    public function withFragment(string $fragment): static
671    {
672        $clone           = clone $this;
673        $clone->fragment = ($fragment !== '') ? $fragment : null;
674        return $clone;
675    }
676
677    /**
678     * Set the URI
679     *
680     * @param  ?string $uri
681     * @param  ?string $basePath
682     * @return Uri
683     */
684    public function setUri(?string $uri = null, ?string $basePath = null): Uri
685    {
686        $isServerRequest = false;
687        if (($uri === null) && isset($_SERVER['REQUEST_URI'])) {
688            $uri = $_SERVER['REQUEST_URI'];
689            $isServerRequest = true;
690        }
691
692        if (!empty($basePath)) {
693            if (substr($uri, 0, (strlen($basePath) + 1)) == $basePath . '/') {
694                $uri = substr($uri, (strpos($uri, $basePath) + strlen($basePath)));
695            } else if (substr($uri, 0, (strlen($basePath) + 1)) == $basePath . '?') {
696                $uri = '/' . substr($uri, (strpos($uri, $basePath) + strlen($basePath)));
697            }
698        }
699
700        if (($uri == '') || ($uri == $basePath)) {
701            $uri = '/';
702        }
703
704        // Some slash clean up
705        $this->uri = $uri;
706
707        if ($isServerRequest) {
708            $docRoot = (isset($_SERVER['DOCUMENT_ROOT'])) ? str_replace('\\', '/', $_SERVER['DOCUMENT_ROOT']) : null;
709            $dir     = str_replace('\\', '/', getcwd());
710
711            if (($dir != $docRoot) && (strlen($dir) > strlen($docRoot))) {
712                $realBasePath = str_replace($docRoot, '', $dir);
713                if (str_starts_with($uri, $realBasePath)) {
714                    $this->uri = substr($uri, strlen($realBasePath));
715                }
716            }
717
718            $this->setBasePath((($basePath === null) ? str_replace($docRoot, '', $dir) : $basePath));
719        } else {
720            $this->setBasePath($basePath);
721        }
722
723        // Get fragment
724        if (str_contains($this->uri, '#')) {
725            $this->fragment = substr($this->uri, (strpos($this->uri, '#') + 1));
726            $this->uri      = substr($this->uri, 0, strpos($this->uri, '#'));
727        }
728
729        // Get query
730        if (str_contains($this->uri, '?')) {
731            $this->query = substr($this->uri, (strpos($this->uri, '?') + 1));
732            $this->uri   = substr($this->uri, 0, strpos($this->uri, '?'));
733        }
734
735        // Get segments
736        if (($this->uri != '/') && (str_contains($this->uri, '/'))) {
737            $uri = (str_starts_with($this->uri, '/')) ? substr($this->uri, 1) : $this->uri;
738            $this->segments = explode('/', $uri);
739        }
740
741        return $this;
742    }
743
744    /**
745     * Render the URI
746     *
747     * @return string
748     */
749    public function render(): string
750    {
751        $uri = '';
752
753        if ($this->hasScheme()) {
754            $uri .= $this->getScheme() . '://';
755        }
756        if (($this->hasUsername()) && ($this->hasPassword())) {
757            $uri .= $this->getUsername() . ':' . $this->getPassword() . '@';
758        }
759        if ($this->hasHost()) {
760            $uri .= $this->getHost();
761        }
762        if ($this->hasPort()) {
763            $uri .= ':' . $this->getPort();
764        }
765
766        $uri .= $this->getFullUri();
767
768        if ($this->hasQuery()) {
769            $uri .= '?' . $this->getQuery();
770        }
771        if ($this->hasFragment()) {
772            $uri .= '#' . $this->getFragment();
773        }
774
775        return $uri;
776    }
777
778    /**
779     * Render the URI
780     *
781     * @return string
782     */
783    public function __toString(): string
784    {
785        return $this->render();
786    }
787
788}