Skip to content
8 changes: 4 additions & 4 deletions system/Cache/CacheInterface.php
Original file line number Diff line number Diff line change
Expand Up @@ -44,11 +44,11 @@ public function save(string $key, mixed $value, int $ttl = 60): bool;
* Attempts to get an item from the cache, or executes the callback
* and stores the result on cache miss.
*
* @param string $key Cache item name
* @param int $ttl Time To Live, in seconds
* @param Closure(): mixed $callback Callback executed on cache miss
* @param string $key Cache item name
* @param callable(): int|callable(mixed): int|int $ttl Time To Live, in seconds
* @param Closure(): mixed $callback Callback executed on cache miss
*/
public function remember(string $key, int $ttl, Closure $callback): mixed;
public function remember(string $key, callable|int $ttl, Closure $callback): mixed;

/**
* Deletes a specific item from the cache store.
Expand Down
6 changes: 5 additions & 1 deletion system/Cache/Handlers/ApcuHandler.php
Original file line number Diff line number Diff line change
Expand Up @@ -55,10 +55,14 @@ public function save(string $key, $value, int $ttl = 60): bool
return apcu_store($key, $value, $ttl);
}

public function remember(string $key, int $ttl, Closure $callback): mixed
public function remember(string $key, callable|int $ttl, Closure $callback): mixed
{
$key = static::validateKey($key, $this->prefix);

if (is_callable($ttl)) {
return parent::remember($key, $ttl, $callback);
}
Comment on lines +62 to +64
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This seems to prefix the APCu key twice when the TTL is callable.

With a cache prefix configured, the value gets saved under one key but normal reads look for another, so the cached value cannot be found afterward.

Could the callable TTL path keep the original key and only apply the prefix in the apcu_entry() path?


return apcu_entry($key, $callback, $ttl);
}

Expand Down
26 changes: 24 additions & 2 deletions system/Cache/Handlers/BaseHandler.php
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@
use CodeIgniter\Cache\CacheInterface;
use CodeIgniter\Exceptions\InvalidArgumentException;
use Config\Cache;
use ReflectionFunction;

/**
* Base class for cache handling
Expand Down Expand Up @@ -64,15 +65,36 @@ public static function validateKey($key, $prefix = ''): string
return strlen($prefix . $key) > static::MAX_KEY_LENGTH ? $prefix . md5($key) : $prefix . $key;
}

public function remember(string $key, int $ttl, Closure $callback): mixed
public function remember(string $key, callable|int $ttl, Closure $callback): mixed
{
$value = $this->get($key);

if ($value !== null) {
return $value;
}

$this->save($key, $value = $callback(), $ttl);
$value = $callback();

if (is_callable($ttl)) {
$ttlClosure = Closure::fromCallable($ttl);
$rf = new ReflectionFunction($ttlClosure);
$params = $rf->getNumberOfRequiredParameters();

if ($params === 0) {
/** @var Closure(): int $ttlClosure */
$ttl = $ttlClosure();
} elseif ($params === 1) {
/** @var Closure(mixed): int $ttlClosure */
$ttl = $ttlClosure($value);
} else {
throw new InvalidArgumentException(sprintf(
'Argument #2 ($ttl) must accept 0 or 1 parameter, %d given.',
$params,
));
}
}

$this->save($key, $value, $ttl);

return $value;
}
Expand Down
2 changes: 1 addition & 1 deletion system/Cache/Handlers/DummyHandler.php
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,7 @@ public function get(string $key): mixed
return null;
}

public function remember(string $key, int $ttl, Closure $callback): mixed
public function remember(string $key, callable|int $ttl, Closure $callback): mixed
{
return null;
}
Expand Down
27 changes: 25 additions & 2 deletions system/Test/Mock/MockCache.php
Original file line number Diff line number Diff line change
Expand Up @@ -18,8 +18,10 @@
use CodeIgniter\Cache\Handlers\BaseHandler;
use CodeIgniter\Cache\LockStoreInterface;
use CodeIgniter\Cache\LockStoreProviderInterface;
use CodeIgniter\Exceptions\InvalidArgumentException;
use CodeIgniter\I18n\Time;
use PHPUnit\Framework\Assert;
use ReflectionFunction;

class MockCache extends BaseHandler implements CacheInterface, LockStoreProviderInterface
{
Expand Down Expand Up @@ -72,15 +74,36 @@ public function get(string $key): mixed
*
* @return bool|null
*/
public function remember(string $key, int $ttl, Closure $callback): mixed
public function remember(string $key, callable|int $ttl, Closure $callback): mixed
{
$value = $this->get($key);

if ($value !== null) {
return $value;
}

$this->save($key, $value = $callback(), $ttl);
$value = $callback();

if (is_callable($ttl)) {
$ttlClosure = Closure::fromCallable($ttl);
$rf = new ReflectionFunction($ttlClosure);
$params = $rf->getNumberOfRequiredParameters();

if ($params === 0) {
/** @var Closure(): int $ttlClosure */
$ttl = $ttlClosure();
} elseif ($params === 1) {
/** @var Closure(mixed): int $ttlClosure */
$ttl = $ttlClosure($value);
} else {
throw new InvalidArgumentException(sprintf(
'Argument #2 ($ttl) must accept 0 or 1 parameter, %d given.',
$params,
));
}
}

$this->save($key, $value, $ttl);

return $value;
}
Expand Down
44 changes: 44 additions & 0 deletions tests/system/Cache/Handlers/ApcuHandlerTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@

use CodeIgniter\Cache\CacheFactory;
use CodeIgniter\CLI\CLI;
use CodeIgniter\Exceptions\InvalidArgumentException;
use CodeIgniter\I18n\Time;
use Config\Cache;
use PHPUnit\Framework\Attributes\Group;
Expand Down Expand Up @@ -92,6 +93,49 @@ public function testRemember(): void
$this->assertNull($this->handler->get(self::$key1));
}

/**
* This test waits for 3 seconds before last assertion so this
* is naturally a "slow" test on the perspective of the default limit.
*
* @timeLimit 3.5
*/
public function testRememberWithTTLCallable(): void
{
$this->handler->remember(self::$key1, static fn (): int => 2, static fn (): string => 'value');

$this->assertSame('value', $this->handler->get(self::$key1));
$this->assertNull($this->handler->get(self::$dummy));

CLI::wait(3);
$this->assertNull($this->handler->get(self::$key1));
}

/**
* This test waits for 3 seconds before last assertion so this
* is naturally a "slow" test on the perspective of the default limit.
*
* @timeLimit 3.5
*/
public function testRememberWithTTLCallableAndValuePassed(): void
{
$this->handler->remember(self::$key1, static fn ($value): int => $value[0], static fn (): array => [2, 3]);

$this->assertSame([2, 3], $this->handler->get(self::$key1));
$this->assertNull($this->handler->get(self::$dummy));

CLI::wait(3);
$this->assertNull($this->handler->get(self::$key1));
}

public function testRememberWithTTLCallableAndMultipleParameters(): void
{
$this->expectException(InvalidArgumentException::class);
$this->expectExceptionMessage('Argument #2 ($ttl) must accept 0 or 1 parameter, 2 given.');

/** @phpstan-ignore argument.type */
$this->handler->remember(self::$key1, static fn ($a, $b): int => 2, static fn (): string => 'value');
}

public function testSave(): void
{
$this->assertTrue($this->handler->save(self::$key1, 'value'));
Expand Down
14 changes: 14 additions & 0 deletions tests/system/Cache/Handlers/DummyHandlerTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,20 @@ public function testRemember(): void
$this->assertNull($dummyHandler);
}

public function testRememberWithTTLCallable(): void
{
$dummyHandler = $this->handler->remember('key', static fn (): int => 2, static fn (): string => 'value');

$this->assertNull($dummyHandler);
}

public function testRememberWithTTLCallableAndValuePassed(): void
{
$dummyHandler = $this->handler->remember('key', static fn ($value): int => $value[0], static fn (): array => [2, 3]);

$this->assertNull($dummyHandler);
}

public function testSave(): void
{
$this->assertTrue($this->handler->save('key', 'value'));
Expand Down
44 changes: 44 additions & 0 deletions tests/system/Cache/Handlers/FileHandlerTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@
use CodeIgniter\Cache\CacheFactory;
use CodeIgniter\Cache\Exceptions\CacheException;
use CodeIgniter\CLI\CLI;
use CodeIgniter\Exceptions\InvalidArgumentException;
use CodeIgniter\I18n\Time;
use Config\Cache;
use PHPUnit\Framework\Attributes\DataProvider;
Expand Down Expand Up @@ -144,6 +145,49 @@ public function testRemember(): void
$this->assertNull($this->handler->get(self::$key1));
}

/**
* This test waits for 3 seconds before last assertion so this
* is naturally a "slow" test on the perspective of the default limit.
*
* @timeLimit 3.5
*/
public function testRememberWithTTLCallable(): void
{
$this->handler->remember(self::$key1, static fn (): int => 2, static fn (): string => 'value');

$this->assertSame('value', $this->handler->get(self::$key1));
$this->assertNull($this->handler->get(self::$dummy));

CLI::wait(3);
$this->assertNull($this->handler->get(self::$key1));
}

/**
* This test waits for 3 seconds before last assertion so this
* is naturally a "slow" test on the perspective of the default limit.
*
* @timeLimit 3.5
*/
public function testRememberWithTTLCallableAndValuePassed(): void
{
$this->handler->remember(self::$key1, static fn ($value): int => $value[0], static fn (): array => [2, 3]);

$this->assertSame([2, 3], $this->handler->get(self::$key1));
$this->assertNull($this->handler->get(self::$dummy));

CLI::wait(3);
$this->assertNull($this->handler->get(self::$key1));
}

public function testRememberWithTTLCallableAndMultipleParameters(): void
{
$this->expectException(InvalidArgumentException::class);
$this->expectExceptionMessage('Argument #2 ($ttl) must accept 0 or 1 parameter, 2 given.');

/** @phpstan-ignore argument.type */
$this->handler->remember(self::$key1, static fn ($a, $b): int => 2, static fn (): string => 'value');
}

/**
* chmod('path', 0444) does not work on Windows
*/
Expand Down
44 changes: 44 additions & 0 deletions tests/system/Cache/Handlers/MemcachedHandlerTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@
use CodeIgniter\Cache\LockStoreProviderInterface;
use CodeIgniter\CLI\CLI;
use CodeIgniter\Exceptions\BadMethodCallException;
use CodeIgniter\Exceptions\InvalidArgumentException;
use CodeIgniter\I18n\Time;
use Config\Cache;
use PHPUnit\Framework\Attributes\Group;
Expand Down Expand Up @@ -101,6 +102,49 @@ public function testRemember(): void
$this->assertNull($this->handler->get(self::$key1));
}

/**
* This test waits for 3 seconds before last assertion so this
* is naturally a "slow" test on the perspective of the default limit.
*
* @timeLimit 3.5
*/
public function testRememberWithTTLCallable(): void
{
$this->handler->remember(self::$key1, static fn (): int => 2, static fn (): string => 'value');

$this->assertSame('value', $this->handler->get(self::$key1));
$this->assertNull($this->handler->get(self::$dummy));

CLI::wait(3);
$this->assertNull($this->handler->get(self::$key1));
}

/**
* This test waits for 3 seconds before last assertion so this
* is naturally a "slow" test on the perspective of the default limit.
*
* @timeLimit 3.5
*/
public function testRememberWithTTLCallableAndValuePassed(): void
{
$this->handler->remember(self::$key1, static fn ($value): int => $value[0], static fn (): array => [2, 3]);

$this->assertSame([2, 3], $this->handler->get(self::$key1));
$this->assertNull($this->handler->get(self::$dummy));

CLI::wait(3);
$this->assertNull($this->handler->get(self::$key1));
}

public function testRememberWithTTLCallableAndMultipleParameters(): void
{
$this->expectException(InvalidArgumentException::class);
$this->expectExceptionMessage('Argument #2 ($ttl) must accept 0 or 1 parameter, 2 given.');

/** @phpstan-ignore argument.type */
$this->handler->remember(self::$key1, static fn ($a, $b): int => 2, static fn (): string => 'value');
}

public function testSave(): void
{
$this->assertTrue($this->handler->save(self::$key1, 'value'));
Expand Down
44 changes: 44 additions & 0 deletions tests/system/Cache/Handlers/PredisHandlerTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@
use CodeIgniter\Cache\LockStoreInterface;
use CodeIgniter\Cache\LockStoreProviderInterface;
use CodeIgniter\CLI\CLI;
use CodeIgniter\Exceptions\InvalidArgumentException;
use CodeIgniter\I18n\Time;
use Config\Cache;
use PHPUnit\Framework\Attributes\Group;
Expand Down Expand Up @@ -109,6 +110,49 @@ public function testRemember(): void
$this->assertNull($this->handler->get(self::$key1));
}

/**
* This test waits for 3 seconds before last assertion so this
* is naturally a "slow" test on the perspective of the default limit.
*
* @timeLimit 3.5
*/
public function testRememberWithTTLCallable(): void
{
$this->handler->remember(self::$key1, static fn (): int => 2, static fn (): string => 'value');

$this->assertSame('value', $this->handler->get(self::$key1));
$this->assertNull($this->handler->get(self::$dummy));

CLI::wait(3);
$this->assertNull($this->handler->get(self::$key1));
}

/**
* This test waits for 3 seconds before last assertion so this
* is naturally a "slow" test on the perspective of the default limit.
*
* @timeLimit 3.5
*/
public function testRememberWithTTLCallableAndValuePassed(): void
{
$this->handler->remember(self::$key1, static fn ($value): int => $value[0], static fn (): array => [2, 3]);

$this->assertSame([2, 3], $this->handler->get(self::$key1));
$this->assertNull($this->handler->get(self::$dummy));

CLI::wait(3);
$this->assertNull($this->handler->get(self::$key1));
}

public function testRememberWithTTLCallableAndMultipleParameters(): void
{
$this->expectException(InvalidArgumentException::class);
$this->expectExceptionMessage('Argument #2 ($ttl) must accept 0 or 1 parameter, 2 given.');

/** @phpstan-ignore argument.type */
$this->handler->remember(self::$key1, static fn ($a, $b): int => 2, static fn (): string => 'value');
}

public function testSave(): void
{
$this->assertTrue($this->handler->save(self::$key1, 'value'));
Expand Down
Loading
Loading