From d717dc696cf87881c6c58fd861d78bdc5704e885 Mon Sep 17 00:00:00 2001 From: patel-vansh Date: Tue, 5 May 2026 19:07:34 +0530 Subject: [PATCH 1/8] feat: Added support for callable for TTL in Cache Handlers --- system/Cache/CacheInterface.php | 8 ++--- system/Cache/Handlers/ApcuHandler.php | 6 +++- system/Cache/Handlers/BaseHandler.php | 13 +++++-- system/Cache/Handlers/DummyHandler.php | 2 +- system/Test/Mock/MockCache.php | 13 +++++-- .../system/Cache/Handlers/ApcuHandlerTest.php | 34 +++++++++++++++++++ .../Cache/Handlers/DummyHandlerTest.php | 14 ++++++++ .../system/Cache/Handlers/FileHandlerTest.php | 34 +++++++++++++++++++ .../Cache/Handlers/MemcachedHandlerTest.php | 34 +++++++++++++++++++ .../Cache/Handlers/PredisHandlerTest.php | 34 +++++++++++++++++++ .../Cache/Handlers/RedisHandlerTest.php | 34 +++++++++++++++++++ user_guide_src/source/changelogs/v4.8.0.rst | 2 ++ user_guide_src/source/libraries/caching.rst | 29 ++++++++++++++-- .../source/libraries/caching/015.php | 11 ++++++ 14 files changed, 255 insertions(+), 13 deletions(-) create mode 100644 user_guide_src/source/libraries/caching/015.php diff --git a/system/Cache/CacheInterface.php b/system/Cache/CacheInterface.php index 4cedb67e9538..464b1e5f03b9 100644 --- a/system/Cache/CacheInterface.php +++ b/system/Cache/CacheInterface.php @@ -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 $value): 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. diff --git a/system/Cache/Handlers/ApcuHandler.php b/system/Cache/Handlers/ApcuHandler.php index ef0f51c50dc7..60fffb64145a 100644 --- a/system/Cache/Handlers/ApcuHandler.php +++ b/system/Cache/Handlers/ApcuHandler.php @@ -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); + } + return apcu_entry($key, $callback, $ttl); } diff --git a/system/Cache/Handlers/BaseHandler.php b/system/Cache/Handlers/BaseHandler.php index 2217e98e35f9..5531c72ba771 100644 --- a/system/Cache/Handlers/BaseHandler.php +++ b/system/Cache/Handlers/BaseHandler.php @@ -17,6 +17,7 @@ use CodeIgniter\Cache\CacheInterface; use CodeIgniter\Exceptions\InvalidArgumentException; use Config\Cache; +use ReflectionFunction; /** * Base class for cache handling @@ -64,7 +65,7 @@ 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); @@ -72,7 +73,15 @@ public function remember(string $key, int $ttl, Closure $callback): mixed return $value; } - $this->save($key, $value = $callback(), $ttl); + $value = $callback(); + + if (is_callable($ttl)) { + $ttl = (new ReflectionFunction($ttl(...)))->getNumberOfParameters() > 0 + ? $ttl($value) + : $ttl(); + } + + $this->save($key, $value, $ttl); return $value; } diff --git a/system/Cache/Handlers/DummyHandler.php b/system/Cache/Handlers/DummyHandler.php index a30475965d6f..891f8ad31d3f 100644 --- a/system/Cache/Handlers/DummyHandler.php +++ b/system/Cache/Handlers/DummyHandler.php @@ -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; } diff --git a/system/Test/Mock/MockCache.php b/system/Test/Mock/MockCache.php index 393246a8d0a9..d3cd3dea7d60 100644 --- a/system/Test/Mock/MockCache.php +++ b/system/Test/Mock/MockCache.php @@ -20,6 +20,7 @@ use CodeIgniter\Cache\LockStoreProviderInterface; use CodeIgniter\I18n\Time; use PHPUnit\Framework\Assert; +use ReflectionFunction; class MockCache extends BaseHandler implements CacheInterface, LockStoreProviderInterface { @@ -72,7 +73,7 @@ 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); @@ -80,7 +81,15 @@ public function remember(string $key, int $ttl, Closure $callback): mixed return $value; } - $this->save($key, $value = $callback(), $ttl); + $value = $callback(); + + if (is_callable($ttl)) { + $ttl = (new ReflectionFunction($ttl(...)))->getNumberOfParameters() > 0 + ? $ttl($value) + : $ttl(); + } + + $this->save($key, $value, $ttl); return $value; } diff --git a/tests/system/Cache/Handlers/ApcuHandlerTest.php b/tests/system/Cache/Handlers/ApcuHandlerTest.php index 5d5d5f7b2f6f..7b89c3a3c26c 100644 --- a/tests/system/Cache/Handlers/ApcuHandlerTest.php +++ b/tests/system/Cache/Handlers/ApcuHandlerTest.php @@ -92,6 +92,40 @@ 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 testSave(): void { $this->assertTrue($this->handler->save(self::$key1, 'value')); diff --git a/tests/system/Cache/Handlers/DummyHandlerTest.php b/tests/system/Cache/Handlers/DummyHandlerTest.php index ab3b3ccd643d..2bdc42b5bd5d 100644 --- a/tests/system/Cache/Handlers/DummyHandlerTest.php +++ b/tests/system/Cache/Handlers/DummyHandlerTest.php @@ -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')); diff --git a/tests/system/Cache/Handlers/FileHandlerTest.php b/tests/system/Cache/Handlers/FileHandlerTest.php index 16c6042be124..02f04892f996 100644 --- a/tests/system/Cache/Handlers/FileHandlerTest.php +++ b/tests/system/Cache/Handlers/FileHandlerTest.php @@ -144,6 +144,40 @@ 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)); + } + /** * chmod('path', 0444) does not work on Windows */ diff --git a/tests/system/Cache/Handlers/MemcachedHandlerTest.php b/tests/system/Cache/Handlers/MemcachedHandlerTest.php index b972db6f7845..882e2a25b3e9 100644 --- a/tests/system/Cache/Handlers/MemcachedHandlerTest.php +++ b/tests/system/Cache/Handlers/MemcachedHandlerTest.php @@ -99,6 +99,40 @@ 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 testSave(): void { $this->assertTrue($this->handler->save(self::$key1, 'value')); diff --git a/tests/system/Cache/Handlers/PredisHandlerTest.php b/tests/system/Cache/Handlers/PredisHandlerTest.php index 3b317df2299e..f09bd3ff8495 100644 --- a/tests/system/Cache/Handlers/PredisHandlerTest.php +++ b/tests/system/Cache/Handlers/PredisHandlerTest.php @@ -109,6 +109,40 @@ 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 testSave(): void { $this->assertTrue($this->handler->save(self::$key1, 'value')); diff --git a/tests/system/Cache/Handlers/RedisHandlerTest.php b/tests/system/Cache/Handlers/RedisHandlerTest.php index 72a66f04df1b..f4ecd4d5cc4b 100644 --- a/tests/system/Cache/Handlers/RedisHandlerTest.php +++ b/tests/system/Cache/Handlers/RedisHandlerTest.php @@ -110,6 +110,40 @@ 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 testSave(): void { $this->assertTrue($this->handler->save(self::$key1, 'value')); diff --git a/user_guide_src/source/changelogs/v4.8.0.rst b/user_guide_src/source/changelogs/v4.8.0.rst index f7009da81746..27820f9e0b97 100644 --- a/user_guide_src/source/changelogs/v4.8.0.rst +++ b/user_guide_src/source/changelogs/v4.8.0.rst @@ -46,6 +46,7 @@ update your implementations to include the new methods or method changes to ensu - **Database:** ``CodeIgniter\Database\ConnectionInterface`` now requires the ``afterCommit()``, ``afterRollback()``, and ``transaction()`` methods. - **Logging:** ``CodeIgniter\Log\Handlers\HandlerInterface::handle()`` now requires a third parameter ``array $context = []``. Any custom log handler that overrides ``handle()`` - whether implementing ``HandlerInterface`` directly or extending a built-in handler class - must add the parameter to its ``handle()`` method signature. - **Security:** The ``SecurityInterface``'s ``verify()`` method now has a native return type of ``static``. +- **Cache:** ``CodeIgniter\Cache\CacheInterface::remember()`` now accepts a TTL callable. All built-in cache handlers inherit this method via ``BaseHandler``, so no changes are required for them. Method Signature Changes ======================== @@ -227,6 +228,7 @@ Model Libraries ========= +- **Cache:** Added support for TTL callables in the ``remember()`` method of cache handlers. This allows you to specify a callable that returns a TTL value, which can be useful for dynamic TTL. See :ref:`cache-ttl-callables` for details. - **Context**: This new feature allows you to easily set and retrieve normal or hidden contextual data for the current request. See :ref:`Context ` for details. - **Images:**: Added support for the AVIF file format. - **Locks:** Added :doc:`Atomic Locks ` for owner-aware, cross-process mutual exclusion backed by supported cache handlers. diff --git a/user_guide_src/source/libraries/caching.rst b/user_guide_src/source/libraries/caching.rst index b208d63499df..cb898c22d67c 100644 --- a/user_guide_src/source/libraries/caching.rst +++ b/user_guide_src/source/libraries/caching.rst @@ -128,10 +128,10 @@ Class Reference .. literalinclude:: caching/003.php - .. php:method:: remember(string $key, int $ttl, Closure $callback) + .. php:method:: remember(string $key, callable|int $ttl, Closure $callback) :param string $key: Cache item name - :param int $ttl: Time to live in seconds + :param callable|int $ttl: Time to live in seconds :param Closure $callback: Callback to invoke when the cache item returns null :returns: The value of the cache item :rtype: mixed @@ -139,6 +139,29 @@ Class Reference Gets an item from the cache. If ``null`` was returned, this will invoke the callback and save the result. Either way, this will return the value. + The ``$ttl`` parameter may also be a callable, allowing the TTL to be + determined dynamically at runtime. This is especially useful when the + expiration time depends on the computed value or requires an expensive + calculation. + + When a callable is provided, it will only be executed on a cache miss, + after the callback has been invoked. The callable may optionally accept + the computed value as its first argument: + + .. literalinclude:: caching/015.php + + This ensures that TTL computation is deferred until necessary and avoids + unnecessary overhead when the cache item already exists. + + .. note:: Prior to v4.8.0, the second parameter only accepted an integer TTL value. The ability to pass a callable was added in v4.8.0. + + .. note:: When using the APCu cache handler, providing a callable TTL disables + the use of ``apcu_entry()`` and falls back to a manual cache retrieval + and storage process. As a result, the operation is no longer atomic + and may be subject to race conditions under high concurrency. + + If atomic behavior is required, use an integer TTL value. + .. php:method:: save(string $key, $data[, int $ttl = 60]) :param string $key: Cache item name @@ -276,7 +299,7 @@ Drivers APCu Caching ============ -APCu is an in-memory key-value store for PHP. +APCu is an in-memory key-value store for PHP. To use it, you need the `APCu PHP extension `_. diff --git a/user_guide_src/source/libraries/caching/015.php b/user_guide_src/source/libraries/caching/015.php new file mode 100644 index 000000000000..cb7f6ccb36f5 --- /dev/null +++ b/user_guide_src/source/libraries/caching/015.php @@ -0,0 +1,11 @@ +remember('key', static fn () => 60, static fn () => fetchData()); + +// Value-aware TTL +$cache->remember( + 'key', + static fn ($value) => $value->expires_at - time(), + static fn () => fetchData(), +); From cd16d93a8c77edd86adcd45778c47530211f07a5 Mon Sep 17 00:00:00 2001 From: patel-vansh Date: Tue, 5 May 2026 20:06:05 +0530 Subject: [PATCH 2/8] remove redundant reference in changelog --- user_guide_src/source/changelogs/v4.8.0.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/user_guide_src/source/changelogs/v4.8.0.rst b/user_guide_src/source/changelogs/v4.8.0.rst index 27820f9e0b97..0373364a3b7e 100644 --- a/user_guide_src/source/changelogs/v4.8.0.rst +++ b/user_guide_src/source/changelogs/v4.8.0.rst @@ -228,7 +228,7 @@ Model Libraries ========= -- **Cache:** Added support for TTL callables in the ``remember()`` method of cache handlers. This allows you to specify a callable that returns a TTL value, which can be useful for dynamic TTL. See :ref:`cache-ttl-callables` for details. +- **Cache:** Added support for TTL callables in the ``remember()`` method of cache handlers. This allows you to specify a callable that returns a TTL value, which can be useful for dynamic TTL. - **Context**: This new feature allows you to easily set and retrieve normal or hidden contextual data for the current request. See :ref:`Context ` for details. - **Images:**: Added support for the AVIF file format. - **Locks:** Added :doc:`Atomic Locks ` for owner-aware, cross-process mutual exclusion backed by supported cache handlers. From d04f1971701af6117a40f920f2039181af3e48fe Mon Sep 17 00:00:00 2001 From: patel-vansh Date: Wed, 6 May 2026 09:35:34 +0530 Subject: [PATCH 3/8] fix Psalm error --- system/Cache/Handlers/BaseHandler.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/system/Cache/Handlers/BaseHandler.php b/system/Cache/Handlers/BaseHandler.php index 5531c72ba771..121fbcc8f81f 100644 --- a/system/Cache/Handlers/BaseHandler.php +++ b/system/Cache/Handlers/BaseHandler.php @@ -76,7 +76,7 @@ public function remember(string $key, callable|int $ttl, Closure $callback): mix $value = $callback(); if (is_callable($ttl)) { - $ttl = (new ReflectionFunction($ttl(...)))->getNumberOfParameters() > 0 + $ttl = (new ReflectionFunction(Closure::fromCallable($ttl)))->getNumberOfParameters() > 0 ? $ttl($value) : $ttl(); } From 957586fd349a2a50f5ea4b41c54793c9b8feea4f Mon Sep 17 00:00:00 2001 From: patel-vansh Date: Wed, 6 May 2026 10:14:25 +0530 Subject: [PATCH 4/8] Add explicit types and throw exception if number of required args > 1 --- system/Cache/CacheInterface.php | 6 +++--- system/Cache/Handlers/BaseHandler.php | 19 ++++++++++++++++--- 2 files changed, 19 insertions(+), 6 deletions(-) diff --git a/system/Cache/CacheInterface.php b/system/Cache/CacheInterface.php index 464b1e5f03b9..2b61b7d8512f 100644 --- a/system/Cache/CacheInterface.php +++ b/system/Cache/CacheInterface.php @@ -44,9 +44,9 @@ 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 callable(): int|callable(mixed $value): int|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, callable|int $ttl, Closure $callback): mixed; diff --git a/system/Cache/Handlers/BaseHandler.php b/system/Cache/Handlers/BaseHandler.php index 121fbcc8f81f..597ac5b0d31e 100644 --- a/system/Cache/Handlers/BaseHandler.php +++ b/system/Cache/Handlers/BaseHandler.php @@ -76,9 +76,22 @@ public function remember(string $key, callable|int $ttl, Closure $callback): mix $value = $callback(); if (is_callable($ttl)) { - $ttl = (new ReflectionFunction(Closure::fromCallable($ttl)))->getNumberOfParameters() > 0 - ? $ttl($value) - : $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); From 701a7108cb0e6107f7f91bf56472838c0431405f Mon Sep 17 00:00:00 2001 From: patel-vansh Date: Wed, 6 May 2026 10:21:33 +0530 Subject: [PATCH 5/8] Copy the logic into MockCache and order the new addition in changelog --- system/Test/Mock/MockCache.php | 20 +++++++++++++++++--- user_guide_src/source/changelogs/v4.8.0.rst | 2 +- 2 files changed, 18 insertions(+), 4 deletions(-) diff --git a/system/Test/Mock/MockCache.php b/system/Test/Mock/MockCache.php index d3cd3dea7d60..dbeebe0e6faf 100644 --- a/system/Test/Mock/MockCache.php +++ b/system/Test/Mock/MockCache.php @@ -18,6 +18,7 @@ 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; @@ -84,9 +85,22 @@ public function remember(string $key, callable|int $ttl, Closure $callback): mix $value = $callback(); if (is_callable($ttl)) { - $ttl = (new ReflectionFunction($ttl(...)))->getNumberOfParameters() > 0 - ? $ttl($value) - : $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); diff --git a/user_guide_src/source/changelogs/v4.8.0.rst b/user_guide_src/source/changelogs/v4.8.0.rst index 0373364a3b7e..fc36f12e7bd1 100644 --- a/user_guide_src/source/changelogs/v4.8.0.rst +++ b/user_guide_src/source/changelogs/v4.8.0.rst @@ -43,10 +43,10 @@ Interface Changes **NOTE:** If you've implemented your own classes that implement these interfaces from scratch, you will need to update your implementations to include the new methods or method changes to ensure compatibility. +- **Cache:** ``CodeIgniter\Cache\CacheInterface::remember()`` now accepts a TTL callable. All built-in cache handlers inherit this method via ``BaseHandler``, so no changes are required for them. - **Database:** ``CodeIgniter\Database\ConnectionInterface`` now requires the ``afterCommit()``, ``afterRollback()``, and ``transaction()`` methods. - **Logging:** ``CodeIgniter\Log\Handlers\HandlerInterface::handle()`` now requires a third parameter ``array $context = []``. Any custom log handler that overrides ``handle()`` - whether implementing ``HandlerInterface`` directly or extending a built-in handler class - must add the parameter to its ``handle()`` method signature. - **Security:** The ``SecurityInterface``'s ``verify()`` method now has a native return type of ``static``. -- **Cache:** ``CodeIgniter\Cache\CacheInterface::remember()`` now accepts a TTL callable. All built-in cache handlers inherit this method via ``BaseHandler``, so no changes are required for them. Method Signature Changes ======================== From e315ba9d459a869e4df80b77d32cf3033e21b356 Mon Sep 17 00:00:00 2001 From: patel-vansh Date: Wed, 6 May 2026 10:26:49 +0530 Subject: [PATCH 6/8] Added tests to check expection thrown --- tests/system/Cache/Handlers/ApcuHandlerTest.php | 9 +++++++++ tests/system/Cache/Handlers/DummyHandlerTest.php | 1 + tests/system/Cache/Handlers/FileHandlerTest.php | 9 +++++++++ tests/system/Cache/Handlers/MemcachedHandlerTest.php | 9 +++++++++ tests/system/Cache/Handlers/PredisHandlerTest.php | 9 +++++++++ tests/system/Cache/Handlers/RedisHandlerTest.php | 9 +++++++++ 6 files changed, 46 insertions(+) diff --git a/tests/system/Cache/Handlers/ApcuHandlerTest.php b/tests/system/Cache/Handlers/ApcuHandlerTest.php index 7b89c3a3c26c..34b61d94bd7a 100644 --- a/tests/system/Cache/Handlers/ApcuHandlerTest.php +++ b/tests/system/Cache/Handlers/ApcuHandlerTest.php @@ -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; @@ -126,6 +127,14 @@ public function testRememberWithTTLCallableAndValuePassed(): void $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.'); + + $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')); diff --git a/tests/system/Cache/Handlers/DummyHandlerTest.php b/tests/system/Cache/Handlers/DummyHandlerTest.php index 2bdc42b5bd5d..df95c06c8636 100644 --- a/tests/system/Cache/Handlers/DummyHandlerTest.php +++ b/tests/system/Cache/Handlers/DummyHandlerTest.php @@ -14,6 +14,7 @@ namespace CodeIgniter\Cache\Handlers; use CodeIgniter\Cache\CacheFactory; +use CodeIgniter\Exceptions\InvalidArgumentException; use CodeIgniter\Test\CIUnitTestCase; use Config\Cache; use PHPUnit\Framework\Attributes\Group; diff --git a/tests/system/Cache/Handlers/FileHandlerTest.php b/tests/system/Cache/Handlers/FileHandlerTest.php index 02f04892f996..8e548a1f8d9d 100644 --- a/tests/system/Cache/Handlers/FileHandlerTest.php +++ b/tests/system/Cache/Handlers/FileHandlerTest.php @@ -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; @@ -178,6 +179,14 @@ public function testRememberWithTTLCallableAndValuePassed(): void $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.'); + + $this->handler->remember(self::$key1, static fn ($a, $b): int => 2, static fn (): string => 'value'); + } + /** * chmod('path', 0444) does not work on Windows */ diff --git a/tests/system/Cache/Handlers/MemcachedHandlerTest.php b/tests/system/Cache/Handlers/MemcachedHandlerTest.php index 882e2a25b3e9..1e0fb8ceeb9d 100644 --- a/tests/system/Cache/Handlers/MemcachedHandlerTest.php +++ b/tests/system/Cache/Handlers/MemcachedHandlerTest.php @@ -16,6 +16,7 @@ use CodeIgniter\Cache\CacheFactory; use CodeIgniter\CLI\CLI; use CodeIgniter\Exceptions\BadMethodCallException; +use CodeIgniter\Exceptions\InvalidArgumentException; use CodeIgniter\I18n\Time; use Config\Cache; use PHPUnit\Framework\Attributes\Group; @@ -133,6 +134,14 @@ public function testRememberWithTTLCallableAndValuePassed(): void $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.'); + + $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')); diff --git a/tests/system/Cache/Handlers/PredisHandlerTest.php b/tests/system/Cache/Handlers/PredisHandlerTest.php index f09bd3ff8495..ffd895b56b4e 100644 --- a/tests/system/Cache/Handlers/PredisHandlerTest.php +++ b/tests/system/Cache/Handlers/PredisHandlerTest.php @@ -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; @@ -143,6 +144,14 @@ public function testRememberWithTTLCallableAndValuePassed(): void $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.'); + + $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')); diff --git a/tests/system/Cache/Handlers/RedisHandlerTest.php b/tests/system/Cache/Handlers/RedisHandlerTest.php index f4ecd4d5cc4b..7735bde2d7dc 100644 --- a/tests/system/Cache/Handlers/RedisHandlerTest.php +++ b/tests/system/Cache/Handlers/RedisHandlerTest.php @@ -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\DataProvider; @@ -144,6 +145,14 @@ public function testRememberWithTTLCallableAndValuePassed(): void $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.'); + + $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')); From b6430bf7820366ebc73f807f688b210c8d2ea82a Mon Sep 17 00:00:00 2001 From: patel-vansh Date: Wed, 6 May 2026 10:27:19 +0530 Subject: [PATCH 7/8] cs-fix --- tests/system/Cache/Handlers/DummyHandlerTest.php | 1 - 1 file changed, 1 deletion(-) diff --git a/tests/system/Cache/Handlers/DummyHandlerTest.php b/tests/system/Cache/Handlers/DummyHandlerTest.php index df95c06c8636..2bdc42b5bd5d 100644 --- a/tests/system/Cache/Handlers/DummyHandlerTest.php +++ b/tests/system/Cache/Handlers/DummyHandlerTest.php @@ -14,7 +14,6 @@ namespace CodeIgniter\Cache\Handlers; use CodeIgniter\Cache\CacheFactory; -use CodeIgniter\Exceptions\InvalidArgumentException; use CodeIgniter\Test\CIUnitTestCase; use Config\Cache; use PHPUnit\Framework\Attributes\Group; From 250df3558a19bced120638ff0d04f1abb5e03e90 Mon Sep 17 00:00:00 2001 From: patel-vansh Date: Wed, 6 May 2026 10:30:20 +0530 Subject: [PATCH 8/8] Added phpstan-ignore to explicitly check for exception --- tests/system/Cache/Handlers/ApcuHandlerTest.php | 1 + tests/system/Cache/Handlers/FileHandlerTest.php | 1 + tests/system/Cache/Handlers/MemcachedHandlerTest.php | 1 + tests/system/Cache/Handlers/PredisHandlerTest.php | 1 + tests/system/Cache/Handlers/RedisHandlerTest.php | 1 + 5 files changed, 5 insertions(+) diff --git a/tests/system/Cache/Handlers/ApcuHandlerTest.php b/tests/system/Cache/Handlers/ApcuHandlerTest.php index 34b61d94bd7a..be4bbaa3b1cb 100644 --- a/tests/system/Cache/Handlers/ApcuHandlerTest.php +++ b/tests/system/Cache/Handlers/ApcuHandlerTest.php @@ -132,6 +132,7 @@ 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'); } diff --git a/tests/system/Cache/Handlers/FileHandlerTest.php b/tests/system/Cache/Handlers/FileHandlerTest.php index 8e548a1f8d9d..1b27c4b0e834 100644 --- a/tests/system/Cache/Handlers/FileHandlerTest.php +++ b/tests/system/Cache/Handlers/FileHandlerTest.php @@ -184,6 +184,7 @@ 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'); } diff --git a/tests/system/Cache/Handlers/MemcachedHandlerTest.php b/tests/system/Cache/Handlers/MemcachedHandlerTest.php index 1e0fb8ceeb9d..400ed6eeaccd 100644 --- a/tests/system/Cache/Handlers/MemcachedHandlerTest.php +++ b/tests/system/Cache/Handlers/MemcachedHandlerTest.php @@ -139,6 +139,7 @@ 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'); } diff --git a/tests/system/Cache/Handlers/PredisHandlerTest.php b/tests/system/Cache/Handlers/PredisHandlerTest.php index ffd895b56b4e..d81fb5d02243 100644 --- a/tests/system/Cache/Handlers/PredisHandlerTest.php +++ b/tests/system/Cache/Handlers/PredisHandlerTest.php @@ -149,6 +149,7 @@ 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'); } diff --git a/tests/system/Cache/Handlers/RedisHandlerTest.php b/tests/system/Cache/Handlers/RedisHandlerTest.php index 7735bde2d7dc..883cd6644957 100644 --- a/tests/system/Cache/Handlers/RedisHandlerTest.php +++ b/tests/system/Cache/Handlers/RedisHandlerTest.php @@ -150,6 +150,7 @@ 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'); }