8 Client
, Client\Request
, Message\Body
, QueryString
, Url
10 use IteratorAggregate
;
12 LoggerInterface
, NullLogger
15 ExtendedPromiseInterface
, function resolve
18 API\Call
, API\Consumer
, API\ContentType
, API\Iterator
, API\Links
, Exception\InvalidArgumentException
21 class API
implements IteratorAggregate
, Countable
{
23 * The current API endpoint URL
30 * @var LoggerInterface
36 * @var Call\Cache\Service
47 * Default headers to send out to the API endpoint
53 * Current endpoint data's Content-Type
54 * @var API\ContentType
59 * Current endpoint's data
65 * Current endpoints links
71 * Create a new API endpoint root
73 * @param array $headers Standard request headers, defaults to ["Accept" => "application/vnd.github.v3+json"]
74 * @param Url $url The API's endpoint, defaults to https://api.github.com
75 * @param Client $client The HTTP client to use for executing requests
76 * @param LoggerInterface $log A logger
78 function __construct(array $headers = null, Url
$url = null, Client
$client = null, LoggerInterface
$log = null, Call\Cache\Service
$cache = null) {
79 $this->cache
= $cache;
80 $this->logger
= $log ??
new NullLogger
;
81 $this->url
= $url ??
new Url("https://api.github.com");
82 $this->client
= $client ??
new Client
;
83 $this->headers
= (array) $headers +
[
84 "Accept" => "application/vnd.github.v3+json"
89 * Ascend one level deep into the API endpoint
91 * @param string|int $seg The "path" element to ascend into
92 * @return API Endpoint clone referring to {$parent}/{$seg}
94 function __get($seg) : API
{
95 if (substr($seg, -4) === "_url") {
96 $url = new Url(uri_template($this->data
->$seg));
97 $that = $this->withUrl($url);
98 $seg = basename($that->url
->path
);
101 $that->url
->path
.= "/".urlencode($seg);
102 $this->exists($seg, $that->data
);
105 $this->logger
->debug("get($seg)", [
116 * Call handler that actually queues a data fetch and returns a promise
118 * @param string $method The API's "path" element to ascend into
119 * @param array $args Array of arguments forwarded to \seekat\API::get()
120 * @return ExtendedPromiseInterface
122 function __call(string $method, array $args) : ExtendedPromiseInterface
{
123 /* We cannot implement an explicit then() method,
124 * because the Promise implementation might think
125 * we're actually implementing Thenable,
126 * which might cause an infinite loop.
128 if ($method === "then") {
129 return $this->get()->then(...$args);
133 * very short-hand version:
134 * ->users->m6w6->gists->get()->then(...)
136 * ->users->m6w6->gists(...)
138 if (is_callable(current($args))) {
139 return $this->api
->get()->then(current($args));
142 return (new Call($this, $method))($args);
146 * Run the send loop through a generator
148 * @param callable|Generator $cbg A \Generator or a factory of a \Generator yielding promises
149 * @return ExtendedPromiseInterface The promise of the generator's return value
150 * @throws InvalidArgumentException
152 function __invoke($cbg) : ExtendedPromiseInterface
{
153 $this->logger
->debug(__FUNCTION__
);
155 $consumer = new Consumer($this->client
);
158 if ($cbg instanceof Generator
) {
159 return $consumer($cbg);
162 if (is_callable($cbg)) {
167 throw new InvalidArgumentException(
168 "Expected callable or Generator, got ".typeof($cbg, true)
173 * Clone handler ensuring the underlying url will be cloned, too
176 $this->url
= clone $this->url
;
180 * The string handler for the endpoint's data
184 function __toString() : string {
185 if (is_scalar($this->data
)) {
186 return (string) $this->data
;
190 return json_encode($this->data
);
194 * Create an iterator over the endpoint's underlying data
198 function getIterator() : Iterator
{
199 return new Iterator($this);
203 * Count the underlying data's entries
207 function count() : int {
208 return count($this->data
);
214 function getUrl() : Url
{
219 * @return LoggerInterface
221 function getLogger() : LoggerInterface
{
222 return $this->logger
;
228 public function getClient(): Client
{
229 return $this->client
;
233 * @return array|object
240 * Accessor to any hypermedia links
244 function getLinks() {
249 * Export the endpoint's underlying data
251 * @return array ["url", "data", "type", "links", "headers"]
253 function export() : array {
255 $url = clone $this->url
;
256 $type = clone $this->type
;
257 $links = $this->links ?
clone $this->links
: null;
258 $headers = $this->headers
;
259 return compact("url", "data", "type", "links", "headers");
266 function with($export) : API
{
268 if (is_array($export) ||
($export instanceof \ArrayAccess
)) {
269 isset($export["url"]) && $that->url
= $export["url"];
270 isset($export["data"]) && $that->data
= $export["data"];
271 isset($export["type"]) && $that->type
= $export["type"];
272 isset($export["links"]) && $that->links
= $export["links"];
273 isset($export["headers"]) && $that->headers
= $export["headers"];
279 * Create a copy of the endpoint with specific data
284 function withData($data) : API
{
291 * Create a copy of the endpoint with a specific Url, but with data reset
296 function withUrl(Url
$url) : API
{
300 #$that->links = null;
305 * Create a copy of the endpoint with a specific header added/replaced
307 * @param string $name
308 * @param mixed $value
311 function withHeader(string $name, $value) : API
{
314 $that->headers
[$name] = $value;
316 unset($that->headers
[$name]);
322 * Create a copy of the endpoint with a customized accept header
324 * Changes the returned endpoint's accept header to "application/vnd.github.v3.{$type}"
326 * @param string $type The expected return data type, e.g. "raw", "html", ..., or a complete content type
327 * @param bool $keepdata Whether to keep already fetched data
330 function as(string $type, bool $keepdata = true) : API
{
331 $that = ContentType
::apply($this, $type);
339 * Perform a GET request against the endpoint's underlying URL
341 * @param mixed $args The HTTP query string parameters
342 * @param array $headers The request's additional HTTP headers
343 * @return ExtendedPromiseInterface
345 function get($args = null, array $headers = null, $cache = null) : ExtendedPromiseInterface
{
346 return $this->request("GET", $args, null, $headers, $cache);
350 * Perform a DELETE request against the endpoint's underlying URL
352 * @param mixed $args The HTTP query string parameters
353 * @param array $headers The request's additional HTTP headers
354 * @return ExtendedPromiseInterface
356 function delete($args = null, array $headers = null) : ExtendedPromiseInterface
{
357 return $this->request("DELETE", $args, null, $headers);
361 * Perform a POST request against the endpoint's underlying URL
363 * @param mixed $body The HTTP message's body
364 * @param mixed $args The HTTP query string parameters
365 * @param array $headers The request's additional HTTP headers
366 * @return ExtendedPromiseInterface
368 function post($body = null, $args = null, array $headers = null) : ExtendedPromiseInterface
{
369 return $this->request("POST", $args, $body, $headers);
373 * Perform a PUT request against the endpoint's underlying URL
375 * @param mixed $body The HTTP message's body
376 * @param mixed $args The HTTP query string parameters
377 * @param array $headers The request's additional HTTP headers
378 * @return ExtendedPromiseInterface
380 function put($body = null, $args = null, array $headers = null) : ExtendedPromiseInterface
{
381 return $this->request("PUT", $args, $body, $headers);
385 * Perform a PATCH 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 ExtendedPromiseInterface
392 function patch($body = null, $args = null, array $headers = null) : ExtendedPromiseInterface
{
393 return $this->request("PATCH", $args, $body, $headers);
397 * Perform all queued HTTP transfers
401 function send() : API
{
402 $this->logger
->debug("send: start loop");
403 while (count($this->client
)) {
404 $this->client
->send();
406 $this->logger
->debug("send: end loop");
411 * Check for a specific key in the endpoint's underlying data
417 function exists($seg, &$val = null) : bool {
418 if (is_array($this->data
) && array_key_exists($seg, $this->data
)) {
419 $val = $this->data
[$seg];
421 } elseif (is_object($this->data
) && property_exists($this->data
, $seg)) {
422 $val = $this->data
->$seg;
429 $this->logger
->debug(sprintf("exists(%s) in %s -> %s",
430 $seg, typeof($this->data
, false), $exists ?
"true" : "false"
432 "url" => (string) $this->url
,
440 * Queue the actual HTTP transfer through \seekat\API\Deferred and return the promise
442 * @param string $method The HTTP request method
443 * @param mixed $args The HTTP query string parameters
444 * @param mixed $body Thee HTTP message's body
445 * @param array $headers The request's additional HTTP headers
446 * @param Call\Cache\Service $cache
447 * @return ExtendedPromiseInterface
449 private function request(string $method, $args = null, $body = null, array $headers = null, Call\Cache\Service
$cache = null) : ExtendedPromiseInterface
{
450 if (isset($this->data
)) {
451 $this->logger
->debug("request -> resolve", [
453 "url" => (string)$this->url
,
456 "headers" => $headers,
459 return resolve($this);
462 $url = $this->url
->mod(["query" => new QueryString($args)]);
463 $request = new Request($method, $url, ((array) $headers) +
$this->headers
,
464 $body = is_array($body) ?
json_encode($body) : (
465 is_resource($body) ?
new Body($body) : (
466 is_scalar($body) ?
(new Body
)->append($body) :
469 $this->logger
->info("request -> deferred", [
471 "url" => (string) $this->url
,
472 "args" => $this->url
->query
,
474 "headers" => $headers,
477 return (new Call\
Deferred($this, $request, $cache ?
: $this->cache
))->promise();