Skip to content

Commit 41a92ec

Browse files
authored
feat: APCu caching driver (#9874)
1 parent af86430 commit 41a92ec

File tree

8 files changed

+323
-2
lines changed

8 files changed

+323
-2
lines changed

.github/workflows/test-phpunit.yml

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -163,7 +163,8 @@ jobs:
163163
enable-artifact-upload: ${{ matrix.php-version == needs.coverage-php-version.outputs.version }}
164164
enable-coverage: ${{ matrix.php-version == needs.coverage-php-version.outputs.version }}
165165
enable-profiling: ${{ matrix.php-version == needs.coverage-php-version.outputs.version }}
166-
extra-extensions: redis, memcached
166+
extra-extensions: redis, memcached, apcu
167+
extra-ini-options: apc.enable_cli=1
167168
extra-composer-options: ${{ matrix.composer-option }}
168169

169170
coveralls:

admin/framework/composer.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,7 @@
2727
"predis/predis": "^3.0"
2828
},
2929
"suggest": {
30+
"ext-apcu": "If you use Cache class ApcuHandler",
3031
"ext-curl": "If you use CURLRequest class",
3132
"ext-dom": "If you use TestResponse",
3233
"ext-exif": "If you run Image class tests",

app/Config/Cache.php

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33
namespace Config;
44

55
use CodeIgniter\Cache\CacheInterface;
6+
use CodeIgniter\Cache\Handlers\ApcuHandler;
67
use CodeIgniter\Cache\Handlers\DummyHandler;
78
use CodeIgniter\Cache\Handlers\FileHandler;
89
use CodeIgniter\Cache\Handlers\MemcachedHandler;
@@ -143,6 +144,7 @@ class Cache extends BaseConfig
143144
* @var array<string, class-string<CacheInterface>>
144145
*/
145146
public array $validHandlers = [
147+
'apcu' => ApcuHandler::class,
146148
'dummy' => DummyHandler::class,
147149
'file' => FileHandler::class,
148150
'memcached' => MemcachedHandler::class,

composer.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,7 @@
3535
"codeigniter4/framework": "self.version"
3636
},
3737
"suggest": {
38+
"ext-apcu": "If you use Cache class ApcuHandler",
3839
"ext-curl": "If you use CURLRequest class",
3940
"ext-dom": "If you use TestResponse",
4041
"ext-exif": "If you run Image class tests",
Lines changed: 130 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,130 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
/**
6+
* This file is part of CodeIgniter 4 framework.
7+
*
8+
* (c) CodeIgniter Foundation <admin@codeigniter.com>
9+
*
10+
* For the full copyright and license information, please view
11+
* the LICENSE file that was distributed with this source code.
12+
*/
13+
14+
namespace CodeIgniter\Cache\Handlers;
15+
16+
use APCUIterator;
17+
use Closure;
18+
use CodeIgniter\I18n\Time;
19+
use Config\Cache;
20+
21+
/**
22+
* APCu cache handler
23+
*
24+
* @see \CodeIgniter\Cache\Handlers\ApcuHandlerTest
25+
*/
26+
class ApcuHandler extends BaseHandler
27+
{
28+
/**
29+
* Note: Use `CacheFactory::getHandler()` to instantiate.
30+
*/
31+
public function __construct(Cache $config)
32+
{
33+
$this->prefix = $config->prefix;
34+
}
35+
36+
public function initialize(): void
37+
{
38+
}
39+
40+
public function get(string $key): mixed
41+
{
42+
$key = static::validateKey($key, $this->prefix);
43+
$success = false;
44+
45+
$data = apcu_fetch($key, $success);
46+
47+
// Success returned by reference from apcu_fetch()
48+
return $success ? $data : null;
49+
}
50+
51+
public function save(string $key, $value, int $ttl = 60): bool
52+
{
53+
$key = static::validateKey($key, $this->prefix);
54+
55+
return apcu_store($key, $value, $ttl);
56+
}
57+
58+
public function remember(string $key, int $ttl, Closure $callback): mixed
59+
{
60+
$key = static::validateKey($key, $this->prefix);
61+
62+
return apcu_entry($key, $callback, $ttl);
63+
}
64+
65+
public function delete(string $key): bool
66+
{
67+
$key = static::validateKey($key, $this->prefix);
68+
69+
return apcu_delete($key);
70+
}
71+
72+
public function deleteMatching(string $pattern): int
73+
{
74+
$matchedKeys = array_filter(
75+
array_keys(iterator_to_array(new APCUIterator(null, APC_ITER_KEY))),
76+
static fn ($key): bool => fnmatch($pattern, $key),
77+
);
78+
79+
if ($matchedKeys !== []) {
80+
return count($matchedKeys) - count(apcu_delete($matchedKeys));
81+
}
82+
83+
return 0;
84+
}
85+
86+
public function increment(string $key, int $offset = 1): false|int
87+
{
88+
$key = static::validateKey($key, $this->prefix);
89+
90+
return apcu_inc($key, $offset);
91+
}
92+
93+
public function decrement(string $key, int $offset = 1): false|int
94+
{
95+
$key = static::validateKey($key, $this->prefix);
96+
97+
return apcu_dec($key, $offset);
98+
}
99+
100+
public function clean(): bool
101+
{
102+
return apcu_clear_cache();
103+
}
104+
105+
public function getCacheInfo(): array|false
106+
{
107+
return apcu_cache_info(true);
108+
}
109+
110+
public function getMetaData(string $key): ?array
111+
{
112+
$key = static::validateKey($key, $this->prefix);
113+
$metadata = apcu_key_info($key);
114+
115+
if ($metadata !== null) {
116+
return [
117+
'expire' => $metadata['ttl'] > 0 ? Time::now()->getTimestamp() + $metadata['ttl'] : null,
118+
'mtime' => $metadata['mtime'],
119+
'data' => apcu_fetch($key),
120+
];
121+
}
122+
123+
return null;
124+
}
125+
126+
public function isSupported(): bool
127+
{
128+
return extension_loaded('apcu') && apcu_enabled();
129+
}
130+
}
Lines changed: 178 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,178 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
/**
6+
* This file is part of CodeIgniter 4 framework.
7+
*
8+
* (c) CodeIgniter Foundation <admin@codeigniter.com>
9+
*
10+
* For the full copyright and license information, please view
11+
* the LICENSE file that was distributed with this source code.
12+
*/
13+
14+
namespace CodeIgniter\Cache\Handlers;
15+
16+
use CodeIgniter\Cache\CacheFactory;
17+
use CodeIgniter\CLI\CLI;
18+
use CodeIgniter\I18n\Time;
19+
use Config\Cache;
20+
use PHPUnit\Framework\Attributes\Group;
21+
use PHPUnit\Framework\Attributes\RequiresPhpExtension;
22+
23+
/**
24+
* @internal
25+
*/
26+
#[Group('CacheLive')]
27+
#[RequiresPhpExtension('apcu')]
28+
final class ApcuHandlerTest extends AbstractHandlerTestCase
29+
{
30+
/**
31+
* @return list<string>
32+
*/
33+
private static function getKeyArray(): array
34+
{
35+
return [
36+
self::$key1,
37+
self::$key2,
38+
self::$key3,
39+
];
40+
}
41+
42+
protected function setUp(): void
43+
{
44+
parent::setUp();
45+
46+
$this->handler = CacheFactory::getHandler(new Cache(), 'apcu');
47+
}
48+
49+
protected function tearDown(): void
50+
{
51+
foreach (self::getKeyArray() as $key) {
52+
$this->handler->delete($key);
53+
}
54+
}
55+
56+
public function testNew(): void
57+
{
58+
$this->assertInstanceOf(ApcuHandler::class, $this->handler);
59+
}
60+
61+
/**
62+
* This test waits for 3 seconds before last assertion so this
63+
* is naturally a "slow" test on the perspective of the default limit.
64+
*
65+
* @timeLimit 3.5
66+
*/
67+
public function testGet(): void
68+
{
69+
$this->handler->save(self::$key1, 'value', 2);
70+
71+
$this->assertSame('value', $this->handler->get(self::$key1));
72+
$this->assertNull($this->handler->get(self::$dummy));
73+
74+
CLI::wait(3);
75+
$this->assertNull($this->handler->get(self::$key1));
76+
}
77+
78+
/**
79+
* This test waits for 3 seconds before last assertion so this
80+
* is naturally a "slow" test on the perspective of the default limit.
81+
*
82+
* @timeLimit 3.5
83+
*/
84+
public function testRemember(): void
85+
{
86+
$this->handler->remember(self::$key1, 2, static fn (): string => 'value');
87+
88+
$this->assertSame('value', $this->handler->get(self::$key1));
89+
$this->assertNull($this->handler->get(self::$dummy));
90+
91+
CLI::wait(3);
92+
$this->assertNull($this->handler->get(self::$key1));
93+
}
94+
95+
public function testSave(): void
96+
{
97+
$this->assertTrue($this->handler->save(self::$key1, 'value'));
98+
}
99+
100+
public function testSavePermanent(): void
101+
{
102+
$this->assertTrue($this->handler->save(self::$key1, 'value', 0));
103+
$metaData = $this->handler->getMetaData(self::$key1);
104+
105+
$this->assertNull($metaData['expire']);
106+
$this->assertLessThanOrEqual(1, $metaData['mtime'] - Time::now()->getTimestamp());
107+
$this->assertSame('value', $metaData['data']);
108+
109+
$this->assertTrue($this->handler->delete(self::$key1));
110+
}
111+
112+
public function testDelete(): void
113+
{
114+
$this->handler->save(self::$key1, 'value');
115+
116+
$this->assertTrue($this->handler->delete(self::$key1));
117+
$this->assertFalse($this->handler->delete(self::$dummy));
118+
}
119+
120+
public function testDeleteMatching(): void
121+
{
122+
// Save items to match on
123+
for ($i = 1; $i <= 50; $i++) {
124+
$this->handler->save('key_' . $i, 'value' . $i);
125+
}
126+
127+
// Checking that with pattern 'key_1*' only 11 entries deleted:
128+
// key_1, key_10, key_11, key_12, key_13, key_14, key_15, key_16, key_17, key_18, key_19
129+
$this->assertSame(11, $this->handler->deleteMatching('key_1*'));
130+
131+
// Checking that with pattern '*1', only 3 entries deleted:
132+
// key_21, key_31, key_41
133+
$this->assertSame(3, $this->handler->deleteMatching('key_*1'));
134+
135+
// Checking that with pattern '*5*' only 5 entries deleted:
136+
// key_5, key_25, key_35, key_45, key_50
137+
$this->assertSame(5, $this->handler->deleteMatching('*5*'));
138+
139+
// Check final number of cache entries
140+
$this->assertSame(31, $this->handler->getCacheInfo()['num_entries']);
141+
}
142+
143+
public function testIncrementAndDecrement(): void
144+
{
145+
$this->handler->save('counter', 100);
146+
147+
foreach (range(1, 10) as $step) {
148+
$this->handler->increment('counter', $step);
149+
}
150+
151+
$this->assertSame(155, $this->handler->get('counter'));
152+
153+
$this->handler->decrement('counter', 20);
154+
$this->assertSame(135, $this->handler->get('counter'));
155+
156+
$this->handler->increment('counter', 5);
157+
$this->assertSame(140, $this->handler->get('counter'));
158+
}
159+
160+
public function testClean(): void
161+
{
162+
$this->handler->save(self::$key1, 1);
163+
164+
$this->assertTrue($this->handler->clean());
165+
}
166+
167+
public function testGetCacheInfo(): void
168+
{
169+
$this->handler->save(self::$key1, 'value');
170+
171+
$this->assertIsArray($this->handler->getCacheInfo());
172+
}
173+
174+
public function testIsSupported(): void
175+
{
176+
$this->assertTrue($this->handler->isSupported());
177+
}
178+
}

user_guide_src/source/changelogs/v4.7.0.rst

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -236,6 +236,7 @@ Libraries
236236
- **Cache:** Added ``persistent`` config item to Redis handler.
237237
- **Cache:** Added support for HTTP status in ``ResponseCache``.
238238
- **Cache:** Added ``Config\Cache::$cacheStatusCodes`` to control which HTTP status codes are allowed to be cached by the ``PageCache`` filter. Defaults to ``[]`` (all status codes for backward compatibility). Recommended value: ``[200]`` to only cache successful responses. See :ref:`Setting $cacheStatusCodes <web_page_caching_cache_status_codes>` for details.
239+
- **Cache:** Added `APCu <https://www.php.net/apcu>`_ caching driver.
239240
- **CURLRequest:** Added ``shareConnection`` config item to change default share connection.
240241
- **CURLRequest:** Added ``dns_cache_timeout`` option to change default DNS cache timeout.
241242
- **CURLRequest:** Added ``fresh_connect`` options to enable/disable request fresh connection.

user_guide_src/source/libraries/caching.rst

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -36,7 +36,7 @@ $handler
3636
========
3737

3838
The is the name of the handler that should be used as the primary handler when starting up the engine.
39-
Available names are: dummy, file, memcached, redis, predis, wincache.
39+
Available names are: apcu, dummy, file, memcached, redis, predis, wincache.
4040

4141
$backupHandler
4242
==============
@@ -273,6 +273,13 @@ Class Reference
273273
Drivers
274274
*******
275275

276+
APCu Caching
277+
============
278+
279+
APCu is an in-memory key-value store for PHP.
280+
281+
To use it, you need the `APCu PHP extension <https://www.php.net/apcu>`_.
282+
276283
File-based Caching
277284
==================
278285

0 commit comments

Comments
 (0)