diff --git a/src/Symfony/Contracts/CHANGELOG.md b/src/Symfony/Contracts/CHANGELOG.md index a6c997d80c755..7dc074f70cbb4 100644 --- a/src/Symfony/Contracts/CHANGELOG.md +++ b/src/Symfony/Contracts/CHANGELOG.md @@ -6,3 +6,4 @@ CHANGELOG * added `Service\ResetInterface` to provide a way to reset an object to its initial state * added `Translation\TranslatorInterface` and `Translation\TranslatorTrait` + * added `Cache` contract to extend PSR-6 with tag invalidation, callback-based computation and stampede protection diff --git a/src/Symfony/Contracts/Cache/CacheInterface.php b/src/Symfony/Contracts/Cache/CacheInterface.php new file mode 100644 index 0000000000000..ad756a7b28d54 --- /dev/null +++ b/src/Symfony/Contracts/Cache/CacheInterface.php @@ -0,0 +1,40 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Contracts\Cache; + +use Psr\Cache\InvalidArgumentException; + +/** + * Gets/Stores items from/to a cache. + * + * On cache misses, a callback is called that should return the missing value. + * This callback is given an ItemInterface object corresponding to the needed key, + * that could be used e.g. for expiration control. + * + * @author Nicolas Grekas + */ +interface CacheInterface +{ + /** + * @param callable(ItemInterface):mixed $callback Should return the computed value for the given key/item + * @param float|null $beta A float that, as it grows, controls the likeliness of triggering + * early expiration. 0 disables it, INF forces immediate expiration. + * The default (or providing null) is implementation dependent but should + * typically be 1.0, which should provide optimal stampede protection. + * See https://en.wikipedia.org/wiki/Cache_stampede#Probabilistic_early_expiration + * + * @return mixed The value corresponding to the provided key + * + * @throws InvalidArgumentException When $key is not valid or when $beta is negative + */ + public function get(string $key, callable $callback, float $beta = null); +} diff --git a/src/Symfony/Contracts/Cache/GetForCacheItemPoolTrait.php b/src/Symfony/Contracts/Cache/GetForCacheItemPoolTrait.php new file mode 100644 index 0000000000000..8e8e514aef7f8 --- /dev/null +++ b/src/Symfony/Contracts/Cache/GetForCacheItemPoolTrait.php @@ -0,0 +1,43 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Contracts\Cache; + +use Psr\Cache\CacheItemPoolInterface; +use Psr\Cache\InvalidArgumentException; + +/** + * A simple implementation of CacheInterface::get() for PSR-6 CacheItemPoolInterface classes. + * + * @author Nicolas Grekas + */ +trait GetForCacheItemPoolTrait +{ + /** + * {@inheritdoc} + */ + public function get(string $key, callable $callback, float $beta = null) + { + if (0 > $beta) { + 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 { + }; + } + + $item = $this->getItem($key); + + if (INF === $beta || !$item->isHit()) { + $value = $callback($item); + $item->set($value); + } + + return $item->get(); + } +} diff --git a/src/Symfony/Contracts/Cache/ItemInterface.php b/src/Symfony/Contracts/Cache/ItemInterface.php new file mode 100644 index 0000000000000..8b02c35600f81 --- /dev/null +++ b/src/Symfony/Contracts/Cache/ItemInterface.php @@ -0,0 +1,58 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Contracts\Cache; + +use Psr\Cache\CacheException; +use Psr\Cache\CacheItemInterface; +use Psr\Cache\InvalidArgumentException; + +/** + * @author Nicolas Grekas + */ +interface ItemInterface extends CacheItemInterface +{ + /** + * References the Unix timestamp stating when the item will expire. + */ + const METADATA_EXPIRY = 'expiry'; + + /** + * References the time the item took to be created, in milliseconds. + */ + const METADATA_CTIME = 'ctime'; + + /** + * References the list of tags that were assigned to the item, as string[]. + */ + const METADATA_TAGS = 'tags'; + + /** + * Adds a tag to a cache item. + * + * Tags are strings that follow the same validation rules as keys. + * + * @param string|string[] $tags A tag or array of tags + * + * @return $this + * + * @throws InvalidArgumentException When $tag is not valid + * @throws CacheException When the item comes from a pool that is not tag-aware + */ + public function tag($tags): self; + + /** + * Returns a list of metadata info that were saved alongside with the cached value. + * + * See ItemInterface::METADATA_* consts for keys potentially found in the returned array. + */ + public function getMetadata(): array; +} diff --git a/src/Symfony/Contracts/Cache/TagAwareCacheInterface.php b/src/Symfony/Contracts/Cache/TagAwareCacheInterface.php new file mode 100644 index 0000000000000..aa8aaaba7f4f8 --- /dev/null +++ b/src/Symfony/Contracts/Cache/TagAwareCacheInterface.php @@ -0,0 +1,38 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Contracts\Cache; + +use Psr\Cache\InvalidArgumentException; + +/** + * Interface for invalidating cached items using tags. + * + * @author Nicolas Grekas + */ +interface TagAwareCacheInterface extends CacheInterface +{ + /** + * Invalidates cached items using tags. + * + * When implemented on a PSR-6 pool, invalidation should not apply + * to deferred items. Instead, they should be committed as usual. + * This allows replacing old tagged values by new ones without + * race conditions. + * + * @param string[] $tags An array of tags to invalidate + * + * @return bool True on success + * + * @throws InvalidArgumentException When $tags is not valid + */ + public function invalidateTags(array $tags); +} diff --git a/src/Symfony/Contracts/README.md b/src/Symfony/Contracts/README.md index 062df3ee541e3..1faf1de23b821 100644 --- a/src/Symfony/Contracts/README.md +++ b/src/Symfony/Contracts/README.md @@ -15,6 +15,10 @@ Design Principles * all contracts must have a proven implementation to enter this repository; * they must be backward compatible with existing Symfony components. +Packages that implement specific contracts should list them in the "provide" +section of their "composer.json" file, using the `symfony/*-contracts` +convention (e.g. `"provide": { "symfony/cache-contracts": "1.0" }`). + FAQ --- diff --git a/src/Symfony/Contracts/composer.json b/src/Symfony/Contracts/composer.json index 9deed9d4706f5..03e05144b80bd 100644 --- a/src/Symfony/Contracts/composer.json +++ b/src/Symfony/Contracts/composer.json @@ -18,6 +18,9 @@ "require": { "php": "^7.1.3" }, + "suggest": { + "psr/cache": "When using the Cache contract" + }, "autoload": { "psr-4": { "Symfony\\Contracts\\": "" }, "exclude-from-classmap": [