You are here

class MultiversionManager in Multiversion 8

Same name and namespace in other branches
  1. 8.2 src/MultiversionManager.php \Drupal\multiversion\MultiversionManager

Hierarchy

Expanded class hierarchy of MultiversionManager

2 files declare their use of MultiversionManager
MenuLinkContentMigrateSubscriber.php in src/EventSubscriber/MenuLinkContentMigrateSubscriber.php
multiversion.install in ./multiversion.install
1 string reference to 'MultiversionManager'
multiversion.services.yml in ./multiversion.services.yml
multiversion.services.yml
1 service uses MultiversionManager
multiversion.manager in ./multiversion.services.yml
Drupal\multiversion\MultiversionManager

File

src/MultiversionManager.php, line 23

Namespace

Drupal\multiversion
View source
class MultiversionManager implements MultiversionManagerInterface, ContainerAwareInterface {
  use ContainerAwareTrait;
  const TO_TMP = 'to_tmp';
  const FROM_TMP = 'from_tmp';
  const OP_ENABLE = 'enable';
  const OP_DISABLE = 'disable';

  /**
   * @var \Drupal\multiversion\Workspace\WorkspaceManagerInterface
   */
  protected $workspaceManager;

  /**
   * @var \Symfony\Component\Serializer\Serializer
   */
  protected $serializer;

  /**
   * @var \Drupal\Core\Entity\EntityTypeManagerInterface
   */
  protected $entityTypeManager;

  /**
   * @var \Drupal\Core\State\StateInterface
   */
  protected $state;

  /**
   * @var \Drupal\Core\Language\LanguageManagerInterface
   */
  protected $languageManager;

  /**
   * @var \Drupal\Core\Cache\CacheBackendInterface
   */
  protected $cache;

  /**
   * The database connection.
   *
   * @var \Drupal\Core\Database\Connection
   */
  protected $connection;

  /**
   * The entity field manager.
   *
   * @var \Drupal\Core\Entity\EntityFieldManagerInterface
   */
  protected $entityFieldManager;

  /**
   * @var int
   */
  protected $lastSequenceId;

  /**
   * @var \Symfony\Component\EventDispatcher\EventDispatcherInterface
   */
  protected $eventDispatcher;

  /**
   * {@inheritdoc}
   *
   * @param \Drupal\multiversion\Workspace\WorkspaceManagerInterface $workspace_manager
   * @param \Symfony\Component\Serializer\Serializer $serializer
   * @param \Drupal\Core\Entity\EntityTypeManagerInterface $entity_type_manager
   * @param \Drupal\Core\State\StateInterface $state
   * @param \Drupal\Core\Language\LanguageManagerInterface $language_manager
   * @param \Drupal\Core\Cache\CacheBackendInterface $cache
   * @param \Drupal\Core\Database\Connection $connection
   * @param \Drupal\Core\Entity\EntityFieldManagerInterface $entity_field_manager
   * @param \Symfony\Component\EventDispatcher\EventDispatcherInterface $event_dispatcher
   */
  public function __construct(WorkspaceManagerInterface $workspace_manager, Serializer $serializer, EntityTypeManagerInterface $entity_type_manager, StateInterface $state, LanguageManagerInterface $language_manager, CacheBackendInterface $cache, Connection $connection, EntityFieldManagerInterface $entity_field_manager, EventDispatcherInterface $event_dispatcher) {
    $this->workspaceManager = $workspace_manager;
    $this->serializer = $serializer;
    $this->entityTypeManager = $entity_type_manager;
    $this->state = $state;
    $this->languageManager = $language_manager;
    $this->cache = $cache;
    $this->connection = $connection;
    $this->entityFieldManager = $entity_field_manager;
    $this->eventDispatcher = $event_dispatcher;
  }

  /**
   * Static method maintaining the enable migration status.
   *
   * This method needs to be static because in some strange situations Drupal
   * might create multiple instances of this manager. Is this only an issue
   * during tests perhaps?
   *
   * @param boolean|array $status
   * @return boolean|array
   */
  public static function enableMigrationIsActive($status = NULL) {
    static $cache = FALSE;
    if ($status !== NULL) {
      $cache = $status;
    }
    return $cache;
  }

  /**
   * Static method maintaining the disable migration status.
   *
   * @param boolean|array $status
   * @return boolean|array
   */
  public static function disableMigrationIsActive($status = NULL) {
    static $cache = FALSE;
    if ($status !== NULL) {
      $cache = $status;
    }
    return $cache;
  }

  /**
   * {@inheritdoc}
   */
  public function getActiveWorkspaceId() {
    return $this->workspaceManager
      ->getActiveWorkspaceId();
  }

  /**
   * {@inheritdoc}
   */
  public function setActiveWorkspaceId($id) {
    $workspace = $this->workspaceManager
      ->load($id);
    return $this->workspaceManager
      ->setActiveWorkspace($workspace);
  }

  /**
   * {@inheritdoc}
   *
   * @todo: {@link https://www.drupal.org/node/2597337 Consider using the
   * nextId API to generate more sequential IDs.}
   * @see \Drupal\Core\Database\Connection::nextId
   */
  public function newSequenceId() {

    // Multiply the microtime by 1 million to ensure we get an accurate integer.
    // Credit goes to @letharion and @logaritmisk for this simple but genius
    // solution.
    $this->lastSequenceId = (int) (microtime(TRUE) * 1000000);
    return $this->lastSequenceId;
  }

  /**
   * {@inheritdoc}
   */
  public function lastSequenceId() {
    return $this->lastSequenceId;
  }

  /**
   * {@inheritdoc}
   */
  public function isSupportedEntityType(EntityTypeInterface $entity_type) {
    $supported_entity_types = \Drupal::config('multiversion.settings')
      ->get('supported_entity_types') ?: [];
    if (empty($supported_entity_types)) {
      return FALSE;
    }
    if (!in_array($entity_type
      ->id(), $supported_entity_types)) {
      return FALSE;
    }
    return $entity_type instanceof ContentEntityTypeInterface;
  }

  /**
   * {@inheritdoc}
   */
  public function getSupportedEntityTypes() {
    $entity_types = [];
    foreach ($this->entityTypeManager
      ->getDefinitions() as $entity_type_id => $entity_type) {
      if ($this
        ->isSupportedEntityType($entity_type)) {
        $entity_types[$entity_type
          ->id()] = $entity_type;
      }
    }
    return $entity_types;
  }

  /**
   * {@inheritdoc}
   */
  public function isEnabledEntityType(EntityTypeInterface $entity_type) {
    if ($this
      ->isSupportedEntityType($entity_type)) {
      $entity_type_id = $entity_type
        ->id();
      $migration_done = $this->state
        ->get("multiversion.migration_done.{$entity_type_id}", FALSE);
      $enabled_entity_types = \Drupal::config('multiversion.settings')
        ->get('enabled_entity_types') ?: [];
      if ($migration_done && in_array($entity_type_id, $enabled_entity_types)) {
        return TRUE;
      }
    }
    return FALSE;
  }

  /**
   * {@inheritdoc}
   */
  public function allowToAlter(EntityTypeInterface $entity_type) {
    $supported_entity_types = \Drupal::config('multiversion.settings')
      ->get('supported_entity_types') ?: [];
    $id = $entity_type
      ->id();
    $enable_migration = self::enableMigrationIsActive();
    $disable_migration = self::disableMigrationIsActive();

    // Don't allow to alter entity type that is not supported.
    if (!in_array($id, $supported_entity_types)) {
      return FALSE;
    }

    // Don't allow to alter entity type that is in process to be disabled.
    if (is_array($disable_migration) && in_array($id, $disable_migration)) {
      return FALSE;
    }

    // Allow to alter entity type that is in process to be enabled.
    if (is_array($enable_migration) && in_array($id, $enable_migration)) {
      return TRUE;
    }
    return $this
      ->isEnabledEntityType($entity_type);
  }

  /**
   * {@inheritdoc}
   */
  public function getEnabledEntityTypes() {
    $entity_types = [];
    foreach ($this
      ->getSupportedEntityTypes() as $entity_type_id => $entity_type) {
      if ($this
        ->isEnabledEntityType($entity_type)) {
        $entity_types[$entity_type_id] = $entity_type;
      }
    }
    return $entity_types;
  }

  /**
   * {@inheritdoc}
   *
   * @todo Ensure nothing breaks if the migration is run twice.
   */
  public function enableEntityTypes($entity_types_to_enable = NULL) {
    $entity_types = $entity_types_to_enable !== NULL ? $entity_types_to_enable : $this
      ->getSupportedEntityTypes();
    $enabled_entity_types = \Drupal::config('multiversion.settings')
      ->get('enabled_entity_types') ?: [];
    if (empty($entity_types)) {
      return $this;
    }
    $migration = $this
      ->createMigration();
    $migration
      ->installDependencies();
    $this->eventDispatcher
      ->dispatch(MultiversionManagerEvents::PRE_MIGRATE, new MultiversionManagerEvent($entity_types, self::OP_ENABLE));
    $has_data = $this
      ->prepareContentForMigration($entity_types, $migration, self::OP_ENABLE);

    // Nasty workaround until {@link https://www.drupal.org/node/2549143 there
    // is a better way to invalidate caches in services}.
    // For some reason we have to clear cache on the "global" service as opposed
    // to the injected one. Services in the dark corners of Entity API won't see
    // the same result otherwise. Very strange.
    \Drupal::entityTypeManager()
      ->clearCachedDefinitions();
    \Drupal::service('entity_field.manager')
      ->clearCachedFieldDefinitions();
    self::enableMigrationIsActive(array_keys($entity_types));
    $migration
      ->applyNewStorage(array_keys($entity_types));

    // Definitions will now be updated. So fetch the new ones.
    if ($entity_types_to_enable !== NULL) {
      $updated_entity_types = [];
      foreach ($entity_types as $entity_type_id => $entity_type) {
        $updated_entity_types[$entity_type_id] = $this->entityTypeManager
          ->getStorage($entity_type_id)
          ->getEntityType();
      }
    }
    else {
      $updated_entity_types = $this
        ->getSupportedEntityTypes();
    }

    // Temporarily disable the maintenance of the {comment_entity_statistics} table.
    $this->state
      ->set('comment.maintain_entity_statistics', FALSE);
    \Drupal::state()
      ->resetCache();
    foreach ($updated_entity_types as $entity_type_id => $entity_type) {

      // Migrate from the temporary storage to the new shiny home.
      if ($has_data[$entity_type_id]) {
        $field_map = $migration
          ->getFieldMap($entity_type, self::OP_ENABLE, self::FROM_TMP);
        $migration
          ->migrateContentFromTemp($entity_type, $field_map);
        $migration
          ->cleanupMigration($entity_type_id . '__' . self::TO_TMP);
        $migration
          ->cleanupMigration($entity_type_id . '__' . self::FROM_TMP);
      }

      // Mark the migration for this particular entity type as done even if no
      // actual content was migrated.
      $this->state
        ->set("multiversion.migration_done.{$entity_type_id}", TRUE);
    }
    foreach ($entity_types as $entity_type_id => $entity_type) {
      $enabled = $this->state
        ->get("multiversion.migration_done.{$entity_type_id}", FALSE);
      if (!in_array($entity_type_id, $enabled_entity_types) && $enabled) {
        $enabled_entity_types[] = $entity_type_id;
      }
    }
    \Drupal::configFactory()
      ->getEditable('multiversion.settings')
      ->set('enabled_entity_types', $enabled_entity_types)
      ->save();

    // Enable the the maintenance of entity statistics for comments.
    $this->state
      ->set('comment.maintain_entity_statistics', TRUE);

    // Clean up after us.
    $migration
      ->uninstallDependencies();
    self::enableMigrationIsActive(FALSE);

    // Mark the whole migration as done. Any entity types installed after this
    // will not need a migration since they will be created directly on top of
    // the Multiversion storage.
    $this->state
      ->set('multiversion.migration_done', TRUE);
    $this->eventDispatcher
      ->dispatch(MultiversionManagerEvents::POST_MIGRATE, new MultiversionManagerEvent($entity_types, self::OP_ENABLE));

    // Another nasty workaround because the cache is getting skewed somewhere.
    // And resetting the cache on the injected state service does not work.
    // Very strange.
    \Drupal::state()
      ->resetCache();
    return $this;
  }

  /**
   * {@inheritdoc}
   */
  public function disableEntityTypes($entity_types_to_disable = NULL) {
    $entity_types = $entity_types_to_disable !== NULL ? $entity_types_to_disable : $this
      ->getEnabledEntityTypes();
    $migration = $this
      ->createMigration();
    $migration
      ->installDependencies();
    $this->eventDispatcher
      ->dispatch(MultiversionManagerEvents::PRE_MIGRATE, new MultiversionManagerEvent($entity_types, self::OP_DISABLE));
    $has_data = $this
      ->prepareContentForMigration($entity_types, $migration, self::OP_DISABLE);
    if (empty($entity_types)) {
      return $this;
    }
    if ($entity_types_to_disable === NULL) {

      // Uninstall field storage definitions provided by multiversion.
      $this->entityTypeManager
        ->clearCachedDefinitions();
      $update_manager = \Drupal::entityDefinitionUpdateManager();
      foreach ($this->entityTypeManager
        ->getDefinitions() as $entity_type) {
        if ($entity_type
          ->entityClassImplements(FieldableEntityInterface::class)) {
          $entity_type_id = $entity_type
            ->id();
          $revision_key = $entity_type
            ->getKey('revision');

          /** @var \Drupal\Core\Entity\FieldableEntityStorageInterface $storage */
          $storage = $this->entityTypeManager
            ->getStorage($entity_type_id);
          foreach ($this->entityFieldManager
            ->getFieldStorageDefinitions($entity_type_id) as $storage_definition) {

            // @todo We need to trigger field purging here.
            //   See https://www.drupal.org/node/2282119.
            if ($storage_definition
              ->getProvider() == 'multiversion' && !$storage
              ->countFieldData($storage_definition, TRUE) && $storage_definition
              ->getName() != $revision_key) {
              $update_manager
                ->uninstallFieldStorageDefinition($storage_definition);
            }
          }
        }
      }
    }
    $enabled_entity_types = \Drupal::config('multiversion.settings')
      ->get('enabled_entity_types') ?: [];
    foreach ($entity_types as $entity_type_id => $entity_type) {
      if (($key = array_search($entity_type_id, $enabled_entity_types)) !== FALSE) {
        unset($enabled_entity_types[$key]);
      }
    }
    if ($entity_types_to_disable === NULL) {
      $enabled_entity_types = [];
    }
    \Drupal::configFactory()
      ->getEditable('multiversion.settings')
      ->set('enabled_entity_types', $enabled_entity_types)
      ->save();
    \Drupal::entityTypeManager()
      ->clearCachedDefinitions();
    \Drupal::service('entity_field.manager')
      ->clearCachedFieldDefinitions();
    self::disableMigrationIsActive(array_keys($entity_types));
    $migration
      ->applyNewStorage(array_keys($entity_types));

    // Temporarily disable the maintenance of the {comment_entity_statistics} table.
    $this->state
      ->set('comment.maintain_entity_statistics', FALSE);
    \Drupal::state()
      ->resetCache();

    // Definitions will now be updated. So fetch the new ones.
    $updated_entity_types = [];
    foreach ($entity_types as $entity_type_id => $entity_type) {
      $updated_entity_types[$entity_type_id] = $this->entityTypeManager
        ->getStorage($entity_type_id)
        ->getEntityType();
    }
    foreach ($updated_entity_types as $entity_type_id => $entity_type) {

      // Drop unique key from uuid on each entity type.
      $base_table = $entity_type
        ->getBaseTable();
      $uuid_key = $entity_type
        ->getKey('uuid');
      $this->connection
        ->schema()
        ->dropUniqueKey($base_table, $entity_type_id . '_field__' . $uuid_key . '__value');

      // Migrate from the temporary storage to the drupal default storage.
      if ($has_data[$entity_type_id]) {
        $field_map = $migration
          ->getFieldMap($entity_type, self::OP_DISABLE, self::FROM_TMP);
        $migration
          ->migrateContentFromTemp($entity_type, $field_map);
        $migration
          ->cleanupMigration($entity_type_id . '__' . self::TO_TMP);
        $migration
          ->cleanupMigration($entity_type_id . '__' . self::FROM_TMP);
      }
      $this->state
        ->delete("multiversion.migration_done.{$entity_type_id}");
    }

    // Enable the the maintenance of entity statistics for comments.
    $this->state
      ->set('comment.maintain_entity_statistics', TRUE);

    // Clean up after us.
    $migration
      ->uninstallDependencies();
    self::disableMigrationIsActive(FALSE);
    $this->state
      ->delete('multiversion.migration_done');
    $this->eventDispatcher
      ->dispatch(MultiversionManagerEvents::POST_MIGRATE, new MultiversionManagerEvent($entity_types, self::OP_DISABLE));
    return $this;
  }

  /**
   * {@inheritdoc}
   */
  public function newRevisionId(ContentEntityInterface $entity, $index = 0) {
    $deleted = $entity->_deleted->value;
    $old_rev = $entity->_rev->value;

    // The 'new_revision_id' context will be used in normalizers (where it's
    // necessary) to identify in which format to return the normalized entity.
    $normalized_entity = $this->serializer
      ->normalize($entity, NULL, [
      'new_revision_id' => TRUE,
    ]);

    // Remove fields internal to the multiversion system.
    $this
      ->filterNormalizedEntity($normalized_entity);

    // The terms being serialized are:
    // - deleted
    // - old sequence ID (@todo: {@link https://www.drupal.org/node/2597341
    // Address this property.})
    // - old revision hash
    // - normalized entity (without revision info field)
    // - attachments (@todo: {@link https://www.drupal.org/node/2597341
    // Address this property.})
    return $index + 1 . '-' . md5($this
      ->termToBinary([
      $deleted,
      0,
      $old_rev,
      $normalized_entity,
      [],
    ]));
  }

  /**
   * @param array $normalized_entity
   */
  protected function filterNormalizedEntity(&$normalized_entity) {
    foreach ($normalized_entity as $key => &$value) {
      if ($key[0] == '_') {
        unset($normalized_entity[$key]);
      }
      elseif (is_array($value)) {
        $this
          ->filterNormalizedEntity($value);
      }
    }
  }
  protected function termToBinary(array $term) {

    // @todo: {@link https://www.drupal.org/node/2597478 Switch to BERT
    // serialization format instead of JSON.}
    return $this->serializer
      ->serialize($term, 'json');
  }

  /**
   * Factory method for a new Multiversion migration.
   *
   * @return \Drupal\multiversion\MultiversionMigrationInterface
   */
  protected function createMigration() {
    return MultiversionMigration::create($this->container, $this->entityTypeManager, $this->entityFieldManager);
  }
  protected function prepareContentForMigration($entity_types, MultiversionMigrationInterface $migration, $op) {
    $has_data = [];

    // Walk through and verify that the original storage is in good order.
    // Flakey contrib modules or mocked tests where some schemas aren't properly
    // installed should be ignored.
    foreach ($entity_types as $entity_type_id => $entity_type) {

      /** @var \Drupal\Core\Entity\EntityStorageInterface $storage */
      $storage = $this->entityTypeManager
        ->getStorage($entity_type_id);
      $has_data[$entity_type_id] = FALSE;
      try {
        if ($storage
          ->hasData()) {
          $has_data[$entity_type_id] = TRUE;
        }
      } catch (\Exception $e) {

        // Don't bother with this entity type any more.
        unset($entity_types[$entity_type_id]);
      }
      if ($has_data[$entity_type_id]) {

        // Migrate content to temporary storage.
        $field_map = $migration
          ->getFieldMap($entity_type, $op, self::TO_TMP);
        $migration
          ->migrateContentToTemp($storage
          ->getEntityType(), $field_map);
      }
    }

    // Empty old storages. Do this just after migrating all entities to
    // temporary storage because deleting some entity types could delete
    // referenced entities (E.g.: deleting poll entities will also delete
    // poll_choice).
    foreach ($entity_types as $entity_type_id => $entity_type) {
      if ($has_data[$entity_type_id] === TRUE) {

        /** @var \Drupal\Core\Entity\EntityStorageInterface $storage */
        $storage = $this->entityTypeManager
          ->getStorage($entity_type_id);

        // Because of the way the Entity API treats entity definition updates we
        // need to ensure each storage is empty before we can apply the new
        // definition.
        $migration
          ->emptyOldStorage($storage);
      }
    }
    return $has_data;
  }

}

Members

Namesort descending Modifiers Type Description Overrides
MultiversionManager::$cache protected property
MultiversionManager::$connection protected property The database connection.
MultiversionManager::$entityFieldManager protected property The entity field manager.
MultiversionManager::$entityTypeManager protected property
MultiversionManager::$eventDispatcher protected property
MultiversionManager::$languageManager protected property
MultiversionManager::$lastSequenceId protected property
MultiversionManager::$serializer protected property
MultiversionManager::$state protected property
MultiversionManager::$workspaceManager protected property
MultiversionManager::allowToAlter public function Overrides MultiversionManagerInterface::allowToAlter
MultiversionManager::createMigration protected function Factory method for a new Multiversion migration.
MultiversionManager::disableEntityTypes public function Overrides MultiversionManagerInterface::disableEntityTypes
MultiversionManager::disableMigrationIsActive public static function Static method maintaining the disable migration status.
MultiversionManager::enableEntityTypes public function @todo Ensure nothing breaks if the migration is run twice. Overrides MultiversionManagerInterface::enableEntityTypes
MultiversionManager::enableMigrationIsActive public static function Static method maintaining the enable migration status.
MultiversionManager::filterNormalizedEntity protected function
MultiversionManager::FROM_TMP constant
MultiversionManager::getActiveWorkspaceId public function Overrides MultiversionManagerInterface::getActiveWorkspaceId
MultiversionManager::getEnabledEntityTypes public function Overrides MultiversionManagerInterface::getEnabledEntityTypes
MultiversionManager::getSupportedEntityTypes public function Overrides MultiversionManagerInterface::getSupportedEntityTypes
MultiversionManager::isEnabledEntityType public function Overrides MultiversionManagerInterface::isEnabledEntityType
MultiversionManager::isSupportedEntityType public function Overrides MultiversionManagerInterface::isSupportedEntityType
MultiversionManager::lastSequenceId public function Overrides MultiversionManagerInterface::lastSequenceId
MultiversionManager::newRevisionId public function Overrides MultiversionManagerInterface::newRevisionId
MultiversionManager::newSequenceId public function @todo: {@link https://www.drupal.org/node/2597337 Consider using the nextId API to generate more sequential IDs.} Overrides MultiversionManagerInterface::newSequenceId
MultiversionManager::OP_DISABLE constant
MultiversionManager::OP_ENABLE constant
MultiversionManager::prepareContentForMigration protected function
MultiversionManager::setActiveWorkspaceId public function Overrides MultiversionManagerInterface::setActiveWorkspaceId
MultiversionManager::termToBinary protected function
MultiversionManager::TO_TMP constant
MultiversionManager::__construct public function