Chris@0: executable)) { Chris@0: $this->executable = Views::executableFactory()->get($this); Chris@0: } Chris@0: Chris@0: return $this->executable; Chris@0: } Chris@0: Chris@0: /** Chris@0: * {@inheritdoc} Chris@0: */ Chris@0: public function createDuplicate() { Chris@0: $duplicate = parent::createDuplicate(); Chris@0: unset($duplicate->executable); Chris@0: return $duplicate; Chris@0: } Chris@0: Chris@0: /** Chris@0: * {@inheritdoc} Chris@0: */ Chris@0: public function label() { Chris@0: if (!$label = $this->get('label')) { Chris@0: $label = $this->id(); Chris@0: } Chris@0: return $label; Chris@0: } Chris@0: Chris@0: /** Chris@0: * {@inheritdoc} Chris@0: */ Chris@0: public function addDisplay($plugin_id = 'page', $title = NULL, $id = NULL) { Chris@0: if (empty($plugin_id)) { Chris@0: return FALSE; Chris@0: } Chris@0: Chris@0: $plugin = Views::pluginManager('display')->getDefinition($plugin_id); Chris@0: Chris@0: if (empty($plugin)) { Chris@0: $plugin['title'] = t('Broken'); Chris@0: } Chris@0: Chris@0: if (empty($id)) { Chris@0: $id = $this->generateDisplayId($plugin_id); Chris@0: Chris@0: // Generate a unique human-readable name by inspecting the counter at the Chris@0: // end of the previous display ID, e.g., 'page_1'. Chris@0: if ($id !== 'default') { Chris@0: preg_match("/[0-9]+/", $id, $count); Chris@0: $count = $count[0]; Chris@0: } Chris@0: else { Chris@0: $count = ''; Chris@0: } Chris@0: Chris@0: if (empty($title)) { Chris@0: // If there is no title provided, use the plugin title, and if there are Chris@0: // multiple displays, append the count. Chris@0: $title = $plugin['title']; Chris@0: if ($count > 1) { Chris@0: $title .= ' ' . $count; Chris@0: } Chris@0: } Chris@0: } Chris@0: Chris@0: $display_options = [ Chris@0: 'display_plugin' => $plugin_id, Chris@0: 'id' => $id, Chris@0: // Cast the display title to a string since it is an object. Chris@0: // @see \Drupal\Core\StringTranslation\TranslatableMarkup Chris@0: 'display_title' => (string) $title, Chris@0: 'position' => $id === 'default' ? 0 : count($this->display), Chris@0: 'display_options' => [], Chris@0: ]; Chris@0: Chris@0: // Add the display options to the view. Chris@0: $this->display[$id] = $display_options; Chris@0: return $id; Chris@0: } Chris@0: Chris@0: /** Chris@0: * Generates a display ID of a certain plugin type. Chris@0: * Chris@0: * @param string $plugin_id Chris@0: * Which plugin should be used for the new display ID. Chris@0: * Chris@0: * @return string Chris@0: */ Chris@0: protected function generateDisplayId($plugin_id) { Chris@0: // 'default' is singular and is unique, so just go with 'default' Chris@0: // for it. For all others, start counting. Chris@0: if ($plugin_id == 'default') { Chris@0: return 'default'; Chris@0: } Chris@0: // Initial ID. Chris@0: $id = $plugin_id . '_1'; Chris@0: $count = 1; Chris@0: Chris@0: // Loop through IDs based upon our style plugin name until Chris@0: // we find one that is unused. Chris@0: while (!empty($this->display[$id])) { Chris@0: $id = $plugin_id . '_' . ++$count; Chris@0: } Chris@0: Chris@0: return $id; Chris@0: } Chris@0: Chris@0: /** Chris@0: * {@inheritdoc} Chris@0: */ Chris@0: public function &getDisplay($display_id) { Chris@0: return $this->display[$display_id]; Chris@0: } Chris@0: Chris@0: /** Chris@0: * {@inheritdoc} Chris@0: */ Chris@0: public function duplicateDisplayAsType($old_display_id, $new_display_type) { Chris@0: $executable = $this->getExecutable(); Chris@0: $display = $executable->newDisplay($new_display_type); Chris@0: $new_display_id = $display->display['id']; Chris@0: $displays = $this->get('display'); Chris@0: Chris@0: // Let the display title be generated by the addDisplay method and set the Chris@0: // right display plugin, but keep the rest from the original display. Chris@0: $display_duplicate = $displays[$old_display_id]; Chris@0: unset($display_duplicate['display_title']); Chris@0: unset($display_duplicate['display_plugin']); Chris@14: unset($display_duplicate['new_id']); Chris@0: Chris@0: $displays[$new_display_id] = NestedArray::mergeDeep($displays[$new_display_id], $display_duplicate); Chris@0: $displays[$new_display_id]['id'] = $new_display_id; Chris@0: Chris@0: // First set the displays. Chris@0: $this->set('display', $displays); Chris@0: Chris@0: // Ensure that we just copy display options, which are provided by the new Chris@0: // display plugin. Chris@0: $executable->setDisplay($new_display_id); Chris@0: Chris@0: $executable->display_handler->filterByDefinedOptions($displays[$new_display_id]['display_options']); Chris@0: // Update the display settings. Chris@0: $this->set('display', $displays); Chris@0: Chris@0: return $new_display_id; Chris@0: } Chris@0: Chris@0: /** Chris@0: * {@inheritdoc} Chris@0: */ Chris@0: public function calculateDependencies() { Chris@0: parent::calculateDependencies(); Chris@0: Chris@0: // Ensure that the view is dependant on the module that implements the view. Chris@0: $this->addDependency('module', $this->module); Chris@0: Chris@0: $executable = $this->getExecutable(); Chris@0: $executable->initDisplay(); Chris@0: $executable->initStyle(); Chris@0: Chris@0: foreach ($executable->displayHandlers as $display) { Chris@0: // Calculate the dependencies each display has. Chris@0: $this->calculatePluginDependencies($display); Chris@0: } Chris@0: Chris@0: return $this; Chris@0: } Chris@0: Chris@0: /** Chris@0: * {@inheritdoc} Chris@0: */ Chris@0: public function preSave(EntityStorageInterface $storage) { Chris@0: parent::preSave($storage); Chris@0: Chris@0: $displays = $this->get('display'); Chris@0: Chris@0: $this->fixTableNames($displays); Chris@0: Chris@0: // Sort the displays. Chris@0: ksort($displays); Chris@0: $this->set('display', ['default' => $displays['default']] + $displays); Chris@0: Chris@0: // @todo Check whether isSyncing is needed. Chris@0: if (!$this->isSyncing()) { Chris@0: $this->addCacheMetadata(); Chris@0: } Chris@0: } Chris@0: Chris@0: /** Chris@0: * Fixes table names for revision metadata fields of revisionable entities. Chris@0: * Chris@0: * Views for revisionable entity types using revision metadata fields might Chris@0: * be using the wrong table to retrieve the fields after system_update_8300 Chris@0: * has moved them correctly to the revision table. This method updates the Chris@0: * views to use the correct tables. Chris@0: * Chris@0: * @param array &$displays Chris@0: * An array containing display handlers of a view. Chris@0: * Chris@0: * @deprecated in Drupal 8.3.0, will be removed in Drupal 9.0.0. Chris@16: * Chris@16: * @see https://www.drupal.org/node/2831499 Chris@0: */ Chris@0: private function fixTableNames(array &$displays) { Chris@0: // Fix wrong table names for entity revision metadata fields. Chris@0: foreach ($displays as $display => $display_data) { Chris@0: if (isset($display_data['display_options']['fields'])) { Chris@0: foreach ($display_data['display_options']['fields'] as $property_name => $property_data) { Chris@0: if (isset($property_data['entity_type']) && isset($property_data['field']) && isset($property_data['table'])) { Chris@0: $entity_type = $this->entityTypeManager()->getDefinition($property_data['entity_type']); Chris@0: // We need to update the table name only for revisionable entity Chris@0: // types, otherwise the view is already using the correct table. Chris@0: if (($entity_type instanceof ContentEntityTypeInterface) && is_subclass_of($entity_type->getClass(), FieldableEntityInterface::class) && $entity_type->isRevisionable()) { Chris@0: $revision_metadata_fields = $entity_type->getRevisionMetadataKeys(); Chris@0: // @see \Drupal\Core\Entity\Sql\SqlContentEntityStorage::initTableLayout() Chris@0: $revision_table = $entity_type->getRevisionTable() ?: $entity_type->id() . '_revision'; Chris@0: Chris@0: // Check if this is a revision metadata field and if it uses the Chris@0: // wrong table. Chris@0: if (in_array($property_data['field'], $revision_metadata_fields) && $property_data['table'] != $revision_table) { Chris@0: $displays[$display]['display_options']['fields'][$property_name]['table'] = $revision_table; Chris@0: } Chris@0: } Chris@0: } Chris@0: } Chris@0: } Chris@0: } Chris@0: } Chris@0: Chris@0: /** Chris@0: * Fills in the cache metadata of this view. Chris@0: * Chris@0: * Cache metadata is set per view and per display, and ends up being stored in Chris@0: * the view's configuration. This allows Views to determine very efficiently: Chris@0: * - the max-age Chris@0: * - the cache contexts Chris@0: * - the cache tags Chris@0: * Chris@0: * In other words: this allows us to do the (expensive) work of initializing Chris@0: * Views plugins and handlers to determine their effect on the cacheability of Chris@0: * a view at save time rather than at runtime. Chris@0: */ Chris@0: protected function addCacheMetadata() { Chris@0: $executable = $this->getExecutable(); Chris@0: Chris@0: $current_display = $executable->current_display; Chris@0: $displays = $this->get('display'); Chris@0: foreach (array_keys($displays) as $display_id) { Chris@0: $display =& $this->getDisplay($display_id); Chris@0: $executable->setDisplay($display_id); Chris@0: Chris@0: $cache_metadata = $executable->getDisplay()->calculateCacheMetadata(); Chris@0: $display['cache_metadata']['max-age'] = $cache_metadata->getCacheMaxAge(); Chris@0: $display['cache_metadata']['contexts'] = $cache_metadata->getCacheContexts(); Chris@0: $display['cache_metadata']['tags'] = $cache_metadata->getCacheTags(); Chris@0: // Always include at least the 'languages:' context as there will most Chris@0: // probably be translatable strings in the view output. Chris@0: $display['cache_metadata']['contexts'] = Cache::mergeContexts($display['cache_metadata']['contexts'], ['languages:' . LanguageInterface::TYPE_INTERFACE]); Chris@0: } Chris@0: // Restore the previous active display. Chris@0: $executable->setDisplay($current_display); Chris@0: } Chris@0: Chris@0: /** Chris@0: * {@inheritdoc} Chris@0: */ Chris@0: public function postSave(EntityStorageInterface $storage, $update = TRUE) { Chris@0: parent::postSave($storage, $update); Chris@0: Chris@0: // @todo Remove if views implements a view_builder controller. Chris@0: views_invalidate_cache(); Chris@0: $this->invalidateCaches(); Chris@0: Chris@14: // Rebuild the router if this is a new view, or its status changed. Chris@0: if (!isset($this->original) || ($this->status() != $this->original->status())) { Chris@0: \Drupal::service('router.builder')->setRebuildNeeded(); Chris@0: } Chris@0: } Chris@0: Chris@0: /** Chris@0: * {@inheritdoc} Chris@0: */ Chris@0: public static function postLoad(EntityStorageInterface $storage, array &$entities) { Chris@0: parent::postLoad($storage, $entities); Chris@0: foreach ($entities as $entity) { Chris@0: $entity->mergeDefaultDisplaysOptions(); Chris@0: } Chris@0: } Chris@0: Chris@0: /** Chris@0: * {@inheritdoc} Chris@0: */ Chris@0: public static function preCreate(EntityStorageInterface $storage, array &$values) { Chris@0: parent::preCreate($storage, $values); Chris@0: Chris@0: // If there is no information about displays available add at least the Chris@0: // default display. Chris@0: $values += [ Chris@0: 'display' => [ Chris@0: 'default' => [ Chris@0: 'display_plugin' => 'default', Chris@0: 'id' => 'default', Chris@0: 'display_title' => 'Master', Chris@0: 'position' => 0, Chris@0: 'display_options' => [], Chris@0: ], Chris@17: ], Chris@0: ]; Chris@0: } Chris@0: Chris@0: /** Chris@0: * {@inheritdoc} Chris@0: */ Chris@0: public function postCreate(EntityStorageInterface $storage) { Chris@0: parent::postCreate($storage); Chris@0: Chris@0: $this->mergeDefaultDisplaysOptions(); Chris@0: } Chris@0: Chris@0: /** Chris@0: * {@inheritdoc} Chris@0: */ Chris@0: public static function preDelete(EntityStorageInterface $storage, array $entities) { Chris@0: parent::preDelete($storage, $entities); Chris@0: Chris@0: // Call the remove() hook on the individual displays. Chris@0: /** @var \Drupal\views\ViewEntityInterface $entity */ Chris@0: foreach ($entities as $entity) { Chris@0: $executable = Views::executableFactory()->get($entity); Chris@0: foreach ($entity->get('display') as $display_id => $display) { Chris@0: $executable->setDisplay($display_id); Chris@0: $executable->getDisplay()->remove(); Chris@0: } Chris@0: } Chris@0: } Chris@0: Chris@0: /** Chris@0: * {@inheritdoc} Chris@0: */ Chris@0: public static function postDelete(EntityStorageInterface $storage, array $entities) { Chris@0: parent::postDelete($storage, $entities); Chris@0: Chris@14: $tempstore = \Drupal::service('tempstore.shared')->get('views'); Chris@0: foreach ($entities as $entity) { Chris@0: $tempstore->delete($entity->id()); Chris@0: } Chris@0: } Chris@0: Chris@0: /** Chris@0: * {@inheritdoc} Chris@0: */ Chris@0: public function mergeDefaultDisplaysOptions() { Chris@0: $displays = []; Chris@0: foreach ($this->get('display') as $key => $options) { Chris@0: $options += [ Chris@0: 'display_options' => [], Chris@0: 'display_plugin' => NULL, Chris@0: 'id' => NULL, Chris@0: 'display_title' => '', Chris@0: 'position' => NULL, Chris@0: ]; Chris@0: // Add the defaults for the display. Chris@0: $displays[$key] = $options; Chris@0: } Chris@0: $this->set('display', $displays); Chris@0: } Chris@0: Chris@0: /** Chris@0: * {@inheritdoc} Chris@0: */ Chris@0: public function isInstallable() { Chris@0: $table_definition = \Drupal::service('views.views_data')->get($this->base_table); Chris@0: // Check whether the base table definition exists and contains a base table Chris@0: // definition. For example, taxonomy_views_data_alter() defines Chris@0: // node_field_data even if it doesn't exist as a base table. Chris@0: return $table_definition && isset($table_definition['table']['base']); Chris@0: } Chris@0: Chris@0: /** Chris@0: * {@inheritdoc} Chris@0: */ Chris@0: public function __sleep() { Chris@0: $keys = parent::__sleep(); Chris@0: unset($keys[array_search('executable', $keys)]); Chris@0: return $keys; Chris@0: } Chris@0: Chris@0: /** Chris@0: * Invalidates cache tags. Chris@0: */ Chris@0: public function invalidateCaches() { Chris@0: // Invalidate cache tags for cached rows. Chris@0: $tags = $this->getCacheTags(); Chris@0: \Drupal::service('cache_tags.invalidator')->invalidateTags($tags); Chris@0: } Chris@0: Chris@0: /** Chris@0: * {@inheritdoc} Chris@0: */ Chris@0: public function onDependencyRemoval(array $dependencies) { Chris@0: $changed = FALSE; Chris@0: Chris@0: // Don't intervene if the views module is removed. Chris@0: if (isset($dependencies['module']) && in_array('views', $dependencies['module'])) { Chris@0: return FALSE; Chris@0: } Chris@0: Chris@0: // If the base table for the View is provided by a module being removed, we Chris@0: // delete the View because this is not something that can be fixed manually. Chris@0: $views_data = Views::viewsData(); Chris@0: $base_table = $this->get('base_table'); Chris@0: $base_table_data = $views_data->get($base_table); Chris@0: if (!empty($base_table_data['table']['provider']) && in_array($base_table_data['table']['provider'], $dependencies['module'])) { Chris@0: return FALSE; Chris@0: } Chris@0: Chris@0: $current_display = $this->getExecutable()->current_display; Chris@0: $handler_types = Views::getHandlerTypes(); Chris@0: Chris@0: // Find all the handlers and check whether they want to do something on Chris@0: // dependency removal. Chris@0: foreach ($this->display as $display_id => $display_plugin_base) { Chris@0: $this->getExecutable()->setDisplay($display_id); Chris@0: $display = $this->getExecutable()->getDisplay(); Chris@0: Chris@0: foreach (array_keys($handler_types) as $handler_type) { Chris@0: $handlers = $display->getHandlers($handler_type); Chris@0: foreach ($handlers as $handler_id => $handler) { Chris@0: if ($handler instanceof DependentWithRemovalPluginInterface) { Chris@0: if ($handler->onDependencyRemoval($dependencies)) { Chris@0: // Remove the handler and indicate we made changes. Chris@0: unset($this->display[$display_id]['display_options'][$handler_types[$handler_type]['plural']][$handler_id]); Chris@0: $changed = TRUE; Chris@0: } Chris@0: } Chris@0: } Chris@0: } Chris@0: } Chris@0: Chris@0: // Disable the View if we made changes. Chris@0: // @todo https://www.drupal.org/node/2832558 Give better feedback for Chris@0: // disabled config. Chris@0: if ($changed) { Chris@0: // Force a recalculation of the dependencies if we made changes. Chris@0: $this->getExecutable()->current_display = NULL; Chris@0: $this->calculateDependencies(); Chris@0: $this->disable(); Chris@0: } Chris@0: Chris@0: $this->getExecutable()->setDisplay($current_display); Chris@0: return $changed; Chris@0: } Chris@0: Chris@0: }