View.php 16.6 KB
Newer Older
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 18
 * Defines a View configuration entity class.
 *
19
 * @ConfigEntityType(
20
 *   id = "view",
21
 *   label = @Translation("View", context = "View entity type"),
22
 *   handlers = {
23
 *     "access" = "Drupal\views\ViewAccessControlHandler"
24
 *   },
25
 *   admin_permission = "administer views",
26
 *   entity_keys = {
27
 *     "id" = "id",
28
 *     "label" = "label",
29
 *     "status" = "status"
30 31 32 33 34 35 36 37 38 39 40
 *   },
 *   config_export = {
 *     "id",
 *     "label",
 *     "module",
 *     "description",
 *     "tag",
 *     "base_table",
 *     "base_field",
 *     "core",
 *     "display",
41 42
 *   }
 * )
43
 */
44
class View extends ConfigEntityBase implements ViewEntityInterface {
45

46 47 48 49 50
  /**
   * The name of the base table this view will use.
   *
   * @var string
   */
51
  protected $base_table = 'node';
52 53

  /**
54
   * The unique ID of the view.
55 56 57
   *
   * @var string
   */
58
  protected $id = NULL;
59

60 61 62 63 64
  /**
   * The label of the view.
   */
  protected $label;

65 66 67 68 69
  /**
   * The description of the view, which is used only in the interface.
   *
   * @var string
   */
70
  protected $description = '';
71 72 73 74 75 76 77 78 79

  /**
   * The "tags" of a view.
   *
   * The tags are stored as a single string, though it is used as multiple tags
   * for example in the views overview.
   *
   * @var string
   */
80
  protected $tag = '';
81 82 83 84

  /**
   * The core version the view was created for.
   *
85
   * @var string
86
   */
87
  protected $core = \Drupal::CORE_COMPATIBILITY;
88 89 90 91 92 93 94 95 96

  /**
   * Stores all display handlers of this view.
   *
   * An array containing Drupal\views\Plugin\views\display\DisplayPluginBase
   * objects.
   *
   * @var array
   */
97
  protected $display = [];
98 99 100 101 102 103

  /**
   * The name of the base field to use.
   *
   * @var string
   */
104
  protected $base_field = 'nid';
105

106
  /**
107
   * Stores a reference to the executable version of this view.
108
   *
109
   * @var \Drupal\views\ViewExecutable
110
   */
111
  protected $executable;
112

113 114 115 116 117
  /**
   * The module implementing this view.
   *
   * @var string
   */
118
  protected $module = 'views';
119

120
  /**
121
   * {@inheritdoc}
122
   */
123
  public function getExecutable() {
124
    // Ensure that an executable View is available.
125 126
    if (!isset($this->executable)) {
      $this->executable = Views::executableFactory()->get($this);
127 128
    }

129
    return $this->executable;
130 131
  }

132
  /**
133
   * {@inheritdoc}
134 135 136 137 138 139 140
   */
  public function createDuplicate() {
    $duplicate = parent::createDuplicate();
    unset($duplicate->executable);
    return $duplicate;
  }

141
  /**
142
   * {@inheritdoc}
143
   */
144
  public function label() {
145 146
    if (!$label = $this->get('label')) {
      $label = $this->id();
147
    }
148
    return $label;
149 150
  }

151
  /**
152
   * {@inheritdoc}
153
   */
154
  public function addDisplay($plugin_id = 'page', $title = NULL, $id = NULL) {
155
    if (empty($plugin_id)) {
156 157 158
      return FALSE;
    }

159
    $plugin = Views::pluginManager('display')->getDefinition($plugin_id);
160

161 162 163 164 165
    if (empty($plugin)) {
      $plugin['title'] = t('Broken');
    }

    if (empty($id)) {
166
      $id = $this->generateDisplayId($plugin_id);
167

168 169
      // Generate a unique human-readable name by inspecting the counter at the
      // end of the previous display ID, e.g., 'page_1'.
170 171 172 173 174 175 176 177 178
      if ($id !== 'default') {
        preg_match("/[0-9]+/", $id, $count);
        $count = $count[0];
      }
      else {
        $count = '';
      }

      if (empty($title)) {
179 180 181
        // If there is no title provided, use the plugin title, and if there are
        // multiple displays, append the count.
        $title = $plugin['title'];
182
        if ($count > 1) {
183
          $title .= ' ' . $count;
184 185 186 187
        }
      }
    }

188
    $display_options = [
189
      'display_plugin' => $plugin_id,
190
      'id' => $id,
191
      // Cast the display title to a string since it is an object.
192
      // @see \Drupal\Core\StringTranslation\TranslatableMarkup
193
      'display_title' => (string) $title,
194
      'position' => $id === 'default' ? 0 : count($this->display),
195 196
      'display_options' => [],
    ];
197

198 199
    // Add the display options to the view.
    $this->display[$id] = $display_options;
200 201 202 203
    return $id;
  }

  /**
204
   * Generates a display ID of a certain plugin type.
205
   *
206 207
   * @param string $plugin_id
   *   Which plugin should be used for the new display ID.
208 209
   *
   * @return string
210
   */
211
  protected function generateDisplayId($plugin_id) {
212 213
    // 'default' is singular and is unique, so just go with 'default'
    // for it. For all others, start counting.
214
    if ($plugin_id == 'default') {
215 216
      return 'default';
    }
217 218
    // Initial ID.
    $id = $plugin_id . '_1';
219 220 221 222 223
    $count = 1;

    // Loop through IDs based upon our style plugin name until
    // we find one that is unused.
    while (!empty($this->display[$id])) {
224
      $id = $plugin_id . '_' . ++$count;
225 226 227 228 229
    }

    return $id;
  }

230
  /**
231
   * {@inheritdoc}
232 233 234 235 236
   */
  public function &getDisplay($display_id) {
    return $this->display[$display_id];
  }

237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268
  /**
   * {@inheritdoc}
   */
  public function duplicateDisplayAsType($old_display_id, $new_display_type) {
    $executable = $this->getExecutable();
    $display = $executable->newDisplay($new_display_type);
    $new_display_id = $display->display['id'];
    $displays = $this->get('display');

    // Let the display title be generated by the addDisplay method and set the
    // right display plugin, but keep the rest from the original display.
    $display_duplicate = $displays[$old_display_id];
    unset($display_duplicate['display_title']);
    unset($display_duplicate['display_plugin']);

    $displays[$new_display_id] = NestedArray::mergeDeep($displays[$new_display_id], $display_duplicate);
    $displays[$new_display_id]['id'] = $new_display_id;

    // First set the displays.
    $this->set('display', $displays);

    // Ensure that we just copy display options, which are provided by the new
    // display plugin.
    $executable->setDisplay($new_display_id);

    $executable->display_handler->filterByDefinedOptions($displays[$new_display_id]['display_options']);
    // Update the display settings.
    $this->set('display', $displays);

    return $new_display_id;
  }

269 270 271 272 273 274 275 276 277
  /**
   * {@inheritdoc}
   */
  public function calculateDependencies() {
    parent::calculateDependencies();

    // Ensure that the view is dependant on the module that implements the view.
    $this->addDependency('module', $this->module);

278 279
    $executable = $this->getExecutable();
    $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 292 293 294 295
  /**
   * {@inheritdoc}
   */
  public function preSave(EntityStorageInterface $storage) {
    parent::preSave($storage);

296 297 298 299
    $displays = $this->get('display');

    $this->fixTableNames($displays);

300
    // Sort the displays.
301 302
    ksort($displays);
    $this->set('display', ['default' => $displays['default']] + $displays);
303

304 305 306 307 308 309
    // @todo Check whether isSyncing is needed.
    if (!$this->isSyncing()) {
      $this->addCacheMetadata();
    }
  }

310 311 312 313 314 315 316 317 318 319 320 321 322 323 324 325 326 327 328 329 330 331 332 333 334 335 336 337 338 339 340 341 342 343 344 345 346 347 348
  /**
   * Fixes table names for revision metadata fields of revisionable entities.
   *
   * Views for revisionable entity types using revision metadata fields might
   * be using the wrong table to retrieve the fields after system_update_8300
   * has moved them correctly to the revision table. This method updates the
   * views to use the correct tables.
   *
   * @param array &$displays
   *   An array containing display handlers of a view.
   *
   * @deprecated in Drupal 8.3.0, will be removed in Drupal 9.0.0.
   */
  private function fixTableNames(array &$displays) {
    // Fix wrong table names for entity revision metadata fields.
    foreach ($displays as $display => $display_data) {
      if (isset($display_data['display_options']['fields'])) {
        foreach ($display_data['display_options']['fields'] as $property_name => $property_data) {
          if (isset($property_data['entity_type']) && isset($property_data['field']) && isset($property_data['table'])) {
            $entity_type = $this->entityTypeManager()->getDefinition($property_data['entity_type']);
            // We need to update the table name only for revisionable entity
            // types, otherwise the view is already using the correct table.
            if (($entity_type instanceof ContentEntityTypeInterface) && is_subclass_of($entity_type->getClass(), FieldableEntityInterface::class) && $entity_type->isRevisionable()) {
              $revision_metadata_fields = $entity_type->getRevisionMetadataKeys();
              // @see \Drupal\Core\Entity\Sql\SqlContentEntityStorage::initTableLayout()
              $revision_table = $entity_type->getRevisionTable() ?: $entity_type->id() . '_revision';

              // Check if this is a revision metadata field and if it uses the
              // wrong table.
              if (in_array($property_data['field'], $revision_metadata_fields) && $property_data['table'] != $revision_table) {
                $displays[$display]['display_options']['fields'][$property_name]['table'] = $revision_table;
              }
            }
          }
        }
      }
    }
  }

349 350 351 352 353
  /**
   * Fills in the cache metadata of this view.
   *
   * Cache metadata is set per view and per display, and ends up being stored in
   * the view's configuration. This allows Views to determine very efficiently:
354 355 356
   * - the max-age
   * - the cache contexts
   * - the cache tags
357 358 359 360 361 362 363 364 365 366
   *
   * In other words: this allows us to do the (expensive) work of initializing
   * Views plugins and handlers to determine their effect on the cacheability of
   * a view at save time rather than at runtime.
   */
  protected function addCacheMetadata() {
    $executable = $this->getExecutable();

    $current_display = $executable->current_display;
    $displays = $this->get('display');
367 368
    foreach (array_keys($displays) as $display_id) {
      $display =& $this->getDisplay($display_id);
369 370
      $executable->setDisplay($display_id);

371 372 373 374
      $cache_metadata = $executable->getDisplay()->calculateCacheMetadata();
      $display['cache_metadata']['max-age'] = $cache_metadata->getCacheMaxAge();
      $display['cache_metadata']['contexts'] = $cache_metadata->getCacheContexts();
      $display['cache_metadata']['tags'] = $cache_metadata->getCacheTags();
375
      // Always include at least the 'languages:' context as there will most
376
      // probably be translatable strings in the view output.
377
      $display['cache_metadata']['contexts'] = Cache::mergeContexts($display['cache_metadata']['contexts'], ['languages:' . LanguageInterface::TYPE_INTERFACE]);
378 379 380 381 382
    }
    // Restore the previous active display.
    $executable->setDisplay($current_display);
  }

383 384 385
  /**
   * {@inheritdoc}
   */
386 387
  public function postSave(EntityStorageInterface $storage, $update = TRUE) {
    parent::postSave($storage, $update);
388

389
    // @todo Remove if views implements a view_builder controller.
390
    views_invalidate_cache();
391
    $this->invalidateCaches();
392

393
    // Rebuild the router if this is a new view, or it's status changed.
394
    if (!isset($this->original) || ($this->status() != $this->original->status())) {
395
      \Drupal::service('router.builder')->setRebuildNeeded();
396
    }
397 398 399 400
  }

  /**
   * {@inheritdoc}
401
   */
402 403
  public static function postLoad(EntityStorageInterface $storage, array &$entities) {
    parent::postLoad($storage, $entities);
404 405 406 407 408 409 410
    foreach ($entities as $entity) {
      $entity->mergeDefaultDisplaysOptions();
    }
  }

  /**
   * {@inheritdoc}
411
   */
412 413
  public static function preCreate(EntityStorageInterface $storage, array &$values) {
    parent::preCreate($storage, $values);
414

415 416
    // If there is no information about displays available add at least the
    // default display.
417 418 419
    $values += [
      'display' => [
        'default' => [
420 421 422 423
          'display_plugin' => 'default',
          'id' => 'default',
          'display_title' => 'Master',
          'position' => 0,
424 425 426 427
          'display_options' => [],
        ],
      ]
    ];
428 429 430 431 432
  }

  /**
   * {@inheritdoc}
   */
433 434
  public function postCreate(EntityStorageInterface $storage) {
    parent::postCreate($storage);
435

436 437 438
    $this->mergeDefaultDisplaysOptions();
  }

439 440 441 442 443 444 445
  /**
   * {@inheritdoc}
   */
  public static function preDelete(EntityStorageInterface $storage, array $entities) {
    parent::preDelete($storage, $entities);

    // Call the remove() hook on the individual displays.
446
    /** @var \Drupal\views\ViewEntityInterface $entity */
447 448 449 450 451 452 453 454 455
    foreach ($entities as $entity) {
      $executable = Views::executableFactory()->get($entity);
      foreach ($entity->get('display') as $display_id => $display) {
        $executable->setDisplay($display_id);
        $executable->getDisplay()->remove();
      }
    }
  }

456 457 458
  /**
   * {@inheritdoc}
   */
459 460
  public static function postDelete(EntityStorageInterface $storage, array $entities) {
    parent::postDelete($storage, $entities);
461

462
    $tempstore = \Drupal::service('user.shared_tempstore')->get('views');
463
    foreach ($entities as $entity) {
464
      $tempstore->delete($entity->id());
465 466 467
    }
  }

468 469 470 471
  /**
   * {@inheritdoc}
   */
  public function mergeDefaultDisplaysOptions() {
472
    $displays = [];
473
    foreach ($this->get('display') as $key => $options) {
474 475
      $options += [
        'display_options' => [],
476 477 478 479
        'display_plugin' => NULL,
        'id' => NULL,
        'display_title' => '',
        'position' => NULL,
480
      ];
481 482 483 484 485
      // Add the defaults for the display.
      $displays[$key] = $options;
    }
    $this->set('display', $displays);
  }
486 487 488 489 490

  /**
   * {@inheritdoc}
   */
  public function isInstallable() {
491 492 493 494 495
    $table_definition = \Drupal::service('views.views_data')->get($this->base_table);
    // Check whether the base table definition exists and contains a base table
    // definition. For example, taxonomy_views_data_alter() defines
    // node_field_data even if it doesn't exist as a base table.
    return $table_definition && isset($table_definition['table']['base']);
496 497
  }

498 499 500 501 502 503 504 505 506
  /**
   * {@inheritdoc}
   */
  public function __sleep() {
    $keys = parent::__sleep();
    unset($keys[array_search('executable', $keys)]);
    return $keys;
  }

507 508 509 510 511 512 513 514 515
  /**
   * Invalidates cache tags.
   */
  public function invalidateCaches() {
    // Invalidate cache tags for cached rows.
    $tags = $this->getCacheTags();
    \Drupal::service('cache_tags.invalidator')->invalidateTags($tags);
  }

516 517 518 519 520 521 522 523 524 525 526 527 528 529 530 531 532 533 534 535 536 537 538 539 540 541 542 543 544 545 546 547 548 549 550 551 552 553 554 555 556 557 558 559 560 561 562 563 564 565 566 567 568 569 570 571 572
  /**
   * {@inheritdoc}
   */
  public function onDependencyRemoval(array $dependencies) {
    $changed = FALSE;

    // Don't intervene if the views module is removed.
    if (isset($dependencies['module']) && in_array('views', $dependencies['module'])) {
      return FALSE;
    }

    // If the base table for the View is provided by a module being removed, we
    // delete the View because this is not something that can be fixed manually.
    $views_data = Views::viewsData();
    $base_table = $this->get('base_table');
    $base_table_data = $views_data->get($base_table);
    if (!empty($base_table_data['table']['provider']) && in_array($base_table_data['table']['provider'], $dependencies['module'])) {
      return FALSE;
    }

    $current_display = $this->getExecutable()->current_display;
    $handler_types = Views::getHandlerTypes();

    // Find all the handlers and check whether they want to do something on
    // dependency removal.
    foreach ($this->display as $display_id => $display_plugin_base) {
      $this->getExecutable()->setDisplay($display_id);
      $display = $this->getExecutable()->getDisplay();

      foreach (array_keys($handler_types) as $handler_type) {
        $handlers = $display->getHandlers($handler_type);
        foreach ($handlers as $handler_id => $handler) {
          if ($handler instanceof DependentWithRemovalPluginInterface) {
            if ($handler->onDependencyRemoval($dependencies)) {
              // Remove the handler and indicate we made changes.
              unset($this->display[$display_id]['display_options'][$handler_types[$handler_type]['plural']][$handler_id]);
              $changed = TRUE;
            }
          }
        }
      }
    }

    // Disable the View if we made changes.
    // @todo https://www.drupal.org/node/2832558 Give better feedback for
    // disabled config.
    if ($changed) {
      // Force a recalculation of the dependencies if we made changes.
      $this->getExecutable()->current_display = NULL;
      $this->calculateDependencies();
      $this->disable();
    }

    $this->getExecutable()->setDisplay($current_display);
    return $changed;
  }

573
}