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