Skip to content

Commit

Permalink
Add APCu cache backend
Browse files Browse the repository at this point in the history
  • Loading branch information
alexandre-daubois committed Feb 26, 2024
1 parent a05b6fe commit 3ca33bc
Show file tree
Hide file tree
Showing 6 changed files with 323 additions and 36 deletions.
71 changes: 36 additions & 35 deletions .github/workflows/ci.yaml
Expand Up @@ -13,38 +13,39 @@ jobs:
runs-on: ubuntu-latest

steps:
- uses: actions/checkout@v3

- name: Setup PHP with fail-fast
uses: shivammathur/setup-php@v2
with:
php-version: '8.2'
env:
fail-fast: true

- name: Validate composer.json and composer.lock
run: composer validate --strict

- name: Cache Composer packages
id: composer-cache
uses: actions/cache@v3
with:
path: vendor
key: ${{ runner.os }}-php-${{ hashFiles('**/composer.lock') }}
restore-keys: |
${{ runner.os }}-php-
- name: Install dependencies
run: composer install --prefer-dist --no-progress

- name: Install PKL
run: ./phikl install

- name: PHPCS Fixer
run: composer run-script cs

- name: PHPStan
run: composer run-script stan

- name: Run test suite
run: composer run-script test
- uses: actions/checkout@v3

- name: Setup PHP with fail-fast
uses: shivammathur/setup-php@v2
with:
php-version: '8.2'
extensions: apcu
ini-values: |
apc.enable_cli=1
- name: Validate composer.json and composer.lock
run: composer validate --strict

- name: Cache Composer packages
id: composer-cache
uses: actions/cache@v3
with:
path: vendor
key: ${{ runner.os }}-php-${{ hashFiles('**/composer.lock') }}
restore-keys: |
${{ runner.os }}-php-
- name: Install dependencies
run: composer install --prefer-dist --no-progress

- name: Install PKL
run: ./phikl install

- name: PHPCS Fixer
run: composer run-script cs

- name: PHPStan
run: composer run-script stan

- name: Run test suite
run: composer run-script test
13 changes: 12 additions & 1 deletion README.md
Expand Up @@ -208,10 +208,14 @@ name in the PHP class.
## Caching

You can (**and should**) cache the PKL modules to improve performance. This is especially useful when evaluating the same PKL file
multiple times. You can use the `warmup` command to dump the PKL modules to a cache file. Phikl will then use the cache file automatically when evaluating a PKL file. If the PKL file is not found in the cache, Phikl will evaluate the PKL file on the go.
multiple times.

**⚠️ Using Phikl with the cache avoids the PKL CLI tool to be executed to evaluate modules and should be done when deploying your application for better performances.**

### Warmup the Cache

You can use the `warmup` command to dump the PKL modules to a cache file by default. Phikl will then use the cache file automatically when evaluating a PKL file. If the PKL file is not found in the cache, Phikl will evaluate the PKL file on the go.

Phikl will go through all `.pkl` files of your project and dump them to the cache file.

Here's an example of how to use the `warmup` command:
Expand Down Expand Up @@ -240,4 +244,11 @@ Here are a few things to note about Phikl cache:
- Phikl will automatically refresh the cache if a PKL module is modified since last warmup
- Any corrupted cache entry will be automatically refreshed

### Cache Backends

If you have your own cache system, you can use the `Pkl::setCache()` method to set the cache system to use. You can pass it any instance of compliant PSR-16 cache system implementing `Psr\SimpleCache\CacheInterface`. This is useful you want to use, for example, a Redis server as a cache system for your Pkl modules.

Phikl comes with the following cache backends:

* `PersistentCache`, which is the default one used by Phikl. It uses a file to store the cache.
* `APCuCache`, which uses the APCu extension to store the cache in memory.
3 changes: 3 additions & 0 deletions composer.json
Expand Up @@ -29,6 +29,9 @@
},
"sort-packages": true
},
"suggest": {
"ext-apcu": "To use the APCu cache backend"
},
"bin": [
"phikl"
],
Expand Down
120 changes: 120 additions & 0 deletions src/Cache/ApcuCache.php
@@ -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);
}
}
4 changes: 4 additions & 0 deletions src/Cache/PersistentCache.php
Expand Up @@ -13,6 +13,10 @@
use Phikl\Exception\EmptyCacheException;
use Psr\SimpleCache\CacheInterface;

/**
* Simple implementation of the PSR-16 CacheInterface using a file for
* Pkl modules evaluation cache.
*/
final class PersistentCache implements CacheInterface
{
private const DEFAULT_CACHE_FILE = '.phikl.cache';
Expand Down
148 changes: 148 additions & 0 deletions tests/Cache/ApcuCacheTest.php
@@ -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'));
}
}

0 comments on commit 3ca33bc

Please sign in to comment.