friendship ended with social-app. php is my new best friend
1<?php
2
3declare(strict_types=1);
4
5namespace Fetch\Concerns;
6
7use Fetch\Enum\ContentType;
8use Fetch\Interfaces\ClientHandler;
9use GuzzleHttp\Cookie\CookieJarInterface;
10use InvalidArgumentException;
11
12trait ConfiguresRequests
13{
14 /**
15 * Set the base URI for the request.
16 *
17 * @param string $baseUri The base URI for requests
18 * @return $this
19 *
20 * @throws InvalidArgumentException If the base URI is invalid
21 */
22 public function baseUri(string $baseUri): ClientHandler
23 {
24 if (! filter_var($baseUri, \FILTER_VALIDATE_URL)) {
25 throw new InvalidArgumentException("Invalid base URI: {$baseUri}");
26 }
27
28 $this->options['base_uri'] = rtrim($baseUri, '/');
29
30 return $this;
31 }
32
33 /**
34 * Set multiple options for the request.
35 *
36 * @param array<string, mixed> $options Request options
37 * @return $this
38 */
39 public function withOptions(array $options): ClientHandler
40 {
41 $this->options = array_merge($this->options, $options);
42
43 return $this;
44 }
45
46 /**
47 * Set a single option for the request.
48 *
49 * @param string $key Option key
50 * @param mixed $value Option value
51 * @return $this
52 */
53 public function withOption(string $key, mixed $value): ClientHandler
54 {
55 $this->options[$key] = $value;
56
57 return $this;
58 }
59
60 /**
61 * Set the form parameters for the request.
62 *
63 * @param array<string, mixed> $params Form parameters
64 * @return $this
65 */
66 public function withFormParams(array $params): ClientHandler
67 {
68 $this->options['form_params'] = $params;
69
70 // Set the content type header for form params
71 if (! $this->hasHeader('Content-Type')) {
72 $this->withHeader('Content-Type', ContentType::FORM_URLENCODED->value);
73 }
74
75 return $this;
76 }
77
78 /**
79 * Set the multipart data for the request.
80 *
81 * @param array<int, array{name: string, contents: mixed, headers?: array<string, string>}> $multipart Multipart data
82 * @return $this
83 */
84 public function withMultipart(array $multipart): ClientHandler
85 {
86 $this->options['multipart'] = $multipart;
87
88 // Remove any content type headers as they're automatically set by the multipart boundary
89 if ($this->hasHeader('Content-Type')) {
90 unset($this->options['headers']['Content-Type']);
91 }
92
93 return $this;
94 }
95
96 /**
97 * Set the bearer token for the request.
98 *
99 * @param string $token Bearer token
100 * @return $this
101 */
102 public function withToken(string $token): ClientHandler
103 {
104 $this->withHeader('Authorization', 'Bearer '.$token);
105
106 return $this;
107 }
108
109 /**
110 * Set basic authentication for the request.
111 *
112 * @param string $username Username
113 * @param string $password Password
114 * @return $this
115 */
116 public function withAuth(string $username, string $password): ClientHandler
117 {
118 $this->options['auth'] = [$username, $password];
119
120 return $this;
121 }
122
123 /**
124 * Set multiple headers for the request.
125 *
126 * @param array<string, mixed> $headers Headers
127 * @return $this
128 */
129 public function withHeaders(array $headers): ClientHandler
130 {
131 // Initialize headers array if not set
132 if (! isset($this->options['headers'])) {
133 $this->options['headers'] = [];
134 }
135
136 $this->options['headers'] = array_merge(
137 $this->options['headers'],
138 $headers
139 );
140
141 return $this;
142 }
143
144 /**
145 * Set a single header for the request.
146 *
147 * @param string $header Header name
148 * @param mixed $value Header value
149 * @return $this
150 */
151 public function withHeader(string $header, mixed $value): ClientHandler
152 {
153 // Initialize headers array if not set
154 if (! isset($this->options['headers'])) {
155 $this->options['headers'] = [];
156 }
157
158 $this->options['headers'][$header] = $value;
159
160 return $this;
161 }
162
163 /**
164 * Set the request body.
165 *
166 * @param array|string $body Request body
167 * @param string|ContentType $contentType Content type
168 * @return $this
169 */
170 public function withBody(array|string $body, string|ContentType $contentType = ContentType::JSON): ClientHandler
171 {
172 // Convert string content type to enum if necessary
173 $contentTypeEnum = ContentType::normalizeContentType($contentType);
174 $contentTypeValue = $contentTypeEnum instanceof ContentType
175 ? $contentTypeEnum->value
176 : $contentTypeEnum;
177
178 if (is_array($body)) {
179 if ($contentTypeEnum === ContentType::JSON) {
180 // Use Guzzle's json option for proper JSON handling
181 $this->options['json'] = $body;
182
183 // IMPORTANT: Remove any existing conflicting options to prevent conflicts
184 $this->unsetConflictingOptions(['body', 'form_params', 'multipart']);
185
186 // Set JSON content type header if not already set
187 if (! $this->hasHeader('Content-Type')) {
188 $this->withHeader('Content-Type', ContentType::JSON->value);
189 }
190 } elseif ($contentTypeEnum === ContentType::FORM_URLENCODED) {
191 $this->withFormParams($body);
192 // Ensure no conflicting body option
193 $this->unsetConflictingOptions(['body', 'json', 'multipart']);
194 } elseif ($contentTypeEnum === ContentType::MULTIPART) {
195 $this->withMultipart($body);
196 // Ensure no conflicting body option
197 $this->unsetConflictingOptions(['body', 'json', 'form_params']);
198 } else {
199 // For any other content type, serialize the array to JSON in body
200 $this->options['body'] = json_encode($body);
201 // Remove conflicting options to prevent conflicts
202 $this->unsetConflictingOptions(['json', 'form_params', 'multipart']);
203 if (! $this->hasHeader('Content-Type')) {
204 $this->withHeader('Content-Type', $contentTypeValue);
205 }
206 }
207 } else {
208 // For string bodies, use body option
209 $this->options['body'] = $body;
210 // Remove body-related options to prevent conflicts
211 $this->unsetConflictingOptions(['json', 'form_params', 'multipart']);
212 if (! $this->hasHeader('Content-Type')) {
213 $this->withHeader('Content-Type', $contentTypeValue);
214 }
215 }
216
217 return $this;
218 }
219
220 /**
221 * Set the JSON body for the request.
222 *
223 * @param array<string, mixed> $data JSON data
224 * @param int $options JSON encoding options
225 * @return $this
226 */
227 public function withJson(array $data, int $options = 0): ClientHandler
228 {
229 // Use Guzzle's built-in json option for proper handling
230 $this->options['json'] = $data;
231
232 // Set JSON content type if not already set
233 if (! $this->hasHeader('Content-Type')) {
234 $this->withHeader('Content-Type', ContentType::JSON->value);
235 }
236
237 return $this;
238 }
239
240 /**
241 * Set multiple query parameters for the request.
242 *
243 * @param array<string, mixed> $queryParams Query parameters
244 * @return $this
245 */
246 public function withQueryParameters(array $queryParams): ClientHandler
247 {
248 // Initialize query array if not set
249 if (! isset($this->options['query'])) {
250 $this->options['query'] = [];
251 }
252
253 $this->options['query'] = array_merge(
254 $this->options['query'],
255 $queryParams
256 );
257
258 return $this;
259 }
260
261 /**
262 * Set a single query parameter for the request.
263 *
264 * @param string $name Parameter name
265 * @param mixed $value Parameter value
266 * @return $this
267 */
268 public function withQueryParameter(string $name, mixed $value): ClientHandler
269 {
270 // Initialize query array if not set
271 if (! isset($this->options['query'])) {
272 $this->options['query'] = [];
273 }
274
275 $this->options['query'][$name] = $value;
276
277 return $this;
278 }
279
280 /**
281 * Set the timeout for the request.
282 *
283 * @param int $seconds Timeout in seconds
284 * @return $this
285 */
286 public function timeout(int $seconds): ClientHandler
287 {
288 $this->timeout = $seconds;
289 $this->options['timeout'] = $seconds;
290
291 return $this;
292 }
293
294 /**
295 * Set the proxy for the request.
296 *
297 * @param string|array $proxy Proxy configuration
298 * @return $this
299 */
300 public function withProxy(string|array $proxy): ClientHandler
301 {
302 $this->options['proxy'] = $proxy;
303
304 return $this;
305 }
306
307 /**
308 * Set the cookies for the request.
309 *
310 * @param bool|CookieJarInterface $cookies Cookie jar or boolean
311 * @return $this
312 */
313 public function withCookies(bool|CookieJarInterface $cookies): ClientHandler
314 {
315 $this->options['cookies'] = $cookies;
316
317 return $this;
318 }
319
320 /**
321 * Set whether to follow redirects.
322 *
323 * @param bool|array $redirects Redirect configuration
324 * @return $this
325 */
326 public function withRedirects(bool|array $redirects = true): ClientHandler
327 {
328 $this->options['allow_redirects'] = $redirects;
329
330 return $this;
331 }
332
333 /**
334 * Set the certificate for the request.
335 *
336 * @param string|array $cert Certificate path or array
337 * @return $this
338 */
339 public function withCert(string|array $cert): ClientHandler
340 {
341 $this->options['cert'] = $cert;
342
343 return $this;
344 }
345
346 /**
347 * Set the SSL key for the request.
348 *
349 * @param string|array $sslKey SSL key configuration
350 * @return $this
351 */
352 public function withSslKey(string|array $sslKey): ClientHandler
353 {
354 $this->options['ssl_key'] = $sslKey;
355
356 return $this;
357 }
358
359 /**
360 * Set the stream option for the request.
361 *
362 * @param bool $stream Whether to stream the response
363 * @return $this
364 */
365 public function withStream(bool $stream): ClientHandler
366 {
367 $this->options['stream'] = $stream;
368
369 return $this;
370 }
371
372 /**
373 * Reset the handler state.
374 *
375 * @return $this
376 */
377 public function reset(): ClientHandler
378 {
379 $this->options = [];
380 $this->timeout = null;
381 $this->maxRetries = null;
382 $this->retryDelay = null;
383 $this->isAsync = false;
384
385 return $this;
386 }
387
388 /**
389 * Configure the request body for POST/PUT/PATCH/DELETE requests.
390 *
391 * @param mixed $body The request body
392 * @param string|ContentType $contentType The content type of the request
393 */
394 protected function configureRequestBody(mixed $body = null, string|ContentType $contentType = ContentType::JSON): void
395 {
396 if (is_null($body)) {
397 return;
398 }
399
400 // Normalize content type
401 $contentTypeEnum = ContentType::normalizeContentType($contentType);
402
403 if (is_array($body)) {
404 match ($contentTypeEnum) {
405 ContentType::JSON => $this->withJson($body),
406 ContentType::FORM_URLENCODED => $this->withFormParams($body),
407 ContentType::MULTIPART => $this->withMultipart($body),
408 default => $this->withBody($body, $contentType)
409 };
410
411 return;
412 }
413
414 $this->withBody($body, $contentType);
415 }
416
417 /**
418 * Remove conflicting options from the request options array.
419 *
420 * @param array<string> $keys The keys to unset from options
421 */
422 protected function unsetConflictingOptions(array $keys): void
423 {
424 foreach ($keys as $key) {
425 unset($this->options[$key]);
426 }
427 }
428}