PageRenderTime 27ms CodeModel.GetById 10ms RepoModel.GetById 0ms app.codeStats 0ms

/src/Symfony/Component/Cache/Adapter/AbstractTagAwareAdapter.php

http://github.com/fabpot/symfony
PHP | 323 lines | 214 code | 37 blank | 72 comment | 25 complexity | 307749a70630390675d2219438391200 MD5 | raw file
  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\Log\LoggerAwareInterface;
  12. use Symfony\Component\Cache\CacheItem;
  13. use Symfony\Component\Cache\Exception\InvalidArgumentException;
  14. use Symfony\Component\Cache\ResettableInterface;
  15. use Symfony\Component\Cache\Traits\AbstractAdapterTrait;
  16. use Symfony\Component\Cache\Traits\ContractsTrait;
  17. use Symfony\Contracts\Cache\TagAwareCacheInterface;
  18. /**
  19. * Abstract for native TagAware adapters.
  20. *
  21. * To keep info on tags, the tags are both serialized as part of cache value and provided as tag ids
  22. * to Adapters on operations when needed for storage to doSave(), doDelete() & doInvalidate().
  23. *
  24. * @author Nicolas Grekas <p@tchwork.com>
  25. * @author André Rømcke <andre.romcke+symfony@gmail.com>
  26. *
  27. * @internal
  28. */
  29. abstract class AbstractTagAwareAdapter implements TagAwareAdapterInterface, TagAwareCacheInterface, LoggerAwareInterface, ResettableInterface
  30. {
  31. use AbstractAdapterTrait;
  32. use ContractsTrait;
  33. private const TAGS_PREFIX = "\0tags\0";
  34. protected function __construct(string $namespace = '', int $defaultLifetime = 0)
  35. {
  36. $this->namespace = '' === $namespace ? '' : CacheItem::validateKey($namespace).':';
  37. if (null !== $this->maxIdLength && \strlen($namespace) > $this->maxIdLength - 24) {
  38. throw new InvalidArgumentException(sprintf('Namespace must be %d chars max, %d given ("%s").', $this->maxIdLength - 24, \strlen($namespace), $namespace));
  39. }
  40. $this->createCacheItem = \Closure::bind(
  41. static function ($key, $value, $isHit) use ($defaultLifetime) {
  42. $item = new CacheItem();
  43. $item->key = $key;
  44. $item->defaultLifetime = $defaultLifetime;
  45. $item->isTaggable = true;
  46. // If structure does not match what we expect return item as is (no value and not a hit)
  47. if (!\is_array($value) || !\array_key_exists('value', $value)) {
  48. return $item;
  49. }
  50. $item->isHit = $isHit;
  51. // Extract value, tags and meta data from the cache value
  52. $item->value = $value['value'];
  53. $item->metadata[CacheItem::METADATA_TAGS] = $value['tags'] ?? [];
  54. if (isset($value['meta'])) {
  55. // For compactness these values are packed, & expiry is offset to reduce size
  56. $v = unpack('Ve/Nc', $value['meta']);
  57. $item->metadata[CacheItem::METADATA_EXPIRY] = $v['e'] + CacheItem::METADATA_EXPIRY_OFFSET;
  58. $item->metadata[CacheItem::METADATA_CTIME] = $v['c'];
  59. }
  60. return $item;
  61. },
  62. null,
  63. CacheItem::class
  64. );
  65. $getId = \Closure::fromCallable([$this, 'getId']);
  66. $tagPrefix = self::TAGS_PREFIX;
  67. $this->mergeByLifetime = \Closure::bind(
  68. static function ($deferred, &$expiredIds) use ($getId, $tagPrefix) {
  69. $byLifetime = [];
  70. $now = microtime(true);
  71. $expiredIds = [];
  72. foreach ($deferred as $key => $item) {
  73. $key = (string) $key;
  74. if (null === $item->expiry) {
  75. $ttl = 0 < $item->defaultLifetime ? $item->defaultLifetime : 0;
  76. } elseif (0 >= $ttl = (int) (0.1 + $item->expiry - $now)) {
  77. $expiredIds[] = $getId($key);
  78. continue;
  79. }
  80. // Store Value and Tags on the cache value
  81. if (isset(($metadata = $item->newMetadata)[CacheItem::METADATA_TAGS])) {
  82. $value = ['value' => $item->value, 'tags' => $metadata[CacheItem::METADATA_TAGS]];
  83. unset($metadata[CacheItem::METADATA_TAGS]);
  84. } else {
  85. $value = ['value' => $item->value, 'tags' => []];
  86. }
  87. if ($metadata) {
  88. // For compactness, expiry and creation duration are packed, using magic numbers as separators
  89. $value['meta'] = pack('VN', (int) (0.1 + $metadata[self::METADATA_EXPIRY] - self::METADATA_EXPIRY_OFFSET), $metadata[self::METADATA_CTIME]);
  90. }
  91. // Extract tag changes, these should be removed from values in doSave()
  92. $value['tag-operations'] = ['add' => [], 'remove' => []];
  93. $oldTags = $item->metadata[CacheItem::METADATA_TAGS] ?? [];
  94. foreach (array_diff($value['tags'], $oldTags) as $addedTag) {
  95. $value['tag-operations']['add'][] = $getId($tagPrefix.$addedTag);
  96. }
  97. foreach (array_diff($oldTags, $value['tags']) as $removedTag) {
  98. $value['tag-operations']['remove'][] = $getId($tagPrefix.$removedTag);
  99. }
  100. $byLifetime[$ttl][$getId($key)] = $value;
  101. }
  102. return $byLifetime;
  103. },
  104. null,
  105. CacheItem::class
  106. );
  107. }
  108. /**
  109. * Persists several cache items immediately.
  110. *
  111. * @param array $values The values to cache, indexed by their cache identifier
  112. * @param int $lifetime The lifetime of the cached values, 0 for persisting until manual cleaning
  113. * @param array[] $addTagData Hash where key is tag id, and array value is list of cache id's to add to tag
  114. * @param array[] $removeTagData Hash where key is tag id, and array value is list of cache id's to remove to tag
  115. *
  116. * @return array The identifiers that failed to be cached or a boolean stating if caching succeeded or not
  117. */
  118. abstract protected function doSave(array $values, int $lifetime, array $addTagData = [], array $removeTagData = []): array;
  119. /**
  120. * Removes multiple items from the pool and their corresponding tags.
  121. *
  122. * @param array $ids An array of identifiers that should be removed from the pool
  123. *
  124. * @return bool True if the items were successfully removed, false otherwise
  125. */
  126. abstract protected function doDelete(array $ids);
  127. /**
  128. * Removes relations between tags and deleted items.
  129. *
  130. * @param array $tagData Array of tag => key identifiers that should be removed from the pool
  131. */
  132. abstract protected function doDeleteTagRelations(array $tagData): bool;
  133. /**
  134. * Invalidates cached items using tags.
  135. *
  136. * @param string[] $tagIds An array of tags to invalidate, key is tag and value is tag id
  137. *
  138. * @return bool True on success
  139. */
  140. abstract protected function doInvalidate(array $tagIds): bool;
  141. /**
  142. * Delete items and yields the tags they were bound to.
  143. */
  144. protected function doDeleteYieldTags(array $ids): iterable
  145. {
  146. foreach ($this->doFetch($ids) as $id => $value) {
  147. yield $id => \is_array($value) && \is_array($value['tags'] ?? null) ? $value['tags'] : [];
  148. }
  149. $this->doDelete($ids);
  150. }
  151. /**
  152. * {@inheritdoc}
  153. */
  154. public function commit(): bool
  155. {
  156. $ok = true;
  157. $byLifetime = $this->mergeByLifetime;
  158. $byLifetime = $byLifetime($this->deferred, $expiredIds);
  159. $retry = $this->deferred = [];
  160. if ($expiredIds) {
  161. // Tags are not cleaned up in this case, however that is done on invalidateTags().
  162. $this->doDelete($expiredIds);
  163. }
  164. foreach ($byLifetime as $lifetime => $values) {
  165. try {
  166. $values = $this->extractTagData($values, $addTagData, $removeTagData);
  167. $e = $this->doSave($values, $lifetime, $addTagData, $removeTagData);
  168. } catch (\Exception $e) {
  169. }
  170. if (true === $e || [] === $e) {
  171. continue;
  172. }
  173. if (\is_array($e) || 1 === \count($values)) {
  174. foreach (\is_array($e) ? $e : array_keys($values) as $id) {
  175. $ok = false;
  176. $v = $values[$id];
  177. $type = get_debug_type($v);
  178. $message = sprintf('Failed to save key "{key}" of type %s%s', $type, $e instanceof \Exception ? ': '.$e->getMessage() : '.');
  179. CacheItem::log($this->logger, $message, ['key' => substr($id, \strlen($this->namespace)), 'exception' => $e instanceof \Exception ? $e : null, 'cache-adapter' => get_debug_type($this)]);
  180. }
  181. } else {
  182. foreach ($values as $id => $v) {
  183. $retry[$lifetime][] = $id;
  184. }
  185. }
  186. }
  187. // When bulk-save failed, retry each item individually
  188. foreach ($retry as $lifetime => $ids) {
  189. foreach ($ids as $id) {
  190. try {
  191. $v = $byLifetime[$lifetime][$id];
  192. $values = $this->extractTagData([$id => $v], $addTagData, $removeTagData);
  193. $e = $this->doSave($values, $lifetime, $addTagData, $removeTagData);
  194. } catch (\Exception $e) {
  195. }
  196. if (true === $e || [] === $e) {
  197. continue;
  198. }
  199. $ok = false;
  200. $type = get_debug_type($v);
  201. $message = sprintf('Failed to save key "{key}" of type %s%s', $type, $e instanceof \Exception ? ': '.$e->getMessage() : '.');
  202. CacheItem::log($this->logger, $message, ['key' => substr($id, \strlen($this->namespace)), 'exception' => $e instanceof \Exception ? $e : null, 'cache-adapter' => get_debug_type($this)]);
  203. }
  204. }
  205. return $ok;
  206. }
  207. /**
  208. * {@inheritdoc}
  209. */
  210. public function deleteItems(array $keys): bool
  211. {
  212. if (!$keys) {
  213. return true;
  214. }
  215. $ok = true;
  216. $ids = [];
  217. $tagData = [];
  218. foreach ($keys as $key) {
  219. $ids[$key] = $this->getId($key);
  220. unset($this->deferred[$key]);
  221. }
  222. try {
  223. foreach ($this->doDeleteYieldTags(array_values($ids)) as $id => $tags) {
  224. foreach ($tags as $tag) {
  225. $tagData[$this->getId(self::TAGS_PREFIX.$tag)][] = $id;
  226. }
  227. }
  228. } catch (\Exception $e) {
  229. $ok = false;
  230. }
  231. try {
  232. if ((!$tagData || $this->doDeleteTagRelations($tagData)) && $ok) {
  233. return true;
  234. }
  235. } catch (\Exception $e) {
  236. }
  237. // When bulk-delete failed, retry each item individually
  238. foreach ($ids as $key => $id) {
  239. try {
  240. $e = null;
  241. if ($this->doDelete([$id])) {
  242. continue;
  243. }
  244. } catch (\Exception $e) {
  245. }
  246. $message = 'Failed to delete key "{key}"'.($e instanceof \Exception ? ': '.$e->getMessage() : '.');
  247. CacheItem::log($this->logger, $message, ['key' => $key, 'exception' => $e, 'cache-adapter' => get_debug_type($this)]);
  248. $ok = false;
  249. }
  250. return $ok;
  251. }
  252. /**
  253. * {@inheritdoc}
  254. */
  255. public function invalidateTags(array $tags)
  256. {
  257. if (empty($tags)) {
  258. return false;
  259. }
  260. $tagIds = [];
  261. foreach (array_unique($tags) as $tag) {
  262. $tagIds[] = $this->getId(self::TAGS_PREFIX.$tag);
  263. }
  264. if ($this->doInvalidate($tagIds)) {
  265. return true;
  266. }
  267. return false;
  268. }
  269. /**
  270. * Extracts tags operation data from $values set in mergeByLifetime, and returns values without it.
  271. */
  272. private function extractTagData(array $values, ?array &$addTagData, ?array &$removeTagData): array
  273. {
  274. $addTagData = $removeTagData = [];
  275. foreach ($values as $id => $value) {
  276. foreach ($value['tag-operations']['add'] as $tag => $tagId) {
  277. $addTagData[$tagId][] = $id;
  278. }
  279. foreach ($value['tag-operations']['remove'] as $tag => $tagId) {
  280. $removeTagData[$tagId][] = $id;
  281. }
  282. unset($values[$id]['tag-operations']);
  283. }
  284. return $values;
  285. }
  286. }