7 use http\
{Client
, Client\Request
, Message\Body
, QueryString
, Url
};
10 use Psr\Log\
{LoggerInterface
, NullLogger
};
11 use seekat\API\
{Call
, Consumer
, ContentType
, Future
, Links
};
12 use seekat\Exception\InvalidArgumentException
;
14 class API
implements IteratorAggregate
, Countable
{
22 * The current API endpoint URL
28 * Default headers to send out to the API endpoint
34 * Current endpoints links
40 * Current endpoint data's Content-Type
41 * @var API\ContentType
46 * Current endpoint's data
53 * @var LoggerInterface
59 * @var Call\Cache\Service
76 * Create a new API endpoint root
80 * @param Future $future pretending to fulfill promises
81 * @param array $headers Standard request headers, defaults to ["Accept" => "application/vnd.github.v3+json"]
82 * @param Url $url The API's endpoint, defaults to https://api.github.com
83 * @param Client $client The HTTP client to use for executing requests
84 * @param LoggerInterface $log A logger
85 * @param Call\Cache\Service $cache A cache
87 function __construct(Future
$future, array $headers = null, Url
$url = null, Client
$client = null, LoggerInterface
$log = null, Call\Cache\Service
$cache = null) {
88 $this->future
= $future;
89 $this->cache
= $cache;
90 $this->logger
= $log ??
new NullLogger
;
91 $this->url
= $url ??
new Url("https://api.github.com");
92 $this->client
= $client ??
new Client
;
93 $this->type
= new ContentType($this->version
, "json");
94 $this->headers
= (array) $headers +
[
95 "Accept" => $this->type
->getContentType()
100 * Ascend one level deep into the API endpoint
102 * @param string|int $seg The "path" element to ascend into
103 * @return API Endpoint clone referring to {$parent}/{$seg}
105 function __get($seg) : API
{
106 if (substr($seg, -4) === "_url") {
107 $url = new Url(uri_template($this->data
->$seg));
108 $that = $this->withUrl($url);
109 $seg = basename($that->url
->path
);
112 $that->url
->path
.= "/".urlencode($seg);
113 $this->exists($seg, $that->data
);
116 $this->logger
->debug("get($seg)", [
121 "data" => $that->data
128 * Call handler that actually queues a data fetch and returns a promise
130 * @param string $method The API's "path" element to ascend into
131 * @param array $args Array of arguments forwarded to \seekat\API::get()
132 * @return mixed promise
134 function __call(string $method, array $args) {
135 /* We cannot implement an explicit then() method,
136 * because the Promise implementation might think
137 * we're actually implementing Thenable,
138 * which might cause an infinite loop.
140 if ($method === "then"
142 * very short-hand version:
143 * ->users->m6w6->gists->get()->then(...)
145 * ->users->m6w6->gists(...)
147 ||
is_callable(current($args))) {
148 return $this->future
->handlePromise($this->get(), ...$args);
151 return (new Call($this, $method))($args);
155 * Run the send loop through a generator
157 * @param callable|Generator $cbg A \Generator or a factory of a \Generator yielding promises
158 * @return mixed The promise of the generator's return value
159 * @throws InvalidArgumentException
161 function __invoke($cbg) {
162 $this->logger
->debug(__FUNCTION__
);
164 $consumer = new Consumer($this->getFuture(), function() {
165 $this->client
->send();
169 if ($cbg instanceof Generator
) {
170 return $consumer($cbg);
173 if (is_callable($cbg)) {
178 throw new InvalidArgumentException(
179 "Expected callable or Generator, got ".typeof($cbg, true)
184 * Clone handler ensuring the underlying url will be cloned, too
187 $this->url
= clone $this->url
;
191 * The string handler for the endpoint's data
195 function __toString() : string {
196 return (string) $this->type
->encode($this->data
);
200 * Create an iterator over the endpoint's underlying data
204 function getIterator() : Iterator
{
205 foreach ($this->data
as $key => $cur) {
206 if ($this->__get($key)->exists("url", $url)) {
207 $url = new Url($url);
208 $val = $this->withUrl($url)->withData($cur);
210 $val = $this->__get($key)->withData($cur);
217 * Count the underlying data's entries
221 function count() : int {
222 if (is_array($this->data
)) {
223 $count = count($this->data
);
224 } else if ($this->data
instanceof Countable
) {
225 $count = count($this->data
);
226 } else if (is_object($this->data
)) {
227 $count = count((array) $this->data
);
231 $this->logger
->debug("count()", [
232 "of type" => typeof($this->data
),
241 function getUrl() : Url
{
246 * @return LoggerInterface
248 function getLogger() : LoggerInterface
{
249 return $this->logger
;
255 function getFuture() {
256 return $this->future
;
262 public function getClient(): Client
{
263 return $this->client
;
267 * @return array|object
274 * Accessor to any hypermedia links
278 function getLinks() {
285 function getVersion() : int {
286 return $this->version
;
290 * Export the endpoint's underlying data
292 * @return array ["url", "data", "type", "links", "headers"]
294 function export() : array {
296 $url = clone $this->url
;
297 $type = $this->type ?
clone $this->type
: null;
298 $links = $this->links ?
clone $this->links
: null;
299 $headers = $this->headers
;
300 return compact("url", "data", "type", "links", "headers");
307 function with($export) : API
{
309 if (is_array($export) ||
($export instanceof \ArrayAccess
)) {
310 isset($export["url"]) && $that->url
= $export["url"];
311 isset($export["data"]) && $that->data
= $export["data"];
312 isset($export["type"]) && $that->type
= $export["type"];
313 isset($export["links"]) && $that->links
= $export["links"];
314 isset($export["headers"]) && $that->headers
= $export["headers"];
320 * Create a copy of the endpoint with specific data
325 function withData($data) : API
{
332 * Create a copy of the endpoint with a specific Url, but with data reset
337 function withUrl(Url
$url) : API
{
341 #$that->links = null;
346 * Create a copy of the endpoint with a specific header added/replaced
348 * @param string $name
349 * @param mixed $value
352 function withHeader(string $name, $value) : API
{
355 $that->headers
[$name] = $value;
357 unset($that->headers
[$name]);
363 * Create a copy of the endpoint with a customized accept header
365 * Changes the returned endpoint's accept header to "application/vnd.github.v3.{$type}"
367 * @param string $type The expected return data type, e.g. "raw", "html", ..., or a complete content type
368 * @param bool $keepdata Whether to keep already fetched data
371 function as(string $type, bool $keepdata = true) : API
{
372 $ct = new ContentType($this->version
, $type);
374 $that = $ct->apply($this);
384 * Perform a HEAD request against the endpoint's underlying URL
386 * @param mixed $args The HTTP query string parameters
387 * @param array $headers The request's additional HTTP headers
388 * @return mixed promise
390 function head($args = null, array $headers = null, $cache = null) {
391 return $this->request("HEAD", $args, null, $headers, $cache);
395 * Perform a GET request against the endpoint's underlying URL
397 * @param mixed $args The HTTP query string parameters
398 * @param array $headers The request's additional HTTP headers
399 * @return mixed promise
401 function get($args = null, array $headers = null, $cache = null) {
402 return $this->request("GET", $args, null, $headers, $cache);
406 * Perform a DELETE request against the endpoint's underlying URL
408 * @param mixed $args The HTTP query string parameters
409 * @param array $headers The request's additional HTTP headers
410 * @return mixed promise
412 function delete($args = null, array $headers = null) {
413 return $this->request("DELETE", $args, null, $headers);
417 * Perform a POST request against the endpoint's underlying URL
419 * @param mixed $body The HTTP message's body
420 * @param mixed $args The HTTP query string parameters
421 * @param array $headers The request's additional HTTP headers
422 * @return mixed promise
424 function post($body = null, $args = null, array $headers = null) {
425 return $this->request("POST", $args, $body, $headers);
429 * Perform a PUT request against the endpoint's underlying URL
431 * @param mixed $body The HTTP message's body
432 * @param mixed $args The HTTP query string parameters
433 * @param array $headers The request's additional HTTP headers
434 * @return mixed promise
436 function put($body = null, $args = null, array $headers = null) {
437 return $this->request("PUT", $args, $body, $headers);
441 * Perform a PATCH request against the endpoint's underlying URL
443 * @param mixed $body The HTTP message's body
444 * @param mixed $args The HTTP query string parameters
445 * @param array $headers The request's additional HTTP headers
446 * @return mixed promise
448 function patch($body = null, $args = null, array $headers = null) {
449 return $this->request("PATCH", $args, $body, $headers);
453 * Perform all queued HTTP transfers
457 function send() : API
{
458 $this->logger
->debug("send: start loop");
459 while (count($this->client
)) {
460 $this->client
->send();
462 $this->logger
->debug("send: end loop");
467 * Check for a specific key in the endpoint's underlying data
473 function exists($seg, &$val = null) : bool {
474 if (is_array($this->data
) && array_key_exists($seg, $this->data
)) {
475 $val = $this->data
[$seg];
477 } elseif (is_object($this->data
) && property_exists($this->data
, $seg)) {
478 $val = $this->data
->$seg;
485 $this->logger
->debug(sprintf("exists(%s) in %s -> %s",
486 $seg, typeof($this->data
, false), $exists ?
"true" : "false"
488 "url" => (string) $this->url
,
489 "val" => typeof($val, false),
496 * Queue the actual HTTP transfer through \seekat\API\Deferred and return the promise
498 * @param string $method The HTTP request method
499 * @param mixed $args The HTTP query string parameters
500 * @param mixed $body Thee HTTP message's body
501 * @param array $headers The request's additional HTTP headers
502 * @param Call\Cache\Service $cache
503 * @return mixed promise
505 private function request(string $method, $args = null, $body = null, array $headers = null, Call\Cache\Service
$cache = null) {
506 if (isset($this->data
)) {
507 $this->logger
->debug("request -> resolve", [
509 "url" => (string) $this->url
,
512 "headers" => $headers,
515 return Future\resolve
($this->future
, $this);
518 $url = $this->url
->mod(["query" => new QueryString($args)]);
519 $request = new Request($method, $url, ((array) $headers) +
$this->headers
,
520 $body = $this->type
->encode(is_resource($body) ?
new Body($body) : $body));
522 $this->logger
->info("request -> deferred", [
524 "url" => (string) $this->url
,
525 "args" => $this->url
->query
,
527 "headers" => $headers,
530 return (new Call\
Deferred($this, $request, $cache ?
: $this->cache
))();