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: