Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions .gitattributes
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@
/.github export-ignore
/.gitattributes export-ignore
/.gitignore export-ignore
/.travis export-ignore
/.*.yml export-ignore
/phpcs.xml export-ignore
/phpunit.xml.dist export-ignore
Expand Down
4 changes: 4 additions & 0 deletions .travis.yml
Original file line number Diff line number Diff line change
Expand Up @@ -7,11 +7,15 @@ php:
env:
- PREFER_LOWEST="--prefer-lowest --prefer-stable"
- PREFER_LOWEST=""
services:
- memcached
matrix:
fast_finish: true
allow_failures:
- php: nightly
before_script:
- phpenv config-add .travis/memcached.ini
- phpenv config-add .travis/memcache.ini
- composer update $PREFER_LOWEST
script:
- ./vendor/bin/phpunit --coverage-clover clover.xml
Expand Down
1 change: 1 addition & 0 deletions .travis/memcache.ini
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
extension=memcache.so
1 change: 1 addition & 0 deletions .travis/memcached.ini
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
extension=memcached.so
22 changes: 22 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -100,6 +100,28 @@ $compute = function() {
$result = $memoize->memoizeCallable('myLongOperation', $compute, 3600);
```

### Memcache
The memcache provider uses the [memcache](https://www.php.net/manual/en/book.memcache.php) library to
cache the results in Memcache. It supports the `$cacheTime` parameter so that
results can be recomputed after the time expires.

This memoizer can be used in a way that makes it persistent between processes
rather than only caching computation for the current process.

#### Example
```php
$memcache = new Memcache;
$memcacheInstance = $memcache->connect('127.0.0.1', 11211);
$memoize = new \TraderInteractive\Memoize\Memcache($memcacheInstance);

$compute = function() {
// Perform some long operation that you want to memoize
};

// Cache he results of $compute for 1 hour.
$result = $memoize->memoizeCallable('myLongOperation', $compute, 3600);
```

### Memory
This is a standard in-memory memoizer. It does not support `$cacheTime` at the
moment and only keeps the results around as long as the memoizer is in memory.
Expand Down
11 changes: 8 additions & 3 deletions composer.json
Original file line number Diff line number Diff line change
Expand Up @@ -14,16 +14,21 @@
"sort-packages": true
},
"require": {
"php": "^7.0 || ^8.0"
"php": "^7.0 || ^8.0",
"ext-json": "*"
},
"require-dev": {
"php-coveralls/php-coveralls": "^1.0",
"phpunit/phpunit": "^6.0 || ^7.0 || ^8.0 || ^9.0",
"predis/predis": "^1.0",
"squizlabs/php_codesniffer": "^3.2"
"squizlabs/php_codesniffer": "^3.2",
"ext-memcache": "*",
"ext-memcached": "*"
},
"suggest": {
"predis/predis": "Allows for Redis-based memoization."
"predis/predis": "Allows for Redis-based memoization.",
"ext-memcache": "Allows for Memcache-based memoization.",
"ext-memcached": "Allows for Memcache-based memoization."
},
"autoload": {
"psr-4": { "TraderInteractive\\Memoize\\": "src/" }
Expand Down
95 changes: 95 additions & 0 deletions src/Memcache.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,95 @@
<?php

namespace TraderInteractive\Memoize;

/**
* A memoizer that caches the results in memcache.
*/
class Memcache implements Memoize
{
/**
* The memcache client
*
* @var \Memcache
*/
private $client;

/**
* Cache refresh
*
* @var boolean
*/
private $refresh;

/**
* Sets the memcache client.
*
* @param \Memcache $client The memcache client to use
* @param boolean $refresh If true we will always overwrite cache even if it is already set
*/
public function __construct(\Memcache $client, bool $refresh = false)
{
$this->client = $client;
$this->refresh = $refresh;
}

/**
* The value is stored in memcache as a json_encoded string,
* so make sure that the value you return from $compute is json-encode-able.
*
* @see Memoize::memoizeCallable
*
* @param string $key
* @param callable $compute
* @param int|null $cacheTime
* @param bool $refresh
*
* @return mixed
*/
public function memoizeCallable(string $key, callable $compute, int $cacheTime = null, bool $refresh = false)
{
if (!$this->refresh && !$refresh) {
try {
$cached = $this->client->get($key, $flags, $flags);
if ($cached !== false && $cached != null) {
$data = json_decode($cached, true);
return $data['result'];
}
} catch (\Exception $e) {
return call_user_func($compute);
}
}

$result = call_user_func($compute);

// If the result is false/null/empty, then there is no point in storing it in cache.
if ($result === false || $result == null || empty($result)) {
return $result;
}

$this->cache($key, json_encode(['result' => $result]), $cacheTime);

return $result;
}

/**
* Caches the value into memcache with errors suppressed.
*
* @param string $key The key.
* @param string $value The value.
* @param int $cacheTime The optional cache time
*
* @return void
*/
private function cache(string $key, string $value, int $cacheTime = null)
{
try {
$this->client->set($key, $value, 0, $cacheTime);
} catch (\Exception $e) {
// We don't want exceptions in accessing the cache to break functionality.
// The cache should be as transparent as possible.
// If insight is needed into these exceptions,
// a better way would be by notifying an observer with the errors.
}
}
}
134 changes: 134 additions & 0 deletions tests/MemcacheTest.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,134 @@
<?php

namespace TraderInteractive\Memoize;

use PHPUnit\Framework\TestCase;

/**
* @coversDefaultClass \TraderInteractive\Memoize\Memcache
* @covers ::<private>
*/
class MemcacheTest extends TestCase
{
/**
* @test
* @covers ::__construct
* @covers ::memoizeCallable
*/
public function memoizeCallableWithCachedValue()
{
$count = 0;
$key = 'foo';
$value = 'bar';
$cachedValue = json_encode(['result' => $value]);
$compute = function () use (&$count, $value) {
$count++;

return $value;
};


$client = $this->getMemcacheMock();
$client->expects(
$this->once()
)->method('get')->with($this->equalTo($key))->will($this->returnValue($cachedValue));

$memoizer = new Memcache($client);

$this->assertSame($value, $memoizer->memoizeCallable($key, $compute));
$this->assertSame(0, $count);
}

/**
* @test
* @covers ::__construct
* @covers ::memoizeCallable
*/
public function memoizeCallableWithExceptionOnGet()
{
$count = 0;
$key = 'foo';
$value = 'bar';
$compute = function () use (&$count, $value) {
$count++;

return $value;
};

$client = $this->getMemcacheMock();
$client->expects(
$this->once()
)->method('get')->with($this->equalTo($key))->will($this->throwException(new \Exception()));

$memoizer = new Memcache($client);

$this->assertSame($value, $memoizer->memoizeCallable($key, $compute));
$this->assertSame(1, $count);
}

/**
* @test
* @covers ::__construct
* @covers ::memoizeCallable
*/
public function memoizeCallableWithUncachedKey()
{
$count = 0;
$key = 'foo';
$value = 'bar';
$cachedValue = json_encode(['result' => $value]);
$cacheTime = 1234;
$compute = function () use (&$count, $value) {
$count++;

return $value;
};

$client = $this->getMemcacheMock();
$client->expects($this->once())->method('get')->with($this->equalTo($key))->will($this->returnValue(null));
$client->expects($this->once())->method('set')
->with($this->equalTo($key), $this->equalTo($cachedValue), $this->equalTo(0), $this->equalTo($cacheTime));

$memoizer = new Memcache($client);

$this->assertSame($value, $memoizer->memoizeCallable($key, $compute, $cacheTime));
$this->assertSame(1, $count);
}

/**
* @test
* @covers ::__construct
* @covers ::memoizeCallable
*/
public function memoizeCallableWithUncachedKeyWithExceptionOnSet()
{
$count = 0;
$key = 'foo';
$value = 'bar';
$cachedValue = json_encode(['result' => $value]);
$compute = function () use (&$count, $value) {
$count++;

return $value;
};

$client = $this->getMemcacheMock();
$client->expects(
$this->once()
)->method('get')->with($this->equalTo($key))->will($this->returnValue(null));
$setExpectation = $client->expects(
$this->once()
)->method('set')->with($this->equalTo($key), $this->equalTo($cachedValue));
$setExpectation->will($this->throwException(new \Exception()));

$memoizer = new Memcache($client);

$this->assertSame($value, $memoizer->memoizeCallable($key, $compute));
$this->assertSame(1, $count);
}

public function getMemcacheMock() : \Memcache
{
return $this->getMockBuilder('\Memcache')->setMethods(['get', 'set'])->getMock();
}
}
Loading