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