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