Skip to content

Commit 4d6b6be

Browse files
committed
feat: APCu caching driver implementation
1 parent 12cf2b4 commit 4d6b6be

File tree

3 files changed

+347
-0
lines changed

3 files changed

+347
-0
lines changed

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

0 commit comments

Comments
 (0)