From 3ca33bc78b3f1a0c020a0b5408c663065ee13ab3 Mon Sep 17 00:00:00 2001 From: Alexandre Daubois <2144837+alexandre-daubois@users.noreply.github.com> Date: Mon, 26 Feb 2024 13:29:34 +0100 Subject: [PATCH] Add APCu cache backend --- .github/workflows/ci.yaml | 71 ++++++++-------- README.md | 13 ++- composer.json | 3 + src/Cache/ApcuCache.php | 120 +++++++++++++++++++++++++++ src/Cache/PersistentCache.php | 4 + tests/Cache/ApcuCacheTest.php | 148 ++++++++++++++++++++++++++++++++++ 6 files changed, 323 insertions(+), 36 deletions(-) create mode 100644 src/Cache/ApcuCache.php create mode 100644 tests/Cache/ApcuCacheTest.php diff --git a/.github/workflows/ci.yaml b/.github/workflows/ci.yaml index 7c40ea8..9ff1abe 100644 --- a/.github/workflows/ci.yaml +++ b/.github/workflows/ci.yaml @@ -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 diff --git a/README.md b/README.md index abe845c..d8bcde2 100644 --- a/README.md +++ b/README.md @@ -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: @@ -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. diff --git a/composer.json b/composer.json index 8893740..74f47af 100644 --- a/composer.json +++ b/composer.json @@ -29,6 +29,9 @@ }, "sort-packages": true }, + "suggest": { + "ext-apcu": "To use the APCu cache backend" + }, "bin": [ "phikl" ], diff --git a/src/Cache/ApcuCache.php b/src/Cache/ApcuCache.php new file mode 100644 index 0000000..91c57db --- /dev/null +++ b/src/Cache/ApcuCache.php @@ -0,0 +1,120 @@ +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 $keys + * + * @return array + */ + public function getMultiple(iterable $keys, mixed $default = null): array + { + $entries = []; + foreach ($keys as $key) { + $entries[$key] = $this->get($key, $default); + } + + return $entries; + } + + /** + * @param iterable $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 $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); + } +} diff --git a/src/Cache/PersistentCache.php b/src/Cache/PersistentCache.php index bd3eb4e..aa0ac38 100644 --- a/src/Cache/PersistentCache.php +++ b/src/Cache/PersistentCache.php @@ -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'; diff --git a/tests/Cache/ApcuCacheTest.php b/tests/Cache/ApcuCacheTest.php new file mode 100644 index 0000000..0ecf6ba --- /dev/null +++ b/tests/Cache/ApcuCacheTest.php @@ -0,0 +1,148 @@ +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')); + } +}