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(__FUNCTION__
."($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 InvalidArgumentException(
168 "Expected callable or Generator, got ".(
170 ?
"instance of ".get_class($cbg)
171 : gettype($cbg).": ".var_export($cbg, true)
177 * Clone handler ensuring the underlying url will be cloned, too
180 $this->url
= clone $this->url
;
184 * The string handler for the endpoint's data
188 function __toString() : string {
189 if (is_scalar($this->data
)) {
190 return (string) $this->data
;
194 return json_encode($this->data
);
198 * Create an iterator over the endpoint's underlying data
202 function getIterator() : Iterator
{
203 return new Iterator($this);
207 * Count the underlying data's entries
211 function count() : int {
212 return count($this->data
);
218 function getUrl() : Url
{
223 * @return LoggerInterface
225 function getLogger() : LoggerInterface
{
226 return $this->logger
;
232 public function getClient(): Client
{
233 return $this->client
;
237 * @return array|object
244 * Accessor to any hypermedia links
248 function getLinks() {
253 * Export the endpoint's underlying data
255 * @return array ["url", "data", "type", "links", "headers"]
257 function export() : array {
259 $url = clone $this->url
;
260 $type = clone $this->type
;
261 $links = $this->links ?
clone $this->links
: null;
262 $headers = $this->headers
;
263 return compact("url", "data", "type", "links", "headers");
270 function with($export) : API
{
272 if (is_array($export) ||
($export instanceof \ArrayAccess
)) {
273 isset($export["url"]) && $that->url
= $export["url"];
274 isset($export["data"]) && $that->data
= $export["data"];
275 isset($export["type"]) && $that->type
= $export["type"];
276 isset($export["links"]) && $that->links
= $export["links"];
277 isset($export["headers"]) && $that->headers
= $export["headers"];
283 * Create a copy of the endpoint with specific data
288 function withData($data) : API
{
295 * Create a copy of the endpoint with a specific Url, but with data reset
300 function withUrl(Url
$url) : API
{
304 #$that->links = null;
309 * Create a copy of the endpoint with a specific header added/replaced
311 * @param string $name
312 * @param mixed $value
315 function withHeader(string $name, $value) : API
{
318 $that->headers
[$name] = $value;
320 unset($that->headers
[$name]);
326 * Create a copy of the endpoint with a customized accept header
328 * Changes the returned endpoint's accept header to "application/vnd.github.v3.{$type}"
330 * @param string $type The expected return data type, e.g. "raw", "html", ..., or a complete content type
331 * @param bool $keepdata Whether to keep already fetched data
334 function as(string $type, bool $keepdata = true) : API
{
335 $that = ContentType
::apply($this, $type);
343 * Perform a GET request against the endpoint's underlying URL
345 * @param mixed $args The HTTP query string parameters
346 * @param array $headers The request's additional HTTP headers
347 * @return ExtendedPromiseInterface
349 function get($args = null, array $headers = null, $cache = null) : ExtendedPromiseInterface
{
350 return $this->request("GET", $args, null, $headers, $cache);
354 * Perform a DELETE request against the endpoint's underlying URL
356 * @param mixed $args The HTTP query string parameters
357 * @param array $headers The request's additional HTTP headers
358 * @return ExtendedPromiseInterface
360 function delete($args = null, array $headers = null) : ExtendedPromiseInterface
{
361 return $this->request("DELETE", $args, null, $headers);
365 * Perform a POST request against the endpoint's underlying URL
367 * @param mixed $body The HTTP message's body
368 * @param mixed $args The HTTP query string parameters
369 * @param array $headers The request's additional HTTP headers
370 * @return ExtendedPromiseInterface
372 function post($body = null, $args = null, array $headers = null) : ExtendedPromiseInterface
{
373 return $this->request("POST", $args, $body, $headers);
377 * Perform a PUT request against the endpoint's underlying URL
379 * @param mixed $body The HTTP message's body
380 * @param mixed $args The HTTP query string parameters
381 * @param array $headers The request's additional HTTP headers
382 * @return ExtendedPromiseInterface
384 function put($body = null, $args = null, array $headers = null) : ExtendedPromiseInterface
{
385 return $this->request("PUT", $args, $body, $headers);
389 * Perform a PATCH request against the endpoint's underlying URL
391 * @param mixed $body The HTTP message's body
392 * @param mixed $args The HTTP query string parameters
393 * @param array $headers The request's additional HTTP headers
394 * @return ExtendedPromiseInterface
396 function patch($body = null, $args = null, array $headers = null) : ExtendedPromiseInterface
{
397 return $this->request("PATCH", $args, $body, $headers);
401 * Perform all queued HTTP transfers
405 function send() : API
{
406 $this->logger
->debug(__FUNCTION__
.": start loop");
407 while (count($this->client
)) {
408 $this->client
->send();
410 $this->logger
->debug(__FUNCTION__
.": end loop");
415 * Check for a specific key in the endpoint's underlying data
421 function exists($seg, &$val = null) : bool {
422 if (is_array($this->data
) && array_key_exists($seg, $this->data
)) {
423 $val = $this->data
[$seg];
425 } elseif (is_object($this->data
) && property_exists($this->data
, $seg)) {
426 $val = $this->data
->$seg;
433 $this->logger
->debug(__FUNCTION__
."($seg) in ".(
434 is_object($this->data
)
435 ?
get_class($this->data
)
436 : gettype($this->data
)
442 "url" => (string) $this->url
,
450 * Queue the actual HTTP transfer through \seekat\API\Deferred and return the promise
452 * @param string $method The HTTP request method
453 * @param mixed $args The HTTP query string parameters
454 * @param mixed $body Thee HTTP message's body
455 * @param array $headers The request's additional HTTP headers
456 * @param Call\Cache\Service $cache
457 * @return ExtendedPromiseInterface
459 private function request(string $method, $args = null, $body = null, array $headers = null, Call\Cache\Service
$cache = null) : ExtendedPromiseInterface
{
460 if (isset($this->data
)) {
461 $this->logger
->debug("request -> resolve", [
463 "url" => (string)$this->url
,
466 "headers" => $headers,
469 return resolve($this);
472 $url = $this->url
->mod(["query" => new QueryString($args)]);
473 $request = new Request($method, $url, ((array) $headers) +
$this->headers
,
474 $body = is_array($body) ?
json_encode($body) : (
475 is_resource($body) ?
new Body($body) : (
476 is_scalar($body) ?
(new Body
)->append($body) :
479 $this->logger
->info("request -> deferred", [
481 "url" => (string) $this->url
,
482 "args" => $this->url
->query
,
484 "headers" => $headers,
487 return (new Call\
Deferred($this, $request, $cache ?
: $this->cache
))->promise();