8 Client
, Client\Request
, Message\Body
, QueryString
, Url
10 use IteratorAggregate
;
12 LoggerInterface
, NullLogger
15 Call
, Consumer
, ContentType
, Future
, Iterator
, Links
17 use seekat\Exception\InvalidArgumentException
;
19 class API
implements IteratorAggregate
, Countable
{
21 * The current API endpoint URL
27 * Default headers to send out to the API endpoint
33 * Current endpoints links
39 * Current endpoint data's Content-Type
40 * @var API\ContentType
45 * Current endpoint's data
52 * @var LoggerInterface
58 * @var Call\Cache\Service
75 * Create a new API endpoint root
77 * @param Future $future pretending to fulfill promises
78 * @param array $headers Standard request headers, defaults to ["Accept" => "application/vnd.github.v3+json"]
79 * @param Url $url The API's endpoint, defaults to https://api.github.com
80 * @param Client $client The HTTP client to use for executing requests
81 * @param LoggerInterface $log A logger
82 * @param Call\Cache\Service $cache A cache
84 function __construct(Future
$future, array $headers = null, Url
$url = null, Client
$client = null, LoggerInterface
$log = null, Call\Cache\Service
$cache = null) {
85 $this->future
= $future;
86 $this->cache
= $cache;
87 $this->logger
= $log ??
new NullLogger
;
88 $this->url
= $url ??
new Url("https://api.github.com");
89 $this->client
= $client ??
new Client
;
90 $this->headers
= (array) $headers +
[
91 "Accept" => "application/vnd.github.v3+json"
96 * Ascend one level deep into the API endpoint
98 * @param string|int $seg The "path" element to ascend into
99 * @return API Endpoint clone referring to {$parent}/{$seg}
101 function __get($seg) : API
{
102 if (substr($seg, -4) === "_url") {
103 $url = new Url(uri_template($this->data
->$seg));
104 $that = $this->withUrl($url);
105 $seg = basename($that->url
->path
);
108 $that->url
->path
.= "/".urlencode($seg);
109 $this->exists($seg, $that->data
);
112 $this->logger
->debug("get($seg)", [
123 * Call handler that actually queues a data fetch and returns a promise
125 * @param string $method The API's "path" element to ascend into
126 * @param array $args Array of arguments forwarded to \seekat\API::get()
127 * @return mixed promise
129 function __call(string $method, array $args) {
130 /* We cannot implement an explicit then() method,
131 * because the Promise implementation might think
132 * we're actually implementing Thenable,
133 * which might cause an infinite loop.
135 if ($method === "then"
137 * very short-hand version:
138 * ->users->m6w6->gists->get()->then(...)
140 * ->users->m6w6->gists(...)
142 ||
is_callable(current($args))) {
143 return $this->future
->handlePromise($this->get(), ...$args);
146 return (new Call($this, $method))($args);
150 * Run the send loop through a generator
152 * @param callable|Generator $cbg A \Generator or a factory of a \Generator yielding promises
153 * @return mixed The promise of the generator's return value
154 * @throws InvalidArgumentException
156 function __invoke($cbg) {
157 $this->logger
->debug(__FUNCTION__
);
159 $consumer = new Consumer($this->getFuture(), function() {
160 $this->client
->send();
164 if ($cbg instanceof Generator
) {
165 return $consumer($cbg);
168 if (is_callable($cbg)) {
173 throw new InvalidArgumentException(
174 "Expected callable or Generator, got ".typeof($cbg, true)
179 * Clone handler ensuring the underlying url will be cloned, too
182 $this->url
= clone $this->url
;
186 * The string handler for the endpoint's data
190 function __toString() : string {
191 if (is_scalar($this->data
)) {
192 return (string) $this->data
;
196 return json_encode($this->data
);
200 * Create an iterator over the endpoint's underlying data
204 function getIterator() : Iterator
{
205 return new Iterator($this);
209 * Count the underlying data's entries
213 function count() : int {
214 return count($this->data
);
220 function getUrl() : Url
{
225 * @return LoggerInterface
227 function getLogger() : LoggerInterface
{
228 return $this->logger
;
234 function getFuture() {
235 return $this->future
;
241 public function getClient(): Client
{
242 return $this->client
;
246 * @return array|object
253 * Accessor to any hypermedia links
257 function getLinks() {
262 * Export the endpoint's underlying data
264 * @return array ["url", "data", "type", "links", "headers"]
266 function export() : array {
268 $url = clone $this->url
;
269 $type = $this->type ?
clone $this->type
: null;
270 $links = $this->links ?
clone $this->links
: null;
271 $headers = $this->headers
;
272 return compact("url", "data", "type", "links", "headers");
279 function with($export) : API
{
281 if (is_array($export) ||
($export instanceof \ArrayAccess
)) {
282 isset($export["url"]) && $that->url
= $export["url"];
283 isset($export["data"]) && $that->data
= $export["data"];
284 isset($export["type"]) && $that->type
= $export["type"];
285 isset($export["links"]) && $that->links
= $export["links"];
286 isset($export["headers"]) && $that->headers
= $export["headers"];
292 * Create a copy of the endpoint with specific data
297 function withData($data) : API
{
304 * Create a copy of the endpoint with a specific Url, but with data reset
309 function withUrl(Url
$url) : API
{
313 #$that->links = null;
318 * Create a copy of the endpoint with a specific header added/replaced
320 * @param string $name
321 * @param mixed $value
324 function withHeader(string $name, $value) : API
{
327 $that->headers
[$name] = $value;
329 unset($that->headers
[$name]);
335 * Create a copy of the endpoint with a customized accept header
337 * Changes the returned endpoint's accept header to "application/vnd.github.v3.{$type}"
339 * @param string $type The expected return data type, e.g. "raw", "html", ..., or a complete content type
340 * @param bool $keepdata Whether to keep already fetched data
343 function as(string $type, bool $keepdata = true) : API
{
344 $that = ContentType
::apply($this, $type);
352 * Perform a HEAD request against the endpoint's underlying URL
354 * @param mixed $args The HTTP query string parameters
355 * @param array $headers The request's additional HTTP headers
356 * @return mixed promise
358 function head($args = null, array $headers = null, $cache = null) {
359 return $this->request("HEAD", $args, null, $headers, $cache);
363 * Perform a GET request against the endpoint's underlying URL
365 * @param mixed $args The HTTP query string parameters
366 * @param array $headers The request's additional HTTP headers
367 * @return mixed promise
369 function get($args = null, array $headers = null, $cache = null) {
370 return $this->request("GET", $args, null, $headers, $cache);
374 * Perform a DELETE request against the endpoint's underlying URL
376 * @param mixed $args The HTTP query string parameters
377 * @param array $headers The request's additional HTTP headers
378 * @return mixed promise
380 function delete($args = null, array $headers = null) {
381 return $this->request("DELETE", $args, null, $headers);
385 * Perform a POST request against the endpoint's underlying URL
387 * @param mixed $body The HTTP message's body
388 * @param mixed $args The HTTP query string parameters
389 * @param array $headers The request's additional HTTP headers
390 * @return mixed promise
392 function post($body = null, $args = null, array $headers = null) {
393 return $this->request("POST", $args, $body, $headers);
397 * Perform a PUT request against the endpoint's underlying URL
399 * @param mixed $body The HTTP message's body
400 * @param mixed $args The HTTP query string parameters
401 * @param array $headers The request's additional HTTP headers
402 * @return mixed promise
404 function put($body = null, $args = null, array $headers = null) {
405 return $this->request("PUT", $args, $body, $headers);
409 * Perform a PATCH request against the endpoint's underlying URL
411 * @param mixed $body The HTTP message's body
412 * @param mixed $args The HTTP query string parameters
413 * @param array $headers The request's additional HTTP headers
414 * @return mixed promise
416 function patch($body = null, $args = null, array $headers = null) {
417 return $this->request("PATCH", $args, $body, $headers);
421 * Perform all queued HTTP transfers
425 function send() : API
{
426 $this->logger
->debug("send: start loop");
427 while (count($this->client
)) {
428 $this->client
->send();
430 $this->logger
->debug("send: end loop");
435 * Check for a specific key in the endpoint's underlying data
441 function exists($seg, &$val = null) : bool {
442 if (is_array($this->data
) && array_key_exists($seg, $this->data
)) {
443 $val = $this->data
[$seg];
445 } elseif (is_object($this->data
) && property_exists($this->data
, $seg)) {
446 $val = $this->data
->$seg;
453 $this->logger
->debug(sprintf("exists(%s) in %s -> %s",
454 $seg, typeof($this->data
, false), $exists ?
"true" : "false"
456 "url" => (string) $this->url
,
464 * Queue the actual HTTP transfer through \seekat\API\Deferred and return the promise
466 * @param string $method The HTTP request method
467 * @param mixed $args The HTTP query string parameters
468 * @param mixed $body Thee HTTP message's body
469 * @param array $headers The request's additional HTTP headers
470 * @param Call\Cache\Service $cache
471 * @return mixed promise
473 private function request(string $method, $args = null, $body = null, array $headers = null, Call\Cache\Service
$cache = null) {
474 if (isset($this->data
)) {
475 $this->logger
->debug("request -> resolve", [
477 "url" => (string) $this->url
,
480 "headers" => $headers,
483 return Future\resolve
($this->future
, $this);
486 $url = $this->url
->mod(["query" => new QueryString($args)]);
487 $request = new Request($method, $url, ((array) $headers) +
$this->headers
,
488 $body = is_array($body) ?
json_encode($body) : (
489 is_resource($body) ?
new Body($body) : (
490 is_scalar($body) ?
(new Body
)->append($body) :
493 $this->logger
->info("request -> deferred", [
495 "url" => (string) $this->url
,
496 "args" => $this->url
->query
,
498 "headers" => $headers,
501 return (new Call\
Deferred($this, $request, $cache ?
: $this->cache
))();