Version 1
[yaffs-website] / web / core / modules / views / src / Plugin / views / cache / CachePluginBase.php
1 <?php
2
3 namespace Drupal\views\Plugin\views\cache;
4
5 use Drupal\Core\Cache\Cache;
6 use Drupal\Core\Cache\CacheableMetadata;
7 use Drupal\views\Plugin\views\PluginBase;
8 use Drupal\Core\Database\Query\Select;
9 use Drupal\views\ResultRow;
10
11 /**
12  * @defgroup views_cache_plugins Views cache plugins
13  * @{
14  * Plugins to handle Views caches.
15  *
16  * Cache plugins control how caching is done in Views.
17  *
18  * Cache plugins extend \Drupal\views\Plugin\views\cache\CachePluginBase.
19  * They must be annotated with \Drupal\views\Annotation\ViewsCache
20  * annotation, and must be in namespace directory Plugin\views\cache.
21  *
22  * @ingroup views_plugins
23  * @see plugin_api
24  */
25
26 /**
27  * The base plugin to handle caching.
28  */
29 abstract class CachePluginBase extends PluginBase {
30
31   /**
32    * Contains all data that should be written/read from cache.
33    */
34   public $storage = [];
35
36   /**
37    * Which cache bin to store query results in.
38    *
39    * @var string
40    */
41   protected $resultsBin = 'data';
42
43   /**
44    * Stores the cache ID used for the results cache.
45    *
46    * The cache ID is stored in generateResultsKey() got executed.
47    *
48    * @var string
49    *
50    * @see \Drupal\views\Plugin\views\cache\CachePluginBase::generateResultsKey()
51    */
52   protected $resultsKey;
53
54   /**
55    * Returns the resultsKey property.
56    *
57    * @return string
58    *   The resultsKey property.
59    */
60   public function getResultsKey() {
61     return $this->resultsKey;
62   }
63
64   /**
65    * Return a string to display as the clickable title for the
66    * access control.
67    */
68   public function summaryTitle() {
69     return $this->t('Unknown');
70   }
71
72   /**
73    * Determine the expiration time of the cache type, or NULL if no expire.
74    *
75    * Plugins must override this to implement expiration.
76    *
77    * @param $type
78    *   The cache type, either 'query', 'result'.
79    */
80   protected function cacheExpire($type) {
81   }
82
83   /**
84    * Determine expiration time in the cache table of the cache type
85    * or CACHE_PERMANENT if item shouldn't be removed automatically from cache.
86    *
87    * Plugins must override this to implement expiration in the cache table.
88    *
89    * @param $type
90    *   The cache type, either 'query', 'result'.
91    */
92   protected function cacheSetMaxAge($type) {
93     return Cache::PERMANENT;
94   }
95
96   /**
97    * Save data to the cache.
98    *
99    * A plugin should override this to provide specialized caching behavior.
100    */
101   public function cacheSet($type) {
102     switch ($type) {
103       case 'query':
104         // Not supported currently, but this is certainly where we'd put it.
105         break;
106       case 'results':
107         $data = [
108           'result' => $this->prepareViewResult($this->view->result),
109           'total_rows' => isset($this->view->total_rows) ? $this->view->total_rows : 0,
110           'current_page' => $this->view->getCurrentPage(),
111         ];
112         $expire = ($this->cacheSetMaxAge($type) === Cache::PERMANENT) ? Cache::PERMANENT : (int) $this->view->getRequest()->server->get('REQUEST_TIME') + $this->cacheSetMaxAge($type);
113         \Drupal::cache($this->resultsBin)->set($this->generateResultsKey(), $data, $expire, $this->getCacheTags());
114         break;
115     }
116   }
117
118   /**
119    * Retrieve data from the cache.
120    *
121    * A plugin should override this to provide specialized caching behavior.
122    */
123   public function cacheGet($type) {
124     $cutoff = $this->cacheExpire($type);
125     switch ($type) {
126       case 'query':
127         // Not supported currently, but this is certainly where we'd put it.
128         return FALSE;
129       case 'results':
130         // Values to set: $view->result, $view->total_rows, $view->execute_time,
131         // $view->current_page.
132         if ($cache = \Drupal::cache($this->resultsBin)->get($this->generateResultsKey())) {
133           if (!$cutoff || $cache->created > $cutoff) {
134             $this->view->result = $cache->data['result'];
135             // Load entities for each result.
136             $this->view->query->loadEntities($this->view->result);
137             $this->view->total_rows = $cache->data['total_rows'];
138             $this->view->setCurrentPage($cache->data['current_page'], TRUE);
139             $this->view->execute_time = 0;
140             return TRUE;
141           }
142         }
143         return FALSE;
144     }
145   }
146
147   /**
148    * Clear out cached data for a view.
149    */
150   public function cacheFlush() {
151     Cache::invalidateTags($this->view->storage->getCacheTagsToInvalidate());
152   }
153
154   /**
155    * Post process any rendered data.
156    *
157    * This can be valuable to be able to cache a view and still have some level of
158    * dynamic output. In an ideal world, the actual output will include HTML
159    * comment based tokens, and then the post process can replace those tokens.
160    *
161    * Example usage. If it is known that the view is a node view and that the
162    * primary field will be a nid, you can do something like this:
163    *
164    * <!--post-FIELD-NID-->
165    *
166    * And then in the post render, create an array with the text that should
167    * go there:
168    *
169    * strtr($output, array('<!--post-FIELD-1-->', 'output for FIELD of nid 1');
170    *
171    * All of the cached result data will be available in $view->result, as well,
172    * so all ids used in the query should be discoverable.
173    */
174   public function postRender(&$output) { }
175
176   /**
177    * Calculates and sets a cache ID used for the result cache.
178    *
179    * @return string
180    *   The generated cache ID.
181    */
182   public function generateResultsKey() {
183     if (!isset($this->resultsKey)) {
184       $build_info = $this->view->build_info;
185
186       foreach (['query', 'count_query'] as $index) {
187         // If the default query back-end is used generate SQL query strings from
188         // the query objects.
189         if ($build_info[$index] instanceof Select) {
190           $query = clone $build_info[$index];
191           $query->preExecute();
192           $build_info[$index] = [
193             'query' => (string)$query,
194             'arguments' => $query->getArguments(),
195           ];
196         }
197       }
198
199       $key_data = [
200         'build_info' => $build_info,
201       ];
202       // @todo https://www.drupal.org/node/2433591 might solve it to not require
203       //    the pager information here.
204       $key_data['pager'] = [
205         'page' => $this->view->getCurrentPage(),
206         'items_per_page' => $this->view->getItemsPerPage(),
207         'offset' => $this->view->getOffset(),
208       ];
209       $key_data += \Drupal::service('cache_contexts_manager')->convertTokensToKeys($this->displayHandler->getCacheMetadata()->getCacheContexts())->getKeys();
210
211       $this->resultsKey = $this->view->storage->id() . ':' . $this->displayHandler->display['id'] . ':results:' . hash('sha256', serialize($key_data));
212     }
213
214     return $this->resultsKey;
215   }
216
217   /**
218    * Gets an array of cache tags for the current view.
219    *
220    * @return string[]
221    *   An array of cache tags based on the current view.
222    */
223   public function getCacheTags() {
224     $tags = $this->view->storage->getCacheTags();
225
226     // The list cache tags for the entity types listed in this view.
227     $entity_information = $this->view->getQuery()->getEntityTableInfo();
228
229     if (!empty($entity_information)) {
230       // Add the list cache tags for each entity type used by this view.
231       foreach ($entity_information as $table => $metadata) {
232         $tags = Cache::mergeTags($tags, \Drupal::entityManager()->getDefinition($metadata['entity_type'])->getListCacheTags());
233       }
234     }
235
236     $tags = Cache::mergeTags($tags, $this->view->getQuery()->getCacheTags());
237
238     return $tags;
239   }
240
241   /**
242    * Gets the max age for the current view.
243    *
244    * @return int
245    */
246   public function getCacheMaxAge() {
247     $max_age = $this->getDefaultCacheMaxAge();
248     $max_age = Cache::mergeMaxAges($max_age, $this->view->getQuery()->getCacheMaxAge());
249     return $max_age;
250   }
251
252   /**
253    * Returns the default cache max age.
254    */
255   protected function getDefaultCacheMaxAge() {
256     // The default cache backend is not caching anything.
257     return 0;
258   }
259
260   /**
261    * Prepares the view result before putting it into cache.
262    *
263    * @param \Drupal\views\ResultRow[] $result
264    *   The result containing loaded entities.
265    *
266    * @return \Drupal\views\ResultRow[]
267    *   The result without loaded entities.
268    */
269   protected function prepareViewResult(array $result) {
270     $return = [];
271
272     // Clone each row object and remove any loaded entities, to keep the
273     // original result rows intact.
274     foreach ($result as $key => $row) {
275       $clone = clone $row;
276       $clone->resetEntityData();
277       $return[$key] = $clone;
278     }
279
280     return $return;
281   }
282
283   /**
284    * Alters the cache metadata of a display upon saving a view.
285    *
286    * @param \Drupal\Core\Cache\CacheableMetadata $cache_metadata
287    *   The cache metadata.
288    */
289   public function alterCacheMetadata(CacheableMetadata $cache_metadata) {
290   }
291
292   /**
293    * Returns the row cache tags.
294    *
295    * @param ResultRow $row
296    *   A result row.
297    *
298    * @return string[]
299    *   The row cache tags.
300    */
301   public function getRowCacheTags(ResultRow $row) {
302     $tags = !empty($row->_entity) ? $row->_entity->getCacheTags() : [];
303
304     if (!empty($row->_relationship_entities)) {
305       foreach ($row->_relationship_entities as $entity) {
306         $tags = Cache::mergeTags($tags, $entity->getCacheTags());
307       }
308     }
309
310     return $tags;
311   }
312
313   /**
314    * Returns the row cache keys.
315    *
316    * @param \Drupal\views\ResultRow $row
317    *   A result row.
318    *
319    * @return string[]
320    *   The row cache keys.
321    */
322   public function getRowCacheKeys(ResultRow $row) {
323     return [
324       'views',
325       'fields',
326       $this->view->id(),
327       $this->view->current_display,
328       $this->getRowId($row),
329     ];
330   }
331
332   /**
333    * Returns a unique identifier for the specified row.
334    *
335    * @param \Drupal\views\ResultRow $row
336    *   A result row.
337    *
338    * @return string
339    *   The row identifier.
340    */
341   public function getRowId(ResultRow $row) {
342     // Here we compute a unique identifier for the row by computing the hash of
343     // its data. We exclude the current index, since the same row could have a
344     // different result index depending on the user permissions. We exclude also
345     // entity data, since serializing entity objects is very expensive. Instead
346     // we include entity cache tags, which are enough to identify all the
347     // entities associated with the row.
348     $row_data = array_diff_key((array) $row, array_flip(['index', '_entity', '_relationship_entities'])) + $this->getRowCacheTags($row);
349
350     // This ensures that we get a unique identifier taking field handler access
351     // into account: users having access to different sets of fields will get
352     // different row identifiers.
353     $field_ids = array_keys($this->view->field);
354     $row_data += array_flip($field_ids);
355
356     // Finally we compute a hash of row data and return it as row identifier.
357     return hash('sha256', serialize($row_data));
358   }
359
360 }
361
362 /**
363  * @}
364  */