1<?php 2 3/* 4 * This file is part of the Symfony package. 5 * 6 * (c) Fabien Potencier <fabien@symfony.com> 7 * 8 * For the full copyright and license information, please view the LICENSE 9 * file that was distributed with this source code. 10 */ 11 12namespace Symfony\Component\Cache\Adapter; 13 14use Psr\Cache\CacheItemInterface; 15use Psr\Cache\InvalidArgumentException; 16use Symfony\Component\Cache\CacheItem; 17use Symfony\Component\Cache\PruneableInterface; 18use Symfony\Component\Cache\ResettableInterface; 19use Symfony\Component\Cache\Traits\ContractsTrait; 20use Symfony\Component\Cache\Traits\ProxyTrait; 21use Symfony\Contracts\Cache\TagAwareCacheInterface; 22 23/** 24 * @author Nicolas Grekas <p@tchwork.com> 25 */ 26class TagAwareAdapter implements TagAwareAdapterInterface, TagAwareCacheInterface, PruneableInterface, ResettableInterface 27{ 28 const TAGS_PREFIX = "\0tags\0"; 29 30 use ProxyTrait; 31 use ContractsTrait; 32 33 private $deferred = []; 34 private $createCacheItem; 35 private $setCacheItemTags; 36 private $getTagsByKey; 37 private $invalidateTags; 38 private $tags; 39 private $knownTagVersions = []; 40 private $knownTagVersionsTtl; 41 42 public function __construct(AdapterInterface $itemsPool, AdapterInterface $tagsPool = null, float $knownTagVersionsTtl = 0.15) 43 { 44 $this->pool = $itemsPool; 45 $this->tags = $tagsPool ?: $itemsPool; 46 $this->knownTagVersionsTtl = $knownTagVersionsTtl; 47 $this->createCacheItem = \Closure::bind( 48 static function ($key, $value, CacheItem $protoItem) { 49 $item = new CacheItem(); 50 $item->key = $key; 51 $item->value = $value; 52 $item->defaultLifetime = $protoItem->defaultLifetime; 53 $item->expiry = $protoItem->expiry; 54 $item->poolHash = $protoItem->poolHash; 55 56 return $item; 57 }, 58 null, 59 CacheItem::class 60 ); 61 $this->setCacheItemTags = \Closure::bind( 62 static function (CacheItem $item, $key, array &$itemTags) { 63 $item->isTaggable = true; 64 if (!$item->isHit) { 65 return $item; 66 } 67 if (isset($itemTags[$key])) { 68 foreach ($itemTags[$key] as $tag => $version) { 69 $item->metadata[CacheItem::METADATA_TAGS][$tag] = $tag; 70 } 71 unset($itemTags[$key]); 72 } else { 73 $item->value = null; 74 $item->isHit = false; 75 } 76 77 return $item; 78 }, 79 null, 80 CacheItem::class 81 ); 82 $this->getTagsByKey = \Closure::bind( 83 static function ($deferred) { 84 $tagsByKey = []; 85 foreach ($deferred as $key => $item) { 86 $tagsByKey[$key] = $item->newMetadata[CacheItem::METADATA_TAGS] ?? []; 87 } 88 89 return $tagsByKey; 90 }, 91 null, 92 CacheItem::class 93 ); 94 $this->invalidateTags = \Closure::bind( 95 static function (AdapterInterface $tagsAdapter, array $tags) { 96 foreach ($tags as $v) { 97 $v->defaultLifetime = 0; 98 $v->expiry = null; 99 $tagsAdapter->saveDeferred($v); 100 } 101 102 return $tagsAdapter->commit(); 103 }, 104 null, 105 CacheItem::class 106 ); 107 } 108 109 /** 110 * {@inheritdoc} 111 */ 112 public function invalidateTags(array $tags) 113 { 114 $ok = true; 115 $tagsByKey = []; 116 $invalidatedTags = []; 117 foreach ($tags as $tag) { 118 CacheItem::validateKey($tag); 119 $invalidatedTags[$tag] = 0; 120 } 121 122 if ($this->deferred) { 123 $items = $this->deferred; 124 foreach ($items as $key => $item) { 125 if (!$this->pool->saveDeferred($item)) { 126 unset($this->deferred[$key]); 127 $ok = false; 128 } 129 } 130 131 $f = $this->getTagsByKey; 132 $tagsByKey = $f($items); 133 $this->deferred = []; 134 } 135 136 $tagVersions = $this->getTagVersions($tagsByKey, $invalidatedTags); 137 $f = $this->createCacheItem; 138 139 foreach ($tagsByKey as $key => $tags) { 140 $this->pool->saveDeferred($f(static::TAGS_PREFIX.$key, array_intersect_key($tagVersions, $tags), $items[$key])); 141 } 142 $ok = $this->pool->commit() && $ok; 143 144 if ($invalidatedTags) { 145 $f = $this->invalidateTags; 146 $ok = $f($this->tags, $invalidatedTags) && $ok; 147 } 148 149 return $ok; 150 } 151 152 /** 153 * {@inheritdoc} 154 * 155 * @return bool 156 */ 157 public function hasItem($key) 158 { 159 if ($this->deferred) { 160 $this->commit(); 161 } 162 if (!$this->pool->hasItem($key)) { 163 return false; 164 } 165 166 $itemTags = $this->pool->getItem(static::TAGS_PREFIX.$key); 167 168 if (!$itemTags->isHit()) { 169 return false; 170 } 171 172 if (!$itemTags = $itemTags->get()) { 173 return true; 174 } 175 176 foreach ($this->getTagVersions([$itemTags]) as $tag => $version) { 177 if ($itemTags[$tag] !== $version && 1 !== $itemTags[$tag] - $version) { 178 return false; 179 } 180 } 181 182 return true; 183 } 184 185 /** 186 * {@inheritdoc} 187 */ 188 public function getItem($key) 189 { 190 foreach ($this->getItems([$key]) as $item) { 191 return $item; 192 } 193 194 return null; 195 } 196 197 /** 198 * {@inheritdoc} 199 */ 200 public function getItems(array $keys = []) 201 { 202 if ($this->deferred) { 203 $this->commit(); 204 } 205 $tagKeys = []; 206 207 foreach ($keys as $key) { 208 if ('' !== $key && \is_string($key)) { 209 $key = static::TAGS_PREFIX.$key; 210 $tagKeys[$key] = $key; 211 } 212 } 213 214 try { 215 $items = $this->pool->getItems($tagKeys + $keys); 216 } catch (InvalidArgumentException $e) { 217 $this->pool->getItems($keys); // Should throw an exception 218 219 throw $e; 220 } 221 222 return $this->generateItems($items, $tagKeys); 223 } 224 225 /** 226 * {@inheritdoc} 227 * 228 * @return bool 229 */ 230 public function clear(string $prefix = '') 231 { 232 if ('' !== $prefix) { 233 foreach ($this->deferred as $key => $item) { 234 if (0 === strpos($key, $prefix)) { 235 unset($this->deferred[$key]); 236 } 237 } 238 } else { 239 $this->deferred = []; 240 } 241 242 if ($this->pool instanceof AdapterInterface) { 243 return $this->pool->clear($prefix); 244 } 245 246 return $this->pool->clear(); 247 } 248 249 /** 250 * {@inheritdoc} 251 * 252 * @return bool 253 */ 254 public function deleteItem($key) 255 { 256 return $this->deleteItems([$key]); 257 } 258 259 /** 260 * {@inheritdoc} 261 * 262 * @return bool 263 */ 264 public function deleteItems(array $keys) 265 { 266 foreach ($keys as $key) { 267 if ('' !== $key && \is_string($key)) { 268 $keys[] = static::TAGS_PREFIX.$key; 269 } 270 } 271 272 return $this->pool->deleteItems($keys); 273 } 274 275 /** 276 * {@inheritdoc} 277 * 278 * @return bool 279 */ 280 public function save(CacheItemInterface $item) 281 { 282 if (!$item instanceof CacheItem) { 283 return false; 284 } 285 $this->deferred[$item->getKey()] = $item; 286 287 return $this->commit(); 288 } 289 290 /** 291 * {@inheritdoc} 292 * 293 * @return bool 294 */ 295 public function saveDeferred(CacheItemInterface $item) 296 { 297 if (!$item instanceof CacheItem) { 298 return false; 299 } 300 $this->deferred[$item->getKey()] = $item; 301 302 return true; 303 } 304 305 /** 306 * {@inheritdoc} 307 * 308 * @return bool 309 */ 310 public function commit() 311 { 312 return $this->invalidateTags([]); 313 } 314 315 public function __sleep() 316 { 317 throw new \BadMethodCallException('Cannot serialize '.__CLASS__); 318 } 319 320 public function __wakeup() 321 { 322 throw new \BadMethodCallException('Cannot unserialize '.__CLASS__); 323 } 324 325 public function __destruct() 326 { 327 $this->commit(); 328 } 329 330 private function generateItems(iterable $items, array $tagKeys) 331 { 332 $bufferedItems = $itemTags = []; 333 $f = $this->setCacheItemTags; 334 335 foreach ($items as $key => $item) { 336 if (!$tagKeys) { 337 yield $key => $f($item, static::TAGS_PREFIX.$key, $itemTags); 338 continue; 339 } 340 if (!isset($tagKeys[$key])) { 341 $bufferedItems[$key] = $item; 342 continue; 343 } 344 345 unset($tagKeys[$key]); 346 347 if ($item->isHit()) { 348 $itemTags[$key] = $item->get() ?: []; 349 } 350 351 if (!$tagKeys) { 352 $tagVersions = $this->getTagVersions($itemTags); 353 354 foreach ($itemTags as $key => $tags) { 355 foreach ($tags as $tag => $version) { 356 if ($tagVersions[$tag] !== $version && 1 !== $version - $tagVersions[$tag]) { 357 unset($itemTags[$key]); 358 continue 2; 359 } 360 } 361 } 362 $tagVersions = $tagKeys = null; 363 364 foreach ($bufferedItems as $key => $item) { 365 yield $key => $f($item, static::TAGS_PREFIX.$key, $itemTags); 366 } 367 $bufferedItems = null; 368 } 369 } 370 } 371 372 private function getTagVersions(array $tagsByKey, array &$invalidatedTags = []) 373 { 374 $tagVersions = $invalidatedTags; 375 376 foreach ($tagsByKey as $tags) { 377 $tagVersions += $tags; 378 } 379 380 if (!$tagVersions) { 381 return []; 382 } 383 384 if (!$fetchTagVersions = 1 !== \func_num_args()) { 385 foreach ($tagsByKey as $tags) { 386 foreach ($tags as $tag => $version) { 387 if ($tagVersions[$tag] > $version) { 388 $tagVersions[$tag] = $version; 389 } 390 } 391 } 392 } 393 394 $now = microtime(true); 395 $tags = []; 396 foreach ($tagVersions as $tag => $version) { 397 $tags[$tag.static::TAGS_PREFIX] = $tag; 398 if ($fetchTagVersions || !isset($this->knownTagVersions[$tag])) { 399 $fetchTagVersions = true; 400 continue; 401 } 402 $version -= $this->knownTagVersions[$tag][1]; 403 if ((0 !== $version && 1 !== $version) || $now - $this->knownTagVersions[$tag][0] >= $this->knownTagVersionsTtl) { 404 // reuse previously fetched tag versions up to the ttl, unless we are storing items or a potential miss arises 405 $fetchTagVersions = true; 406 } else { 407 $this->knownTagVersions[$tag][1] += $version; 408 } 409 } 410 411 if (!$fetchTagVersions) { 412 return $tagVersions; 413 } 414 415 foreach ($this->tags->getItems(array_keys($tags)) as $tag => $version) { 416 $tagVersions[$tag = $tags[$tag]] = $version->get() ?: 0; 417 if (isset($invalidatedTags[$tag])) { 418 $invalidatedTags[$tag] = $version->set(++$tagVersions[$tag]); 419 } 420 $this->knownTagVersions[$tag] = [$now, $tagVersions[$tag]]; 421 } 422 423 return $tagVersions; 424 } 425} 426