diff --git a/src/BrandingClient.php b/src/BrandingClient.php index 2538a42..8350870 100644 --- a/src/BrandingClient.php +++ b/src/BrandingClient.php @@ -5,8 +5,11 @@ use DateTime; use GuzzleHttp\Client; use GuzzleHttp\Exception\RequestException; +use GuzzleHttp\Promise\FulfilledPromise; +use Psr\Http\Message\ResponseInterface; use Psr\Cache\CacheItemInterface; use Psr\Cache\CacheItemPoolInterface; +use Exception; use function GuzzleHttp\Psr7\parse_header; class BrandingClient @@ -79,55 +82,56 @@ public function __construct( public function getContent($projectId, $themeVersionId = null) { $url = $this->getUrl($projectId, $themeVersionId); - $cacheKey = $this->options['cacheKeyPrefix'] . '.' . md5($url); + $cacheItem = $this->fetchCacheItem($url); + if ($cacheItem->isHit()) { + return $this->mapResultToBrandingObject($cacheItem->get()); + } - if ($this->flushCacheItems) { - $this->cache->deleteItem($cacheKey); + try { + $response = $this->client->get($url, [ + 'headers' => ['Accept-Encoding' => 'gzip'] + ]); + $result = $this->parseAndCacheResponse($response, $cacheItem); + } catch (RequestException $e) { + throw $this->makeInvalidResponseException($e); } - /** @var CacheItemInterface $cacheItem */ - $cacheItem = $this->cache->getItem($cacheKey); - if (!$cacheItem->isHit()) { - try { - $response = $this->client->get($url, [ - 'headers' => ['Accept-Encoding' => 'gzip'] - ]); - $result = json_decode($response->getBody()->getContents(), true); - } catch (RequestException $e) { - throw new BrandingException('Invalid Branding Response. Could not get data from webservice', 0, $e); - } + return $this->mapResultToBrandingObject($result); + } - if (!$result || !isset($result['head'])) { - throw new BrandingException('Invalid Branding Response. Response JSON object was invalid or malformed'); - } + public function getContentAsync($projectId, $themeVersionId = null) + { + $url = $this->getUrl($projectId, $themeVersionId); + $cacheItem = $this->fetchCacheItem($url); + if ($cacheItem->isHit()) { + $brandingObject = $this->mapResultToBrandingObject($cacheItem->get()); + return new FulfilledPromise($brandingObject); + } - // Determine how long to cache for - $cacheTime = self::FALLBACK_CACHE_DURATION; - if ($this->options['cacheTime']) { - $cacheTime = $this->options['cacheTime']; - } else { - $cacheControl = $response->getHeaderLine('cache-control'); - if (isset(parse_header($cacheControl)[0]['max-age'])) { - $cacheTime = (int)parse_header($cacheControl)[0]['max-age']; - } else { - $expiryDate = $this->getDateFromHeader($response, 'Expires'); - $currentDate = $this->getDateFromHeader($response, 'Date'); - if ($currentDate && $expiryDate) { - // Beware of a cache time of 0 as 0 is treated by Doctrine - // Cache as "Cache for an infinite time" which is very much - // not what we want. -1 will be treated as already expired - $cacheTime = $expiryDate->getTimestamp() - $currentDate->getTimestamp(); - $cacheTime = ($cacheTime > 0 ? $cacheTime : -1); + try { + $requestPromise = $this->client->requestAsync('GET', $url, [ + 'headers' => ['Accept-Encoding' => 'gzip'] + ]); + $promise = $requestPromise->then( + // Success callback + function ($response) use ($cacheItem) { + $result = $this->parseAndCacheResponse($response, $cacheItem); + return $this->mapResultToBrandingObject($result); + }, + // Error callback + function ($reason) { + if ($reason instanceof RequestException) { + throw $this->makeInvalidResponseException($reason); + } + if ($reason instanceof Exception) { + throw $reason; } + throw new BrandingException("Invalid Branding Response. Unknown error reason in callback"); } - } - - // cache the result - $cacheItem->set($result); - $cacheItem->expiresAfter($cacheTime); - $this->cache->save($cacheItem); + ); + } catch (RequestException $e) { + throw $this->makeInvalidResponseException($e); } - - return $this->mapResultToBrandingObject($cacheItem->get()); + return $promise; } /** @@ -140,9 +144,9 @@ public function getOptions() return $this->options; } - public function setFlushCacheItems(bool $flushCacheItems): void + public function setFlushCacheItems($flushCacheItems) { - $this->flushCacheItems = $flushCacheItems; + $this->flushCacheItems = (bool) $flushCacheItems; } private function mapResultToBrandingObject(array $branding) @@ -198,4 +202,60 @@ private function getDateFromHeader($response, $headerName) return null; } + + private function fetchCacheItem($url) + { + $cacheKey = $this->options['cacheKeyPrefix'] . '.' . md5($url); + + if ($this->flushCacheItems) { + $this->cache->deleteItem($cacheKey); + } + /** @var CacheItemInterface $cacheItem */ + $cacheItem = $this->cache->getItem($cacheKey); + return $cacheItem; + } + + private function parseAndCacheResponse(ResponseInterface $response, CacheItemInterface $cacheItem) + { + $result = json_decode($response->getBody()->getContents(), true); + if (!$result || !isset($result['head'])) { + throw new BrandingException('Invalid Branding Response. Response JSON object was invalid or malformed'); + } + + // Determine how long to cache for + $cacheTime = self::FALLBACK_CACHE_DURATION; + if ($this->options['cacheTime']) { + $cacheTime = $this->options['cacheTime']; + } else { + $cacheControl = $response->getHeaderLine('cache-control'); + if (isset(parse_header($cacheControl)[0]['max-age'])) { + $cacheTime = (int)parse_header($cacheControl)[0]['max-age']; + } else { + $expiryDate = $this->getDateFromHeader($response, 'Expires'); + $currentDate = $this->getDateFromHeader($response, 'Date'); + if ($currentDate && $expiryDate) { + // Beware of a cache time of 0 as 0 is treated by Doctrine + // Cache as "Cache for an infinite time" which is very much + // not what we want. -1 will be treated as already expired + $cacheTime = $expiryDate->getTimestamp() - $currentDate->getTimestamp(); + $cacheTime = ($cacheTime > 0 ? $cacheTime : -1); + } + } + } + + // cache the result + $cacheItem->set($result); + $cacheItem->expiresAfter($cacheTime); + $this->cache->save($cacheItem); + return $result; + } + + private function makeInvalidResponseException(Exception $innerException) + { + return new BrandingException( + 'Invalid Branding Response. Could not get data from webservice', + 0, + $innerException + ); + } } diff --git a/src/BrandingStubClient.php b/src/BrandingStubClient.php index 2089e7d..0e9e203 100644 --- a/src/BrandingStubClient.php +++ b/src/BrandingStubClient.php @@ -3,6 +3,7 @@ namespace BBC\BrandingClient; use GuzzleHttp\Client; +use GuzzleHttp\Promise\FulfilledPromise; use Psr\Cache\CacheItemPoolInterface; class BrandingStubClient extends BrandingClient @@ -15,6 +16,16 @@ public function __construct( } public function getContent($projectId, $themeVersionId = null) + { + return $this->getMockBranding(); + } + + public function getContentAsync($projectId, $themeVersionId = null) + { + return new FulfilledPromise($this->getMockBranding()); + } + + private function getMockBranding() { return new Branding( '', diff --git a/tests/BrandingClientTest.php b/tests/BrandingClientTest.php index 27f2e39..51b1c19 100644 --- a/tests/BrandingClientTest.php +++ b/tests/BrandingClientTest.php @@ -131,6 +131,23 @@ public function testGetContentReturnsBrandingObject() $this->assertEquals($expectedContent, $brandingClient->getContent('br-123')); } + public function testGetContentPromiseReturnsBrandingObject() + { + $expectedContent = new Branding( + 'headContent', + 'bodyFirstContent', + 'bodyLastContent', + ['body' => ['bg' => '#eeeeee']], + ['language' => 'en'] + ); + + $client = $this->getClient([$this->mockSuccessfulJsonResponse()]); + + $brandingClient = new BrandingClient($client, $this->cache); + $promise = $brandingClient->getContentAsync('br-123'); + $this->assertEquals($expectedContent, $promise->wait(true)); + } + /** * @expectedException BBC\BrandingClient\BrandingException * @expectedExceptionMessage Invalid Branding Response. Could not get data from webservice @@ -143,6 +160,19 @@ public function testInvalidContentThrowsException() $brandingClient->getContent('br-123'); } + /** + * @expectedException BBC\BrandingClient\BrandingException + * @expectedExceptionMessage Invalid Branding Response. Could not get data from webservice + */ + public function testInvalidContentThrowsExceptionWhenPromiseResolved() + { + $client = $this->getClient([$this->mockInvalidJsonResponse()]); + + $brandingClient = new BrandingClient($client, $this->cache); + $promise = $brandingClient->getContentAsync('br-123'); + $promise->wait(true); + } + /** * @expectedException BBC\BrandingClient\BrandingException * @expectedExceptionMessage Invalid Branding Response. Response JSON object was invalid or malformed