1: <?php declare(strict_types=1);
2:
3: namespace Salient\Contract\Curler;
4:
5: use Psr\Http\Client\ClientInterface;
6: use Psr\Http\Message\RequestInterface;
7: use Psr\Http\Message\ResponseInterface;
8: use Psr\Http\Message\UriInterface as PsrUriInterface;
9: use Salient\Contract\Cache\CacheInterface;
10: use Salient\Contract\Core\DateFormatterInterface;
11: use Salient\Contract\Core\Immutable;
12: use Salient\Contract\Http\AccessTokenInterface;
13: use Salient\Contract\Http\FormDataFlag;
14: use Salient\Contract\Http\HttpHeader;
15: use Salient\Contract\Http\HttpHeaderGroup;
16: use Salient\Contract\Http\HttpHeadersInterface;
17: use Salient\Contract\Http\HttpRequestHandlerInterface;
18: use Salient\Contract\Http\HttpResponseInterface;
19: use Salient\Contract\Http\UriInterface;
20: use Closure;
21: use OutOfRangeException;
22: use Stringable;
23:
24: /**
25: * @api
26: */
27: interface CurlerInterface extends ClientInterface, Immutable
28: {
29: /**
30: * Get the URI of the endpoint
31: */
32: public function getUri(): UriInterface;
33:
34: /**
35: * Get an instance with the given endpoint URI
36: *
37: * An exception is thrown if the endpoint URI has a query or fragment.
38: *
39: * @param PsrUriInterface|Stringable|string|null $uri
40: * @return static
41: */
42: public function withUri($uri);
43:
44: /**
45: * Apply the URI and headers of the given request to a copy of the instance
46: *
47: * @return static
48: */
49: public function withRequest(RequestInterface $request);
50:
51: /**
52: * Get the last request sent to the endpoint or passed to middleware
53: */
54: public function getLastRequest(): ?RequestInterface;
55:
56: /**
57: * Get the last response received from the endpoint or returned by
58: * middleware
59: */
60: public function getLastResponse(): ?HttpResponseInterface;
61:
62: /**
63: * Check if the last response contains JSON-encoded data
64: *
65: * @throws OutOfRangeException if no response has been received from the
66: * endpoint or returned by middleware.
67: */
68: public function lastResponseIsJson(): bool;
69:
70: // --
71:
72: /**
73: * Send a HEAD request to the endpoint
74: *
75: * @param mixed[]|null $query
76: */
77: public function head(?array $query = null): HttpHeadersInterface;
78:
79: /**
80: * Send a GET request to the endpoint and return the body of the response
81: *
82: * @param mixed[]|null $query
83: * @return mixed
84: */
85: public function get(?array $query = null);
86:
87: /**
88: * Send a POST request to the endpoint and return the body of the response
89: *
90: * @param mixed[]|object|null $data
91: * @param mixed[]|null $query
92: * @return mixed
93: */
94: public function post($data = null, ?array $query = null);
95:
96: /**
97: * Send a PUT request to the endpoint and return the body of the response
98: *
99: * @param mixed[]|object|null $data
100: * @param mixed[]|null $query
101: * @return mixed
102: */
103: public function put($data = null, ?array $query = null);
104:
105: /**
106: * Send a PATCH request to the endpoint and return the body of the response
107: *
108: * @param mixed[]|object|null $data
109: * @param mixed[]|null $query
110: * @return mixed
111: */
112: public function patch($data = null, ?array $query = null);
113:
114: /**
115: * Send a DELETE request to the endpoint and return the body of the response
116: *
117: * @param mixed[]|object|null $data
118: * @param mixed[]|null $query
119: * @return mixed
120: */
121: public function delete($data = null, ?array $query = null);
122:
123: // --
124:
125: /**
126: * Send a GET request to the endpoint and iterate over response pages
127: *
128: * @param mixed[]|null $query
129: * @return iterable<mixed>
130: */
131: public function getP(?array $query = null): iterable;
132:
133: /**
134: * Send a POST request to the endpoint and iterate over response pages
135: *
136: * @param mixed[]|object|null $data
137: * @param mixed[]|null $query
138: * @return iterable<mixed>
139: */
140: public function postP($data = null, ?array $query = null): iterable;
141:
142: /**
143: * Send a PUT request to the endpoint and iterate over response pages
144: *
145: * @param mixed[]|object|null $data
146: * @param mixed[]|null $query
147: * @return iterable<mixed>
148: */
149: public function putP($data = null, ?array $query = null): iterable;
150:
151: /**
152: * Send a PATCH request to the endpoint and iterate over response pages
153: *
154: * @param mixed[]|object|null $data
155: * @param mixed[]|null $query
156: * @return iterable<mixed>
157: */
158: public function patchP($data = null, ?array $query = null): iterable;
159:
160: /**
161: * Send a DELETE request to the endpoint and iterate over response pages
162: *
163: * @param mixed[]|object|null $data
164: * @param mixed[]|null $query
165: * @return iterable<mixed>
166: */
167: public function deleteP($data = null, ?array $query = null): iterable;
168:
169: // --
170:
171: /**
172: * Send raw data to the endpoint in a POST request and return the body of
173: * the response
174: *
175: * @param mixed[]|null $query
176: * @return mixed
177: */
178: public function postR(string $data, string $mediaType, ?array $query = null);
179:
180: /**
181: * Send raw data to the endpoint in a PUT request and return the body of the
182: * response
183: *
184: * @param mixed[]|null $query
185: * @return mixed
186: */
187: public function putR(string $data, string $mediaType, ?array $query = null);
188:
189: /**
190: * Send raw data to the endpoint in a PATCH request and return the body of
191: * the response
192: *
193: * @param mixed[]|null $query
194: * @return mixed
195: */
196: public function patchR(string $data, string $mediaType, ?array $query = null);
197:
198: /**
199: * Send raw data to the endpoint in a DELETE request and return the body of
200: * the response
201: *
202: * @param mixed[]|null $query
203: * @return mixed
204: */
205: public function deleteR(string $data, string $mediaType, ?array $query = null);
206:
207: // --
208:
209: /**
210: * Invalidate cached cookies
211: *
212: * Calling this method has no effect if the instance does not handle
213: * cookies.
214: *
215: * @return $this
216: */
217: public function flushCookies();
218:
219: // --
220:
221: /**
222: * Get request headers
223: */
224: public function getHttpHeaders(): HttpHeadersInterface;
225:
226: /**
227: * Get request headers that are not considered sensitive
228: */
229: public function getPublicHttpHeaders(): HttpHeadersInterface;
230:
231: /**
232: * Get an array that maps request header names to values
233: *
234: * @return array<string,string[]>
235: */
236: public function getHeaders(): array;
237:
238: /**
239: * Check if a request header exists
240: */
241: public function hasHeader(string $name): bool;
242:
243: /**
244: * Get the value of a request header as a list of values
245: *
246: * @return string[]
247: */
248: public function getHeader(string $name): array;
249:
250: /**
251: * Get the value of a request header as a list of values, splitting any
252: * comma-separated values
253: *
254: * @return string[]
255: */
256: public function getHeaderValues(string $name): array;
257:
258: /**
259: * Get the comma-separated values of a request header
260: */
261: public function getHeaderLine(string $name): string;
262:
263: /**
264: * Get the first value of a request header after splitting any
265: * comma-separated values
266: */
267: public function getFirstHeaderLine(string $name): string;
268:
269: /**
270: * Get the last value of a request header after splitting any
271: * comma-separated values
272: */
273: public function getLastHeaderLine(string $name): string;
274:
275: /**
276: * Get the only value of a request header after splitting any
277: * comma-separated values
278: *
279: * An exception is thrown if the request header has more than one value.
280: */
281: public function getOneHeaderLine(string $name, bool $orSame = false): string;
282:
283: /**
284: * Get an instance with a value applied to a request header, replacing any
285: * existing values
286: *
287: * @param string[]|string $value
288: * @return static
289: */
290: public function withHeader(string $name, $value);
291:
292: /**
293: * Get an instance with a value applied to a request header, preserving any
294: * existing values
295: *
296: * @param string[]|string $value
297: * @return static
298: */
299: public function withAddedHeader(string $name, $value);
300:
301: /**
302: * Get an instance with a request header removed
303: *
304: * @return static
305: */
306: public function withoutHeader(string $name);
307:
308: /**
309: * Check if the instance has an access token
310: */
311: public function hasAccessToken(): bool;
312:
313: /**
314: * Get an instance that applies an access token to request headers
315: *
316: * @return static
317: */
318: public function withAccessToken(
319: ?AccessTokenInterface $token,
320: string $headerName = HttpHeader::AUTHORIZATION
321: );
322:
323: /**
324: * Check if a header is considered sensitive
325: */
326: public function isSensitiveHeader(string $name): bool;
327:
328: /**
329: * Get an instance that treats a header as sensitive
330: *
331: * Headers in {@see HttpHeaderGroup::SENSITIVE} are considered sensitive by
332: * default.
333: *
334: * @return static
335: */
336: public function withSensitiveHeader(string $name);
337:
338: /**
339: * Get an instance that does not treat the given header as sensitive
340: *
341: * @return static
342: */
343: public function withoutSensitiveHeader(string $name);
344:
345: /**
346: * Get the media type applied to request headers
347: */
348: public function getMediaType(): ?string;
349:
350: /**
351: * Get an instance that applies the given media type to request headers
352: *
353: * If `$type` is `null` (the default), `Content-Type` headers are
354: * automatically applied to requests as needed.
355: *
356: * @return static
357: */
358: public function withMediaType(?string $type);
359:
360: /**
361: * Get the current user agent string
362: */
363: public function getUserAgent(): string;
364:
365: /**
366: * Get an instance with the given user agent string
367: *
368: * If `$userAgent` is `null`, the default user agent string is restored.
369: *
370: * @return static
371: */
372: public function withUserAgent(?string $userAgent);
373:
374: /**
375: * Check if the instance explicitly accepts JSON-encoded responses and
376: * assumes responses with no content type contain JSON
377: */
378: public function expectsJson(): bool;
379:
380: /**
381: * Get an instance that explicitly accepts JSON-encoded responses and
382: * assumes responses with no content type contain JSON
383: *
384: * @return static
385: */
386: public function withExpectJson(bool $expectJson = true);
387:
388: /**
389: * Check if the instance uses JSON to encode POST/PUT/PATCH/DELETE data
390: */
391: public function postsJson(): bool;
392:
393: /**
394: * Get an instance that uses JSON to encode POST/PUT/PATCH/DELETE data
395: *
396: * @return static
397: */
398: public function withPostJson(bool $postJson = true);
399:
400: /**
401: * Get the date formatter applied to the instance
402: */
403: public function getDateFormatter(): ?DateFormatterInterface;
404:
405: /**
406: * Get an instance that uses the given date formatter to format and parse
407: * the endpoint's date and time values
408: *
409: * @return static
410: */
411: public function withDateFormatter(?DateFormatterInterface $formatter);
412:
413: /**
414: * Get form data flags applied to the instance
415: *
416: * @return int-mask-of<FormDataFlag::*>
417: */
418: public function getFormDataFlags(): int;
419:
420: /**
421: * Get an instance with the given form data flags
422: *
423: * Form data flags are used to encode data for query strings and message
424: * bodies.
425: *
426: * {@see FormDataFlag::PRESERVE_NUMERIC_KEYS} and
427: * {@see FormDataFlag::PRESERVE_STRING_KEYS} are applied by default.
428: *
429: * @param int-mask-of<FormDataFlag::*> $flags
430: * @return static
431: */
432: public function withFormDataFlags(int $flags);
433:
434: /**
435: * Get an instance with the given json_decode() flags
436: *
437: * {@see \JSON_OBJECT_AS_ARRAY} is applied by default.
438: * {@see \JSON_THROW_ON_ERROR} is always applied and cannot be disabled.
439: *
440: * @param int-mask-of<\JSON_BIGINT_AS_STRING|\JSON_INVALID_UTF8_IGNORE|\JSON_INVALID_UTF8_SUBSTITUTE|\JSON_OBJECT_AS_ARRAY|\JSON_THROW_ON_ERROR> $flags
441: * @return static
442: */
443: public function withJsonDecodeFlags(int $flags);
444:
445: /**
446: * Get an instance with the given middleware applied to the request handler
447: * stack
448: *
449: * @param CurlerMiddlewareInterface|HttpRequestHandlerInterface|Closure(RequestInterface $request, Closure(RequestInterface): HttpResponseInterface $next, CurlerInterface $curler): ResponseInterface $middleware
450: * @return static
451: */
452: public function withMiddleware($middleware, ?string $name = null);
453:
454: /**
455: * Get an instance where the given middleware is not applied to requests
456: *
457: * @param CurlerMiddlewareInterface|HttpRequestHandlerInterface|Closure|string $middleware
458: * @return static
459: */
460: public function withoutMiddleware($middleware);
461:
462: /**
463: * Get the endpoint's pagination handler
464: */
465: public function getPager(): ?CurlerPagerInterface;
466:
467: /**
468: * Check if the endpoint's pagination handler is used to process every
469: * request
470: */
471: public function alwaysPaginates(): bool;
472:
473: /**
474: * Get an instance with the given pagination handler
475: *
476: * @param bool $alwaysPaginate If `true`, the pager is used to process
477: * requests even if no pagination is required.
478: * @return static
479: */
480: public function withPager(?CurlerPagerInterface $pager, bool $alwaysPaginate = false);
481:
482: /**
483: * Get the endpoint's cache store
484: */
485: public function getCacheStore(): ?CacheInterface;
486:
487: /**
488: * Get an instance with the given cache store
489: *
490: * If no `$store` is given, cookies and responses are cached in the default
491: * cache store as needed.
492: *
493: * @return static
494: */
495: public function withCacheStore(?CacheInterface $store = null);
496:
497: /**
498: * Check if the instance handles cookies
499: */
500: public function hasCookies(): bool;
501:
502: /**
503: * Get an instance that handles cookies
504: *
505: * @return static
506: */
507: public function withCookies(?string $cacheKey = null);
508:
509: /**
510: * Get an instance that does not handle cookies
511: *
512: * @return static
513: */
514: public function withoutCookies();
515:
516: /**
517: * Check if response caching is enabled
518: */
519: public function hasResponseCache(): bool;
520:
521: /**
522: * Get an instance that caches responses to GET and HEAD requests
523: *
524: * HTTP caching headers are ignored. USE RESPONSIBLY.
525: *
526: * @return static
527: */
528: public function withResponseCache(bool $cacheResponses = true);
529:
530: /**
531: * Check if POST response caching is enabled
532: */
533: public function hasPostResponseCache(): bool;
534:
535: /**
536: * Get an instance that caches responses to repeatable POST requests
537: *
538: * {@see withResponseCache()} must also be called to enable caching.
539: *
540: * @return static
541: */
542: public function withPostResponseCache(bool $cachePostResponses = true);
543:
544: /**
545: * Get an instance that uses a callback to generate response cache keys
546: *
547: * The callback's return value is hashed and combined with request method
548: * and URI to create a response cache key.
549: *
550: * @param (callable(RequestInterface $request, CurlerInterface $curler): (string[]|string))|null $callback
551: * @return static
552: */
553: public function withCacheKeyCallback(?callable $callback);
554:
555: /**
556: * Get the lifetime of cached responses, in seconds
557: *
558: * @return int<-1,max>
559: */
560: public function getCacheLifetime(): int;
561:
562: /**
563: * Get an instance where cached responses expire after the given number of
564: * seconds
565: *
566: * `3600` is applied by default.
567: *
568: * {@see withResponseCache()} must also be called to enable caching.
569: *
570: * @param int<-1,max> $seconds - `0`: cache responses indefinitely
571: * - `-1`: disable caching until the method is called again with `$seconds`
572: * greater than or equal to `0`
573: * @return static
574: */
575: public function withCacheLifetime(int $seconds);
576:
577: /**
578: * Check if the instance replaces cached responses even if they haven't
579: * expired
580: */
581: public function refreshesCache(): bool;
582:
583: /**
584: * Get an instance that replaces cached responses even if they haven't
585: * expired
586: *
587: * @return static
588: */
589: public function withRefreshCache(bool $refresh = true);
590:
591: /**
592: * Get the connection timeout applied to the instance, in seconds
593: *
594: * @return int<0,max>|null
595: */
596: public function getTimeout(): ?int;
597:
598: /**
599: * Get an instance with the given connection timeout
600: *
601: * @param int<0,max>|null $seconds - `0`: wait indefinitely
602: * - `null` (default): use the underlying client's default connection
603: * timeout
604: * @return static
605: */
606: public function withTimeout(?int $seconds);
607:
608: /**
609: * Check if the instance follows "Location" headers
610: */
611: public function followsRedirects(): bool;
612:
613: /**
614: * Get an instance that follows "Location" headers
615: *
616: * @return static
617: */
618: public function withFollowRedirects(bool $follow = true);
619:
620: /**
621: * Get the maximum number of "Location" headers followed
622: *
623: * @return int<-1,max>|null
624: */
625: public function getMaxRedirects(): ?int;
626:
627: /**
628: * Get an instance that limits the number of "Location" headers followed
629: *
630: * @param int<-1,max>|null $redirects - `-1`: allow unlimited redirects
631: * - `0`: disable redirects (same effect as `withFollowRedirects(false)`)
632: * - `null` (default): use the underlying client's default redirect limit
633: * @return static
634: */
635: public function withMaxRedirects(?int $redirects);
636:
637: /**
638: * Check if the instance retries throttled requests when the endpoint
639: * returns a "Retry-After" header
640: */
641: public function getRetryAfterTooManyRequests(): bool;
642:
643: /**
644: * Get the maximum delay between request attempts
645: *
646: * @return int<0,max>
647: */
648: public function getRetryAfterMaxSeconds(): int;
649:
650: /**
651: * Get an instance that retries throttled requests when the endpoint returns
652: * a "Retry-After" header
653: *
654: * @return static
655: */
656: public function withRetryAfterTooManyRequests(bool $retry = true);
657:
658: /**
659: * Get an instance that limits the delay between request attempts
660: *
661: * `300` is applied by default.
662: *
663: * @param int<0,max> $seconds If `0`, unlimited delays are allowed.
664: * @return static
665: */
666: public function withRetryAfterMaxSeconds(int $seconds);
667:
668: /**
669: * Check if exceptions are thrown for HTTP errors
670: */
671: public function throwsHttpErrors(): bool;
672:
673: /**
674: * Get an instance that throws exceptions for HTTP errors
675: *
676: * @return static
677: */
678: public function withThrowHttpErrors(bool $throw = true);
679:
680: /**
681: * Use the form data flags and date formatter applied to the instance to
682: * replace the query string of a request or URI
683: *
684: * @template T of RequestInterface|PsrUriInterface|Stringable|string
685: *
686: * @param T $value
687: * @param mixed[] $query
688: * @return (T is RequestInterface|PsrUriInterface ? T : UriInterface)
689: */
690: public function replaceQuery($value, array $query);
691: }
692: