You are here

class MigrateManager in Workbench Moderation to Content Moderation 8

Manages migrating from WBM to CM.

The intention is for the migration to have the following recovery points: 1. States and transitions are stored in key value (i.e. the Workflow entity is created) 2. Entity state maps are stored in key value 3. WBM uninstalled 4. Workflows installed 5. CM installed 6. States and transitions are migrated (i.e. the Workflow entity is created) 7. Entity state maps are migrated 8. Remove all temporary data from key value used for the migration.

Hierarchy

Expanded class hierarchy of MigrateManager

1 file declares its use of MigrateManager
MigrateForm.php in src/Form/MigrateForm.php
1 string reference to 'MigrateManager'
wbm2cm.services.yml in ./wbm2cm.services.yml
wbm2cm.services.yml
1 service uses MigrateManager
wbm2cm.migrate_manager in ./wbm2cm.services.yml
\Drupal\wbm2cm\MigrateManager

File

src/MigrateManager.php, line 25

Namespace

Drupal\wbm2cm
View source
class MigrateManager {

  /**
   * The config factory.
   *
   * @var \Drupal\Core\Config\ConfigFactoryInterface
   */
  protected $configFactory;

  /**
   * The entity type manager service.
   *
   * @var \Drupal\Core\Entity\EntityTypeManagerInterface
   */
  protected $entityTypeManager;

  /**
   * The module installer.
   *
   * @var \Drupal\Core\Extension\ModuleInstallerInterface
   */
  protected $moduleInstaller;

  /**
   * The key value store for the wbm2cm module.
   *
   * @var \Drupal\Core\KeyValueStore\KeyValueStoreInterface
   */
  protected $wbm2cmStore;

  /**
   * The key value store for the state map data.
   *
   * Note: we store state map data separately to make it easier to compute if
   * all states were successfully migrated.
   *
   * @var \Drupal\Core\KeyValueStore\KeyValueStoreInterface
   */
  protected $stateMapStore;

  /**
   * Logger service.
   *
   * @var \Drupal\Core\Logger\LoggerChannelInterface
   */
  protected $logger;

  /**
   * Instantiate the MigrateManager service.
   *
   * @param \Drupal\Core\Config\ConfigFactoryInterface $config_factory
   *   The config factory.
   * @param \Drupal\Core\Entity\EntityTypeManagerInterface $entity_type_manager
   *   The entity type manager.
   * @param \Drupal\Core\Extension\ModuleInstallerInterface $module_installer
   *   The module installer.
   * @param \Drupal\Core\KeyValueStore\KeyValueFactoryInterface $key_value_factory
   *   The key value store factory.
   * @param \Psr\Log\LoggerInterface $logger
   *   A logger instance.
   */
  public function __construct(ConfigFactoryInterface $config_factory, EntityTypeManagerInterface $entity_type_manager, ModuleInstallerInterface $module_installer, KeyValueFactoryInterface $key_value_factory, LoggerInterface $logger) {
    $this->configFactory = $config_factory;
    $this->entityTypeManager = $entity_type_manager;
    $this->moduleInstaller = $module_installer;
    $this->migrateStore = $key_value_factory
      ->get('wbm2cm_migrate');
    $this->stateMapStore = $key_value_factory
      ->get('wbm2cm_migrate_state_map');
    $this->logger = $logger;
  }

  /**
   * Get messages for whether or not the migration is valid and ready.
   *
   * @return string[]
   *   An array of messages to display to the user about, keyed on a unique
   *   identifier.
   */
  public function getValidationMessages() {
    $messages = [];

    // @todo validate WBM does not have multiple transitions, see WorkflowTypeBase
    // @todo validate that there is a 'draft' and 'published' state, which are required
    // @todo check if migration was run and failed, i.e. we need to trigger a "re-run" from a failed state
    return $messages;
  }

  /**
   * Determine if the migration is valid.
   *
   * @return bool
   *   True if the migration is valid and ready to begin.
   */
  public function isValid() {
    return empty($this
      ->getValidationMessages());
  }

  /**
   * Flags that the migration finished.
   */
  public function setFinished() {
    $this->migrateStore
      ->set('finished', TRUE);
  }

  /**
   * Flags that the migration is not finished.
   */
  public function setUnfinished() {
    $this->migrateStore
      ->set('finished', FALSE);
  }

  /**
   * Determines if the migration finished.
   *
   * @see isComplete
   *
   * @return bool
   *   True if the full migration is considered to have been run; else, false.
   */
  public function isFinished() {
    return TRUE == $this->migrateStore
      ->get('finished');
  }

  /**
   * Determine if the migration is complete.
   *
   * The migration is complete if:
   *   - It is marked as finished.
   *   - There are no states left in the state key value store.
   *
   * @return bool
   *   True if the migration completed successfully. Otherwise, false.
   */
  public function isComplete() {
    if ($this
      ->isFinished()) {

      // @todo is there a less costly way to compute that the key value storage is empty?
      $states = $this->stateMapStore
        ->getAll();
      if (empty($states)) {
        $this->logger
          ->debug('isComplete was called, it isFinished, and there are NO states remaining in storage.');
        return TRUE;
      }
      else {
        $this->logger
          ->debug('isComplete was called, it isFinished, but there are %count states remaining in storage.', [
          '%count' => count($states),
        ]);
        return FALSE;
      }
    }
    $this->logger
      ->debug('isComplete was called and returned FALSE.');
    return FALSE;
  }

  /**
   * Save the Workbench Moderation states and transitions.
   */
  public function saveWorkbenchModerationStatesAndTransitions() {

    // Collect all states.
    $states = [];
    foreach ($this->configFactory
      ->listAll('workbench_moderation.moderation_state.') as $state_ids) {
      $state = $this->configFactory
        ->getEditable($state_ids);
      $states[] = $state
        ->get();
    }
    $this->logger
      ->info('Found Workbench Moderation states: %state_ids', [
      '%state_ids' => print_r($states, 1),
    ]);

    // Save states.
    $this->migrateStore
      ->set('states', $states);

    // Collect all transitions.
    $transitions = [];
    foreach ($this->configFactory
      ->listAll('workbench_moderation.moderation_state_transition.') as $transition_ids) {
      $transition = $this->configFactory
        ->getEditable($transition_ids);
      $transitions[] = $transition
        ->get();
    }
    $this->logger
      ->info('Found Workbench Moderation transitions: %transition_ids', [
      '%transition_ids' => print_r($transitions, 1),
    ]);

    // Save transitions.
    $this->migrateStore
      ->set('transitions', $transitions);
  }

  /**
   * Save the Workbench Moderation states on all entities.
   */
  public function saveWorkbenchModerationSateMap() {

    // @todo avoid needing to save enabled bundles?
    // Collect all moderated bundles.
    $this->entityTypeManager
      ->clearCachedDefinitions();

    // @todo consider leveraging WBM to get the list of enabled bundles?
    $enabled_bundles = [];
    foreach ($this->configFactory
      ->listAll() as $bundle_config_id) {
      $bundle_config = $this->configFactory
        ->getEditable($bundle_config_id);
      if (!($third_party_settings = $bundle_config
        ->get('third_party_settings'))) {
        $this->logger
          ->debug('Skipping entity bundle that is not moderated: %bundle_id', [
          '%bundle_id' => $bundle_config_id,
        ]);
        continue;
      }
      $third_party_settings_updated = array_diff_key($third_party_settings, array_flip([
        'workbench_moderation',
      ]));
      if (count($third_party_settings) !== count($third_party_settings_updated)) {
        $this->logger
          ->debug('Found Workbench Moderation bundle that is moderated: %bundle_id', [
          '%bundle_id' => $bundle_config_id,
        ]);

        // Collect which entity types and bundles have moderation enabled.
        list($entity_provider, $bundle_config_prefix, $bundle_id) = explode('.', $bundle_config_id);
        $entity_type_id = FALSE;
        foreach ($this->entityTypeManager
          ->getDefinitions() as $entity_definition) {
          if ($entity_definition
            ->getProvider() === $entity_provider && $entity_definition
            ->get('config_prefix') === $bundle_config_prefix) {
            $entity_type_id = $entity_definition
              ->getBundleOf();
            break;
          }
        }

        // @todo cleanup and clarify what error state we are dealing with
        if (!$entity_type_id) {
          throw new \Exception('Something went wrong.');
        }
        $enabled_bundles[$entity_type_id][] = $bundle_id;
      }
      else {
        $this->logger
          ->debug('Skipping Workbench Moderation bundle that is moderated, but is incorrect format? %bundle_id', [
          '%bundle_id' => $bundle_config_id,
        ]);
      }
    }

    // Save enabled bundles.
    $this->migrateStore
      ->set('enabled_bundles', $enabled_bundles);

    // Collect entity state map and remove Workbench moderation_state field from
    // enabled bundles.
    foreach ($enabled_bundles as $entity_type_id => $bundles) {
      $entity_storage = $this->entityTypeManager
        ->getStorage($entity_type_id);
      foreach ($bundles as $bundle_id) {
        $this->logger
          ->debug('Querying for all %bundle_id revisions...', [
          '%bundle_id' => $bundle_id,
        ]);
        $entity_revisions = \Drupal::entityQuery($entity_type_id)
          ->condition('type', $bundle_id)
          ->allRevisions()
          ->execute();
        foreach ($entity_revisions as $revision_id => $id) {
          $entity = $entity_storage
            ->loadRevision($revision_id);
          $state_map_key = "state_map.{$entity_type_id}.{$bundle_id}.{$revision_id}";
          $this->stateMapStore
            ->set($state_map_key, $entity->moderation_state->target_id);
          $this->logger
            ->debug('Setting Workbench Moderation state field on id:%id, revision:%revision_id from %state to NULL', [
            '%id' => $id,
            '%revision_id' => $revision_id,
            '%state' => $entity->moderation_state->target_id,
          ]);
          $entity->moderation_state = NULL;
          $entity
            ->save();

          // Handle translations.
          // Get all languages except the default (which we've already handled).
          $languages = $entity
            ->getTranslationLanguages(FALSE);
          $language_ids = [];
          foreach ($languages as $language) {
            $language_ids[] = $language
              ->getId();
          }
          $this->logger
            ->debug('Found the following translations on id:%id, revision:%revision_id: %languages', [
            '%id' => $id,
            '%revision_id' => $revision_id,
            '%languages' => implode(', ', $language_ids),
          ]);
          foreach ($language_ids as $language_id) {

            // @todo how to get all revisions for this translation?
            $translated_entity = $entity
              ->getTranslation($language_id);
            $state_map_key = "state_map.{$entity_type_id}.{$bundle_id}.{$revision_id}.{$language_id}";
            $this->stateMapStore
              ->set($state_map_key, $translated_entity->moderation_state->target_id);
            $this->logger
              ->debug('Setting Workbench Moderation state field on id:%id, revision:%revision_id, lang:%lang from %state to NULL', [
              '%id' => $id,
              '%revision_id' => $revision_id,
              '%lang' => $language_id,
              '%state' => $entity->moderation_state->target_id,
            ]);
            $translated_entity->moderation_state = NULL;
            $translated_entity
              ->save();
          }
        }
      }
    }
    $this->logger
      ->notice('Workbench Moderation states have been removed from all entities and temporarily stored in key value storage.');
  }

  /**
   * Uninstall the Workbench Moderation module.
   *
   * Note: no need to check if the module is uninstalled before calling
   * uninstall--the module installer will check for us.
   */
  public function uninstallWorkbenchModeration() {
    $this->moduleInstaller
      ->uninstall([
      'workbench_moderation',
    ], FALSE);
    $this->logger
      ->notice('Workbench Moderation module is uninstalled.');
  }

  /**
   * Install the Workflows module.
   *
   * Note: no need to check if the module is installed before calling
   * install--the module installer will check for us.
   */
  public function installWorkflows() {
    $this->moduleInstaller
      ->install([
      'workflows',
    ]);
    $this->logger
      ->notice('Workflows module is installed.');
  }

  /**
   * Install the Content Moderation module.
   *
   * Note: no need to check if the module is installed before calling
   * install--the module installer will check for us.
   */
  public function installContentModeration() {
    $this->moduleInstaller
      ->install([
      'content_moderation',
    ]);
    $this->logger
      ->notice('Content Moderation module is installed.');
  }

  /**
   * Create the Workflow based on info from WBM states and transitions.
   */
  public function recreateWorkbenchModerationWorkflow() {
    $states = $this->migrateStore
      ->get('states');
    $transitions = $this->migrateStore
      ->get('transitions');
    $enabled_bundles = $this->migrateStore
      ->get('enabled_bundles');

    // Create and save a workflow entity with the information gathered.
    // Note: this implies all entities will be squished into a single workflow.
    $workflow_config = [
      'id' => 'content_moderation_workflow',
      'label' => 'Content Moderation Workflow',
      'type' => 'content_moderation',
      'type_settings' => [
        'states' => [],
        'transitions' => [],
      ],
    ];
    foreach ($states as $state) {
      $workflow_config['type_settings']['states'][$state['id']] = [
        'label' => $state['label'],
        'published' => $state['published'],
        'default_revision' => $state['default_revision'],
      ];
    }
    foreach ($transitions as $transition) {
      $workflow_config['type_settings']['transitions'][$transition['id']] = [
        'label' => $transition['label'],
        'to' => $transition['stateTo'],
        'from' => explode(',', $transition['stateFrom']),
      ];
    }

    // Instantiate the workflow from the config.
    $this->logger
      ->info('Create workflow from config: %config', [
      '%config' => print_r($workflow_config, 1),
    ]);
    $workflow = new Workflow($workflow_config, 'workflow');
    $workflow_type_plugin = $workflow
      ->getTypePlugin();

    // Add Content Moderation moderation to bundles that were Workbench
    // Moderation moderated.
    foreach ($enabled_bundles as $entity_type_id => $bundles) {
      foreach ($bundles as $bundle_id) {
        $workflow_type_plugin
          ->addEntityTypeAndBundle($entity_type_id, $bundle_id);
        $this->logger
          ->notice('Setting Content Moderation to be enabled on %bundle_id', [
          '%bundle_id' => $bundle_id,
        ]);
      }
    }

    // Save the workflow now that it has all the configurations set.
    $workflow
      ->save();
    $this->logger
      ->notice('Content Moderation Workflow created.');
  }

  /**
   * Create the moderation states on all entities based on WBM data.
   */
  public function recreateModerationStatesOnEntities() {
    $enabled_bundles = $this->migrateStore
      ->get('enabled_bundles');
    foreach ($enabled_bundles as $entity_type_id => $bundles) {
      $entity_storage = $this->entityTypeManager
        ->getStorage($entity_type_id);
      foreach ($bundles as $bundle_id) {

        // Get all entity revisions.
        $this->logger
          ->debug('Querying for all %bundle_id revisions...', [
          '%bundle_id' => $bundle_id,
        ]);
        $entity_revisions = \Drupal::entityQuery($entity_type_id)
          ->condition('type', $bundle_id)
          ->allRevisions()
          ->execute();
        $this->logger
          ->debug('Setting Content Moderation states on %bundle_id entities', [
          '%bundle_id' => $bundle_id,
        ]);

        // Update the state for all revisions if a state id is found.
        foreach ($entity_revisions as $revision_id => $id) {

          // Get the state if it exists.
          $state_map_key = "state_map.{$entity_type_id}.{$bundle_id}.{$revision_id}";
          $state_id = $this->stateMapStore
            ->get($state_map_key);
          if (!$state_id) {
            $this->logger
              ->debug('Skipping updating state on id:%id, revision:%revision_id because no state exists', [
              '%id' => $entity
                ->id(),
              '%revision_id' => $revision_id,
              '%state' => $state_id,
            ]);
            continue;
          }

          // Set the state.
          $entity = $entity_storage
            ->loadRevision($revision_id);
          $entity->moderation_state = $state_id;
          $entity
            ->save();
          $this->logger
            ->debug('Setting Workbench Moderation state field on id:%id, revision:%revision_id to %state', [
            '%id' => $entity
              ->id(),
            '%revision_id' => $revision_id,
            '%state' => $state_id,
          ]);

          // Remove the state from key value store to indicate the entity has
          // been successfully updated.
          $this->stateMapStore
            ->delete($state_map_key);

          // Handle translations.
          // Get all languages except the default (which we've already handled).
          $languages = $entity
            ->getTranslationLanguages(FALSE);
          $language_ids = [];
          foreach ($languages as $language) {
            $language_ids[] = $language
              ->getId();
          }
          foreach ($language_ids as $language_id) {

            // Get the state if it exists.
            // @todo how to get all revisions for this translation?
            $translated_entity = $entity
              ->getTranslation($language_id);
            $state_map_key = "state_map.{$entity_type_id}.{$bundle_id}.{$revision_id}.{$language_id}";
            $state_id = $this->stateMapStore
              ->get($state_map_key);
            if (!$state_id) {
              $this->logger
                ->debug('Skipping updating state on id:%id, revision:%revision_id, lang:%lang because no state exists', [
                '%id' => $translated_entity
                  ->id(),
                '%revision_id' => $revision_id,
                '%lang' => $language_id,
                '%state' => $state_id,
              ]);
              continue;
            }

            // Set the state.
            $translated_entity->moderation_state = $state_id;
            $translated_entity
              ->save();
            $this->logger
              ->debug('Setting Workbench Moderation state field on id:%id, revision:%revision_id to %state', [
              '%id' => $translated_entity
                ->id(),
              '%revision_id' => $revision_id,
              '%state' => $state_id,
            ]);

            // Remove the state from key value store to indicate the entity has
            // been successfully updated.
            $this->stateMapStore
              ->delete($state_map_key);
          }
        }
      }
    }
  }

  /**
   * Remove all temporary data from key value used for the migration.
   *
   * The following are not cleaned up by this:
   *   - Any state used to manage progress of the migration, e.g. BatchManager.
   *   - The state map keys, which should be cleaned up during processing.
   */
  public function cleanupKeyValue() {
    $this->migrateStore
      ->deleteMultiple([
      'states',
      'transitions',
      'enabled_bundles',
    ]);
  }

  /**
   * Purge all key value stores used used by the migrate manager.
   *
   * Note: this should only be used during the module's uninstall.
   */
  public function purgeAllKeyValueStores() {
    $this->migrateStore
      ->deleteAll();
    $this->stateMapStore
      ->deleteAll();
  }

}

Members

Namesort descending Modifiers Type Description Overrides
MigrateManager::$configFactory protected property The config factory.
MigrateManager::$entityTypeManager protected property The entity type manager service.
MigrateManager::$logger protected property Logger service.
MigrateManager::$moduleInstaller protected property The module installer.
MigrateManager::$stateMapStore protected property The key value store for the state map data.
MigrateManager::$wbm2cmStore protected property The key value store for the wbm2cm module.
MigrateManager::cleanupKeyValue public function Remove all temporary data from key value used for the migration.
MigrateManager::getValidationMessages public function Get messages for whether or not the migration is valid and ready.
MigrateManager::installContentModeration public function Install the Content Moderation module.
MigrateManager::installWorkflows public function Install the Workflows module.
MigrateManager::isComplete public function Determine if the migration is complete.
MigrateManager::isFinished public function Determines if the migration finished.
MigrateManager::isValid public function Determine if the migration is valid.
MigrateManager::purgeAllKeyValueStores public function Purge all key value stores used used by the migrate manager.
MigrateManager::recreateModerationStatesOnEntities public function Create the moderation states on all entities based on WBM data.
MigrateManager::recreateWorkbenchModerationWorkflow public function Create the Workflow based on info from WBM states and transitions.
MigrateManager::saveWorkbenchModerationSateMap public function Save the Workbench Moderation states on all entities.
MigrateManager::saveWorkbenchModerationStatesAndTransitions public function Save the Workbench Moderation states and transitions.
MigrateManager::setFinished public function Flags that the migration finished.
MigrateManager::setUnfinished public function Flags that the migration is not finished.
MigrateManager::uninstallWorkbenchModeration public function Uninstall the Workbench Moderation module.
MigrateManager::__construct public function Instantiate the MigrateManager service.