Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
100.00% covered (success)
100.00%
42 / 42
100.00% covered (success)
100.00%
4 / 4
CRAP
100.00% covered (success)
100.00%
1 / 1
Url
100.00% covered (success)
100.00%
42 / 42
100.00% covered (success)
100.00%
4 / 4
22
100.00% covered (success)
100.00%
1 / 1
 __construct
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 getQueryString
100.00% covered (success)
100.00%
2 / 2
100.00% covered (success)
100.00%
1 / 1
1
 hydrateFromArray
100.00% covered (success)
100.00%
25 / 25
100.00% covered (success)
100.00%
1 / 1
10
 render
100.00% covered (success)
100.00%
14 / 14
100.00% covered (success)
100.00%
1 / 1
10
1<?php
2
3/**
4 * @author Doug Wilbourne (dougwilbourne@gmail.com)
5 */
6
7namespace pvc\http\url;
8
9use pvc\http\err\InvalidUrlException;
10use pvc\interfaces\http\QueryStringInterface;
11use pvc\interfaces\http\UrlInterface;
12use pvc\interfaces\parser\ParserQueryStringInterface;
13use pvc\interfaces\validator\ValTesterInterface;
14
15/**
16 * Class Url
17 *
18 * The purpose of the class is to make it easy to manipulate the various parts of a url without having to resort
19 * to string manipulation.
20 *
21 * There is no validation done when setting the values of the individual components.  But, by default the render
22 * method will validate the url before returning the generated url and will throw an exception if it is not valid.
23 * This behavior is configurable.
24 *
25 * You can create a url from scratch with this object.  You can also start with an existing url and hydrate this
26 * object using the ParserUrl class found in the pvc Parser library.  And you can even hydrate this object
27 * directly from an array which is produced by php's parse_url method.  Just be aware that the parse_url verb
28 * will mangle pieces of a url when it finds characters it does not like.  The ParserUrl class validates a url
29 * before parsing and automatically hydrates the Url object for you.
30 *
31 * @phpstan-type UrlShape array{string: 'scheme', string: 'host', non-negative-int: 'port', string: 'user', string: 'password', string: 'path', string: 'fragment'}
32 */
33class Url implements UrlInterface
34{
35    /**
36     * @var string
37     * protocol e.g. http, https, ftp, etc.
38     */
39    public string $scheme = '';
40
41    public string $host = '';
42
43    /**
44     * @var non-negative-int|null
45     */
46    public int|null $port = null;
47
48    public string $user = '';
49
50    public string $password = '';
51
52    public string $path = '';
53
54    public string $fragment = '';
55
56    /**
57     * @param ParserQueryStringInterface $parserQueryString
58     * @param ValTesterInterface<string> $urlTester
59     */
60    public function __construct(
61        protected ParserQueryStringInterface $parserQueryString,
62        protected ValTesterInterface         $urlTester,
63    )
64    {
65    }
66
67    /**
68     * getQueryString
69     * @return QueryStringInterface
70     */
71    public function getQueryString(): QueryStringInterface
72    {
73        /** @var QueryStringInterface $qstr */
74        $qstr = $this->parserQueryString->getParsedValue();
75        return $qstr;
76    }
77
78    /**
79     * @param UrlShape $urlParts
80     * @return void
81     */
82    public function hydrateFromArray(array $urlParts): void
83    {
84        foreach ($urlParts as $partName => $part) {
85            switch ($partName) {
86                case 'scheme':
87                    $this->scheme = $part;
88                    break;
89                case 'host':
90                    $this->host = $part;
91                    break;
92                case 'port':
93                    /**
94                     * get a very odd phpstan error here: $port (int<0, max>|null) does not accept 'fragment'|'port'
95                     * @phpstan-ignore-next-line
96                     */
97                    $this->port = $part;
98                    break;
99                case 'user':
100                    $this->user = $part;
101                    break;
102                case 'password':
103                    $this->password = $part;
104                    break;
105                case 'path':
106                    $this->path = $part;
107                    break;
108                case 'query':
109                    $this->parserQueryString->parse($part);
110                    /**
111                     * nothing needs to be set.  The querystring parser contains a querystring object.  You can get
112                     * that object and manipulate it if you need to.
113                     */
114                    break;
115                case 'fragment':
116                    $this->fragment = $part;
117                    break;
118            }
119        }
120    }
121
122    /**
123     * generateURLString
124     * @param bool $validateBeforeRender
125     * @return string
126     * @throws InvalidUrlException
127     *
128     */
129    public function render(bool $validateBeforeRender = true): string
130    {
131        $urlString = '';
132        $urlString .= ($this->scheme !== '') ? $this->scheme . '://' : '';
133        $urlString .= $this->user;
134
135        /**
136         * user is separated from password by a colon.  Does it make sense to output a password if there is no user?
137         * For now, this outputs a password even if there is no user.
138         */
139        $urlString .= ($this->password !== '') ? ':' . $this->password : '';
140
141        /**
142         * separate userid / password from path with a '@'
143         */
144        $urlString .= ($this->user || $this->password) ? '@' : '';
145
146        $urlString .= $this->host;
147        $urlString .= $this->port ? ':' . $this->port : '';
148        $urlString .= $this->path;
149
150        $query = $this->getQueryString()->render();
151        $urlString .= ($query !== '') ? '?' . $query : '';
152
153        $urlString .= ($this->fragment !== '') ? '#' . $this->fragment : '';
154
155        if ($validateBeforeRender && !$this->urlTester->testValue($urlString)) {
156            throw new InvalidUrlException($urlString);
157        }
158
159        return $urlString;
160    }
161}