Code Coverage |
||||||||||
Lines |
Functions and Methods |
Classes and Traits |
||||||||
Total | |
100.00% |
56 / 56 |
|
100.00% |
7 / 7 |
CRAP | |
100.00% |
1 / 1 |
Url | |
100.00% |
56 / 56 |
|
100.00% |
7 / 7 |
35 | |
100.00% |
1 / 1 |
__construct | |
100.00% |
1 / 1 |
|
100.00% |
1 / 1 |
1 | |||
testUrl | |
100.00% |
1 / 1 |
|
100.00% |
1 / 1 |
2 | |||
getQuery | |
100.00% |
1 / 1 |
|
100.00% |
1 / 1 |
1 | |||
hydrateFromArray | |
100.00% |
25 / 25 |
|
100.00% |
1 / 1 |
10 | |||
parse | |
100.00% |
3 / 3 |
|
100.00% |
1 / 1 |
3 | |||
getUrlParts | |
100.00% |
11 / 11 |
|
100.00% |
1 / 1 |
9 | |||
render | |
100.00% |
14 / 14 |
|
100.00% |
1 / 1 |
9 |
1 | <?php |
2 | |
3 | /** |
4 | * @author Doug Wilbourne (dougwilbourne@gmail.com) |
5 | */ |
6 | |
7 | namespace pvc\http\url; |
8 | |
9 | use pvc\http\err\InvalidUrlException; |
10 | use pvc\interfaces\http\QueryStringInterface; |
11 | use pvc\interfaces\http\UrlInterface; |
12 | use pvc\interfaces\validator\ValTesterInterface; |
13 | |
14 | /** |
15 | * Class Url |
16 | * |
17 | * The purpose of the class is to make it easy to manipulate the various parts of a url without having to resort |
18 | * to string manipulation. |
19 | * |
20 | * There is no validation done when setting the values of the individual components. But, by default the render |
21 | * method will validate the url before returning the generated url and will throw an exception if it is not valid. |
22 | * This behavior is configurable. |
23 | * |
24 | * You can create a url from scratch with this object. You can also start with an existing url and hydrate this |
25 | * object using the ParserUrl class found in the pvc Parser library. And you can even hydrate this object |
26 | * directly from an array which is produced by php's parse_url method. Just be aware that the parse_url verb |
27 | * will mangle pieces of a url when it finds characters it does not like. The ParserUrl class validates a url |
28 | * before parsing and automatically hydrates the Url object for you. |
29 | * |
30 | * @phpstan-type UrlShape array{scheme?:string, host?:string, port?:int<0, 65535>, user?:string, pass?:string, path?:string, query?:string, fragment?:string} |
31 | */ |
32 | class Url implements UrlInterface |
33 | { |
34 | /** |
35 | * @var ?string |
36 | * protocol e.g. http, https, ftp, etc. |
37 | */ |
38 | public ?string $scheme = null; |
39 | |
40 | public ?string $host = null; |
41 | |
42 | /** |
43 | * @var int<0, 65535>|null |
44 | */ |
45 | public ?int $port = null; |
46 | |
47 | public ?string $user = null; |
48 | |
49 | public ?string $pass = null; |
50 | |
51 | public ?string $path = null; |
52 | |
53 | public ?string $fragment = null; |
54 | |
55 | /** |
56 | * @param QueryStringInterface $queryString |
57 | * @param ?ValTesterInterface<string> $urlTester |
58 | * |
59 | * if $urlTester is not supplied, incoming url strings to be parsed and |
60 | * outgoing url strings that have been rendered will not be checked for |
61 | * validity |
62 | */ |
63 | public function __construct( |
64 | protected QueryStringInterface $queryString, |
65 | protected ?ValTesterInterface $urlTester = null, |
66 | ) |
67 | { |
68 | } |
69 | |
70 | protected function testUrl(string $url): bool |
71 | { |
72 | return null === $this->urlTester || $this->urlTester->testValue($url); |
73 | } |
74 | |
75 | /** |
76 | * getQueryString |
77 | * @return QueryStringInterface |
78 | */ |
79 | public function getQuery(): QueryStringInterface |
80 | { |
81 | return $this->queryString; |
82 | } |
83 | |
84 | /** |
85 | * @param UrlShape $urlParts |
86 | * @return void |
87 | */ |
88 | public function hydrateFromArray(array $urlParts): void |
89 | { |
90 | foreach ($urlParts as $partName => $part) { |
91 | switch ($partName) { |
92 | case 'scheme': |
93 | $this->scheme = $part; |
94 | break; |
95 | case 'host': |
96 | $this->host = $part; |
97 | break; |
98 | case 'port': |
99 | $this->port = $part; |
100 | break; |
101 | case 'user': |
102 | $this->user = $part; |
103 | break; |
104 | case 'pass': |
105 | $this->pass = $part; |
106 | break; |
107 | case 'path': |
108 | $this->path = $part; |
109 | break; |
110 | case 'query': |
111 | $this->getQuery()->parse($part); |
112 | break; |
113 | case 'fragment': |
114 | $this->fragment = $part; |
115 | break; |
116 | } |
117 | } |
118 | } |
119 | |
120 | /** |
121 | * parse |
122 | * |
123 | * @param string $url |
124 | * |
125 | * @return void |
126 | */ |
127 | public function parse(string $url): void |
128 | { |
129 | /** |
130 | * parse_url will happily mangle the results of urls which are not well-formed, |
131 | * so we optionally validate the url first |
132 | */ |
133 | if (!$this->testUrl($url) || (false === ($urlParts = parse_url($url)))) { |
134 | throw new InvalidUrlException($url); |
135 | } |
136 | |
137 | $this->hydrateFromArray($urlParts); |
138 | } |
139 | |
140 | /** |
141 | * getUrlParts |
142 | * @return UrlShape |
143 | */ |
144 | public function getUrlParts(): array |
145 | { |
146 | $result = []; |
147 | |
148 | if ($this->scheme) $result['scheme'] = $this->scheme; |
149 | if ($this->host) $result['host'] = $this->host; |
150 | if ($this->port) $result['port'] = $this->port; |
151 | if ($this->user) $result['user'] = $this->user; |
152 | if ($this->pass) $result['pass'] = $this->pass; |
153 | if ($this->path) $result['path'] = $this->path; |
154 | |
155 | $query = $this->queryString->render(); |
156 | if ($query) $result['query'] = $query; |
157 | |
158 | if ($this->fragment) $result['fragment'] = $this->fragment; |
159 | return $result; |
160 | |
161 | } |
162 | |
163 | /** |
164 | * generateURLString |
165 | * @return string |
166 | * @throws InvalidUrlException |
167 | * |
168 | */ |
169 | public function render(): string |
170 | { |
171 | $urlString = ''; |
172 | $urlString .= $this->scheme ? $this->scheme . '://' : ''; |
173 | $urlString .= $this->user; |
174 | |
175 | /** |
176 | * user is separated from password by a colon. Does it make sense to output a password if there is no user? |
177 | * For now, this outputs a password even if there is no user. |
178 | */ |
179 | $urlString .= $this->pass ? ':' . $this->pass : ''; |
180 | |
181 | /** |
182 | * separate userid / password from path with a '@' |
183 | */ |
184 | $urlString .= ($this->user || $this->pass) ? '@' : ''; |
185 | |
186 | $urlString .= $this->host; |
187 | $urlString .= $this->port ? ':' . $this->port : ''; |
188 | $urlString .= $this->path; |
189 | |
190 | $query = $this->getQuery()->render(); |
191 | $urlString .= $query ? '?' . $query : ''; |
192 | |
193 | $urlString .= $this->fragment ? '#'.$this->fragment : ''; |
194 | |
195 | if (!$this->testUrl($urlString)) { |
196 | throw new InvalidUrlException($urlString); |
197 | } |
198 | |
199 | return $urlString; |
200 | } |
201 | } |