CacheCollector.php 9.7 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356
  1. <?php
  2. namespace Drupal\Core\Cache;
  3. use Drupal\Component\Assertion\Inspector;
  4. use Drupal\Component\Utility\Crypt;
  5. use Drupal\Core\DestructableInterface;
  6. use Drupal\Core\Lock\LockBackendInterface;
  7. /**
  8. * Default implementation for CacheCollectorInterface.
  9. *
  10. * By default, the class accounts for caches where calling functions might
  11. * request keys that won't exist even after a cache rebuild. This prevents
  12. * situations where a cache rebuild would be triggered over and over due to a
  13. * 'missing' item. These cases are stored internally as a value of NULL. This
  14. * means that the CacheCollector::get() method must be overridden if caching
  15. * data where the values can legitimately be NULL, and where
  16. * CacheCollector->has() needs to correctly return (equivalent to
  17. * array_key_exists() vs. isset()). This should not be necessary in the majority
  18. * of cases.
  19. *
  20. * @ingroup cache
  21. */
  22. abstract class CacheCollector implements CacheCollectorInterface, DestructableInterface {
  23. /**
  24. * The cache id that is used for the cache entry.
  25. *
  26. * @var string
  27. */
  28. protected $cid;
  29. /**
  30. * A list of tags that are used for the cache entry.
  31. *
  32. * @var array
  33. */
  34. protected $tags;
  35. /**
  36. * The cache backend that should be used.
  37. *
  38. * @var \Drupal\Core\Cache\CacheBackendInterface
  39. */
  40. protected $cache;
  41. /**
  42. * The lock backend that should be used.
  43. *
  44. * @var \Drupal\Core\Lock\LockBackendInterface
  45. */
  46. protected $lock;
  47. /**
  48. * An array of keys to add to the cache on service termination.
  49. *
  50. * @var array
  51. */
  52. protected $keysToPersist = [];
  53. /**
  54. * An array of keys to remove from the cache on service termination.
  55. *
  56. * @var array
  57. */
  58. protected $keysToRemove = [];
  59. /**
  60. * Storage for the data itself.
  61. *
  62. * @var array
  63. */
  64. protected $storage = [];
  65. /**
  66. * Stores the cache creation time.
  67. *
  68. * This is used to check if an invalidated cache item has been overwritten in
  69. * the meantime.
  70. *
  71. * @var int
  72. */
  73. protected $cacheCreated;
  74. /**
  75. * Flag that indicates of the cache has been invalidated.
  76. *
  77. * @var bool
  78. */
  79. protected $cacheInvalidated = FALSE;
  80. /**
  81. * Indicates if the collected cache was already loaded.
  82. *
  83. * The collected cache is lazy loaded when an entry is set, get or deleted.
  84. *
  85. * @var bool
  86. */
  87. protected $cacheLoaded = FALSE;
  88. /**
  89. * Constructs a CacheCollector object.
  90. *
  91. * @param string $cid
  92. * The cid for the array being cached.
  93. * @param \Drupal\Core\Cache\CacheBackendInterface $cache
  94. * The cache backend.
  95. * @param \Drupal\Core\Lock\LockBackendInterface $lock
  96. * The lock backend.
  97. * @param array $tags
  98. * (optional) The tags to specify for the cache item.
  99. */
  100. public function __construct($cid, CacheBackendInterface $cache, LockBackendInterface $lock, array $tags = []) {
  101. assert(Inspector::assertAllStrings($tags), 'Cache tags must be strings.');
  102. $this->cid = $cid;
  103. $this->cache = $cache;
  104. $this->tags = $tags;
  105. $this->lock = $lock;
  106. }
  107. /**
  108. * Gets the cache ID.
  109. *
  110. * @return string
  111. */
  112. protected function getCid() {
  113. return $this->cid;
  114. }
  115. /**
  116. * {@inheritdoc}
  117. */
  118. public function has($key) {
  119. // Make sure the value is loaded.
  120. $this->get($key);
  121. return isset($this->storage[$key]) || array_key_exists($key, $this->storage);
  122. }
  123. /**
  124. * {@inheritdoc}
  125. */
  126. public function get($key) {
  127. $this->lazyLoadCache();
  128. if (isset($this->storage[$key]) || array_key_exists($key, $this->storage)) {
  129. return $this->storage[$key];
  130. }
  131. else {
  132. return $this->resolveCacheMiss($key);
  133. }
  134. }
  135. /**
  136. * Implements \Drupal\Core\Cache\CacheCollectorInterface::set().
  137. *
  138. * This is not persisted by default. In practice this means that setting a
  139. * value will only apply while the object is in scope and will not be written
  140. * back to the persistent cache. This follows a similar pattern to static vs.
  141. * persistent caching in procedural code. Extending classes may wish to alter
  142. * this behavior, for example by adding a call to persist().
  143. */
  144. public function set($key, $value) {
  145. $this->lazyLoadCache();
  146. $this->storage[$key] = $value;
  147. // The key might have been marked for deletion.
  148. unset($this->keysToRemove[$key]);
  149. $this->invalidateCache();
  150. }
  151. /**
  152. * {@inheritdoc}
  153. */
  154. public function delete($key) {
  155. $this->lazyLoadCache();
  156. unset($this->storage[$key]);
  157. $this->keysToRemove[$key] = $key;
  158. // The key might have been marked for persisting.
  159. unset($this->keysToPersist[$key]);
  160. $this->invalidateCache();
  161. }
  162. /**
  163. * Flags an offset value to be written to the persistent cache.
  164. *
  165. * @param string $key
  166. * The key that was requested.
  167. * @param bool $persist
  168. * (optional) Whether the offset should be persisted or not, defaults to
  169. * TRUE. When called with $persist = FALSE the offset will be unflagged so
  170. * that it will not be written at the end of the request.
  171. */
  172. protected function persist($key, $persist = TRUE) {
  173. $this->keysToPersist[$key] = $persist;
  174. }
  175. /**
  176. * Resolves a cache miss.
  177. *
  178. * When an offset is not found in the object, this is treated as a cache
  179. * miss. This method allows classes using this implementation to look up the
  180. * actual value and allow it to be cached.
  181. *
  182. * @param string $key
  183. * The offset that was requested.
  184. *
  185. * @return mixed
  186. * The value of the offset, or NULL if no value was found.
  187. */
  188. abstract protected function resolveCacheMiss($key);
  189. /**
  190. * Writes a value to the persistent cache immediately.
  191. *
  192. * @param bool $lock
  193. * (optional) Whether to acquire a lock before writing to cache. Defaults to
  194. * TRUE.
  195. */
  196. protected function updateCache($lock = TRUE) {
  197. $data = [];
  198. foreach ($this->keysToPersist as $offset => $persist) {
  199. if ($persist) {
  200. $data[$offset] = $this->storage[$offset];
  201. }
  202. }
  203. if (empty($data) && empty($this->keysToRemove)) {
  204. return;
  205. }
  206. // Lock cache writes to help avoid stampedes.
  207. $cid = $this->getCid();
  208. $lock_name = $this->normalizeLockName($cid . ':' . __CLASS__);
  209. if (!$lock || $this->lock->acquire($lock_name)) {
  210. // Set and delete operations invalidate the cache item. Try to also load
  211. // an eventually invalidated cache entry, only update an invalidated cache
  212. // entry if the creation date did not change as this could result in an
  213. // inconsistent cache.
  214. if ($cache = $this->cache->get($cid, $this->cacheInvalidated)) {
  215. if ($this->cacheInvalidated && $cache->created != $this->cacheCreated) {
  216. // We have invalidated the cache in this request and got a different
  217. // cache entry. Do not attempt to overwrite data that might have been
  218. // changed in a different request. We'll let the cache rebuild in
  219. // later requests.
  220. $this->cache->delete($cid);
  221. $this->lock->release($lock_name);
  222. return;
  223. }
  224. $data = array_merge($cache->data, $data);
  225. }
  226. elseif ($this->cacheCreated) {
  227. // Getting here indicates that there was a cache entry at the
  228. // beginning of the request, but now it's gone (some other process
  229. // must have cleared it). We back out to prevent corrupting the cache
  230. // with incomplete data, since we won't be able to properly merge
  231. // the existing cache data from earlier with the new data.
  232. // A future request will properly hydrate the cache from scratch.
  233. if ($lock) {
  234. $this->lock->release($lock_name);
  235. }
  236. return;
  237. }
  238. // Remove keys marked for deletion.
  239. foreach ($this->keysToRemove as $delete_key) {
  240. unset($data[$delete_key]);
  241. }
  242. $this->cache->set($cid, $data, Cache::PERMANENT, $this->tags);
  243. if ($lock) {
  244. $this->lock->release($lock_name);
  245. }
  246. }
  247. $this->keysToPersist = [];
  248. $this->keysToRemove = [];
  249. }
  250. /**
  251. * Normalizes a cache ID in order to comply with database limitations.
  252. *
  253. * @param string $cid
  254. * The passed in cache ID.
  255. *
  256. * @return string
  257. * An ASCII-encoded cache ID that is at most 255 characters long.
  258. */
  259. protected function normalizeLockName($cid) {
  260. // Nothing to do if the ID is a US ASCII string of 255 characters or less.
  261. $cid_is_ascii = mb_check_encoding($cid, 'ASCII');
  262. if (strlen($cid) <= 255 && $cid_is_ascii) {
  263. return $cid;
  264. }
  265. // Return a string that uses as much as possible of the original cache ID
  266. // with the hash appended.
  267. $hash = Crypt::hashBase64($cid);
  268. if (!$cid_is_ascii) {
  269. return $hash;
  270. }
  271. return substr($cid, 0, 255 - strlen($hash)) . $hash;
  272. }
  273. /**
  274. * {@inheritdoc}
  275. */
  276. public function reset() {
  277. $this->storage = [];
  278. $this->keysToPersist = [];
  279. $this->keysToRemove = [];
  280. $this->cacheLoaded = FALSE;
  281. }
  282. /**
  283. * {@inheritdoc}
  284. */
  285. public function clear() {
  286. $this->reset();
  287. if ($this->tags) {
  288. Cache::invalidateTags($this->tags);
  289. }
  290. else {
  291. $this->cache->delete($this->getCid());
  292. }
  293. }
  294. /**
  295. * {@inheritdoc}
  296. */
  297. public function destruct() {
  298. $this->updateCache();
  299. }
  300. /**
  301. * Loads the cache if not already done.
  302. */
  303. protected function lazyLoadCache() {
  304. if ($this->cacheLoaded) {
  305. return;
  306. }
  307. // The cache was not yet loaded, set flag to TRUE.
  308. $this->cacheLoaded = TRUE;
  309. if ($cache = $this->cache->get($this->getCid())) {
  310. $this->cacheCreated = $cache->created;
  311. $this->storage = $cache->data;
  312. }
  313. }
  314. /**
  315. * Invalidate the cache.
  316. */
  317. protected function invalidateCache() {
  318. // Invalidate the cache to make sure that other requests immediately see the
  319. // deletion before this request is terminated.
  320. $this->cache->invalidate($this->getCid());
  321. $this->cacheInvalidated = TRUE;
  322. }
  323. }