Skip to content

Commit 51c1c17

Browse files
authored
Merge pull request #49 from chadicus/fea/mongo-cache
Add MongoDB Caching Support
2 parents ee9f14c + 599c4a6 commit 51c1c17

10 files changed

Lines changed: 410 additions & 8 deletions

.travis.yml

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,8 @@ php:
44
- 7.1
55
- 7.2
66
- nightly
7+
services:
8+
- mongodb
79
env:
810
- PREFER_LOWEST="--prefer-lowest --prefer-stable"
911
- PREFER_LOWEST=""
@@ -12,6 +14,8 @@ matrix:
1214
allow_failures:
1315
- php: nightly
1416
before_script:
17+
- composer self-update
18+
- yes '' | pecl install -f mongodb-1.1
1519
- composer update $PREFER_LOWEST
1620
script:
1721
- ./vendor/bin/phpunit --coverage-clover clover.xml

composer.json

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -35,10 +35,15 @@
3535
"config": {
3636
"sort-packages": true
3737
},
38+
"suggest": {
39+
"subjective-php/psr-cache-mongodb": "Used for Caching with Mongo"
40+
},
3841
"require-dev": {
42+
"helmich/mongomock": "^2.1",
3943
"php-coveralls/php-coveralls": "^2.1",
4044
"phpunit/phpunit": "^6.5.2",
41-
"squizlabs/php_codesniffer": "^3.2"
45+
"squizlabs/php_codesniffer": "^3.2",
46+
"subjective-php/psr-cache-mongodb": "^2.1"
4247
},
4348
"autoload": {
4449
"psr-4": { "TraderInteractive\\Api\\": "src" }

src/CacheFactory.php

Lines changed: 52 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,52 @@
1+
<?php
2+
3+
namespace TraderInteractive\Api;
4+
5+
use MongoDB;
6+
use Psr\SimpleCache\CacheInterface;
7+
use SubjectivePHP\Psr\SimpleCache\InMemoryCache;
8+
use SubjectivePHP\Psr\SimpleCache\MongoCache;
9+
use SubjectivePHP\Psr\SimpleCache\NullCache;
10+
11+
final class CacheFactory
12+
{
13+
/**
14+
* @param string $name The name of the cache object to make.
15+
* @param array $config Options to use when constructing the cache.
16+
*
17+
* @return CacheInterface
18+
*/
19+
public static function make(string $name, array $config) : CacheInterface
20+
{
21+
if ($name === MongoCache::class) {
22+
return self::getMongoCache($config);
23+
}
24+
25+
if ($name === InMemoryCache::class) {
26+
return new InMemoryCache();
27+
}
28+
29+
if ($name === NullCache::class) {
30+
return new NullCache();
31+
}
32+
33+
throw new \RuntimeException("Cannot create cache instance of '{$name}'");
34+
}
35+
36+
private static function getMongoCache(array $config) : MongoCache
37+
{
38+
return new MongoCache(self::getMongoCollectionFromConfig($config), new ResponseSerializer());
39+
}
40+
41+
private static function getMongoCollectionFromConfig(array $config) : MongoDB\Collection
42+
{
43+
$collection = $config['collection'];
44+
if ($collection instanceof MongoDB\Collection) {
45+
return $collection;
46+
}
47+
48+
$uri = $config['uri'];
49+
$database = $config['database'];
50+
return (new MongoDB\Client($uri))->selectDatabase($database)->selectCollection($collection);
51+
}
52+
}

src/Collection.php

Lines changed: 4 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -156,12 +156,10 @@ public function next()
156156
$indexResponse = $this->client->index($this->resource, $this->filters);
157157

158158
$httpCode = $indexResponse->getHttpCode();
159-
Util::ensure(
160-
200,
161-
$httpCode,
162-
Exception::class,
163-
["Did not receive 200 from API. Instead received {$httpCode}", $indexResponse]
164-
);
159+
$expectedHttpCode = 200;
160+
$exceptionArgs = ["Did not receive 200 from API. Instead received {$httpCode}", $indexResponse];
161+
162+
Util::ensure($expectedHttpCode, $httpCode, Exception::class, $exceptionArgs);
165163

166164
$response = $indexResponse->getResponse();
167165
$this->limit = $response['pagination']['limit'];

src/Response.php

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -96,7 +96,7 @@ private static function decodeBody(string $json) : array
9696
} finally {
9797
self::ensureJson();
9898
}
99-
}//@codeCoverageIgnore Unreachable line
99+
}//@codeCoverageIgnore
100100

101101
private static function ensureJson()
102102
{

src/ResponseSerializer.php

Lines changed: 77 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,77 @@
1+
<?php
2+
3+
namespace TraderInteractive\Api;
4+
5+
use GuzzleHttp\Psr7;
6+
use Psr\Http\Message\ResponseInterface;
7+
use SubjectivePHP\Psr\SimpleCache\Serializer\SerializerInterface;
8+
9+
final class ResponseSerializer implements SerializerInterface
10+
{
11+
/**
12+
* @var array
13+
*/
14+
const REQUIRED_CACHE_KEYS = [
15+
'statusCode',
16+
'headers',
17+
'body',
18+
];
19+
20+
/**
21+
* Unserializes cached data into the original psr response.
22+
*
23+
* @param mixed $data The data to unserialize.
24+
*
25+
* @return Psr7\Response
26+
*
27+
* @throws SerializerException Thrown if the given value cannot be unserialized.
28+
*/
29+
public function unserialize($data)
30+
{
31+
$this->validateCachedData($data);
32+
$statusCode = $data['statusCode'];
33+
$headers = $data['headers'];
34+
$body = $data['body'];
35+
if ($body !== '') {
36+
$body = Psr7\stream_for($body);
37+
}
38+
39+
return new Psr7\Response($statusCode, $headers, $body);
40+
}
41+
42+
/**
43+
* Serializes the given psr response for storage in caching.
44+
*
45+
* @param Psr7\Response $response The http response message to serialize for caching.
46+
*
47+
* @return mixed The result of serializing the given $data.
48+
*
49+
* @throws SerializerException Thrown if the given value cannot be serialized for caching.
50+
*/
51+
public function serialize($response)
52+
{
53+
if (!($response instanceof ResponseInterface)) {
54+
$type = is_object($response) ? get_class($response) : gettype($response);
55+
throw new SerializerException("Cannot serialize value of type '{$type}'");
56+
}
57+
58+
return [
59+
'statusCode' => $response->getStatusCode(),
60+
'headers' => $response->getHeaders(),
61+
'body' => $response->getBody()->getContents(),
62+
];
63+
}
64+
65+
private function validateCachedData($data)
66+
{
67+
if (!is_array($data)) {
68+
throw new SerializerException('Serialized data is not an array');
69+
}
70+
71+
foreach (self::REQUIRED_CACHE_KEYS as $key) {
72+
if (!array_key_exists($key, $data)) {
73+
throw new SerializerException("Data is missing '{$key}' value");
74+
}
75+
}
76+
}
77+
}

src/SerializerException.php

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
<?php
2+
3+
namespace TraderInteractive\Api;
4+
5+
use Psr\SimpleCache\InvalidArgumentException;
6+
use RuntimeException;
7+
8+
final class SerializerException extends RuntimeException implements InvalidArgumentException
9+
{
10+
}

tests/CacheFactoryTest.php

Lines changed: 74 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,74 @@
1+
<?php
2+
3+
namespace TraderInteractive\Api;
4+
5+
use MongoDB;
6+
use PHPUnit\Framework\TestCase;
7+
use SubjectivePHP\Psr\SimpleCache\InMemoryCache;
8+
use SubjectivePHP\Psr\SimpleCache\MongoCache;
9+
use SubjectivePHP\Psr\SimpleCache\NullCache;
10+
use SubjectivePHP\Psr\SimpleCache\RedisCache;
11+
12+
/**
13+
* @coversDefaultClass \TraderInteractive\Api\CacheFactory
14+
* @covers ::<private>
15+
*/
16+
final class CacheFactoryTest extends TestCase
17+
{
18+
/**
19+
* @param string $name The name of the cache to create.
20+
* @param array $config Config data to pass to the factory.
21+
*
22+
* @test
23+
* @covers ::make
24+
* @dataProvider provideMakeData
25+
*/
26+
public function makeCache(string $name, array $config)
27+
{
28+
$this->assertInstanceOf($name, CacheFactory::make($name, $config));
29+
}
30+
31+
/**
32+
* @return array
33+
*/
34+
public function provideMakeData() : array
35+
{
36+
return [
37+
'null cache' => [
38+
'name' => NullCache::class,
39+
'config' => [],
40+
],
41+
'in-memory cache' => [
42+
'name' => InMemoryCache::class,
43+
'config' => [],
44+
],
45+
'mongo cache with collection' => [
46+
'name' => MongoCache::class,
47+
'config' => [
48+
'collection' => $this->getMockBuilder(\MongoDB\Collection::class)
49+
->disableOriginalConstructor()
50+
->getMock(),
51+
],
52+
],
53+
'mongo cache' => [
54+
'name' => MongoCache::class,
55+
'config' => [
56+
'uri' => 'mongodb://localhost:27017',
57+
'database' => 'testing',
58+
'collection' => 'cache',
59+
],
60+
],
61+
];
62+
}
63+
64+
/**
65+
* @test
66+
* @covers ::make
67+
* @expectedException \RuntimeException
68+
* @expectedExceptionMessage Cannot create cache instance of 'Invalid'
69+
*/
70+
public function cannotMakeUnsupportedCacheInstance()
71+
{
72+
CacheFactory::make('Invalid', []);
73+
}
74+
}

tests/ClientTest.php

Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,13 +3,16 @@
33
namespace TraderInteractive\Api;
44

55
use Fig\Http\Message\StatusCodeInterface as StatusCodes;
6+
use Helmich\MongoMock\MockCollection;
67
use SubjectivePHP\Psr\SimpleCache\InMemoryCache;
78
use DominionEnterprises\Util\Arrays;
89
use DominionEnterprises\Util\Http;
910
use GuzzleHttp\Psr7\Request;
1011
use GuzzleHttp\Psr7\Response as Psr7Response;
1112
use PHPUnit\Framework\TestCase;
1213
use Psr\Http\Message\RequestInterface;
14+
use SubjectivePHP\Psr\SimpleCache\MongoCache;
15+
use SubjectivePHP\Psr\SimpleCache\RedisCache;
1316

1417
/**
1518
* Unit tests for the Client class
@@ -932,6 +935,44 @@ function (RequestInterface $request) {
932935
$this->assertEquals($expected, Response::fromPsr7Response($actual));
933936
}
934937

938+
/**
939+
* @test
940+
* @covers ::end
941+
*/
942+
public function useMongoCache()
943+
{
944+
$hasBeenCalled = false;
945+
$adapter = new FakeAdapter(
946+
function (RequestInterface $request) use (&$hasBeenCalled) {
947+
if (substr_count($request->getUri(), 'token') == 1) {
948+
return new Psr7Response(
949+
200,
950+
['Content-Type' => ['application/json']],
951+
json_encode(['access_token' => 'token', 'expires_in' => 1])
952+
);
953+
}
954+
955+
if ($hasBeenCalled) {
956+
throw new \Exception('Adapter called twice');
957+
}
958+
959+
$hasBeenCalled = true;
960+
if (substr_count($request->getUri(), 'a+url') == 1) {
961+
return new Psr7Response(200, ['header' => ['value']], json_encode(['doesnt' => 'matter']));
962+
}
963+
}
964+
);
965+
$collection = new MockCollection('cache');
966+
$cache = CacheFactory::make(MongoCache::class, ['collection' => $collection]);
967+
$client = new Client($adapter, $this->getAuthentication(), 'baseUrl', Client::CACHE_MODE_GET, $cache);
968+
$expected = $client->get('a url', 'id');
969+
$actual = $cache->get('GET|baseUrl_FSLASH_a+url_FSLASH_id|');
970+
$this->assertEquals($expected, Response::fromPsr7Response($actual));
971+
972+
$expected = $client->get('a url', 'id');
973+
$this->assertEquals($expected, Response::fromPsr7Response($actual));
974+
}
975+
935976
/**
936977
* Verify client uses in memory token only if originially pulled from cache
937978
*

0 commit comments

Comments
 (0)