Skip to content

[master] Add Cache::touch() & Store::touch() for TTL Extension #55954

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 4 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
14 changes: 14 additions & 0 deletions src/Illuminate/Cache/ApcStore.php
Original file line number Diff line number Diff line change
Expand Up @@ -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.
*
Expand Down
19 changes: 19 additions & 0 deletions src/Illuminate/Cache/ArrayStore.php
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
namespace Illuminate\Cache;

use Illuminate\Contracts\Cache\LockProvider;
use Illuminate\Support\Arr;
use Illuminate\Support\Carbon;
use Illuminate\Support\InteractsWithTime;

Expand Down Expand Up @@ -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.
*
Expand Down
11 changes: 11 additions & 0 deletions src/Illuminate/Cache/DatabaseStore.php
Original file line number Diff line number Diff line change
Expand Up @@ -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.
*
Expand Down
33 changes: 33 additions & 0 deletions src/Illuminate/Cache/DynamoDbStore.php
Original file line number Diff line number Diff line change
Expand Up @@ -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.
*
Expand Down
14 changes: 14 additions & 0 deletions src/Illuminate/Cache/FileStore.php
Original file line number Diff line number Diff line change
Expand Up @@ -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.
*
Expand Down
8 changes: 8 additions & 0 deletions src/Illuminate/Cache/MemcachedStore.php
Original file line number Diff line number Diff line change
Expand Up @@ -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.
*
Expand Down
10 changes: 10 additions & 0 deletions src/Illuminate/Cache/MemoizedStore.php
Original file line number Diff line number Diff line change
Expand Up @@ -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.
*
Expand Down
8 changes: 8 additions & 0 deletions src/Illuminate/Cache/NullStore.php
Original file line number Diff line number Diff line change
Expand Up @@ -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.
*
Expand Down
8 changes: 8 additions & 0 deletions src/Illuminate/Cache/RedisStore.php
Original file line number Diff line number Diff line change
Expand Up @@ -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.
*
Expand Down
16 changes: 16 additions & 0 deletions src/Illuminate/Cache/Repository.php
Original file line number Diff line number Diff line change
Expand Up @@ -525,6 +525,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.
*
Expand Down
7 changes: 7 additions & 0 deletions src/Illuminate/Contracts/Cache/Repository.php
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,8 @@
namespace Illuminate\Contracts\Cache;

use Closure;
use DateInterval;
use DateTimeInterface;
use Psr\SimpleCache\CacheInterface;

interface Repository extends CacheInterface
Expand Down Expand Up @@ -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;
Copy link
Member

Choose a reason for hiding this comment

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

Adding a method to a public interface is a major breaking change.

Copy link
Member

Choose a reason for hiding this comment

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

this is targetting master now, should be fine.

Copy link
Contributor Author

@yitzwillroth yitzwillroth Jun 11, 2025

Choose a reason for hiding this comment

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

In theory, I could drop the notation from the Cache facade and the method from the Repository interface, but would still need to do something like method_exists and return false or throw an exception that the driver doesn't support touch() if we dropped it from the Store interface. That would allow it to target 12.x, but not sure that's necessarily better than targeting master.

I'm open to whatever y'all think, of course.


/**
* Get an item from the cache, or execute the given Closure and store the result forever.
*
Expand Down
5 changes: 5 additions & 0 deletions src/Illuminate/Contracts/Cache/Store.php
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Copy link
Member

Choose a reason for hiding this comment

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

Adding a method to a public interface is a major breaking change.

Copy link
Member

Choose a reason for hiding this comment

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

this is targetting master now, should be fine.

Copy link
Contributor Author

@yitzwillroth yitzwillroth Jun 11, 2025

Choose a reason for hiding this comment

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

It can be removed from the Store interface, but that would require doing something like method_exists and return false or throw an exception that the driver doesn't support the feature in Repository::touch(). That would allow it to target 12.x, but not sure that's necessarily better than targeting master.

I'm open to whatever y'all think, of course.


/**
* Get the cache key prefix.
*
Expand Down
1 change: 1 addition & 0 deletions src/Illuminate/Support/Facades/Cache.php
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down
13 changes: 13 additions & 0 deletions tests/Cache/CacheApcStoreTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -124,6 +124,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();
Expand Down
17 changes: 17 additions & 0 deletions tests/Cache/CacheArrayStoreTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -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();
Expand Down
44 changes: 44 additions & 0 deletions tests/Cache/CacheDatabaseStoreTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -224,6 +224,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');
Expand Down
Loading