multiversion-8.x-1.0-beta34/src/MultiversionManager.php
src/MultiversionManager.php
<?php
namespace Drupal\multiversion;
use Drupal\Core\Entity\ContentEntityInterface;
use Drupal\Core\Entity\ContentEntityTypeInterface;
use Drupal\Core\Entity\EntityFieldManagerInterface;
use Drupal\Core\Entity\EntityTypeInterface;
use Drupal\Core\Entity\EntityTypeManagerInterface;
use Drupal\Core\Entity\FieldableEntityInterface;
use Drupal\Core\State\StateInterface;
use Drupal\Core\Language\LanguageManagerInterface;
use Drupal\Core\Cache\CacheBackendInterface;
use Drupal\Core\Database\Connection;
use Drupal\multiversion\Event\MultiversionManagerEvent;
use Drupal\multiversion\Event\MultiversionManagerEvents;
use Drupal\multiversion\Workspace\WorkspaceManagerInterface;
use Symfony\Component\DependencyInjection\ContainerAwareInterface;
use Symfony\Component\DependencyInjection\ContainerAwareTrait;
use Symfony\Component\EventDispatcher\EventDispatcherInterface;
use Symfony\Component\Serializer\Serializer;
class MultiversionManager implements MultiversionManagerInterface, ContainerAwareInterface {
use ContainerAwareTrait;
const TO_TMP = 'to_tmp';
const FROM_TMP = 'from_tmp';
const OP_ENABLE = 'enable';
const OP_DISABLE = 'disable';
/**
* @var \Drupal\multiversion\Workspace\WorkspaceManagerInterface
*/
protected $workspaceManager;
/**
* @var \Symfony\Component\Serializer\Serializer
*/
protected $serializer;
/**
* @var \Drupal\Core\Entity\EntityTypeManagerInterface
*/
protected $entityTypeManager;
/**
* @var \Drupal\Core\State\StateInterface
*/
protected $state;
/**
* @var \Drupal\Core\Language\LanguageManagerInterface
*/
protected $languageManager;
/**
* @var \Drupal\Core\Cache\CacheBackendInterface
*/
protected $cache;
/**
* The database connection.
*
* @var \Drupal\Core\Database\Connection
*/
protected $connection;
/**
* The entity field manager.
*
* @var \Drupal\Core\Entity\EntityFieldManagerInterface
*/
protected $entityFieldManager;
/**
* @var int
*/
protected $lastSequenceId;
/**
* @var \Symfony\Component\EventDispatcher\EventDispatcherInterface
*/
protected $eventDispatcher;
/**
* {@inheritdoc}
*
* @param \Drupal\multiversion\Workspace\WorkspaceManagerInterface $workspace_manager
* @param \Symfony\Component\Serializer\Serializer $serializer
* @param \Drupal\Core\Entity\EntityTypeManagerInterface $entity_type_manager
* @param \Drupal\Core\State\StateInterface $state
* @param \Drupal\Core\Language\LanguageManagerInterface $language_manager
* @param \Drupal\Core\Cache\CacheBackendInterface $cache
* @param \Drupal\Core\Database\Connection $connection
* @param \Drupal\Core\Entity\EntityFieldManagerInterface $entity_field_manager
* @param \Symfony\Component\EventDispatcher\EventDispatcherInterface $event_dispatcher
*/
public function __construct(WorkspaceManagerInterface $workspace_manager, Serializer $serializer, EntityTypeManagerInterface $entity_type_manager, StateInterface $state, LanguageManagerInterface $language_manager, CacheBackendInterface $cache, Connection $connection, EntityFieldManagerInterface $entity_field_manager, EventDispatcherInterface $event_dispatcher) {
$this->workspaceManager = $workspace_manager;
$this->serializer = $serializer;
$this->entityTypeManager = $entity_type_manager;
$this->state = $state;
$this->languageManager = $language_manager;
$this->cache = $cache;
$this->connection = $connection;
$this->entityFieldManager = $entity_field_manager;
$this->eventDispatcher = $event_dispatcher;
}
/**
* Static method maintaining the enable migration status.
*
* This method needs to be static because in some strange situations Drupal
* might create multiple instances of this manager. Is this only an issue
* during tests perhaps?
*
* @param boolean|array $status
* @return boolean|array
*/
public static function enableMigrationIsActive($status = NULL) {
static $cache = FALSE;
if ($status !== NULL) {
$cache = $status;
}
return $cache;
}
/**
* Static method maintaining the disable migration status.
*
* @param boolean|array $status
* @return boolean|array
*/
public static function disableMigrationIsActive($status = NULL) {
static $cache = FALSE;
if ($status !== NULL) {
$cache = $status;
}
return $cache;
}
/**
* {@inheritdoc}
*/
public function getActiveWorkspaceId() {
return $this->workspaceManager->getActiveWorkspaceId();
}
/**
* {@inheritdoc}
*/
public function setActiveWorkspaceId($id) {
$workspace = $this->workspaceManager->load($id);
return $this->workspaceManager->setActiveWorkspace($workspace);
}
/**
* {@inheritdoc}
*
* @todo: {@link https://www.drupal.org/node/2597337 Consider using the
* nextId API to generate more sequential IDs.}
* @see \Drupal\Core\Database\Connection::nextId
*/
public function newSequenceId() {
// Multiply the microtime by 1 million to ensure we get an accurate integer.
// Credit goes to @letharion and @logaritmisk for this simple but genius
// solution.
$this->lastSequenceId = (int) (microtime(TRUE) * 1000000);
return $this->lastSequenceId;
}
/**
* {@inheritdoc}
*/
public function lastSequenceId() {
return $this->lastSequenceId;
}
/**
* {@inheritdoc}
*/
public function isSupportedEntityType(EntityTypeInterface $entity_type) {
$supported_entity_types = \Drupal::config('multiversion.settings')->get('supported_entity_types') ?: [];
if (empty($supported_entity_types)) {
return FALSE;
}
if (!in_array($entity_type->id(), $supported_entity_types)) {
return FALSE;
}
return ($entity_type instanceof ContentEntityTypeInterface);
}
/**
* {@inheritdoc}
*/
public function getSupportedEntityTypes() {
$entity_types = [];
foreach ($this->entityTypeManager->getDefinitions() as $entity_type_id => $entity_type) {
if ($this->isSupportedEntityType($entity_type)) {
$entity_types[$entity_type->id()] = $entity_type;
}
}
return $entity_types;
}
/**
* {@inheritdoc}
*/
public function isEnabledEntityType(EntityTypeInterface $entity_type) {
if ($this->isSupportedEntityType($entity_type)) {
$entity_type_id = $entity_type->id();
$migration_done = $this->state->get("multiversion.migration_done.$entity_type_id", FALSE);
$enabled_entity_types = \Drupal::config('multiversion.settings')->get('enabled_entity_types') ?: [];
if ($migration_done && in_array($entity_type_id, $enabled_entity_types)) {
return TRUE;
}
}
return FALSE;
}
/**
* {@inheritdoc}
*/
public function allowToAlter(EntityTypeInterface $entity_type) {
$supported_entity_types = \Drupal::config('multiversion.settings')->get('supported_entity_types') ?: [];
$id = $entity_type->id();
$enable_migration = self::enableMigrationIsActive();
$disable_migration = self::disableMigrationIsActive();
// Don't allow to alter entity type that is not supported.
if (!in_array($id, $supported_entity_types)) {
return FALSE;
}
// Don't allow to alter entity type that is in process to be disabled.
if (is_array($disable_migration) && in_array($id, $disable_migration)) {
return FALSE;
}
// Allow to alter entity type that is in process to be enabled.
if (is_array($enable_migration) && in_array($id, $enable_migration)) {
return TRUE;
}
return ($this->isEnabledEntityType($entity_type));
}
/**
* {@inheritdoc}
*/
public function getEnabledEntityTypes() {
$entity_types = [];
foreach ($this->getSupportedEntityTypes() as $entity_type_id => $entity_type) {
if ($this->isEnabledEntityType($entity_type)) {
$entity_types[$entity_type_id] = $entity_type;
}
}
return $entity_types;
}
/**
* {@inheritdoc}
*
* @todo Ensure nothing breaks if the migration is run twice.
*/
public function enableEntityTypes($entity_types_to_enable = NULL) {
$entity_types = ($entity_types_to_enable !== NULL) ? $entity_types_to_enable : $this->getSupportedEntityTypes();
$enabled_entity_types = \Drupal::config('multiversion.settings')->get('enabled_entity_types') ?: [];
if (empty($entity_types)) {
return $this;
}
$migration = $this->createMigration();
$migration->installDependencies();
$this->eventDispatcher->dispatch(
MultiversionManagerEvents::PRE_MIGRATE,
new MultiversionManagerEvent($entity_types, self::OP_ENABLE)
);
$has_data = $this->prepareContentForMigration($entity_types, $migration, self::OP_ENABLE);
// Nasty workaround until {@link https://www.drupal.org/node/2549143 there
// is a better way to invalidate caches in services}.
// For some reason we have to clear cache on the "global" service as opposed
// to the injected one. Services in the dark corners of Entity API won't see
// the same result otherwise. Very strange.
\Drupal::entityTypeManager()->clearCachedDefinitions();
\Drupal::service('entity_field.manager')->clearCachedFieldDefinitions();
self::enableMigrationIsActive(array_keys($entity_types));
$migration->applyNewStorage(array_keys($entity_types));
// Definitions will now be updated. So fetch the new ones.
if ($entity_types_to_enable !== NULL) {
$updated_entity_types = [];
foreach ($entity_types as $entity_type_id => $entity_type) {
$updated_entity_types[$entity_type_id] = $this->entityTypeManager->getStorage($entity_type_id)->getEntityType();
}
}
else {
$updated_entity_types = $this->getSupportedEntityTypes();
}
// Temporarily disable the maintenance of the {comment_entity_statistics} table.
$this->state->set('comment.maintain_entity_statistics', FALSE);
\Drupal::state()->resetCache();
foreach ($updated_entity_types as $entity_type_id => $entity_type) {
// Migrate from the temporary storage to the new shiny home.
if ($has_data[$entity_type_id]) {
$field_map = $migration->getFieldMap($entity_type, self::OP_ENABLE, self::FROM_TMP);
$migration->migrateContentFromTemp($entity_type, $field_map);
$migration->cleanupMigration($entity_type_id . '__' . self::TO_TMP);
$migration->cleanupMigration($entity_type_id . '__' . self::FROM_TMP);
}
// Mark the migration for this particular entity type as done even if no
// actual content was migrated.
$this->state->set("multiversion.migration_done.$entity_type_id", TRUE);
}
foreach ($entity_types as $entity_type_id => $entity_type) {
$enabled = $this->state->get("multiversion.migration_done.$entity_type_id", FALSE);
if (!in_array($entity_type_id, $enabled_entity_types) && $enabled) {
$enabled_entity_types[] = $entity_type_id;
}
}
\Drupal::configFactory()
->getEditable('multiversion.settings')
->set('enabled_entity_types', $enabled_entity_types)
->save();
// Enable the the maintenance of entity statistics for comments.
$this->state->set('comment.maintain_entity_statistics', TRUE);
// Clean up after us.
$migration->uninstallDependencies();
self::enableMigrationIsActive(FALSE);
// Mark the whole migration as done. Any entity types installed after this
// will not need a migration since they will be created directly on top of
// the Multiversion storage.
$this->state->set('multiversion.migration_done', TRUE);
$this->eventDispatcher->dispatch(
MultiversionManagerEvents::POST_MIGRATE,
new MultiversionManagerEvent($entity_types, self::OP_ENABLE)
);
// Another nasty workaround because the cache is getting skewed somewhere.
// And resetting the cache on the injected state service does not work.
// Very strange.
\Drupal::state()->resetCache();
return $this;
}
/**
* {@inheritdoc}
*/
public function disableEntityTypes($entity_types_to_disable = NULL) {
$entity_types = ($entity_types_to_disable !== NULL) ? $entity_types_to_disable : $this->getEnabledEntityTypes();
$migration = $this->createMigration();
$migration->installDependencies();
$this->eventDispatcher->dispatch(
MultiversionManagerEvents::PRE_MIGRATE,
new MultiversionManagerEvent($entity_types, self::OP_DISABLE)
);
$has_data = $this->prepareContentForMigration($entity_types, $migration, self::OP_DISABLE);
if (empty($entity_types)) {
return $this;
}
if ($entity_types_to_disable === NULL) {
// Uninstall field storage definitions provided by multiversion.
$this->entityTypeManager->clearCachedDefinitions();
$update_manager = \Drupal::entityDefinitionUpdateManager();
foreach ($this->entityTypeManager->getDefinitions() as $entity_type) {
if ($entity_type->entityClassImplements(FieldableEntityInterface::class)) {
$entity_type_id = $entity_type->id();
$revision_key = $entity_type->getKey('revision');
/** @var \Drupal\Core\Entity\FieldableEntityStorageInterface $storage */
$storage = $this->entityTypeManager->getStorage($entity_type_id);
foreach ($this->entityFieldManager->getFieldStorageDefinitions($entity_type_id) as $storage_definition) {
// @todo We need to trigger field purging here.
// See https://www.drupal.org/node/2282119.
if ($storage_definition->getProvider() == 'multiversion' && !$storage->countFieldData($storage_definition, TRUE) && $storage_definition->getName() != $revision_key) {
$update_manager->uninstallFieldStorageDefinition($storage_definition);
}
}
}
}
}
$enabled_entity_types = \Drupal::config('multiversion.settings')->get('enabled_entity_types') ?: [];
foreach ($entity_types as $entity_type_id => $entity_type) {
if (($key = array_search($entity_type_id, $enabled_entity_types)) !== FALSE) {
unset($enabled_entity_types[$key]);
}
}
if ($entity_types_to_disable === NULL) {
$enabled_entity_types = [];
}
\Drupal::configFactory()
->getEditable('multiversion.settings')
->set('enabled_entity_types', $enabled_entity_types)
->save();
\Drupal::entityTypeManager()->clearCachedDefinitions();
\Drupal::service('entity_field.manager')->clearCachedFieldDefinitions();
self::disableMigrationIsActive(array_keys($entity_types));
$migration->applyNewStorage(array_keys($entity_types));
// Temporarily disable the maintenance of the {comment_entity_statistics} table.
$this->state->set('comment.maintain_entity_statistics', FALSE);
\Drupal::state()->resetCache();
// Definitions will now be updated. So fetch the new ones.
$updated_entity_types = [];
foreach ($entity_types as $entity_type_id => $entity_type) {
$updated_entity_types[$entity_type_id] = $this->entityTypeManager->getStorage($entity_type_id)->getEntityType();
}
foreach ($updated_entity_types as $entity_type_id => $entity_type) {
// Drop unique key from uuid on each entity type.
$base_table = $entity_type->getBaseTable();
$uuid_key = $entity_type->getKey('uuid');
$this->connection->schema()->dropUniqueKey($base_table, $entity_type_id . '_field__' . $uuid_key . '__value');
// Migrate from the temporary storage to the drupal default storage.
if ($has_data[$entity_type_id]) {
$field_map = $migration->getFieldMap($entity_type, self::OP_DISABLE, self::FROM_TMP);
$migration->migrateContentFromTemp($entity_type, $field_map);
$migration->cleanupMigration($entity_type_id . '__' . self::TO_TMP);
$migration->cleanupMigration($entity_type_id . '__' . self::FROM_TMP);
}
$this->state->delete("multiversion.migration_done.$entity_type_id");
}
// Enable the the maintenance of entity statistics for comments.
$this->state->set('comment.maintain_entity_statistics', TRUE);
// Clean up after us.
$migration->uninstallDependencies();
self::disableMigrationIsActive(FALSE);
$this->state->delete('multiversion.migration_done');
$this->eventDispatcher->dispatch(
MultiversionManagerEvents::POST_MIGRATE,
new MultiversionManagerEvent($entity_types, self::OP_DISABLE)
);
return $this;
}
/**
* {@inheritdoc}
*/
public function newRevisionId(ContentEntityInterface $entity, $index = 0) {
$deleted = $entity->_deleted->value;
$old_rev = $entity->_rev->value;
// The 'new_revision_id' context will be used in normalizers (where it's
// necessary) to identify in which format to return the normalized entity.
$normalized_entity = $this->serializer->normalize($entity, NULL, ['new_revision_id' => TRUE]);
// Remove fields internal to the multiversion system.
$this->filterNormalizedEntity($normalized_entity);
// The terms being serialized are:
// - deleted
// - old sequence ID (@todo: {@link https://www.drupal.org/node/2597341
// Address this property.})
// - old revision hash
// - normalized entity (without revision info field)
// - attachments (@todo: {@link https://www.drupal.org/node/2597341
// Address this property.})
return ($index + 1) . '-' . md5($this->termToBinary([$deleted, 0, $old_rev, $normalized_entity, []]));
}
/**
* @param array $normalized_entity
*/
protected function filterNormalizedEntity(&$normalized_entity){
foreach ($normalized_entity as $key => &$value) {
if ($key{0} == '_') {
unset($normalized_entity[$key]);
}
elseif (is_array($value)) {
$this->filterNormalizedEntity($value);
}
}
}
protected function termToBinary(array $term) {
// @todo: {@link https://www.drupal.org/node/2597478 Switch to BERT
// serialization format instead of JSON.}
return $this->serializer->serialize($term, 'json');
}
/**
* Factory method for a new Multiversion migration.
*
* @return \Drupal\multiversion\MultiversionMigrationInterface
*/
protected function createMigration() {
return MultiversionMigration::create($this->container, $this->entityTypeManager, $this->entityFieldManager);
}
protected function prepareContentForMigration($entity_types, MultiversionMigrationInterface $migration, $op) {
$has_data = [];
// Walk through and verify that the original storage is in good order.
// Flakey contrib modules or mocked tests where some schemas aren't properly
// installed should be ignored.
foreach ($entity_types as $entity_type_id => $entity_type) {
/** @var \Drupal\Core\Entity\EntityStorageInterface $storage */
$storage = $this->entityTypeManager->getStorage($entity_type_id);
$has_data[$entity_type_id] = FALSE;
try {
if ($storage->hasData()) {
$has_data[$entity_type_id] = TRUE;
}
}
catch (\Exception $e) {
// Don't bother with this entity type any more.
unset($entity_types[$entity_type_id]);
}
if ($has_data[$entity_type_id]) {
// Migrate content to temporary storage.
$field_map = $migration->getFieldMap($entity_type, $op, self::TO_TMP);
$migration->migrateContentToTemp($storage->getEntityType(), $field_map);
}
}
// Empty old storages. Do this just after migrating all entities to
// temporary storage because deleting some entity types could delete
// referenced entities (E.g.: deleting poll entities will also delete
// poll_choice).
foreach ($entity_types as $entity_type_id => $entity_type) {
if ($has_data[$entity_type_id] === TRUE) {
/** @var \Drupal\Core\Entity\EntityStorageInterface $storage */
$storage = $this->entityTypeManager->getStorage($entity_type_id);
// Because of the way the Entity API treats entity definition updates we
// need to ensure each storage is empty before we can apply the new
// definition.
$migration->emptyOldStorage($storage);
}
}
return $has_data;
}
}
