Skip to content

Commit cdfb6aa

Browse files
committed
feat(support): add Uri utils
1 parent a6145d0 commit cdfb6aa

File tree

6 files changed

+1080
-1
lines changed

6 files changed

+1080
-1
lines changed

composer.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -179,6 +179,7 @@
179179
"packages/support/src/Regex/functions.php",
180180
"packages/support/src/Str/constants.php",
181181
"packages/support/src/Str/functions.php",
182+
"packages/support/src/Uri/functions.php",
182183
"packages/support/src/functions.php",
183184
"packages/view/src/functions.php",
184185
"packages/vite/src/functions.php"

packages/support/composer.json

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -27,7 +27,8 @@
2727
"src/Math/constants.php",
2828
"src/Math/functions.php",
2929
"src/Json/functions.php",
30-
"src/Filesystem/functions.php"
30+
"src/Filesystem/functions.php",
31+
"src/Uri/functions.php"
3132
]
3233
},
3334
"autoload-dev": {

packages/support/src/Uri/Uri.php

Lines changed: 333 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,333 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
namespace Tempest\Support\Uri;
6+
7+
use Tempest\Support\Str;
8+
use Tempest\Support\Str\ImmutableString;
9+
use Tempest\Support\Str\MutableString;
10+
11+
use function parse_url;
12+
13+
final class Uri
14+
{
15+
/**
16+
* The path segments as an array.
17+
*/
18+
public array $segments {
19+
get {
20+
if ($this->path === null || $this->path === '' || $this->path === '/') {
21+
return [];
22+
}
23+
24+
return array_values(array_filter(explode('/', $this->path), fn (string $segment) => $segment !== ''));
25+
}
26+
}
27+
28+
/**
29+
* The parsed query parameters as an associative array.
30+
*/
31+
public array $query {
32+
get {
33+
if ($this->queryString === null || $this->queryString === '') {
34+
return [];
35+
}
36+
37+
parse_str($this->queryString, $query);
38+
39+
return $query;
40+
}
41+
}
42+
43+
/**
44+
* @param null|string $scheme The scheme component of the URI.
45+
* @param null|string $user The user component of the URI.
46+
* @param null|string $password The password component of the URI.
47+
* @param null|string $host The host component of the URI.
48+
* @param null|int $port The port component of the URI.
49+
* @param null|string $path The path component of the URI.
50+
* @param null|string $queryString The query string component of the URI.
51+
* @param null|string $fragment The fragment component of the URI.
52+
*/
53+
public function __construct(
54+
public readonly ?string $scheme = null,
55+
public readonly ?string $user = null,
56+
public readonly ?string $password = null,
57+
public readonly ?string $host = null,
58+
public readonly ?int $port = null,
59+
public readonly ?string $path = null,
60+
public readonly ?string $queryString = null,
61+
public readonly ?string $fragment = null,
62+
) {}
63+
64+
/**
65+
* Creates a Uri instance from a URI string.
66+
*/
67+
public static function from(string $uri): self
68+
{
69+
$parts = parse_url($uri);
70+
71+
if ($parts === false) {
72+
return new self(path: $uri);
73+
}
74+
75+
return new self(
76+
scheme: $parts['scheme'] ?? null,
77+
user: $parts['user'] ?? null,
78+
password: $parts['pass'] ?? null,
79+
host: $parts['host'] ?? null,
80+
port: $parts['port'] ?? null,
81+
path: $parts['path'] ?? null,
82+
queryString: $parts['query'] ?? null,
83+
fragment: $parts['fragment'] ?? null,
84+
);
85+
}
86+
87+
/**
88+
* Returns a new Uri with the provided scheme.
89+
*/
90+
public function withScheme(string $scheme): self
91+
{
92+
return $this->with(scheme: $scheme);
93+
}
94+
95+
/**
96+
* Returns a new Uri with the provided user.
97+
*/
98+
public function withUser(string $user): self
99+
{
100+
return $this->with(user: $user);
101+
}
102+
103+
/**
104+
* Returns a new Uri with the provided password.
105+
*/
106+
public function withPassword(string $password): self
107+
{
108+
return $this->with(
109+
user: $this->user ?? '',
110+
password: $password,
111+
);
112+
}
113+
114+
/**
115+
* Returns a new Uri with the provided host.
116+
*/
117+
public function withHost(string $host): self
118+
{
119+
return $this->with(host: $host);
120+
}
121+
122+
/**
123+
* Returns a new Uri with the provided port.
124+
*/
125+
public function withPort(int $port): self
126+
{
127+
return $this->with(port: $port);
128+
}
129+
130+
/**
131+
* Returns a new Uri with the provided path.
132+
*/
133+
public function withPath(string $path): self
134+
{
135+
return $this->with(path: $path);
136+
}
137+
138+
/**
139+
* Returns a new Uri with the provided query parameters (replaces existing).
140+
*/
141+
public function withQuery(mixed ...$query): self
142+
{
143+
return $this->with(queryString: $this->buildQueryString($query));
144+
}
145+
146+
/**
147+
* Returns a new Uri with added query parameters (merges with existing).
148+
*/
149+
public function addQuery(mixed ...$query): self
150+
{
151+
return $this->with(queryString: $this->buildQueryString(
152+
query: array_merge($this->query, $query),
153+
));
154+
}
155+
156+
/**
157+
* Returns a new Uri with all query parameters removed.
158+
*/
159+
public function removeQuery(): self
160+
{
161+
return new self(
162+
scheme: $this->scheme,
163+
user: $this->user,
164+
password: $this->password,
165+
host: $this->host,
166+
port: $this->port,
167+
path: $this->path,
168+
queryString: null,
169+
fragment: $this->fragment,
170+
);
171+
}
172+
173+
/**
174+
* Returns a new Uri with specific query parameters removed.
175+
*/
176+
public function withoutQuery(mixed ...$query): self
177+
{
178+
$currentQuery = $this->query;
179+
180+
foreach ($query as $key => $value) {
181+
if (is_int($key)) {
182+
unset($currentQuery[$value]);
183+
} else {
184+
if (isset($currentQuery[$key]) && $currentQuery[$key] === $value) {
185+
unset($currentQuery[$key]);
186+
}
187+
}
188+
}
189+
190+
$newQueryString = $this->buildQueryString($currentQuery);
191+
192+
return new self(
193+
scheme: $this->scheme,
194+
user: $this->user,
195+
password: $this->password,
196+
host: $this->host,
197+
port: $this->port,
198+
path: $this->path,
199+
queryString: $newQueryString,
200+
fragment: $this->fragment,
201+
);
202+
}
203+
204+
/**
205+
* Returns a new Uri with the provided fragment.
206+
*/
207+
public function withFragment(string $fragment): self
208+
{
209+
return $this->with(fragment: $fragment);
210+
}
211+
212+
/**
213+
* Returns a new Uri with the specified components changed.
214+
*/
215+
private function with(
216+
?string $scheme = null,
217+
?string $user = null,
218+
?string $password = null,
219+
?string $host = null,
220+
?int $port = null,
221+
?string $path = null,
222+
?string $queryString = null,
223+
array $query = [],
224+
?string $fragment = null,
225+
): self {
226+
return new self(
227+
scheme: $scheme ?? $this->scheme,
228+
user: $user ?? $this->user,
229+
password: $password ?? $this->password,
230+
host: $host ?? $this->host,
231+
port: $port ?? $this->port,
232+
path: Str\ensure_starts_with($path ?? $this->path, '/'),
233+
queryString: match (true) {
234+
$queryString !== null => $queryString,
235+
$query !== [] => $this->buildQueryString($query),
236+
default => $this->queryString,
237+
},
238+
fragment: $fragment ?? $this->fragment,
239+
);
240+
}
241+
242+
/**
243+
* Builds a query string from an array of query parameters.
244+
*/
245+
private function buildQueryString(array $query): ?string
246+
{
247+
if ($query === []) {
248+
return null;
249+
}
250+
251+
$processedQuery = [];
252+
253+
foreach ($query as $key => $value) {
254+
if (is_int($key) && is_string($value)) {
255+
$processedQuery[$value] = '';
256+
} else {
257+
if (is_bool($value)) {
258+
$value = $value ? 'true' : 'false';
259+
}
260+
261+
$processedQuery[$key] = $value;
262+
}
263+
}
264+
265+
$queryString = http_build_query($processedQuery, arg_separator: '&', encoding_type: PHP_QUERY_RFC3986);
266+
$queryString = preg_replace('/([^=&]+)=(?=&|$)/', replacement: '$1', subject: $queryString);
267+
268+
return $queryString;
269+
}
270+
271+
/**
272+
* Builds the URI string from its components.
273+
*/
274+
public function toString(): string
275+
{
276+
$uri = '';
277+
278+
if ($this->scheme !== null) {
279+
$uri .= $this->scheme . ':';
280+
}
281+
282+
if ($this->user !== null || $this->host !== null) {
283+
$uri .= '//';
284+
285+
if ($this->user !== null) {
286+
$uri .= $this->user;
287+
288+
if ($this->password !== null) {
289+
$uri .= ':' . $this->password;
290+
}
291+
292+
$uri .= '@';
293+
}
294+
295+
if ($this->host !== null) {
296+
$uri .= $this->host;
297+
}
298+
299+
if ($this->port !== null) {
300+
$uri .= ':' . $this->port;
301+
}
302+
}
303+
304+
if ($this->path !== null) {
305+
$uri .= $this->path;
306+
}
307+
308+
if ($this->queryString !== null && $this->queryString !== '') {
309+
$uri .= '?' . $this->queryString;
310+
}
311+
312+
if ($this->fragment !== null) {
313+
$uri .= '#' . $this->fragment;
314+
}
315+
316+
return $uri;
317+
}
318+
319+
public function __toString(): string
320+
{
321+
return $this->toString();
322+
}
323+
324+
public function toImmutableString(): ImmutableString
325+
{
326+
return new ImmutableString($this->toString());
327+
}
328+
329+
public function toMutableString(): MutableString
330+
{
331+
return new MutableString($this->toString());
332+
}
333+
}

0 commit comments

Comments
 (0)