friendship ended with social-app. php is my new best friend
at main 13 kB view raw
1<?php 2 3declare(strict_types=1); 4 5use Fetch\Enum\ContentType; 6use Fetch\Enum\Method; 7use Fetch\Http\Client; 8use Fetch\Interfaces\ClientHandler as ClientHandlerInterface; 9use Fetch\Interfaces\Response as ResponseInterface; 10use Psr\Http\Client\ClientExceptionInterface; 11use Psr\Http\Message\RequestInterface; 12 13if (! function_exists('fetch')) { 14 /** 15 * Perform an HTTP request similar to JavaScript's fetch API. 16 * 17 * @param string|RequestInterface|null $resource URL to fetch or a pre-configured Request object 18 * @param array<string, mixed>|null $options Request options including: 19 * - method: HTTP method (string|Method enum) 20 * - headers: Request headers (array) 21 * - body: Request body (mixed) 22 * - json: JSON data to send as body (array, takes precedence over body) 23 * - form: Form data to send as body (array, takes precedence if no json) 24 * - multipart: Multipart form data (array, takes precedence if no json/form) 25 * - query: Query parameters (array) 26 * - base_uri: Base URI (string) 27 * - timeout: Request timeout in seconds (int) 28 * - retries: Number of retries (int) 29 * - auth: Basic auth credentials [username, password] (array) 30 * - token: Bearer token (string) 31 * @return ResponseInterface|ClientHandlerInterface|Client Response or handler for method chaining 32 * 33 * @throws ClientExceptionInterface If a client exception occurs 34 */ 35 function fetch(string|RequestInterface|null $resource = null, ?array $options = []): ResponseInterface|ClientHandlerInterface|Client 36 { 37 $options = $options ?? []; 38 39 // If a Request object is provided, we can't use options with it 40 if ($resource instanceof RequestInterface) { 41 return fetch_client()->sendRequest($resource); 42 } 43 44 // If no resource is provided, return the client handler for chaining 45 if ($resource === null) { 46 return fetch_client(); 47 } 48 49 // Process fetch-style options 50 $processedOptions = process_request_options($options); 51 52 // Handle base URI if provided 53 if (isset($options['base_uri'])) { 54 return handle_request_with_base_uri($resource, $options, $processedOptions); 55 } 56 57 // No base URI, use direct fetch with options 58 return fetch_client()->fetch($resource, $processedOptions); 59 } 60} 61 62if (! function_exists('process_request_options')) { 63 /** 64 * Process and normalize request options. 65 * 66 * @param array<string, mixed> $options Raw options 67 * @return array<string, mixed> Processed options 68 */ 69 function process_request_options(array $options): array 70 { 71 $processedOptions = []; 72 73 // Method (default to GET) 74 $method = $options['method'] ?? Method::GET; 75 $methodValue = $method instanceof Method ? $method->value : (string) $method; 76 $processedOptions['method'] = $methodValue; 77 78 // Headers 79 if (isset($options['headers'])) { 80 $processedOptions['headers'] = $options['headers']; 81 } 82 83 // Content type and body handling 84 [$body, $contentType] = extract_body_and_content_type($options); 85 86 if ($body !== null) { 87 $processedOptions['body'] = $body; 88 if ($contentType !== null) { 89 $processedOptions['content_type'] = $contentType; 90 } 91 } 92 93 // Query parameters 94 if (isset($options['query'])) { 95 $processedOptions['query'] = $options['query']; 96 } 97 98 // Copy other direct pass options 99 $directPassOptions = [ 100 'timeout', 'retries', 'auth', 'token', 101 'proxy', 'cookies', 'allow_redirects', 'cert', 'ssl_key', 'stream', 102 ]; 103 104 foreach ($directPassOptions as $opt) { 105 if (isset($options[$opt])) { 106 $processedOptions[$opt] = $options[$opt]; 107 } 108 } 109 110 return $processedOptions; 111 } 112} 113 114if (! function_exists('extract_body_and_content_type')) { 115 /** 116 * Extract body and content type from options. 117 * 118 * @param array<string, mixed> $options Request options 119 * @return array{0: mixed, 1: ?ContentType|string} Tuple of [body, contentType] 120 */ 121 function extract_body_and_content_type(array $options): array 122 { 123 $body = null; 124 $contentType = null; 125 126 // Body handling - json takes precedence, then form, then multipart, then raw body 127 if (isset($options['json'])) { 128 $body = $options['json']; 129 $contentType = ContentType::JSON; 130 } elseif (isset($options['form'])) { 131 $body = $options['form']; 132 $contentType = ContentType::FORM_URLENCODED; 133 } elseif (isset($options['multipart'])) { 134 $body = $options['multipart']; 135 $contentType = ContentType::MULTIPART; 136 } elseif (isset($options['body'])) { 137 $body = $options['body']; 138 // IMPORTANT: Don't auto-convert arrays to JSON here 139 // The content type should be explicitly set 140 $rawContentType = $options['content_type'] ?? null; 141 $contentType = $rawContentType !== null ? ContentType::normalizeContentType($rawContentType) : null; 142 } 143 144 return [$body, $contentType]; 145 } 146} 147 148if (! function_exists('handle_request_with_base_uri')) { 149 /** 150 * Handle a request with a base URI. 151 * 152 * @param string $resource URL to fetch 153 * @param array<string, mixed> $options Original options 154 * @param array<string, mixed> $processedOptions Processed options 155 * @return ResponseInterface The response 156 */ 157 function handle_request_with_base_uri(string $resource, array $options, array $processedOptions): ResponseInterface 158 { 159 $client = fetch_client(); 160 $handler = $client->getHandler(); 161 $handler->baseUri($options['base_uri']); 162 $handler->withOptions($processedOptions); 163 164 // Extract body and content type if not already processed 165 [$body, $contentType] = extract_body_and_content_type($options); 166 167 if ($body !== null) { 168 if ($contentType !== null) { 169 $handler->withBody($body, $contentType); 170 } else { 171 $handler->withBody($body); 172 } 173 } 174 175 return $handler->sendRequest($processedOptions['method'], $resource); 176 } 177} 178 179if (! function_exists('fetch_client')) { 180 /** 181 * Get or configure the global fetch client instance. 182 * 183 * @param array<string, mixed>|null $options Global client options 184 * @param bool $reset Whether to reset the client instance 185 * @return Client The client instance 186 * 187 * @throws RuntimeException If client creation or configuration fails 188 */ 189 function fetch_client(?array $options = null, bool $reset = false): Client 190 { 191 static $client = null; 192 193 try { 194 // Create a new client or reset the existing one 195 if ($client === null || $reset) { 196 $client = new Client(options: $options ?? []); 197 } 198 // Apply new options to the existing client if provided 199 elseif ($options !== null) { 200 // Get the existing handler 201 $handler = $client->getHandler(); 202 203 // Only clone and apply options if there are options to apply 204 if (! empty($options)) { 205 try { 206 $handler = $handler->withClonedOptions($options); 207 } catch (Throwable $e) { 208 // More specific error message for options application failures 209 throw new RuntimeException( 210 sprintf('Failed to apply options to client: %s', $e->getMessage()), 211 0, 212 $e 213 ); 214 } 215 } 216 217 // Create a new client with the modified handler 218 $client = new Client(handler: $handler); 219 } 220 221 return $client; 222 } catch (Throwable $e) { 223 // If it's already a RuntimeException from our code, re-throw it 224 if ($e instanceof RuntimeException && $e->getPrevious() !== null) { 225 throw $e; 226 } 227 228 // Otherwise, wrap the exception with more context 229 throw new RuntimeException( 230 sprintf('Error configuring fetch client: %s', $e->getMessage()), 231 0, 232 $e 233 ); 234 } 235 } 236} 237 238if (! function_exists('request_method')) { 239 /** 240 * Common helper for making HTTP requests with various methods. 241 * 242 * @param string $method HTTP method (GET, POST, etc.) 243 * @param string $url URL to fetch 244 * @param mixed $data Request data (for body or query parameters) 245 * @param array<string, mixed>|null $options Additional request options 246 * @param bool $dataIsQuery Whether data is used as query parameters (true) or request body (false) 247 * @return ResponseInterface The response 248 * 249 * @throws ClientExceptionInterface If a client exception occurs 250 */ 251 function request_method(string $method, string $url, mixed $data = null, ?array $options = [], bool $dataIsQuery = false): ResponseInterface 252 { 253 $options = $options ?? []; 254 $options['method'] = $method; 255 256 if ($data !== null) { 257 if ($dataIsQuery) { 258 $options['query'] = $data; 259 } elseif (is_array($data)) { 260 $options['json'] = $data; // Treat arrays as JSON by default 261 } else { 262 $options['body'] = $data; 263 } 264 } 265 266 return fetch($url, $options); 267 } 268} 269 270// We'll keep these convenience functions for PHP developers who prefer a more traditional API 271if (! function_exists('get')) { 272 /** 273 * Perform a GET request. 274 * 275 * @param string $url URL to fetch 276 * @param array<string, mixed>|null $query Query parameters 277 * @param array<string, mixed>|null $options Additional request options 278 * @return ResponseInterface The response 279 * 280 * @throws ClientExceptionInterface If a client exception occurs 281 */ 282 function get(string $url, ?array $query = null, ?array $options = []): ResponseInterface 283 { 284 return request_method('GET', $url, $query, $options, true); 285 } 286} 287 288if (! function_exists('post')) { 289 /** 290 * Perform a POST request. 291 * 292 * @param string $url URL to fetch 293 * @param mixed $data Request body or JSON data 294 * @param array<string, mixed>|null $options Additional request options 295 * @return ResponseInterface The response 296 * 297 * @throws ClientExceptionInterface If a client exception occurs 298 */ 299 function post(string $url, mixed $data = null, ?array $options = []): ResponseInterface 300 { 301 return request_method('POST', $url, $data, $options); 302 } 303} 304 305if (! function_exists('put')) { 306 /** 307 * Perform a PUT request. 308 * 309 * @param string $url URL to fetch 310 * @param mixed $data Request body or JSON data 311 * @param array<string, mixed>|null $options Additional request options 312 * @return ResponseInterface The response 313 * 314 * @throws ClientExceptionInterface If a client exception occurs 315 */ 316 function put(string $url, mixed $data = null, ?array $options = []): ResponseInterface 317 { 318 return request_method('PUT', $url, $data, $options); 319 } 320} 321 322if (! function_exists('patch')) { 323 /** 324 * Perform a PATCH request. 325 * 326 * @param string $url URL to fetch 327 * @param mixed $data Request body or JSON data 328 * @param array<string, mixed>|null $options Additional request options 329 * @return ResponseInterface The response 330 * 331 * @throws ClientExceptionInterface If a client exception occurs 332 */ 333 function patch(string $url, mixed $data = null, ?array $options = []): ResponseInterface 334 { 335 return request_method('PATCH', $url, $data, $options); 336 } 337} 338 339if (! function_exists('delete')) { 340 /** 341 * Perform a DELETE request. 342 * 343 * @param string $url URL to fetch 344 * @param mixed $data Request body or JSON data 345 * @param array<string, mixed>|null $options Additional request options 346 * @return ResponseInterface The response 347 * 348 * @throws ClientExceptionInterface If a client exception occurs 349 */ 350 function delete(string $url, mixed $data = null, ?array $options = []): ResponseInterface 351 { 352 return request_method('DELETE', $url, $data, $options); 353 } 354}