vendor/symfony/cache/Adapter/TagAwareAdapter.php line 166

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