You are here

ConfigEntityRevisionsRevisionStorageHandler.php in Config Entity Revisions 8.2

File

src/Entity/Handler/ConfigEntityRevisionsRevisionStorageHandler.php
View source
<?php

namespace Drupal\config_entity_revisions\Entity\Handler;

use Drupal\config_entity_revisions\ConfigEntityRevisionsConfigEntityInterface;
use Drupal\config_entity_revisions\ConfigEntityRevisionsRevisionStorageHandlerInterface;
use Drupal\config_entity_revisions\Entity\ConfigEntityRevisions;
use Drupal\Core\Cache\CacheBackendInterface;
use Drupal\Core\Cache\MemoryCache\MemoryCacheInterface;
use Drupal\Core\Database\Connection;
use Drupal\Component\Datetime\Time;
use Drupal\Core\Entity\ContentEntityInterface;
use Drupal\Core\Entity\EntityManagerInterface;
use Drupal\Core\Entity\EntityTypeBundleInfo;
use Drupal\Core\Entity\EntityTypeInterface;
use Drupal\Core\Entity\EntityTypeManager;
use Drupal\Core\Entity\Sql\SqlContentEntityStorage;
use Drupal\Core\Language\LanguageManagerInterface;
use Drupal\Core\Session\AccountProxyInterface;
use mysql_xdevapi\Exception;
use Symfony\Component\Serializer\Serializer;
use Symfony\Component\DependencyInjection\ContainerInterface;

/**
 * Class ConfigEntityRevisionsRevisionStorageHandler.
 *
 * @package Drupal\config_entity_revisions\Entity
 */
class ConfigEntityRevisionsRevisionStorageHandler extends SqlContentEntityStorage implements ConfigEntityRevisionsRevisionStorageHandlerInterface {

  /**
   * The config entity being used.
   *
   * @var \Drupal\config_entity_revisions\Entity\ConfigEntityRevisions
   */
  protected $configEntity;

  /**
   * An array containing the statuses of revisions for this entity.
   *
   * @var array
   */
  protected $revisionStates = NULL;

  /**
   * The serialiser service.
   *
   * @var \Symfony\Component\Serializer\Serializer
   */
  protected $serialiser = NULL;

  /**
   * The current user.
   *
   * @var \Drupal\Core\Session\AccountProxyInterface
   */
  protected $currentUser = NULL;

  /**
   * The date time service.
   *
   * @var \Drupal\Component\Datetime\Time
   */
  protected $dateTimeService = NULL;

  /**
   * The entity type manager.
   *
   * @var \Drupal\Core\Entity\EntityTypeManager
   */

  /**
   * Constructs a ConfigEntityRevisionsRevisionStorageHandler class instance.
   *
   * @param \Drupal\Core\Entity\EntityTypeInterface $entity_type
   *   The entity type definition.
   * @param \Drupal\Core\Database\Connection $database
   *   The database connection to be used.
   * @param \Drupal\Core\Entity\EntityManagerInterface $entity_manager
   *   The entity manager.
   * @param \Drupal\Core\Cache\CacheBackendInterface $cache
   *   The cache backend to be used.
   * @param \Drupal\Core\Language\LanguageManagerInterface $language_manager
   *   The language manager.
   * @param \Drupal\Core\Cache\MemoryCache\MemoryCacheInterface|null $memory_cache
   *   The memory cache backend to be used.
   * @param \Symfony\Component\Serializer\Serializer $serialiser
   *   The serialiser service instance.
   * @param \Drupal\Core\Session\AccountProxyInterface $current_user
   *   The current user object.
   * @param \Drupal\Component\Datetime\Time $date_time
   *   The date/time service.
   * @param \Drupal\Core\Entity\EntityTypeBundleInfoInterface $bundle_info
   *   The entity type bundle info.
   * @param \Drupal\Core\Entity\EntityTypeManager $entity_type_manager
   *   The entity type manager.
   */
  public function __construct(EntityTypeInterface $entity_type, Connection $database, EntityManagerInterface $entity_manager, CacheBackendInterface $cache, LanguageManagerInterface $language_manager, MemoryCacheInterface $memory_cache = NULL, Serializer $serialiser, AccountProxyInterface $current_user, Time $date_time, EntityTypeBundleInfo $bundle_info, EntityTypeManager $entity_type_manager) {
    parent::__construct($entity_type, $database, $entity_manager, $cache, $language_manager, $memory_cache, $bundle_info, $entity_type_manager);
    $this->serialiser = $serialiser;
    $this->currentUser = $current_user;
    $this->dateTimeService = $date_time;
    $this->entityTypeManager = $entity_type_manager;
  }

  /**
   * {@inheritdoc}
   */
  public static function createInstance(ContainerInterface $container, EntityTypeInterface $entity_type) {
    return new static($entity_type, $container
      ->get('database'), $container
      ->get('entity.manager'), $container
      ->get('cache.entity'), $container
      ->get('language_manager'), $container
      ->get('entity.memory_cache'), $container
      ->get('serializer'), $container
      ->get('current_user'), $container
      ->get('datetime.time'), $container
      ->get('entity_type.bundle.info'), $container
      ->get('entity_type.manager'));
  }

  /**
   * Set the config entity under consideration.
   *
   * @param \Drupal\config_entity_revisions\Entity\ConfigEntityRevisionsConfigEntityInterface $configEntity
   *   The config_entity_revisions entity.
   */
  public function setConfigEntity(ConfigEntityRevisionsConfigEntityInterface $configEntity) {
    $this->configEntity = $configEntity;
  }

  /**
   * Populate the revision states array.
   */
  protected function ensureRevisionStatesLoaded() {
    if (is_null($this->revisionStates)) {
      $this->revisionStates = $this->database
        ->select("config_entity_revisions_revision", 'c')
        ->fields('c', [])
        ->condition($this->entityType
        ->getKey('id'), $this->configEntity
        ->getContentEntityID())
        ->orderby('revision', 'DESC')
        ->execute()
        ->fetchAll();
    }
  }

  /**
   * Gets the latest published revision ID of the entity.
   *
   * @param int $ignore
   *   A revision ID to ignore (optional).
   *
   * @return int
   *   The identifier of the latest published revision of the entity, or NULL
   *   if the entity does not have a published revision.
   */
  public function getLatestPublishedRevisionId($ignore = NULL) {
    $this
      ->ensureRevisionStatesLoaded();
    foreach ($this->revisionStates as $state) {
      if (!is_null($ignore) && $state->revision == $ignore) {
        continue;
      }
      if ($state->published) {
        return $state->revision;
      }
    }
    return NULL;
  }

  /**
   * Gets the latest published revision ID of the entity.
   *
   * @param int $ignore
   *   A revision ID to ignore (optional).
   *
   * @return int
   *   The identifier of the latest published revision of the entity, or NULL
   *   if the entity does not have a published revision.
   */
  public function getLatestPublishedRevision($ignore = NULL) {
    $revision = NULL;
    $revision_id = $this
      ->getLatestPublishedRevisionId($ignore);
    if ($revision_id) {
      $revision = $this
        ->loadRevision($revision_id);
    }
    return $revision;
  }

  /**
   * Gets the latest revision ID of the entity.
   *
   * @param int $ignore
   *   A revision ID to ignore (optional).
   *
   * @return int
   *   The identifier of the latest published revision of the entity, or NULL
   *   if the entity does not have a published revision.
   */
  public function getLatestRevisionId($ignore = NULL) {
    $this
      ->ensureRevisionStatesLoaded();
    return empty($this->revisionStates) ? NULL : $this->revisionStates[0]->revision;
  }

  /**
   * Gets the latest revision of the entity.
   *
   * @param int $ignore
   *   A revision ID to ignore (optional).
   *
   * @return int
   *   The identifier of the latest published revision of the entity, or NULL
   *   if the entity does not have a published revision.
   */
  public function getLatestRevision($ignore = NULL) {
    $id = $this
      ->getLatestRevisionId($ignore);
    return $id ? $this
      ->loadRevision($id) : NULL;
  }

  /**
   * Gets the latest revision ID of the entity.
   *
   * @param int $ignore
   *   A revision ID to ignore (optional).
   *
   * @return int
   *   The identifier of the latest published revision of the entity, or NULL
   *   if the entity does not have a published revision.
   */
  public function getLatestPublishedRevisionOrLatestId($ignore = NULL) {
    $revision_id = $this
      ->getLatestPublishedRevisionId($ignore);
    return $revision_id ? $revision_id : $this
      ->getLatestRevisionId($ignore);
  }

  /**
   * Get a Config Entity instance from Content Entity Revision.
   *
   * @param \Drupal\config_entity_revisions\Entity\ConfigEntityRevisions $content_entity
   *   The ConfigEntityRevisions entity instance.
   *
   * @return \Drupal\config_entity_revisions\Entity\ConfigEntityRevisions
   *   The deserialised config entity.
   */
  public function getConfigEntity(ConfigEntityRevisions $content_entity, $class = '') {
    if ($class == '') {
      if (!$this->configEntity) {
        throw new Exception('No class name or instance suppled to ConfigEntityRevisionsRevisionStorageHandler::getConfigEntity');
      }
      $class = get_class($this->configEntity);
    }
    $config_entity = $this->serialiser
      ->deserialize($content_entity
      ->get('configuration')->value, $class, 'json');

    // The result of serialising and then deserialising is not an exact
    // copy of the original. This causes problems downstream if we don't fix
    // a few attributes here.
    $config_entity
      ->set('settingsOriginal', $config_entity
      ->get('settings'));
    $config_entity
      ->set('enforceIsNew', FALSE);
    $config_entity->loadedRevisionId = $content_entity
      ->getRevisionId();
    if ($config_entity->moderation_state) {
      $config_entity->moderation_state->value = $content_entity->moderation_state->value;
    }
    return $config_entity;
  }

  /**
   * Create an initial revision record.
   *
   * @param \Drupal\config_entity_revisions\ConfigEntityRevisionsConfigEntityInterface $config_entity
   *   The configuration entity.
   *
   * @return \Drupal\Core\Entity\ContentEntityInterface|null
   *   The content entity created.
   *
   * @throws \Drupal\Core\Entity\EntityStorageException
   */
  public function createInitialRevision(ConfigEntityRevisionsConfigEntityInterface $config_entity) {
    $contentID = $config_entity
      ->getContentEntityID();

    // Already created.
    if ($contentID) {
      return NULL;
    }

    // Make a content revisions entity using either the previous version of
    // the config entity or (failing that) the current version.
    // We're doing this here rather than in the update hook because we want
    // to save the reference to the entity config entity version that is being
    // saved now.

    /* @var $originalEntity ConfigEntityInterface */
    $originalEntity = $config_entity
      ->contentEntityStorage()
      ->load($config_entity
      ->id());
    $source = $originalEntity ? $originalEntity : $config_entity;
    $bundle_type = $config_entity
      ->getBundleName();

    /* @var $contentEntity ContentEntityInterface */
    $contentEntity = $config_entity
      ->contentEntityStorage()
      ->create([
      'form' => $source
        ->get('uuid'),
      'configuration' => $this->serialiser
        ->serialize($source, 'json'),
      'revision_user' => $this->currentUser
        ->id(),
      'revision_creation_time' => $this->dateTimeService
        ->getRequestTime(),
      'revision_log_message' => 'Original revision.',
      'moderation_state' => 'draft',
      'type' => $bundle_type,
    ]);
    $contentEntity
      ->save();
    $contentID = $contentEntity
      ->id();
    $config_entity
      ->setContentEntityID($contentID);
    $config_entity
      ->save();
    return $contentEntity;
  }

  /**
   * Create revision when a new config entity version is saved.
   *
   * @param \Drupal\config_entity_revisions\ConfigEntityRevisionsConfigEntityInterface $config_entity
   *   The configuration entity.
   *
   * @throws \Drupal\Core\Entity\EntityStorageException
   */
  public function createUpdateRevision(ConfigEntityRevisionsConfigEntityInterface $config_entity) {

    /* @var $revisionsEntity \Drupal\config_entity_revisions\ConfigEntityRevisionsConfigEntityInterface */
    $revisionsEntity = NULL;
    $previous_state = FALSE;
    $moderated = FALSE;
    if (!empty($config_entity
      ->getRevisionId())) {
      $revisionsEntity = $config_entity
        ->contentEntityStorage()
        ->loadRevision($config_entity
        ->getRevisionID());
      $moderated = $revisionsEntity
        ->hasField('moderation_state');
      if ($moderated) {
        $previous_state = $revisionsEntity->moderation_state->value;
      }
    }
    else {
      $contentID = $config_entity
        ->getContentEntityID();
      if (is_null($contentID)) {

        // No related content entity yet.
        return;
      }
      $revisionsEntity = $config_entity
        ->contentEntityStorage()
        ->load($contentID);
    }
    $revisionsEntity
      ->set('configuration', $this->serialiser
      ->serialize($config_entity, 'json'));
    $revisionsEntity
      ->setRevisionUserId($this->currentUser
      ->id());
    $revisionsEntity
      ->setRevisionCreationTime($this->dateTimeService
      ->getRequestTime());
    $new_message = $config_entity
      ->get('revision_log_message')[0]['value'];
    $new_revision = $config_entity
      ->get('revision');
    $moderation_state = $moderated ? $config_entity->moderation_state->value : NULL;
    $published = NULL;
    if (!is_null($moderation_state)) {
      $published = $moderation_state == 'published';
    }
    if (is_null($moderation_state) && is_null($new_revision)) {
      $new_revision = FALSE;
    }
    if (!is_null($new_message)) {
      $revisionsEntity
        ->setRevisionLogMessage($config_entity
        ->get('revision_log_message')[0]['value']);
    }
    $revisionsEntity
      ->setNewRevision($new_revision);
    if (!is_null($moderation_state)) {
      $revisionsEntity->moderation_state = $moderation_state;
    }
    if (!is_null($published)) {
      if ($published) {
        $revisionsEntity
          ->setPublished();
      }
      else {
        $revisionsEntity
          ->setUnpublished();
      }

      // @TODO Published <> default?
      $revisionsEntity
        ->isDefaultRevision($published);
    }
    $revisionsEntity
      ->save();
    if (($previous_state == 'published') !== $published) {

      // Modify another revision to be published and default if possible.
      $this
        ->resetDefaultRevision($revisionsEntity);
    }
    return $revisionsEntity
      ->getRevisionId();
  }

  /**
   * Make default the most recently published or most recent revision.
   *
   * This is needed because content_moderation has a concept of a default
   * revision, which this module doesn't really care about, but which will
   * cause problems if we attempt to delete a revision that's marked as the
   * default.
   *
   * @param \Drupal\Core\Entity\ContentEntityInterface $content_entity
   *   The content (revisions) entity.
   * @param int $deleting
   *   An optional revision ID that is about to be deleted.
   */
  public function resetDefaultRevision(ContentEntityInterface $content_entity, $deleting = NULL) {
    $content_entity_id = $content_entity
      ->id();

    // Ensure our data is up to date after a save.
    $this->revisionStates = NULL;
    $this
      ->ensureRevisionStatesLoaded();
    $first_published = NULL;
    $first_revision = NULL;
    $remove_default = [];
    foreach ($this->revisionStates as $revision) {
      if (!$first_revision && $revision->revision != $deleting) {
        $first_revision = $revision;
      }
      if ($revision->published && !$first_published && $revision->revision != $deleting) {
        $first_published = $revision;
      }
      if ($revision->revision_default) {
        $remove_default[$revision->revision] = 1;
      }
    }
    $default_revision = $first_published ?: $first_revision;
    if ($default_revision) {
      unset($remove_default[$default_revision->revision]);
    }
    if (!empty($remove_default)) {
      $this->database
        ->update("config_entity_revisions_revision")
        ->condition('revision', array_keys($remove_default), 'IN')
        ->fields([
        'revision_default' => 0,
      ])
        ->execute();
    }
    if ($default_revision) {
      $this->database
        ->update("config_entity_revisions")
        ->condition('id', $content_entity_id)
        ->fields([
        'revision' => $default_revision->revision,
      ])
        ->execute();
      if ($this->database
        ->schema()
        ->tableExists('content_moderation_state_field_revision')) {
        $content_moderation_rev_ids = $this->database
          ->select('content_moderation_state_field_revision', 'c')
          ->condition('content_entity_type_id', 'config_entity_revisions')
          ->condition('content_entity_id', $content_entity_id)
          ->condition('content_entity_revision_id', $default_revision->revision)
          ->fields('c', [
          'id',
          'revision_id',
        ])
          ->execute()
          ->fetchAssoc();
        $this->database
          ->update('content_moderation_state')
          ->condition('id', $content_moderation_rev_ids['id'])
          ->fields([
          'revision_id' => $content_moderation_rev_ids['revision_id'],
        ])
          ->execute();
      }
    }
  }

  /**
   * Get a list of revision IDs for a content entity.
   */
  public function getRevisionIds($content_entity_id) {
    $revisions = $this->connection
      ->select("config_entity_revisions_revision", 'c')
      ->fields('c', [
      'revision',
    ])
      ->condition('id', $content_entity_id)
      ->orderBy('revision', 'DESC')
      ->execute()
      ->fetchCol();
    return $revisions;
  }

  /**
   * Delete a single revision.
   *
   * @param \Drupal\Core\Entity\ContentEntityInterface $revision
   *   The revision to be deleted.
   */
  public function deleteSingleRevision(ContentEntityInterface $revision) {
    $was_default = $revision
      ->isDefaultRevision();
    if ($was_default) {

      // Change the default to the next newer (if we're deleting the default,
      // there must be no published revisions so it doesn't matter which we
      // choose. Ensure revision_default isn't set on our revision in
      // config_entity_revisions_revision - $was_default can return FALSE
      // even when that value is 1, and that will cause the content moderation
      // module (which does look at that field) to throw an exception.
      $revisions = $this
        ->getRevisionIds($revision
        ->id());
      $revision_to_use = $revisions[0] == $revision
        ->getRevisionId() ? $revisions[1] : $revisions[0];
      $new_default = $this->entityTypeManager
        ->getStorage('config_entity_revisions')
        ->loadRevision($revision_to_use);
      $new_default
        ->enforceIsNew(FALSE);
      $new_default
        ->isDefaultRevision(TRUE);
      $new_default
        ->save();
      $content_moderation_state_storage = $this->entityTypeManager
        ->getStorage('content_moderation_state');
      if ($content_moderation_state_storage) {
        $content_moderation_ids = $content_moderation_state_storage
          ->getQuery()
          ->allRevisions()
          ->condition('content_entity_type_id', 'config_entity_revisions')
          ->condition('content_entity_revision_id', $revision_to_use)
          ->execute();

        // Revision default is whether the revision WAS default when created.
        // The content moderation module doesn't provide a method to update the
        // default revision so we need to do a direct update query and clear
        // caches.
        $target_entity_type = $this->entityTypeManager
          ->getDefinition('content_moderation_state');
        $this->connection
          ->update($target_entity_type
          ->getBaseTable())
          ->condition('id', array_values($content_moderation_ids)[0])
          ->fields([
          'revision_id' => array_keys($content_moderation_ids)[0],
        ])
          ->execute();
        $content_moderation_state_storage
          ->resetCache();
      }
    }
    $this->entityTypeManager
      ->getStorage('config_entity_revisions')
      ->deleteRevision($revision
      ->getRevisionId());
  }

  /**
   * Delete revisions when a config entity is deleted.
   *
   * @param ConfigEntityRevisionsConfigEntityInterface $config_entity
   *   The configuration entity being deleted.
   *
   * @throws \Drupal\Component\Plugin\Exception\InvalidPluginDefinitionException
   * @throws \Drupal\Component\Plugin\Exception\PluginNotFoundException
   * @throws \Drupal\Core\Entity\EntityStorageException
   */
  public function deleteRevisions(ConfigEntityRevisionsConfigEntityInterface $config_entity) {
    $contentEntity = $config_entity
      ->getContentEntity();
    if ($contentEntity) {
      $config_entity
        ->contentEntityStorage()
        ->delete([
        $contentEntity,
      ]);
    }
  }

  /**
   * Load a particular revision of a config entity.
   *
   * @param int $revision
   *   The revision ID to load.
   * @param mixed $entity
   *   The entity type to load.
   *
   * @return mixed
   *   The loaded revision or NULL.
   */
  public function loadConfigEntityRevision($revision = NULL, $entity = '') {
    $config_entity_name = $this
      ->configEntityName();
    if (!$entity) {
      $match = \Drupal::service('router')
        ->matchRequest(\Drupal::request());
      $entity = $match[$config_entity_name];
    }
    if (is_string($entity)) {
      $entity = $this->entityTypeManager
        ->getStorage($config_entity_name)
        ->load($entity);
    }
    if ($revision) {
      $revisionsEntity = $this->entityTypeManager
        ->getStorage('config_entity_revisions')
        ->loadRevision($revision);
      $entity = \Drupal::getContainer()
        ->get('serializer')
        ->deserialize($revisionsEntity
        ->get('configuration')->value, get_class($entity), 'json');

      // The result of serialising and then deserialising is not an exact
      // copy of the original. This causes problems downstream if we don't fix
      // a few attributes here.
      $entity
        ->set('settingsOriginal', $entity
        ->get('settings'));
      $entity
        ->set('enforceIsNew', FALSE);
    }
    return $entity;
  }

}

Classes

Namesort descending Description
ConfigEntityRevisionsRevisionStorageHandler Class ConfigEntityRevisionsRevisionStorageHandler.