Skip to content

Commit df30ce9

Browse files
committed
feature #28718 [Cache] add CacheInterface::delete() + improve CacheTrait (nicolas-grekas)
This PR was merged into the 4.2-dev branch. Discussion ---------- [Cache] add CacheInterface::delete() + improve CacheTrait | Q | A | ------------- | --- | Branch? | 4.2 | Bug fix? | no | New feature? | yes | BC breaks? | no | Deprecations? | no | Tests pass? | yes | Fixed tickets | - | License | MIT | Doc PR | - I've been hesitating a lot on this topic, but I think we should add a `delete()` method to `CacheInterface`. Deleting is a very common invalidation strategy and invalidating by passing `$beta=INF` to `get()` has the drawback of requiring a fetch+unserialize+save-with-past-expiration. That's complexity that a delete removes. This PR fixes an issue found meanwhile on `get()`: passing an `ItemInterface` to its callback makes it harder than required to implement on top of PSR-6. Let's pass a `CacheItemInterface`. Lastly, the early expiration logic can be moved from the component to the trait shipped on contracts. Here we are for all these steps. Commits ------- c6cf690b2f [Cache] add CacheInterface::delete() + improve CacheTrait
2 parents 7e5e157 + bbeabdb commit df30ce9

File tree

6 files changed

+145
-65
lines changed

6 files changed

+145
-65
lines changed

Cache/CacheInterface.php

Lines changed: 27 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -11,31 +11,46 @@
1111

1212
namespace Symfony\Contracts\Cache;
1313

14+
use Psr\Cache\CacheItemInterface;
1415
use Psr\Cache\InvalidArgumentException;
1516

1617
/**
17-
* Gets/Stores items from/to a cache.
18-
*
19-
* On cache misses, a callback is called that should return the missing value.
20-
* This callback is given an ItemInterface object corresponding to the needed key,
21-
* that could be used e.g. for expiration control.
18+
* Covers most simple to advanced caching needs.
2219
*
2320
* @author Nicolas Grekas <[email protected]>
2421
*/
2522
interface CacheInterface
2623
{
2724
/**
28-
* @param string $key The key of the item to retrieve from the cache
29-
* @param callable(ItemInterface):mixed $callback Should return the computed value for the given key/item
30-
* @param float|null $beta A float that, as it grows, controls the likeliness of triggering
31-
* early expiration. 0 disables it, INF forces immediate expiration.
32-
* The default (or providing null) is implementation dependent but should
33-
* typically be 1.0, which should provide optimal stampede protection.
34-
* See https://en.wikipedia.org/wiki/Cache_stampede#Probabilistic_early_expiration
25+
* Fetches a value from the pool or computes it if not found.
26+
*
27+
* On cache misses, a callback is called that should return the missing value.
28+
* This callback is given a PSR-6 CacheItemInterface instance corresponding to the
29+
* requested key, that could be used e.g. for expiration control. It could also
30+
* be an ItemInterface instance when its additional features are needed.
31+
*
32+
* @param string $key The key of the item to retrieve from the cache
33+
* @param callable(CacheItemInterface):mixed $callback Should return the computed value for the given key/item
34+
* @param float|null $beta A float that, as it grows, controls the likeliness of triggering
35+
* early expiration. 0 disables it, INF forces immediate expiration.
36+
* The default (or providing null) is implementation dependent but should
37+
* typically be 1.0, which should provide optimal stampede protection.
38+
* See https://en.wikipedia.org/wiki/Cache_stampede#Probabilistic_early_expiration
3539
*
3640
* @return mixed The value corresponding to the provided key
3741
*
3842
* @throws InvalidArgumentException When $key is not valid or when $beta is negative
3943
*/
4044
public function get(string $key, callable $callback, float $beta = null);
45+
46+
/**
47+
* Removes an item from the pool.
48+
*
49+
* @param string $key The key to delete
50+
*
51+
* @throws InvalidArgumentException When $key is not valid
52+
*
53+
* @return bool True if the item was successfully removed, false if there was any error
54+
*/
55+
public function delete(string $key): bool;
4156
}

Cache/CacheTrait.php

Lines changed: 67 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,67 @@
1+
<?php
2+
3+
/*
4+
* This file is part of the Symfony package.
5+
*
6+
* (c) Fabien Potencier <[email protected]>
7+
*
8+
* For the full copyright and license information, please view the LICENSE
9+
* file that was distributed with this source code.
10+
*/
11+
12+
namespace Symfony\Contracts\Cache;
13+
14+
use Psr\Cache\CacheItemPoolInterface;
15+
use Psr\Cache\InvalidArgumentException;
16+
17+
/**
18+
* An implementation of CacheInterface for PSR-6 CacheItemPoolInterface classes.
19+
*
20+
* @author Nicolas Grekas <[email protected]>
21+
*/
22+
trait CacheTrait
23+
{
24+
/**
25+
* {@inheritdoc}
26+
*/
27+
public function get(string $key, callable $callback, float $beta = null)
28+
{
29+
return $this->doGet($this, $key, $callback, $beta);
30+
}
31+
32+
/**
33+
* {@inheritdoc}
34+
*/
35+
public function delete(string $key): bool
36+
{
37+
return $this->deleteItem($key);
38+
}
39+
40+
private function doGet(CacheItemPoolInterface $pool, string $key, callable $callback, ?float $beta)
41+
{
42+
if (0 > $beta = $beta ?? 1.0) {
43+
throw new class(sprintf('Argument "$beta" provided to "%s::get()" must be a positive number, %f given.', \get_class($this), $beta)) extends \InvalidArgumentException implements InvalidArgumentException {
44+
};
45+
}
46+
47+
$item = $pool->getItem($key);
48+
$recompute = !$item->isHit() || INF === $beta;
49+
50+
if (!$recompute && $item instanceof ItemInterface) {
51+
$metadata = $item->getMetadata();
52+
$expiry = $metadata[ItemInterface::METADATA_EXPIRY] ?? false;
53+
$ctime = $metadata[ItemInterface::METADATA_CTIME] ?? false;
54+
55+
if ($recompute = $ctime && $expiry && $expiry <= microtime(true) - $ctime / 1000 * $beta * log(random_int(1, PHP_INT_MAX) / PHP_INT_MAX)) {
56+
// force applying defaultLifetime to expiry
57+
$item->expiresAt(null);
58+
}
59+
}
60+
61+
if ($recompute) {
62+
$pool->save($item->set($callback($item)));
63+
}
64+
65+
return $item->get();
66+
}
67+
}

Cache/GetForCacheItemPoolTrait.php

Lines changed: 0 additions & 44 deletions
This file was deleted.

Cache/ItemInterface.php

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,8 @@
1616
use Psr\Cache\InvalidArgumentException;
1717

1818
/**
19+
* Augments PSR-6's CacheItemInterface with support for tags and metadata.
20+
*
1921
* @author Nicolas Grekas <[email protected]>
2022
*/
2123
interface ItemInterface extends CacheItemInterface

Cache/TagAwareCacheInterface.php

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -14,12 +14,19 @@
1414
use Psr\Cache\InvalidArgumentException;
1515

1616
/**
17-
* Interface for invalidating cached items using tags.
17+
* Allows invalidating cached items using tags.
1818
*
1919
* @author Nicolas Grekas <[email protected]>
2020
*/
2121
interface TagAwareCacheInterface extends CacheInterface
2222
{
23+
/**
24+
* {@inheritdoc}
25+
*
26+
* @param callable(ItemInterface):mixed $callback Should return the computed value for the given key/item
27+
*/
28+
public function get(string $key, callable $callback, float $beta = null);
29+
2330
/**
2431
* Invalidates cached items using tags.
2532
*

Tests/Cache/GetForCacheItemPoolTraitTest.php renamed to Tests/Cache/CacheTraitTest.php

Lines changed: 41 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -13,24 +13,27 @@
1313

1414
use PHPUnit\Framework\TestCase;
1515
use Psr\Cache\CacheItemInterface;
16-
use Symfony\Contracts\Cache\GetForCacheItemPoolTrait;
16+
use Psr\Cache\CacheItemPoolInterface;
17+
use Symfony\Contracts\Cache\CacheTrait;
1718

1819
/**
1920
* @author Tobias Nyholm <[email protected]>
2021
*/
21-
class GetForCacheItemPoolTraitTest extends TestCase
22+
class CacheTraitTest extends TestCase
2223
{
2324
public function testSave()
2425
{
2526
$item = $this->getMockBuilder(CacheItemInterface::class)->getMock();
27+
$item->method('set')
28+
->willReturn($item);
2629
$item->method('isHit')
2730
->willReturn(false);
2831

2932
$item->expects($this->once())
3033
->method('set')
3134
->with('computed data');
3235

33-
$cache = $this->getMockBuilder(ClassUsingTrait::class)
36+
$cache = $this->getMockBuilder(TestPool::class)
3437
->setMethods(array('getItem', 'save'))
3538
->getMock();
3639
$cache->expects($this->once())
@@ -56,7 +59,7 @@ public function testNoCallbackCallOnHit()
5659
$item->expects($this->never())
5760
->method('set');
5861

59-
$cache = $this->getMockBuilder(ClassUsingTrait::class)
62+
$cache = $this->getMockBuilder(TestPool::class)
6063
->setMethods(array('getItem', 'save'))
6164
->getMock();
6265

@@ -77,6 +80,8 @@ public function testNoCallbackCallOnHit()
7780
public function testRecomputeOnBetaInf()
7881
{
7982
$item = $this->getMockBuilder(CacheItemInterface::class)->getMock();
83+
$item->method('set')
84+
->willReturn($item);
8085
$item->method('isHit')
8186
// We want to recompute even if it is a hit
8287
->willReturn(true);
@@ -85,7 +90,7 @@ public function testRecomputeOnBetaInf()
8590
->method('set')
8691
->with('computed data');
8792

88-
$cache = $this->getMockBuilder(ClassUsingTrait::class)
93+
$cache = $this->getMockBuilder(TestPool::class)
8994
->setMethods(array('getItem', 'save'))
9095
->getMock();
9196

@@ -105,7 +110,7 @@ public function testRecomputeOnBetaInf()
105110

106111
public function testExceptionOnNegativeBeta()
107112
{
108-
$cache = $this->getMockBuilder(ClassUsingTrait::class)
113+
$cache = $this->getMockBuilder(TestPool::class)
109114
->setMethods(array('getItem', 'save'))
110115
->getMock();
111116

@@ -118,15 +123,43 @@ public function testExceptionOnNegativeBeta()
118123
}
119124
}
120125

121-
class ClassUsingTrait
126+
class TestPool implements CacheItemPoolInterface
122127
{
123-
use GetForCacheItemPoolTrait;
128+
use CacheTrait;
129+
130+
public function hasItem($key)
131+
{
132+
}
133+
134+
public function deleteItem($key)
135+
{
136+
}
137+
138+
public function deleteItems(array $keys = array())
139+
{
140+
}
124141

125142
public function getItem($key)
126143
{
127144
}
128145

146+
public function getItems(array $key = array())
147+
{
148+
}
149+
150+
public function saveDeferred(CacheItemInterface $item)
151+
{
152+
}
153+
129154
public function save(CacheItemInterface $item)
130155
{
131156
}
157+
158+
public function commit()
159+
{
160+
}
161+
162+
public function clear()
163+
{
164+
}
132165
}

0 commit comments

Comments
 (0)