You are here

class EntityReferenceRevisionsOrphanPurger in Entity Reference Revisions 8

Manages orphan composite revision deletion.

Hierarchy

Expanded class hierarchy of EntityReferenceRevisionsOrphanPurger

3 files declare their use of EntityReferenceRevisionsOrphanPurger
EntityReferenceRevisionsCommands.php in src/Commands/EntityReferenceRevisionsCommands.php
OrphanedCompositeEntitiesDeleteForm.php in src/Form/OrphanedCompositeEntitiesDeleteForm.php
OrphanPurger.php in src/Plugin/QueueWorker/OrphanPurger.php
1 string reference to 'EntityReferenceRevisionsOrphanPurger'
entity_reference_revisions.services.yml in ./entity_reference_revisions.services.yml
entity_reference_revisions.services.yml
1 service uses EntityReferenceRevisionsOrphanPurger
entity_reference_revisions.orphan_purger in ./entity_reference_revisions.services.yml
Drupal\entity_reference_revisions\EntityReferenceRevisionsOrphanPurger

File

src/EntityReferenceRevisionsOrphanPurger.php, line 18

Namespace

Drupal\entity_reference_revisions
View source
class EntityReferenceRevisionsOrphanPurger {
  use StringTranslationTrait;

  /**
   * Parent is valid.
   */
  const PARENT_VALID = 0;

  /**
   * Parent is invalid and usage can not be verified.
   */
  const PARENT_INVALID_SKIP = 1;

  /**
   * Parent is invalid and paragraph is safe to delete.
   */
  const PARENT_INVALID_DELETE = 2;

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

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

  /**
   * The date formatter service.
   *
   * @var \Drupal\Core\Datetime\DateFormatterInterface
   */
  protected $dateFormatter;

  /**
   * The time service.
   *
   * @var \Drupal\Component\Datetime\TimeInterface
   */
  protected $time;

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

  /**
   * The messenger.
   *
   * @var \Drupal\Core\Messenger\MessengerInterface
   */
  protected $messenger;

  /**
   * List of already checked parents.
   *
   * @var bool[][]
   */
  protected $validParents = [];

  /**
   * Constructs a EntityReferenceRevisionsOrphanManager object.
   *
   * @param \Drupal\Core\Entity\EntityTypeManagerInterface $entity_type_manager
   *   The entity type manager.
   * @param \Drupal\Core\Entity\EntityFieldManagerInterface $entity_field_manager
   *   The entity field manager.
   * @param \Drupal\Core\Datetime\DateFormatterInterface $date_formatter
   *   The date formatter service.
   * @param \Drupal\Component\Datetime\TimeInterface $time
   *   The time service.
   * @param \Drupal\Core\Database\Connection $database
   *   The database service.
   * @param \Drupal\Core\Messenger\MessengerInterface $messenger
   *   The messenger service.
   */
  public function __construct(EntityTypeManagerInterface $entity_type_manager, EntityFieldManagerInterface $entity_field_manager, DateFormatterInterface $date_formatter, TimeInterface $time, Connection $database, MessengerInterface $messenger) {
    $this->entityTypeManager = $entity_type_manager;
    $this->entityFieldManager = $entity_field_manager;
    $this->dateFormatter = $date_formatter;
    $this->time = $time;
    $this->database = $database;
    $this->messenger = $messenger;
  }

  /**
   * Deletes unused revision or an entity if there are no revisions remaining.
   *
   * @param \Drupal\Core\Entity\ContentEntityInterface $composite_revision
   *   The composite revision.
   *
   * @return bool
   *   TRUE if an entity revision was deleted. Otherwise, FALSE.
   */
  public function deleteUnusedRevision(ContentEntityInterface $composite_revision) {

    // If this is the default revision of the composite entity, check if there
    // are other revisions. If there are not, delete the composite entity.
    $composite_storage = $this->entityTypeManager
      ->getStorage($composite_revision
      ->getEntityTypeId());
    if ($composite_revision
      ->isDefaultRevision()) {
      $count = $composite_storage
        ->getQuery()
        ->accessCheck(FALSE)
        ->allRevisions()
        ->condition($composite_storage
        ->getEntityType()
        ->getKey('id'), $composite_revision
        ->id())
        ->count()
        ->execute();
      if ($count <= 1) {
        $composite_revision
          ->delete();
        return TRUE;
      }
    }
    else {

      // Delete the revision if this is not the default one.
      $composite_storage
        ->deleteRevision($composite_revision
        ->getRevisionId());
      return TRUE;
    }
    return FALSE;
  }

  /**
   * Batch operation for checking orphans for a given entity type.
   *
   * @param string $entity_type_id
   *   The entity type id, for example 'paragraph'.
   * @param Iterable|array $context
   *   The context array.
   */
  public function deleteOrphansBatchOperation($entity_type_id, &$context) {
    $composite_type = $this->entityTypeManager
      ->getDefinition($entity_type_id);
    $composite_revision_key = $composite_type
      ->getKey('revision');

    /** @var \Drupal\Core\Entity\ContentEntityStorageInterface $composite_storage */
    $composite_storage = $this->entityTypeManager
      ->getStorage($entity_type_id);
    $batch_size = Settings::get('entity_update_batch_size', 50);
    if (empty($context['sandbox']['total'])) {
      $context['sandbox']['progress'] = 0;
      $context['sandbox']['current_revision_id'] = -1;
      $context['sandbox']['total'] = (int) $composite_storage
        ->getQuery()
        ->allRevisions()
        ->accessCheck(FALSE)
        ->count()
        ->execute();
    }
    if (!isset($context['results'][$entity_type_id])) {
      $context['results'][$entity_type_id]['entity_count'] = 0;
      $context['results'][$entity_type_id]['revision_count'] = 0;
      $context['results'][$entity_type_id]['start'] = $this->time
        ->getRequestTime();
    }

    // Get the next batch of revision ids from the selected entity type.
    // @todo Replace with an entity query on all revisions with a revision ID
    //   condition after https://www.drupal.org/project/drupal/issues/2766135.
    $revision_table = $composite_type
      ->getRevisionTable();
    $entity_revision_ids = $this->database
      ->select($revision_table, 'r')
      ->fields('r', [
      $composite_revision_key,
    ])
      ->range(0, $batch_size)
      ->orderBy($composite_revision_key)
      ->condition($composite_revision_key, $context['sandbox']['current_revision_id'], '>')
      ->execute()
      ->fetchCol();

    /** @var \Drupal\Core\Entity\ContentEntityInterface $composite_revision */
    foreach ($composite_storage
      ->loadMultipleRevisions($entity_revision_ids) as $composite_revision) {
      $context['sandbox']['progress']++;
      $context['sandbox']['current_revision_id'] = $composite_revision
        ->getRevisionId();
      if ($this
        ->isUsed($composite_revision)) {
        continue;
      }
      if ($this
        ->deleteUnusedRevision($composite_revision)) {
        $context['results'][$entity_type_id]['revision_count']++;
        if ($composite_revision
          ->isDefaultRevision()) {
          $context['results'][$entity_type_id]['entity_count']++;
        }
      }
    }

    // This entity type is completed if no new revision ids were found or the
    // total is reached.
    if ($entity_revision_ids && $context['sandbox']['progress'] < $context['sandbox']['total']) {
      $context['finished'] = $context['sandbox']['progress'] / $context['sandbox']['total'];
    }
    else {
      $context['finished'] = 1;
      $context['results'][$entity_type_id]['end'] = $this->time
        ->getRequestTime();
    }
    $interval = $this->dateFormatter
      ->formatInterval($this->time
      ->getRequestTime() - $context['results'][$entity_type_id]['start']);
    $context['message'] = t('Checked @entity_type revisions for orphans: @current of @total in @interval (@deletions deleted)', [
      '@entity_type' => $composite_type
        ->getLabel(),
      '@current' => $context['sandbox']['progress'],
      '@total' => $context['sandbox']['total'],
      '@interval' => $interval,
      '@deletions' => $context['results'][$entity_type_id]['revision_count'],
    ]);
  }

  /**
   * Batch dispatch submission finished callback.
   */
  public static function batchSubmitFinished($success, $results, $operations) {
    return \Drupal::service('entity_reference_revisions.orphan_purger')
      ->doBatchSubmitFinished($success, $results, $operations);
  }

  /**
   * Sets a batch for executing deletion of the orphaned composite entities.
   *
   * @param array $composite_entity_type_ids
   *   An array of composite entity type IDs to remove orphaned items for.
   */
  public function setBatch(array $composite_entity_type_ids) {
    if (empty($composite_entity_type_ids)) {
      return;
    }
    $operations = [];
    foreach ($composite_entity_type_ids as $entity_type_id) {
      $operations[] = [
        '_entity_reference_revisions_orphan_purger_batch_dispatcher',
        [
          'entity_reference_revisions.orphan_purger:deleteOrphansBatchOperation',
          $entity_type_id,
        ],
      ];
    }
    $batch = [
      'operations' => $operations,
      'finished' => [
        EntityReferenceRevisionsOrphanPurger::class,
        'batchSubmitFinished',
      ],
      'title' => $this
        ->t('Removing orphaned entities.'),
      'progress_message' => $this
        ->t('Processed @current of @total entity types.'),
      'error_message' => $this
        ->t('This batch encountered an error.'),
    ];
    batch_set($batch);
  }

  /**
   * Finished callback for the batch process.
   *
   * @param bool $success
   *   Whether the batch completed successfully.
   * @param array $results
   *   The results array.
   * @param array $operations
   *   The operations array.
   */
  public function doBatchSubmitFinished($success, $results, $operations) {
    if ($success) {
      foreach ($results as $entity_type_id => $result) {
        if ($this->entityTypeManager
          ->hasDefinition($entity_type_id)) {
          $entity_type = $this->entityTypeManager
            ->getDefinition($entity_type_id);
          $interval = $this->dateFormatter
            ->formatInterval($result['end'] - $result['start']);
          $this->messenger
            ->addMessage($this
            ->t('@label: Deleted @revision_count revisions (@entity_count entities) in @interval.', [
            '@label' => $entity_type
              ->getLabel(),
            '@revision_count' => $result['revision_count'],
            '@entity_count' => $result['entity_count'],
            '@interval' => $interval,
          ]));
        }
      }
    }
    else {

      // $operations contains the operations that remained unprocessed.
      $error_operation = reset($operations);
      $this->messenger
        ->addError($this
        ->t('An error occurred while processing @operation with arguments : @args', [
        '@operation' => $error_operation[0],
        '@args' => print_r($error_operation[0], TRUE),
      ]));
    }
  }

  /**
   * Checks if the composite entity is used.
   *
   * @param \Drupal\Core\Entity\ContentEntityInterface $composite_revision
   *   The composite revision.
   *
   * @return bool
   *   Whether the composite entity is used, FALSE if it is safe to delete.
   */
  public function isUsed(ContentEntityInterface $composite_revision) {
    $composite_type = $this->entityTypeManager
      ->getDefinition($composite_revision
      ->getEntityTypeId());
    $parent_type_field = $composite_type
      ->get('entity_revision_parent_type_field');
    $parent_type = $composite_revision
      ->get($parent_type_field)->value;
    $parent_field_name_field = $composite_type
      ->get('entity_revision_parent_field_name_field');
    $parent_field_name = $composite_revision
      ->get($parent_field_name_field)->value;
    $status = $this
      ->isValidParent($parent_type, $parent_field_name);
    if ($status !== static::PARENT_VALID) {
      return $status == static::PARENT_INVALID_SKIP ? TRUE : FALSE;
    }

    // Check if the revision is used in any revision of the parent, if that
    // entity type supports revisions.
    $query = $this->entityTypeManager
      ->getStorage($parent_type)
      ->getQuery()
      ->condition("{$parent_field_name}.target_revision_id", $composite_revision
      ->getRevisionId())
      ->range(0, 1)
      ->accessCheck(FALSE);
    if ($this->entityTypeManager
      ->getDefinition($parent_type)
      ->isRevisionable()) {
      $query = $query
        ->allRevisions();
    }
    $revisions = $query
      ->execute();

    // If there are parent revisions where this revision is used, skip it.
    return !empty($revisions);
  }

  /**
   * Checks if the parent type/field is a valid combination that can be queried.
   *
   * @param string $parent_type
   *   Parent entity type ID.
   * @param string $parent_field_name
   *   Parent field name.
   *
   * @return int
   *   static::PARENT_VALID, static::PARENT_INVALID_SKIP or
   *   static::PARENT_INVALID_DELETE.
   */
  protected function isValidParent($parent_type, $parent_field_name) {

    // There is not certainty that this revision is not used because we do not
    // know what to query for if the parent fields are empty.
    if ($parent_type == NULL) {
      return static::PARENT_INVALID_SKIP;
    }
    if (isset($this->validParents[$parent_type][$parent_field_name])) {
      return $this->validParents[$parent_type][$parent_field_name];
    }
    $status = static::PARENT_VALID;

    // If the parent type does not exist anymore, the composite is not used.
    if (!$this->entityTypeManager
      ->hasDefinition($parent_type)) {
      $status = static::PARENT_INVALID_DELETE;
    }
    else {
      $parent_field_definitions = $this->entityFieldManager
        ->getFieldStorageDefinitions($parent_type);
      if (!isset($parent_field_definitions[$parent_field_name])) {
        $status = static::PARENT_INVALID_DELETE;
      }
      elseif (empty($parent_field_definitions[$parent_field_name]
        ->getSchema()['columns']['target_revision_id'])) {
        $status = static::PARENT_INVALID_SKIP;
      }
    }
    $this->validParents[$parent_type][$parent_field_name] = $status;
    return $status;
  }

  /**
   * Returns a list of composite entity types.
   *
   * @return \Drupal\Core\Entity\EntityTypeInterface[]
   *   An array of composite entity types.
   */
  public function getCompositeEntityTypes() {
    $composite_entity_types = [];
    $entity_types = $this->entityTypeManager
      ->getDefinitions();
    foreach ($entity_types as $entity_type) {
      $has_parent_type_field = $entity_type
        ->get('entity_revision_parent_type_field');
      $has_parent_id_field = $entity_type
        ->get('entity_revision_parent_id_field');
      $has_parent_field_name_field = $entity_type
        ->get('entity_revision_parent_field_name_field');
      if ($has_parent_type_field && $has_parent_id_field && $has_parent_field_name_field) {
        $composite_entity_types[] = $entity_type;
      }
    }
    return $composite_entity_types;
  }

}

Members

Namesort descending Modifiers Type Description Overrides
EntityReferenceRevisionsOrphanPurger::$database protected property The database service.
EntityReferenceRevisionsOrphanPurger::$dateFormatter protected property The date formatter service.
EntityReferenceRevisionsOrphanPurger::$entityFieldManager protected property The entity field manager.
EntityReferenceRevisionsOrphanPurger::$entityTypeManager protected property The entity type manager.
EntityReferenceRevisionsOrphanPurger::$messenger protected property The messenger.
EntityReferenceRevisionsOrphanPurger::$time protected property The time service.
EntityReferenceRevisionsOrphanPurger::$validParents protected property List of already checked parents.
EntityReferenceRevisionsOrphanPurger::batchSubmitFinished public static function Batch dispatch submission finished callback.
EntityReferenceRevisionsOrphanPurger::deleteOrphansBatchOperation public function Batch operation for checking orphans for a given entity type.
EntityReferenceRevisionsOrphanPurger::deleteUnusedRevision public function Deletes unused revision or an entity if there are no revisions remaining.
EntityReferenceRevisionsOrphanPurger::doBatchSubmitFinished public function Finished callback for the batch process.
EntityReferenceRevisionsOrphanPurger::getCompositeEntityTypes public function Returns a list of composite entity types.
EntityReferenceRevisionsOrphanPurger::isUsed public function Checks if the composite entity is used.
EntityReferenceRevisionsOrphanPurger::isValidParent protected function Checks if the parent type/field is a valid combination that can be queried.
EntityReferenceRevisionsOrphanPurger::PARENT_INVALID_DELETE constant Parent is invalid and paragraph is safe to delete.
EntityReferenceRevisionsOrphanPurger::PARENT_INVALID_SKIP constant Parent is invalid and usage can not be verified.
EntityReferenceRevisionsOrphanPurger::PARENT_VALID constant Parent is valid.
EntityReferenceRevisionsOrphanPurger::setBatch public function Sets a batch for executing deletion of the orphaned composite entities.
EntityReferenceRevisionsOrphanPurger::__construct public function Constructs a EntityReferenceRevisionsOrphanManager object.
StringTranslationTrait::$stringTranslation protected property The string translation service. 1
StringTranslationTrait::formatPlural protected function Formats a string containing a count of items.
StringTranslationTrait::getNumberOfPlurals protected function Returns the number of plurals supported by a given language.
StringTranslationTrait::getStringTranslation protected function Gets the string translation service.
StringTranslationTrait::setStringTranslation public function Sets the string translation service to use. 2
StringTranslationTrait::t protected function Translates a string to the current language or to a given language.