You are here

trait SqlFieldableEntityTypeListenerTrait in Drupal 10

Same name and namespace in other branches
  1. 8 core/lib/Drupal/Core/Entity/Sql/SqlFieldableEntityTypeListenerTrait.php \Drupal\Core\Entity\Sql\SqlFieldableEntityTypeListenerTrait
  2. 9 core/lib/Drupal/Core/Entity/Sql/SqlFieldableEntityTypeListenerTrait.php \Drupal\Core\Entity\Sql\SqlFieldableEntityTypeListenerTrait

Helper methods for EntityTypeListenerInterface.

@property \Drupal\Core\Entity\EntityTypeManagerInterface $entityTypeManager @property \Drupal\Core\Database\Connection $database

@internal

Hierarchy

See also

\Drupal\Core\Entity\EntityTypeListenerInterface

File

core/lib/Drupal/Core/Entity/Sql/SqlFieldableEntityTypeListenerTrait.php, line 19

Namespace

Drupal\Core\Entity\Sql
View source
trait SqlFieldableEntityTypeListenerTrait {

  /**
   * {@inheritdoc}
   */
  public function onFieldableEntityTypeUpdate(EntityTypeInterface $entity_type, EntityTypeInterface $original, array $field_storage_definitions, array $original_field_storage_definitions, array &$sandbox = NULL) {

    /** @var \Drupal\Core\Entity\EntityStorageInterface $original_storage */
    $original_storage = $this->entityTypeManager
      ->createHandlerInstance($original
      ->getStorageClass(), $original);
    $has_data = $original_storage
      ->hasData();

    // If 'progress' is not set, then this will be the first run of the batch.
    if (!isset($sandbox['progress'])) {

      // We cannot support updating the schema of an entity type from
      // revisionable to non-revisionable or translatable to non-translatable
      // because that can lead to unintended data loss.
      // @todo Add support for these conversions in case there is no data loss.
      //   @see https://www.drupal.org/project/drupal/issues/3024727
      $convert_rev_to_non_rev = $original
        ->isRevisionable() && !$entity_type
        ->isRevisionable();
      $convert_mul_to_non_mul = $original
        ->isTranslatable() && !$entity_type
        ->isTranslatable();
      if ($has_data && ($convert_rev_to_non_rev || $convert_mul_to_non_mul)) {
        throw new EntityStorageException('Converting an entity type from revisionable to non-revisionable or from translatable to non-translatable is not supported.');
      }

      // Check that the fields required by a revisionable entity type exist.
      if ($entity_type
        ->isRevisionable() && !isset($field_storage_definitions[$entity_type
        ->getKey('revision')])) {
        throw new EntityStorageException('Missing revision field.');
      }
      if ($entity_type
        ->isRevisionable() && !isset($field_storage_definitions[$entity_type
        ->getRevisionMetadataKey('revision_default')])) {
        throw new EntityStorageException('Missing revision_default field.');
      }

      // Check that the fields required by a translatable entity type exist.
      if ($entity_type
        ->isTranslatable() && !isset($field_storage_definitions[$entity_type
        ->getKey('langcode')])) {
        throw new EntityStorageException('Missing langcode field.');
      }
      if ($entity_type
        ->isTranslatable() && !isset($field_storage_definitions[$entity_type
        ->getKey('default_langcode')])) {
        throw new EntityStorageException('Missing default_langcode field.');
      }

      // Check that the fields required by a revisionable and translatable
      // entity type exist.
      if ($entity_type
        ->isRevisionable() && $entity_type
        ->isTranslatable() && !isset($field_storage_definitions[$entity_type
        ->getKey('revision_translation_affected')])) {
        throw new EntityStorageException('Missing revision_translation_affected field.');
      }
      $this
        ->preUpdateEntityTypeSchema($entity_type, $original, $field_storage_definitions, $original_field_storage_definitions, $sandbox);
    }

    // Copy data from the original storage to the temporary one.
    if ($has_data) {
      $this
        ->copyData($entity_type, $original, $field_storage_definitions, $original_field_storage_definitions, $sandbox);
    }
    else {

      // If there is no existing data, we still need to run the
      // post-schema-update tasks.
      $sandbox['#finished'] = 1;
    }

    // If the data copying has finished successfully, allow the storage schema
    // to do any required cleanup tasks. For example, this process should take
    // care of transforming the temporary storage into the current storage.
    if ($sandbox['#finished'] == 1) {
      $this
        ->postUpdateEntityTypeSchema($entity_type, $original, $field_storage_definitions, $original_field_storage_definitions, $sandbox);
    }
  }

  /**
   * Allows subscribers to prepare their schema before data copying.
   *
   * @param \Drupal\Core\Entity\EntityTypeInterface $entity_type
   *   The updated entity type definition.
   * @param \Drupal\Core\Entity\EntityTypeInterface $original
   *   The original entity type definition.
   * @param \Drupal\Core\Field\FieldStorageDefinitionInterface[] $field_storage_definitions
   *   The updated field storage definitions, including possibly new ones.
   * @param \Drupal\Core\Field\FieldStorageDefinitionInterface[] $original_field_storage_definitions
   *   The original field storage definitions.
   * @param array &$sandbox
   *   (optional) A sandbox array provided by a hook_update_N() implementation
   *   or a Batch API callback. If the entity schema update requires a data
   *   migration, this parameter is mandatory. Defaults to NULL.
   */
  protected function preUpdateEntityTypeSchema(EntityTypeInterface $entity_type, EntityTypeInterface $original, array $field_storage_definitions, array $original_field_storage_definitions, array &$sandbox = NULL) {
  }

  /**
   * Allows subscribers to do any cleanup necessary after data copying.
   *
   * @param \Drupal\Core\Entity\EntityTypeInterface $entity_type
   *   The updated entity type definition.
   * @param \Drupal\Core\Entity\EntityTypeInterface $original
   *   The original entity type definition.
   * @param \Drupal\Core\Field\FieldStorageDefinitionInterface[] $field_storage_definitions
   *   The updated field storage definitions, including possibly new ones.
   * @param \Drupal\Core\Field\FieldStorageDefinitionInterface[] $original_field_storage_definitions
   *   The original field storage definitions.
   * @param array &$sandbox
   *   (optional) A sandbox array provided by a hook_update_N() implementation
   *   or a Batch API callback. If the entity schema update requires a data
   *   migration, this parameter is mandatory. Defaults to NULL.
   */
  protected function postUpdateEntityTypeSchema(EntityTypeInterface $entity_type, EntityTypeInterface $original, array $field_storage_definitions, array $original_field_storage_definitions, array &$sandbox = NULL) {
  }

  /**
   * Copies entity data from the original storage to the temporary one.
   *
   * @param \Drupal\Core\Entity\EntityTypeInterface $entity_type
   *   The updated entity type definition.
   * @param \Drupal\Core\Entity\EntityTypeInterface $original
   *   The original entity type definition.
   * @param \Drupal\Core\Field\FieldStorageDefinitionInterface[] $field_storage_definitions
   *   The updated field storage definitions, including possibly new ones.
   * @param \Drupal\Core\Field\FieldStorageDefinitionInterface[] $original_field_storage_definitions
   *   The original field storage definitions.
   * @param array &$sandbox
   *   The sandbox array from a hook_update_N() implementation.
   */
  protected function copyData(EntityTypeInterface $entity_type, EntityTypeInterface $original, array $field_storage_definitions, array $original_field_storage_definitions, array &$sandbox) {

    /** @var \Drupal\Core\Entity\ContentEntityTypeInterface $entity_type */
    $id_key = $entity_type
      ->getKey('id');
    $revision_id_key = $entity_type
      ->getKey('revision');
    $revision_default_key = $entity_type
      ->getRevisionMetadataKey('revision_default');
    $langcode_key = $entity_type
      ->getKey('langcode');
    $default_langcode_key = $entity_type
      ->getKey('default_langcode');
    $revision_translation_affected_key = $entity_type
      ->getKey('revision_translation_affected');

    // If 'progress' is not set, then this will be the first run of the batch.
    if (!isset($sandbox['progress'])) {
      $sandbox['progress'] = 0;
      $sandbox['current_id'] = -1;
    }

    // If the original entity type is revisionable, we need to copy all the
    // revisions.
    $load_revisions = $original
      ->isRevisionable();
    if ($load_revisions) {
      $table_name = $original
        ->getRevisionTable();
      $identifier_field = $revision_id_key;
    }
    else {
      $table_name = $original
        ->getBaseTable();
      $identifier_field = $id_key;
    }

    // Get the next entity identifiers to migrate.
    // @todo Use an entity query when it is able to use the last installed
    //   entity type and field storage definitions.
    // @see https://www.drupal.org/project/drupal/issues/2554235
    $step_size = Settings::get('entity_update_batch_size', 50);
    $entity_identifiers = $this->database
      ->select($table_name, 't')
      ->condition("t.{$identifier_field}", $sandbox['current_id'], '>')
      ->fields('t', [
      $identifier_field,
    ])
      ->orderBy($identifier_field, 'ASC')
      ->range(0, $step_size)
      ->execute()
      ->fetchCol();

    /** @var \Drupal\Core\Entity\ContentEntityInterface[] $entities */
    $entities = $load_revisions ? $this->storage
      ->loadMultipleRevisions($entity_identifiers) : $this->storage
      ->loadMultiple($entity_identifiers);

    /** @var \Drupal\Core\Entity\Sql\SqlContentEntityStorage $temporary_storage */
    $temporary_storage = $this->entityTypeManager
      ->createHandlerInstance($entity_type
      ->getStorageClass(), $entity_type);
    $temporary_storage
      ->setEntityType($entity_type);
    $temporary_storage
      ->setFieldStorageDefinitions($field_storage_definitions);
    $temporary_storage
      ->setTableMapping($sandbox['temporary_table_mapping']);
    foreach ($entities as $identifier => $entity) {
      try {
        if (!$original
          ->isRevisionable() && $entity_type
          ->isRevisionable()) {

          // Set the revision ID to be same as the entity ID.
          $entity
            ->set($revision_id_key, $entity
            ->id());

          // We had no revisions so far, so the existing data belongs to the
          // default revision now.
          $entity
            ->set($revision_default_key, TRUE);
        }

        // Set the 'langcode' and 'default_langcode' values as needed.
        if (!$original
          ->isTranslatable() && $entity_type
          ->isTranslatable()) {
          if ($entity
            ->get($langcode_key)
            ->isEmpty()) {
            $entity
              ->set($langcode_key, \Drupal::languageManager()
              ->getDefaultLanguage()
              ->getId());
          }
          $entity
            ->set($default_langcode_key, TRUE);
        }

        // Set the 'revision_translation_affected' field to TRUE to match the
        // return value of the case when the field does not exist.
        if ((!$original
          ->isRevisionable() || !$original
          ->isTranslatable()) && $entity_type
          ->isRevisionable() && $entity_type
          ->isTranslatable()) {
          $entity
            ->set($revision_translation_affected_key, TRUE);
        }

        // Finally, save the entity in the temporary storage.
        $temporary_storage
          ->restore($entity);
      } catch (\Exception $e) {
        $this
          ->handleEntityTypeSchemaUpdateExceptionOnDataCopy($entity_type, $original, $sandbox);

        // Re-throw the original exception with a helpful message.
        $error_revision_id = $load_revisions ? ", revision ID: {$entity->getLoadedRevisionId()}" : '';
        throw new EntityStorageException("The entity update process failed while processing the entity type {$entity_type->id()}, ID: {$entity->id()}{$error_revision_id}.", $e
          ->getCode(), $e);
      }
      $sandbox['progress']++;
      $sandbox['current_id'] = $identifier;
    }

    // Reset the cache in order to free memory as we progress.
    \Drupal::service('entity.memory_cache')
      ->deleteAll();

    // Get an updated count of entities that still need to migrated to the new
    // storage.
    $missing = $this->database
      ->select($table_name, 't')
      ->condition("t.{$identifier_field}", $sandbox['current_id'], '>')
      ->orderBy($identifier_field, 'ASC')
      ->countQuery()
      ->execute()
      ->fetchField();
    $sandbox['#finished'] = $missing ? $sandbox['progress'] / ($sandbox['progress'] + (int) $missing) : 1;
  }

  /**
   * Handles the case when an error occurs during the data copying step.
   *
   * @param \Drupal\Core\Entity\EntityTypeInterface $entity_type
   *   The updated entity type definition.
   * @param \Drupal\Core\Entity\EntityTypeInterface $original
   *   The original entity type definition.
   * @param array &$sandbox
   *   The sandbox array from a hook_update_N() implementation.
   */
  protected function handleEntityTypeSchemaUpdateExceptionOnDataCopy(EntityTypeInterface $entity_type, EntityTypeInterface $original, array &$sandbox) {
  }

}

Members

Namesort descending Modifiers Type Description Overrides
SqlFieldableEntityTypeListenerTrait::copyData protected function Copies entity data from the original storage to the temporary one.
SqlFieldableEntityTypeListenerTrait::handleEntityTypeSchemaUpdateExceptionOnDataCopy protected function Handles the case when an error occurs during the data copying step. 1
SqlFieldableEntityTypeListenerTrait::onFieldableEntityTypeUpdate public function
SqlFieldableEntityTypeListenerTrait::postUpdateEntityTypeSchema protected function Allows subscribers to do any cleanup necessary after data copying. 1
SqlFieldableEntityTypeListenerTrait::preUpdateEntityTypeSchema protected function Allows subscribers to prepare their schema before data copying. 1