acbbf006a05aa57e189e245c74091ff14675f116
[yaffs-website] / web / core / lib / Drupal / Core / Cache / CacheCollector.php
1 <?php
2
3 namespace Drupal\Core\Cache;
4
5 use Drupal\Component\Utility\Crypt;
6 use Drupal\Core\DestructableInterface;
7 use Drupal\Core\Lock\LockBackendInterface;
8
9 /**
10  * Default implementation for CacheCollectorInterface.
11  *
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
20  * of cases.
21  *
22  * @ingroup cache
23  */
24 abstract class CacheCollector implements CacheCollectorInterface, DestructableInterface {
25
26   /**
27    * The cache id that is used for the cache entry.
28    *
29    * @var string
30    */
31   protected $cid;
32
33   /**
34    * A list of tags that are used for the cache entry.
35    *
36    * @var array
37    */
38   protected $tags;
39
40   /**
41    * The cache backend that should be used.
42    *
43    * @var \Drupal\Core\Cache\CacheBackendInterface
44    */
45   protected $cache;
46
47   /**
48    * The lock backend that should be used.
49    *
50    * @var \Drupal\Core\Lock\LockBackendInterface
51    */
52   protected $lock;
53
54   /**
55    * An array of keys to add to the cache on service termination.
56    *
57    * @var array
58    */
59   protected $keysToPersist = [];
60
61   /**
62    * An array of keys to remove from the cache on service termination.
63    *
64    * @var array
65    */
66   protected $keysToRemove = [];
67
68   /**
69    * Storage for the data itself.
70    *
71    * @var array
72    */
73   protected $storage = [];
74
75   /**
76    * Stores the cache creation time.
77    *
78    * This is used to check if an invalidated cache item has been overwritten in
79    * the meantime.
80    *
81    * @var int
82    */
83   protected $cacheCreated;
84
85   /**
86    * Flag that indicates of the cache has been invalidated.
87    *
88    * @var bool
89    */
90   protected $cacheInvalidated = FALSE;
91
92   /**
93    * Indicates if the collected cache was already loaded.
94    *
95    * The collected cache is lazy loaded when an entry is set, get or deleted.
96    *
97    * @var bool
98    */
99   protected $cacheLoaded = FALSE;
100
101   /**
102    * Constructs a CacheCollector object.
103    *
104    * @param string $cid
105    *   The cid for the array being cached.
106    * @param \Drupal\Core\Cache\CacheBackendInterface $cache
107    *   The cache backend.
108    * @param \Drupal\Core\Lock\LockBackendInterface $lock
109    *   The lock backend.
110    * @param array $tags
111    *   (optional) The tags to specify for the cache item.
112    */
113   public function __construct($cid, CacheBackendInterface $cache, LockBackendInterface $lock, array $tags = []) {
114     assert('\Drupal\Component\Assertion\Inspector::assertAllStrings($tags)', 'Cache tags must be strings.');
115     $this->cid = $cid;
116     $this->cache = $cache;
117     $this->tags = $tags;
118     $this->lock = $lock;
119   }
120
121   /**
122    * Gets the cache ID.
123    *
124    * @return string
125    */
126   protected function getCid() {
127     return $this->cid;
128   }
129
130   /**
131    * {@inheritdoc}
132    */
133   public function has($key) {
134     // Make sure the value is loaded.
135     $this->get($key);
136     return isset($this->storage[$key]) || array_key_exists($key, $this->storage);
137   }
138
139   /**
140    * {@inheritdoc}
141    */
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];
146     }
147     else {
148       return $this->resolveCacheMiss($key);
149     }
150   }
151
152   /**
153    * Implements \Drupal\Core\Cache\CacheCollectorInterface::set().
154    *
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().
160    */
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();
167
168   }
169
170   /**
171    * {@inheritdoc}
172    */
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();
180   }
181
182   /**
183    * Flags an offset value to be written to the persistent cache.
184    *
185    * @param string $key
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.
191    */
192   protected function persist($key, $persist = TRUE) {
193     $this->keysToPersist[$key] = $persist;
194   }
195
196   /**
197    * Resolves a cache miss.
198    *
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.
202    *
203    * @param string $key
204    *   The offset that was requested.
205    *
206    * @return mixed
207    *   The value of the offset, or NULL if no value was found.
208    */
209   abstract protected function resolveCacheMiss($key);
210
211   /**
212    * Writes a value to the persistent cache immediately.
213    *
214    * @param bool $lock
215    *   (optional) Whether to acquire a lock before writing to cache. Defaults to
216    *   TRUE.
217    */
218   protected function updateCache($lock = TRUE) {
219     $data = [];
220     foreach ($this->keysToPersist as $offset => $persist) {
221       if ($persist) {
222         $data[$offset] = $this->storage[$offset];
223       }
224     }
225     if (empty($data) && empty($this->keysToRemove)) {
226       return;
227     }
228
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
242           // later requests.
243           $this->cache->delete($cid);
244           $this->lock->release($lock_name);
245           return;
246         }
247         $data = array_merge($cache->data, $data);
248       }
249       // Remove keys marked for deletion.
250       foreach ($this->keysToRemove as $delete_key) {
251         unset($data[$delete_key]);
252       }
253       $this->cache->set($cid, $data, Cache::PERMANENT, $this->tags);
254       if ($lock) {
255         $this->lock->release($lock_name);
256       }
257     }
258
259     $this->keysToPersist = [];
260     $this->keysToRemove = [];
261   }
262
263   /**
264    * Normalizes a cache ID in order to comply with database limitations.
265    *
266    * @param string $cid
267    *   The passed in cache ID.
268    *
269    * @return string
270    *   An ASCII-encoded cache ID that is at most 255 characters long.
271    */
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) {
276       return $cid;
277     }
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) {
282       return $hash;
283     }
284     return substr($cid, 0, 255 - strlen($hash)) . $hash;
285   }
286
287   /**
288    * {@inheritdoc}
289    */
290   public function reset() {
291     $this->storage = [];
292     $this->keysToPersist = [];
293     $this->keysToRemove = [];
294     $this->cacheLoaded = FALSE;
295   }
296
297   /**
298    * {@inheritdoc}
299    */
300   public function clear() {
301     $this->reset();
302     if ($this->tags) {
303       Cache::invalidateTags($this->tags);
304     }
305     else {
306       $this->cache->delete($this->getCid());
307     }
308   }
309
310   /**
311    * {@inheritdoc}
312    */
313   public function destruct() {
314     $this->updateCache();
315   }
316
317   /**
318    * Loads the cache if not already done.
319    */
320   protected function lazyLoadCache() {
321     if ($this->cacheLoaded) {
322       return;
323     }
324     // The cache was not yet loaded, set flag to TRUE.
325     $this->cacheLoaded = TRUE;
326
327     if ($cache = $this->cache->get($this->getCid())) {
328       $this->cacheCreated = $cache->created;
329       $this->storage = $cache->data;
330     }
331   }
332
333   /**
334    * Invalidate the cache.
335    */
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;
341   }
342
343 }