11a0938449c5b1d871170cb1c5c4d15872a07c03
[yaffs-website] / src / Entity / View.php
1 <?php
2
3 namespace Drupal\views\Entity;
4
5 use Drupal\Component\Utility\NestedArray;
6 use Drupal\Core\Cache\Cache;
7 use Drupal\Core\Config\Entity\ConfigEntityBase;
8 use Drupal\Core\Entity\ContentEntityTypeInterface;
9 use Drupal\Core\Entity\EntityStorageInterface;
10 use Drupal\Core\Entity\FieldableEntityInterface;
11 use Drupal\Core\Language\LanguageInterface;
12 use Drupal\views\Plugin\DependentWithRemovalPluginInterface;
13 use Drupal\views\Views;
14 use Drupal\views\ViewEntityInterface;
15
16 /**
17  * Defines a View configuration entity class.
18  *
19  * @ConfigEntityType(
20  *   id = "view",
21  *   label = @Translation("View", context = "View entity type"),
22  *   label_collection = @Translation("Views", context = "View entity type"),
23  *   label_singular = @Translation("view", context = "View entity type"),
24  *   label_plural = @Translation("views", context = "View entity type"),
25  *   label_count = @PluralTranslation(
26  *     singular = "@count view",
27  *     plural = "@count views",
28  *     context = "View entity type",
29  *   ),
30  *   admin_permission = "administer views",
31  *   entity_keys = {
32  *     "id" = "id",
33  *     "label" = "label",
34  *     "status" = "status"
35  *   },
36  *   config_export = {
37  *     "id",
38  *     "label",
39  *     "module",
40  *     "description",
41  *     "tag",
42  *     "base_table",
43  *     "base_field",
44  *     "core",
45  *     "display",
46  *   }
47  * )
48  */
49 class View extends ConfigEntityBase implements ViewEntityInterface {
50
51   /**
52    * The name of the base table this view will use.
53    *
54    * @var string
55    */
56   protected $base_table = 'node';
57
58   /**
59    * The unique ID of the view.
60    *
61    * @var string
62    */
63   protected $id = NULL;
64
65   /**
66    * The label of the view.
67    */
68   protected $label;
69
70   /**
71    * The description of the view, which is used only in the interface.
72    *
73    * @var string
74    */
75   protected $description = '';
76
77   /**
78    * The "tags" of a view.
79    *
80    * The tags are stored as a single string, though it is used as multiple tags
81    * for example in the views overview.
82    *
83    * @var string
84    */
85   protected $tag = '';
86
87   /**
88    * The core version the view was created for.
89    *
90    * @var string
91    */
92   protected $core = \Drupal::CORE_COMPATIBILITY;
93
94   /**
95    * Stores all display handlers of this view.
96    *
97    * An array containing Drupal\views\Plugin\views\display\DisplayPluginBase
98    * objects.
99    *
100    * @var array
101    */
102   protected $display = [];
103
104   /**
105    * The name of the base field to use.
106    *
107    * @var string
108    */
109   protected $base_field = 'nid';
110
111   /**
112    * Stores a reference to the executable version of this view.
113    *
114    * @var \Drupal\views\ViewExecutable
115    */
116   protected $executable;
117
118   /**
119    * The module implementing this view.
120    *
121    * @var string
122    */
123   protected $module = 'views';
124
125   /**
126    * {@inheritdoc}
127    */
128   public function getExecutable() {
129     // Ensure that an executable View is available.
130     if (!isset($this->executable)) {
131       $this->executable = Views::executableFactory()->get($this);
132     }
133
134     return $this->executable;
135   }
136
137   /**
138    * {@inheritdoc}
139    */
140   public function createDuplicate() {
141     $duplicate = parent::createDuplicate();
142     unset($duplicate->executable);
143     return $duplicate;
144   }
145
146   /**
147    * {@inheritdoc}
148    */
149   public function label() {
150     if (!$label = $this->get('label')) {
151       $label = $this->id();
152     }
153     return $label;
154   }
155
156   /**
157    * {@inheritdoc}
158    */
159   public function addDisplay($plugin_id = 'page', $title = NULL, $id = NULL) {
160     if (empty($plugin_id)) {
161       return FALSE;
162     }
163
164     $plugin = Views::pluginManager('display')->getDefinition($plugin_id);
165
166     if (empty($plugin)) {
167       $plugin['title'] = t('Broken');
168     }
169
170     if (empty($id)) {
171       $id = $this->generateDisplayId($plugin_id);
172
173       // Generate a unique human-readable name by inspecting the counter at the
174       // end of the previous display ID, e.g., 'page_1'.
175       if ($id !== 'default') {
176         preg_match("/[0-9]+/", $id, $count);
177         $count = $count[0];
178       }
179       else {
180         $count = '';
181       }
182
183       if (empty($title)) {
184         // If there is no title provided, use the plugin title, and if there are
185         // multiple displays, append the count.
186         $title = $plugin['title'];
187         if ($count > 1) {
188           $title .= ' ' . $count;
189         }
190       }
191     }
192
193     $display_options = [
194       'display_plugin' => $plugin_id,
195       'id' => $id,
196       // Cast the display title to a string since it is an object.
197       // @see \Drupal\Core\StringTranslation\TranslatableMarkup
198       'display_title' => (string) $title,
199       'position' => $id === 'default' ? 0 : count($this->display),
200       'display_options' => [],
201     ];
202
203     // Add the display options to the view.
204     $this->display[$id] = $display_options;
205     return $id;
206   }
207
208   /**
209    * Generates a display ID of a certain plugin type.
210    *
211    * @param string $plugin_id
212    *   Which plugin should be used for the new display ID.
213    *
214    * @return string
215    */
216   protected function generateDisplayId($plugin_id) {
217     // 'default' is singular and is unique, so just go with 'default'
218     // for it. For all others, start counting.
219     if ($plugin_id == 'default') {
220       return 'default';
221     }
222     // Initial ID.
223     $id = $plugin_id . '_1';
224     $count = 1;
225
226     // Loop through IDs based upon our style plugin name until
227     // we find one that is unused.
228     while (!empty($this->display[$id])) {
229       $id = $plugin_id . '_' . ++$count;
230     }
231
232     return $id;
233   }
234
235   /**
236    * {@inheritdoc}
237    */
238   public function &getDisplay($display_id) {
239     return $this->display[$display_id];
240   }
241
242   /**
243    * {@inheritdoc}
244    */
245   public function duplicateDisplayAsType($old_display_id, $new_display_type) {
246     $executable = $this->getExecutable();
247     $display = $executable->newDisplay($new_display_type);
248     $new_display_id = $display->display['id'];
249     $displays = $this->get('display');
250
251     // Let the display title be generated by the addDisplay method and set the
252     // right display plugin, but keep the rest from the original display.
253     $display_duplicate = $displays[$old_display_id];
254     unset($display_duplicate['display_title']);
255     unset($display_duplicate['display_plugin']);
256     unset($display_duplicate['new_id']);
257
258     $displays[$new_display_id] = NestedArray::mergeDeep($displays[$new_display_id], $display_duplicate);
259     $displays[$new_display_id]['id'] = $new_display_id;
260
261     // First set the displays.
262     $this->set('display', $displays);
263
264     // Ensure that we just copy display options, which are provided by the new
265     // display plugin.
266     $executable->setDisplay($new_display_id);
267
268     $executable->display_handler->filterByDefinedOptions($displays[$new_display_id]['display_options']);
269     // Update the display settings.
270     $this->set('display', $displays);
271
272     return $new_display_id;
273   }
274
275   /**
276    * {@inheritdoc}
277    */
278   public function calculateDependencies() {
279     parent::calculateDependencies();
280
281     // Ensure that the view is dependant on the module that implements the view.
282     $this->addDependency('module', $this->module);
283
284     $executable = $this->getExecutable();
285     $executable->initDisplay();
286     $executable->initStyle();
287
288     foreach ($executable->displayHandlers as $display) {
289       // Calculate the dependencies each display has.
290       $this->calculatePluginDependencies($display);
291     }
292
293     return $this;
294   }
295
296   /**
297    * {@inheritdoc}
298    */
299   public function preSave(EntityStorageInterface $storage) {
300     parent::preSave($storage);
301
302     $displays = $this->get('display');
303
304     $this->fixTableNames($displays);
305
306     // Sort the displays.
307     ksort($displays);
308     $this->set('display', ['default' => $displays['default']] + $displays);
309
310     // @todo Check whether isSyncing is needed.
311     if (!$this->isSyncing()) {
312       $this->addCacheMetadata();
313     }
314   }
315
316   /**
317    * Fixes table names for revision metadata fields of revisionable entities.
318    *
319    * Views for revisionable entity types using revision metadata fields might
320    * be using the wrong table to retrieve the fields after system_update_8300
321    * has moved them correctly to the revision table. This method updates the
322    * views to use the correct tables.
323    *
324    * @param array &$displays
325    *   An array containing display handlers of a view.
326    *
327    * @deprecated in Drupal 8.3.0, will be removed in Drupal 9.0.0.
328    *
329    * @see https://www.drupal.org/node/2831499
330    */
331   private function fixTableNames(array &$displays) {
332     // Fix wrong table names for entity revision metadata fields.
333     foreach ($displays as $display => $display_data) {
334       if (isset($display_data['display_options']['fields'])) {
335         foreach ($display_data['display_options']['fields'] as $property_name => $property_data) {
336           if (isset($property_data['entity_type']) && isset($property_data['field']) && isset($property_data['table'])) {
337             $entity_type = $this->entityTypeManager()->getDefinition($property_data['entity_type']);
338             // We need to update the table name only for revisionable entity
339             // types, otherwise the view is already using the correct table.
340             if (($entity_type instanceof ContentEntityTypeInterface) && is_subclass_of($entity_type->getClass(), FieldableEntityInterface::class) && $entity_type->isRevisionable()) {
341               $revision_metadata_fields = $entity_type->getRevisionMetadataKeys();
342               // @see \Drupal\Core\Entity\Sql\SqlContentEntityStorage::initTableLayout()
343               $revision_table = $entity_type->getRevisionTable() ?: $entity_type->id() . '_revision';
344
345               // Check if this is a revision metadata field and if it uses the
346               // wrong table.
347               if (in_array($property_data['field'], $revision_metadata_fields) && $property_data['table'] != $revision_table) {
348                 $displays[$display]['display_options']['fields'][$property_name]['table'] = $revision_table;
349               }
350             }
351           }
352         }
353       }
354     }
355   }
356
357   /**
358    * Fills in the cache metadata of this view.
359    *
360    * Cache metadata is set per view and per display, and ends up being stored in
361    * the view's configuration. This allows Views to determine very efficiently:
362    * - the max-age
363    * - the cache contexts
364    * - the cache tags
365    *
366    * In other words: this allows us to do the (expensive) work of initializing
367    * Views plugins and handlers to determine their effect on the cacheability of
368    * a view at save time rather than at runtime.
369    */
370   protected function addCacheMetadata() {
371     $executable = $this->getExecutable();
372
373     $current_display = $executable->current_display;
374     $displays = $this->get('display');
375     foreach (array_keys($displays) as $display_id) {
376       $display =& $this->getDisplay($display_id);
377       $executable->setDisplay($display_id);
378
379       $cache_metadata = $executable->getDisplay()->calculateCacheMetadata();
380       $display['cache_metadata']['max-age'] = $cache_metadata->getCacheMaxAge();
381       $display['cache_metadata']['contexts'] = $cache_metadata->getCacheContexts();
382       $display['cache_metadata']['tags'] = $cache_metadata->getCacheTags();
383       // Always include at least the 'languages:' context as there will most
384       // probably be translatable strings in the view output.
385       $display['cache_metadata']['contexts'] = Cache::mergeContexts($display['cache_metadata']['contexts'], ['languages:' . LanguageInterface::TYPE_INTERFACE]);
386     }
387     // Restore the previous active display.
388     $executable->setDisplay($current_display);
389   }
390
391   /**
392    * {@inheritdoc}
393    */
394   public function postSave(EntityStorageInterface $storage, $update = TRUE) {
395     parent::postSave($storage, $update);
396
397     // @todo Remove if views implements a view_builder controller.
398     views_invalidate_cache();
399     $this->invalidateCaches();
400
401     // Rebuild the router if this is a new view, or its status changed.
402     if (!isset($this->original) || ($this->status() != $this->original->status())) {
403       \Drupal::service('router.builder')->setRebuildNeeded();
404     }
405   }
406
407   /**
408    * {@inheritdoc}
409    */
410   public static function postLoad(EntityStorageInterface $storage, array &$entities) {
411     parent::postLoad($storage, $entities);
412     foreach ($entities as $entity) {
413       $entity->mergeDefaultDisplaysOptions();
414     }
415   }
416
417   /**
418    * {@inheritdoc}
419    */
420   public static function preCreate(EntityStorageInterface $storage, array &$values) {
421     parent::preCreate($storage, $values);
422
423     // If there is no information about displays available add at least the
424     // default display.
425     $values += [
426       'display' => [
427         'default' => [
428           'display_plugin' => 'default',
429           'id' => 'default',
430           'display_title' => 'Master',
431           'position' => 0,
432           'display_options' => [],
433         ],
434       ],
435     ];
436   }
437
438   /**
439    * {@inheritdoc}
440    */
441   public function postCreate(EntityStorageInterface $storage) {
442     parent::postCreate($storage);
443
444     $this->mergeDefaultDisplaysOptions();
445   }
446
447   /**
448    * {@inheritdoc}
449    */
450   public static function preDelete(EntityStorageInterface $storage, array $entities) {
451     parent::preDelete($storage, $entities);
452
453     // Call the remove() hook on the individual displays.
454     /** @var \Drupal\views\ViewEntityInterface $entity */
455     foreach ($entities as $entity) {
456       $executable = Views::executableFactory()->get($entity);
457       foreach ($entity->get('display') as $display_id => $display) {
458         $executable->setDisplay($display_id);
459         $executable->getDisplay()->remove();
460       }
461     }
462   }
463
464   /**
465    * {@inheritdoc}
466    */
467   public static function postDelete(EntityStorageInterface $storage, array $entities) {
468     parent::postDelete($storage, $entities);
469
470     $tempstore = \Drupal::service('tempstore.shared')->get('views');
471     foreach ($entities as $entity) {
472       $tempstore->delete($entity->id());
473     }
474   }
475
476   /**
477    * {@inheritdoc}
478    */
479   public function mergeDefaultDisplaysOptions() {
480     $displays = [];
481     foreach ($this->get('display') as $key => $options) {
482       $options += [
483         'display_options' => [],
484         'display_plugin' => NULL,
485         'id' => NULL,
486         'display_title' => '',
487         'position' => NULL,
488       ];
489       // Add the defaults for the display.
490       $displays[$key] = $options;
491     }
492     $this->set('display', $displays);
493   }
494
495   /**
496    * {@inheritdoc}
497    */
498   public function isInstallable() {
499     $table_definition = \Drupal::service('views.views_data')->get($this->base_table);
500     // Check whether the base table definition exists and contains a base table
501     // definition. For example, taxonomy_views_data_alter() defines
502     // node_field_data even if it doesn't exist as a base table.
503     return $table_definition && isset($table_definition['table']['base']);
504   }
505
506   /**
507    * {@inheritdoc}
508    */
509   public function __sleep() {
510     $keys = parent::__sleep();
511     unset($keys[array_search('executable', $keys)]);
512     return $keys;
513   }
514
515   /**
516    * Invalidates cache tags.
517    */
518   public function invalidateCaches() {
519     // Invalidate cache tags for cached rows.
520     $tags = $this->getCacheTags();
521     \Drupal::service('cache_tags.invalidator')->invalidateTags($tags);
522   }
523
524   /**
525    * {@inheritdoc}
526    */
527   public function onDependencyRemoval(array $dependencies) {
528     $changed = FALSE;
529
530     // Don't intervene if the views module is removed.
531     if (isset($dependencies['module']) && in_array('views', $dependencies['module'])) {
532       return FALSE;
533     }
534
535     // If the base table for the View is provided by a module being removed, we
536     // delete the View because this is not something that can be fixed manually.
537     $views_data = Views::viewsData();
538     $base_table = $this->get('base_table');
539     $base_table_data = $views_data->get($base_table);
540     if (!empty($base_table_data['table']['provider']) && in_array($base_table_data['table']['provider'], $dependencies['module'])) {
541       return FALSE;
542     }
543
544     $current_display = $this->getExecutable()->current_display;
545     $handler_types = Views::getHandlerTypes();
546
547     // Find all the handlers and check whether they want to do something on
548     // dependency removal.
549     foreach ($this->display as $display_id => $display_plugin_base) {
550       $this->getExecutable()->setDisplay($display_id);
551       $display = $this->getExecutable()->getDisplay();
552
553       foreach (array_keys($handler_types) as $handler_type) {
554         $handlers = $display->getHandlers($handler_type);
555         foreach ($handlers as $handler_id => $handler) {
556           if ($handler instanceof DependentWithRemovalPluginInterface) {
557             if ($handler->onDependencyRemoval($dependencies)) {
558               // Remove the handler and indicate we made changes.
559               unset($this->display[$display_id]['display_options'][$handler_types[$handler_type]['plural']][$handler_id]);
560               $changed = TRUE;
561             }
562           }
563         }
564       }
565     }
566
567     // Disable the View if we made changes.
568     // @todo https://www.drupal.org/node/2832558 Give better feedback for
569     // disabled config.
570     if ($changed) {
571       // Force a recalculation of the dependencies if we made changes.
572       $this->getExecutable()->current_display = NULL;
573       $this->calculateDependencies();
574       $this->disable();
575     }
576
577     $this->getExecutable()->setDisplay($current_display);
578     return $changed;
579   }
580
581 }