Code Coverage |
||||||||||
Lines |
Functions and Methods |
Classes and Traits |
||||||||
Total | |
100.00% |
42 / 42 |
|
100.00% |
4 / 4 |
CRAP | |
100.00% |
1 / 1 |
Url | |
100.00% |
42 / 42 |
|
100.00% |
4 / 4 |
22 | |
100.00% |
1 / 1 |
__construct | |
100.00% |
1 / 1 |
|
100.00% |
1 / 1 |
1 | |||
getQueryString | |
100.00% |
2 / 2 |
|
100.00% |
1 / 1 |
1 | |||
hydrateFromArray | |
100.00% |
25 / 25 |
|
100.00% |
1 / 1 |
10 | |||
render | |
100.00% |
14 / 14 |
|
100.00% |
1 / 1 |
10 |
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\parser\ParserQueryStringInterface; |
13 | use 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 | */ |
33 | class 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 | } |