From 3c6f9e9ff8690e9d4e63733213bb9d9f84699151 Mon Sep 17 00:00:00 2001 From: Yitz Willroth Date: Sun, 8 Jun 2025 10:38:52 -0400 Subject: [PATCH 1/4] :feat: add touch for cache ttl extension --- src/Illuminate/Cache/ApcStore.php | 14 ++++++ src/Illuminate/Cache/ArrayStore.php | 19 ++++++++ src/Illuminate/Cache/DatabaseStore.php | 11 +++++ src/Illuminate/Cache/DynamoDbStore.php | 33 +++++++++++++ src/Illuminate/Cache/FileStore.php | 17 ++++++- src/Illuminate/Cache/MemcachedStore.php | 8 ++++ src/Illuminate/Cache/MemoizedStore.php | 10 ++++ src/Illuminate/Cache/NullStore.php | 8 ++++ src/Illuminate/Cache/RedisStore.php | 9 +++- src/Illuminate/Cache/Repository.php | 32 ++++++------- src/Illuminate/Contracts/Cache/Repository.php | 7 +++ src/Illuminate/Contracts/Cache/Store.php | 5 ++ tests/Cache/CacheApcStoreTest.php | 16 +++++++ tests/Cache/CacheArrayStoreTest.php | 17 +++++++ tests/Cache/CacheDatabaseStoreTest.php | 45 ++++++++++++++++++ tests/Cache/CacheDynamoDbStoreTest.php | 46 +++++++++++++++++++ tests/Cache/CacheFileStoreTest.php | 33 +++++++++++++ tests/Cache/CacheMemcachedStoreTest.php | 14 ++++++ tests/Cache/CacheMemoizedStoreTest.php | 29 ++++++++++++ tests/Cache/CacheNullStoreTest.php | 5 ++ tests/Cache/CacheRedisStoreTest.php | 21 ++++++++- tests/Cache/CacheRepositoryTest.php | 46 +++++++++++++++++++ 22 files changed, 425 insertions(+), 20 deletions(-) create mode 100755 tests/Cache/CacheDynamoDbStoreTest.php create mode 100644 tests/Cache/CacheMemoizedStoreTest.php diff --git a/src/Illuminate/Cache/ApcStore.php b/src/Illuminate/Cache/ApcStore.php index 89c31a3f7f0c..541099132b82 100755 --- a/src/Illuminate/Cache/ApcStore.php +++ b/src/Illuminate/Cache/ApcStore.php @@ -103,6 +103,20 @@ public function forget($key) return $this->apc->delete($this->prefix.$key); } + /** + * Set the expiration time of a cached item. + */ + public function touch(string $key, int $ttl): bool + { + $value = $this->apc->get($key = $this->getPrefix().$key); + + if (is_null($value)) { + return false; + } + + return $this->apc->put($key, $value, $ttl); + } + /** * Remove all items from the cache. * diff --git a/src/Illuminate/Cache/ArrayStore.php b/src/Illuminate/Cache/ArrayStore.php index 112501831822..64e127939395 100644 --- a/src/Illuminate/Cache/ArrayStore.php +++ b/src/Illuminate/Cache/ArrayStore.php @@ -3,6 +3,7 @@ namespace Illuminate\Cache; use Illuminate\Contracts\Cache\LockProvider; +use Illuminate\Support\Arr; use Illuminate\Support\Carbon; use Illuminate\Support\InteractsWithTime; @@ -130,6 +131,24 @@ public function forever($key, $value) return $this->put($key, $value, 0); } + /** + * Set the expiration time of a cached item. + */ + public function touch(string $key, int $ttl): bool + { + $item = Arr::get($this->storage, $key = $this->getPrefix().$key, null); + + if (is_null($item)) { + return false; + } + + $item['expiresAt'] = $this->calculateExpiration($ttl); + + $this->storage = array_merge($this->storage, [$key => $item]); + + return true; + } + /** * Remove an item from the cache. * diff --git a/src/Illuminate/Cache/DatabaseStore.php b/src/Illuminate/Cache/DatabaseStore.php index 04c52e45922d..828b9bf51ef7 100755 --- a/src/Illuminate/Cache/DatabaseStore.php +++ b/src/Illuminate/Cache/DatabaseStore.php @@ -411,6 +411,17 @@ protected function forgetManyIfExpired(array $keys, bool $prefixed = false) return true; } + /** + * Set the expiration time of a cached item. + */ + public function touch(string $key, int $ttl): bool + { + return (bool) $this->table() + ->where('key', '=', $this->getPrefix().$key) + ->where('expiration', '>', $now = $this->getTime()) + ->update(['expiration' => $now + $ttl]); + } + /** * Remove all items from the cache. * diff --git a/src/Illuminate/Cache/DynamoDbStore.php b/src/Illuminate/Cache/DynamoDbStore.php index 1bc7aa879865..ea342be6dc17 100644 --- a/src/Illuminate/Cache/DynamoDbStore.php +++ b/src/Illuminate/Cache/DynamoDbStore.php @@ -448,6 +448,39 @@ public function forget($key) return true; } + /** + * Set the expiration time of a cached item. + * + * @throws DynamoDbException + */ + public function touch(string $key, int $ttl): bool + { + try { + $this->dynamo->updateItem([ + 'TableName' => $this->table, + 'Key' => [$this->keyAttribute => ['S' => $this->getPrefix().$key]], + 'UpdateExpression' => 'SET #expiry = :expiry', + 'ConditionExpression' => 'attribute_exists(#key) AND #expiry > :now', + 'ExpressionAttributeNames' => [ + '#key' => $this->keyAttribute, + '#expiry' => $this->expirationAttribute, + ], + 'ExpressionAttributeValues' => [ + ':expiry' => ['N' => (string) $this->toTimestamp($ttl)], + ':now' => ['N' => (string) $this->currentTime()], + ], + ]); + } catch (DynamoDbException $e) { + if (str_contains($e->getMessage(), 'ConditionalCheckFailed')) { + return false; + } + + throw $e; + } + + return true; + } + /** * Remove all items from the cache. * diff --git a/src/Illuminate/Cache/FileStore.php b/src/Illuminate/Cache/FileStore.php index d445f5fc7c23..f963fa70f8ea 100755 --- a/src/Illuminate/Cache/FileStore.php +++ b/src/Illuminate/Cache/FileStore.php @@ -257,6 +257,20 @@ public function forget($key) return false; } + /** + * Set the expiration time of a cached item. + */ + public function touch(string $key, int $ttl): bool + { + $payload = $this->getPayload($this->getPrefix().$key); + + if (is_null($payload['data'])) { + return false; + } + + return $this->put($key, $payload['data'], $ttl); + } + /** * Remove all items from the cache. * @@ -298,7 +312,7 @@ protected function getPayload($key) } $expire = substr($contents, 0, 10); - } catch (Exception) { + } catch (Exception $e) { return $this->emptyPayload(); } @@ -314,6 +328,7 @@ protected function getPayload($key) try { $data = unserialize(substr($contents, 10)); } catch (Exception) { + $this->forget($key); return $this->emptyPayload(); diff --git a/src/Illuminate/Cache/MemcachedStore.php b/src/Illuminate/Cache/MemcachedStore.php index b05560e1a986..2afe2bdb46a0 100755 --- a/src/Illuminate/Cache/MemcachedStore.php +++ b/src/Illuminate/Cache/MemcachedStore.php @@ -202,6 +202,14 @@ public function restoreLock($name, $owner) return $this->lock($name, 0, $owner); } + /** + * Set the expiration time of a cached item. + */ + public function touch(string $key, int $ttl): bool + { + return $this->memcached->touch($this->getPrefix().$key, $this->calculateExpiration($ttl)); + } + /** * Remove an item from the cache. * diff --git a/src/Illuminate/Cache/MemoizedStore.php b/src/Illuminate/Cache/MemoizedStore.php index fc6313db2a1a..c1abc9fbf43a 100644 --- a/src/Illuminate/Cache/MemoizedStore.php +++ b/src/Illuminate/Cache/MemoizedStore.php @@ -208,6 +208,16 @@ public function forget($key) return $this->repository->forget($key); } + /** + * Set the expiration time of a cached item. + */ + public function touch(string $key, int $ttl): bool + { + unset($this->cache[$this->prefix($key)]); + + return $this->repository->touch($key, $ttl); + } + /** * Remove all items from the cache. * diff --git a/src/Illuminate/Cache/NullStore.php b/src/Illuminate/Cache/NullStore.php index 6c35ee386c26..5555a4869e94 100755 --- a/src/Illuminate/Cache/NullStore.php +++ b/src/Illuminate/Cache/NullStore.php @@ -93,6 +93,14 @@ public function restoreLock($name, $owner) return $this->lock($name, 0, $owner); } + /** + * Set the expiration time of a cached item. + */ + public function touch(string $key, int $ttl): bool + { + return false; + } + /** * Remove an item from the cache. * diff --git a/src/Illuminate/Cache/RedisStore.php b/src/Illuminate/Cache/RedisStore.php index 33cdf87307c7..8f0365571762 100755 --- a/src/Illuminate/Cache/RedisStore.php +++ b/src/Illuminate/Cache/RedisStore.php @@ -248,6 +248,14 @@ public function restoreLock($name, $owner) return $this->lock($name, 0, $owner); } + /** + * Set the expiration time of a cached item. + */ + public function touch(string $key, int $ttl): bool + { + return (bool) $this->connection()->expire($this->getPrefix().$key, (int) max(1, $ttl)); + } + /** * Remove an item from the cache. * @@ -458,7 +466,6 @@ protected function serialize($value) * Determine if the given value should be stored as plain value. * * @param mixed $value - * @return bool */ protected function shouldBeStoredWithoutSerialization($value): bool { diff --git a/src/Illuminate/Cache/Repository.php b/src/Illuminate/Cache/Repository.php index 3eb6f700ed01..03f3b99b5a98 100755 --- a/src/Illuminate/Cache/Repository.php +++ b/src/Illuminate/Cache/Repository.php @@ -106,7 +106,6 @@ public function missing($key) * * @param array|string $key * @param mixed $default - * @return mixed */ public function get($key, $default = null): mixed { @@ -251,8 +250,6 @@ public function put($key, $value, $ttl = null) /** * {@inheritdoc} - * - * @return bool */ public function set($key, $value, $ttl = null): bool { @@ -262,7 +259,6 @@ public function set($key, $value, $ttl = null): bool /** * Store multiple items in the cache for a given number of seconds. * - * @param array $values * @param \DateTimeInterface|\DateInterval|int|null $ttl * @return bool */ @@ -296,7 +292,6 @@ public function putMany(array $values, $ttl = null) /** * Store multiple items in the cache indefinitely. * - * @param array $values * @return bool */ protected function putManyForever(array $values) @@ -525,6 +520,22 @@ public function flexible($key, $ttl, $callback, $lock = null) return $value; } + /** + * Set the expiration of a cached item; null TTL will retain indefinitely. + */ + public function touch(string $key, \DateTimeInterface|\DateInterval|int|null $ttl = null): bool + { + $value = $this->get($key); + + if (is_null($value)) { + return false; + } + + return is_null($ttl) + ? $this->forever($key, $value) + : $this->store->touch($this->itemKey($key), $this->getSeconds($ttl)); + } + /** * Remove an item from the cache. * @@ -546,8 +557,6 @@ public function forget($key) /** * {@inheritdoc} - * - * @return bool */ public function delete($key): bool { @@ -556,8 +565,6 @@ public function delete($key): bool /** * {@inheritdoc} - * - * @return bool */ public function deleteMultiple($keys): bool { @@ -574,8 +581,6 @@ public function deleteMultiple($keys): bool /** * {@inheritdoc} - * - * @return bool */ public function clear(): bool { @@ -737,7 +742,6 @@ public function getEventDispatcher() /** * Set the event dispatcher instance. * - * @param \Illuminate\Contracts\Events\Dispatcher $events * @return void */ public function setEventDispatcher(Dispatcher $events) @@ -749,7 +753,6 @@ public function setEventDispatcher(Dispatcher $events) * Determine if a cached value exists. * * @param string $key - * @return bool */ public function offsetExists($key): bool { @@ -760,7 +763,6 @@ public function offsetExists($key): bool * Retrieve an item from the cache by key. * * @param string $key - * @return mixed */ public function offsetGet($key): mixed { @@ -772,7 +774,6 @@ public function offsetGet($key): mixed * * @param string $key * @param mixed $value - * @return void */ public function offsetSet($key, $value): void { @@ -783,7 +784,6 @@ public function offsetSet($key, $value): void * Remove an item from the cache. * * @param string $key - * @return void */ public function offsetUnset($key): void { diff --git a/src/Illuminate/Contracts/Cache/Repository.php b/src/Illuminate/Contracts/Cache/Repository.php index 4bc4638e46fe..beffc6b25cf6 100644 --- a/src/Illuminate/Contracts/Cache/Repository.php +++ b/src/Illuminate/Contracts/Cache/Repository.php @@ -3,6 +3,8 @@ namespace Illuminate\Contracts\Cache; use Closure; +use DateInterval; +use DateTimeInterface; use Psr\SimpleCache\CacheInterface; interface Repository extends CacheInterface @@ -88,6 +90,11 @@ public function remember($key, $ttl, Closure $callback); */ public function sear($key, Closure $callback); + /** + * Set the expiration of a cached item; null TTL will retain indefinitely. + */ + public function touch(string $key, DateTimeInterface|DateInterval|int|null $ttl = null): bool; + /** * Get an item from the cache, or execute the given Closure and store the result forever. * diff --git a/src/Illuminate/Contracts/Cache/Store.php b/src/Illuminate/Contracts/Cache/Store.php index 4ededd4efbc8..b29b74193ad5 100644 --- a/src/Illuminate/Contracts/Cache/Store.php +++ b/src/Illuminate/Contracts/Cache/Store.php @@ -83,6 +83,11 @@ public function forget($key); */ public function flush(); + /** + * Set the expiration of a cached item. + */ + public function touch(string $key, int $ttl): bool; + /** * Get the cache key prefix. * diff --git a/tests/Cache/CacheApcStoreTest.php b/tests/Cache/CacheApcStoreTest.php index e13d2adf63ec..f43b025f0792 100755 --- a/tests/Cache/CacheApcStoreTest.php +++ b/tests/Cache/CacheApcStoreTest.php @@ -4,6 +4,7 @@ use Illuminate\Cache\ApcStore; use Illuminate\Cache\ApcWrapper; +use Illuminate\Support\Carbon; use Mockery; use PHPUnit\Framework\TestCase; @@ -124,6 +125,19 @@ public function testForgetMethodProperlyCallsAPC() $this->assertTrue($result); } + public function testTouchMethodProperlyCallsAPC(): void + { + $key = 'key'; + $ttl = 60; + + $apc = $this->getMockBuilder(ApcWrapper::class)->onlyMethods(['get', 'put'])->getMock(); + + $apc->expects($this->once())->method('get')->with($this->equalTo($key))->willReturn('bar'); + $apc->expects($this->once())->method('put')->with($this->equalTo($key), $this->equalTo('bar'), $this->equalTo($ttl))->willReturn(true); + + $this->assertTrue((new ApcStore($apc))->touch($key, $ttl)); + } + public function testFlushesCached() { $apc = $this->getMockBuilder(ApcWrapper::class)->onlyMethods(['flush'])->getMock(); @@ -132,4 +146,6 @@ public function testFlushesCached() $result = $store->flush(); $this->assertTrue($result); } + + } diff --git a/tests/Cache/CacheArrayStoreTest.php b/tests/Cache/CacheArrayStoreTest.php index 08326e4a4a8a..daf287cd7621 100755 --- a/tests/Cache/CacheArrayStoreTest.php +++ b/tests/Cache/CacheArrayStoreTest.php @@ -69,6 +69,23 @@ public function testItemsCanExpire() $this->assertNull($result); } + public function testTouchExtendsTtl(): void + { + $key = 'key'; + $value = 'value'; + + $store = new ArrayStore; + + Carbon::setTestNow($now = Carbon::now()); + + $store->put($key, $value, 30); + $store->touch($key, 60); + + Carbon::setTestNow($now->addSeconds(45)); + + $this->assertSame($value, $store->get($key)); + } + public function testStoreItemForeverProperlyStoresInArray() { $mock = $this->getMockBuilder(ArrayStore::class)->onlyMethods(['put'])->getMock(); diff --git a/tests/Cache/CacheDatabaseStoreTest.php b/tests/Cache/CacheDatabaseStoreTest.php index e069421b2c43..be6fc62685a6 100755 --- a/tests/Cache/CacheDatabaseStoreTest.php +++ b/tests/Cache/CacheDatabaseStoreTest.php @@ -7,6 +7,7 @@ use Illuminate\Database\Connection; use Illuminate\Database\PostgresConnection; use Illuminate\Database\SQLiteConnection; +use Illuminate\Support\Carbon; use Mockery as m; use PHPUnit\Framework\TestCase; use stdClass; @@ -224,6 +225,50 @@ public function testDecrementReturnsCorrectValues() $this->assertEquals(2, $store->decrement('bar')); } + public function testTouchExtendsTtl() + { + $ttl = 60; + + $store = $this->getMockBuilder(DatabaseStore::class)->onlyMethods(['getTime'])->setConstructorArgs($this->getMocks())->getMock(); + $table = m::mock(stdClass::class); + + $store->getConnection()->shouldReceive('table')->with('table')->andReturn($table); + $store->expects($this->once())->method('getTime')->willReturn(0); + $table->shouldReceive('where')->twice()->andReturn($table); + $table->shouldReceive('update')->once()->with(['expiration' => $ttl])->andReturn(1); + + $this->assertTrue($store->touch('foo', $ttl)); + } + public function testTouchExtendsTtlOnPostgres(): void + { + $ttl = 60; + + $store = $this->getMockBuilder(DatabaseStore::class)->onlyMethods(['getTime'])->setConstructorArgs($this->getPostgresMocks())->getMock(); + $table = m::mock(stdClass::class); + + $store->getConnection()->shouldReceive('table')->with('table')->andReturn($table); + $store->expects($this->once())->method('getTime')->willReturn(0); + $table->shouldReceive('where')->twice()->andReturn($table); + $table->shouldReceive('update')->once()->with(['expiration' => $ttl])->andReturn(1); + + $this->assertTrue($store->touch('foo', $ttl)); + } + + public function testTouchExtendsTtlOnSqlite() + { + $ttl = 60; + + $store = $this->getMockBuilder(DatabaseStore::class)->onlyMethods(['getTime'])->setConstructorArgs($this->getSqliteMocks())->getMock(); + $table = m::mock(stdClass::class); + + $store->getConnection()->shouldReceive('table')->with('table')->andReturn($table); + $store->expects($this->once())->method('getTime')->willReturn(0); + $table->shouldReceive('where')->twice()->andReturn($table); + $table->shouldReceive('update')->once()->with(['expiration' => $ttl])->andReturn(1); + + $this->assertTrue($store->touch('foo', $ttl)); + } + protected function getStore() { return new DatabaseStore(m::mock(Connection::class), 'table', 'prefix'); diff --git a/tests/Cache/CacheDynamoDbStoreTest.php b/tests/Cache/CacheDynamoDbStoreTest.php new file mode 100755 index 000000000000..a7945edc7ccc --- /dev/null +++ b/tests/Cache/CacheDynamoDbStoreTest.php @@ -0,0 +1,46 @@ +assertTrue((new DynamoDbStore($dynamo = new TestDynamo, $table))->touch($key, $ttl)); + + $this->assertTrue( + isset($dynamo->args['UpdateExpression'], $dynamo->args['TableName'], $dynamo->args['Key']['key']['S']) + && $dynamo->args['TableName'] === $table + && $dynamo->args['Key']['key']['S'] === $key + && str_contains($dynamo->args['UpdateExpression'], 'SET') + ); + + $this->assertTrue( + $ttl === $dynamo->args['ExpressionAttributeValues'][':expiry']['N'] + - $dynamo->args['ExpressionAttributeValues'][':now']['N'] + ); + } +} + +class TestDynamo extends DynamoDbClient +{ + public array $args; + + public function __construct() {} + + public function updateItem(array $args): bool + { + $this->args = $args; + + return true; + } +} diff --git a/tests/Cache/CacheFileStoreTest.php b/tests/Cache/CacheFileStoreTest.php index 66ff0b94a8f7..513bc867b1ea 100755 --- a/tests/Cache/CacheFileStoreTest.php +++ b/tests/Cache/CacheFileStoreTest.php @@ -111,6 +111,39 @@ public function testStoreItemProperlyStoresValues() $this->assertTrue($result); } + public function testTouchExtendsTtl(): void + { + $files = $this->mockFilesystem(); + $store = $this->getMockBuilder(FileStore::class)->onlyMethods(['expiration', 'get', 'getPayload'])->setConstructorArgs([$files, __DIR__])->getMock(); + + $now = Carbon::now(); + + $key = 'foo'; + $content = 'Hello World'; + $ttl = 60; + $hash = sha1($key); + $path = __DIR__.'/'.substr($hash, 0, 2).'/'.substr($hash, 2, 2).'/'.$hash; + + $store->expects($this->once()) + ->method('expiration') + ->with($this->equalTo($ttl)) + ->willReturn($now->clone()->addSeconds($ttl)->getTimestamp()); + $store->expects($this->once()) + ->method('getPayload') + ->with($key) + ->willReturn(['data' => $content, 'expiration' => $now->clone()->addSeconds($ttl)->getTimestamp()]); + $files->expects($this->once()) + ->method('put') + ->with( + $this->equalTo($path), + $this->equalTo(($now->clone()->addSeconds($ttl)->getTimestamp()).serialize($content)), + $this->equalTo(true) + ) + ->willReturn(1); + + $this->assertTrue($store->touch($key, $ttl)); + } + public function testStoreItemProperlySetsPermissions() { $files = m::mock(Filesystem::class); diff --git a/tests/Cache/CacheMemcachedStoreTest.php b/tests/Cache/CacheMemcachedStoreTest.php index cb5dadc8f9bb..70fdcfa025b1 100755 --- a/tests/Cache/CacheMemcachedStoreTest.php +++ b/tests/Cache/CacheMemcachedStoreTest.php @@ -67,6 +67,20 @@ public function testSetMethodProperlyCallsMemcache() Carbon::setTestNow(null); } + public function testTouchMethodProperlyCallsMemcache(): void + { + $key = 'key'; + $ttl = 60; + + $now = Carbon::now(); + + $memcache = $this->getMockBuilder(Memcached::class)->onlyMethods(['touch'])->getMock(); + + $memcache->expects($this->once())->method('touch')->with($this->equalTo($key), $this->equalTo($now->addSeconds($ttl)->getTimestamp()))->willReturn(true); + + $this->assertTrue((new MemcachedStore($memcache))->touch($key, $ttl)); + } + public function testIncrementMethodProperlyCallsMemcache() { $memcached = m::mock(Memcached::class); diff --git a/tests/Cache/CacheMemoizedStoreTest.php b/tests/Cache/CacheMemoizedStoreTest.php new file mode 100644 index 000000000000..c848970708f0 --- /dev/null +++ b/tests/Cache/CacheMemoizedStoreTest.php @@ -0,0 +1,29 @@ +put('foo', 'bar', 30); + $store->touch('foo', 60); + + Carbon::setTestNow($now->addSeconds(45)); + + $this->assertSame('bar', $store->get('foo')); + } +} diff --git a/tests/Cache/CacheNullStoreTest.php b/tests/Cache/CacheNullStoreTest.php index 545c9621bc24..f30bedce39d0 100644 --- a/tests/Cache/CacheNullStoreTest.php +++ b/tests/Cache/CacheNullStoreTest.php @@ -33,4 +33,9 @@ public function testIncrementAndDecrementReturnFalse() $this->assertFalse($store->increment('foo')); $this->assertFalse($store->decrement('foo')); } + + public function testTouchReturnsFalse(): void + { + $this->assertFalse((new NullStore)->touch('foo', 30)); + } } diff --git a/tests/Cache/CacheRedisStoreTest.php b/tests/Cache/CacheRedisStoreTest.php index 30a100b22e8f..a6e7e48d5bfb 100755 --- a/tests/Cache/CacheRedisStoreTest.php +++ b/tests/Cache/CacheRedisStoreTest.php @@ -121,12 +121,29 @@ public function testStoreItemForeverProperlyCallsRedis() $this->assertTrue($result); } + public function testTouchMethodProperlyCallsRedis(): void + { + $key = 'key'; + $ttl = 60; + + $redis = $this->getRedis(); + + $redis->getRedis()->shouldReceive('connection')->once()->with('default')->andReturn($redis->getRedis()); + $redis->getRedis()->shouldReceive('expire')->once()->with("prefix:$key", $ttl)->andReturn(true); + + $this->assertTrue($redis->touch($key, $ttl)); + } + public function testForgetMethodProperlyCallsRedis() { + $key = 'key'; + $redis = $this->getRedis(); + $redis->getRedis()->shouldReceive('connection')->once()->with('default')->andReturn($redis->getRedis()); - $redis->getRedis()->shouldReceive('del')->once()->with('prefix:foo'); - $redis->forget('foo'); + $redis->getRedis()->shouldReceive('del')->once()->with("prefix:$key"); + + $redis->forget($key); } public function testFlushesCached() diff --git a/tests/Cache/CacheRepositoryTest.php b/tests/Cache/CacheRepositoryTest.php index 5097a2797795..4be0094f5dc1 100755 --- a/tests/Cache/CacheRepositoryTest.php +++ b/tests/Cache/CacheRepositoryTest.php @@ -433,6 +433,52 @@ public function testNonTaggableRepositoryDoesNotSupportTags() $this->assertFalse($nonTaggableRepo->supportsTags()); } + public function testTouchWithNullTTLRemembersItemForever(): void + { + $key = 'key'; + $ttl = null; + + $repo = $this->getRepository(); + $repo->getStore()->shouldReceive('get')->with($key)->andReturn('bar'); + $repo->getStore()->shouldReceive('forever')->once()->with($key, 'bar')->andReturn(true); + $this->assertTrue($repo->touch($key, $ttl)); + } + + public function testTouchWithSecondsTtlCorrectlyProxiesToStore(): void + { + $key = 'key'; + $ttl = 60; + + $repo = $this->getRepository(); + $repo->getStore()->shouldReceive('get')->with($key)->andReturn('bar'); + $repo->getStore()->shouldReceive('touch')->once()->with($key, $ttl)->andReturn(true); + $this->assertTrue($repo->touch($key, $ttl)); + } + + public function testTouchWithDatetimeTtlCorrectlyProxiesToStore(): void + { + $key = 'key'; + $ttl = 60; + + Carbon::setTestNow($now = Carbon::now()); + + $repo = $this->getRepository(); + $repo->getStore()->shouldReceive('get')->with($key)->andReturn('bar'); + $repo->getStore()->shouldReceive('touch')->once()->with($key, $ttl)->andReturn(true); + $this->assertTrue($repo->touch($key, $now->addSeconds($ttl))); + } + + public function testTouchWithDateIntervalTtlCorrectlyProxiesToStore(): void + { + $key = 'key'; + $ttl = 60; + + $repo = $this->getRepository(); + $repo->getStore()->shouldReceive('get')->with($key)->andReturn('bar'); + $repo->getStore()->shouldReceive('touch')->once()->with($key, $ttl)->andReturn(true); + $this->assertTrue($repo->touch($key, DateInterval::createFromDateString("$ttl seconds"))); + } + protected function getRepository() { $dispatcher = new Dispatcher(m::mock(Container::class)); From 24b0957da08d67565dbe21d6eb54f53a2b87c08c Mon Sep 17 00:00:00 2001 From: Yitz Willroth Date: Sun, 8 Jun 2025 13:02:19 -0400 Subject: [PATCH 2/4] :test: fix failing memoized store test --- src/Illuminate/Support/Facades/Cache.php | 1 + tests/Integration/Cache/MemoizedStoreTest.php | 5 +++++ 2 files changed, 6 insertions(+) diff --git a/src/Illuminate/Support/Facades/Cache.php b/src/Illuminate/Support/Facades/Cache.php index 07553d8bb812..0d49bbe1d3a3 100755 --- a/src/Illuminate/Support/Facades/Cache.php +++ b/src/Illuminate/Support/Facades/Cache.php @@ -36,6 +36,7 @@ * @method static mixed flexible(string $key, array $ttl, callable $callback, array|null $lock = null) * @method static bool forget(string $key) * @method static bool delete(string $key) + * @method static bool touch(string $key, \DateTimeInterface|\DateInterval|int|null $ttl = null) * @method static bool deleteMultiple(iterable $keys) * @method static bool clear() * @method static \Illuminate\Cache\TaggedCache tags(array|mixed $names) diff --git a/tests/Integration/Cache/MemoizedStoreTest.php b/tests/Integration/Cache/MemoizedStoreTest.php index 009906f1555f..a6cd10bd17a7 100644 --- a/tests/Integration/Cache/MemoizedStoreTest.php +++ b/tests/Integration/Cache/MemoizedStoreTest.php @@ -461,6 +461,11 @@ public function forget($key) return Cache::forget(...func_get_args()); } + public function touch(string $key, int $ttl): bool + { + return Cache::touch(...func_get_args()); + } + public function flush() { return Cache::flush(...func_get_args()); From 485efd0f176248def96c3bdf18ee39d298197ffe Mon Sep 17 00:00:00 2001 From: Yitz Willroth Date: Sun, 8 Jun 2025 13:17:00 -0400 Subject: [PATCH 3/4] :clean: remove inadvertent inclusions --- src/Illuminate/Cache/FileStore.php | 3 +-- src/Illuminate/Cache/RedisStore.php | 1 + src/Illuminate/Cache/Repository.php | 18 +++++++++++++++++- tests/Cache/CacheApcStoreTest.php | 2 -- tests/Cache/CacheRedisStoreTest.php | 8 ++------ 5 files changed, 21 insertions(+), 11 deletions(-) diff --git a/src/Illuminate/Cache/FileStore.php b/src/Illuminate/Cache/FileStore.php index f963fa70f8ea..21a104abb109 100755 --- a/src/Illuminate/Cache/FileStore.php +++ b/src/Illuminate/Cache/FileStore.php @@ -312,7 +312,7 @@ protected function getPayload($key) } $expire = substr($contents, 0, 10); - } catch (Exception $e) { + } catch (Exception) { return $this->emptyPayload(); } @@ -328,7 +328,6 @@ protected function getPayload($key) try { $data = unserialize(substr($contents, 10)); } catch (Exception) { - $this->forget($key); return $this->emptyPayload(); diff --git a/src/Illuminate/Cache/RedisStore.php b/src/Illuminate/Cache/RedisStore.php index 8f0365571762..d8909687d54a 100755 --- a/src/Illuminate/Cache/RedisStore.php +++ b/src/Illuminate/Cache/RedisStore.php @@ -466,6 +466,7 @@ protected function serialize($value) * Determine if the given value should be stored as plain value. * * @param mixed $value + * @return bool */ protected function shouldBeStoredWithoutSerialization($value): bool { diff --git a/src/Illuminate/Cache/Repository.php b/src/Illuminate/Cache/Repository.php index 03f3b99b5a98..4a195cc0e788 100755 --- a/src/Illuminate/Cache/Repository.php +++ b/src/Illuminate/Cache/Repository.php @@ -106,6 +106,7 @@ public function missing($key) * * @param array|string $key * @param mixed $default + * @return mixed */ public function get($key, $default = null): mixed { @@ -250,6 +251,8 @@ public function put($key, $value, $ttl = null) /** * {@inheritdoc} + * + * @return bool */ public function set($key, $value, $ttl = null): bool { @@ -259,6 +262,7 @@ public function set($key, $value, $ttl = null): bool /** * Store multiple items in the cache for a given number of seconds. * + * @param array $values * @param \DateTimeInterface|\DateInterval|int|null $ttl * @return bool */ @@ -292,6 +296,7 @@ public function putMany(array $values, $ttl = null) /** * Store multiple items in the cache indefinitely. * + * @param array $values * @return bool */ protected function putManyForever(array $values) @@ -526,7 +531,7 @@ public function flexible($key, $ttl, $callback, $lock = null) public function touch(string $key, \DateTimeInterface|\DateInterval|int|null $ttl = null): bool { $value = $this->get($key); - + if (is_null($value)) { return false; } @@ -557,6 +562,8 @@ public function forget($key) /** * {@inheritdoc} + * + * @return bool */ public function delete($key): bool { @@ -565,6 +572,8 @@ public function delete($key): bool /** * {@inheritdoc} + * + * @return bool */ public function deleteMultiple($keys): bool { @@ -581,6 +590,8 @@ public function deleteMultiple($keys): bool /** * {@inheritdoc} + * + * @return bool */ public function clear(): bool { @@ -742,6 +753,7 @@ public function getEventDispatcher() /** * Set the event dispatcher instance. * + * @param \Illuminate\Contracts\Events\Dispatcher $events * @return void */ public function setEventDispatcher(Dispatcher $events) @@ -753,6 +765,7 @@ public function setEventDispatcher(Dispatcher $events) * Determine if a cached value exists. * * @param string $key + * @return bool */ public function offsetExists($key): bool { @@ -763,6 +776,7 @@ public function offsetExists($key): bool * Retrieve an item from the cache by key. * * @param string $key + * @return mixed */ public function offsetGet($key): mixed { @@ -774,6 +788,7 @@ public function offsetGet($key): mixed * * @param string $key * @param mixed $value + * @return void */ public function offsetSet($key, $value): void { @@ -784,6 +799,7 @@ public function offsetSet($key, $value): void * Remove an item from the cache. * * @param string $key + * @return void */ public function offsetUnset($key): void { diff --git a/tests/Cache/CacheApcStoreTest.php b/tests/Cache/CacheApcStoreTest.php index f43b025f0792..5dc1d49d8e9e 100755 --- a/tests/Cache/CacheApcStoreTest.php +++ b/tests/Cache/CacheApcStoreTest.php @@ -146,6 +146,4 @@ public function testFlushesCached() $result = $store->flush(); $this->assertTrue($result); } - - } diff --git a/tests/Cache/CacheRedisStoreTest.php b/tests/Cache/CacheRedisStoreTest.php index a6e7e48d5bfb..38df3c62d13f 100755 --- a/tests/Cache/CacheRedisStoreTest.php +++ b/tests/Cache/CacheRedisStoreTest.php @@ -136,14 +136,10 @@ public function testTouchMethodProperlyCallsRedis(): void public function testForgetMethodProperlyCallsRedis() { - $key = 'key'; - $redis = $this->getRedis(); - $redis->getRedis()->shouldReceive('connection')->once()->with('default')->andReturn($redis->getRedis()); - $redis->getRedis()->shouldReceive('del')->once()->with("prefix:$key"); - - $redis->forget($key); + $redis->getRedis()->shouldReceive('del')->once()->with('prefix:foo'); + $redis->forget('foo'); } public function testFlushesCached() From d5aeaf8e89a31aa1ffaca41fb6d4dcba2513317a Mon Sep 17 00:00:00 2001 From: Yitz Willroth Date: Wed, 11 Jun 2025 07:27:18 -0400 Subject: [PATCH 4/4] :test: remove unnecessary imports in tests --- tests/Cache/CacheApcStoreTest.php | 1 - tests/Cache/CacheDatabaseStoreTest.php | 1 - 2 files changed, 2 deletions(-) diff --git a/tests/Cache/CacheApcStoreTest.php b/tests/Cache/CacheApcStoreTest.php index 5dc1d49d8e9e..d366a660e671 100755 --- a/tests/Cache/CacheApcStoreTest.php +++ b/tests/Cache/CacheApcStoreTest.php @@ -4,7 +4,6 @@ use Illuminate\Cache\ApcStore; use Illuminate\Cache\ApcWrapper; -use Illuminate\Support\Carbon; use Mockery; use PHPUnit\Framework\TestCase; diff --git a/tests/Cache/CacheDatabaseStoreTest.php b/tests/Cache/CacheDatabaseStoreTest.php index be6fc62685a6..af52ca677eab 100755 --- a/tests/Cache/CacheDatabaseStoreTest.php +++ b/tests/Cache/CacheDatabaseStoreTest.php @@ -7,7 +7,6 @@ use Illuminate\Database\Connection; use Illuminate\Database\PostgresConnection; use Illuminate\Database\SQLiteConnection; -use Illuminate\Support\Carbon; use Mockery as m; use PHPUnit\Framework\TestCase; use stdClass;