Skip to content

Commit

Permalink
Add ability to make asynchronous HTTP requests and return Guzzle prom…
Browse files Browse the repository at this point in the history
…ises (#15)

* Add the ability to return a promise instead of doing the request immediately
  • Loading branch information
BjornTwachtmann authored Jan 18, 2018
1 parent c621ad7 commit 2d8e16f
Show file tree
Hide file tree
Showing 3 changed files with 145 additions and 44 deletions.
148 changes: 104 additions & 44 deletions src/BrandingClient.php
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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;
}

/**
Expand All @@ -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)
Expand Down Expand Up @@ -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
);
}
}
11 changes: 11 additions & 0 deletions src/BrandingStubClient.php
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
namespace BBC\BrandingClient;

use GuzzleHttp\Client;
use GuzzleHttp\Promise\FulfilledPromise;
use Psr\Cache\CacheItemPoolInterface;

class BrandingStubClient extends BrandingClient
Expand All @@ -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(
'<branding-head/>',
Expand Down
30 changes: 30 additions & 0 deletions tests/BrandingClientTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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
Expand Down

0 comments on commit 2d8e16f

Please sign in to comment.