-
Notifications
You must be signed in to change notification settings - Fork 0
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
- Loading branch information
1 parent
a05b6fe
commit 92ea72c
Showing
6 changed files
with
323 additions
and
36 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,120 @@ | ||
<?php | ||
|
||
namespace Phikl\Cache; | ||
|
||
use Psr\SimpleCache\CacheInterface; | ||
|
||
/** | ||
* Simple implementation of the PSR-16 CacheInterface using APCu | ||
* for the Pkl modules evaluation cache. | ||
*/ | ||
final class ApcuCache implements CacheInterface | ||
{ | ||
public function __construct() | ||
{ | ||
if (!\extension_loaded('apcu')) { | ||
throw new \RuntimeException('APCu extension is not loaded'); | ||
} | ||
|
||
if (!function_exists('apcu_enabled') || !apcu_enabled()) { | ||
throw new \RuntimeException('APCu is not enabled'); | ||
} | ||
} | ||
|
||
/** | ||
* @param non-empty-string $key | ||
*/ | ||
public function get(string $key, mixed $default = null): Entry|null | ||
{ | ||
if ($default !== null && !$default instanceof Entry) { | ||
throw new \InvalidArgumentException('Default value must be null or an instance of Entry'); | ||
} | ||
|
||
$entry = apcu_fetch($key); | ||
if ($entry === false) { | ||
return $default; | ||
} | ||
|
||
$entry = @unserialize($entry); | ||
if ($entry === false) { | ||
return $default; | ||
} | ||
|
||
return $entry; | ||
} | ||
|
||
public function set(string $key, mixed $value, \DateInterval|int|null $ttl = null): bool | ||
{ | ||
if (!$value instanceof Entry) { | ||
return false; | ||
} | ||
|
||
return apcu_store( | ||
$key, | ||
serialize($value), | ||
$ttl instanceof \DateInterval ? (int) ($ttl->format('U')) - \time() : ($ttl ?? 0) | ||
); | ||
} | ||
|
||
public function delete(string $key): bool | ||
{ | ||
return apcu_delete($key); | ||
} | ||
|
||
/** | ||
* Caution, this method will clear the entire cache, not just the cache for this application. | ||
*/ | ||
public function clear(): bool | ||
{ | ||
return apcu_clear_cache(); | ||
} | ||
|
||
/** | ||
* @param iterable<non-empty-string> $keys | ||
* | ||
* @return array<non-empty-string, Entry|null> | ||
*/ | ||
public function getMultiple(iterable $keys, mixed $default = null): array | ||
{ | ||
$entries = []; | ||
foreach ($keys as $key) { | ||
$entries[$key] = $this->get($key, $default); | ||
} | ||
|
||
return $entries; | ||
} | ||
|
||
/** | ||
* @param iterable<string, Entry> $values | ||
*/ | ||
public function setMultiple(iterable $values, \DateInterval|int|null $ttl = null): bool | ||
{ | ||
foreach ($values as $key => $value) { | ||
if (!$this->set($key, $value, $ttl)) { | ||
return false; | ||
} | ||
} | ||
|
||
return true; | ||
} | ||
|
||
/** | ||
* @param iterable<string> $keys | ||
*/ | ||
public function deleteMultiple(iterable $keys): bool | ||
{ | ||
$success = true; | ||
foreach ($keys as $key) { | ||
if (!$this->delete($key)) { | ||
$success = false; | ||
} | ||
} | ||
|
||
return $success; | ||
} | ||
|
||
public function has(string $key): bool | ||
{ | ||
return apcu_exists($key); | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,148 @@ | ||
<?php | ||
|
||
namespace Cache; | ||
|
||
use Phikl\Cache\ApcuCache; | ||
use Phikl\Cache\Entry; | ||
use PHPUnit\Framework\Attributes\CoversClass; | ||
use PHPUnit\Framework\Attributes\RequiresPhpExtension; | ||
use PHPUnit\Framework\TestCase; | ||
|
||
#[RequiresPhpExtension('apcu')] | ||
#[CoversClass(ApcuCache::class)] | ||
class ApcuCacheTest extends TestCase | ||
{ | ||
protected function tearDown(): void | ||
{ | ||
apcu_clear_cache(); | ||
} | ||
|
||
public function testGetWithDefaultOtherThanEntryInstance(): void | ||
{ | ||
$cache = new ApcuCache(); | ||
|
||
$this->expectException(\InvalidArgumentException::class); | ||
$this->expectExceptionMessage('Default value must be null or an instance of Entry'); | ||
|
||
$cache->get('key', 'invalid'); | ||
} | ||
|
||
public function testGetReturnsDefaultIfKeyDoesNotExist(): void | ||
{ | ||
$cache = new ApcuCache(); | ||
|
||
$entry = new Entry('content', 'hash', 0); | ||
|
||
$this->assertNull($cache->get('nonexistent')); | ||
$this->assertSame($entry, $cache->get('nonexistent', $entry)); | ||
$this->assertFalse($cache->has('nonexistent')); | ||
} | ||
|
||
public function testGetOnValidSetEntry(): void | ||
{ | ||
$cache = new ApcuCache(); | ||
|
||
$entry = new Entry('content', 'hash', $time = \time()); | ||
|
||
$this->assertTrue($cache->set('key', $entry)); | ||
|
||
$entry = $cache->get('key'); | ||
$this->assertInstanceOf(Entry::class, $entry); | ||
$this->assertSame('content', $entry->content); | ||
$this->assertSame('hash', $entry->hash); | ||
$this->assertSame($time, $entry->timestamp); | ||
} | ||
|
||
public function testSetReturnsFalseOnInvalidEntry(): void | ||
{ | ||
$cache = new ApcuCache(); | ||
|
||
$this->assertFalse($cache->set('key', 'invalid')); | ||
} | ||
|
||
public function testDeleteEntry(): void | ||
{ | ||
$cache = new ApcuCache(); | ||
|
||
$entry = new Entry('content', 'hash', 0); | ||
$cache->set('key', $entry); | ||
|
||
$this->assertTrue($cache->delete('key')); | ||
$this->assertNull($cache->get('key')); | ||
} | ||
|
||
public function testClear(): void | ||
{ | ||
$cache = new ApcuCache(); | ||
|
||
$entry = new Entry('content', 'hash', 0); | ||
$cache->set('key', $entry); | ||
|
||
$this->assertTrue($cache->clear()); | ||
$this->assertNull($cache->get('key')); | ||
} | ||
|
||
public function testGetSetMultiple(): void | ||
{ | ||
$cache = new ApcuCache(); | ||
|
||
$entry1 = new Entry('content1', 'hash1', 0); | ||
$entry2 = new Entry('content2', 'hash2', 0); | ||
$entry3 = new Entry('content3', 'hash3', 0); | ||
|
||
$cache->setMultiple([ | ||
'key1' => $entry1, | ||
'key2' => $entry2, | ||
'key3' => $entry3, | ||
]); | ||
|
||
$entries = $cache->getMultiple(['key1', 'key2', 'key3']); | ||
|
||
$this->assertArrayHasKey('key1', $entries); | ||
$this->assertArrayHasKey('key2', $entries); | ||
$this->assertArrayHasKey('key3', $entries); | ||
|
||
$this->assertInstanceOf(Entry::class, $entries['key1']); | ||
$this->assertSame('content1', $entries['key1']->content); | ||
$this->assertSame('hash1', $entries['key1']->hash); | ||
|
||
$this->assertInstanceOf(Entry::class, $entries['key2']); | ||
$this->assertSame('content2', $entries['key2']->content); | ||
$this->assertSame('hash2', $entries['key2']->hash); | ||
|
||
$this->assertInstanceOf(Entry::class, $entries['key3']); | ||
$this->assertSame('content3', $entries['key3']->content); | ||
$this->assertSame('hash3', $entries['key3']->hash); | ||
} | ||
|
||
public function testDeleteMultiple(): void | ||
{ | ||
$cache = new ApcuCache(); | ||
|
||
$entry1 = new Entry('content1', 'hash1', 0); | ||
$entry2 = new Entry('content2', 'hash2', 0); | ||
$entry3 = new Entry('content3', 'hash3', 0); | ||
|
||
$cache->setMultiple([ | ||
'key1' => $entry1, | ||
'key2' => $entry2, | ||
'key3' => $entry3, | ||
]); | ||
|
||
$this->assertTrue($cache->deleteMultiple(['key1', 'key2'])); | ||
$this->assertNull($cache->get('key1')); | ||
$this->assertNull($cache->get('key2')); | ||
$this->assertNotNull($cache->get('key3')); | ||
} | ||
|
||
public function testHas(): void | ||
{ | ||
$cache = new ApcuCache(); | ||
|
||
$entry = new Entry('content', 'hash', 0); | ||
$cache->set('key', $entry); | ||
|
||
$this->assertTrue($cache->has('key')); | ||
$this->assertFalse($cache->has('invalid')); | ||
} | ||
} |