wse-1.0.x-dev/src/WseWorkspaceAssociation.php

src/WseWorkspaceAssociation.php
<?php

namespace Drupal\wse;

use Drupal\Core\Database\Connection;
use Drupal\Core\Entity\EntityTypeManagerInterface;
use Drupal\Core\Entity\RevisionableInterface;
use Drupal\Core\Utility\Error;
use Drupal\depcalc\DependencyStack;
use Drupal\depcalc\DependentEntityWrapper;
use Drupal\depcalc\DependentEntityWrapperInterface;
use Drupal\workspaces\Entity\Workspace;
use Drupal\workspaces\Event\WorkspacePostPublishEvent;
use Drupal\workspaces\WorkspaceAssociationInterface;
use Drupal\workspaces\WorkspaceInterface;
use Psr\Log\LoggerInterface;
use Symfony\Component\EventDispatcher\EventSubscriberInterface;

/**
 * Provides an override for core's workspace association service.
 */
class WseWorkspaceAssociation implements WorkspaceAssociationInterface, EventSubscriberInterface {

  /**
   * A multidimensional array of entity IDs that are associated to a workspace.
   *
   * The first level keys are workspace IDs, the second level keys are entity
   * type IDs, and the third level array are entity IDs, keyed by revision IDs.
   *
   * @var array
   */
  protected array $associatedRevisions = [];

  /**
   * A multidimensional array of entity IDs that were created in a workspace.
   *
   * The first level keys are workspace IDs, the second level keys are entity
   * type IDs, and the third level array are entity IDs, keyed by revision IDs.
   *
   * @var array
   */
  protected array $associatedInitialRevisions = [];

  public function __construct(
    protected WorkspaceAssociationInterface $innerWorkspaceAssociation,
    protected EntityTypeManagerInterface $entityTypeManager,
    protected Connection $database,
    protected LoggerInterface $logger,
  ) {}

  /**
   * {@inheritdoc}
   */
  public function trackEntity(RevisionableInterface $entity, WorkspaceInterface $workspace) {
    $this->innerWorkspaceAssociation->trackEntity($entity, $workspace);
    $this->associatedRevisions = $this->associatedInitialRevisions = [];
  }

  /**
   * Moves the given entity to another workspace.
   *
   * @param \Drupal\Core\Entity\RevisionableInterface $entity
   *   The entity to move.
   * @param \Drupal\workspaces\WorkspaceInterface $source_workspace
   *   The workspace in which the entity is currently tracked.
   * @param \Drupal\workspaces\WorkspaceInterface $target_workspace
   *   The workspace in which the entity will be tracked.
   * @param bool $include_dependencies
   *   (optional) Whether to move all the dependencies too. Defaults to TRUE.
   */
  public function moveEntity(RevisionableInterface $entity, WorkspaceInterface $source_workspace, WorkspaceInterface $target_workspace, bool $include_dependencies = TRUE) {
    $affected_entity_ids[$entity->getEntityTypeId()][$entity->id()] = TRUE;

    // Use the 'depcalc' module, if available, for gathering all the
    // dependencies of the moved entity.
    if ($include_dependencies && \Drupal::moduleHandler()->moduleExists('depcalc')) {
      $this->gatherAffectedDependencies($affected_entity_ids, $entity, $source_workspace);
    }

    $transaction = $this->database->startTransaction();
    try {
      \Drupal::service('workspaces.manager')->executeOutsideWorkspace(function () use ($affected_entity_ids, $source_workspace, $target_workspace) {
        // Move all workspace-specific revisions to the new workspace.
        foreach ($affected_entity_ids as $entity_type_id => $entity_ids) {
          $affected_revision_ids = $this->getAssociatedRevisions($source_workspace->id(), $entity_type_id, array_keys($entity_ids));
          $affected_revisions = $this->entityTypeManager->getStorage($entity_type_id)
            ->loadMultipleRevisions(array_keys($affected_revision_ids));

          foreach ($affected_revisions as $revision) {
            $field_name = $revision->getEntityType()
              ->getRevisionMetadataKey('workspace');
            $revision->{$field_name}->target_id = $target_workspace->id();
            $revision->setSyncing(TRUE);
            $revision->save();

            // Delete the association entries for the source workspace, and
            // track the entity in the target workspace.
            $this->innerWorkspaceAssociation->deleteAssociations($source_workspace->id(), $revision->getEntityTypeId(), [$revision->id()]);
            $this->innerWorkspaceAssociation->trackEntity($revision, $target_workspace);
          }
        }
      });
    }
    catch (\Exception $e) {
      $transaction->rollBack();
      Error::logException($this->logger, $e);
      throw $e;
    }
  }

  /**
   * Retrieves all the dependencies of an entity.
   */
  protected function gatherAffectedDependencies(array &$affected_entity_ids, RevisionableInterface $entity, WorkspaceInterface $source_workspace) {
    $dependencies = \Drupal::service('workspaces.manager')->executeInWorkspace($source_workspace->id(), function () use ($entity) {
      $wrapper = new DependentEntityWrapper($entity);
      $dependency_stack = new DependencyStack();
      $dependency_stack->ignoreCache(TRUE);
      $dependency_stack->ignoreConfig(TRUE);

      return \Drupal::service('entity.dependency.calculator')->calculateDependencies($wrapper, $dependency_stack);
    });

    /** @var \Drupal\depcalc\DependentEntityWrapperInterface $dependency */
    $tracked_entities = $this->getTrackedEntities($source_workspace->id());
    foreach ($dependencies as $dependency) {
      if ($dependency instanceof DependentEntityWrapperInterface
          && isset($tracked_entities[$dependency->getEntityTypeId()])
          && in_array($dependency->getId(), $tracked_entities[$dependency->getEntityTypeId()])) {
        $affected_entity_ids[$dependency->getEntityTypeId()][$dependency->getId()] = TRUE;
      }
    }
  }

  /**
   * Discards the changes for an entity in the given workspace.
   *
   * @param \Drupal\Core\Entity\RevisionableInterface $entity
   *   The entity to discard.
   * @param \Drupal\workspaces\WorkspaceInterface $workspace
   *   The workspace in which the entity will be discarded.
   * @param bool $include_dependencies
   *   (optional) Whether to discard all the dependencies. Defaults to FALSE.
   */
  public function discardEntity(RevisionableInterface $entity, WorkspaceInterface $workspace, bool $include_dependencies = FALSE) {
    $affected_entity_ids[$entity->getEntityTypeId()][$entity->id()] = TRUE;

    // Use the 'depcalc' module, if available, for gathering all the
    // dependencies of the moved entity.
    if ($include_dependencies && \Drupal::moduleHandler()->moduleExists('depcalc')) {
      $this->gatherAffectedDependencies($affected_entity_ids, $entity, $workspace);
    }

    $transaction = $this->database->startTransaction();
    try {
      \Drupal::service('workspaces.manager')->executeOutsideWorkspace(function () use ($affected_entity_ids, $workspace) {
        // Discard all workspace-specific revisions of the entity and its
        // dependencies.
        foreach ($affected_entity_ids as $entity_type_id => $entity_ids) {
          $associated_entity_storage = $this->entityTypeManager->getStorage($entity_type_id);

          foreach (array_keys($entity_ids) as $entity_id) {
            $associated_revisions = $this->getAssociatedRevisions($workspace->id(), $entity_type_id, [$entity_id]);

            // Sort the associated revisions in reverse ID order, so we can
            // delete the most recent revisions first.
            krsort($associated_revisions);

            // Get a list of default revisions tracked by the given workspace,
            // because they need to be handled differently than pending
            // revisions.
            $initial_revision_ids = $this->getAssociatedInitialRevisions($workspace->id(), $entity_type_id, [$entity_id]);

            foreach (array_keys($associated_revisions) as $revision_id) {
              // If the workspace is tracking the entity's default revision
              // (i.e. the entity was created inside that workspace), we need to
              // delete the whole entity after all of its pending revisions are
              // gone.
              if (isset($initial_revision_ids[$revision_id])) {
                $associated_entity_storage->delete([$associated_entity_storage->load($initial_revision_ids[$revision_id])]);
              }
              else {
                // Delete the associated entity revision.
                $associated_entity_storage->deleteRevision($revision_id);
              }
            }
          }
        }
      });
    }
    catch (\Exception $e) {
      $transaction->rollBack();
      Error::logException($this->logger, $e);
      throw $e;
    }
  }

  /**
   * {@inheritdoc}
   */
  public function workspaceInsert(WorkspaceInterface $workspace) {
    $this->innerWorkspaceAssociation->workspaceInsert($workspace);
  }

  /**
   * {@inheritdoc}
   */
  public function getTrackedEntities($workspace_id, $entity_type_id = NULL, $entity_ids = NULL, $offset = NULL, $limit = NULL) {
    // For closed workspaces, try to use the data from the published revision
    // storage.
    $workspace = Workspace::load($workspace_id);
    if ($workspace && wse_workspace_get_status($workspace) === WSE_STATUS_CLOSED) {
      // We can't inject the published revision storage because it causes a
      // circular dependency with the workspace association service.
      return \Drupal::service('wse.published_revision_storage')->getPublishedRevisions($workspace_id, $offset, $limit);
    }

    return $this->innerWorkspaceAssociation->getTrackedEntities($workspace_id, $entity_type_id, $entity_ids, $offset, $limit);
  }

  /**
   * {@inheritdoc}
   */
  public function getTrackedEntitiesForListing($workspace_id, ?int $pager_id = NULL, int|false $limit = 50): array {
    return $this->innerWorkspaceAssociation->getTrackedEntitiesForListing($workspace_id, $pager_id, $limit);
  }

  /**
   * {@inheritdoc}
   */
  public function getAssociatedRevisions($workspace_id, $entity_type_id, $entity_ids = NULL) {
    if (isset($this->associatedRevisions[$workspace_id][$entity_type_id])) {
      if ($entity_ids) {
        return array_intersect($this->associatedRevisions[$workspace_id][$entity_type_id], $entity_ids);
      }
      else {
        return $this->associatedRevisions[$workspace_id][$entity_type_id];
      }
    }

    // WSE ensures that workspaces have unique IDs, so we can simplify core's
    // way of retrieving the associated revisions.
    $entity_type = $this->entityTypeManager->getDefinition($entity_type_id);
    $query = $this->entityTypeManager->getStorage($entity_type_id)
      ->getQuery()
      ->accessCheck(FALSE)
      ->allRevisions()
      ->condition($entity_type->get('revision_metadata_keys')['workspace'], $workspace_id, '=')
      ->sort($entity_type->getKey('revision'), 'ASC');

    if ($entity_ids) {
      $query->condition($entity_type->getKey('id'), $entity_ids, 'IN');
    }

    $result = $query->execute();

    // Cache the list of associated entity IDs if the full list was requested.
    if (!$entity_ids) {
      $this->associatedRevisions[$workspace_id][$entity_type_id] = $result;
    }

    return $result;
  }

  /**
   * {@inheritdoc}
   */
  public function getAssociatedInitialRevisions(string $workspace_id, string $entity_type_id, array $entity_ids = []) {
    if (isset($this->associatedInitialRevisions[$workspace_id][$entity_type_id])) {
      if ($entity_ids) {
        return array_intersect($this->associatedInitialRevisions[$workspace_id][$entity_type_id], $entity_ids);
      }
      else {
        return $this->associatedInitialRevisions[$workspace_id][$entity_type_id];
      }
    }

    // WSE ensures that workspaces have unique IDs, so we can simplify core's
    // way of retrieving the associated initial (default) revisions.
    $entity_type = $this->entityTypeManager->getDefinition($entity_type_id);
    $query = $this->entityTypeManager->getStorage($entity_type_id)
      ->getQuery()
      ->accessCheck(FALSE)
      ->allRevisions()
      ->condition($entity_type->get('revision_metadata_keys')['revision_default'], TRUE)
      ->condition($entity_type->get('revision_metadata_keys')['workspace'], $workspace_id, '=');

    if ($entity_ids) {
      $query->condition($entity_type->getKey('id'), $entity_ids, 'IN');
    }

    $result = $query->execute();

    // Cache the list of associated entity IDs if the full list was requested.
    if (!$entity_ids) {
      $this->associatedInitialRevisions[$workspace_id][$entity_type_id] = $result;
    }

    return $result;
  }

  /**
   * {@inheritdoc}
   */
  public function getEntityTrackingWorkspaceIds(RevisionableInterface $entity, bool $latest_revision = FALSE) {
    return $this->innerWorkspaceAssociation->getEntityTrackingWorkspaceIds($entity);
  }

  /**
   * {@inheritdoc}
   */
  public function postPublish(WorkspaceInterface $workspace) {
    $this->innerWorkspaceAssociation->postPublish($workspace);
  }

  /**
   * {@inheritdoc}
   */
  public function deleteAssociations($workspace_id = NULL, $entity_type_id = NULL, $entity_ids = NULL, $revision_ids = NULL) {
    $this->innerWorkspaceAssociation->deleteAssociations($workspace_id, $entity_type_id, $entity_ids, $revision_ids);
    $this->associatedRevisions = $this->associatedInitialRevisions = [];
  }

  /**
   * {@inheritdoc}
   */
  public function initializeWorkspace(WorkspaceInterface $workspace) {
    $this->innerWorkspaceAssociation->initializeWorkspace($workspace);
    $this->associatedRevisions = $this->associatedInitialRevisions = [];
  }

  /**
   * {@inheritdoc}
   */
  public static function getSubscribedEvents(): array {
    // Workspace association records cleanup should happen as late as possible.
    $events[WorkspacePostPublishEvent::class][] = ['onPostPublish', -500];
    return $events;
  }

  /**
   * Triggers clean-up operations after a workspace is published.
   *
   * @param \Drupal\workspaces\Event\WorkspacePostPublishEvent $event
   *   The workspace publish event.
   */
  public function onPostPublish(WorkspacePostPublishEvent $event): void {
    $this->innerWorkspaceAssociation->onPostPublish($event);
  }

}

Главная | Обратная связь

drupal hosting | друпал хостинг | it patrol .inc