From 94eaffbe8c349ce03f5b5f3e2e8d9347118ae76f Mon Sep 17 00:00:00 2001 From: Dennis de Greef Date: Mon, 13 Mar 2017 11:52:13 +0100 Subject: [PATCH 1/2] Initial tryout of PaginatedResponse --- src/Client.php | 31 ++---- src/PaginatedResponse.php | 184 ++++++++++++++++++++++++++++++++++++ src/Service/UserService.php | 2 +- 3 files changed, 194 insertions(+), 23 deletions(-) create mode 100644 src/PaginatedResponse.php diff --git a/src/Client.php b/src/Client.php index 9db12b4..1f432ee 100644 --- a/src/Client.php +++ b/src/Client.php @@ -21,7 +21,7 @@ use Link0\Bunq\Middleware\ResponseSignatureMiddleware; use Psr\Http\Message\ResponseInterface; -final class Client +class Client { /** * @var GuzzleClient @@ -58,7 +58,7 @@ public function __construct(Environment $environment, Keypair $keypair, PublicKe * @param string $endpoint * @return array */ - public function get(string $endpoint, array $headers = []): array + public function get(string $endpoint, array $headers = []) { return $this->processResponse( $this->guzzle->request('GET', $endpoint, [ @@ -73,7 +73,7 @@ public function get(string $endpoint, array $headers = []): array * @param array $headers * @return array */ - public function post(string $endpoint, array $body, array $headers = []): array + public function post(string $endpoint, array $body, array $headers = []) { return $this->processResponse( $this->guzzle->request('POST', $endpoint, [ @@ -89,7 +89,7 @@ public function post(string $endpoint, array $body, array $headers = []): array * @param array $headers * @return array */ - public function put(string $endpoint, array $body, array $headers = []): array + public function put(string $endpoint, array $body, array $headers = []) { return $this->processResponse( $this->guzzle->request('PUT', $endpoint, [ @@ -113,30 +113,17 @@ public function delete(string $endpoint, array $headers = []) /** * @param ResponseInterface $response - * @return array + * @return array|PaginatedResponse */ - private function processResponse(ResponseInterface $response): array + private function processResponse(ResponseInterface $response) { $contents = (string) $response->getBody(); - $json = json_decode($contents, true)['Response']; - - // Return empty responses - if (count($json) === 0) { - return []; - } + $json = json_decode($contents, true); - foreach ($json as $key => $value) { - if (is_numeric($key)) { - // Often only a single associative entry here - foreach ($value as $type => $struct) { - $json[$key] = $this->mapResponse($type, $struct); - } - } - } - return $json; + return new PaginatedResponse($this, $json); } - private function mapResponse(string $key, array $value) + public function mapResponse(string $key, array $value) { switch ($key) { case 'DeviceServer': diff --git a/src/PaginatedResponse.php b/src/PaginatedResponse.php new file mode 100644 index 0000000..43e12b2 --- /dev/null +++ b/src/PaginatedResponse.php @@ -0,0 +1,184 @@ + [ + * 0 => [ + * 'Foo' => [ + * 'id' => 123, + * ], + * ], + * 1 => [ + * 'Foo' => [ + * 'id' => 456, + * ], + * ], + * + * ], + * 'Pagination' => [ + * 'future_url' => '/v1/foo?newer_id=123', + * 'newer_url' => '/v1/foo?newer_id=456', + * 'older_url' => null, + * ] + * ) + */ +final class PaginatedResponse implements IteratorAggregate, ArrayAccess +{ + /** + * @var Client + */ + private $client; + + /** + * @var array + */ + private $list; + + /** + * @var array + */ + private $pagination; + + /** + * @param Client $client + */ + public function __construct(Client $client, array $body) + { + $this->client = $client; + + $this->guardAndSetResponseBody($body); + $this->guardAndSetPagination($body); + } + + /** + * @param array $body + * @return void + */ + private function guardAndSetResponseBody(array $body) + { + if (!isset($body['Response'])) { + throw new InvalidArgumentException("Response body should contain key 'Response'"); + } + $this->list = $body['Response']; + } + + /** + * @param array $body + * @return void + */ + private function guardAndSetPagination(array $body) + { + $this->pagination = [ + 'future_url' => null, + 'newer_url' => null, + 'older_url' => null, + ]; + + if (isset($body['Pagination'])) { + $pagination = $body['Pagination']; + + if (!array_key_exists('future_url', $pagination)) { + throw new InvalidArgumentException("Pagination should contain future_url"); + } + if (!array_key_exists('newer_url', $pagination)) { + throw new InvalidArgumentException("Pagination should contain newer_url"); + } + if (!array_key_exists('older_url', $pagination)) { + throw new InvalidArgumentException("Pagination should contain older_url"); + } + $this->pagination = $pagination; + } + } + + /** + * Retrieve an external iterator + * @link http://php.net/manual/en/iteratoraggregate.getiterator.php + * @return Traversable An instance of an object implementing Iterator or + * Traversable + * @since 5.0.0 + */ + public function getIterator() + { + foreach ($this->list as $key => $value) { + // mapResponse takes the struct and instantiates Value Objects + yield $key => $this->client->mapResponse($key, $value); + } + + if ($this->pagination['newer_url'] !== null) { + foreach ($this->client->get($this->pagination['newer_url']) as $newKey => $newValue) { + $this->list[] = $newValue; + yield $newKey => $newValue; + } + } + } + + /** + * Whether a offset exists + * @link http://php.net/manual/en/arrayaccess.offsetexists.php + * @param mixed $offset

+ * An offset to check for. + *

+ * @return boolean true on success or false on failure. + *

+ *

+ * The return value will be casted to boolean if non-boolean was returned. + * @since 5.0.0 + */ + public function offsetExists($offset) + { + return array_key_exists($offset, $this->list); + } + + /** + * Offset to retrieve + * @link http://php.net/manual/en/arrayaccess.offsetget.php + * @param mixed $offset

+ * The offset to retrieve. + *

+ * @return mixed Can return all value types. + * @since 5.0.0 + */ + public function offsetGet($offset) + { + + return $this->list[$offset]; + } + + /** + * Offset to set + * @link http://php.net/manual/en/arrayaccess.offsetset.php + * @param mixed $offset

+ * The offset to assign the value to. + *

+ * @param mixed $value

+ * The value to set. + *

+ * @return void + * @since 5.0.0 + */ + public function offsetSet($offset, $value) + { + throw new \LogicException("Unable to set value on immutable object"); + } + + /** + * Offset to unset + * @link http://php.net/manual/en/arrayaccess.offsetunset.php + * @param mixed $offset

+ * The offset to unset. + *

+ * @return void + * @since 5.0.0 + */ + public function offsetUnset($offset) + { + throw new \LogicException("Unable to unset value on immutable object"); + } +} diff --git a/src/Service/UserService.php b/src/Service/UserService.php index f9bf179..0505f39 100644 --- a/src/Service/UserService.php +++ b/src/Service/UserService.php @@ -24,7 +24,7 @@ public function __construct(Client $client) /** * @return User[] */ - public function listUsers(): array + public function listUsers() { return $this->client->get('user'); } From 27087905b7e4185c74a1451c9ca83663d9cd4cde Mon Sep 17 00:00:00 2001 From: Dennis de Greef Date: Mon, 13 Mar 2017 12:30:55 +0100 Subject: [PATCH 2/2] Prevent exponential cache buildup --- src/PaginatedResponse.php | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/src/PaginatedResponse.php b/src/PaginatedResponse.php index 43e12b2..2b42271 100644 --- a/src/PaginatedResponse.php +++ b/src/PaginatedResponse.php @@ -112,8 +112,12 @@ public function getIterator() } if ($this->pagination['newer_url'] !== null) { - foreach ($this->client->get($this->pagination['newer_url']) as $newKey => $newValue) { + /** @var PaginatedResponse $nextPagination */ + $nextPagination = $this->client->get($this->pagination['newer_url']); + + foreach ($nextPagination as $newKey => $newValue) { $this->list[] = $newValue; + unset($nextPagination->list[$newKey]); yield $newKey => $newValue; } }