5 use AsyncInterop\Promise
;
9 Client
, Client\Request
, Message\Body
, QueryString
, Url
11 use IteratorAggregate
;
13 LoggerInterface
, NullLogger
16 Call
, Consumer
, ContentType
, Future
, Iterator
, Links
18 use seekat\Exception\InvalidArgumentException
;
20 class API
implements IteratorAggregate
, Countable
{
22 * The current API endpoint URL
29 * @var LoggerInterface
35 * @var Call\Cache\Service
52 * Default headers to send out to the API endpoint
58 * Current endpoint data's Content-Type
59 * @var API\ContentType
64 * Current endpoint's data
70 * Current endpoints links
76 * Create a new API endpoint root
78 * @param Future $future pretending to fulfill promises
79 * @param array $headers Standard request headers, defaults to ["Accept" => "application/vnd.github.v3+json"]
80 * @param Url $url The API's endpoint, defaults to https://api.github.com
81 * @param Client $client The HTTP client to use for executing requests
82 * @param LoggerInterface $log A logger
83 * @param Call\Cache\Service $cache A cache
85 function __construct(Future
$future, array $headers = null, Url
$url = null, Client
$client = null, LoggerInterface
$log = null, Call\Cache\Service
$cache = null) {
86 $this->future
= $future;
87 $this->cache
= $cache;
88 $this->logger
= $log ??
new NullLogger
;
89 $this->url
= $url ??
new Url("https://api.github.com");
90 $this->client
= $client ??
new Client
;
91 $this->headers
= (array) $headers +
[
92 "Accept" => "application/vnd.github.v3+json"
97 * Ascend one level deep into the API endpoint
99 * @param string|int $seg The "path" element to ascend into
100 * @return API Endpoint clone referring to {$parent}/{$seg}
102 function __get($seg) : API
{
103 if (substr($seg, -4) === "_url") {
104 $url = new Url(uri_template($this->data
->$seg));
105 $that = $this->withUrl($url);
106 $seg = basename($that->url
->path
);
109 $that->url
->path
.= "/".urlencode($seg);
110 $this->exists($seg, $that->data
);
113 $this->logger
->debug("get($seg)", [
124 * Call handler that actually queues a data fetch and returns a promise
126 * @param string $method The API's "path" element to ascend into
127 * @param array $args Array of arguments forwarded to \seekat\API::get()
130 function __call(string $method, array $args) : Promise
{
131 /* We cannot implement an explicit then() method,
132 * because the Promise implementation might think
133 * we're actually implementing Thenable,
134 * which might cause an infinite loop.
137 if ($method === "when") {
138 $promise = $this->get();
139 $promise->when(...$args);
144 * very short-hand version:
145 * ->users->m6w6->gists->get()->when(...)
147 * ->users->m6w6->gists(...)
149 if (is_callable(current($args))) {
150 $promise = $this->get();
151 $promise->when(current($args));
155 return (new Call($this, $method))($args);
159 * Run the send loop through a generator
161 * @param callable|Generator $cbg A \Generator or a factory of a \Generator yielding promises
162 * @return Promise The promise of the generator's return value
163 * @throws InvalidArgumentException
165 function __invoke($cbg) : Promise
{
166 $this->logger
->debug(__FUNCTION__
);
168 $consumer = new Consumer($this->client
);
171 if ($cbg instanceof Generator
) {
172 return $consumer($cbg);
175 if (is_callable($cbg)) {
180 throw new InvalidArgumentException(
181 "Expected callable or Generator, got ".typeof($cbg, true)
186 * Clone handler ensuring the underlying url will be cloned, too
189 $this->url
= clone $this->url
;
193 * The string handler for the endpoint's data
197 function __toString() : string {
198 if (is_scalar($this->data
)) {
199 return (string) $this->data
;
203 return json_encode($this->data
);
207 * Create an iterator over the endpoint's underlying data
211 function getIterator() : Iterator
{
212 return new Iterator($this);
216 * Count the underlying data's entries
220 function count() : int {
221 return count($this->data
);
227 function getUrl() : Url
{
232 * @return LoggerInterface
234 function getLogger() : LoggerInterface
{
235 return $this->logger
;
241 function getFuture() {
242 return $this->future
;
248 public function getClient(): Client
{
249 return $this->client
;
253 * @return array|object
260 * Accessor to any hypermedia links
264 function getLinks() {
269 * Export the endpoint's underlying data
271 * @return array ["url", "data", "type", "links", "headers"]
273 function export() : array {
275 $url = clone $this->url
;
276 $type = clone $this->type
;
277 $links = $this->links ?
clone $this->links
: null;
278 $headers = $this->headers
;
279 return compact("url", "data", "type", "links", "headers");
286 function with($export) : API
{
288 if (is_array($export) ||
($export instanceof \ArrayAccess
)) {
289 isset($export["url"]) && $that->url
= $export["url"];
290 isset($export["data"]) && $that->data
= $export["data"];
291 isset($export["type"]) && $that->type
= $export["type"];
292 isset($export["links"]) && $that->links
= $export["links"];
293 isset($export["headers"]) && $that->headers
= $export["headers"];
299 * Create a copy of the endpoint with specific data
304 function withData($data) : API
{
311 * Create a copy of the endpoint with a specific Url, but with data reset
316 function withUrl(Url
$url) : API
{
320 #$that->links = null;
325 * Create a copy of the endpoint with a specific header added/replaced
327 * @param string $name
328 * @param mixed $value
331 function withHeader(string $name, $value) : API
{
334 $that->headers
[$name] = $value;
336 unset($that->headers
[$name]);
342 * Create a copy of the endpoint with a customized accept header
344 * Changes the returned endpoint's accept header to "application/vnd.github.v3.{$type}"
346 * @param string $type The expected return data type, e.g. "raw", "html", ..., or a complete content type
347 * @param bool $keepdata Whether to keep already fetched data
350 function as(string $type, bool $keepdata = true) : API
{
351 $that = ContentType
::apply($this, $type);
359 * Perform a GET request against the endpoint's underlying URL
361 * @param mixed $args The HTTP query string parameters
362 * @param array $headers The request's additional HTTP headers
365 function get($args = null, array $headers = null, $cache = null) : Promise
{
366 return $this->request("GET", $args, null, $headers, $cache);
370 * Perform a DELETE request against the endpoint's underlying URL
372 * @param mixed $args The HTTP query string parameters
373 * @param array $headers The request's additional HTTP headers
376 function delete($args = null, array $headers = null) : Promise
{
377 return $this->request("DELETE", $args, null, $headers);
381 * Perform a POST request against the endpoint's underlying URL
383 * @param mixed $body The HTTP message's body
384 * @param mixed $args The HTTP query string parameters
385 * @param array $headers The request's additional HTTP headers
388 function post($body = null, $args = null, array $headers = null) : Promise
{
389 return $this->request("POST", $args, $body, $headers);
393 * Perform a PUT request against the endpoint's underlying URL
395 * @param mixed $body The HTTP message's body
396 * @param mixed $args The HTTP query string parameters
397 * @param array $headers The request's additional HTTP headers
400 function put($body = null, $args = null, array $headers = null) : Promise
{
401 return $this->request("PUT", $args, $body, $headers);
405 * Perform a PATCH request against the endpoint's underlying URL
407 * @param mixed $body The HTTP message's body
408 * @param mixed $args The HTTP query string parameters
409 * @param array $headers The request's additional HTTP headers
412 function patch($body = null, $args = null, array $headers = null) : Promise
{
413 return $this->request("PATCH", $args, $body, $headers);
417 * Perform all queued HTTP transfers
421 function send() : API
{
422 $this->logger
->debug("send: start loop");
423 while (count($this->client
)) {
424 $this->client
->send();
426 $this->logger
->debug("send: end loop");
431 * Check for a specific key in the endpoint's underlying data
437 function exists($seg, &$val = null) : bool {
438 if (is_array($this->data
) && array_key_exists($seg, $this->data
)) {
439 $val = $this->data
[$seg];
441 } elseif (is_object($this->data
) && property_exists($this->data
, $seg)) {
442 $val = $this->data
->$seg;
449 $this->logger
->debug(sprintf("exists(%s) in %s -> %s",
450 $seg, typeof($this->data
, false), $exists ?
"true" : "false"
452 "url" => (string) $this->url
,
460 * Queue the actual HTTP transfer through \seekat\API\Deferred and return the promise
462 * @param string $method The HTTP request method
463 * @param mixed $args The HTTP query string parameters
464 * @param mixed $body Thee HTTP message's body
465 * @param array $headers The request's additional HTTP headers
466 * @param Call\Cache\Service $cache
469 private function request(string $method, $args = null, $body = null, array $headers = null, Call\Cache\Service
$cache = null) : Promise
{
470 if (isset($this->data
)) {
471 $this->logger
->debug("request -> resolve", [
473 "url" => (string) $this->url
,
476 "headers" => $headers,
479 return Future\resolve
($this->future
, $this);
482 $url = $this->url
->mod(["query" => new QueryString($args)]);
483 $request = new Request($method, $url, ((array) $headers) +
$this->headers
,
484 $body = is_array($body) ?
json_encode($body) : (
485 is_resource($body) ?
new Body($body) : (
486 is_scalar($body) ?
(new Body
)->append($body) :
489 $this->logger
->info("request -> deferred", [
491 "url" => (string) $this->url
,
492 "args" => $this->url
->query
,
494 "headers" => $headers,
497 return (new Call\
Deferred($this, $request, $cache ?
: $this->cache
))();