Skip to content
Open
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
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

This is hard to manually parse and read. For callable signatures, PHPStan actually recommends to wrap them in parenthesis, e.g. (callable(mixed): int)|int

* @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,8 +55,12 @@ 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
{
if (is_callable($ttl)) {
return parent::remember($key, $ttl, $callback);
}
Comment thread
patel-vansh marked this conversation as resolved.

$key = static::validateKey($key, $this->prefix);

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,
));
}
}

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

I think we can forego this parameter counting by simply defining the callable as callable(mixed): int. In this case, a no-arg closure will still pass and PHP won't complain. This would remove the need for the reflection call.

$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