3 namespace Drupal\Core\Cache;
5 use Drupal\Component\Utility\Crypt;
6 use Drupal\Core\DestructableInterface;
7 use Drupal\Core\Lock\LockBackendInterface;
10 * Default implementation for CacheCollectorInterface.
12 * By default, the class accounts for caches where calling functions might
13 * request keys that won't exist even after a cache rebuild. This prevents
14 * situations where a cache rebuild would be triggered over and over due to a
15 * 'missing' item. These cases are stored internally as a value of NULL. This
16 * means that the CacheCollector::get() method must be overridden if caching
17 * data where the values can legitimately be NULL, and where
18 * CacheCollector->has() needs to correctly return (equivalent to
19 * array_key_exists() vs. isset()). This should not be necessary in the majority
24 abstract class CacheCollector implements CacheCollectorInterface, DestructableInterface {
27 * The cache id that is used for the cache entry.
34 * A list of tags that are used for the cache entry.
41 * The cache backend that should be used.
43 * @var \Drupal\Core\Cache\CacheBackendInterface
48 * The lock backend that should be used.
50 * @var \Drupal\Core\Lock\LockBackendInterface
55 * An array of keys to add to the cache on service termination.
59 protected $keysToPersist = [];
62 * An array of keys to remove from the cache on service termination.
66 protected $keysToRemove = [];
69 * Storage for the data itself.
73 protected $storage = [];
76 * Stores the cache creation time.
78 * This is used to check if an invalidated cache item has been overwritten in
83 protected $cacheCreated;
86 * Flag that indicates of the cache has been invalidated.
90 protected $cacheInvalidated = FALSE;
93 * Indicates if the collected cache was already loaded.
95 * The collected cache is lazy loaded when an entry is set, get or deleted.
99 protected $cacheLoaded = FALSE;
102 * Constructs a CacheCollector object.
105 * The cid for the array being cached.
106 * @param \Drupal\Core\Cache\CacheBackendInterface $cache
108 * @param \Drupal\Core\Lock\LockBackendInterface $lock
111 * (optional) The tags to specify for the cache item.
113 public function __construct($cid, CacheBackendInterface $cache, LockBackendInterface $lock, array $tags = []) {
114 assert('\Drupal\Component\Assertion\Inspector::assertAllStrings($tags)', 'Cache tags must be strings.');
116 $this->cache = $cache;
126 protected function getCid() {
133 public function has($key) {
134 // Make sure the value is loaded.
136 return isset($this->storage[$key]) || array_key_exists($key, $this->storage);
142 public function get($key) {
143 $this->lazyLoadCache();
144 if (isset($this->storage[$key]) || array_key_exists($key, $this->storage)) {
145 return $this->storage[$key];
148 return $this->resolveCacheMiss($key);
153 * Implements \Drupal\Core\Cache\CacheCollectorInterface::set().
155 * This is not persisted by default. In practice this means that setting a
156 * value will only apply while the object is in scope and will not be written
157 * back to the persistent cache. This follows a similar pattern to static vs.
158 * persistent caching in procedural code. Extending classes may wish to alter
159 * this behavior, for example by adding a call to persist().
161 public function set($key, $value) {
162 $this->lazyLoadCache();
163 $this->storage[$key] = $value;
164 // The key might have been marked for deletion.
165 unset($this->keysToRemove[$key]);
166 $this->invalidateCache();
173 public function delete($key) {
174 $this->lazyLoadCache();
175 unset($this->storage[$key]);
176 $this->keysToRemove[$key] = $key;
177 // The key might have been marked for persisting.
178 unset($this->keysToPersist[$key]);
179 $this->invalidateCache();
183 * Flags an offset value to be written to the persistent cache.
186 * The key that was requested.
187 * @param bool $persist
188 * (optional) Whether the offset should be persisted or not, defaults to
189 * TRUE. When called with $persist = FALSE the offset will be unflagged so
190 * that it will not be written at the end of the request.
192 protected function persist($key, $persist = TRUE) {
193 $this->keysToPersist[$key] = $persist;
197 * Resolves a cache miss.
199 * When an offset is not found in the object, this is treated as a cache
200 * miss. This method allows classes using this implementation to look up the
201 * actual value and allow it to be cached.
204 * The offset that was requested.
207 * The value of the offset, or NULL if no value was found.
209 abstract protected function resolveCacheMiss($key);
212 * Writes a value to the persistent cache immediately.
215 * (optional) Whether to acquire a lock before writing to cache. Defaults to
218 protected function updateCache($lock = TRUE) {
220 foreach ($this->keysToPersist as $offset => $persist) {
222 $data[$offset] = $this->storage[$offset];
225 if (empty($data) && empty($this->keysToRemove)) {
229 // Lock cache writes to help avoid stampedes.
230 $cid = $this->getCid();
231 $lock_name = $this->normalizeLockName($cid . ':' . __CLASS__);
232 if (!$lock || $this->lock->acquire($lock_name)) {
233 // Set and delete operations invalidate the cache item. Try to also load
234 // an eventually invalidated cache entry, only update an invalidated cache
235 // entry if the creation date did not change as this could result in an
236 // inconsistent cache.
237 if ($cache = $this->cache->get($cid, $this->cacheInvalidated)) {
238 if ($this->cacheInvalidated && $cache->created != $this->cacheCreated) {
239 // We have invalidated the cache in this request and got a different
240 // cache entry. Do not attempt to overwrite data that might have been
241 // changed in a different request. We'll let the cache rebuild in
243 $this->cache->delete($cid);
244 $this->lock->release($lock_name);
247 $data = array_merge($cache->data, $data);
249 // Remove keys marked for deletion.
250 foreach ($this->keysToRemove as $delete_key) {
251 unset($data[$delete_key]);
253 $this->cache->set($cid, $data, Cache::PERMANENT, $this->tags);
255 $this->lock->release($lock_name);
259 $this->keysToPersist = [];
260 $this->keysToRemove = [];
264 * Normalizes a cache ID in order to comply with database limitations.
267 * The passed in cache ID.
270 * An ASCII-encoded cache ID that is at most 255 characters long.
272 protected function normalizeLockName($cid) {
273 // Nothing to do if the ID is a US ASCII string of 255 characters or less.
274 $cid_is_ascii = mb_check_encoding($cid, 'ASCII');
275 if (strlen($cid) <= 255 && $cid_is_ascii) {
278 // Return a string that uses as much as possible of the original cache ID
279 // with the hash appended.
280 $hash = Crypt::hashBase64($cid);
281 if (!$cid_is_ascii) {
284 return substr($cid, 0, 255 - strlen($hash)) . $hash;
290 public function reset() {
292 $this->keysToPersist = [];
293 $this->keysToRemove = [];
294 $this->cacheLoaded = FALSE;
300 public function clear() {
303 Cache::invalidateTags($this->tags);
306 $this->cache->delete($this->getCid());
313 public function destruct() {
314 $this->updateCache();
318 * Loads the cache if not already done.
320 protected function lazyLoadCache() {
321 if ($this->cacheLoaded) {
324 // The cache was not yet loaded, set flag to TRUE.
325 $this->cacheLoaded = TRUE;
327 if ($cache = $this->cache->get($this->getCid())) {
328 $this->cacheCreated = $cache->created;
329 $this->storage = $cache->data;
334 * Invalidate the cache.
336 protected function invalidateCache() {
337 // Invalidate the cache to make sure that other requests immediately see the
338 // deletion before this request is terminated.
339 $this->cache->invalidate($this->getCid());
340 $this->cacheInvalidated = TRUE;