├── Adapter ├── AbstractAdapter.php ├── AbstractTagAwareAdapter.php ├── AdapterInterface.php ├── ApcuAdapter.php ├── ArrayAdapter.php ├── ChainAdapter.php ├── CouchbaseBucketAdapter.php ├── CouchbaseCollectionAdapter.php ├── DoctrineDbalAdapter.php ├── FilesystemAdapter.php ├── FilesystemTagAwareAdapter.php ├── MemcachedAdapter.php ├── NullAdapter.php ├── ParameterNormalizer.php ├── PdoAdapter.php ├── PhpArrayAdapter.php ├── PhpFilesAdapter.php ├── ProxyAdapter.php ├── Psr16Adapter.php ├── RedisAdapter.php ├── RedisTagAwareAdapter.php ├── TagAwareAdapter.php ├── TagAwareAdapterInterface.php ├── TraceableAdapter.php └── TraceableTagAwareAdapter.php ├── CHANGELOG.md ├── CacheItem.php ├── DataCollector └── CacheDataCollector.php ├── DependencyInjection ├── CacheCollectorPass.php ├── CachePoolClearerPass.php ├── CachePoolPass.php └── CachePoolPrunerPass.php ├── Exception ├── BadMethodCallException.php ├── CacheException.php ├── InvalidArgumentException.php └── LogicException.php ├── LICENSE ├── LockRegistry.php ├── Marshaller ├── DefaultMarshaller.php ├── DeflateMarshaller.php ├── MarshallerInterface.php ├── SodiumMarshaller.php └── TagAwareMarshaller.php ├── Messenger ├── EarlyExpirationDispatcher.php ├── EarlyExpirationHandler.php └── EarlyExpirationMessage.php ├── PruneableInterface.php ├── Psr16Cache.php ├── README.md ├── ResettableInterface.php ├── Traits ├── AbstractAdapterTrait.php ├── ContractsTrait.php ├── FilesystemCommonTrait.php ├── FilesystemTrait.php ├── ProxyTrait.php ├── Redis5Proxy.php ├── Redis6Proxy.php ├── Redis6ProxyTrait.php ├── RedisCluster5Proxy.php ├── RedisCluster6Proxy.php ├── RedisCluster6ProxyTrait.php ├── RedisClusterNodeProxy.php ├── RedisClusterProxy.php ├── RedisProxy.php ├── RedisProxyTrait.php ├── RedisTrait.php ├── Relay │ ├── CopyTrait.php │ ├── GeosearchTrait.php │ ├── GetrangeTrait.php │ ├── HsetTrait.php │ ├── MoveTrait.php │ ├── NullableReturnTrait.php │ └── PfcountTrait.php ├── RelayClusterProxy.php ├── RelayProxy.php ├── RelayProxyTrait.php └── ValueWrapper.php └── composer.json /Adapter/AbstractAdapter.php: -------------------------------------------------------------------------------- 1 | 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\Component\Cache\Adapter; 13 | 14 | use Psr\Log\LoggerAwareInterface; 15 | use Psr\Log\LoggerInterface; 16 | use Symfony\Component\Cache\CacheItem; 17 | use Symfony\Component\Cache\Exception\InvalidArgumentException; 18 | use Symfony\Component\Cache\ResettableInterface; 19 | use Symfony\Component\Cache\Traits\AbstractAdapterTrait; 20 | use Symfony\Component\Cache\Traits\ContractsTrait; 21 | use Symfony\Contracts\Cache\CacheInterface; 22 | use Symfony\Contracts\Cache\NamespacedPoolInterface; 23 | 24 | /** 25 | * @author Nicolas Grekas 26 | */ 27 | abstract class AbstractAdapter implements AdapterInterface, CacheInterface, NamespacedPoolInterface, LoggerAwareInterface, ResettableInterface 28 | { 29 | use AbstractAdapterTrait; 30 | use ContractsTrait; 31 | 32 | /** 33 | * @internal 34 | */ 35 | protected const NS_SEPARATOR = ':'; 36 | 37 | private static bool $apcuSupported; 38 | 39 | protected function __construct(string $namespace = '', int $defaultLifetime = 0) 40 | { 41 | if ('' !== $namespace) { 42 | if (str_contains($namespace, static::NS_SEPARATOR)) { 43 | if (str_contains($namespace, static::NS_SEPARATOR.static::NS_SEPARATOR)) { 44 | throw new InvalidArgumentException(\sprintf('Cache namespace "%s" contains empty sub-namespace.', $namespace)); 45 | } 46 | CacheItem::validateKey(str_replace(static::NS_SEPARATOR, '', $namespace)); 47 | } else { 48 | CacheItem::validateKey($namespace); 49 | } 50 | $this->namespace = $namespace.static::NS_SEPARATOR; 51 | } 52 | $this->rootNamespace = $this->namespace; 53 | 54 | $this->defaultLifetime = $defaultLifetime; 55 | if (null !== $this->maxIdLength && \strlen($namespace) > $this->maxIdLength - 24) { 56 | throw new InvalidArgumentException(\sprintf('Namespace must be %d chars max, %d given ("%s").', $this->maxIdLength - 24, \strlen($namespace), $namespace)); 57 | } 58 | self::$createCacheItem ??= \Closure::bind( 59 | static function ($key, $value, $isHit) { 60 | $item = new CacheItem(); 61 | $item->key = $key; 62 | $item->value = $value; 63 | $item->isHit = $isHit; 64 | $item->unpack(); 65 | 66 | return $item; 67 | }, 68 | null, 69 | CacheItem::class 70 | ); 71 | self::$mergeByLifetime ??= \Closure::bind( 72 | static function ($deferred, $namespace, &$expiredIds, $getId, $defaultLifetime) { 73 | $byLifetime = []; 74 | $now = microtime(true); 75 | $expiredIds = []; 76 | 77 | foreach ($deferred as $key => $item) { 78 | $key = (string) $key; 79 | if (null === $item->expiry) { 80 | $ttl = 0 < $defaultLifetime ? $defaultLifetime : 0; 81 | } elseif (!$item->expiry) { 82 | $ttl = 0; 83 | } elseif (0 >= $ttl = (int) (0.1 + $item->expiry - $now)) { 84 | $expiredIds[] = $getId($key); 85 | continue; 86 | } 87 | $byLifetime[$ttl][$getId($key)] = $item->pack(); 88 | } 89 | 90 | return $byLifetime; 91 | }, 92 | null, 93 | CacheItem::class 94 | ); 95 | } 96 | 97 | /** 98 | * Returns the best possible adapter that your runtime supports. 99 | * 100 | * Using ApcuAdapter makes system caches compatible with read-only filesystems. 101 | */ 102 | public static function createSystemCache(string $namespace, int $defaultLifetime, string $version, string $directory, ?LoggerInterface $logger = null): AdapterInterface 103 | { 104 | $opcache = new PhpFilesAdapter($namespace, $defaultLifetime, $directory, true); 105 | if (null !== $logger) { 106 | $opcache->setLogger($logger); 107 | } 108 | 109 | if (!self::$apcuSupported ??= ApcuAdapter::isSupported()) { 110 | return $opcache; 111 | } 112 | 113 | if ('cli' === \PHP_SAPI && !filter_var(\ini_get('apc.enable_cli'), \FILTER_VALIDATE_BOOL)) { 114 | return $opcache; 115 | } 116 | 117 | $apcu = new ApcuAdapter($namespace, intdiv($defaultLifetime, 5), $version); 118 | if (null !== $logger) { 119 | $apcu->setLogger($logger); 120 | } 121 | 122 | return new ChainAdapter([$apcu, $opcache]); 123 | } 124 | 125 | public static function createConnection(#[\SensitiveParameter] string $dsn, array $options = []): mixed 126 | { 127 | if (str_starts_with($dsn, 'redis:') || str_starts_with($dsn, 'rediss:') || str_starts_with($dsn, 'valkey:') || str_starts_with($dsn, 'valkeys:')) { 128 | return RedisAdapter::createConnection($dsn, $options); 129 | } 130 | if (str_starts_with($dsn, 'memcached:')) { 131 | return MemcachedAdapter::createConnection($dsn, $options); 132 | } 133 | if (str_starts_with($dsn, 'couchbase:')) { 134 | if (class_exists(\CouchbaseBucket::class) && CouchbaseBucketAdapter::isSupported()) { 135 | return CouchbaseBucketAdapter::createConnection($dsn, $options); 136 | } 137 | 138 | return CouchbaseCollectionAdapter::createConnection($dsn, $options); 139 | } 140 | if (preg_match('/^(mysql|oci|pgsql|sqlsrv|sqlite):/', $dsn)) { 141 | return PdoAdapter::createConnection($dsn, $options); 142 | } 143 | 144 | throw new InvalidArgumentException('Unsupported DSN: it does not start with "redis[s]:", "valkey[s]:", "memcached:", "couchbase:", "mysql:", "oci:", "pgsql:", "sqlsrv:" nor "sqlite:".'); 145 | } 146 | 147 | public function commit(): bool 148 | { 149 | $ok = true; 150 | $byLifetime = (self::$mergeByLifetime)($this->deferred, $this->namespace, $expiredIds, $this->getId(...), $this->defaultLifetime); 151 | $retry = $this->deferred = []; 152 | 153 | if ($expiredIds) { 154 | try { 155 | $this->doDelete($expiredIds); 156 | } catch (\Exception $e) { 157 | $ok = false; 158 | CacheItem::log($this->logger, 'Failed to delete expired items: '.$e->getMessage(), ['exception' => $e, 'cache-adapter' => get_debug_type($this)]); 159 | } 160 | } 161 | foreach ($byLifetime as $lifetime => $values) { 162 | try { 163 | $e = $this->doSave($values, $lifetime); 164 | } catch (\Exception $e) { 165 | } 166 | if (true === $e || [] === $e) { 167 | continue; 168 | } 169 | if (\is_array($e) || 1 === \count($values)) { 170 | foreach (\is_array($e) ? $e : array_keys($values) as $id) { 171 | $ok = false; 172 | $v = $values[$id]; 173 | $type = get_debug_type($v); 174 | $message = \sprintf('Failed to save key "{key}" of type %s%s', $type, $e instanceof \Exception ? ': '.$e->getMessage() : '.'); 175 | CacheItem::log($this->logger, $message, ['key' => substr($id, \strlen($this->rootNamespace)), 'exception' => $e instanceof \Exception ? $e : null, 'cache-adapter' => get_debug_type($this)]); 176 | } 177 | } else { 178 | foreach ($values as $id => $v) { 179 | $retry[$lifetime][] = $id; 180 | } 181 | } 182 | } 183 | 184 | // When bulk-save failed, retry each item individually 185 | foreach ($retry as $lifetime => $ids) { 186 | foreach ($ids as $id) { 187 | try { 188 | $v = $byLifetime[$lifetime][$id]; 189 | $e = $this->doSave([$id => $v], $lifetime); 190 | } catch (\Exception $e) { 191 | } 192 | if (true === $e || [] === $e) { 193 | continue; 194 | } 195 | $ok = false; 196 | $type = get_debug_type($v); 197 | $message = \sprintf('Failed to save key "{key}" of type %s%s', $type, $e instanceof \Exception ? ': '.$e->getMessage() : '.'); 198 | CacheItem::log($this->logger, $message, ['key' => substr($id, \strlen($this->rootNamespace)), 'exception' => $e instanceof \Exception ? $e : null, 'cache-adapter' => get_debug_type($this)]); 199 | } 200 | } 201 | 202 | return $ok; 203 | } 204 | } 205 | -------------------------------------------------------------------------------- /Adapter/AdapterInterface.php: -------------------------------------------------------------------------------- 1 | 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\Component\Cache\Adapter; 13 | 14 | use Psr\Cache\CacheItemPoolInterface; 15 | use Symfony\Component\Cache\CacheItem; 16 | 17 | // Help opcache.preload discover always-needed symbols 18 | class_exists(CacheItem::class); 19 | 20 | /** 21 | * Interface for adapters managing instances of Symfony's CacheItem. 22 | * 23 | * @author Kévin Dunglas 24 | */ 25 | interface AdapterInterface extends CacheItemPoolInterface 26 | { 27 | public function getItem(mixed $key): CacheItem; 28 | 29 | /** 30 | * @return iterable 31 | */ 32 | public function getItems(array $keys = []): iterable; 33 | 34 | public function clear(string $prefix = ''): bool; 35 | } 36 | -------------------------------------------------------------------------------- /Adapter/ApcuAdapter.php: -------------------------------------------------------------------------------- 1 | 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\Component\Cache\Adapter; 13 | 14 | use Symfony\Component\Cache\CacheItem; 15 | use Symfony\Component\Cache\Exception\CacheException; 16 | use Symfony\Component\Cache\Marshaller\MarshallerInterface; 17 | 18 | /** 19 | * @author Nicolas Grekas 20 | */ 21 | class ApcuAdapter extends AbstractAdapter 22 | { 23 | /** 24 | * @throws CacheException if APCu is not enabled 25 | */ 26 | public function __construct( 27 | string $namespace = '', 28 | int $defaultLifetime = 0, 29 | ?string $version = null, 30 | private ?MarshallerInterface $marshaller = null, 31 | ) { 32 | if (!static::isSupported()) { 33 | throw new CacheException('APCu is not enabled.'); 34 | } 35 | if ('cli' === \PHP_SAPI) { 36 | ini_set('apc.use_request_time', 0); 37 | } 38 | parent::__construct($namespace, $defaultLifetime); 39 | 40 | if (null !== $version) { 41 | CacheItem::validateKey($version); 42 | 43 | if (!apcu_exists($version.'@'.$namespace)) { 44 | $this->doClear($namespace); 45 | apcu_add($version.'@'.$namespace, null); 46 | } 47 | } 48 | } 49 | 50 | public static function isSupported(): bool 51 | { 52 | return \function_exists('apcu_fetch') && filter_var(\ini_get('apc.enabled'), \FILTER_VALIDATE_BOOL); 53 | } 54 | 55 | protected function doFetch(array $ids): iterable 56 | { 57 | $unserializeCallbackHandler = ini_set('unserialize_callback_func', __CLASS__.'::handleUnserializeCallback'); 58 | try { 59 | $values = []; 60 | foreach (apcu_fetch($ids, $ok) ?: [] as $k => $v) { 61 | if (null !== $v || $ok) { 62 | $values[$k] = null !== $this->marshaller ? $this->marshaller->unmarshall($v) : $v; 63 | } 64 | } 65 | 66 | return $values; 67 | } catch (\Error $e) { 68 | throw new \ErrorException($e->getMessage(), $e->getCode(), \E_ERROR, $e->getFile(), $e->getLine()); 69 | } finally { 70 | ini_set('unserialize_callback_func', $unserializeCallbackHandler); 71 | } 72 | } 73 | 74 | protected function doHave(string $id): bool 75 | { 76 | return apcu_exists($id); 77 | } 78 | 79 | protected function doClear(string $namespace): bool 80 | { 81 | return isset($namespace[0]) && class_exists(\APCUIterator::class, false) && ('cli' !== \PHP_SAPI || filter_var(\ini_get('apc.enable_cli'), \FILTER_VALIDATE_BOOL)) 82 | ? apcu_delete(new \APCUIterator(\sprintf('/^%s/', preg_quote($namespace, '/')), \APC_ITER_KEY)) 83 | : apcu_clear_cache(); 84 | } 85 | 86 | protected function doDelete(array $ids): bool 87 | { 88 | foreach ($ids as $id) { 89 | apcu_delete($id); 90 | } 91 | 92 | return true; 93 | } 94 | 95 | protected function doSave(array $values, int $lifetime): array|bool 96 | { 97 | if (null !== $this->marshaller && (!$values = $this->marshaller->marshall($values, $failed))) { 98 | return $failed; 99 | } 100 | 101 | if (false === $failures = apcu_store($values, null, $lifetime)) { 102 | $failures = $values; 103 | } 104 | 105 | return array_keys($failures); 106 | } 107 | } 108 | -------------------------------------------------------------------------------- /Adapter/ChainAdapter.php: -------------------------------------------------------------------------------- 1 | 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\Component\Cache\Adapter; 13 | 14 | use Psr\Cache\CacheItemInterface; 15 | use Psr\Cache\CacheItemPoolInterface; 16 | use Symfony\Component\Cache\CacheItem; 17 | use Symfony\Component\Cache\Exception\BadMethodCallException; 18 | use Symfony\Component\Cache\Exception\InvalidArgumentException; 19 | use Symfony\Component\Cache\PruneableInterface; 20 | use Symfony\Component\Cache\ResettableInterface; 21 | use Symfony\Component\Cache\Traits\ContractsTrait; 22 | use Symfony\Contracts\Cache\CacheInterface; 23 | use Symfony\Contracts\Cache\NamespacedPoolInterface; 24 | use Symfony\Contracts\Service\ResetInterface; 25 | 26 | /** 27 | * Chains several adapters together. 28 | * 29 | * Cached items are fetched from the first adapter having them in its data store. 30 | * They are saved and deleted in all adapters at once. 31 | * 32 | * @author Kévin Dunglas 33 | */ 34 | class ChainAdapter implements AdapterInterface, CacheInterface, NamespacedPoolInterface, PruneableInterface, ResettableInterface 35 | { 36 | use ContractsTrait; 37 | 38 | private array $adapters = []; 39 | private int $adapterCount; 40 | 41 | private static \Closure $syncItem; 42 | 43 | /** 44 | * @param CacheItemPoolInterface[] $adapters The ordered list of adapters used to fetch cached items 45 | * @param int $defaultLifetime The default lifetime of items propagated from lower adapters to upper ones 46 | */ 47 | public function __construct( 48 | array $adapters, 49 | private int $defaultLifetime = 0, 50 | ) { 51 | if (!$adapters) { 52 | throw new InvalidArgumentException('At least one adapter must be specified.'); 53 | } 54 | 55 | foreach ($adapters as $adapter) { 56 | if (!$adapter instanceof CacheItemPoolInterface) { 57 | throw new InvalidArgumentException(\sprintf('The class "%s" does not implement the "%s" interface.', get_debug_type($adapter), CacheItemPoolInterface::class)); 58 | } 59 | if ('cli' === \PHP_SAPI && $adapter instanceof ApcuAdapter && !filter_var(\ini_get('apc.enable_cli'), \FILTER_VALIDATE_BOOL)) { 60 | continue; // skip putting APCu in the chain when the backend is disabled 61 | } 62 | 63 | if ($adapter instanceof AdapterInterface) { 64 | $this->adapters[] = $adapter; 65 | } else { 66 | $this->adapters[] = new ProxyAdapter($adapter); 67 | } 68 | } 69 | $this->adapterCount = \count($this->adapters); 70 | 71 | self::$syncItem ??= \Closure::bind( 72 | static function ($sourceItem, $item, $defaultLifetime, $sourceMetadata = null) { 73 | $sourceItem->isTaggable = false; 74 | $sourceMetadata ??= $sourceItem->metadata; 75 | 76 | $item->value = $sourceItem->value; 77 | $item->isHit = $sourceItem->isHit; 78 | $item->metadata = $item->newMetadata = $sourceItem->metadata = $sourceMetadata; 79 | 80 | if (isset($item->metadata[CacheItem::METADATA_EXPIRY])) { 81 | $item->expiresAt(\DateTimeImmutable::createFromFormat('U.u', \sprintf('%.6F', $item->metadata[CacheItem::METADATA_EXPIRY]))); 82 | } elseif (0 < $defaultLifetime) { 83 | $item->expiresAfter($defaultLifetime); 84 | } 85 | 86 | return $item; 87 | }, 88 | null, 89 | CacheItem::class 90 | ); 91 | } 92 | 93 | public function get(string $key, callable $callback, ?float $beta = null, ?array &$metadata = null): mixed 94 | { 95 | $doSave = true; 96 | $callback = static function (CacheItem $item, bool &$save) use ($callback, &$doSave) { 97 | $value = $callback($item, $save); 98 | $doSave = $save; 99 | 100 | return $value; 101 | }; 102 | 103 | $wrap = function (?CacheItem $item = null, bool &$save = true) use ($key, $callback, $beta, &$wrap, &$doSave, &$metadata) { 104 | static $lastItem; 105 | static $i = 0; 106 | $adapter = $this->adapters[$i]; 107 | if (isset($this->adapters[++$i])) { 108 | $callback = $wrap; 109 | $beta = \INF === $beta ? \INF : 0; 110 | } 111 | if ($adapter instanceof CacheInterface) { 112 | $value = $adapter->get($key, $callback, $beta, $metadata); 113 | } else { 114 | $value = $this->doGet($adapter, $key, $callback, $beta, $metadata); 115 | } 116 | if (null !== $item) { 117 | (self::$syncItem)($lastItem ??= $item, $item, $this->defaultLifetime, $metadata); 118 | } 119 | $save = $doSave; 120 | 121 | return $value; 122 | }; 123 | 124 | return $wrap(); 125 | } 126 | 127 | public function getItem(mixed $key): CacheItem 128 | { 129 | $syncItem = self::$syncItem; 130 | $misses = []; 131 | 132 | foreach ($this->adapters as $i => $adapter) { 133 | $item = $adapter->getItem($key); 134 | 135 | if ($item->isHit()) { 136 | while (0 <= --$i) { 137 | $this->adapters[$i]->save($syncItem($item, $misses[$i], $this->defaultLifetime)); 138 | } 139 | 140 | return $item; 141 | } 142 | 143 | $misses[$i] = $item; 144 | } 145 | 146 | return $item; 147 | } 148 | 149 | public function getItems(array $keys = []): iterable 150 | { 151 | return $this->generateItems($this->adapters[0]->getItems($keys), 0); 152 | } 153 | 154 | private function generateItems(iterable $items, int $adapterIndex): \Generator 155 | { 156 | $missing = []; 157 | $misses = []; 158 | $nextAdapterIndex = $adapterIndex + 1; 159 | $nextAdapter = $this->adapters[$nextAdapterIndex] ?? null; 160 | 161 | foreach ($items as $k => $item) { 162 | if (!$nextAdapter || $item->isHit()) { 163 | yield $k => $item; 164 | } else { 165 | $missing[] = $k; 166 | $misses[$k] = $item; 167 | } 168 | } 169 | 170 | if ($missing) { 171 | $syncItem = self::$syncItem; 172 | $adapter = $this->adapters[$adapterIndex]; 173 | $items = $this->generateItems($nextAdapter->getItems($missing), $nextAdapterIndex); 174 | 175 | foreach ($items as $k => $item) { 176 | if ($item->isHit()) { 177 | $adapter->save($syncItem($item, $misses[$k], $this->defaultLifetime)); 178 | } 179 | 180 | yield $k => $item; 181 | } 182 | } 183 | } 184 | 185 | public function hasItem(mixed $key): bool 186 | { 187 | foreach ($this->adapters as $adapter) { 188 | if ($adapter->hasItem($key)) { 189 | return true; 190 | } 191 | } 192 | 193 | return false; 194 | } 195 | 196 | public function clear(string $prefix = ''): bool 197 | { 198 | $cleared = true; 199 | $i = $this->adapterCount; 200 | 201 | while ($i--) { 202 | if ($this->adapters[$i] instanceof AdapterInterface) { 203 | $cleared = $this->adapters[$i]->clear($prefix) && $cleared; 204 | } else { 205 | $cleared = $this->adapters[$i]->clear() && $cleared; 206 | } 207 | } 208 | 209 | return $cleared; 210 | } 211 | 212 | public function deleteItem(mixed $key): bool 213 | { 214 | $deleted = true; 215 | $i = $this->adapterCount; 216 | 217 | while ($i--) { 218 | $deleted = $this->adapters[$i]->deleteItem($key) && $deleted; 219 | } 220 | 221 | return $deleted; 222 | } 223 | 224 | public function deleteItems(array $keys): bool 225 | { 226 | $deleted = true; 227 | $i = $this->adapterCount; 228 | 229 | while ($i--) { 230 | $deleted = $this->adapters[$i]->deleteItems($keys) && $deleted; 231 | } 232 | 233 | return $deleted; 234 | } 235 | 236 | public function save(CacheItemInterface $item): bool 237 | { 238 | $saved = true; 239 | $i = $this->adapterCount; 240 | 241 | while ($i--) { 242 | $saved = $this->adapters[$i]->save($item) && $saved; 243 | } 244 | 245 | return $saved; 246 | } 247 | 248 | public function saveDeferred(CacheItemInterface $item): bool 249 | { 250 | $saved = true; 251 | $i = $this->adapterCount; 252 | 253 | while ($i--) { 254 | $saved = $this->adapters[$i]->saveDeferred($item) && $saved; 255 | } 256 | 257 | return $saved; 258 | } 259 | 260 | public function commit(): bool 261 | { 262 | $committed = true; 263 | $i = $this->adapterCount; 264 | 265 | while ($i--) { 266 | $committed = $this->adapters[$i]->commit() && $committed; 267 | } 268 | 269 | return $committed; 270 | } 271 | 272 | public function prune(): bool 273 | { 274 | $pruned = true; 275 | 276 | foreach ($this->adapters as $adapter) { 277 | if ($adapter instanceof PruneableInterface) { 278 | $pruned = $adapter->prune() && $pruned; 279 | } 280 | } 281 | 282 | return $pruned; 283 | } 284 | 285 | public function withSubNamespace(string $namespace): static 286 | { 287 | $clone = clone $this; 288 | $adapters = []; 289 | 290 | foreach ($this->adapters as $adapter) { 291 | if (!$adapter instanceof NamespacedPoolInterface) { 292 | throw new BadMethodCallException('All adapters must implement NamespacedPoolInterface to support namespaces.'); 293 | } 294 | 295 | $adapters[] = $adapter->withSubNamespace($namespace); 296 | } 297 | $clone->adapters = $adapters; 298 | 299 | return $clone; 300 | } 301 | 302 | public function reset(): void 303 | { 304 | foreach ($this->adapters as $adapter) { 305 | if ($adapter instanceof ResetInterface) { 306 | $adapter->reset(); 307 | } 308 | } 309 | } 310 | } 311 | -------------------------------------------------------------------------------- /Adapter/CouchbaseBucketAdapter.php: -------------------------------------------------------------------------------- 1 | 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\Component\Cache\Adapter; 13 | 14 | use Symfony\Component\Cache\Exception\CacheException; 15 | use Symfony\Component\Cache\Exception\InvalidArgumentException; 16 | use Symfony\Component\Cache\Marshaller\DefaultMarshaller; 17 | use Symfony\Component\Cache\Marshaller\MarshallerInterface; 18 | 19 | trigger_deprecation('symfony/cache', '7.1', 'The "%s" class is deprecated, use "%s" instead.', CouchbaseBucketAdapter::class, CouchbaseCollectionAdapter::class); 20 | 21 | /** 22 | * @author Antonio Jose Cerezo Aranda 23 | * 24 | * @deprecated since Symfony 7.1, use {@see CouchbaseCollectionAdapter} instead 25 | */ 26 | class CouchbaseBucketAdapter extends AbstractAdapter 27 | { 28 | private const THIRTY_DAYS_IN_SECONDS = 2592000; 29 | private const MAX_KEY_LENGTH = 250; 30 | private const KEY_NOT_FOUND = 13; 31 | private const VALID_DSN_OPTIONS = [ 32 | 'operationTimeout', 33 | 'configTimeout', 34 | 'configNodeTimeout', 35 | 'n1qlTimeout', 36 | 'httpTimeout', 37 | 'configDelay', 38 | 'htconfigIdleTimeout', 39 | 'durabilityInterval', 40 | 'durabilityTimeout', 41 | ]; 42 | 43 | private MarshallerInterface $marshaller; 44 | 45 | public function __construct( 46 | private \CouchbaseBucket $bucket, 47 | string $namespace = '', 48 | int $defaultLifetime = 0, 49 | ?MarshallerInterface $marshaller = null, 50 | ) { 51 | if (!static::isSupported()) { 52 | throw new CacheException('Couchbase >= 2.6.0 < 3.0.0 is required.'); 53 | } 54 | 55 | $this->maxIdLength = static::MAX_KEY_LENGTH; 56 | 57 | parent::__construct($namespace, $defaultLifetime); 58 | $this->enableVersioning(); 59 | $this->marshaller = $marshaller ?? new DefaultMarshaller(); 60 | } 61 | 62 | public static function createConnection(#[\SensitiveParameter] array|string $servers, array $options = []): \CouchbaseBucket 63 | { 64 | if (\is_string($servers)) { 65 | $servers = [$servers]; 66 | } 67 | 68 | if (!static::isSupported()) { 69 | throw new CacheException('Couchbase >= 2.6.0 < 3.0.0 is required.'); 70 | } 71 | 72 | set_error_handler(static fn ($type, $msg, $file, $line) => throw new \ErrorException($msg, 0, $type, $file, $line)); 73 | 74 | $dsnPattern = '/^(?couchbase(?:s)?)\:\/\/(?:(?[^\:]+)\:(?[^\@]{6,})@)?' 75 | .'(?[^\:]+(?:\:\d+)?)(?:\/(?[^\?]+))(?:\?(?.*))?$/i'; 76 | 77 | $newServers = []; 78 | $protocol = 'couchbase'; 79 | try { 80 | $options = self::initOptions($options); 81 | $username = $options['username']; 82 | $password = $options['password']; 83 | 84 | foreach ($servers as $dsn) { 85 | if (!str_starts_with($dsn, 'couchbase:')) { 86 | throw new InvalidArgumentException('Invalid Couchbase DSN: it does not start with "couchbase:".'); 87 | } 88 | 89 | preg_match($dsnPattern, $dsn, $matches); 90 | 91 | $username = $matches['username'] ?: $username; 92 | $password = $matches['password'] ?: $password; 93 | $protocol = $matches['protocol'] ?: $protocol; 94 | 95 | if (isset($matches['options'])) { 96 | $optionsInDsn = self::getOptions($matches['options']); 97 | 98 | foreach ($optionsInDsn as $parameter => $value) { 99 | $options[$parameter] = $value; 100 | } 101 | } 102 | 103 | $newServers[] = $matches['host']; 104 | } 105 | 106 | $connectionString = $protocol.'://'.implode(',', $newServers); 107 | 108 | $client = new \CouchbaseCluster($connectionString); 109 | $client->authenticateAs($username, $password); 110 | 111 | $bucket = $client->openBucket($matches['bucketName']); 112 | 113 | unset($options['username'], $options['password']); 114 | foreach ($options as $option => $value) { 115 | if ($value) { 116 | $bucket->$option = $value; 117 | } 118 | } 119 | 120 | return $bucket; 121 | } finally { 122 | restore_error_handler(); 123 | } 124 | } 125 | 126 | public static function isSupported(): bool 127 | { 128 | return \extension_loaded('couchbase') && version_compare(phpversion('couchbase'), '2.6.0', '>=') && version_compare(phpversion('couchbase'), '3.0', '<'); 129 | } 130 | 131 | private static function getOptions(string $options): array 132 | { 133 | $results = []; 134 | $optionsInArray = explode('&', $options); 135 | 136 | foreach ($optionsInArray as $option) { 137 | [$key, $value] = explode('=', $option); 138 | 139 | if (\in_array($key, static::VALID_DSN_OPTIONS, true)) { 140 | $results[$key] = $value; 141 | } 142 | } 143 | 144 | return $results; 145 | } 146 | 147 | private static function initOptions(array $options): array 148 | { 149 | $options['username'] ??= ''; 150 | $options['password'] ??= ''; 151 | $options['operationTimeout'] ??= 0; 152 | $options['configTimeout'] ??= 0; 153 | $options['configNodeTimeout'] ??= 0; 154 | $options['n1qlTimeout'] ??= 0; 155 | $options['httpTimeout'] ??= 0; 156 | $options['configDelay'] ??= 0; 157 | $options['htconfigIdleTimeout'] ??= 0; 158 | $options['durabilityInterval'] ??= 0; 159 | $options['durabilityTimeout'] ??= 0; 160 | 161 | return $options; 162 | } 163 | 164 | protected function doFetch(array $ids): iterable 165 | { 166 | $resultsCouchbase = $this->bucket->get($ids); 167 | 168 | $results = []; 169 | foreach ($resultsCouchbase as $key => $value) { 170 | if (null !== $value->error) { 171 | continue; 172 | } 173 | $results[$key] = $this->marshaller->unmarshall($value->value); 174 | } 175 | 176 | return $results; 177 | } 178 | 179 | protected function doHave(string $id): bool 180 | { 181 | return false !== $this->bucket->get($id); 182 | } 183 | 184 | protected function doClear(string $namespace): bool 185 | { 186 | if ('' === $namespace) { 187 | $this->bucket->manager()->flush(); 188 | 189 | return true; 190 | } 191 | 192 | return false; 193 | } 194 | 195 | protected function doDelete(array $ids): bool 196 | { 197 | $results = $this->bucket->remove(array_values($ids)); 198 | 199 | foreach ($results as $key => $result) { 200 | if (null !== $result->error && static::KEY_NOT_FOUND !== $result->error->getCode()) { 201 | continue; 202 | } 203 | unset($results[$key]); 204 | } 205 | 206 | return 0 === \count($results); 207 | } 208 | 209 | protected function doSave(array $values, int $lifetime): array|bool 210 | { 211 | if (!$values = $this->marshaller->marshall($values, $failed)) { 212 | return $failed; 213 | } 214 | 215 | $lifetime = $this->normalizeExpiry($lifetime); 216 | 217 | $ko = []; 218 | foreach ($values as $key => $value) { 219 | $result = $this->bucket->upsert($key, $value, ['expiry' => $lifetime]); 220 | 221 | if (null !== $result->error) { 222 | $ko[$key] = $result; 223 | } 224 | } 225 | 226 | return [] === $ko ? true : $ko; 227 | } 228 | 229 | private function normalizeExpiry(int $expiry): int 230 | { 231 | if ($expiry && $expiry > static::THIRTY_DAYS_IN_SECONDS) { 232 | $expiry += time(); 233 | } 234 | 235 | return $expiry; 236 | } 237 | } 238 | -------------------------------------------------------------------------------- /Adapter/CouchbaseCollectionAdapter.php: -------------------------------------------------------------------------------- 1 | 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\Component\Cache\Adapter; 13 | 14 | use Couchbase\Bucket; 15 | use Couchbase\Cluster; 16 | use Couchbase\ClusterOptions; 17 | use Couchbase\Collection; 18 | use Couchbase\DocumentNotFoundException; 19 | use Couchbase\UpsertOptions; 20 | use Symfony\Component\Cache\Exception\CacheException; 21 | use Symfony\Component\Cache\Exception\InvalidArgumentException; 22 | use Symfony\Component\Cache\Marshaller\DefaultMarshaller; 23 | use Symfony\Component\Cache\Marshaller\MarshallerInterface; 24 | 25 | /** 26 | * @author Antonio Jose Cerezo Aranda 27 | */ 28 | class CouchbaseCollectionAdapter extends AbstractAdapter 29 | { 30 | private const MAX_KEY_LENGTH = 250; 31 | 32 | private MarshallerInterface $marshaller; 33 | 34 | public function __construct( 35 | private Collection $connection, 36 | string $namespace = '', 37 | int $defaultLifetime = 0, 38 | ?MarshallerInterface $marshaller = null, 39 | ) { 40 | if (!static::isSupported()) { 41 | throw new CacheException('Couchbase >= 3.0.5 < 4.0.0 is required.'); 42 | } 43 | 44 | $this->maxIdLength = static::MAX_KEY_LENGTH; 45 | 46 | parent::__construct($namespace, $defaultLifetime); 47 | $this->enableVersioning(); 48 | $this->marshaller = $marshaller ?? new DefaultMarshaller(); 49 | } 50 | 51 | public static function createConnection(#[\SensitiveParameter] array|string $dsn, array $options = []): Bucket|Collection 52 | { 53 | if (\is_string($dsn)) { 54 | $dsn = [$dsn]; 55 | } 56 | 57 | if (!static::isSupported()) { 58 | throw new CacheException('Couchbase >= 3.0.5 < 4.0.0 is required.'); 59 | } 60 | 61 | set_error_handler(static fn ($type, $msg, $file, $line) => throw new \ErrorException($msg, 0, $type, $file, $line)); 62 | 63 | $pathPattern = '/^(?:\/(?[^\/\?]+))(?:(?:\/(?[^\/]+))(?:\/(?[^\/\?]+)))?(?:\/)?$/'; 64 | $newServers = []; 65 | $protocol = 'couchbase'; 66 | try { 67 | $username = $options['username'] ?? ''; 68 | $password = $options['password'] ?? ''; 69 | 70 | foreach ($dsn as $server) { 71 | if (!str_starts_with($server, 'couchbase:')) { 72 | throw new InvalidArgumentException('Invalid Couchbase DSN: it does not start with "couchbase:".'); 73 | } 74 | 75 | $params = parse_url($server); 76 | 77 | $username = isset($params['user']) ? rawurldecode($params['user']) : $username; 78 | $password = isset($params['pass']) ? rawurldecode($params['pass']) : $password; 79 | $protocol = $params['scheme'] ?? $protocol; 80 | 81 | if (isset($params['query'])) { 82 | $optionsInDsn = self::getOptions($params['query']); 83 | 84 | foreach ($optionsInDsn as $parameter => $value) { 85 | $options[$parameter] = $value; 86 | } 87 | } 88 | 89 | $newServers[] = $params['host']; 90 | } 91 | 92 | $option = isset($params['query']) ? '?'.$params['query'] : ''; 93 | $connectionString = $protocol.'://'.implode(',', $newServers).$option; 94 | 95 | $clusterOptions = new ClusterOptions(); 96 | $clusterOptions->credentials($username, $password); 97 | 98 | $client = new Cluster($connectionString, $clusterOptions); 99 | 100 | preg_match($pathPattern, $params['path'] ?? '', $matches); 101 | $bucket = $client->bucket($matches['bucketName']); 102 | $collection = $bucket->defaultCollection(); 103 | if (!empty($matches['scopeName'])) { 104 | $scope = $bucket->scope($matches['scopeName']); 105 | $collection = $scope->collection($matches['collectionName']); 106 | } 107 | 108 | return $collection; 109 | } finally { 110 | restore_error_handler(); 111 | } 112 | } 113 | 114 | public static function isSupported(): bool 115 | { 116 | return \extension_loaded('couchbase') && version_compare(phpversion('couchbase'), '3.0.5', '>=') && version_compare(phpversion('couchbase'), '4.0', '<'); 117 | } 118 | 119 | private static function getOptions(string $options): array 120 | { 121 | $results = []; 122 | $optionsInArray = explode('&', $options); 123 | 124 | foreach ($optionsInArray as $option) { 125 | [$key, $value] = explode('=', $option); 126 | 127 | $results[$key] = $value; 128 | } 129 | 130 | return $results; 131 | } 132 | 133 | protected function doFetch(array $ids): array 134 | { 135 | $results = []; 136 | foreach ($ids as $id) { 137 | try { 138 | $resultCouchbase = $this->connection->get($id); 139 | } catch (DocumentNotFoundException) { 140 | continue; 141 | } 142 | 143 | $content = $resultCouchbase->value ?? $resultCouchbase->content(); 144 | 145 | $results[$id] = $this->marshaller->unmarshall($content); 146 | } 147 | 148 | return $results; 149 | } 150 | 151 | protected function doHave($id): bool 152 | { 153 | return $this->connection->exists($id)->exists(); 154 | } 155 | 156 | protected function doClear($namespace): bool 157 | { 158 | return false; 159 | } 160 | 161 | protected function doDelete(array $ids): bool 162 | { 163 | $idsErrors = []; 164 | foreach ($ids as $id) { 165 | try { 166 | $result = $this->connection->remove($id); 167 | 168 | if (null === $result->mutationToken()) { 169 | $idsErrors[] = $id; 170 | } 171 | } catch (DocumentNotFoundException) { 172 | } 173 | } 174 | 175 | return 0 === \count($idsErrors); 176 | } 177 | 178 | protected function doSave(array $values, $lifetime): array|bool 179 | { 180 | if (!$values = $this->marshaller->marshall($values, $failed)) { 181 | return $failed; 182 | } 183 | 184 | $upsertOptions = new UpsertOptions(); 185 | $upsertOptions->expiry($lifetime); 186 | 187 | $ko = []; 188 | foreach ($values as $key => $value) { 189 | try { 190 | $this->connection->upsert($key, $value, $upsertOptions); 191 | } catch (\Exception) { 192 | $ko[$key] = ''; 193 | } 194 | } 195 | 196 | return [] === $ko ? true : $ko; 197 | } 198 | } 199 | -------------------------------------------------------------------------------- /Adapter/FilesystemAdapter.php: -------------------------------------------------------------------------------- 1 | 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\Component\Cache\Adapter; 13 | 14 | use Symfony\Component\Cache\Marshaller\DefaultMarshaller; 15 | use Symfony\Component\Cache\Marshaller\MarshallerInterface; 16 | use Symfony\Component\Cache\PruneableInterface; 17 | use Symfony\Component\Cache\Traits\FilesystemTrait; 18 | 19 | class FilesystemAdapter extends AbstractAdapter implements PruneableInterface 20 | { 21 | use FilesystemTrait; 22 | 23 | public function __construct(string $namespace = '', int $defaultLifetime = 0, ?string $directory = null, ?MarshallerInterface $marshaller = null) 24 | { 25 | $this->marshaller = $marshaller ?? new DefaultMarshaller(); 26 | parent::__construct('', $defaultLifetime); 27 | $this->init($namespace, $directory); 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /Adapter/FilesystemTagAwareAdapter.php: -------------------------------------------------------------------------------- 1 | 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\Component\Cache\Adapter; 13 | 14 | use Symfony\Component\Cache\Marshaller\MarshallerInterface; 15 | use Symfony\Component\Cache\Marshaller\TagAwareMarshaller; 16 | use Symfony\Component\Cache\PruneableInterface; 17 | use Symfony\Component\Cache\Traits\FilesystemTrait; 18 | 19 | /** 20 | * Stores tag id <> cache id relationship as a symlink, and lookup on invalidation calls. 21 | * 22 | * @author Nicolas Grekas 23 | * @author André Rømcke 24 | */ 25 | class FilesystemTagAwareAdapter extends AbstractTagAwareAdapter implements PruneableInterface 26 | { 27 | use FilesystemTrait { 28 | prune as private doPrune; 29 | doClear as private doClearCache; 30 | doSave as private doSaveCache; 31 | } 32 | 33 | /** 34 | * Folder used for tag symlinks. 35 | */ 36 | private const TAG_FOLDER = 'tags'; 37 | 38 | public function __construct(string $namespace = '', int $defaultLifetime = 0, ?string $directory = null, ?MarshallerInterface $marshaller = null) 39 | { 40 | $this->marshaller = new TagAwareMarshaller($marshaller); 41 | parent::__construct('', $defaultLifetime); 42 | $this->init($namespace, $directory); 43 | } 44 | 45 | public function prune(): bool 46 | { 47 | $ok = $this->doPrune(); 48 | 49 | set_error_handler(static function () {}); 50 | $chars = '+-ABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789'; 51 | 52 | try { 53 | foreach ($this->scanHashDir($this->directory.self::TAG_FOLDER.\DIRECTORY_SEPARATOR) as $dir) { 54 | $dir .= \DIRECTORY_SEPARATOR; 55 | $keepDir = false; 56 | for ($i = 0; $i < 38; ++$i) { 57 | if (!is_dir($dir.$chars[$i])) { 58 | continue; 59 | } 60 | for ($j = 0; $j < 38; ++$j) { 61 | if (!is_dir($d = $dir.$chars[$i].\DIRECTORY_SEPARATOR.$chars[$j])) { 62 | continue; 63 | } 64 | foreach (scandir($d, \SCANDIR_SORT_NONE) ?: [] as $link) { 65 | if ('.' === $link || '..' === $link) { 66 | continue; 67 | } 68 | if ('_' !== $dir[-2] && realpath($d.\DIRECTORY_SEPARATOR.$link)) { 69 | $keepDir = true; 70 | } else { 71 | unlink($d.\DIRECTORY_SEPARATOR.$link); 72 | } 73 | } 74 | $keepDir ?: rmdir($d); 75 | } 76 | $keepDir ?: rmdir($dir.$chars[$i]); 77 | } 78 | $keepDir ?: rmdir($dir); 79 | } 80 | } finally { 81 | restore_error_handler(); 82 | } 83 | 84 | return $ok; 85 | } 86 | 87 | protected function doClear(string $namespace): bool 88 | { 89 | $ok = $this->doClearCache($namespace); 90 | 91 | if ('' !== $namespace) { 92 | return $ok; 93 | } 94 | 95 | set_error_handler(static function () {}); 96 | $chars = '+-ABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789'; 97 | 98 | $this->tmpSuffix ??= str_replace('/', '-', base64_encode(random_bytes(6))); 99 | 100 | try { 101 | foreach ($this->scanHashDir($this->directory.self::TAG_FOLDER.\DIRECTORY_SEPARATOR) as $dir) { 102 | if (rename($dir, $renamed = substr_replace($dir, $this->tmpSuffix.'_', -9))) { 103 | $dir = $renamed.\DIRECTORY_SEPARATOR; 104 | } else { 105 | $dir .= \DIRECTORY_SEPARATOR; 106 | $renamed = null; 107 | } 108 | 109 | for ($i = 0; $i < 38; ++$i) { 110 | if (!is_dir($dir.$chars[$i])) { 111 | continue; 112 | } 113 | for ($j = 0; $j < 38; ++$j) { 114 | if (!is_dir($d = $dir.$chars[$i].\DIRECTORY_SEPARATOR.$chars[$j])) { 115 | continue; 116 | } 117 | foreach (scandir($d, \SCANDIR_SORT_NONE) ?: [] as $link) { 118 | if ('.' !== $link && '..' !== $link && (null !== $renamed || !realpath($d.\DIRECTORY_SEPARATOR.$link))) { 119 | unlink($d.\DIRECTORY_SEPARATOR.$link); 120 | } 121 | } 122 | null === $renamed ?: rmdir($d); 123 | } 124 | null === $renamed ?: rmdir($dir.$chars[$i]); 125 | } 126 | null === $renamed ?: rmdir($renamed); 127 | } 128 | } finally { 129 | restore_error_handler(); 130 | } 131 | 132 | return $ok; 133 | } 134 | 135 | protected function doSave(array $values, int $lifetime, array $addTagData = [], array $removeTagData = []): array 136 | { 137 | $failed = $this->doSaveCache($values, $lifetime); 138 | 139 | // Add Tags as symlinks 140 | foreach ($addTagData as $tagId => $ids) { 141 | $tagFolder = $this->getTagFolder($tagId); 142 | foreach ($ids as $id) { 143 | if ($failed && \in_array($id, $failed, true)) { 144 | continue; 145 | } 146 | 147 | $file = $this->getFile($id); 148 | 149 | if (!@symlink($file, $tagLink = $this->getFile($id, true, $tagFolder)) && !is_link($tagLink)) { 150 | @unlink($file); 151 | $failed[] = $id; 152 | } 153 | } 154 | } 155 | 156 | // Unlink removed Tags 157 | foreach ($removeTagData as $tagId => $ids) { 158 | $tagFolder = $this->getTagFolder($tagId); 159 | foreach ($ids as $id) { 160 | if ($failed && \in_array($id, $failed, true)) { 161 | continue; 162 | } 163 | 164 | @unlink($this->getFile($id, false, $tagFolder)); 165 | } 166 | } 167 | 168 | return $failed; 169 | } 170 | 171 | protected function doDeleteYieldTags(array $ids): iterable 172 | { 173 | foreach ($ids as $id) { 174 | $file = $this->getFile($id); 175 | if (!is_file($file) || !$h = @fopen($file, 'r')) { 176 | continue; 177 | } 178 | 179 | if (!@unlink($file)) { 180 | fclose($h); 181 | continue; 182 | } 183 | 184 | $meta = explode("\n", fread($h, 4096), 3)[2] ?? ''; 185 | 186 | // detect the compact format used in marshall() using magic numbers in the form 9D-..-..-..-..-00-..-..-..-5F 187 | if (13 < \strlen($meta) && "\x9D" === $meta[0] && "\0" === $meta[5] && "\x5F" === $meta[9]) { 188 | $meta[9] = "\0"; 189 | $tagLen = unpack('Nlen', $meta, 9)['len']; 190 | $meta = substr($meta, 13, $tagLen); 191 | 192 | if (0 < $tagLen -= \strlen($meta)) { 193 | $meta .= fread($h, $tagLen); 194 | } 195 | 196 | try { 197 | yield $id => '' === $meta ? [] : $this->marshaller->unmarshall($meta); 198 | } catch (\Exception) { 199 | yield $id => []; 200 | } 201 | } 202 | 203 | fclose($h); 204 | } 205 | } 206 | 207 | protected function doDeleteTagRelations(array $tagData): bool 208 | { 209 | foreach ($tagData as $tagId => $idList) { 210 | $tagFolder = $this->getTagFolder($tagId); 211 | foreach ($idList as $id) { 212 | @unlink($this->getFile($id, false, $tagFolder)); 213 | } 214 | } 215 | 216 | return true; 217 | } 218 | 219 | protected function doInvalidate(array $tagIds): bool 220 | { 221 | foreach ($tagIds as $tagId) { 222 | if (!is_dir($tagFolder = $this->getTagFolder($tagId))) { 223 | continue; 224 | } 225 | 226 | $this->tmpSuffix ??= str_replace('/', '-', base64_encode(random_bytes(6))); 227 | 228 | set_error_handler(static function () {}); 229 | 230 | try { 231 | if (rename($tagFolder, $renamed = substr_replace($tagFolder, $this->tmpSuffix.'_', -10))) { 232 | $tagFolder = $renamed.\DIRECTORY_SEPARATOR; 233 | } else { 234 | $renamed = null; 235 | } 236 | 237 | foreach ($this->scanHashDir($tagFolder) as $itemLink) { 238 | unlink(realpath($itemLink) ?: $itemLink); 239 | unlink($itemLink); 240 | } 241 | 242 | if (null === $renamed) { 243 | continue; 244 | } 245 | 246 | $chars = '+-ABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789'; 247 | 248 | for ($i = 0; $i < 38; ++$i) { 249 | for ($j = 0; $j < 38; ++$j) { 250 | rmdir($tagFolder.$chars[$i].\DIRECTORY_SEPARATOR.$chars[$j]); 251 | } 252 | rmdir($tagFolder.$chars[$i]); 253 | } 254 | rmdir($renamed); 255 | } finally { 256 | restore_error_handler(); 257 | } 258 | } 259 | 260 | return true; 261 | } 262 | 263 | private function getTagFolder(string $tagId): string 264 | { 265 | return $this->getFile($tagId, false, $this->directory.self::TAG_FOLDER.\DIRECTORY_SEPARATOR).\DIRECTORY_SEPARATOR; 266 | } 267 | } 268 | -------------------------------------------------------------------------------- /Adapter/NullAdapter.php: -------------------------------------------------------------------------------- 1 | 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\Component\Cache\Adapter; 13 | 14 | use Psr\Cache\CacheItemInterface; 15 | use Symfony\Component\Cache\CacheItem; 16 | use Symfony\Contracts\Cache\CacheInterface; 17 | use Symfony\Contracts\Cache\NamespacedPoolInterface; 18 | 19 | /** 20 | * @author Titouan Galopin 21 | */ 22 | class NullAdapter implements AdapterInterface, CacheInterface, NamespacedPoolInterface 23 | { 24 | private static \Closure $createCacheItem; 25 | 26 | public function __construct() 27 | { 28 | self::$createCacheItem ??= \Closure::bind( 29 | static function ($key) { 30 | $item = new CacheItem(); 31 | $item->key = $key; 32 | $item->isHit = false; 33 | 34 | return $item; 35 | }, 36 | null, 37 | CacheItem::class 38 | ); 39 | } 40 | 41 | public function get(string $key, callable $callback, ?float $beta = null, ?array &$metadata = null): mixed 42 | { 43 | $save = true; 44 | 45 | return $callback((self::$createCacheItem)($key), $save); 46 | } 47 | 48 | public function getItem(mixed $key): CacheItem 49 | { 50 | return (self::$createCacheItem)($key); 51 | } 52 | 53 | public function getItems(array $keys = []): iterable 54 | { 55 | return $this->generateItems($keys); 56 | } 57 | 58 | public function hasItem(mixed $key): bool 59 | { 60 | return false; 61 | } 62 | 63 | public function clear(string $prefix = ''): bool 64 | { 65 | return true; 66 | } 67 | 68 | public function deleteItem(mixed $key): bool 69 | { 70 | return true; 71 | } 72 | 73 | public function deleteItems(array $keys): bool 74 | { 75 | return true; 76 | } 77 | 78 | public function save(CacheItemInterface $item): bool 79 | { 80 | return true; 81 | } 82 | 83 | public function saveDeferred(CacheItemInterface $item): bool 84 | { 85 | return true; 86 | } 87 | 88 | public function commit(): bool 89 | { 90 | return true; 91 | } 92 | 93 | public function delete(string $key): bool 94 | { 95 | return $this->deleteItem($key); 96 | } 97 | 98 | public function withSubNamespace(string $namespace): static 99 | { 100 | return clone $this; 101 | } 102 | 103 | private function generateItems(array $keys): \Generator 104 | { 105 | $f = self::$createCacheItem; 106 | 107 | foreach ($keys as $key) { 108 | yield $key => $f($key); 109 | } 110 | } 111 | } 112 | -------------------------------------------------------------------------------- /Adapter/ParameterNormalizer.php: -------------------------------------------------------------------------------- 1 | 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\Component\Cache\Adapter; 13 | 14 | /** 15 | * @author Lars Strojny 16 | */ 17 | final class ParameterNormalizer 18 | { 19 | public static function normalizeDuration(string $duration): int 20 | { 21 | if (is_numeric($duration)) { 22 | return $duration; 23 | } 24 | 25 | if (false !== $time = strtotime($duration, 0)) { 26 | return $time; 27 | } 28 | 29 | try { 30 | return \DateTimeImmutable::createFromFormat('U', 0)->add(new \DateInterval($duration))->getTimestamp(); 31 | } catch (\Exception $e) { 32 | throw new \InvalidArgumentException(\sprintf('Cannot parse date interval "%s".', $duration), 0, $e); 33 | } 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /Adapter/PhpFilesAdapter.php: -------------------------------------------------------------------------------- 1 | 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\Component\Cache\Adapter; 13 | 14 | use Symfony\Component\Cache\Exception\CacheException; 15 | use Symfony\Component\Cache\Exception\InvalidArgumentException; 16 | use Symfony\Component\Cache\PruneableInterface; 17 | use Symfony\Component\Cache\Traits\FilesystemCommonTrait; 18 | use Symfony\Component\VarExporter\VarExporter; 19 | 20 | /** 21 | * @author Piotr Stankowski 22 | * @author Nicolas Grekas 23 | * @author Rob Frawley 2nd 24 | */ 25 | class PhpFilesAdapter extends AbstractAdapter implements PruneableInterface 26 | { 27 | use FilesystemCommonTrait { 28 | doClear as private doCommonClear; 29 | doDelete as private doCommonDelete; 30 | } 31 | 32 | private \Closure $includeHandler; 33 | private array $values = []; 34 | private array $files = []; 35 | 36 | private static int $startTime; 37 | private static array $valuesCache = []; 38 | 39 | /** 40 | * @param bool $appendOnly Set to `true` to gain extra performance when the items stored in this pool never expire. 41 | * Doing so is encouraged because it fits perfectly OPcache's memory model. 42 | * 43 | * @throws CacheException if OPcache is not enabled 44 | */ 45 | public function __construct( 46 | string $namespace = '', 47 | int $defaultLifetime = 0, 48 | ?string $directory = null, 49 | private bool $appendOnly = false, 50 | ) { 51 | self::$startTime ??= $_SERVER['REQUEST_TIME'] ?? time(); 52 | parent::__construct('', $defaultLifetime); 53 | $this->init($namespace, $directory); 54 | $this->includeHandler = static function ($type, $msg, $file, $line) { 55 | throw new \ErrorException($msg, 0, $type, $file, $line); 56 | }; 57 | } 58 | 59 | public static function isSupported(): bool 60 | { 61 | self::$startTime ??= $_SERVER['REQUEST_TIME'] ?? time(); 62 | 63 | return \function_exists('opcache_invalidate') && filter_var(\ini_get('opcache.enable'), \FILTER_VALIDATE_BOOL) && (!\in_array(\PHP_SAPI, ['cli', 'phpdbg', 'embed'], true) || filter_var(\ini_get('opcache.enable_cli'), \FILTER_VALIDATE_BOOL)); 64 | } 65 | 66 | public function prune(): bool 67 | { 68 | $time = time(); 69 | $pruned = true; 70 | $getExpiry = true; 71 | 72 | set_error_handler($this->includeHandler); 73 | try { 74 | foreach ($this->scanHashDir($this->directory) as $file) { 75 | try { 76 | if (\is_array($expiresAt = include $file)) { 77 | $expiresAt = $expiresAt[0]; 78 | } 79 | } catch (\ErrorException $e) { 80 | $expiresAt = $time; 81 | } 82 | 83 | if ($time >= $expiresAt) { 84 | $pruned = ($this->doUnlink($file) || !file_exists($file)) && $pruned; 85 | } 86 | } 87 | } finally { 88 | restore_error_handler(); 89 | } 90 | 91 | return $pruned; 92 | } 93 | 94 | protected function doFetch(array $ids): iterable 95 | { 96 | if ($this->appendOnly) { 97 | $now = 0; 98 | $missingIds = []; 99 | } else { 100 | $now = time(); 101 | $missingIds = $ids; 102 | $ids = []; 103 | } 104 | $values = []; 105 | 106 | while (true) { 107 | $getExpiry = false; 108 | 109 | foreach ($ids as $id) { 110 | if (null === $value = $this->values[$id] ?? null) { 111 | $missingIds[] = $id; 112 | } elseif ('N;' === $value) { 113 | $values[$id] = null; 114 | } elseif (!\is_object($value)) { 115 | $values[$id] = $value; 116 | } elseif (!$value instanceof LazyValue) { 117 | $values[$id] = $value(); 118 | } elseif (false === $values[$id] = include $value->file) { 119 | unset($values[$id], $this->values[$id]); 120 | $missingIds[] = $id; 121 | } 122 | if (!$this->appendOnly) { 123 | unset($this->values[$id]); 124 | } 125 | } 126 | 127 | if (!$missingIds) { 128 | return $values; 129 | } 130 | 131 | set_error_handler($this->includeHandler); 132 | try { 133 | $getExpiry = true; 134 | 135 | foreach ($missingIds as $k => $id) { 136 | try { 137 | $file = $this->files[$id] ??= $this->getFile($id); 138 | 139 | if (isset(self::$valuesCache[$file])) { 140 | [$expiresAt, $this->values[$id]] = self::$valuesCache[$file]; 141 | } elseif (\is_array($expiresAt = include $file)) { 142 | if ($this->appendOnly) { 143 | self::$valuesCache[$file] = $expiresAt; 144 | } 145 | 146 | [$expiresAt, $this->values[$id]] = $expiresAt; 147 | } elseif ($now < $expiresAt) { 148 | $this->values[$id] = new LazyValue($file); 149 | } 150 | 151 | if ($now >= $expiresAt) { 152 | unset($this->values[$id], $missingIds[$k], self::$valuesCache[$file]); 153 | } 154 | } catch (\ErrorException $e) { 155 | unset($missingIds[$k]); 156 | } 157 | } 158 | } finally { 159 | restore_error_handler(); 160 | } 161 | 162 | $ids = $missingIds; 163 | $missingIds = []; 164 | } 165 | } 166 | 167 | protected function doHave(string $id): bool 168 | { 169 | if ($this->appendOnly && isset($this->values[$id])) { 170 | return true; 171 | } 172 | 173 | set_error_handler($this->includeHandler); 174 | try { 175 | $file = $this->files[$id] ??= $this->getFile($id); 176 | $getExpiry = true; 177 | 178 | if (isset(self::$valuesCache[$file])) { 179 | [$expiresAt, $value] = self::$valuesCache[$file]; 180 | } elseif (\is_array($expiresAt = include $file)) { 181 | if ($this->appendOnly) { 182 | self::$valuesCache[$file] = $expiresAt; 183 | } 184 | 185 | [$expiresAt, $value] = $expiresAt; 186 | } elseif ($this->appendOnly) { 187 | $value = new LazyValue($file); 188 | } 189 | } catch (\ErrorException) { 190 | return false; 191 | } finally { 192 | restore_error_handler(); 193 | } 194 | if ($this->appendOnly) { 195 | $now = 0; 196 | $this->values[$id] = $value; 197 | } else { 198 | $now = time(); 199 | } 200 | 201 | return $now < $expiresAt; 202 | } 203 | 204 | protected function doSave(array $values, int $lifetime): array|bool 205 | { 206 | $ok = true; 207 | $expiry = $lifetime ? time() + $lifetime : 'PHP_INT_MAX'; 208 | $allowCompile = self::isSupported(); 209 | 210 | foreach ($values as $key => $value) { 211 | unset($this->values[$key]); 212 | $isStaticValue = true; 213 | if (null === $value) { 214 | $value = "'N;'"; 215 | } elseif (\is_object($value) || \is_array($value)) { 216 | try { 217 | $value = VarExporter::export($value, $isStaticValue); 218 | } catch (\Exception $e) { 219 | throw new InvalidArgumentException(\sprintf('Cache key "%s" has non-serializable "%s" value.', $key, get_debug_type($value)), 0, $e); 220 | } 221 | } elseif (\is_string($value)) { 222 | // Wrap "N;" in a closure to not confuse it with an encoded `null` 223 | if ('N;' === $value) { 224 | $isStaticValue = false; 225 | } 226 | $value = var_export($value, true); 227 | } elseif (!\is_scalar($value)) { 228 | throw new InvalidArgumentException(\sprintf('Cache key "%s" has non-serializable "%s" value.', $key, get_debug_type($value))); 229 | } else { 230 | $value = var_export($value, true); 231 | } 232 | 233 | $encodedKey = rawurlencode($key); 234 | 235 | if ($isStaticValue) { 236 | $value = "return [{$expiry}, {$value}];"; 237 | } elseif ($this->appendOnly) { 238 | $value = "return [{$expiry}, static fn () => {$value}];"; 239 | } else { 240 | // We cannot use a closure here because of https://bugs.php.net/76982 241 | $value = str_replace('\Symfony\Component\VarExporter\Internal\\', '', $value); 242 | $value = "namespace Symfony\Component\VarExporter\Internal;\n\nreturn \$getExpiry ? {$expiry} : {$value};"; 243 | } 244 | 245 | $file = $this->files[$key] = $this->getFile($key, true); 246 | // Since OPcache only compiles files older than the script execution start, set the file's mtime in the past 247 | $ok = $this->write($file, "directory)) { 257 | throw new CacheException(\sprintf('Cache directory is not writable (%s).', $this->directory)); 258 | } 259 | 260 | return $ok; 261 | } 262 | 263 | protected function doClear(string $namespace): bool 264 | { 265 | $this->values = []; 266 | 267 | return $this->doCommonClear($namespace); 268 | } 269 | 270 | protected function doDelete(array $ids): bool 271 | { 272 | foreach ($ids as $id) { 273 | unset($this->values[$id]); 274 | } 275 | 276 | return $this->doCommonDelete($ids); 277 | } 278 | 279 | protected function doUnlink(string $file): bool 280 | { 281 | unset(self::$valuesCache[$file]); 282 | 283 | if (self::isSupported()) { 284 | @opcache_invalidate($file, true); 285 | } 286 | 287 | return @unlink($file); 288 | } 289 | 290 | private function getFileKey(string $file): string 291 | { 292 | if (!$h = @fopen($file, 'r')) { 293 | return ''; 294 | } 295 | 296 | $encodedKey = substr(fgets($h), 8); 297 | fclose($h); 298 | 299 | return rawurldecode(rtrim($encodedKey)); 300 | } 301 | } 302 | 303 | /** 304 | * @internal 305 | */ 306 | class LazyValue 307 | { 308 | public function __construct( 309 | public string $file, 310 | ) { 311 | } 312 | } 313 | -------------------------------------------------------------------------------- /Adapter/ProxyAdapter.php: -------------------------------------------------------------------------------- 1 | 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\Component\Cache\Adapter; 13 | 14 | use Psr\Cache\CacheItemInterface; 15 | use Psr\Cache\CacheItemPoolInterface; 16 | use Symfony\Component\Cache\CacheItem; 17 | use Symfony\Component\Cache\PruneableInterface; 18 | use Symfony\Component\Cache\ResettableInterface; 19 | use Symfony\Component\Cache\Traits\ContractsTrait; 20 | use Symfony\Component\Cache\Traits\ProxyTrait; 21 | use Symfony\Contracts\Cache\CacheInterface; 22 | use Symfony\Contracts\Cache\NamespacedPoolInterface; 23 | 24 | /** 25 | * @author Nicolas Grekas 26 | */ 27 | class ProxyAdapter implements AdapterInterface, NamespacedPoolInterface, CacheInterface, PruneableInterface, ResettableInterface 28 | { 29 | use ContractsTrait; 30 | use ProxyTrait; 31 | 32 | private string $namespace = ''; 33 | private int $namespaceLen; 34 | private string $poolHash; 35 | private int $defaultLifetime; 36 | 37 | private static \Closure $createCacheItem; 38 | private static \Closure $setInnerItem; 39 | 40 | public function __construct(CacheItemPoolInterface $pool, string $namespace = '', int $defaultLifetime = 0) 41 | { 42 | if ('' !== $namespace) { 43 | if ($pool instanceof NamespacedPoolInterface) { 44 | $pool = $pool->withSubNamespace($namespace); 45 | $this->namespace = $namespace = ''; 46 | } else { 47 | \assert('' !== CacheItem::validateKey($namespace)); 48 | $this->namespace = $namespace; 49 | } 50 | } 51 | $this->pool = $pool; 52 | $this->poolHash = spl_object_hash($pool); 53 | $this->namespaceLen = \strlen($namespace); 54 | $this->defaultLifetime = $defaultLifetime; 55 | self::$createCacheItem ??= \Closure::bind( 56 | static function ($key, $innerItem, $poolHash) { 57 | $item = new CacheItem(); 58 | $item->key = $key; 59 | 60 | if (null === $innerItem) { 61 | return $item; 62 | } 63 | 64 | $item->value = $innerItem->get(); 65 | $item->isHit = $innerItem->isHit(); 66 | $item->innerItem = $innerItem; 67 | $item->poolHash = $poolHash; 68 | 69 | if (!$item->unpack() && $innerItem instanceof CacheItem) { 70 | $item->metadata = $innerItem->metadata; 71 | } 72 | $innerItem->set(null); 73 | 74 | return $item; 75 | }, 76 | null, 77 | CacheItem::class 78 | ); 79 | self::$setInnerItem ??= \Closure::bind( 80 | static function (CacheItemInterface $innerItem, CacheItem $item, $expiry = null) { 81 | $innerItem->set($item->pack()); 82 | $innerItem->expiresAt(($expiry ?? $item->expiry) ? \DateTimeImmutable::createFromFormat('U.u', \sprintf('%.6F', $expiry ?? $item->expiry)) : null); 83 | }, 84 | null, 85 | CacheItem::class 86 | ); 87 | } 88 | 89 | public function get(string $key, callable $callback, ?float $beta = null, ?array &$metadata = null): mixed 90 | { 91 | if (!$this->pool instanceof CacheInterface) { 92 | return $this->doGet($this, $key, $callback, $beta, $metadata); 93 | } 94 | 95 | return $this->pool->get($this->getId($key), function ($innerItem, bool &$save) use ($key, $callback) { 96 | $item = (self::$createCacheItem)($key, $innerItem, $this->poolHash); 97 | $item->set($value = $callback($item, $save)); 98 | (self::$setInnerItem)($innerItem, $item); 99 | 100 | return $value; 101 | }, $beta, $metadata); 102 | } 103 | 104 | public function getItem(mixed $key): CacheItem 105 | { 106 | $item = $this->pool->getItem($this->getId($key)); 107 | 108 | return (self::$createCacheItem)($key, $item, $this->poolHash); 109 | } 110 | 111 | public function getItems(array $keys = []): iterable 112 | { 113 | if ($this->namespaceLen) { 114 | foreach ($keys as $i => $key) { 115 | $keys[$i] = $this->getId($key); 116 | } 117 | } 118 | 119 | return $this->generateItems($this->pool->getItems($keys)); 120 | } 121 | 122 | public function hasItem(mixed $key): bool 123 | { 124 | return $this->pool->hasItem($this->getId($key)); 125 | } 126 | 127 | public function clear(string $prefix = ''): bool 128 | { 129 | if ($this->pool instanceof AdapterInterface) { 130 | return $this->pool->clear($this->namespace.$prefix); 131 | } 132 | 133 | return $this->pool->clear(); 134 | } 135 | 136 | public function deleteItem(mixed $key): bool 137 | { 138 | return $this->pool->deleteItem($this->getId($key)); 139 | } 140 | 141 | public function deleteItems(array $keys): bool 142 | { 143 | if ($this->namespaceLen) { 144 | foreach ($keys as $i => $key) { 145 | $keys[$i] = $this->getId($key); 146 | } 147 | } 148 | 149 | return $this->pool->deleteItems($keys); 150 | } 151 | 152 | public function save(CacheItemInterface $item): bool 153 | { 154 | return $this->doSave($item, __FUNCTION__); 155 | } 156 | 157 | public function saveDeferred(CacheItemInterface $item): bool 158 | { 159 | return $this->doSave($item, __FUNCTION__); 160 | } 161 | 162 | public function commit(): bool 163 | { 164 | return $this->pool->commit(); 165 | } 166 | 167 | public function withSubNamespace(string $namespace): static 168 | { 169 | $clone = clone $this; 170 | 171 | if ($clone->pool instanceof NamespacedPoolInterface) { 172 | $clone->pool = $clone->pool->withSubNamespace($namespace); 173 | } else { 174 | $clone->namespace .= CacheItem::validateKey($namespace); 175 | $clone->namespaceLen = \strlen($clone->namespace); 176 | } 177 | 178 | return $clone; 179 | } 180 | 181 | private function doSave(CacheItemInterface $item, string $method): bool 182 | { 183 | if (!$item instanceof CacheItem) { 184 | return false; 185 | } 186 | $castItem = (array) $item; 187 | 188 | if (null === $castItem["\0*\0expiry"] && 0 < $this->defaultLifetime) { 189 | $castItem["\0*\0expiry"] = microtime(true) + $this->defaultLifetime; 190 | } 191 | 192 | if ($castItem["\0*\0poolHash"] === $this->poolHash && $castItem["\0*\0innerItem"]) { 193 | $innerItem = $castItem["\0*\0innerItem"]; 194 | } elseif ($this->pool instanceof AdapterInterface) { 195 | // this is an optimization specific for AdapterInterface implementations 196 | // so we can save a round-trip to the backend by just creating a new item 197 | $innerItem = (self::$createCacheItem)($this->namespace.$castItem["\0*\0key"], null, $this->poolHash); 198 | } else { 199 | $innerItem = $this->pool->getItem($this->namespace.$castItem["\0*\0key"]); 200 | } 201 | 202 | (self::$setInnerItem)($innerItem, $item, $castItem["\0*\0expiry"]); 203 | 204 | return $this->pool->$method($innerItem); 205 | } 206 | 207 | private function generateItems(iterable $items): \Generator 208 | { 209 | $f = self::$createCacheItem; 210 | 211 | foreach ($items as $key => $item) { 212 | if ($this->namespaceLen) { 213 | $key = substr($key, $this->namespaceLen); 214 | } 215 | 216 | yield $key => $f($key, $item, $this->poolHash); 217 | } 218 | } 219 | 220 | private function getId(mixed $key): string 221 | { 222 | \assert('' !== CacheItem::validateKey($key)); 223 | 224 | return $this->namespace.$key; 225 | } 226 | } 227 | -------------------------------------------------------------------------------- /Adapter/Psr16Adapter.php: -------------------------------------------------------------------------------- 1 | 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\Component\Cache\Adapter; 13 | 14 | use Psr\SimpleCache\CacheInterface; 15 | use Symfony\Component\Cache\PruneableInterface; 16 | use Symfony\Component\Cache\ResettableInterface; 17 | use Symfony\Component\Cache\Traits\ProxyTrait; 18 | 19 | /** 20 | * Turns a PSR-16 cache into a PSR-6 one. 21 | * 22 | * @author Nicolas Grekas 23 | */ 24 | class Psr16Adapter extends AbstractAdapter implements PruneableInterface, ResettableInterface 25 | { 26 | use ProxyTrait; 27 | 28 | /** 29 | * @internal 30 | */ 31 | protected const NS_SEPARATOR = '_'; 32 | 33 | private object $miss; 34 | 35 | public function __construct(CacheInterface $pool, string $namespace = '', int $defaultLifetime = 0) 36 | { 37 | parent::__construct($namespace, $defaultLifetime); 38 | 39 | $this->pool = $pool; 40 | $this->miss = new \stdClass(); 41 | } 42 | 43 | protected function doFetch(array $ids): iterable 44 | { 45 | foreach ($this->pool->getMultiple($ids, $this->miss) as $key => $value) { 46 | if ($this->miss !== $value) { 47 | yield $key => $value; 48 | } 49 | } 50 | } 51 | 52 | protected function doHave(string $id): bool 53 | { 54 | return $this->pool->has($id); 55 | } 56 | 57 | protected function doClear(string $namespace): bool 58 | { 59 | return $this->pool->clear(); 60 | } 61 | 62 | protected function doDelete(array $ids): bool 63 | { 64 | return $this->pool->deleteMultiple($ids); 65 | } 66 | 67 | protected function doSave(array $values, int $lifetime): array|bool 68 | { 69 | return $this->pool->setMultiple($values, 0 === $lifetime ? null : $lifetime); 70 | } 71 | } 72 | -------------------------------------------------------------------------------- /Adapter/RedisAdapter.php: -------------------------------------------------------------------------------- 1 | 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\Component\Cache\Adapter; 13 | 14 | use Symfony\Component\Cache\Marshaller\MarshallerInterface; 15 | use Symfony\Component\Cache\Traits\RedisTrait; 16 | 17 | class RedisAdapter extends AbstractAdapter 18 | { 19 | use RedisTrait; 20 | 21 | public function __construct(\Redis|\RedisArray|\RedisCluster|\Predis\ClientInterface|\Relay\Relay|\Relay\Cluster $redis, string $namespace = '', int $defaultLifetime = 0, ?MarshallerInterface $marshaller = null) 22 | { 23 | $this->init($redis, $namespace, $defaultLifetime, $marshaller); 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /Adapter/RedisTagAwareAdapter.php: -------------------------------------------------------------------------------- 1 | 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\Component\Cache\Adapter; 13 | 14 | use Predis\Connection\Aggregate\ClusterInterface; 15 | use Predis\Connection\Aggregate\PredisCluster; 16 | use Predis\Connection\Aggregate\ReplicationInterface; 17 | use Predis\Response\ErrorInterface; 18 | use Predis\Response\Status; 19 | use Relay\Relay; 20 | use Symfony\Component\Cache\CacheItem; 21 | use Symfony\Component\Cache\Exception\InvalidArgumentException; 22 | use Symfony\Component\Cache\Exception\LogicException; 23 | use Symfony\Component\Cache\Marshaller\DeflateMarshaller; 24 | use Symfony\Component\Cache\Marshaller\MarshallerInterface; 25 | use Symfony\Component\Cache\Marshaller\TagAwareMarshaller; 26 | use Symfony\Component\Cache\Traits\RedisTrait; 27 | 28 | /** 29 | * Stores tag id <> cache id relationship as a Redis Set. 30 | * 31 | * Set (tag relation info) is stored without expiry (non-volatile), while cache always gets an expiry (volatile) even 32 | * if not set by caller. Thus if you configure redis with the right eviction policy you can be safe this tag <> cache 33 | * relationship survives eviction (cache cleanup when Redis runs out of memory). 34 | * 35 | * Redis server 2.8+ with any `volatile-*` eviction policy, OR `noeviction` if you're sure memory will NEVER fill up 36 | * 37 | * Design limitations: 38 | * - Max 4 billion cache keys per cache tag as limited by Redis Set datatype. 39 | * E.g. If you use a "all" items tag for expiry instead of clear(), that limits you to 4 billion cache items also. 40 | * 41 | * @see https://redis.io/topics/lru-cache#eviction-policies Documentation for Redis eviction policies. 42 | * @see https://redis.io/topics/data-types#sets Documentation for Redis Set datatype. 43 | * 44 | * @author Nicolas Grekas 45 | * @author André Rømcke 46 | */ 47 | class RedisTagAwareAdapter extends AbstractTagAwareAdapter 48 | { 49 | use RedisTrait; 50 | 51 | /** 52 | * On cache items without a lifetime set, we set it to 100 days. This is to make sure cache items are 53 | * preferred to be evicted over tag Sets, if eviction policy is configured according to requirements. 54 | */ 55 | private const DEFAULT_CACHE_TTL = 8640000; 56 | 57 | /** 58 | * detected eviction policy used on Redis server. 59 | */ 60 | private string $redisEvictionPolicy; 61 | 62 | public function __construct( 63 | \Redis|Relay|\Relay\Cluster|\RedisArray|\RedisCluster|\Predis\ClientInterface $redis, 64 | private string $namespace = '', 65 | int $defaultLifetime = 0, 66 | ?MarshallerInterface $marshaller = null, 67 | ) { 68 | if ($redis instanceof \Predis\ClientInterface && $redis->getConnection() instanceof ClusterInterface && !$redis->getConnection() instanceof PredisCluster) { 69 | throw new InvalidArgumentException(\sprintf('Unsupported Predis cluster connection: only "%s" is, "%s" given.', PredisCluster::class, get_debug_type($redis->getConnection()))); 70 | } 71 | 72 | $isRelay = $redis instanceof Relay || $redis instanceof \Relay\Cluster; 73 | if ($isRelay || \defined('Redis::OPT_COMPRESSION') && \in_array($redis::class, [\Redis::class, \RedisArray::class, \RedisCluster::class], true)) { 74 | $compression = $redis->getOption($isRelay ? Relay::OPT_COMPRESSION : \Redis::OPT_COMPRESSION); 75 | 76 | foreach (\is_array($compression) ? $compression : [$compression] as $c) { 77 | if ($isRelay ? Relay::COMPRESSION_NONE : \Redis::COMPRESSION_NONE !== $c) { 78 | throw new InvalidArgumentException(\sprintf('redis compression must be disabled when using "%s", use "%s" instead.', static::class, DeflateMarshaller::class)); 79 | } 80 | } 81 | } 82 | 83 | $this->init($redis, $namespace, $defaultLifetime, new TagAwareMarshaller($marshaller)); 84 | } 85 | 86 | protected function doSave(array $values, int $lifetime, array $addTagData = [], array $delTagData = []): array 87 | { 88 | $eviction = $this->getRedisEvictionPolicy(); 89 | if ('noeviction' !== $eviction && !str_starts_with($eviction, 'volatile-')) { 90 | throw new LogicException(\sprintf('Redis maxmemory-policy setting "%s" is *not* supported by RedisTagAwareAdapter, use "noeviction" or "volatile-*" eviction policies.', $eviction)); 91 | } 92 | 93 | // serialize values 94 | if (!$serialized = $this->marshaller->marshall($values, $failed)) { 95 | return $failed; 96 | } 97 | 98 | // While pipeline isn't supported on RedisCluster, other setups will at least benefit from doing this in one op 99 | $results = $this->pipeline(static function () use ($serialized, $lifetime, $addTagData, $delTagData, $failed) { 100 | // Store cache items, force a ttl if none is set, as there is no MSETEX we need to set each one 101 | foreach ($serialized as $id => $value) { 102 | yield 'setEx' => [ 103 | $id, 104 | 0 >= $lifetime ? self::DEFAULT_CACHE_TTL : $lifetime, 105 | $value, 106 | ]; 107 | } 108 | 109 | // Add and Remove Tags 110 | foreach ($addTagData as $tagId => $ids) { 111 | if (!$failed || $ids = array_diff($ids, $failed)) { 112 | yield 'sAdd' => array_merge([$tagId], $ids); 113 | } 114 | } 115 | 116 | foreach ($delTagData as $tagId => $ids) { 117 | if (!$failed || $ids = array_diff($ids, $failed)) { 118 | yield 'sRem' => array_merge([$tagId], $ids); 119 | } 120 | } 121 | }); 122 | 123 | foreach ($results as $id => $result) { 124 | // Skip results of SADD/SREM operations, they'll be 1 or 0 depending on if set value already existed or not 125 | if (is_numeric($result)) { 126 | continue; 127 | } 128 | // setEx results 129 | if (true !== $result && (!$result instanceof Status || Status::get('OK') !== $result)) { 130 | $failed[] = $id; 131 | } 132 | } 133 | 134 | return $failed; 135 | } 136 | 137 | protected function doDeleteYieldTags(array $ids): iterable 138 | { 139 | $lua = <<<'EOLUA' 140 | local v = redis.call('GET', KEYS[1]) 141 | local e = redis.pcall('UNLINK', KEYS[1]) 142 | 143 | if type(e) ~= 'number' then 144 | redis.call('DEL', KEYS[1]) 145 | end 146 | 147 | if not v or v:len() <= 13 or v:byte(1) ~= 0x9D or v:byte(6) ~= 0 or v:byte(10) ~= 0x5F then 148 | return '' 149 | end 150 | 151 | return v:sub(14, 13 + v:byte(13) + v:byte(12) * 256 + v:byte(11) * 65536) 152 | EOLUA; 153 | 154 | $results = $this->pipeline(function () use ($ids, $lua) { 155 | foreach ($ids as $id) { 156 | yield 'eval' => $this->redis instanceof \Predis\ClientInterface ? [$lua, 1, $id] : [$lua, [$id], 1]; 157 | } 158 | }); 159 | 160 | foreach ($results as $id => $result) { 161 | if ($result instanceof \RedisException || $result instanceof \Relay\Exception || $result instanceof ErrorInterface) { 162 | CacheItem::log($this->logger, 'Failed to delete key "{key}": '.$result->getMessage(), ['key' => substr($id, \strlen($this->rootNamespace)), 'exception' => $result]); 163 | 164 | continue; 165 | } 166 | 167 | try { 168 | yield $id => !\is_string($result) || '' === $result ? [] : $this->marshaller->unmarshall($result); 169 | } catch (\Exception) { 170 | yield $id => []; 171 | } 172 | } 173 | } 174 | 175 | protected function doDeleteTagRelations(array $tagData): bool 176 | { 177 | $results = $this->pipeline(static function () use ($tagData) { 178 | foreach ($tagData as $tagId => $idList) { 179 | array_unshift($idList, $tagId); 180 | yield 'sRem' => $idList; 181 | } 182 | }); 183 | foreach ($results as $result) { 184 | // no-op 185 | } 186 | 187 | return true; 188 | } 189 | 190 | protected function doInvalidate(array $tagIds): bool 191 | { 192 | // This script scans the set of items linked to tag: it empties the set 193 | // and removes the linked items. When the set is still not empty after 194 | // the scan, it means we're in cluster mode and that the linked items 195 | // are on other nodes: we move the links to a temporary set and we 196 | // garbage collect that set from the client side. 197 | 198 | $lua = <<<'EOLUA' 199 | redis.replicate_commands() 200 | 201 | local cursor = '0' 202 | local id = KEYS[1] 203 | repeat 204 | local result = redis.call('SSCAN', id, cursor, 'COUNT', 5000); 205 | cursor = result[1]; 206 | local rems = {} 207 | 208 | for _, v in ipairs(result[2]) do 209 | local ok, _ = pcall(redis.call, 'DEL', ARGV[1]..v) 210 | if ok then 211 | table.insert(rems, v) 212 | end 213 | end 214 | if 0 < #rems then 215 | redis.call('SREM', id, unpack(rems)) 216 | end 217 | until '0' == cursor; 218 | 219 | redis.call('SUNIONSTORE', '{'..id..'}'..id, id) 220 | redis.call('DEL', id) 221 | 222 | return redis.call('SSCAN', '{'..id..'}'..id, '0', 'COUNT', 5000) 223 | EOLUA; 224 | 225 | $results = $this->pipeline(function () use ($tagIds, $lua) { 226 | if ($this->redis instanceof \Predis\ClientInterface) { 227 | $prefix = $this->redis->getOptions()->prefix ? $this->redis->getOptions()->prefix->getPrefix() : ''; 228 | } elseif (\is_array($prefix = $this->redis->getOption(($this->redis instanceof Relay || $this->redis instanceof \Relay\Cluster) ? Relay::OPT_PREFIX : \Redis::OPT_PREFIX) ?? '')) { 229 | $prefix = current($prefix); 230 | } 231 | 232 | foreach ($tagIds as $id) { 233 | yield 'eval' => $this->redis instanceof \Predis\ClientInterface ? [$lua, 1, $id, $prefix] : [$lua, [$id, $prefix], 1]; 234 | } 235 | }); 236 | 237 | $lua = <<<'EOLUA' 238 | redis.replicate_commands() 239 | 240 | local id = KEYS[1] 241 | local cursor = table.remove(ARGV) 242 | redis.call('SREM', '{'..id..'}'..id, unpack(ARGV)) 243 | 244 | return redis.call('SSCAN', '{'..id..'}'..id, cursor, 'COUNT', 5000) 245 | EOLUA; 246 | 247 | $success = true; 248 | foreach ($results as $id => $values) { 249 | if ($values instanceof \RedisException || $values instanceof \Relay\Exception || $values instanceof ErrorInterface) { 250 | CacheItem::log($this->logger, 'Failed to invalidate key "{key}": '.$values->getMessage(), ['key' => substr($id, \strlen($this->namespace)), 'exception' => $values]); 251 | $success = false; 252 | 253 | continue; 254 | } 255 | 256 | [$cursor, $ids] = $values; 257 | 258 | while ($ids || '0' !== $cursor) { 259 | $this->doDelete($ids); 260 | 261 | $evalArgs = [$id, $cursor]; 262 | array_splice($evalArgs, 1, 0, $ids); 263 | 264 | if ($this->redis instanceof \Predis\ClientInterface) { 265 | array_unshift($evalArgs, $lua, 1); 266 | } else { 267 | $evalArgs = [$lua, $evalArgs, 1]; 268 | } 269 | 270 | $results = $this->pipeline(function () use ($evalArgs) { 271 | yield 'eval' => $evalArgs; 272 | }); 273 | 274 | foreach ($results as [$cursor, $ids]) { 275 | // no-op 276 | } 277 | } 278 | } 279 | 280 | return $success; 281 | } 282 | 283 | private function getRedisEvictionPolicy(): string 284 | { 285 | if (isset($this->redisEvictionPolicy)) { 286 | return $this->redisEvictionPolicy; 287 | } 288 | 289 | $hosts = $this->getHosts(); 290 | $host = reset($hosts); 291 | if ($host instanceof \Predis\Client && $host->getConnection() instanceof ReplicationInterface) { 292 | // Predis supports info command only on the master in replication environments 293 | $hosts = [$host->getClientFor('master')]; 294 | } 295 | 296 | foreach ($hosts as $host) { 297 | $info = $host->info('Memory'); 298 | 299 | if (false === $info || null === $info || $info instanceof ErrorInterface) { 300 | continue; 301 | } 302 | 303 | $info = $info['Memory'] ?? $info; 304 | 305 | return $this->redisEvictionPolicy = $info['maxmemory_policy'] ?? ''; 306 | } 307 | 308 | return $this->redisEvictionPolicy = ''; 309 | } 310 | } 311 | -------------------------------------------------------------------------------- /Adapter/TagAwareAdapterInterface.php: -------------------------------------------------------------------------------- 1 | 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\Component\Cache\Adapter; 13 | 14 | use Psr\Cache\InvalidArgumentException; 15 | 16 | /** 17 | * Interface for invalidating cached items using tags. 18 | * 19 | * @author Nicolas Grekas 20 | */ 21 | interface TagAwareAdapterInterface extends AdapterInterface 22 | { 23 | /** 24 | * Invalidates cached items using tags. 25 | * 26 | * @param string[] $tags An array of tags to invalidate 27 | * 28 | * @throws InvalidArgumentException When $tags is not valid 29 | */ 30 | public function invalidateTags(array $tags): bool; 31 | } 32 | -------------------------------------------------------------------------------- /Adapter/TraceableAdapter.php: -------------------------------------------------------------------------------- 1 | 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\Component\Cache\Adapter; 13 | 14 | use Psr\Cache\CacheItemInterface; 15 | use Symfony\Component\Cache\CacheItem; 16 | use Symfony\Component\Cache\Exception\BadMethodCallException; 17 | use Symfony\Component\Cache\PruneableInterface; 18 | use Symfony\Component\Cache\ResettableInterface; 19 | use Symfony\Contracts\Cache\CacheInterface; 20 | use Symfony\Contracts\Cache\NamespacedPoolInterface; 21 | use Symfony\Contracts\Service\ResetInterface; 22 | 23 | /** 24 | * An adapter that collects data about all cache calls. 25 | * 26 | * @author Aaron Scherer 27 | * @author Tobias Nyholm 28 | * @author Nicolas Grekas 29 | */ 30 | class TraceableAdapter implements AdapterInterface, CacheInterface, NamespacedPoolInterface, PruneableInterface, ResettableInterface 31 | { 32 | private string $namespace = ''; 33 | private array $calls = []; 34 | 35 | public function __construct( 36 | protected AdapterInterface $pool, 37 | protected readonly ?\Closure $disabled = null, 38 | ) { 39 | } 40 | 41 | /** 42 | * @throws BadMethodCallException When the item pool is not a CacheInterface 43 | */ 44 | public function get(string $key, callable $callback, ?float $beta = null, ?array &$metadata = null): mixed 45 | { 46 | if (!$this->pool instanceof CacheInterface) { 47 | throw new BadMethodCallException(\sprintf('Cannot call "%s::get()": this class doesn\'t implement "%s".', get_debug_type($this->pool), CacheInterface::class)); 48 | } 49 | if ($this->disabled?->__invoke()) { 50 | return $this->pool->get($key, $callback, $beta, $metadata); 51 | } 52 | 53 | $isHit = true; 54 | $callback = function (CacheItem $item, bool &$save) use ($callback, &$isHit) { 55 | $isHit = $item->isHit(); 56 | 57 | return $callback($item, $save); 58 | }; 59 | 60 | $event = $this->start(__FUNCTION__); 61 | try { 62 | $value = $this->pool->get($key, $callback, $beta, $metadata); 63 | $event->result[$key] = get_debug_type($value); 64 | } finally { 65 | $event->end = microtime(true); 66 | } 67 | if ($isHit) { 68 | ++$event->hits; 69 | } else { 70 | ++$event->misses; 71 | } 72 | 73 | return $value; 74 | } 75 | 76 | public function getItem(mixed $key): CacheItem 77 | { 78 | if ($this->disabled?->__invoke()) { 79 | return $this->pool->getItem($key); 80 | } 81 | $event = $this->start(__FUNCTION__); 82 | try { 83 | $item = $this->pool->getItem($key); 84 | } finally { 85 | $event->end = microtime(true); 86 | } 87 | if ($event->result[$key] = $item->isHit()) { 88 | ++$event->hits; 89 | } else { 90 | ++$event->misses; 91 | } 92 | 93 | return $item; 94 | } 95 | 96 | public function hasItem(mixed $key): bool 97 | { 98 | if ($this->disabled?->__invoke()) { 99 | return $this->pool->hasItem($key); 100 | } 101 | $event = $this->start(__FUNCTION__); 102 | try { 103 | return $event->result[$key] = $this->pool->hasItem($key); 104 | } finally { 105 | $event->end = microtime(true); 106 | } 107 | } 108 | 109 | public function deleteItem(mixed $key): bool 110 | { 111 | if ($this->disabled?->__invoke()) { 112 | return $this->pool->deleteItem($key); 113 | } 114 | $event = $this->start(__FUNCTION__); 115 | try { 116 | return $event->result[$key] = $this->pool->deleteItem($key); 117 | } finally { 118 | $event->end = microtime(true); 119 | } 120 | } 121 | 122 | public function save(CacheItemInterface $item): bool 123 | { 124 | if ($this->disabled?->__invoke()) { 125 | return $this->pool->save($item); 126 | } 127 | $event = $this->start(__FUNCTION__); 128 | try { 129 | return $event->result[$item->getKey()] = $this->pool->save($item); 130 | } finally { 131 | $event->end = microtime(true); 132 | } 133 | } 134 | 135 | public function saveDeferred(CacheItemInterface $item): bool 136 | { 137 | if ($this->disabled?->__invoke()) { 138 | return $this->pool->saveDeferred($item); 139 | } 140 | $event = $this->start(__FUNCTION__); 141 | try { 142 | return $event->result[$item->getKey()] = $this->pool->saveDeferred($item); 143 | } finally { 144 | $event->end = microtime(true); 145 | } 146 | } 147 | 148 | public function getItems(array $keys = []): iterable 149 | { 150 | if ($this->disabled?->__invoke()) { 151 | return $this->pool->getItems($keys); 152 | } 153 | $event = $this->start(__FUNCTION__); 154 | try { 155 | $result = $this->pool->getItems($keys); 156 | } finally { 157 | $event->end = microtime(true); 158 | } 159 | $f = function () use ($result, $event) { 160 | $event->result = []; 161 | foreach ($result as $key => $item) { 162 | if ($event->result[$key] = $item->isHit()) { 163 | ++$event->hits; 164 | } else { 165 | ++$event->misses; 166 | } 167 | yield $key => $item; 168 | } 169 | }; 170 | 171 | return $f(); 172 | } 173 | 174 | public function clear(string $prefix = ''): bool 175 | { 176 | if ($this->disabled?->__invoke()) { 177 | return $this->pool->clear($prefix); 178 | } 179 | $event = $this->start(__FUNCTION__); 180 | try { 181 | if ($this->pool instanceof AdapterInterface) { 182 | return $event->result = $this->pool->clear($prefix); 183 | } 184 | 185 | return $event->result = $this->pool->clear(); 186 | } finally { 187 | $event->end = microtime(true); 188 | } 189 | } 190 | 191 | public function deleteItems(array $keys): bool 192 | { 193 | if ($this->disabled?->__invoke()) { 194 | return $this->pool->deleteItems($keys); 195 | } 196 | $event = $this->start(__FUNCTION__); 197 | $event->result['keys'] = $keys; 198 | try { 199 | return $event->result['result'] = $this->pool->deleteItems($keys); 200 | } finally { 201 | $event->end = microtime(true); 202 | } 203 | } 204 | 205 | public function commit(): bool 206 | { 207 | if ($this->disabled?->__invoke()) { 208 | return $this->pool->commit(); 209 | } 210 | $event = $this->start(__FUNCTION__); 211 | try { 212 | return $event->result = $this->pool->commit(); 213 | } finally { 214 | $event->end = microtime(true); 215 | } 216 | } 217 | 218 | public function prune(): bool 219 | { 220 | if (!$this->pool instanceof PruneableInterface) { 221 | return false; 222 | } 223 | if ($this->disabled?->__invoke()) { 224 | return $this->pool->prune(); 225 | } 226 | $event = $this->start(__FUNCTION__); 227 | try { 228 | return $event->result = $this->pool->prune(); 229 | } finally { 230 | $event->end = microtime(true); 231 | } 232 | } 233 | 234 | public function reset(): void 235 | { 236 | if ($this->pool instanceof ResetInterface) { 237 | $this->pool->reset(); 238 | } 239 | 240 | $this->clearCalls(); 241 | } 242 | 243 | public function delete(string $key): bool 244 | { 245 | if ($this->disabled?->__invoke()) { 246 | return $this->pool->deleteItem($key); 247 | } 248 | $event = $this->start(__FUNCTION__); 249 | try { 250 | return $event->result[$key] = $this->pool->deleteItem($key); 251 | } finally { 252 | $event->end = microtime(true); 253 | } 254 | } 255 | 256 | public function getCalls(): array 257 | { 258 | return $this->calls; 259 | } 260 | 261 | public function clearCalls(): void 262 | { 263 | $this->calls = []; 264 | } 265 | 266 | public function getPool(): AdapterInterface 267 | { 268 | return $this->pool; 269 | } 270 | 271 | /** 272 | * @throws BadMethodCallException When the item pool is not a NamespacedPoolInterface 273 | */ 274 | public function withSubNamespace(string $namespace): static 275 | { 276 | if (!$this->pool instanceof NamespacedPoolInterface) { 277 | throw new BadMethodCallException(\sprintf('Cannot call "%s::withSubNamespace()": this class doesn\'t implement "%s".', get_debug_type($this->pool), NamespacedPoolInterface::class)); 278 | } 279 | 280 | $calls = &$this->calls; // ensures clones share the same array 281 | $clone = clone $this; 282 | $clone->namespace .= CacheItem::validateKey($namespace).':'; 283 | $clone->pool = $this->pool->withSubNamespace($namespace); 284 | 285 | return $clone; 286 | } 287 | 288 | protected function start(string $name): TraceableAdapterEvent 289 | { 290 | $this->calls[] = $event = new TraceableAdapterEvent(); 291 | $event->name = $name; 292 | $event->start = microtime(true); 293 | $event->namespace = $this->namespace; 294 | 295 | return $event; 296 | } 297 | } 298 | 299 | /** 300 | * @internal 301 | */ 302 | class TraceableAdapterEvent 303 | { 304 | public string $name; 305 | public float $start; 306 | public float $end; 307 | public array|bool $result; 308 | public int $hits = 0; 309 | public int $misses = 0; 310 | public string $namespace; 311 | } 312 | -------------------------------------------------------------------------------- /Adapter/TraceableTagAwareAdapter.php: -------------------------------------------------------------------------------- 1 | 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\Component\Cache\Adapter; 13 | 14 | use Symfony\Contracts\Cache\TagAwareCacheInterface; 15 | 16 | /** 17 | * @author Robin Chalas 18 | */ 19 | class TraceableTagAwareAdapter extends TraceableAdapter implements TagAwareAdapterInterface, TagAwareCacheInterface 20 | { 21 | public function __construct(TagAwareAdapterInterface $pool, ?\Closure $disabled = null) 22 | { 23 | parent::__construct($pool, $disabled); 24 | } 25 | 26 | public function invalidateTags(array $tags): bool 27 | { 28 | if ($this->disabled?->__invoke()) { 29 | return $this->pool->invalidateTags($tags); 30 | } 31 | $event = $this->start(__FUNCTION__); 32 | try { 33 | return $event->result = $this->pool->invalidateTags($tags); 34 | } finally { 35 | $event->end = microtime(true); 36 | } 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | CHANGELOG 2 | ========= 3 | 4 | 7.3 5 | --- 6 | 7 | * Add support for `\Relay\Cluster` in `RedisAdapter` 8 | * Add support for `valkey:` / `valkeys:` schemes 9 | * Add support for namespace-based invalidation 10 | * Rename options "redis_cluster" and "redis_sentinel" to "cluster" and "sentinel" respectively 11 | 12 | 7.2 13 | --- 14 | 15 | * `igbinary_serialize()` is no longer used instead of `serialize()` by default when the igbinary extension is installed, 16 | due to behavior compatibilities between the two 17 | * Add optional `Psr\Clock\ClockInterface` parameter to `ArrayAdapter` 18 | 19 | 7.1 20 | --- 21 | 22 | * Add option `sentinel_master` as an alias for `redis_sentinel` 23 | * Deprecate `CouchbaseBucketAdapter`, use `CouchbaseCollectionAdapter` 24 | * Add support for URL encoded characters in Couchbase DSN 25 | * Add support for using DSN with PDOAdapter 26 | * The algorithm for the default cache namespace changed from SHA256 to XXH128 27 | 28 | 7.0 29 | --- 30 | 31 | * Add parameter `$isSameDatabase` to `DoctrineDbalAdapter::configureSchema()` 32 | * Drop support for Postgres < 9.5 and SQL Server < 2008 in `DoctrineDbalAdapter` 33 | 34 | 6.4 35 | --- 36 | 37 | * `EarlyExpirationHandler` no longer implements `MessageHandlerInterface`, rely on `AsMessageHandler` instead 38 | 39 | 6.3 40 | --- 41 | 42 | * Add support for Relay PHP extension for Redis 43 | * Updates to allow Redis cluster connections using predis/predis:^2.0 44 | * Add optional parameter `$isSameDatabase` to `DoctrineDbalAdapter::configureSchema()` 45 | 46 | 6.1 47 | --- 48 | 49 | * Add support for ACL auth in RedisAdapter 50 | * Improve reliability and performance of `TagAwareAdapter` by making tag versions an integral part of item value 51 | 52 | 6.0 53 | --- 54 | 55 | * Remove `DoctrineProvider` and `DoctrineAdapter` 56 | * Remove support of Doctrine DBAL in `PdoAdapter` 57 | 58 | 5.4 59 | --- 60 | 61 | * Deprecate `DoctrineProvider` and `DoctrineAdapter` because these classes have been added to the `doctrine/cache` package 62 | * Add `DoctrineDbalAdapter` identical to `PdoAdapter` for `Doctrine\DBAL\Connection` or DBAL URL 63 | * Deprecate usage of `PdoAdapter` with `Doctrine\DBAL\Connection` or DBAL URL 64 | 65 | 5.3 66 | --- 67 | 68 | * added support for connecting to Redis Sentinel clusters when using the Redis PHP extension 69 | * add support for a custom serializer to the `ApcuAdapter` class 70 | 71 | 5.2.0 72 | ----- 73 | 74 | * added integration with Messenger to allow computing cached values in a worker 75 | * allow ISO 8601 time intervals to specify default lifetime 76 | 77 | 5.1.0 78 | ----- 79 | 80 | * added max-items + LRU + max-lifetime capabilities to `ArrayCache` 81 | * added `CouchbaseBucketAdapter` 82 | * added context `cache-adapter` to log messages 83 | 84 | 5.0.0 85 | ----- 86 | 87 | * removed all PSR-16 implementations in the `Simple` namespace 88 | * removed `SimpleCacheAdapter` 89 | * removed `AbstractAdapter::unserialize()` 90 | * removed `CacheItem::getPreviousTags()` 91 | 92 | 4.4.0 93 | ----- 94 | 95 | * added support for connecting to Redis Sentinel clusters 96 | * added argument `$prefix` to `AdapterInterface::clear()` 97 | * improved `RedisTagAwareAdapter` to support Redis server >= 2.8 and up to 4B items per tag 98 | * added `TagAwareMarshaller` for optimized data storage when using `AbstractTagAwareAdapter` 99 | * added `DeflateMarshaller` to compress serialized values 100 | * removed support for phpredis 4 `compression` 101 | * [BC BREAK] `RedisTagAwareAdapter` is not compatible with `RedisCluster` from `Predis` anymore, use `phpredis` instead 102 | * Marked the `CacheDataCollector` class as `@final`. 103 | * added `SodiumMarshaller` to encrypt/decrypt values using libsodium 104 | 105 | 4.3.0 106 | ----- 107 | 108 | * removed `psr/simple-cache` dependency, run `composer require psr/simple-cache` if you need it 109 | * deprecated all PSR-16 adapters, use `Psr16Cache` or `Symfony\Contracts\Cache\CacheInterface` implementations instead 110 | * deprecated `SimpleCacheAdapter`, use `Psr16Adapter` instead 111 | 112 | 4.2.0 113 | ----- 114 | 115 | * added support for connecting to Redis clusters via DSN 116 | * added support for configuring multiple Memcached servers via DSN 117 | * added `MarshallerInterface` and `DefaultMarshaller` to allow changing the serializer and provide one that automatically uses igbinary when available 118 | * implemented `CacheInterface`, which provides stampede protection via probabilistic early expiration and should become the preferred way to use a cache 119 | * added sub-second expiry accuracy for backends that support it 120 | * added support for phpredis 4 `compression` and `tcp_keepalive` options 121 | * added automatic table creation when using Doctrine DBAL with PDO-based backends 122 | * throw `LogicException` when `CacheItem::tag()` is called on an item coming from a non tag-aware pool 123 | * deprecated `CacheItem::getPreviousTags()`, use `CacheItem::getMetadata()` instead 124 | * deprecated the `AbstractAdapter::unserialize()` and `AbstractCache::unserialize()` methods 125 | * added `CacheCollectorPass` (originally in `FrameworkBundle`) 126 | * added `CachePoolClearerPass` (originally in `FrameworkBundle`) 127 | * added `CachePoolPass` (originally in `FrameworkBundle`) 128 | * added `CachePoolPrunerPass` (originally in `FrameworkBundle`) 129 | 130 | 3.4.0 131 | ----- 132 | 133 | * added using options from Memcached DSN 134 | * added PruneableInterface so PSR-6 or PSR-16 cache implementations can declare support for manual stale cache pruning 135 | * added prune logic to FilesystemTrait, PhpFilesTrait, PdoTrait, TagAwareAdapter and ChainTrait 136 | * now FilesystemAdapter, PhpFilesAdapter, FilesystemCache, PhpFilesCache, PdoAdapter, PdoCache, ChainAdapter, and 137 | ChainCache implement PruneableInterface and support manual stale cache pruning 138 | 139 | 3.3.0 140 | ----- 141 | 142 | * added CacheItem::getPreviousTags() to get bound tags coming from the pool storage if any 143 | * added PSR-16 "Simple Cache" implementations for all existing PSR-6 adapters 144 | * added Psr6Cache and SimpleCacheAdapter for bidirectional interoperability between PSR-6 and PSR-16 145 | * added MemcachedAdapter (PSR-6) and MemcachedCache (PSR-16) 146 | * added TraceableAdapter (PSR-6) and TraceableCache (PSR-16) 147 | 148 | 3.2.0 149 | ----- 150 | 151 | * added TagAwareAdapter for tags-based invalidation 152 | * added PdoAdapter with PDO and Doctrine DBAL support 153 | * added PhpArrayAdapter and PhpFilesAdapter for OPcache-backed shared memory storage (PHP 7+ only) 154 | * added NullAdapter 155 | 156 | 3.1.0 157 | ----- 158 | 159 | * added the component with strict PSR-6 implementations 160 | * added ApcuAdapter, ArrayAdapter, FilesystemAdapter and RedisAdapter 161 | * added AbstractAdapter, ChainAdapter and ProxyAdapter 162 | * added DoctrineAdapter and DoctrineProvider for bidirectional interoperability with Doctrine Cache 163 | -------------------------------------------------------------------------------- /CacheItem.php: -------------------------------------------------------------------------------- 1 | 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\Component\Cache; 13 | 14 | use Psr\Cache\CacheItemInterface; 15 | use Psr\Log\LoggerInterface; 16 | use Symfony\Component\Cache\Exception\InvalidArgumentException; 17 | use Symfony\Component\Cache\Exception\LogicException; 18 | use Symfony\Contracts\Cache\ItemInterface; 19 | 20 | /** 21 | * @author Nicolas Grekas 22 | */ 23 | final class CacheItem implements ItemInterface 24 | { 25 | private const METADATA_EXPIRY_OFFSET = 1527506807; 26 | private const VALUE_WRAPPER = "\xA9"; 27 | 28 | protected string $key; 29 | protected mixed $value = null; 30 | protected bool $isHit = false; 31 | protected float|int|null $expiry = null; 32 | protected array $metadata = []; 33 | protected array $newMetadata = []; 34 | protected ?CacheItemInterface $innerItem = null; 35 | protected ?string $poolHash = null; 36 | protected bool $isTaggable = false; 37 | 38 | public function getKey(): string 39 | { 40 | return $this->key; 41 | } 42 | 43 | public function get(): mixed 44 | { 45 | return $this->value; 46 | } 47 | 48 | public function isHit(): bool 49 | { 50 | return $this->isHit; 51 | } 52 | 53 | /** 54 | * @return $this 55 | */ 56 | public function set($value): static 57 | { 58 | $this->value = $value; 59 | 60 | return $this; 61 | } 62 | 63 | /** 64 | * @return $this 65 | */ 66 | public function expiresAt(?\DateTimeInterface $expiration): static 67 | { 68 | $this->expiry = null !== $expiration ? (float) $expiration->format('U.u') : null; 69 | 70 | return $this; 71 | } 72 | 73 | /** 74 | * @return $this 75 | */ 76 | public function expiresAfter(mixed $time): static 77 | { 78 | if (null === $time) { 79 | $this->expiry = null; 80 | } elseif ($time instanceof \DateInterval) { 81 | $this->expiry = microtime(true) + \DateTimeImmutable::createFromFormat('U', 0)->add($time)->format('U.u'); 82 | } elseif (\is_int($time)) { 83 | $this->expiry = $time + microtime(true); 84 | } else { 85 | throw new InvalidArgumentException(\sprintf('Expiration date must be an integer, a DateInterval or null, "%s" given.', get_debug_type($time))); 86 | } 87 | 88 | return $this; 89 | } 90 | 91 | public function tag(mixed $tags): static 92 | { 93 | if (!$this->isTaggable) { 94 | throw new LogicException(\sprintf('Cache item "%s" comes from a non tag-aware pool: you cannot tag it.', $this->key)); 95 | } 96 | if (!\is_array($tags) && !$tags instanceof \Traversable) { // don't use is_iterable(), it's slow 97 | $tags = [$tags]; 98 | } 99 | foreach ($tags as $tag) { 100 | if (!\is_string($tag) && !$tag instanceof \Stringable) { 101 | throw new InvalidArgumentException(\sprintf('Cache tag must be string or object that implements __toString(), "%s" given.', get_debug_type($tag))); 102 | } 103 | $tag = (string) $tag; 104 | if (isset($this->newMetadata[self::METADATA_TAGS][$tag])) { 105 | continue; 106 | } 107 | if ('' === $tag) { 108 | throw new InvalidArgumentException('Cache tag length must be greater than zero.'); 109 | } 110 | if (false !== strpbrk($tag, self::RESERVED_CHARACTERS)) { 111 | throw new InvalidArgumentException(\sprintf('Cache tag "%s" contains reserved characters "%s".', $tag, self::RESERVED_CHARACTERS)); 112 | } 113 | $this->newMetadata[self::METADATA_TAGS][$tag] = $tag; 114 | } 115 | 116 | return $this; 117 | } 118 | 119 | public function getMetadata(): array 120 | { 121 | return $this->metadata; 122 | } 123 | 124 | /** 125 | * Validates a cache key according to PSR-6. 126 | * 127 | * @param mixed $key The key to validate 128 | * 129 | * @throws InvalidArgumentException When $key is not valid 130 | */ 131 | public static function validateKey($key): string 132 | { 133 | if (!\is_string($key)) { 134 | throw new InvalidArgumentException(\sprintf('Cache key must be string, "%s" given.', get_debug_type($key))); 135 | } 136 | if ('' === $key) { 137 | throw new InvalidArgumentException('Cache key length must be greater than zero.'); 138 | } 139 | if (false !== strpbrk($key, self::RESERVED_CHARACTERS)) { 140 | throw new InvalidArgumentException(\sprintf('Cache key "%s" contains reserved characters "%s".', $key, self::RESERVED_CHARACTERS)); 141 | } 142 | 143 | return $key; 144 | } 145 | 146 | /** 147 | * Internal logging helper. 148 | * 149 | * @internal 150 | */ 151 | public static function log(?LoggerInterface $logger, string $message, array $context = []): void 152 | { 153 | if ($logger) { 154 | $logger->warning($message, $context); 155 | } else { 156 | $replace = []; 157 | foreach ($context as $k => $v) { 158 | if (\is_scalar($v)) { 159 | $replace['{'.$k.'}'] = $v; 160 | } 161 | } 162 | @trigger_error(strtr($message, $replace), \E_USER_WARNING); 163 | } 164 | } 165 | 166 | private function pack(): mixed 167 | { 168 | if (!$m = $this->newMetadata) { 169 | return $this->value; 170 | } 171 | $valueWrapper = self::VALUE_WRAPPER; 172 | 173 | return new $valueWrapper($this->value, $m + ['expiry' => $this->expiry]); 174 | } 175 | 176 | private function unpack(): bool 177 | { 178 | $v = $this->value; 179 | $valueWrapper = self::VALUE_WRAPPER; 180 | 181 | if ($v instanceof $valueWrapper) { 182 | $this->value = $v->value; 183 | $this->metadata = $v->metadata; 184 | 185 | return true; 186 | } 187 | 188 | if (!\is_array($v) || 1 !== \count($v) || 10 !== \strlen($k = (string) array_key_first($v)) || "\x9D" !== $k[0] || "\0" !== $k[5] || "\x5F" !== $k[9]) { 189 | return false; 190 | } 191 | 192 | // BC with pools populated before v6.1 193 | $this->value = $v[$k]; 194 | $this->metadata = unpack('Vexpiry/Nctime', substr($k, 1, -1)); 195 | $this->metadata['expiry'] += self::METADATA_EXPIRY_OFFSET; 196 | 197 | return true; 198 | } 199 | } 200 | -------------------------------------------------------------------------------- /DataCollector/CacheDataCollector.php: -------------------------------------------------------------------------------- 1 | 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\Component\Cache\DataCollector; 13 | 14 | use Symfony\Component\Cache\Adapter\TraceableAdapter; 15 | use Symfony\Component\Cache\Adapter\TraceableAdapterEvent; 16 | use Symfony\Component\HttpFoundation\Request; 17 | use Symfony\Component\HttpFoundation\Response; 18 | use Symfony\Component\HttpKernel\DataCollector\DataCollector; 19 | use Symfony\Component\HttpKernel\DataCollector\LateDataCollectorInterface; 20 | 21 | /** 22 | * @author Aaron Scherer 23 | * @author Tobias Nyholm 24 | * 25 | * @final 26 | */ 27 | class CacheDataCollector extends DataCollector implements LateDataCollectorInterface 28 | { 29 | /** 30 | * @var TraceableAdapter[] 31 | */ 32 | private array $instances = []; 33 | 34 | public function addInstance(string $name, TraceableAdapter $instance): void 35 | { 36 | $this->instances[$name] = $instance; 37 | } 38 | 39 | public function collect(Request $request, Response $response, ?\Throwable $exception = null): void 40 | { 41 | $this->lateCollect(); 42 | } 43 | 44 | public function reset(): void 45 | { 46 | $this->data = []; 47 | foreach ($this->instances as $instance) { 48 | $instance->clearCalls(); 49 | } 50 | } 51 | 52 | public function lateCollect(): void 53 | { 54 | $empty = ['calls' => [], 'adapters' => [], 'config' => [], 'options' => [], 'statistics' => []]; 55 | $this->data = ['instances' => $empty, 'total' => $empty]; 56 | foreach ($this->instances as $name => $instance) { 57 | $this->data['instances']['calls'][$name] = $instance->getCalls(); 58 | $this->data['instances']['adapters'][$name] = get_debug_type($instance->getPool()); 59 | } 60 | 61 | $this->data['instances']['statistics'] = $this->calculateStatistics(); 62 | $this->data['total']['statistics'] = $this->calculateTotalStatistics(); 63 | $this->data['instances']['calls'] = $this->cloneVar($this->data['instances']['calls']); 64 | } 65 | 66 | public function getName(): string 67 | { 68 | return 'cache'; 69 | } 70 | 71 | /** 72 | * Method returns amount of logged Cache reads: "get" calls. 73 | */ 74 | public function getStatistics(): array 75 | { 76 | return $this->data['instances']['statistics']; 77 | } 78 | 79 | /** 80 | * Method returns the statistic totals. 81 | */ 82 | public function getTotals(): array 83 | { 84 | return $this->data['total']['statistics']; 85 | } 86 | 87 | /** 88 | * Method returns all logged Cache call objects. 89 | */ 90 | public function getCalls(): mixed 91 | { 92 | return $this->data['instances']['calls']; 93 | } 94 | 95 | /** 96 | * Method returns all logged Cache adapter classes. 97 | */ 98 | public function getAdapters(): array 99 | { 100 | return $this->data['instances']['adapters']; 101 | } 102 | 103 | private function calculateStatistics(): array 104 | { 105 | $statistics = []; 106 | foreach ($this->data['instances']['calls'] as $name => $calls) { 107 | $statistics[$name] = [ 108 | 'calls' => 0, 109 | 'time' => 0, 110 | 'reads' => 0, 111 | 'writes' => 0, 112 | 'deletes' => 0, 113 | 'hits' => 0, 114 | 'misses' => 0, 115 | ]; 116 | /** @var TraceableAdapterEvent $call */ 117 | foreach ($calls as $call) { 118 | ++$statistics[$name]['calls']; 119 | $statistics[$name]['time'] += ($call->end ?? microtime(true)) - $call->start; 120 | if ('get' === $call->name) { 121 | ++$statistics[$name]['reads']; 122 | if ($call->hits) { 123 | ++$statistics[$name]['hits']; 124 | } else { 125 | ++$statistics[$name]['misses']; 126 | ++$statistics[$name]['writes']; 127 | } 128 | } elseif ('getItem' === $call->name) { 129 | ++$statistics[$name]['reads']; 130 | if ($call->hits) { 131 | ++$statistics[$name]['hits']; 132 | } else { 133 | ++$statistics[$name]['misses']; 134 | } 135 | } elseif ('getItems' === $call->name) { 136 | $statistics[$name]['reads'] += $call->hits + $call->misses; 137 | $statistics[$name]['hits'] += $call->hits; 138 | $statistics[$name]['misses'] += $call->misses; 139 | } elseif ('hasItem' === $call->name) { 140 | ++$statistics[$name]['reads']; 141 | foreach ($call->result ?? [] as $result) { 142 | ++$statistics[$name][$result ? 'hits' : 'misses']; 143 | } 144 | } elseif ('save' === $call->name) { 145 | ++$statistics[$name]['writes']; 146 | } elseif ('deleteItem' === $call->name) { 147 | ++$statistics[$name]['deletes']; 148 | } 149 | } 150 | if ($statistics[$name]['reads']) { 151 | $statistics[$name]['hit_read_ratio'] = round(100 * $statistics[$name]['hits'] / $statistics[$name]['reads'], 2); 152 | } else { 153 | $statistics[$name]['hit_read_ratio'] = null; 154 | } 155 | } 156 | 157 | return $statistics; 158 | } 159 | 160 | private function calculateTotalStatistics(): array 161 | { 162 | $statistics = $this->getStatistics(); 163 | $totals = [ 164 | 'calls' => 0, 165 | 'time' => 0, 166 | 'reads' => 0, 167 | 'writes' => 0, 168 | 'deletes' => 0, 169 | 'hits' => 0, 170 | 'misses' => 0, 171 | ]; 172 | foreach ($statistics as $name => $values) { 173 | foreach ($totals as $key => $value) { 174 | $totals[$key] += $statistics[$name][$key]; 175 | } 176 | } 177 | if ($totals['reads']) { 178 | $totals['hit_read_ratio'] = round(100 * $totals['hits'] / $totals['reads'], 2); 179 | } else { 180 | $totals['hit_read_ratio'] = null; 181 | } 182 | 183 | return $totals; 184 | } 185 | } 186 | -------------------------------------------------------------------------------- /DependencyInjection/CacheCollectorPass.php: -------------------------------------------------------------------------------- 1 | 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\Component\Cache\DependencyInjection; 13 | 14 | use Symfony\Component\Cache\Adapter\TagAwareAdapterInterface; 15 | use Symfony\Component\Cache\Adapter\TraceableAdapter; 16 | use Symfony\Component\Cache\Adapter\TraceableTagAwareAdapter; 17 | use Symfony\Component\DependencyInjection\Compiler\CompilerPassInterface; 18 | use Symfony\Component\DependencyInjection\ContainerBuilder; 19 | use Symfony\Component\DependencyInjection\Definition; 20 | use Symfony\Component\DependencyInjection\Reference; 21 | 22 | /** 23 | * Inject a data collector to all the cache services to be able to get detailed statistics. 24 | * 25 | * @author Tobias Nyholm 26 | */ 27 | class CacheCollectorPass implements CompilerPassInterface 28 | { 29 | public function process(ContainerBuilder $container): void 30 | { 31 | if (!$container->hasDefinition('data_collector.cache')) { 32 | return; 33 | } 34 | 35 | foreach ($container->findTaggedServiceIds('cache.pool') as $id => $attributes) { 36 | $poolName = $attributes[0]['name'] ?? $id; 37 | 38 | $this->addToCollector($id, $poolName, $container); 39 | } 40 | } 41 | 42 | private function addToCollector(string $id, string $name, ContainerBuilder $container): void 43 | { 44 | $definition = $container->getDefinition($id); 45 | if ($definition->isAbstract()) { 46 | return; 47 | } 48 | 49 | $collectorDefinition = $container->getDefinition('data_collector.cache'); 50 | $recorder = new Definition(is_subclass_of($definition->getClass(), TagAwareAdapterInterface::class) ? TraceableTagAwareAdapter::class : TraceableAdapter::class); 51 | $recorder->setTags($definition->getTags()); 52 | if (!$definition->isPublic() || !$definition->isPrivate()) { 53 | $recorder->setPublic($definition->isPublic()); 54 | } 55 | $recorder->setArguments([new Reference($innerId = $id.'.recorder_inner'), new Reference('profiler.is_disabled_state_checker', ContainerBuilder::IGNORE_ON_INVALID_REFERENCE)]); 56 | 57 | foreach ($definition->getMethodCalls() as [$method, $args]) { 58 | if ('setCallbackWrapper' !== $method || !$args[0] instanceof Definition || !($args[0]->getArguments()[2] ?? null) instanceof Definition) { 59 | continue; 60 | } 61 | if ([new Reference($id), 'setCallbackWrapper'] == $args[0]->getArguments()[2]->getFactory()) { 62 | $args[0]->getArguments()[2]->setFactory([new Reference($innerId), 'setCallbackWrapper']); 63 | } 64 | } 65 | 66 | $definition->setTags([]); 67 | $definition->setPublic(false); 68 | 69 | $container->setDefinition($innerId, $definition); 70 | $container->setDefinition($id, $recorder); 71 | 72 | // Tell the collector to add the new instance 73 | $collectorDefinition->addMethodCall('addInstance', [$name, new Reference($id)]); 74 | } 75 | } 76 | -------------------------------------------------------------------------------- /DependencyInjection/CachePoolClearerPass.php: -------------------------------------------------------------------------------- 1 | 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\Component\Cache\DependencyInjection; 13 | 14 | use Symfony\Component\DependencyInjection\Compiler\CompilerPassInterface; 15 | use Symfony\Component\DependencyInjection\ContainerBuilder; 16 | use Symfony\Component\DependencyInjection\Reference; 17 | 18 | /** 19 | * @author Nicolas Grekas 20 | */ 21 | class CachePoolClearerPass implements CompilerPassInterface 22 | { 23 | public function process(ContainerBuilder $container): void 24 | { 25 | $container->getParameterBag()->remove('cache.prefix.seed'); 26 | 27 | foreach ($container->findTaggedServiceIds('cache.pool.clearer') as $id => $attr) { 28 | $clearer = $container->getDefinition($id); 29 | $pools = []; 30 | foreach ($clearer->getArgument(0) as $name => $ref) { 31 | if ($container->hasDefinition($ref)) { 32 | $pools[$name] = new Reference($ref); 33 | } 34 | } 35 | $clearer->replaceArgument(0, $pools); 36 | } 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /DependencyInjection/CachePoolPass.php: -------------------------------------------------------------------------------- 1 | 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\Component\Cache\DependencyInjection; 13 | 14 | use Symfony\Component\Cache\Adapter\AbstractAdapter; 15 | use Symfony\Component\Cache\Adapter\ArrayAdapter; 16 | use Symfony\Component\Cache\Adapter\ChainAdapter; 17 | use Symfony\Component\Cache\Adapter\NullAdapter; 18 | use Symfony\Component\Cache\Adapter\ParameterNormalizer; 19 | use Symfony\Component\Cache\Messenger\EarlyExpirationDispatcher; 20 | use Symfony\Component\DependencyInjection\ChildDefinition; 21 | use Symfony\Component\DependencyInjection\Compiler\CompilerPassInterface; 22 | use Symfony\Component\DependencyInjection\ContainerBuilder; 23 | use Symfony\Component\DependencyInjection\Definition; 24 | use Symfony\Component\DependencyInjection\Exception\InvalidArgumentException; 25 | use Symfony\Component\DependencyInjection\Reference; 26 | 27 | /** 28 | * @author Nicolas Grekas 29 | */ 30 | class CachePoolPass implements CompilerPassInterface 31 | { 32 | public function process(ContainerBuilder $container): void 33 | { 34 | if ($container->hasParameter('cache.prefix.seed')) { 35 | $seed = $container->getParameterBag()->resolveValue($container->getParameter('cache.prefix.seed')); 36 | } else { 37 | $seed = '_'.$container->getParameter('kernel.project_dir'); 38 | $seed .= '.'.$container->getParameter('kernel.container_class'); 39 | } 40 | 41 | $needsMessageHandler = false; 42 | $allPools = []; 43 | $clearers = []; 44 | $attributes = [ 45 | 'provider', 46 | 'name', 47 | 'namespace', 48 | 'default_lifetime', 49 | 'early_expiration_message_bus', 50 | 'reset', 51 | ]; 52 | foreach ($container->findTaggedServiceIds('cache.pool') as $id => $tags) { 53 | $adapter = $pool = $container->getDefinition($id); 54 | if ($pool->isAbstract()) { 55 | continue; 56 | } 57 | $class = $adapter->getClass(); 58 | while ($adapter instanceof ChildDefinition) { 59 | $adapter = $container->findDefinition($adapter->getParent()); 60 | $class = $class ?: $adapter->getClass(); 61 | if ($t = $adapter->getTag('cache.pool')) { 62 | $tags[0] += $t[0]; 63 | } 64 | } 65 | $name = $tags[0]['name'] ?? $id; 66 | if (!isset($tags[0]['namespace'])) { 67 | $namespaceSeed = $seed; 68 | if (null !== $class) { 69 | $namespaceSeed .= '.'.$class; 70 | } 71 | 72 | $tags[0]['namespace'] = $this->getNamespace($namespaceSeed, $name); 73 | } 74 | if (isset($tags[0]['clearer'])) { 75 | $clearer = $tags[0]['clearer']; 76 | while ($container->hasAlias($clearer)) { 77 | $clearer = (string) $container->getAlias($clearer); 78 | } 79 | } else { 80 | $clearer = null; 81 | } 82 | unset($tags[0]['clearer'], $tags[0]['name']); 83 | 84 | if (isset($tags[0]['provider'])) { 85 | $tags[0]['provider'] = new Reference(static::getServiceProvider($container, $tags[0]['provider'])); 86 | } 87 | 88 | if (ChainAdapter::class === $class) { 89 | $adapters = []; 90 | foreach ($adapter->getArgument(0) as $provider => $adapter) { 91 | if ($adapter instanceof ChildDefinition) { 92 | $chainedPool = $adapter; 93 | } else { 94 | $chainedPool = $adapter = new ChildDefinition($adapter); 95 | } 96 | 97 | $chainedTags = [\is_int($provider) ? [] : ['provider' => $provider]]; 98 | $chainedClass = ''; 99 | 100 | while ($adapter instanceof ChildDefinition) { 101 | $adapter = $container->findDefinition($adapter->getParent()); 102 | $chainedClass = $chainedClass ?: $adapter->getClass(); 103 | if ($t = $adapter->getTag('cache.pool')) { 104 | $chainedTags[0] += $t[0]; 105 | } 106 | } 107 | 108 | if (ChainAdapter::class === $chainedClass) { 109 | throw new InvalidArgumentException(\sprintf('Invalid service "%s": chain of adapters cannot reference another chain, found "%s".', $id, $chainedPool->getParent())); 110 | } 111 | 112 | $i = 0; 113 | 114 | if (isset($chainedTags[0]['provider'])) { 115 | $chainedPool->replaceArgument($i++, new Reference(static::getServiceProvider($container, $chainedTags[0]['provider']))); 116 | } 117 | 118 | if (isset($tags[0]['namespace']) && !\in_array($adapter->getClass(), [ArrayAdapter::class, NullAdapter::class], true)) { 119 | $chainedPool->replaceArgument($i++, $tags[0]['namespace']); 120 | } 121 | 122 | if (isset($tags[0]['default_lifetime'])) { 123 | $chainedPool->replaceArgument($i++, $tags[0]['default_lifetime']); 124 | } 125 | 126 | $adapters[] = $chainedPool; 127 | } 128 | 129 | $pool->replaceArgument(0, $adapters); 130 | unset($tags[0]['provider'], $tags[0]['namespace']); 131 | $i = 1; 132 | } else { 133 | $i = 0; 134 | } 135 | 136 | foreach ($attributes as $attr) { 137 | if (!isset($tags[0][$attr])) { 138 | // no-op 139 | } elseif ('reset' === $attr) { 140 | if ($tags[0][$attr]) { 141 | $pool->addTag('kernel.reset', ['method' => $tags[0][$attr]]); 142 | } 143 | } elseif ('early_expiration_message_bus' === $attr) { 144 | $needsMessageHandler = true; 145 | $pool->addMethodCall('setCallbackWrapper', [(new Definition(EarlyExpirationDispatcher::class)) 146 | ->addArgument(new Reference($tags[0]['early_expiration_message_bus'])) 147 | ->addArgument(new Reference('reverse_container')) 148 | ->addArgument((new Definition('callable')) 149 | ->setFactory([new Reference($id), 'setCallbackWrapper']) 150 | ->addArgument(null) 151 | ), 152 | ]); 153 | $pool->addTag('container.reversible'); 154 | } elseif ('namespace' !== $attr || !\in_array($class, [ArrayAdapter::class, NullAdapter::class], true)) { 155 | $argument = $tags[0][$attr]; 156 | 157 | if ('default_lifetime' === $attr && !is_numeric($argument)) { 158 | $argument = (new Definition('int', [$argument])) 159 | ->setFactory([ParameterNormalizer::class, 'normalizeDuration']); 160 | } 161 | 162 | $pool->replaceArgument($i++, $argument); 163 | } 164 | unset($tags[0][$attr]); 165 | } 166 | if (!empty($tags[0])) { 167 | throw new InvalidArgumentException(\sprintf('Invalid "cache.pool" tag for service "%s": accepted attributes are "clearer", "provider", "name", "namespace", "default_lifetime", "early_expiration_message_bus" and "reset", found "%s".', $id, implode('", "', array_keys($tags[0])))); 168 | } 169 | 170 | if (null !== $clearer) { 171 | $clearers[$clearer][$name] = new Reference($id, $container::IGNORE_ON_UNINITIALIZED_REFERENCE); 172 | } 173 | 174 | $allPools[$name] = new Reference($id, $container::IGNORE_ON_UNINITIALIZED_REFERENCE); 175 | } 176 | 177 | if (!$needsMessageHandler) { 178 | $container->removeDefinition('cache.early_expiration_handler'); 179 | } 180 | 181 | $notAliasedCacheClearerId = 'cache.global_clearer'; 182 | while ($container->hasAlias($notAliasedCacheClearerId)) { 183 | $notAliasedCacheClearerId = (string) $container->getAlias($notAliasedCacheClearerId); 184 | } 185 | if ($container->hasDefinition($notAliasedCacheClearerId)) { 186 | $clearers[$notAliasedCacheClearerId] = $allPools; 187 | } 188 | 189 | foreach ($clearers as $id => $pools) { 190 | $clearer = $container->getDefinition($id); 191 | if ($clearer instanceof ChildDefinition) { 192 | $clearer->replaceArgument(0, $pools); 193 | } else { 194 | $clearer->setArgument(0, $pools); 195 | } 196 | $clearer->addTag('cache.pool.clearer'); 197 | } 198 | 199 | $allPoolsKeys = array_keys($allPools); 200 | 201 | if ($container->hasDefinition('console.command.cache_pool_list')) { 202 | $container->getDefinition('console.command.cache_pool_list')->replaceArgument(0, $allPoolsKeys); 203 | } 204 | 205 | if ($container->hasDefinition('console.command.cache_pool_clear')) { 206 | $container->getDefinition('console.command.cache_pool_clear')->addArgument($allPoolsKeys); 207 | } 208 | 209 | if ($container->hasDefinition('console.command.cache_pool_delete')) { 210 | $container->getDefinition('console.command.cache_pool_delete')->addArgument($allPoolsKeys); 211 | } 212 | } 213 | 214 | private function getNamespace(string $seed, string $id): string 215 | { 216 | return substr(str_replace('/', '-', base64_encode(hash('xxh128', $id.$seed, true))), 0, 10); 217 | } 218 | 219 | /** 220 | * @internal 221 | */ 222 | public static function getServiceProvider(ContainerBuilder $container, string $name): string 223 | { 224 | $container->resolveEnvPlaceholders($name, null, $usedEnvs); 225 | 226 | if ($usedEnvs || preg_match('#^[a-z]++:#', $name)) { 227 | $dsn = $name; 228 | 229 | if (!$container->hasDefinition($name = '.cache_connection.'.ContainerBuilder::hash($dsn))) { 230 | $definition = new Definition(AbstractAdapter::class); 231 | $definition->setFactory([AbstractAdapter::class, 'createConnection']); 232 | $definition->setArguments([$dsn, ['lazy' => true]]); 233 | $container->setDefinition($name, $definition); 234 | } 235 | } 236 | 237 | return $name; 238 | } 239 | } 240 | -------------------------------------------------------------------------------- /DependencyInjection/CachePoolPrunerPass.php: -------------------------------------------------------------------------------- 1 | 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\Component\Cache\DependencyInjection; 13 | 14 | use Symfony\Component\Cache\PruneableInterface; 15 | use Symfony\Component\DependencyInjection\Argument\IteratorArgument; 16 | use Symfony\Component\DependencyInjection\Compiler\CompilerPassInterface; 17 | use Symfony\Component\DependencyInjection\ContainerBuilder; 18 | use Symfony\Component\DependencyInjection\Exception\InvalidArgumentException; 19 | use Symfony\Component\DependencyInjection\Reference; 20 | 21 | /** 22 | * @author Rob Frawley 2nd 23 | */ 24 | class CachePoolPrunerPass implements CompilerPassInterface 25 | { 26 | public function process(ContainerBuilder $container): void 27 | { 28 | if (!$container->hasDefinition('console.command.cache_pool_prune')) { 29 | return; 30 | } 31 | 32 | $services = []; 33 | 34 | foreach ($container->findTaggedServiceIds('cache.pool') as $id => $tags) { 35 | $class = $container->getParameterBag()->resolveValue($container->getDefinition($id)->getClass()); 36 | 37 | if (!$reflection = $container->getReflectionClass($class)) { 38 | throw new InvalidArgumentException(\sprintf('Class "%s" used for service "%s" cannot be found.', $class, $id)); 39 | } 40 | 41 | if ($reflection->implementsInterface(PruneableInterface::class)) { 42 | $services[$id] = new Reference($id); 43 | } 44 | } 45 | 46 | $container->getDefinition('console.command.cache_pool_prune')->replaceArgument(0, new IteratorArgument($services)); 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /Exception/BadMethodCallException.php: -------------------------------------------------------------------------------- 1 | 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\Component\Cache\Exception; 13 | 14 | use Psr\Cache\CacheException as Psr6CacheInterface; 15 | use Psr\SimpleCache\CacheException as SimpleCacheInterface; 16 | 17 | if (interface_exists(SimpleCacheInterface::class)) { 18 | class BadMethodCallException extends \BadMethodCallException implements Psr6CacheInterface, SimpleCacheInterface 19 | { 20 | } 21 | } else { 22 | class BadMethodCallException extends \BadMethodCallException implements Psr6CacheInterface 23 | { 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /Exception/CacheException.php: -------------------------------------------------------------------------------- 1 | 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\Component\Cache\Exception; 13 | 14 | use Psr\Cache\CacheException as Psr6CacheInterface; 15 | use Psr\SimpleCache\CacheException as SimpleCacheInterface; 16 | 17 | if (interface_exists(SimpleCacheInterface::class)) { 18 | class CacheException extends \Exception implements Psr6CacheInterface, SimpleCacheInterface 19 | { 20 | } 21 | } else { 22 | class CacheException extends \Exception implements Psr6CacheInterface 23 | { 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /Exception/InvalidArgumentException.php: -------------------------------------------------------------------------------- 1 | 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\Component\Cache\Exception; 13 | 14 | use Psr\Cache\InvalidArgumentException as Psr6CacheInterface; 15 | use Psr\SimpleCache\InvalidArgumentException as SimpleCacheInterface; 16 | 17 | if (interface_exists(SimpleCacheInterface::class)) { 18 | class InvalidArgumentException extends \InvalidArgumentException implements Psr6CacheInterface, SimpleCacheInterface 19 | { 20 | } 21 | } else { 22 | class InvalidArgumentException extends \InvalidArgumentException implements Psr6CacheInterface 23 | { 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /Exception/LogicException.php: -------------------------------------------------------------------------------- 1 | 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\Component\Cache\Exception; 13 | 14 | use Psr\Cache\CacheException as Psr6CacheInterface; 15 | use Psr\SimpleCache\CacheException as SimpleCacheInterface; 16 | 17 | if (interface_exists(SimpleCacheInterface::class)) { 18 | class LogicException extends \LogicException implements Psr6CacheInterface, SimpleCacheInterface 19 | { 20 | } 21 | } else { 22 | class LogicException extends \LogicException implements Psr6CacheInterface 23 | { 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (c) 2016-present Fabien Potencier 2 | 3 | Permission is hereby granted, free of charge, to any person obtaining a copy 4 | of this software and associated documentation files (the "Software"), to deal 5 | in the Software without restriction, including without limitation the rights 6 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 7 | copies of the Software, and to permit persons to whom the Software is furnished 8 | to do so, subject to the following conditions: 9 | 10 | The above copyright notice and this permission notice shall be included in all 11 | copies or substantial portions of the Software. 12 | 13 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 14 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 15 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 16 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 17 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 18 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 19 | THE SOFTWARE. 20 | -------------------------------------------------------------------------------- /LockRegistry.php: -------------------------------------------------------------------------------- 1 | 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\Component\Cache; 13 | 14 | use Psr\Log\LoggerInterface; 15 | use Symfony\Contracts\Cache\CacheInterface; 16 | use Symfony\Contracts\Cache\ItemInterface; 17 | 18 | /** 19 | * LockRegistry is used internally by existing adapters to protect against cache stampede. 20 | * 21 | * It does so by wrapping the computation of items in a pool of locks. 22 | * Foreach each apps, there can be at most 20 concurrent processes that 23 | * compute items at the same time and only one per cache-key. 24 | * 25 | * @author Nicolas Grekas 26 | */ 27 | final class LockRegistry 28 | { 29 | private static array $openedFiles = []; 30 | private static ?array $lockedFiles = null; 31 | private static \Exception $signalingException; 32 | private static \Closure $signalingCallback; 33 | 34 | /** 35 | * The number of items in this list controls the max number of concurrent processes. 36 | */ 37 | private static array $files = [ 38 | __DIR__.\DIRECTORY_SEPARATOR.'Adapter'.\DIRECTORY_SEPARATOR.'AbstractAdapter.php', 39 | __DIR__.\DIRECTORY_SEPARATOR.'Adapter'.\DIRECTORY_SEPARATOR.'AbstractTagAwareAdapter.php', 40 | __DIR__.\DIRECTORY_SEPARATOR.'Adapter'.\DIRECTORY_SEPARATOR.'AdapterInterface.php', 41 | __DIR__.\DIRECTORY_SEPARATOR.'Adapter'.\DIRECTORY_SEPARATOR.'ApcuAdapter.php', 42 | __DIR__.\DIRECTORY_SEPARATOR.'Adapter'.\DIRECTORY_SEPARATOR.'ArrayAdapter.php', 43 | __DIR__.\DIRECTORY_SEPARATOR.'Adapter'.\DIRECTORY_SEPARATOR.'ChainAdapter.php', 44 | __DIR__.\DIRECTORY_SEPARATOR.'Adapter'.\DIRECTORY_SEPARATOR.'CouchbaseBucketAdapter.php', 45 | __DIR__.\DIRECTORY_SEPARATOR.'Adapter'.\DIRECTORY_SEPARATOR.'CouchbaseCollectionAdapter.php', 46 | __DIR__.\DIRECTORY_SEPARATOR.'Adapter'.\DIRECTORY_SEPARATOR.'DoctrineDbalAdapter.php', 47 | __DIR__.\DIRECTORY_SEPARATOR.'Adapter'.\DIRECTORY_SEPARATOR.'FilesystemAdapter.php', 48 | __DIR__.\DIRECTORY_SEPARATOR.'Adapter'.\DIRECTORY_SEPARATOR.'FilesystemTagAwareAdapter.php', 49 | __DIR__.\DIRECTORY_SEPARATOR.'Adapter'.\DIRECTORY_SEPARATOR.'MemcachedAdapter.php', 50 | __DIR__.\DIRECTORY_SEPARATOR.'Adapter'.\DIRECTORY_SEPARATOR.'NullAdapter.php', 51 | __DIR__.\DIRECTORY_SEPARATOR.'Adapter'.\DIRECTORY_SEPARATOR.'ParameterNormalizer.php', 52 | __DIR__.\DIRECTORY_SEPARATOR.'Adapter'.\DIRECTORY_SEPARATOR.'PdoAdapter.php', 53 | __DIR__.\DIRECTORY_SEPARATOR.'Adapter'.\DIRECTORY_SEPARATOR.'PhpArrayAdapter.php', 54 | __DIR__.\DIRECTORY_SEPARATOR.'Adapter'.\DIRECTORY_SEPARATOR.'PhpFilesAdapter.php', 55 | __DIR__.\DIRECTORY_SEPARATOR.'Adapter'.\DIRECTORY_SEPARATOR.'ProxyAdapter.php', 56 | __DIR__.\DIRECTORY_SEPARATOR.'Adapter'.\DIRECTORY_SEPARATOR.'Psr16Adapter.php', 57 | __DIR__.\DIRECTORY_SEPARATOR.'Adapter'.\DIRECTORY_SEPARATOR.'RedisAdapter.php', 58 | __DIR__.\DIRECTORY_SEPARATOR.'Adapter'.\DIRECTORY_SEPARATOR.'RedisTagAwareAdapter.php', 59 | __DIR__.\DIRECTORY_SEPARATOR.'Adapter'.\DIRECTORY_SEPARATOR.'TagAwareAdapter.php', 60 | __DIR__.\DIRECTORY_SEPARATOR.'Adapter'.\DIRECTORY_SEPARATOR.'TagAwareAdapterInterface.php', 61 | __DIR__.\DIRECTORY_SEPARATOR.'Adapter'.\DIRECTORY_SEPARATOR.'TraceableAdapter.php', 62 | __DIR__.\DIRECTORY_SEPARATOR.'Adapter'.\DIRECTORY_SEPARATOR.'TraceableTagAwareAdapter.php', 63 | ]; 64 | 65 | /** 66 | * Defines a set of existing files that will be used as keys to acquire locks. 67 | * 68 | * @return array The previously defined set of files 69 | */ 70 | public static function setFiles(array $files): array 71 | { 72 | $previousFiles = self::$files; 73 | self::$files = $files; 74 | 75 | foreach (self::$openedFiles as $file) { 76 | if ($file) { 77 | flock($file, \LOCK_UN); 78 | fclose($file); 79 | } 80 | } 81 | self::$openedFiles = self::$lockedFiles = []; 82 | 83 | return $previousFiles; 84 | } 85 | 86 | public static function compute(callable $callback, ItemInterface $item, bool &$save, CacheInterface $pool, ?\Closure $setMetadata = null, ?LoggerInterface $logger = null): mixed 87 | { 88 | if ('\\' === \DIRECTORY_SEPARATOR && null === self::$lockedFiles) { 89 | // disable locking on Windows by default 90 | self::$files = self::$lockedFiles = []; 91 | } 92 | 93 | $key = self::$files ? abs(crc32($item->getKey())) % \count(self::$files) : -1; 94 | 95 | if ($key < 0 || self::$lockedFiles || !$lock = self::open($key)) { 96 | return $callback($item, $save); 97 | } 98 | 99 | self::$signalingException ??= unserialize("O:9:\"Exception\":1:{s:16:\"\0Exception\0trace\";a:0:{}}"); 100 | self::$signalingCallback ??= fn () => throw self::$signalingException; 101 | 102 | while (true) { 103 | try { 104 | // race to get the lock in non-blocking mode 105 | $locked = flock($lock, \LOCK_EX | \LOCK_NB, $wouldBlock); 106 | 107 | if ($locked || !$wouldBlock) { 108 | $logger?->info(\sprintf('Lock %s, now computing item "{key}"', $locked ? 'acquired' : 'not supported'), ['key' => $item->getKey()]); 109 | self::$lockedFiles[$key] = true; 110 | 111 | $value = $callback($item, $save); 112 | 113 | if ($save) { 114 | if ($setMetadata) { 115 | $setMetadata($item); 116 | } 117 | 118 | $pool->save($item->set($value)); 119 | $save = false; 120 | } 121 | 122 | return $value; 123 | } 124 | // if we failed the race, retry locking in blocking mode to wait for the winner 125 | $logger?->info('Item "{key}" is locked, waiting for it to be released', ['key' => $item->getKey()]); 126 | flock($lock, \LOCK_SH); 127 | } finally { 128 | flock($lock, \LOCK_UN); 129 | unset(self::$lockedFiles[$key]); 130 | } 131 | 132 | try { 133 | $value = $pool->get($item->getKey(), self::$signalingCallback, 0); 134 | $logger?->info('Item "{key}" retrieved after lock was released', ['key' => $item->getKey()]); 135 | $save = false; 136 | 137 | return $value; 138 | } catch (\Exception $e) { 139 | if (self::$signalingException !== $e) { 140 | throw $e; 141 | } 142 | $logger?->info('Item "{key}" not found while lock was released, now retrying', ['key' => $item->getKey()]); 143 | } 144 | } 145 | 146 | return null; 147 | } 148 | 149 | /** 150 | * @return resource|false 151 | */ 152 | private static function open(int $key) 153 | { 154 | if (null !== $h = self::$openedFiles[$key] ?? null) { 155 | return $h; 156 | } 157 | set_error_handler(static fn () => null); 158 | try { 159 | $h = fopen(self::$files[$key], 'r+'); 160 | } finally { 161 | restore_error_handler(); 162 | } 163 | 164 | return self::$openedFiles[$key] = $h ?: @fopen(self::$files[$key], 'r'); 165 | } 166 | } 167 | -------------------------------------------------------------------------------- /Marshaller/DefaultMarshaller.php: -------------------------------------------------------------------------------- 1 | 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\Component\Cache\Marshaller; 13 | 14 | use Symfony\Component\Cache\Exception\CacheException; 15 | 16 | /** 17 | * Serializes/unserializes values using igbinary_serialize() if available, serialize() otherwise. 18 | * 19 | * @author Nicolas Grekas 20 | */ 21 | class DefaultMarshaller implements MarshallerInterface 22 | { 23 | private bool $useIgbinarySerialize = false; 24 | private bool $throwOnSerializationFailure = false; 25 | 26 | public function __construct(?bool $useIgbinarySerialize = null, bool $throwOnSerializationFailure = false) 27 | { 28 | if ($useIgbinarySerialize && (!\extension_loaded('igbinary') || version_compare('3.1.6', phpversion('igbinary'), '>'))) { 29 | throw new CacheException(\extension_loaded('igbinary') ? 'Please upgrade the "igbinary" PHP extension to v3.1.6 or higher.' : 'The "igbinary" PHP extension is not loaded.'); 30 | } 31 | $this->useIgbinarySerialize = true === $useIgbinarySerialize; 32 | $this->throwOnSerializationFailure = $throwOnSerializationFailure; 33 | } 34 | 35 | public function marshall(array $values, ?array &$failed): array 36 | { 37 | $serialized = $failed = []; 38 | 39 | foreach ($values as $id => $value) { 40 | try { 41 | if ($this->useIgbinarySerialize) { 42 | $serialized[$id] = igbinary_serialize($value); 43 | } else { 44 | $serialized[$id] = serialize($value); 45 | } 46 | } catch (\Exception $e) { 47 | if ($this->throwOnSerializationFailure) { 48 | throw new \ValueError($e->getMessage(), 0, $e); 49 | } 50 | $failed[] = $id; 51 | } 52 | } 53 | 54 | return $serialized; 55 | } 56 | 57 | public function unmarshall(string $value): mixed 58 | { 59 | if ('b:0;' === $value) { 60 | return false; 61 | } 62 | if ('N;' === $value) { 63 | return null; 64 | } 65 | static $igbinaryNull; 66 | if ($value === $igbinaryNull ??= \extension_loaded('igbinary') ? igbinary_serialize(null) : false) { 67 | return null; 68 | } 69 | $unserializeCallbackHandler = ini_set('unserialize_callback_func', __CLASS__.'::handleUnserializeCallback'); 70 | try { 71 | if (':' === ($value[1] ?? ':')) { 72 | if (false !== $value = unserialize($value)) { 73 | return $value; 74 | } 75 | } elseif (false === $igbinaryNull) { 76 | throw new \RuntimeException('Failed to unserialize values, did you forget to install the "igbinary" extension?'); 77 | } elseif (null !== $value = igbinary_unserialize($value)) { 78 | return $value; 79 | } 80 | 81 | throw new \DomainException(error_get_last() ? error_get_last()['message'] : 'Failed to unserialize values.'); 82 | } catch (\Error $e) { 83 | throw new \ErrorException($e->getMessage(), $e->getCode(), \E_ERROR, $e->getFile(), $e->getLine()); 84 | } finally { 85 | ini_set('unserialize_callback_func', $unserializeCallbackHandler); 86 | } 87 | } 88 | 89 | /** 90 | * @internal 91 | */ 92 | public static function handleUnserializeCallback(string $class): never 93 | { 94 | throw new \DomainException('Class not found: '.$class); 95 | } 96 | } 97 | -------------------------------------------------------------------------------- /Marshaller/DeflateMarshaller.php: -------------------------------------------------------------------------------- 1 | 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\Component\Cache\Marshaller; 13 | 14 | use Symfony\Component\Cache\Exception\CacheException; 15 | 16 | /** 17 | * Compresses values using gzdeflate(). 18 | * 19 | * @author Nicolas Grekas 20 | */ 21 | class DeflateMarshaller implements MarshallerInterface 22 | { 23 | public function __construct( 24 | private MarshallerInterface $marshaller, 25 | ) { 26 | if (!\function_exists('gzdeflate')) { 27 | throw new CacheException('The "zlib" PHP extension is not loaded.'); 28 | } 29 | } 30 | 31 | public function marshall(array $values, ?array &$failed): array 32 | { 33 | return array_map('gzdeflate', $this->marshaller->marshall($values, $failed)); 34 | } 35 | 36 | public function unmarshall(string $value): mixed 37 | { 38 | if (false !== $inflatedValue = @gzinflate($value)) { 39 | $value = $inflatedValue; 40 | } 41 | 42 | return $this->marshaller->unmarshall($value); 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /Marshaller/MarshallerInterface.php: -------------------------------------------------------------------------------- 1 | 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\Component\Cache\Marshaller; 13 | 14 | /** 15 | * Serializes/unserializes PHP values. 16 | * 17 | * Implementations of this interface MUST deal with errors carefully. They MUST 18 | * also deal with forward and backward compatibility at the storage format level. 19 | * 20 | * @author Nicolas Grekas 21 | */ 22 | interface MarshallerInterface 23 | { 24 | /** 25 | * Serializes a list of values. 26 | * 27 | * When serialization fails for a specific value, no exception should be 28 | * thrown. Instead, its key should be listed in $failed. 29 | */ 30 | public function marshall(array $values, ?array &$failed): array; 31 | 32 | /** 33 | * Unserializes a single value and throws an exception if anything goes wrong. 34 | * 35 | * @throws \Exception Whenever unserialization fails 36 | */ 37 | public function unmarshall(string $value): mixed; 38 | } 39 | -------------------------------------------------------------------------------- /Marshaller/SodiumMarshaller.php: -------------------------------------------------------------------------------- 1 | 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\Component\Cache\Marshaller; 13 | 14 | use Symfony\Component\Cache\Exception\CacheException; 15 | use Symfony\Component\Cache\Exception\InvalidArgumentException; 16 | 17 | /** 18 | * Encrypt/decrypt values using Libsodium. 19 | * 20 | * @author Ahmed TAILOULOUTE 21 | */ 22 | class SodiumMarshaller implements MarshallerInterface 23 | { 24 | private MarshallerInterface $marshaller; 25 | 26 | /** 27 | * @param string[] $decryptionKeys The key at index "0" is required and is used to decrypt and encrypt values; 28 | * more rotating keys can be provided to decrypt values; 29 | * each key must be generated using sodium_crypto_box_keypair() 30 | */ 31 | public function __construct( 32 | private array $decryptionKeys, 33 | ?MarshallerInterface $marshaller = null, 34 | ) { 35 | if (!self::isSupported()) { 36 | throw new CacheException('The "sodium" PHP extension is not loaded.'); 37 | } 38 | 39 | if (!isset($decryptionKeys[0])) { 40 | throw new InvalidArgumentException('At least one decryption key must be provided at index "0".'); 41 | } 42 | 43 | $this->marshaller = $marshaller ?? new DefaultMarshaller(); 44 | } 45 | 46 | public static function isSupported(): bool 47 | { 48 | return \function_exists('sodium_crypto_box_seal'); 49 | } 50 | 51 | public function marshall(array $values, ?array &$failed): array 52 | { 53 | $encryptionKey = sodium_crypto_box_publickey($this->decryptionKeys[0]); 54 | 55 | $encryptedValues = []; 56 | foreach ($this->marshaller->marshall($values, $failed) as $k => $v) { 57 | $encryptedValues[$k] = sodium_crypto_box_seal($v, $encryptionKey); 58 | } 59 | 60 | return $encryptedValues; 61 | } 62 | 63 | public function unmarshall(string $value): mixed 64 | { 65 | foreach ($this->decryptionKeys as $k) { 66 | if (false !== $decryptedValue = @sodium_crypto_box_seal_open($value, $k)) { 67 | $value = $decryptedValue; 68 | break; 69 | } 70 | } 71 | 72 | return $this->marshaller->unmarshall($value); 73 | } 74 | } 75 | -------------------------------------------------------------------------------- /Marshaller/TagAwareMarshaller.php: -------------------------------------------------------------------------------- 1 | 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\Component\Cache\Marshaller; 13 | 14 | /** 15 | * A marshaller optimized for data structures generated by AbstractTagAwareAdapter. 16 | * 17 | * @author Nicolas Grekas 18 | */ 19 | class TagAwareMarshaller implements MarshallerInterface 20 | { 21 | private MarshallerInterface $marshaller; 22 | 23 | public function __construct(?MarshallerInterface $marshaller = null) 24 | { 25 | $this->marshaller = $marshaller ?? new DefaultMarshaller(); 26 | } 27 | 28 | public function marshall(array $values, ?array &$failed): array 29 | { 30 | $failed = $notSerialized = $serialized = []; 31 | 32 | foreach ($values as $id => $value) { 33 | if (\is_array($value) && \is_array($value['tags'] ?? null) && \array_key_exists('value', $value) && \count($value) === 2 + (\is_string($value['meta'] ?? null) && 8 === \strlen($value['meta']))) { 34 | // if the value is an array with keys "tags", "value" and "meta", use a compact serialization format 35 | // magic numbers in the form 9D-..-..-..-..-00-..-..-..-5F allow detecting this format quickly in unmarshall() 36 | 37 | $v = $this->marshaller->marshall($value, $f); 38 | 39 | if ($f) { 40 | $f = []; 41 | $failed[] = $id; 42 | } else { 43 | if ([] === $value['tags']) { 44 | $v['tags'] = ''; 45 | } 46 | 47 | $serialized[$id] = "\x9D".($value['meta'] ?? "\0\0\0\0\0\0\0\0").pack('N', \strlen($v['tags'])).$v['tags'].$v['value']; 48 | $serialized[$id][9] = "\x5F"; 49 | } 50 | } else { 51 | // other arbitrary values are serialized using the decorated marshaller below 52 | $notSerialized[$id] = $value; 53 | } 54 | } 55 | 56 | if ($notSerialized) { 57 | $serialized += $this->marshaller->marshall($notSerialized, $f); 58 | $failed = array_merge($failed, $f); 59 | } 60 | 61 | return $serialized; 62 | } 63 | 64 | public function unmarshall(string $value): mixed 65 | { 66 | // detect the compact format used in marshall() using magic numbers in the form 9D-..-..-..-..-00-..-..-..-5F 67 | if (13 >= \strlen($value) || "\x9D" !== $value[0] || "\0" !== $value[5] || "\x5F" !== $value[9]) { 68 | return $this->marshaller->unmarshall($value); 69 | } 70 | 71 | // data consists of value, tags and metadata which we need to unpack 72 | $meta = substr($value, 1, 12); 73 | $meta[8] = "\0"; 74 | $tagLen = unpack('Nlen', $meta, 8)['len']; 75 | $meta = substr($meta, 0, 8); 76 | 77 | return [ 78 | 'value' => $this->marshaller->unmarshall(substr($value, 13 + $tagLen)), 79 | 'tags' => $tagLen ? $this->marshaller->unmarshall(substr($value, 13, $tagLen)) : [], 80 | 'meta' => "\0\0\0\0\0\0\0\0" === $meta ? null : $meta, 81 | ]; 82 | } 83 | } 84 | -------------------------------------------------------------------------------- /Messenger/EarlyExpirationDispatcher.php: -------------------------------------------------------------------------------- 1 | 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\Component\Cache\Messenger; 13 | 14 | use Psr\Log\LoggerInterface; 15 | use Symfony\Component\Cache\Adapter\AdapterInterface; 16 | use Symfony\Component\Cache\CacheItem; 17 | use Symfony\Component\DependencyInjection\ReverseContainer; 18 | use Symfony\Component\Messenger\MessageBusInterface; 19 | use Symfony\Component\Messenger\Stamp\HandledStamp; 20 | 21 | /** 22 | * Sends the computation of cached values to a message bus. 23 | */ 24 | class EarlyExpirationDispatcher 25 | { 26 | private ?\Closure $callbackWrapper; 27 | 28 | public function __construct( 29 | private MessageBusInterface $bus, 30 | private ReverseContainer $reverseContainer, 31 | ?callable $callbackWrapper = null, 32 | ) { 33 | $this->callbackWrapper = null === $callbackWrapper ? null : $callbackWrapper(...); 34 | } 35 | 36 | public function __invoke(callable $callback, CacheItem $item, bool &$save, AdapterInterface $pool, \Closure $setMetadata, ?LoggerInterface $logger = null): mixed 37 | { 38 | if (!$item->isHit() || null === $message = EarlyExpirationMessage::create($this->reverseContainer, $callback, $item, $pool)) { 39 | // The item is stale or the callback cannot be reversed: we must compute the value now 40 | $logger?->info('Computing item "{key}" online: '.($item->isHit() ? 'callback cannot be reversed' : 'item is stale'), ['key' => $item->getKey()]); 41 | 42 | return null !== $this->callbackWrapper ? ($this->callbackWrapper)($callback, $item, $save, $pool, $setMetadata, $logger) : $callback($item, $save); 43 | } 44 | 45 | $envelope = $this->bus->dispatch($message); 46 | 47 | if ($logger) { 48 | if ($envelope->last(HandledStamp::class)) { 49 | $logger->info('Item "{key}" was computed online', ['key' => $item->getKey()]); 50 | } else { 51 | $logger->info('Item "{key}" sent for recomputation', ['key' => $item->getKey()]); 52 | } 53 | } 54 | 55 | // The item's value is not stale, no need to write it to the backend 56 | $save = false; 57 | 58 | return $message->getItem()->get() ?? $item->get(); 59 | } 60 | } 61 | -------------------------------------------------------------------------------- /Messenger/EarlyExpirationHandler.php: -------------------------------------------------------------------------------- 1 | 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\Component\Cache\Messenger; 13 | 14 | use Symfony\Component\Cache\CacheItem; 15 | use Symfony\Component\DependencyInjection\ReverseContainer; 16 | use Symfony\Component\Messenger\Attribute\AsMessageHandler; 17 | 18 | /** 19 | * Computes cached values sent to a message bus. 20 | */ 21 | #[AsMessageHandler] 22 | class EarlyExpirationHandler 23 | { 24 | private array $processedNonces = []; 25 | 26 | public function __construct( 27 | private ReverseContainer $reverseContainer, 28 | ) { 29 | } 30 | 31 | public function __invoke(EarlyExpirationMessage $message): void 32 | { 33 | $item = $message->getItem(); 34 | $metadata = $item->getMetadata(); 35 | $expiry = $metadata[CacheItem::METADATA_EXPIRY] ?? 0; 36 | $ctime = $metadata[CacheItem::METADATA_CTIME] ?? 0; 37 | 38 | if ($expiry && $ctime) { 39 | // skip duplicate or expired messages 40 | 41 | $processingNonce = [$expiry, $ctime]; 42 | $pool = $message->getPool(); 43 | $key = $item->getKey(); 44 | 45 | if (($this->processedNonces[$pool][$key] ?? null) === $processingNonce) { 46 | return; 47 | } 48 | 49 | if (microtime(true) >= $expiry) { 50 | return; 51 | } 52 | 53 | $this->processedNonces[$pool] = [$key => $processingNonce] + ($this->processedNonces[$pool] ?? []); 54 | 55 | if (\count($this->processedNonces[$pool]) > 100) { 56 | array_pop($this->processedNonces[$pool]); 57 | } 58 | } 59 | 60 | static $setMetadata; 61 | 62 | $setMetadata ??= \Closure::bind( 63 | function (CacheItem $item, float $startTime) { 64 | if ($item->expiry > $endTime = microtime(true)) { 65 | $item->newMetadata[CacheItem::METADATA_EXPIRY] = $item->expiry; 66 | $item->newMetadata[CacheItem::METADATA_CTIME] = (int) ceil(1000 * ($endTime - $startTime)); 67 | } 68 | }, 69 | null, 70 | CacheItem::class 71 | ); 72 | 73 | $startTime = microtime(true); 74 | $pool = $message->findPool($this->reverseContainer); 75 | $callback = $message->findCallback($this->reverseContainer); 76 | $save = true; 77 | $value = $callback($item, $save); 78 | $setMetadata($item, $startTime); 79 | $pool->save($item->set($value)); 80 | } 81 | } 82 | -------------------------------------------------------------------------------- /Messenger/EarlyExpirationMessage.php: -------------------------------------------------------------------------------- 1 | 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\Component\Cache\Messenger; 13 | 14 | use Symfony\Component\Cache\Adapter\AdapterInterface; 15 | use Symfony\Component\Cache\CacheItem; 16 | use Symfony\Component\DependencyInjection\ReverseContainer; 17 | 18 | /** 19 | * Conveys a cached value that needs to be computed. 20 | */ 21 | final class EarlyExpirationMessage 22 | { 23 | public static function create(ReverseContainer $reverseContainer, callable $callback, CacheItem $item, AdapterInterface $pool): ?self 24 | { 25 | try { 26 | $item = clone $item; 27 | $item->set(null); 28 | } catch (\Exception) { 29 | return null; 30 | } 31 | 32 | $pool = $reverseContainer->getId($pool); 33 | 34 | if (\is_object($callback)) { 35 | if (null === $id = $reverseContainer->getId($callback)) { 36 | return null; 37 | } 38 | 39 | $callback = '@'.$id; 40 | } elseif (!\is_array($callback)) { 41 | $callback = (string) $callback; 42 | } elseif (!\is_object($callback[0])) { 43 | $callback = [(string) $callback[0], (string) $callback[1]]; 44 | } else { 45 | if (null === $id = $reverseContainer->getId($callback[0])) { 46 | return null; 47 | } 48 | 49 | $callback = ['@'.$id, (string) $callback[1]]; 50 | } 51 | 52 | return new self($item, $pool, $callback); 53 | } 54 | 55 | public function getItem(): CacheItem 56 | { 57 | return $this->item; 58 | } 59 | 60 | public function getPool(): string 61 | { 62 | return $this->pool; 63 | } 64 | 65 | /** 66 | * @return string|string[] 67 | */ 68 | public function getCallback(): string|array 69 | { 70 | return $this->callback; 71 | } 72 | 73 | public function findPool(ReverseContainer $reverseContainer): AdapterInterface 74 | { 75 | return $reverseContainer->getService($this->pool); 76 | } 77 | 78 | public function findCallback(ReverseContainer $reverseContainer): callable 79 | { 80 | if (\is_string($callback = $this->callback)) { 81 | return '@' === $callback[0] ? $reverseContainer->getService(substr($callback, 1)) : $callback; 82 | } 83 | if ('@' === $callback[0][0]) { 84 | $callback[0] = $reverseContainer->getService(substr($callback[0], 1)); 85 | } 86 | 87 | return $callback; 88 | } 89 | 90 | private function __construct( 91 | private CacheItem $item, 92 | private string $pool, 93 | private string|array $callback, 94 | ) { 95 | } 96 | } 97 | -------------------------------------------------------------------------------- /PruneableInterface.php: -------------------------------------------------------------------------------- 1 | 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\Component\Cache; 13 | 14 | /** 15 | * Interface extends psr-6 and psr-16 caches to allow for pruning (deletion) of all expired cache items. 16 | */ 17 | interface PruneableInterface 18 | { 19 | public function prune(): bool; 20 | } 21 | -------------------------------------------------------------------------------- /Psr16Cache.php: -------------------------------------------------------------------------------- 1 | 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\Component\Cache; 13 | 14 | use Psr\Cache\CacheException as Psr6CacheException; 15 | use Psr\Cache\CacheItemPoolInterface; 16 | use Psr\SimpleCache\CacheException as SimpleCacheException; 17 | use Psr\SimpleCache\CacheInterface; 18 | use Symfony\Component\Cache\Adapter\AdapterInterface; 19 | use Symfony\Component\Cache\Exception\InvalidArgumentException; 20 | use Symfony\Component\Cache\Traits\ProxyTrait; 21 | 22 | /** 23 | * Turns a PSR-6 cache into a PSR-16 one. 24 | * 25 | * @author Nicolas Grekas 26 | */ 27 | class Psr16Cache implements CacheInterface, PruneableInterface, ResettableInterface 28 | { 29 | use ProxyTrait; 30 | 31 | private ?\Closure $createCacheItem = null; 32 | private ?CacheItem $cacheItemPrototype = null; 33 | private static \Closure $packCacheItem; 34 | 35 | public function __construct(CacheItemPoolInterface $pool) 36 | { 37 | $this->pool = $pool; 38 | 39 | if (!$pool instanceof AdapterInterface) { 40 | return; 41 | } 42 | $cacheItemPrototype = &$this->cacheItemPrototype; 43 | $createCacheItem = \Closure::bind( 44 | static function ($key, $value, $allowInt = false) use (&$cacheItemPrototype) { 45 | $item = clone $cacheItemPrototype; 46 | $item->poolHash = $item->innerItem = null; 47 | if ($allowInt && \is_int($key)) { 48 | $item->key = (string) $key; 49 | } else { 50 | \assert('' !== CacheItem::validateKey($key)); 51 | $item->key = $key; 52 | } 53 | $item->value = $value; 54 | $item->isHit = false; 55 | 56 | return $item; 57 | }, 58 | null, 59 | CacheItem::class 60 | ); 61 | $this->createCacheItem = function ($key, $value, $allowInt = false) use ($createCacheItem) { 62 | if (null === $this->cacheItemPrototype) { 63 | $this->get($allowInt && \is_int($key) ? (string) $key : $key); 64 | } 65 | $this->createCacheItem = $createCacheItem; 66 | 67 | return $createCacheItem($key, null, $allowInt)->set($value); 68 | }; 69 | self::$packCacheItem ??= \Closure::bind( 70 | static function (CacheItem $item) { 71 | $item->newMetadata = $item->metadata; 72 | 73 | return $item->pack(); 74 | }, 75 | null, 76 | CacheItem::class 77 | ); 78 | } 79 | 80 | public function get($key, $default = null): mixed 81 | { 82 | try { 83 | $item = $this->pool->getItem($key); 84 | } catch (SimpleCacheException $e) { 85 | throw $e; 86 | } catch (Psr6CacheException $e) { 87 | throw new InvalidArgumentException($e->getMessage(), $e->getCode(), $e); 88 | } 89 | if (null === $this->cacheItemPrototype) { 90 | $this->cacheItemPrototype = clone $item; 91 | $this->cacheItemPrototype->set(null); 92 | } 93 | 94 | return $item->isHit() ? $item->get() : $default; 95 | } 96 | 97 | public function set($key, $value, $ttl = null): bool 98 | { 99 | try { 100 | if (null !== $f = $this->createCacheItem) { 101 | $item = $f($key, $value); 102 | } else { 103 | $item = $this->pool->getItem($key)->set($value); 104 | } 105 | } catch (SimpleCacheException $e) { 106 | throw $e; 107 | } catch (Psr6CacheException $e) { 108 | throw new InvalidArgumentException($e->getMessage(), $e->getCode(), $e); 109 | } 110 | if (null !== $ttl) { 111 | $item->expiresAfter($ttl); 112 | } 113 | 114 | return $this->pool->save($item); 115 | } 116 | 117 | public function delete($key): bool 118 | { 119 | try { 120 | return $this->pool->deleteItem($key); 121 | } catch (SimpleCacheException $e) { 122 | throw $e; 123 | } catch (Psr6CacheException $e) { 124 | throw new InvalidArgumentException($e->getMessage(), $e->getCode(), $e); 125 | } 126 | } 127 | 128 | public function clear(): bool 129 | { 130 | return $this->pool->clear(); 131 | } 132 | 133 | public function getMultiple($keys, $default = null): iterable 134 | { 135 | if ($keys instanceof \Traversable) { 136 | $keys = iterator_to_array($keys, false); 137 | } elseif (!\is_array($keys)) { 138 | throw new InvalidArgumentException(\sprintf('Cache keys must be array or Traversable, "%s" given.', get_debug_type($keys))); 139 | } 140 | 141 | try { 142 | $items = $this->pool->getItems($keys); 143 | } catch (SimpleCacheException $e) { 144 | throw $e; 145 | } catch (Psr6CacheException $e) { 146 | throw new InvalidArgumentException($e->getMessage(), $e->getCode(), $e); 147 | } 148 | $values = []; 149 | 150 | if (!$this->pool instanceof AdapterInterface) { 151 | foreach ($items as $key => $item) { 152 | $values[$key] = $item->isHit() ? $item->get() : $default; 153 | } 154 | 155 | return $values; 156 | } 157 | 158 | foreach ($items as $key => $item) { 159 | $values[$key] = $item->isHit() ? (self::$packCacheItem)($item) : $default; 160 | } 161 | 162 | return $values; 163 | } 164 | 165 | public function setMultiple($values, $ttl = null): bool 166 | { 167 | $valuesIsArray = \is_array($values); 168 | if (!$valuesIsArray && !$values instanceof \Traversable) { 169 | throw new InvalidArgumentException(\sprintf('Cache values must be array or Traversable, "%s" given.', get_debug_type($values))); 170 | } 171 | $items = []; 172 | 173 | try { 174 | if (null !== $f = $this->createCacheItem) { 175 | $valuesIsArray = false; 176 | foreach ($values as $key => $value) { 177 | $items[$key] = $f($key, $value, true); 178 | } 179 | } elseif ($valuesIsArray) { 180 | $items = []; 181 | foreach ($values as $key => $value) { 182 | $items[] = (string) $key; 183 | } 184 | $items = $this->pool->getItems($items); 185 | } else { 186 | foreach ($values as $key => $value) { 187 | if (\is_int($key)) { 188 | $key = (string) $key; 189 | } 190 | $items[$key] = $this->pool->getItem($key)->set($value); 191 | } 192 | } 193 | } catch (SimpleCacheException $e) { 194 | throw $e; 195 | } catch (Psr6CacheException $e) { 196 | throw new InvalidArgumentException($e->getMessage(), $e->getCode(), $e); 197 | } 198 | $ok = true; 199 | 200 | foreach ($items as $key => $item) { 201 | if ($valuesIsArray) { 202 | $item->set($values[$key]); 203 | } 204 | if (null !== $ttl) { 205 | $item->expiresAfter($ttl); 206 | } 207 | $ok = $this->pool->saveDeferred($item) && $ok; 208 | } 209 | 210 | return $this->pool->commit() && $ok; 211 | } 212 | 213 | public function deleteMultiple($keys): bool 214 | { 215 | if ($keys instanceof \Traversable) { 216 | $keys = iterator_to_array($keys, false); 217 | } elseif (!\is_array($keys)) { 218 | throw new InvalidArgumentException(\sprintf('Cache keys must be array or Traversable, "%s" given.', get_debug_type($keys))); 219 | } 220 | 221 | try { 222 | return $this->pool->deleteItems($keys); 223 | } catch (SimpleCacheException $e) { 224 | throw $e; 225 | } catch (Psr6CacheException $e) { 226 | throw new InvalidArgumentException($e->getMessage(), $e->getCode(), $e); 227 | } 228 | } 229 | 230 | public function has($key): bool 231 | { 232 | try { 233 | return $this->pool->hasItem($key); 234 | } catch (SimpleCacheException $e) { 235 | throw $e; 236 | } catch (Psr6CacheException $e) { 237 | throw new InvalidArgumentException($e->getMessage(), $e->getCode(), $e); 238 | } 239 | } 240 | } 241 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | Symfony PSR-6 implementation for caching 2 | ======================================== 3 | 4 | The Cache component provides extended 5 | [PSR-6](https://www.php-fig.org/psr/psr-6/) implementations for adding cache to 6 | your applications. It is designed to have a low overhead so that caching is 7 | fastest. It ships with adapters for the most widespread caching backends. 8 | It also provides a [PSR-16](https://www.php-fig.org/psr/psr-16/) adapter, 9 | and implementations for [symfony/cache-contracts](https://github.com/symfony/cache-contracts)' 10 | `CacheInterface` and `TagAwareCacheInterface`. 11 | 12 | Resources 13 | --------- 14 | 15 | * [Documentation](https://symfony.com/doc/current/components/cache.html) 16 | * [Contributing](https://symfony.com/doc/current/contributing/index.html) 17 | * [Report issues](https://github.com/symfony/symfony/issues) and 18 | [send Pull Requests](https://github.com/symfony/symfony/pulls) 19 | in the [main Symfony repository](https://github.com/symfony/symfony) 20 | -------------------------------------------------------------------------------- /ResettableInterface.php: -------------------------------------------------------------------------------- 1 | 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\Component\Cache; 13 | 14 | use Symfony\Contracts\Service\ResetInterface; 15 | 16 | /** 17 | * Resets a pool's local state. 18 | */ 19 | interface ResettableInterface extends ResetInterface 20 | { 21 | } 22 | -------------------------------------------------------------------------------- /Traits/ContractsTrait.php: -------------------------------------------------------------------------------- 1 | 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\Component\Cache\Traits; 13 | 14 | use Psr\Log\LoggerInterface; 15 | use Symfony\Component\Cache\Adapter\AdapterInterface; 16 | use Symfony\Component\Cache\CacheItem; 17 | use Symfony\Component\Cache\Exception\InvalidArgumentException; 18 | use Symfony\Component\Cache\LockRegistry; 19 | use Symfony\Contracts\Cache\CacheInterface; 20 | use Symfony\Contracts\Cache\CacheTrait; 21 | use Symfony\Contracts\Cache\ItemInterface; 22 | 23 | /** 24 | * @author Nicolas Grekas 25 | * 26 | * @internal 27 | */ 28 | trait ContractsTrait 29 | { 30 | use CacheTrait { 31 | doGet as private contractsGet; 32 | } 33 | 34 | private \Closure $callbackWrapper; 35 | private array $computing = []; 36 | 37 | /** 38 | * Wraps the callback passed to ->get() in a callable. 39 | * 40 | * @return callable the previous callback wrapper 41 | */ 42 | public function setCallbackWrapper(?callable $callbackWrapper): callable 43 | { 44 | if (!isset($this->callbackWrapper)) { 45 | $this->callbackWrapper = LockRegistry::compute(...); 46 | 47 | if (\in_array(\PHP_SAPI, ['cli', 'phpdbg', 'embed'], true)) { 48 | $this->setCallbackWrapper(null); 49 | } 50 | } 51 | 52 | if (null !== $callbackWrapper && !$callbackWrapper instanceof \Closure) { 53 | $callbackWrapper = $callbackWrapper(...); 54 | } 55 | 56 | $previousWrapper = $this->callbackWrapper; 57 | $this->callbackWrapper = $callbackWrapper ?? static fn (callable $callback, ItemInterface $item, bool &$save, CacheInterface $pool, \Closure $setMetadata, ?LoggerInterface $logger) => $callback($item, $save); 58 | 59 | return $previousWrapper; 60 | } 61 | 62 | private function doGet(AdapterInterface $pool, string $key, callable $callback, ?float $beta, ?array &$metadata = null): mixed 63 | { 64 | if (0 > $beta ??= 1.0) { 65 | throw new InvalidArgumentException(\sprintf('Argument "$beta" provided to "%s::get()" must be a positive number, %f given.', static::class, $beta)); 66 | } 67 | 68 | static $setMetadata; 69 | 70 | $setMetadata ??= \Closure::bind( 71 | static function (CacheItem $item, float $startTime, ?array &$metadata) { 72 | if ($item->expiry > $endTime = microtime(true)) { 73 | $item->newMetadata[CacheItem::METADATA_EXPIRY] = $metadata[CacheItem::METADATA_EXPIRY] = $item->expiry; 74 | $item->newMetadata[CacheItem::METADATA_CTIME] = $metadata[CacheItem::METADATA_CTIME] = (int) ceil(1000 * ($endTime - $startTime)); 75 | } else { 76 | unset($metadata[CacheItem::METADATA_EXPIRY], $metadata[CacheItem::METADATA_CTIME], $metadata[CacheItem::METADATA_TAGS]); 77 | } 78 | }, 79 | null, 80 | CacheItem::class 81 | ); 82 | 83 | $this->callbackWrapper ??= LockRegistry::compute(...); 84 | 85 | return $this->contractsGet($pool, $key, function (CacheItem $item, bool &$save) use ($pool, $callback, $setMetadata, &$metadata, $key) { 86 | // don't wrap nor save recursive calls 87 | if (isset($this->computing[$key])) { 88 | $value = $callback($item, $save); 89 | $save = false; 90 | 91 | return $value; 92 | } 93 | 94 | $this->computing[$key] = $key; 95 | $startTime = microtime(true); 96 | 97 | if (!isset($this->callbackWrapper)) { 98 | $this->setCallbackWrapper($this->setCallbackWrapper(null)); 99 | } 100 | 101 | try { 102 | $value = ($this->callbackWrapper)($callback, $item, $save, $pool, function (CacheItem $item) use ($setMetadata, $startTime, &$metadata) { 103 | $setMetadata($item, $startTime, $metadata); 104 | }, $this->logger ?? null); 105 | $setMetadata($item, $startTime, $metadata); 106 | 107 | return $value; 108 | } finally { 109 | unset($this->computing[$key]); 110 | } 111 | }, $beta, $metadata, $this->logger ?? null); 112 | } 113 | } 114 | -------------------------------------------------------------------------------- /Traits/FilesystemCommonTrait.php: -------------------------------------------------------------------------------- 1 | 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\Component\Cache\Traits; 13 | 14 | use Symfony\Component\Cache\Exception\InvalidArgumentException; 15 | 16 | /** 17 | * @author Nicolas Grekas 18 | * 19 | * @internal 20 | */ 21 | trait FilesystemCommonTrait 22 | { 23 | private string $directory; 24 | private string $tmpSuffix; 25 | 26 | private function init(string $namespace, ?string $directory): void 27 | { 28 | if (!isset($directory[0])) { 29 | $directory = sys_get_temp_dir().\DIRECTORY_SEPARATOR.'symfony-cache'; 30 | } else { 31 | $directory = realpath($directory) ?: $directory; 32 | } 33 | if (isset($namespace[0])) { 34 | if (preg_match('#[^-+_.A-Za-z0-9]#', $namespace, $match)) { 35 | throw new InvalidArgumentException(\sprintf('Namespace contains "%s" but only characters in [-+_.A-Za-z0-9] are allowed.', $match[0])); 36 | } 37 | $directory .= \DIRECTORY_SEPARATOR.$namespace; 38 | } else { 39 | $directory .= \DIRECTORY_SEPARATOR.'@'; 40 | } 41 | if (!is_dir($directory)) { 42 | @mkdir($directory, 0777, true); 43 | } 44 | $directory .= \DIRECTORY_SEPARATOR; 45 | // On Windows the whole path is limited to 258 chars 46 | if ('\\' === \DIRECTORY_SEPARATOR && \strlen($directory) > 234) { 47 | throw new InvalidArgumentException(\sprintf('Cache directory too long (%s).', $directory)); 48 | } 49 | 50 | $this->directory = $directory; 51 | } 52 | 53 | protected function doClear(string $namespace): bool 54 | { 55 | $ok = true; 56 | 57 | foreach ($this->scanHashDir($this->directory) as $file) { 58 | if ('' !== $namespace && !str_starts_with($this->getFileKey($file), $namespace)) { 59 | continue; 60 | } 61 | 62 | $ok = ($this->doUnlink($file) || !file_exists($file)) && $ok; 63 | } 64 | 65 | return $ok; 66 | } 67 | 68 | protected function doDelete(array $ids): bool 69 | { 70 | $ok = true; 71 | 72 | foreach ($ids as $id) { 73 | $file = $this->getFile($id); 74 | $ok = (!is_file($file) || $this->doUnlink($file) || !file_exists($file)) && $ok; 75 | } 76 | 77 | return $ok; 78 | } 79 | 80 | protected function doUnlink(string $file): bool 81 | { 82 | return @unlink($file); 83 | } 84 | 85 | private function write(string $file, string $data, ?int $expiresAt = null): bool 86 | { 87 | $unlink = false; 88 | set_error_handler(static fn ($type, $message, $file, $line) => throw new \ErrorException($message, 0, $type, $file, $line)); 89 | try { 90 | $tmp = $this->directory.$this->tmpSuffix ??= str_replace('/', '-', base64_encode(random_bytes(6))); 91 | try { 92 | $h = fopen($tmp, 'x'); 93 | } catch (\ErrorException $e) { 94 | if (!str_contains($e->getMessage(), 'File exists')) { 95 | throw $e; 96 | } 97 | 98 | $tmp = $this->directory.$this->tmpSuffix = str_replace('/', '-', base64_encode(random_bytes(6))); 99 | $h = fopen($tmp, 'x'); 100 | } 101 | fwrite($h, $data); 102 | fclose($h); 103 | $unlink = true; 104 | 105 | if (null !== $expiresAt) { 106 | touch($tmp, $expiresAt ?: time() + 31556952); // 1 year in seconds 107 | } 108 | 109 | if ('\\' === \DIRECTORY_SEPARATOR) { 110 | $success = copy($tmp, $file); 111 | } else { 112 | $success = rename($tmp, $file); 113 | $unlink = !$success; 114 | } 115 | 116 | return $success; 117 | } finally { 118 | restore_error_handler(); 119 | 120 | if ($unlink) { 121 | @unlink($tmp); 122 | } 123 | } 124 | } 125 | 126 | private function getFile(string $id, bool $mkdir = false, ?string $directory = null): string 127 | { 128 | // Use xxh128 to favor speed over security, which is not an issue here 129 | $hash = str_replace('/', '-', base64_encode(hash('xxh128', static::class.$id, true))); 130 | $dir = ($directory ?? $this->directory).strtoupper($hash[0].\DIRECTORY_SEPARATOR.$hash[1].\DIRECTORY_SEPARATOR); 131 | 132 | if ($mkdir && !is_dir($dir)) { 133 | @mkdir($dir, 0777, true); 134 | } 135 | 136 | return $dir.substr($hash, 2, 20); 137 | } 138 | 139 | private function getFileKey(string $file): string 140 | { 141 | return ''; 142 | } 143 | 144 | private function scanHashDir(string $directory): \Generator 145 | { 146 | if (!is_dir($directory)) { 147 | return; 148 | } 149 | 150 | $chars = '+-ABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789'; 151 | 152 | for ($i = 0; $i < 38; ++$i) { 153 | if (!is_dir($directory.$chars[$i])) { 154 | continue; 155 | } 156 | 157 | for ($j = 0; $j < 38; ++$j) { 158 | if (!is_dir($dir = $directory.$chars[$i].\DIRECTORY_SEPARATOR.$chars[$j])) { 159 | continue; 160 | } 161 | 162 | foreach (@scandir($dir, \SCANDIR_SORT_NONE) ?: [] as $file) { 163 | if ('.' !== $file && '..' !== $file) { 164 | yield $dir.\DIRECTORY_SEPARATOR.$file; 165 | } 166 | } 167 | } 168 | } 169 | } 170 | 171 | public function __sleep(): array 172 | { 173 | throw new \BadMethodCallException('Cannot serialize '.__CLASS__); 174 | } 175 | 176 | public function __wakeup(): void 177 | { 178 | throw new \BadMethodCallException('Cannot unserialize '.__CLASS__); 179 | } 180 | 181 | public function __destruct() 182 | { 183 | if (method_exists(parent::class, '__destruct')) { 184 | parent::__destruct(); 185 | } 186 | if (isset($this->tmpSuffix) && is_file($this->directory.$this->tmpSuffix)) { 187 | unlink($this->directory.$this->tmpSuffix); 188 | } 189 | } 190 | } 191 | -------------------------------------------------------------------------------- /Traits/FilesystemTrait.php: -------------------------------------------------------------------------------- 1 | 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\Component\Cache\Traits; 13 | 14 | use Symfony\Component\Cache\Exception\CacheException; 15 | use Symfony\Component\Cache\Marshaller\MarshallerInterface; 16 | 17 | /** 18 | * @author Nicolas Grekas 19 | * @author Rob Frawley 2nd 20 | * 21 | * @internal 22 | */ 23 | trait FilesystemTrait 24 | { 25 | use FilesystemCommonTrait; 26 | 27 | private MarshallerInterface $marshaller; 28 | 29 | public function prune(): bool 30 | { 31 | $time = time(); 32 | $pruned = true; 33 | 34 | foreach ($this->scanHashDir($this->directory) as $file) { 35 | if (!$h = @fopen($file, 'r')) { 36 | continue; 37 | } 38 | 39 | if (($expiresAt = (int) fgets($h)) && $time >= $expiresAt) { 40 | fclose($h); 41 | $pruned = (@unlink($file) || !file_exists($file)) && $pruned; 42 | } else { 43 | fclose($h); 44 | } 45 | } 46 | 47 | return $pruned; 48 | } 49 | 50 | protected function doFetch(array $ids): iterable 51 | { 52 | $values = []; 53 | $now = time(); 54 | 55 | foreach ($ids as $id) { 56 | $file = $this->getFile($id); 57 | if (!is_file($file) || !$h = @fopen($file, 'r')) { 58 | continue; 59 | } 60 | if (($expiresAt = (int) fgets($h)) && $now >= $expiresAt) { 61 | fclose($h); 62 | @unlink($file); 63 | } else { 64 | $i = rawurldecode(rtrim(fgets($h))); 65 | $value = stream_get_contents($h); 66 | fclose($h); 67 | if ($i === $id) { 68 | $values[$id] = $this->marshaller->unmarshall($value); 69 | } 70 | } 71 | } 72 | 73 | return $values; 74 | } 75 | 76 | protected function doHave(string $id): bool 77 | { 78 | $file = $this->getFile($id); 79 | 80 | return is_file($file) && (@filemtime($file) > time() || $this->doFetch([$id])); 81 | } 82 | 83 | protected function doSave(array $values, int $lifetime): array|bool 84 | { 85 | $expiresAt = $lifetime ? (time() + $lifetime) : 0; 86 | $values = $this->marshaller->marshall($values, $failed); 87 | 88 | foreach ($values as $id => $value) { 89 | if (!$this->write($this->getFile($id, true), $expiresAt."\n".rawurlencode($id)."\n".$value, $expiresAt)) { 90 | $failed[] = $id; 91 | } 92 | } 93 | 94 | if ($failed && !is_writable($this->directory)) { 95 | throw new CacheException(\sprintf('Cache directory is not writable (%s).', $this->directory)); 96 | } 97 | 98 | return $failed; 99 | } 100 | 101 | private function getFileKey(string $file): string 102 | { 103 | if (!$h = @fopen($file, 'r')) { 104 | return ''; 105 | } 106 | 107 | fgets($h); // expiry 108 | $encodedKey = fgets($h); 109 | fclose($h); 110 | 111 | return rawurldecode(rtrim($encodedKey)); 112 | } 113 | } 114 | -------------------------------------------------------------------------------- /Traits/ProxyTrait.php: -------------------------------------------------------------------------------- 1 | 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\Component\Cache\Traits; 13 | 14 | use Symfony\Component\Cache\PruneableInterface; 15 | use Symfony\Contracts\Service\ResetInterface; 16 | 17 | /** 18 | * @author Nicolas Grekas 19 | * 20 | * @internal 21 | */ 22 | trait ProxyTrait 23 | { 24 | private object $pool; 25 | 26 | public function prune(): bool 27 | { 28 | return $this->pool instanceof PruneableInterface && $this->pool->prune(); 29 | } 30 | 31 | public function reset(): void 32 | { 33 | if ($this->pool instanceof ResetInterface) { 34 | $this->pool->reset(); 35 | } 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /Traits/Redis6ProxyTrait.php: -------------------------------------------------------------------------------- 1 | 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\Component\Cache\Traits; 13 | 14 | if (version_compare(phpversion('redis'), '6.1.0-dev', '>=')) { 15 | /** 16 | * @internal 17 | */ 18 | trait Redis6ProxyTrait 19 | { 20 | public function dump($key): \Redis|string|false 21 | { 22 | return $this->initializeLazyObject()->dump(...\func_get_args()); 23 | } 24 | 25 | public function hRandField($key, $options = null): \Redis|array|string|false 26 | { 27 | return $this->initializeLazyObject()->hRandField(...\func_get_args()); 28 | } 29 | 30 | public function hSet($key, ...$fields_and_vals): \Redis|false|int 31 | { 32 | return $this->initializeLazyObject()->hSet(...\func_get_args()); 33 | } 34 | 35 | public function mget($keys): \Redis|array|false 36 | { 37 | return $this->initializeLazyObject()->mget(...\func_get_args()); 38 | } 39 | 40 | public function sRandMember($key, $count = 0): mixed 41 | { 42 | return $this->initializeLazyObject()->sRandMember(...\func_get_args()); 43 | } 44 | 45 | public function waitaof($numlocal, $numreplicas, $timeout): \Redis|array|false 46 | { 47 | return $this->initializeLazyObject()->waitaof(...\func_get_args()); 48 | } 49 | } 50 | } else { 51 | /** 52 | * @internal 53 | */ 54 | trait Redis6ProxyTrait 55 | { 56 | public function dump($key): \Redis|string 57 | { 58 | return $this->initializeLazyObject()->dump(...\func_get_args()); 59 | } 60 | 61 | public function hRandField($key, $options = null): \Redis|array|string 62 | { 63 | return $this->initializeLazyObject()->hRandField(...\func_get_args()); 64 | } 65 | 66 | public function hSet($key, $member, $value): \Redis|false|int 67 | { 68 | return $this->initializeLazyObject()->hSet(...\func_get_args()); 69 | } 70 | 71 | public function mget($keys): \Redis|array 72 | { 73 | return $this->initializeLazyObject()->mget(...\func_get_args()); 74 | } 75 | 76 | public function sRandMember($key, $count = 0): \Redis|array|false|string 77 | { 78 | return $this->initializeLazyObject()->sRandMember(...\func_get_args()); 79 | } 80 | } 81 | } 82 | -------------------------------------------------------------------------------- /Traits/RedisCluster6ProxyTrait.php: -------------------------------------------------------------------------------- 1 | 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\Component\Cache\Traits; 13 | 14 | if (version_compare(phpversion('redis'), '6.1.0-dev', '>')) { 15 | /** 16 | * @internal 17 | */ 18 | trait RedisCluster6ProxyTrait 19 | { 20 | public function getex($key, $options = []): \RedisCluster|string|false 21 | { 22 | return $this->initializeLazyObject()->getex(...\func_get_args()); 23 | } 24 | 25 | public function publish($channel, $message): \RedisCluster|bool|int 26 | { 27 | return $this->initializeLazyObject()->publish(...\func_get_args()); 28 | } 29 | 30 | public function waitaof($key_or_address, $numlocal, $numreplicas, $timeout): \RedisCluster|array|false 31 | { 32 | return $this->initializeLazyObject()->waitaof(...\func_get_args()); 33 | } 34 | } 35 | } else { 36 | /** 37 | * @internal 38 | */ 39 | trait RedisCluster6ProxyTrait 40 | { 41 | public function publish($channel, $message): \RedisCluster|bool 42 | { 43 | return $this->initializeLazyObject()->publish(...\func_get_args()); 44 | } 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /Traits/RedisClusterNodeProxy.php: -------------------------------------------------------------------------------- 1 | 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\Component\Cache\Traits; 13 | 14 | /** 15 | * This file acts as a wrapper to the \RedisCluster implementation so it can accept the same type of calls as 16 | * individual \Redis objects. 17 | * 18 | * Calls are made to individual nodes via: RedisCluster->{method}($host, ...args)' 19 | * according to https://github.com/phpredis/phpredis/blob/develop/cluster.markdown#directed-node-commands 20 | * 21 | * @author Jack Thomas 22 | * 23 | * @internal 24 | */ 25 | class RedisClusterNodeProxy 26 | { 27 | public function __construct( 28 | private array $host, 29 | private \RedisCluster $redis, 30 | ) { 31 | } 32 | 33 | public function __call(string $method, array $args) 34 | { 35 | return $this->redis->{$method}($this->host, ...$args); 36 | } 37 | 38 | public function scan(&$iIterator, $strPattern = null, $iCount = null) 39 | { 40 | return $this->redis->scan($iIterator, $this->host, $strPattern, $iCount); 41 | } 42 | 43 | public function getOption($name) 44 | { 45 | return $this->redis->getOption($name); 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /Traits/RedisClusterProxy.php: -------------------------------------------------------------------------------- 1 | 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\Component\Cache\Traits; 13 | 14 | class_alias(6.0 <= (float) phpversion('redis') ? RedisCluster6Proxy::class : RedisCluster5Proxy::class, RedisClusterProxy::class); 15 | 16 | if (false) { 17 | /** 18 | * @internal 19 | */ 20 | class RedisClusterProxy extends \RedisCluster 21 | { 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /Traits/RedisProxy.php: -------------------------------------------------------------------------------- 1 | 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\Component\Cache\Traits; 13 | 14 | class_alias(6.0 <= (float) phpversion('redis') ? Redis6Proxy::class : Redis5Proxy::class, RedisProxy::class); 15 | 16 | if (false) { 17 | /** 18 | * @internal 19 | */ 20 | class RedisProxy extends \Redis 21 | { 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /Traits/RedisProxyTrait.php: -------------------------------------------------------------------------------- 1 | 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\Component\Cache\Traits; 13 | 14 | /** 15 | * @internal 16 | */ 17 | trait RedisProxyTrait 18 | { 19 | private \Closure $initializer; 20 | private ?parent $realInstance = null; 21 | 22 | public static function createLazyProxy(\Closure $initializer, ?self $instance = null): static 23 | { 24 | $instance ??= (new \ReflectionClass(static::class))->newInstanceWithoutConstructor(); 25 | $instance->realInstance = null; 26 | $instance->initializer = $initializer; 27 | 28 | return $instance; 29 | } 30 | 31 | public function isLazyObjectInitialized(bool $partial = false): bool 32 | { 33 | return isset($this->realInstance); 34 | } 35 | 36 | public function initializeLazyObject(): object 37 | { 38 | return $this->realInstance ??= ($this->initializer)(); 39 | } 40 | 41 | public function resetLazyObject(): bool 42 | { 43 | $this->realInstance = null; 44 | 45 | return true; 46 | } 47 | 48 | public function __destruct() 49 | { 50 | } 51 | } 52 | -------------------------------------------------------------------------------- /Traits/Relay/CopyTrait.php: -------------------------------------------------------------------------------- 1 | 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\Component\Cache\Traits\Relay; 13 | 14 | if (version_compare(phpversion('relay'), '0.8.1', '>=')) { 15 | /** 16 | * @internal 17 | */ 18 | trait CopyTrait 19 | { 20 | public function copy($src, $dst, $options = null): \Relay\Relay|bool 21 | { 22 | return $this->initializeLazyObject()->copy(...\func_get_args()); 23 | } 24 | } 25 | } else { 26 | /** 27 | * @internal 28 | */ 29 | trait CopyTrait 30 | { 31 | public function copy($src, $dst, $options = null): \Relay\Relay|false|int 32 | { 33 | return $this->initializeLazyObject()->copy(...\func_get_args()); 34 | } 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /Traits/Relay/GeosearchTrait.php: -------------------------------------------------------------------------------- 1 | 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\Component\Cache\Traits\Relay; 13 | 14 | if (version_compare(phpversion('relay'), '0.9.0', '>=')) { 15 | /** 16 | * @internal 17 | */ 18 | trait GeosearchTrait 19 | { 20 | public function geosearch($key, $position, $shape, $unit, $options = []): \Relay\Relay|array|false 21 | { 22 | return $this->initializeLazyObject()->geosearch(...\func_get_args()); 23 | } 24 | } 25 | } else { 26 | /** 27 | * @internal 28 | */ 29 | trait GeosearchTrait 30 | { 31 | public function geosearch($key, $position, $shape, $unit, $options = []): \Relay\Relay|array 32 | { 33 | return $this->initializeLazyObject()->geosearch(...\func_get_args()); 34 | } 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /Traits/Relay/GetrangeTrait.php: -------------------------------------------------------------------------------- 1 | 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\Component\Cache\Traits\Relay; 13 | 14 | if (version_compare(phpversion('relay'), '0.9.0', '>=')) { 15 | /** 16 | * @internal 17 | */ 18 | trait GetrangeTrait 19 | { 20 | public function getrange($key, $start, $end): mixed 21 | { 22 | return $this->initializeLazyObject()->getrange(...\func_get_args()); 23 | } 24 | } 25 | } else { 26 | /** 27 | * @internal 28 | */ 29 | trait GetrangeTrait 30 | { 31 | public function getrange($key, $start, $end): \Relay\Relay|false|string 32 | { 33 | return $this->initializeLazyObject()->getrange(...\func_get_args()); 34 | } 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /Traits/Relay/HsetTrait.php: -------------------------------------------------------------------------------- 1 | 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\Component\Cache\Traits\Relay; 13 | 14 | if (version_compare(phpversion('relay'), '0.9.0', '>=')) { 15 | /** 16 | * @internal 17 | */ 18 | trait HsetTrait 19 | { 20 | public function hset($key, ...$keys_and_vals): \Relay\Relay|false|int 21 | { 22 | return $this->initializeLazyObject()->hset(...\func_get_args()); 23 | } 24 | } 25 | } else { 26 | /** 27 | * @internal 28 | */ 29 | trait HsetTrait 30 | { 31 | public function hset($key, $mem, $val, ...$kvals): \Relay\Relay|false|int 32 | { 33 | return $this->initializeLazyObject()->hset(...\func_get_args()); 34 | } 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /Traits/Relay/MoveTrait.php: -------------------------------------------------------------------------------- 1 | 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\Component\Cache\Traits\Relay; 13 | 14 | if (version_compare(phpversion('relay'), '0.9.0', '>=')) { 15 | /** 16 | * @internal 17 | */ 18 | trait MoveTrait 19 | { 20 | public function blmove($srckey, $dstkey, $srcpos, $dstpos, $timeout): mixed 21 | { 22 | return $this->initializeLazyObject()->blmove(...\func_get_args()); 23 | } 24 | 25 | public function lmove($srckey, $dstkey, $srcpos, $dstpos): mixed 26 | { 27 | return $this->initializeLazyObject()->lmove(...\func_get_args()); 28 | } 29 | } 30 | } else { 31 | /** 32 | * @internal 33 | */ 34 | trait MoveTrait 35 | { 36 | public function blmove($srckey, $dstkey, $srcpos, $dstpos, $timeout): \Relay\Relay|false|string|null 37 | { 38 | return $this->initializeLazyObject()->blmove(...\func_get_args()); 39 | } 40 | 41 | public function lmove($srckey, $dstkey, $srcpos, $dstpos): \Relay\Relay|false|string|null 42 | { 43 | return $this->initializeLazyObject()->lmove(...\func_get_args()); 44 | } 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /Traits/Relay/NullableReturnTrait.php: -------------------------------------------------------------------------------- 1 | 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\Component\Cache\Traits\Relay; 13 | 14 | if (version_compare(phpversion('relay'), '0.9.0', '>=')) { 15 | /** 16 | * @internal 17 | */ 18 | trait NullableReturnTrait 19 | { 20 | public function dump($key): \Relay\Relay|false|string|null 21 | { 22 | return $this->initializeLazyObject()->dump(...\func_get_args()); 23 | } 24 | 25 | public function geodist($key, $src, $dst, $unit = null): \Relay\Relay|false|float|null 26 | { 27 | return $this->initializeLazyObject()->geodist(...\func_get_args()); 28 | } 29 | 30 | public function hrandfield($hash, $options = null): \Relay\Relay|array|false|string|null 31 | { 32 | return $this->initializeLazyObject()->hrandfield(...\func_get_args()); 33 | } 34 | 35 | public function xadd($key, $id, $values, $maxlen = 0, $approx = false, $nomkstream = false): \Relay\Relay|false|string|null 36 | { 37 | return $this->initializeLazyObject()->xadd(...\func_get_args()); 38 | } 39 | 40 | public function zrank($key, $rank, $withscore = false): \Relay\Relay|array|false|int|null 41 | { 42 | return $this->initializeLazyObject()->zrank(...\func_get_args()); 43 | } 44 | 45 | public function zrevrank($key, $rank, $withscore = false): \Relay\Relay|array|false|int|null 46 | { 47 | return $this->initializeLazyObject()->zrevrank(...\func_get_args()); 48 | } 49 | 50 | public function zscore($key, $member): \Relay\Relay|false|float|null 51 | { 52 | return $this->initializeLazyObject()->zscore(...\func_get_args()); 53 | } 54 | } 55 | } else { 56 | /** 57 | * @internal 58 | */ 59 | trait NullableReturnTrait 60 | { 61 | public function dump($key): \Relay\Relay|false|string 62 | { 63 | return $this->initializeLazyObject()->dump(...\func_get_args()); 64 | } 65 | 66 | public function geodist($key, $src, $dst, $unit = null): \Relay\Relay|false|float 67 | { 68 | return $this->initializeLazyObject()->geodist(...\func_get_args()); 69 | } 70 | 71 | public function hrandfield($hash, $options = null): \Relay\Relay|array|false|string 72 | { 73 | return $this->initializeLazyObject()->hrandfield(...\func_get_args()); 74 | } 75 | 76 | public function xadd($key, $id, $values, $maxlen = 0, $approx = false, $nomkstream = false): \Relay\Relay|false|string 77 | { 78 | return $this->initializeLazyObject()->xadd(...\func_get_args()); 79 | } 80 | 81 | public function zrank($key, $rank, $withscore = false): \Relay\Relay|array|false|int 82 | { 83 | return $this->initializeLazyObject()->zrank(...\func_get_args()); 84 | } 85 | 86 | public function zrevrank($key, $rank, $withscore = false): \Relay\Relay|array|false|int 87 | { 88 | return $this->initializeLazyObject()->zrevrank(...\func_get_args()); 89 | } 90 | 91 | public function zscore($key, $member): \Relay\Relay|false|float 92 | { 93 | return $this->initializeLazyObject()->zscore(...\func_get_args()); 94 | } 95 | } 96 | } 97 | -------------------------------------------------------------------------------- /Traits/Relay/PfcountTrait.php: -------------------------------------------------------------------------------- 1 | 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\Component\Cache\Traits\Relay; 13 | 14 | if (version_compare(phpversion('relay'), '0.9.0', '>=')) { 15 | /** 16 | * @internal 17 | */ 18 | trait PfcountTrait 19 | { 20 | public function pfcount($key_or_keys): \Relay\Relay|false|int 21 | { 22 | return $this->initializeLazyObject()->pfcount(...\func_get_args()); 23 | } 24 | } 25 | } else { 26 | /** 27 | * @internal 28 | */ 29 | trait PfcountTrait 30 | { 31 | public function pfcount($key): \Relay\Relay|false|int 32 | { 33 | return $this->initializeLazyObject()->pfcount(...\func_get_args()); 34 | } 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /Traits/RelayProxyTrait.php: -------------------------------------------------------------------------------- 1 | 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\Component\Cache\Traits; 13 | 14 | if (version_compare(phpversion('relay'), '0.8.1', '>=')) { 15 | /** 16 | * @internal 17 | */ 18 | trait RelayProxyTrait 19 | { 20 | public function jsonArrAppend($key, $value_or_array, $path = null): \Relay\Relay|array|false 21 | { 22 | return $this->initializeLazyObject()->jsonArrAppend(...\func_get_args()); 23 | } 24 | 25 | public function jsonArrIndex($key, $path, $value, $start = 0, $stop = -1): \Relay\Relay|array|false 26 | { 27 | return $this->initializeLazyObject()->jsonArrIndex(...\func_get_args()); 28 | } 29 | 30 | public function jsonArrInsert($key, $path, $index, $value, ...$other_values): \Relay\Relay|array|false 31 | { 32 | return $this->initializeLazyObject()->jsonArrInsert(...\func_get_args()); 33 | } 34 | 35 | public function jsonArrLen($key, $path = null): \Relay\Relay|array|false 36 | { 37 | return $this->initializeLazyObject()->jsonArrLen(...\func_get_args()); 38 | } 39 | 40 | public function jsonArrPop($key, $path = null, $index = -1): \Relay\Relay|array|false 41 | { 42 | return $this->initializeLazyObject()->jsonArrPop(...\func_get_args()); 43 | } 44 | 45 | public function jsonArrTrim($key, $path, $start, $stop): \Relay\Relay|array|false 46 | { 47 | return $this->initializeLazyObject()->jsonArrTrim(...\func_get_args()); 48 | } 49 | 50 | public function jsonClear($key, $path = null): \Relay\Relay|false|int 51 | { 52 | return $this->initializeLazyObject()->jsonClear(...\func_get_args()); 53 | } 54 | 55 | public function jsonDebug($command, $key, $path = null): \Relay\Relay|false|int 56 | { 57 | return $this->initializeLazyObject()->jsonDebug(...\func_get_args()); 58 | } 59 | 60 | public function jsonDel($key, $path = null): \Relay\Relay|false|int 61 | { 62 | return $this->initializeLazyObject()->jsonDel(...\func_get_args()); 63 | } 64 | 65 | public function jsonForget($key, $path = null): \Relay\Relay|false|int 66 | { 67 | return $this->initializeLazyObject()->jsonForget(...\func_get_args()); 68 | } 69 | 70 | public function jsonGet($key, $options = [], ...$paths): mixed 71 | { 72 | return $this->initializeLazyObject()->jsonGet(...\func_get_args()); 73 | } 74 | 75 | public function jsonMerge($key, $path, $value): \Relay\Relay|bool 76 | { 77 | return $this->initializeLazyObject()->jsonMerge(...\func_get_args()); 78 | } 79 | 80 | public function jsonMget($key_or_array, $path): \Relay\Relay|array|false 81 | { 82 | return $this->initializeLazyObject()->jsonMget(...\func_get_args()); 83 | } 84 | 85 | public function jsonMset($key, $path, $value, ...$other_triples): \Relay\Relay|bool 86 | { 87 | return $this->initializeLazyObject()->jsonMset(...\func_get_args()); 88 | } 89 | 90 | public function jsonNumIncrBy($key, $path, $value): \Relay\Relay|array|false 91 | { 92 | return $this->initializeLazyObject()->jsonNumIncrBy(...\func_get_args()); 93 | } 94 | 95 | public function jsonNumMultBy($key, $path, $value): \Relay\Relay|array|false 96 | { 97 | return $this->initializeLazyObject()->jsonNumMultBy(...\func_get_args()); 98 | } 99 | 100 | public function jsonObjKeys($key, $path = null): \Relay\Relay|array|false 101 | { 102 | return $this->initializeLazyObject()->jsonObjKeys(...\func_get_args()); 103 | } 104 | 105 | public function jsonObjLen($key, $path = null): \Relay\Relay|array|false 106 | { 107 | return $this->initializeLazyObject()->jsonObjLen(...\func_get_args()); 108 | } 109 | 110 | public function jsonResp($key, $path = null): \Relay\Relay|array|false|int|string 111 | { 112 | return $this->initializeLazyObject()->jsonResp(...\func_get_args()); 113 | } 114 | 115 | public function jsonSet($key, $path, $value, $condition = null): \Relay\Relay|bool 116 | { 117 | return $this->initializeLazyObject()->jsonSet(...\func_get_args()); 118 | } 119 | 120 | public function jsonStrAppend($key, $value, $path = null): \Relay\Relay|array|false 121 | { 122 | return $this->initializeLazyObject()->jsonStrAppend(...\func_get_args()); 123 | } 124 | 125 | public function jsonStrLen($key, $path = null): \Relay\Relay|array|false 126 | { 127 | return $this->initializeLazyObject()->jsonStrLen(...\func_get_args()); 128 | } 129 | 130 | public function jsonToggle($key, $path): \Relay\Relay|array|false 131 | { 132 | return $this->initializeLazyObject()->jsonToggle(...\func_get_args()); 133 | } 134 | 135 | public function jsonType($key, $path = null): \Relay\Relay|array|false 136 | { 137 | return $this->initializeLazyObject()->jsonType(...\func_get_args()); 138 | } 139 | } 140 | } else { 141 | /** 142 | * @internal 143 | */ 144 | trait RelayProxyTrait 145 | { 146 | } 147 | } 148 | -------------------------------------------------------------------------------- /Traits/ValueWrapper.php: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/symfony/cache/c4b217b578c11ec764867aa0c73e602c602965de/Traits/ValueWrapper.php -------------------------------------------------------------------------------- /composer.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "symfony/cache", 3 | "type": "library", 4 | "description": "Provides extended PSR-6, PSR-16 (and tags) implementations", 5 | "keywords": ["caching", "psr6"], 6 | "homepage": "https://symfony.com", 7 | "license": "MIT", 8 | "authors": [ 9 | { 10 | "name": "Nicolas Grekas", 11 | "email": "p@tchwork.com" 12 | }, 13 | { 14 | "name": "Symfony Community", 15 | "homepage": "https://symfony.com/contributors" 16 | } 17 | ], 18 | "provide": { 19 | "psr/cache-implementation": "2.0|3.0", 20 | "psr/simple-cache-implementation": "1.0|2.0|3.0", 21 | "symfony/cache-implementation": "1.1|2.0|3.0" 22 | }, 23 | "require": { 24 | "php": ">=8.2", 25 | "psr/cache": "^2.0|^3.0", 26 | "psr/log": "^1.1|^2|^3", 27 | "symfony/cache-contracts": "^3.6", 28 | "symfony/deprecation-contracts": "^2.5|^3.0", 29 | "symfony/service-contracts": "^2.5|^3", 30 | "symfony/var-exporter": "^6.4|^7.0" 31 | }, 32 | "require-dev": { 33 | "cache/integration-tests": "dev-master", 34 | "doctrine/dbal": "^3.6|^4", 35 | "predis/predis": "^1.1|^2.0", 36 | "psr/simple-cache": "^1.0|^2.0|^3.0", 37 | "symfony/clock": "^6.4|^7.0", 38 | "symfony/config": "^6.4|^7.0", 39 | "symfony/dependency-injection": "^6.4|^7.0", 40 | "symfony/filesystem": "^6.4|^7.0", 41 | "symfony/http-kernel": "^6.4|^7.0", 42 | "symfony/messenger": "^6.4|^7.0", 43 | "symfony/var-dumper": "^6.4|^7.0" 44 | }, 45 | "conflict": { 46 | "doctrine/dbal": "<3.6", 47 | "symfony/dependency-injection": "<6.4", 48 | "symfony/http-kernel": "<6.4", 49 | "symfony/var-dumper": "<6.4" 50 | }, 51 | "autoload": { 52 | "psr-4": { "Symfony\\Component\\Cache\\": "" }, 53 | "classmap": [ 54 | "Traits/ValueWrapper.php" 55 | ], 56 | "exclude-from-classmap": [ 57 | "/Tests/" 58 | ] 59 | }, 60 | "minimum-stability": "dev" 61 | } 62 | --------------------------------------------------------------------------------