friendship ended with social-app. php is my new best friend
1<?php
2
3declare(strict_types=1);
4
5namespace Fetch\Concerns;
6
7use InvalidArgumentException;
8
9trait HandlesUris
10{
11 /**
12 * Build and get the full URI from a base URI and path.
13 *
14 * @param string $uri The path or absolute URI
15 * @return string The full URI
16 *
17 * @throws InvalidArgumentException If the URI is invalid
18 */
19 protected function buildFullUri(string $uri): string
20 {
21 // Get base URI and query parameters from options
22 $baseUri = $this->options['base_uri'] ?? '';
23 $queryParams = $this->options['query'] ?? [];
24
25 // Normalize URIs before processing
26 $uri = $this->normalizeUri($uri);
27 if (! empty($baseUri)) {
28 $baseUri = $this->normalizeUri($baseUri);
29 }
30
31 // Validate inputs
32 $this->validateUriInputs($uri, $baseUri);
33
34 // Build the final URI
35 $fullUri = $this->isAbsoluteUrl($uri)
36 ? $uri
37 : $this->joinUriPaths($baseUri, $uri);
38
39 // Add query parameters if any
40 return $this->appendQueryParameters($fullUri, $queryParams);
41 }
42
43 /**
44 * Get the full URI using the URI from options.
45 *
46 * @return string The full URI
47 *
48 * @throws InvalidArgumentException If the URI is invalid
49 */
50 protected function getFullUri(): string
51 {
52 $uri = $this->options['uri'] ?? '';
53
54 return $this->buildFullUri($uri);
55 }
56
57 /**
58 * Validate URI and base URI inputs.
59 *
60 * @param string $uri The URI or path
61 * @param string $baseUri The base URI
62 *
63 * @throws InvalidArgumentException If validation fails
64 */
65 protected function validateUriInputs(string $uri, string $baseUri): void
66 {
67 // Check if we have any URI to work with
68 if (empty($uri) && empty($baseUri)) {
69 throw new InvalidArgumentException('URI cannot be empty');
70 }
71
72 // For relative URIs, ensure we have a base URI
73 if (! $this->isAbsoluteUrl($uri) && empty($baseUri)) {
74 throw new InvalidArgumentException(
75 "Relative URI '{$uri}' cannot be used without a base URI. ".
76 'Set a base URI using the baseUri() method.'
77 );
78 }
79
80 // Ensure base URI is valid if provided
81 if (! empty($baseUri) && ! $this->isAbsoluteUrl($baseUri)) {
82 throw new InvalidArgumentException("Invalid base URI: {$baseUri}");
83 }
84 }
85
86 /**
87 * Check if a URI is an absolute URL.
88 *
89 * @param string $uri The URI to check
90 * @return bool Whether the URI is absolute
91 */
92 protected function isAbsoluteUrl(string $uri): bool
93 {
94 return filter_var($uri, \FILTER_VALIDATE_URL) !== false;
95 }
96
97 /**
98 * Join base URI with a path properly.
99 *
100 * @param string $baseUri The base URI
101 * @param string $path The path to append
102 * @return string The combined URI
103 */
104 protected function joinUriPaths(string $baseUri, string $path): string
105 {
106 return rtrim($baseUri, '/').'/'.ltrim($path, '/');
107 }
108
109 /**
110 * Append query parameters to a URI.
111 *
112 * @param string $uri The URI
113 * @param array<string, mixed> $queryParams The query parameters
114 * @return string The URI with query parameters
115 */
116 protected function appendQueryParameters(string $uri, array $queryParams): string
117 {
118 if (empty($queryParams)) {
119 return $uri;
120 }
121
122 // Split URI to preserve any fragment
123 [$baseUri, $fragment] = $this->splitUriFragment($uri);
124
125 // Determine the separator based on URI structure
126 $separator = $this->getQuerySeparator($baseUri);
127
128 // Build the query string
129 $queryString = http_build_query($queryParams);
130
131 // Combine everything
132 return $baseUri.$separator.$queryString.$fragment;
133 }
134
135 /**
136 * Split a URI into its base and fragment parts.
137 *
138 * @param string $uri The URI to split
139 * @return array{0: string, 1: string} [baseUri, fragment]
140 */
141 protected function splitUriFragment(string $uri): array
142 {
143 $fragments = explode('#', $uri, 2);
144 $baseUri = $fragments[0];
145 $fragment = isset($fragments[1]) ? '#'.$fragments[1] : '';
146
147 return [$baseUri, $fragment];
148 }
149
150 /**
151 * Determine the appropriate query string separator for a URI.
152 *
153 * @param string $uri The URI
154 * @return string The separator ('?' or '&' or '')
155 */
156 protected function getQuerySeparator(string $uri): string
157 {
158 // Handle special case: URI already ends with a question mark
159 if (str_ends_with($uri, '?')) {
160 return '';
161 }
162
163 // Check if the URI already has query parameters
164 $parsedUrl = parse_url($uri);
165
166 return isset($parsedUrl['query']) && ! empty($parsedUrl['query']) ? '&' : '?';
167 }
168
169 /**
170 * Normalize a URI by removing redundant slashes.
171 *
172 * @param string $uri The URI to normalize
173 * @return string The normalized URI
174 */
175 protected function normalizeUri(string $uri): string
176 {
177 // Extract scheme if present (e.g., http://)
178 if (preg_match('~^(https?://)~i', $uri, $matches)) {
179 $scheme = $matches[1];
180 $rest = substr($uri, strlen($scheme));
181 // Normalize consecutive slashes in the path
182 $rest = preg_replace('~//+~', '/', $rest);
183
184 return $scheme.$rest;
185 }
186
187 // For non-URLs, just normalize consecutive slashes
188 return preg_replace('~//+~', '/', $uri);
189 }
190}