Skip to content

Commit e484fae

Browse files
markchallonerBilge
authored andcommitted
Added cache key generator interface and default implementation. (#32)
1 parent 506e1c6 commit e484fae

File tree

7 files changed

+198
-11
lines changed

7 files changed

+198
-11
lines changed

README.md

Lines changed: 23 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -190,7 +190,9 @@ Caching
190190

191191
Caching is available at the connector level if the connector implements `CacheToggle`. Connectors typically extend `CachingConnector` which implements [PSR-6][PSR-6]-compatible caching. Porter ships with just one cache implementation, `MemoryCache`, which stores data in memory but this can be substituted for any PSR-6 cache if the connector permits it.
192192

193-
When available, the connector caches raw responses for each unique cache key. The cache key is comprised of the source and options parameters passed to `Connector::fetch`. Options are sorted before the cache key is created so the order of options are insignificant.
193+
When available, the connector caches raw responses for each unique [cache key](#cache-key). Cache keys are generated by an implementation-defined strategy or the default `JsonCacheKeyGenerator` strategy.
194+
195+
### Cache advice
194196

195197
Caching behaviour is specified by one of the `CacheAdvice` enumeration constants listed below.
196198

@@ -212,6 +214,26 @@ $records = $porter->import(
212214
);
213215
```
214216

217+
### Cache key
218+
219+
The cache key can optionally be generated by an implementation of `CacheKeyGeneratorInterface` if the connector permits it. This implementation should provide one method `generateCacheKey` which returns a [PSR-6][PSR-6]-compatible cache key.
220+
221+
The default implementation `JsonCacheKeyGenerator` generates keys comprised of the source and options parameters passed to `Connector::fetch`. Options are sorted before the cache key is created so the order of options are insignificant.
222+
223+
#### Implementation example
224+
225+
The following example demonstrates a simple cache key generation implementation using an md5 hash of the json encoded parameters.
226+
227+
```php
228+
class MyCacheKeyGenerator implements CacheKeyGenerator
229+
{
230+
public function generateCacheKey($source, array $sortedOptions)
231+
{
232+
return md5(json_encode([$source, $optionsSorted]));
233+
}
234+
}
235+
```
236+
215237
Architecture
216238
------------
217239

src/Cache/CacheKeyGenerator.php

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
<?php
2+
namespace ScriptFUSION\Porter\Cache;
3+
4+
interface CacheKeyGenerator
5+
{
6+
/**
7+
* @param string $source
8+
* @param array $sortedOptions Options sorted by key.
9+
*
10+
* @return string A PSR-6 compatible cache key.
11+
*/
12+
public function generateCacheKey($source, array $sortedOptions);
13+
}
Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
<?php
2+
namespace ScriptFUSION\Porter\Cache;
3+
4+
class InvalidCacheKeyException extends \RuntimeException implements \Psr\Cache\InvalidArgumentException
5+
{
6+
// Intentionally empty.
7+
}
Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
<?php
2+
namespace ScriptFUSION\Porter\Cache;
3+
4+
use ScriptFUSION\Porter\Connector\CachingConnector;
5+
6+
class JsonCacheKeyGenerator implements CacheKeyGenerator
7+
{
8+
public function generateCacheKey($source, array $sortedOptions)
9+
{
10+
return str_replace(
11+
str_split(CachingConnector::RESERVED_CHARACTERS),
12+
'.',
13+
json_encode([$source, $sortedOptions], JSON_UNESCAPED_SLASHES)
14+
);
15+
}
16+
}

src/Connector/CachingConnector.php

Lines changed: 54 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,10 @@
22
namespace ScriptFUSION\Porter\Connector;
33

44
use Psr\Cache\CacheItemPoolInterface;
5+
use ScriptFUSION\Porter\Cache\CacheKeyGenerator;
56
use ScriptFUSION\Porter\Cache\CacheToggle;
7+
use ScriptFUSION\Porter\Cache\InvalidCacheKeyException;
8+
use ScriptFUSION\Porter\Cache\JsonCacheKeyGenerator;
69
use ScriptFUSION\Porter\Cache\MemoryCache;
710
use ScriptFUSION\Porter\Options\EncapsulatedOptions;
811

@@ -11,6 +14,8 @@
1114
*/
1215
abstract class CachingConnector implements Connector, CacheToggle
1316
{
17+
const RESERVED_CHARACTERS = '{}()/\@:';
18+
1419
/**
1520
* @var CacheItemPoolInterface
1621
*/
@@ -21,28 +26,42 @@ abstract class CachingConnector implements Connector, CacheToggle
2126
*/
2227
private $cacheEnabled = true;
2328

24-
public function __construct(CacheItemPoolInterface $cache = null)
29+
/**
30+
* @var CacheKeyGenerator
31+
*/
32+
private $cacheKeyGenerator;
33+
34+
public function __construct(CacheItemPoolInterface $cache = null, CacheKeyGenerator $cacheKeyGenerator = null)
2535
{
2636
$this->cache = $cache ?: new MemoryCache;
37+
$this->cacheKeyGenerator = $cacheKeyGenerator ?: new JsonCacheKeyGenerator;
2738
}
2839

40+
/**
41+
* @param string $source
42+
* @param EncapsulatedOptions|null $options
43+
*
44+
* @return mixed
45+
*
46+
* @throws InvalidCacheKeyException
47+
*/
2948
public function fetch($source, EncapsulatedOptions $options = null)
3049
{
31-
$optionsCopy = $options ? $options->copy() : [];
32-
3350
if ($this->isCacheEnabled()) {
51+
$optionsCopy = $options ? $options->copy() : [];
52+
3453
ksort($optionsCopy);
3554

36-
$hash = $this->hash([$source, $optionsCopy]);
55+
$key = $this->validateCacheKey($this->getCacheKeyGenerator()->generateCacheKey($source, $optionsCopy));
3756

38-
if ($this->cache->hasItem($hash)) {
39-
return $this->cache->getItem($hash)->get();
57+
if ($this->cache->hasItem($key)) {
58+
return $this->cache->getItem($key)->get();
4059
}
4160
}
4261

4362
$data = $this->fetchFreshData($source, $options);
4463

45-
isset($hash) && $this->cache->save($this->cache->getItem($hash)->set($data));
64+
isset($key) && $this->cache->save($this->cache->getItem($key)->set($data));
4665

4766
return $data;
4867
}
@@ -74,8 +93,34 @@ public function isCacheEnabled()
7493
return $this->cacheEnabled;
7594
}
7695

77-
private function hash(array $structure)
96+
public function getCacheKeyGenerator()
7897
{
79-
return str_replace(str_split('{}()/\@:'), '.', json_encode($structure, JSON_UNESCAPED_SLASHES));
98+
return $this->cacheKeyGenerator;
99+
}
100+
101+
public function setCacheKeyGenerator(CacheKeyGenerator $cacheKeyGenerator)
102+
{
103+
$this->cacheKeyGenerator = $cacheKeyGenerator;
104+
}
105+
106+
/**
107+
* @param mixed $key
108+
*
109+
* @return string
110+
*
111+
* @throws InvalidCacheKeyException
112+
*/
113+
private function validateCacheKey($key)
114+
{
115+
if (!is_string($key)) {
116+
throw new InvalidCacheKeyException('Cache key must be of type string.');
117+
}
118+
if (strpbrk($key, self::RESERVED_CHARACTERS) !== false) {
119+
throw new InvalidCacheKeyException(
120+
sprintf('Cache key "%s" contains one or more reserved characters: "%s"', $key, self::RESERVED_CHARACTERS)
121+
);
122+
}
123+
124+
return $key;
80125
}
81126
}

test/Integration/Porter/Connector/CachingConnectorTest.php

Lines changed: 66 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,9 @@
55
use Mockery\MockInterface;
66
use Psr\Cache\CacheItemInterface;
77
use Psr\Cache\CacheItemPoolInterface;
8+
use ScriptFUSION\Porter\Cache\CacheKeyGenerator;
9+
use ScriptFUSION\Porter\Cache\InvalidCacheKeyException;
10+
use ScriptFUSION\Porter\Cache\JsonCacheKeyGenerator;
811
use ScriptFUSION\Porter\Cache\MemoryCache;
912
use ScriptFUSION\Porter\Connector\CachingConnector;
1013
use ScriptFUSION\Porter\Options\EncapsulatedOptions;
@@ -53,6 +56,15 @@ public function testGetSetCache()
5356
self::assertSame($cache, $this->connector->getCache());
5457
}
5558

59+
public function testGetSetCacheKeyGenerator()
60+
{
61+
self::assertInstanceOf(CacheKeyGenerator::class, $this->connector->getCacheKeyGenerator());
62+
self::assertNotSame($cacheKeyGenerator = new JsonCacheKeyGenerator, $this->connector->getCacheKeyGenerator());
63+
64+
$this->connector->setCacheKeyGenerator($cacheKeyGenerator);
65+
self::assertSame($cacheKeyGenerator, $this->connector->getCacheKeyGenerator());
66+
}
67+
5668
public function testCacheBypassedForDifferentOptions()
5769
{
5870
self::assertSame('foo', $this->connector->fetch('baz', $this->options));
@@ -68,6 +80,59 @@ public function testCacheUsedForDifferentOptionsInstance()
6880
self::assertSame('foo', $this->connector->fetch('baz', clone $this->options));
6981
}
7082

83+
public function testCacheUsedForCacheKeyGenerator()
84+
{
85+
$this->connector->setCacheKeyGenerator(
86+
\Mockery::mock(CacheKeyGenerator::class)
87+
->shouldReceive('generateCacheKey')
88+
->with('quux', $this->options->copy())
89+
->andReturn('quuz', 'quuz', 'corge')
90+
->getMock()
91+
);
92+
93+
self::assertSame('foo', $this->connector->fetch('quux', $this->options));
94+
self::assertSame('foo', $this->connector->fetch('quux', $this->options));
95+
self::assertSame('bar', $this->connector->fetch('quux', $this->options));
96+
}
97+
98+
public function testFetchThrowsInvalidCacheKeyExceptionOnNonStringCackeKey()
99+
{
100+
$this->setExpectedException(InvalidCacheKeyException::class, 'Cache key must be of type string.');
101+
102+
$this->connector->setCacheKeyGenerator(
103+
\Mockery::mock(CacheKeyGenerator::class)
104+
->shouldReceive('generateCacheKey')
105+
->with('quux', $this->options->copy())
106+
->andReturn([])
107+
->getMock()
108+
);
109+
110+
$this->connector->fetch('quux', $this->options);
111+
}
112+
113+
public function testFetchThrowsInvalidCacheKeyExceptionOnNonPSR6CompliantCacheKey()
114+
{
115+
$cacheKey = CachingConnector::RESERVED_CHARACTERS;
116+
117+
$this->setExpectedException(
118+
InvalidCacheKeyException::class,
119+
sprintf('Cache key "%s" contains one or more reserved characters: "%s"',
120+
$cacheKey,
121+
CachingConnector::RESERVED_CHARACTERS
122+
)
123+
);
124+
125+
$this->connector->setCacheKeyGenerator(
126+
\Mockery::mock(CacheKeyGenerator::class)
127+
->shouldReceive('generateCacheKey')
128+
->with('quux', $this->options->copy())
129+
->andReturn($cacheKey)
130+
->getMock()
131+
);
132+
133+
$this->connector->fetch('quux', $this->options);
134+
}
135+
71136
public function testNullAndEmptyAreEquivalent()
72137
{
73138
/** @var EncapsulatedOptions $options */
@@ -92,7 +157,7 @@ public function testEnableCache()
92157

93158
public function testCacheKeyExcludesReservedCharacters()
94159
{
95-
$reservedCharacters = '{}()/\@:';
160+
$reservedCharacters = CachingConnector::RESERVED_CHARACTERS;
96161

97162
$this->connector->setCache($cache = \Mockery::spy(CacheItemPoolInterface::class));
98163

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
<?php
2+
namespace ScriptFUSIONTest\Unit\Porter\Cache;
3+
4+
use ScriptFUSION\Porter\Cache\JsonCacheKeyGenerator;
5+
use ScriptFUSIONTest\Stubs\TestOptions;
6+
7+
final class JsonCacheKeyGeneratorTest extends \PHPUnit_Framework_TestCase
8+
{
9+
public function testGenerateCacheKey()
10+
{
11+
$options = new TestOptions;
12+
$options->setFoo('(baz@quz\quux/quuz)');
13+
14+
self::assertSame(
15+
'["bar",."foo".".baz.quz..quux.quuz.".]',
16+
(new JsonCacheKeyGenerator)->generateCacheKey('bar', $options->copy())
17+
);
18+
}
19+
}

0 commit comments

Comments
 (0)