Skip to content

Commit 95e5fd3

Browse files
authored
Merge pull request #31 from traderinteractive/master-memcache
Add memcache memoize support
2 parents dae01c0 + 6d11f67 commit 95e5fd3

13 files changed

Lines changed: 284 additions & 50 deletions

.coveralls.yml

Lines changed: 0 additions & 3 deletions
This file was deleted.

.gitattributes

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@
77
/.github export-ignore
88
/.gitattributes export-ignore
99
/.gitignore export-ignore
10+
/.travis export-ignore
1011
/.*.yml export-ignore
1112
/phpcs.xml export-ignore
1213
/phpunit.xml.dist export-ignore

.github/workflows/php.yml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@ jobs:
1111
runs-on: ubuntu-latest
1212
strategy:
1313
matrix:
14-
php-versions: ['7.0', '7.1', '7.2', '7.3', '7.4', '8.0', '8.1', '8.2']
14+
php-versions: ['7.0', '7.1', '7.2', '7.3', '7.4', '8.0', '8.1', '8.2', '8.3', '8.4']
1515
steps:
1616
- name: Checkout
1717
uses: actions/checkout@v2

.scrutinizer.yml

Lines changed: 0 additions & 22 deletions
This file was deleted.

.travis.yml

Lines changed: 0 additions & 18 deletions
This file was deleted.

README.md

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -100,6 +100,28 @@ $compute = function() {
100100
$result = $memoize->memoizeCallable('myLongOperation', $compute, 3600);
101101
```
102102

103+
### Memcache
104+
The memcache provider uses the [memcache](https://www.php.net/manual/en/book.memcache.php) library to
105+
cache the results in Memcache. It supports the `$cacheTime` parameter so that
106+
results can be recomputed after the time expires.
107+
108+
This memoizer can be used in a way that makes it persistent between processes
109+
rather than only caching computation for the current process.
110+
111+
#### Example
112+
```php
113+
$memcache = new Memcache;
114+
$memcacheInstance = $memcache->connect('127.0.0.1', 11211);
115+
$memoize = new \TraderInteractive\Memoize\Memcache($memcacheInstance);
116+
117+
$compute = function() {
118+
// Perform some long operation that you want to memoize
119+
};
120+
121+
// Cache he results of $compute for 1 hour.
122+
$result = $memoize->memoizeCallable('myLongOperation', $compute, 3600);
123+
```
124+
103125
### Memory
104126
This is a standard in-memory memoizer. It does not support `$cacheTime` at the
105127
moment and only keeps the results around as long as the memoizer is in memory.

composer.json

Lines changed: 12 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -14,20 +14,29 @@
1414
"sort-packages": true
1515
},
1616
"require": {
17-
"php": "^7.0 || ^8.0"
17+
"php": "^7.0 || ^8.0",
18+
"ext-json": "*"
1819
},
1920
"require-dev": {
2021
"php-coveralls/php-coveralls": "^1.0",
2122
"phpunit/phpunit": "^6.0 || ^7.0 || ^8.0 || ^9.0",
2223
"predis/predis": "^1.0",
23-
"squizlabs/php_codesniffer": "^3.2"
24+
"squizlabs/php_codesniffer": "^3.2",
25+
"ext-memcache": "*",
26+
"ext-memcached": "*"
2427
},
2528
"suggest": {
26-
"predis/predis": "Allows for Redis-based memoization."
29+
"predis/predis": "Allows for Redis-based memoization.",
30+
"ext-memcache": "Allows for Memcache-based memoization.",
31+
"ext-memcached": "Allows for Memcache-based memoization."
2732
},
2833
"autoload": {
2934
"psr-4": { "TraderInteractive\\Memoize\\": "src/" }
3035
},
36+
"autoload-dev": {
37+
"psr-4": { "TraderInteractiveTest\\Memoize\\": "tests/" }
38+
},
39+
3140
"scripts": {
3241
"lint": "vendor/bin/phpcs",
3342
"test": "vendor/bin/phpunit"

src/Memcache.php

Lines changed: 95 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,95 @@
1+
<?php
2+
3+
namespace TraderInteractive\Memoize;
4+
5+
/**
6+
* A memoizer that caches the results in memcache.
7+
*/
8+
class Memcache implements Memoize
9+
{
10+
/**
11+
* The memcache client
12+
*
13+
* @var \Memcache
14+
*/
15+
private $client;
16+
17+
/**
18+
* Cache refresh
19+
*
20+
* @var boolean
21+
*/
22+
private $refresh;
23+
24+
/**
25+
* Sets the memcache client.
26+
*
27+
* @param \Memcache $client The memcache client to use
28+
* @param boolean $refresh If true we will always overwrite cache even if it is already set
29+
*/
30+
public function __construct(\Memcache $client, bool $refresh = false)
31+
{
32+
$this->client = $client;
33+
$this->refresh = $refresh;
34+
}
35+
36+
/**
37+
* The value is stored in memcache as a json_encoded string,
38+
* so make sure that the value you return from $compute is json-encode-able.
39+
*
40+
* @see Memoize::memoizeCallable
41+
*
42+
* @param string $key
43+
* @param callable $compute
44+
* @param int|null $cacheTime
45+
* @param bool $refresh
46+
*
47+
* @return mixed
48+
*/
49+
public function memoizeCallable(string $key, callable $compute, int $cacheTime = null, bool $refresh = false)
50+
{
51+
if (!$this->refresh && !$refresh) {
52+
try {
53+
$cached = $this->client->get($key, $flags, $flags);
54+
if ($cached !== false && $cached != null) {
55+
$data = json_decode($cached, true);
56+
return $data['result'];
57+
}
58+
} catch (\Exception $e) {
59+
return call_user_func($compute);
60+
}
61+
}
62+
63+
$result = call_user_func($compute);
64+
65+
// If the result is false/null/empty, then there is no point in storing it in cache.
66+
if ($result === false || $result == null || empty($result)) {
67+
return $result;
68+
}
69+
70+
$this->cache($key, json_encode(['result' => $result]), $cacheTime);
71+
72+
return $result;
73+
}
74+
75+
/**
76+
* Caches the value into memcache with errors suppressed.
77+
*
78+
* @param string $key The key.
79+
* @param string $value The value.
80+
* @param int $cacheTime The optional cache time
81+
*
82+
* @return void
83+
*/
84+
private function cache(string $key, string $value, int $cacheTime = null)
85+
{
86+
try {
87+
$this->client->set($key, $value, 0, $cacheTime);
88+
} catch (\Exception $e) {
89+
// We don't want exceptions in accessing the cache to break functionality.
90+
// The cache should be as transparent as possible.
91+
// If insight is needed into these exceptions,
92+
// a better way would be by notifying an observer with the errors.
93+
}
94+
}
95+
}

tests/MemcacheMockable.php

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
<?php
2+
3+
namespace TraderInteractiveTest\Memoize;
4+
5+
class MemcacheMockable extends \Memcache
6+
{
7+
public function get($name, &$flags, &$cas)
8+
{
9+
}
10+
}

tests/MemcacheTest.php

Lines changed: 137 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,137 @@
1+
<?php
2+
3+
namespace TraderInteractiveTest\Memoize;
4+
5+
use PHPUnit\Framework\TestCase;
6+
use TraderInteractive\Memoize\Memcache;
7+
8+
/**
9+
* @coversDefaultClass \TraderInteractive\Memoize\Memcache
10+
* @covers ::<private>
11+
*/
12+
class MemcacheTest extends TestCase
13+
{
14+
/**
15+
* @test
16+
* @covers ::__construct
17+
* @covers ::memoizeCallable
18+
*/
19+
public function memoizeCallableWithCachedValue()
20+
{
21+
$count = 0;
22+
$key = 'foo';
23+
$value = 'bar';
24+
$cachedValue = json_encode(['result' => $value]);
25+
$compute = function () use (&$count, $value) {
26+
$count++;
27+
28+
return $value;
29+
};
30+
31+
32+
$client = $this->getMemcacheMock();
33+
$client->expects(
34+
$this->once()
35+
)->method('get')->with($this->equalTo($key))->will($this->returnValue($cachedValue));
36+
37+
$memoizer = new Memcache($client);
38+
39+
$this->assertSame($value, $memoizer->memoizeCallable($key, $compute));
40+
$this->assertSame(0, $count);
41+
}
42+
43+
/**
44+
* @test
45+
* @covers ::__construct
46+
* @covers ::memoizeCallable
47+
*/
48+
public function memoizeCallableWithExceptionOnGet()
49+
{
50+
$count = 0;
51+
$key = 'foo';
52+
$value = 'bar';
53+
$compute = function () use (&$count, $value) {
54+
$count++;
55+
56+
return $value;
57+
};
58+
59+
$client = $this->getMemcacheMock();
60+
$client->expects(
61+
$this->once()
62+
)->method('get')->with($this->equalTo($key))->will($this->throwException(new \Exception()));
63+
64+
$memoizer = new Memcache($client);
65+
66+
$this->assertSame($value, $memoizer->memoizeCallable($key, $compute));
67+
$this->assertSame(1, $count);
68+
}
69+
70+
/**
71+
* @test
72+
* @covers ::__construct
73+
* @covers ::memoizeCallable
74+
*/
75+
public function memoizeCallableWithUncachedKey()
76+
{
77+
$count = 0;
78+
$key = 'foo';
79+
$value = 'bar';
80+
$cachedValue = json_encode(['result' => $value]);
81+
$cacheTime = 1234;
82+
$compute = function () use (&$count, $value) {
83+
$count++;
84+
85+
return $value;
86+
};
87+
88+
$client = $this->getMemcacheMock();
89+
$client->expects($this->once())->method('get')->with($this->equalTo($key))->will($this->returnValue(null));
90+
$client->expects($this->once())->method('set')
91+
->with($this->equalTo($key), $this->equalTo($cachedValue), $this->equalTo(0), $this->equalTo($cacheTime));
92+
93+
$memoizer = new Memcache($client);
94+
95+
$this->assertSame($value, $memoizer->memoizeCallable($key, $compute, $cacheTime));
96+
$this->assertSame(1, $count);
97+
}
98+
99+
/**
100+
* @test
101+
* @covers ::__construct
102+
* @covers ::memoizeCallable
103+
*/
104+
public function memoizeCallableWithUncachedKeyWithExceptionOnSet()
105+
{
106+
$count = 0;
107+
$key = 'foo';
108+
$value = 'bar';
109+
$cachedValue = json_encode(['result' => $value]);
110+
$compute = function () use (&$count, $value) {
111+
$count++;
112+
113+
return $value;
114+
};
115+
116+
$client = $this->getMemcacheMock();
117+
$client->expects(
118+
$this->once()
119+
)->method('get')->with($this->equalTo($key))->will($this->returnValue(null));
120+
$setExpectation = $client->expects(
121+
$this->once()
122+
)->method('set')->with($this->equalTo($key), $this->equalTo($cachedValue));
123+
$setExpectation->will($this->throwException(new \Exception()));
124+
125+
$memoizer = new Memcache($client);
126+
127+
$this->assertSame($value, $memoizer->memoizeCallable($key, $compute));
128+
$this->assertSame(1, $count);
129+
}
130+
131+
public function getMemcacheMock() : \Memcache
132+
{
133+
$isOlderPHPVersion = PHP_VERSION_ID < 80000;
134+
$memcacheClass = $isOlderPHPVersion ? MemcacheMockable::class : \Memcache::class;
135+
return $this->getMockBuilder($memcacheClass)->setMethods(['get', 'set'])->getMock();
136+
}
137+
}

0 commit comments

Comments
 (0)