ViewsEntitySchemaSubscriber.php

Same filename in other branches
  1. 9 core/modules/views/src/EventSubscriber/ViewsEntitySchemaSubscriber.php
  2. 8.9.x core/modules/views/src/EventSubscriber/ViewsEntitySchemaSubscriber.php
  3. 11.x core/modules/views/src/EventSubscriber/ViewsEntitySchemaSubscriber.php

Namespace

Drupal\views\EventSubscriber

File

core/modules/views/src/EventSubscriber/ViewsEntitySchemaSubscriber.php

View source
<?php

namespace Drupal\views\EventSubscriber;

use Drupal\Core\Entity\EntityTypeEventSubscriberTrait;
use Drupal\Core\Entity\EntityTypeInterface;
use Drupal\Core\Entity\EntityTypeListenerInterface;
use Drupal\Core\Entity\EntityTypeManagerInterface;
use Drupal\Core\Entity\Sql\SqlContentEntityStorage;
use Drupal\views\ViewEntityInterface;
use Drupal\views\Views;
use Psr\Log\LoggerInterface;
use Symfony\Component\EventDispatcher\EventSubscriberInterface;

/**
 * Reacts to changes on entity types to update all views entities.
 */
class ViewsEntitySchemaSubscriber implements EntityTypeListenerInterface, EventSubscriberInterface {
    use EntityTypeEventSubscriberTrait;
    
    /**
     * Indicates that a base table got renamed.
     */
    const BASE_TABLE_RENAME = 0;
    
    /**
     * Indicates that a data table got renamed.
     */
    const DATA_TABLE_RENAME = 1;
    
    /**
     * Indicates that a data table got added.
     */
    const DATA_TABLE_ADDITION = 2;
    
    /**
     * Indicates that a data table got removed.
     */
    const DATA_TABLE_REMOVAL = 3;
    
    /**
     * Indicates that a revision table got renamed.
     */
    const REVISION_TABLE_RENAME = 4;
    
    /**
     * Indicates that a revision table got added.
     */
    const REVISION_TABLE_ADDITION = 5;
    
    /**
     * Indicates that a revision table got removed.
     */
    const REVISION_TABLE_REMOVAL = 6;
    
    /**
     * Indicates that a revision data table got renamed.
     */
    const REVISION_DATA_TABLE_RENAME = 7;
    
    /**
     * Indicates that a revision data table got added.
     */
    const REVISION_DATA_TABLE_ADDITION = 8;
    
    /**
     * Indicates that a revision data table got removed.
     */
    const REVISION_DATA_TABLE_REMOVAL = 9;
    
    /**
     * The entity type manager.
     *
     * @var \Drupal\Core\Entity\EntityTypeManagerInterface
     */
    protected $entityTypeManager;
    
    /**
     * The default logger service.
     *
     * @var \Psr\Log\LoggerInterface
     */
    protected $logger;
    
    /**
     * Array of views that need to be saved, indexed by view name.
     *
     * @var \Drupal\views\ViewEntityInterface[]
     */
    protected $viewsToSave = [];
    
    /**
     * Constructs a ViewsEntitySchemaSubscriber.
     *
     * @param \Drupal\Core\Entity\EntityTypeManagerInterface $entity_type_manager
     *   The entity type manager.
     * @param \Psr\Log\LoggerInterface $logger
     *   A logger instance.
     */
    public function __construct(EntityTypeManagerInterface $entity_type_manager, LoggerInterface $logger) {
        $this->entityTypeManager = $entity_type_manager;
        $this->logger = $logger;
    }
    
    /**
     * {@inheritdoc}
     */
    public static function getSubscribedEvents() : array {
        return static::getEntityTypeEvents();
    }
    
    /**
     * {@inheritdoc}
     */
    public function onEntityTypeUpdate(EntityTypeInterface $entity_type, EntityTypeInterface $original) {
        $changes = [];
        // We implement a specific logic for table updates, which is bound to the
        // default sql content entity storage.
        if (!$this->entityTypeManager
            ->getStorage($entity_type->id()) instanceof SqlContentEntityStorage) {
            return;
        }
        if ($entity_type->getBaseTable() != $original->getBaseTable()) {
            $changes[] = static::BASE_TABLE_RENAME;
        }
        $revision_add = $entity_type->isRevisionable() && !$original->isRevisionable();
        $revision_remove = !$entity_type->isRevisionable() && $original->isRevisionable();
        $translation_add = $entity_type->isTranslatable() && !$original->isTranslatable();
        $translation_remove = !$entity_type->isTranslatable() && $original->isTranslatable();
        if ($revision_add) {
            $changes[] = static::REVISION_TABLE_ADDITION;
        }
        elseif ($revision_remove) {
            $changes[] = static::REVISION_TABLE_REMOVAL;
        }
        elseif ($entity_type->isRevisionable() && $entity_type->getRevisionTable() != $original->getRevisionTable()) {
            $changes[] = static::REVISION_TABLE_RENAME;
        }
        if ($translation_add) {
            $changes[] = static::DATA_TABLE_ADDITION;
        }
        elseif ($translation_remove) {
            $changes[] = static::DATA_TABLE_REMOVAL;
        }
        elseif ($entity_type->isTranslatable() && $entity_type->getDataTable() != $original->getDataTable()) {
            $changes[] = static::DATA_TABLE_RENAME;
        }
        if ($entity_type->isRevisionable() && $entity_type->isTranslatable()) {
            if ($revision_add || $translation_add) {
                $changes[] = static::REVISION_DATA_TABLE_ADDITION;
            }
            elseif ($entity_type->getRevisionDataTable() != $original->getRevisionDataTable()) {
                $changes[] = static::REVISION_DATA_TABLE_RENAME;
            }
        }
        elseif ($original->isRevisionable() && $original->isTranslatable() && ($revision_remove || $translation_remove)) {
            $changes[] = static::REVISION_DATA_TABLE_REMOVAL;
        }
        // Stop here if no changes are needed.
        if (empty($changes)) {
            return;
        }
        
        /** @var \Drupal\views\Entity\View[] $all_views */
        $all_views = $this->entityTypeManager
            ->getStorage('view')
            ->loadMultiple(NULL);
        foreach ($changes as $change) {
            switch ($change) {
                case static::BASE_TABLE_RENAME:
                    $this->baseTableRename($all_views, $entity_type->id(), $original->getBaseTable(), $entity_type->getBaseTable());
                    break;
                case static::DATA_TABLE_RENAME:
                    $this->dataTableRename($all_views, $entity_type->id(), $original->getDataTable(), $entity_type->getDataTable());
                    break;
                case static::DATA_TABLE_ADDITION:
                    $this->dataTableAddition($all_views, $entity_type, $entity_type->getDataTable(), $entity_type->getBaseTable());
                    break;
                case static::DATA_TABLE_REMOVAL:
                    $this->dataTableRemoval($all_views, $entity_type->id(), $original->getDataTable(), $entity_type->getBaseTable());
                    break;
                case static::REVISION_TABLE_RENAME:
                    $this->baseTableRename($all_views, $entity_type->id(), $original->getRevisionTable(), $entity_type->getRevisionTable());
                    break;
                case static::REVISION_TABLE_ADDITION:
                    // If we add revision support we don't have to do anything.
                    break;
                case static::REVISION_TABLE_REMOVAL:
                    $this->revisionRemoval($all_views, $original);
                    break;
                case static::REVISION_DATA_TABLE_RENAME:
                    $this->dataTableRename($all_views, $entity_type->id(), $original->getRevisionDataTable(), $entity_type->getRevisionDataTable());
                    break;
                case static::REVISION_DATA_TABLE_ADDITION:
                    $this->dataTableAddition($all_views, $entity_type, $entity_type->getRevisionDataTable(), $entity_type->getRevisionTable());
                    break;
                case static::REVISION_DATA_TABLE_REMOVAL:
                    $this->dataTableRemoval($all_views, $entity_type->id(), $original->getRevisionDataTable(), $entity_type->getRevisionTable());
                    break;
            }
        }
        foreach ($this->viewsToSave as $view) {
            try {
                // All changes done to the views here can be trusted and this might be
                // called during updates, when it is not safe to rely on configuration
                // containing valid schema. Trust the data and disable schema validation
                // and casting.
                $view->trustData()
                    ->save();
            } catch (\Exception $e) {
                // In case the view could not be saved, log an error message that the
                // view needs to be updated manually instead of failing the entire
                // entity update process.
                $this->logger
                    ->critical("The %view_id view could not be updated automatically while processing an entity schema update for the %entity_type_id entity type.", [
                    '%view_id' => $view->id(),
                    '%entity_type_id' => $entity_type->id(),
                ]);
            }
        }
        $this->viewsToSave = [];
    }
    
    /**
     * {@inheritdoc}
     */
    public function onEntityTypeDelete(EntityTypeInterface $entity_type) {
        $tables = [
            $entity_type->getBaseTable(),
            $entity_type->getDataTable(),
            $entity_type->getRevisionTable(),
            $entity_type->getRevisionDataTable(),
        ];
        $all_views = $this->entityTypeManager
            ->getStorage('view')
            ->loadMultiple(NULL);
        
        /** @var \Drupal\views\Entity\View $view */
        foreach ($all_views as $view) {
            // First check just the base table.
            if (in_array($view->get('base_table'), $tables)) {
                $view->disable();
                $view->save();
            }
        }
    }
    
    /**
     * Applies a callable onto all handlers of all passed in views.
     *
     * @param \Drupal\views\Entity\View[] $all_views
     *   All views entities.
     * @param callable $process
     *   A callable which retrieves a handler config array.
     */
    protected function processHandlers(array $all_views, callable $process) {
        foreach ($all_views as $view) {
            foreach (array_keys($view->get('display')) as $display_id) {
                $display =& $view->getDisplay($display_id);
                foreach (Views::getHandlerTypes() as $handler_type) {
                    $handler_type = $handler_type['plural'];
                    if (!isset($display['display_options'][$handler_type])) {
                        continue;
                    }
                    foreach ($display['display_options'][$handler_type] as $id => &$handler_config) {
                        $process($handler_config, $view);
                        if ($handler_config === NULL) {
                            unset($display['display_options'][$handler_type][$id]);
                        }
                    }
                }
            }
        }
    }
    
    /**
     * Updates views if a base table is renamed.
     *
     * @param \Drupal\views\Entity\View[] $all_views
     *   All views.
     * @param string $entity_type_id
     *   The entity type ID.
     * @param string $old_base_table
     *   The old base table name.
     * @param string $new_base_table
     *   The new base table name.
     */
    protected function baseTableRename($all_views, $entity_type_id, $old_base_table, $new_base_table) {
        foreach ($all_views as $view) {
            if ($view->get('base_table') == $old_base_table) {
                $view->set('base_table', $new_base_table);
                $this->viewsToSave[$view->id()] = $view;
            }
        }
        $this->processHandlers($all_views, function (&$handler_config, ViewEntityInterface $view) use ($entity_type_id, $old_base_table, $new_base_table) {
            if (isset($handler_config['entity_type']) && $handler_config['entity_type'] == $entity_type_id && $handler_config['table'] == $old_base_table) {
                $handler_config['table'] = $new_base_table;
                $this->viewsToSave[$view->id()] = $view;
            }
        });
    }
    
    /**
     * Updates views if a data table is renamed.
     *
     * @param \Drupal\views\Entity\View[] $all_views
     *   All views.
     * @param string $entity_type_id
     *   The entity type ID.
     * @param string $old_data_table
     *   The old data table name.
     * @param string $new_data_table
     *   The new data table name.
     */
    protected function dataTableRename($all_views, $entity_type_id, $old_data_table, $new_data_table) {
        foreach ($all_views as $view) {
            if ($view->get('base_table') == $old_data_table) {
                $view->set('base_table', $new_data_table);
                $this->viewsToSave[$view->id()] = $view;
            }
        }
        $this->processHandlers($all_views, function (&$handler_config, ViewEntityInterface $view) use ($entity_type_id, $old_data_table, $new_data_table) {
            if (isset($handler_config['entity_type']) && $handler_config['entity_type'] == $entity_type_id && $handler_config['table'] == $old_data_table) {
                $handler_config['table'] = $new_data_table;
                $this->viewsToSave[$view->id()] = $view;
            }
        });
    }
    
    /**
     * Updates views if a data table is added.
     *
     * @param \Drupal\views\Entity\View[] $all_views
     *   All views.
     * @param \Drupal\Core\Entity\EntityTypeInterface $entity_type
     *   The entity type.
     * @param string $new_data_table
     *   The new data table.
     * @param string $base_table
     *   The base table.
     */
    protected function dataTableAddition($all_views, EntityTypeInterface $entity_type, $new_data_table, $base_table) {
        
        /** @var \Drupal\Core\Entity\Sql\SqlContentEntityStorage $storage */
        $entity_type_id = $entity_type->id();
        $storage = $this->entityTypeManager
            ->getStorage($entity_type_id);
        $storage->setEntityType($entity_type);
        $table_mapping = $storage->getTableMapping();
        $data_table_fields = $table_mapping->getFieldNames($new_data_table);
        $base_table_fields = $table_mapping->getFieldNames($base_table);
        $data_table = $new_data_table;
        $this->processHandlers($all_views, function (&$handler_config, ViewEntityInterface $view) use ($entity_type_id, $base_table, $data_table, $base_table_fields, $data_table_fields) {
            if (isset($handler_config['entity_type']) && isset($handler_config['entity_field']) && $handler_config['entity_type'] == $entity_type_id) {
                // Move all fields which just exists on the data table.
                if ($handler_config['table'] == $base_table && in_array($handler_config['entity_field'], $data_table_fields) && !in_array($handler_config['entity_field'], $base_table_fields)) {
                    $handler_config['table'] = $data_table;
                    $this->viewsToSave[$view->id()] = $view;
                }
            }
        });
    }
    
    /**
     * Updates views if a data table is removed.
     *
     * @param \Drupal\views\Entity\View[] $all_views
     *   All views.
     * @param string $entity_type_id
     *   The entity type ID.
     * @param string $old_data_table
     *   The name of the previous existing data table.
     * @param string $base_table
     *   The name of the base table.
     */
    protected function dataTableRemoval($all_views, $entity_type_id, $old_data_table, $base_table) {
        // We move back the data table back to the base table.
        $this->processHandlers($all_views, function (&$handler_config, ViewEntityInterface $view) use ($entity_type_id, $old_data_table, $base_table) {
            if (isset($handler_config['entity_type']) && $handler_config['entity_type'] == $entity_type_id) {
                if ($handler_config['table'] == $old_data_table) {
                    $handler_config['table'] = $base_table;
                    $this->viewsToSave[$view->id()] = $view;
                }
            }
        });
    }
    
    /**
     * Updates views if revision support is removed.
     *
     * @param \Drupal\views\Entity\View[] $all_views
     *   All views.
     * @param \Drupal\Core\Entity\EntityTypeInterface $original
     *   The origin entity type.
     */
    protected function revisionRemoval($all_views, EntityTypeInterface $original) {
        $revision_base_table = $original->getRevisionTable();
        $revision_data_table = $original->getRevisionDataTable();
        foreach ($all_views as $view) {
            if (in_array($view->get('base_table'), [
                $revision_base_table,
                $revision_data_table,
            ])) {
                // Let's disable the views as we no longer support revisions.
                $view->setStatus(FALSE);
                $this->viewsToSave[$view->id()] = $view;
            }
            // For any kind of field, let's rely on the broken handler functionality.
        }
    }

}

Classes

Title Deprecated Summary
ViewsEntitySchemaSubscriber Reacts to changes on entity types to update all views entities.

Buggy or inaccurate documentation? Please file an issue. Need support? Need help programming? Connect with the Drupal community.