Commit 71399ed9 authored by plach's avatar plach

Issue #3062486 by amateescu, pmelab, blazey, dixon_, plach, catch, webchick,...

Issue #3062486 by amateescu, pmelab, blazey, dixon_, plach, catch, webchick, jeqq, vijaycs85: Add the ability to create sub-workspaces in order to enable the dev -> stage -> live workflow for content

(cherry picked from commit f10a3971)
parent 92390915
......@@ -11,4 +11,5 @@ bundle: workspace
mode: deploy
content: { }
hidden:
parent: true
uid: true
......@@ -100,6 +100,17 @@ public static function baseFieldDefinitions(EntityTypeInterface $entity_type) {
])
->setDisplayConfigurable('form', TRUE);
$fields['parent'] = BaseFieldDefinition::create('entity_reference')
->setLabel(new TranslatableMarkup('Parent'))
->setDescription(new TranslatableMarkup('The parent workspace.'))
->setSetting('target_type', 'workspace')
->setReadOnly(TRUE)
->setDisplayConfigurable('form', TRUE)
->setDisplayOptions('form', [
'type' => 'options_select',
'weight' => 10,
]);
$fields['changed'] = BaseFieldDefinition::create('changed')
->setLabel(new TranslatableMarkup('Changed'))
->setDescription(new TranslatableMarkup('The time that the workspace was last edited.'))
......@@ -141,6 +152,29 @@ public function setCreatedTime($created) {
return $this->set('created', (int) $created);
}
/**
* {@inheritdoc}
*/
public function hasParent() {
return !$this->get('parent')->isEmpty();
}
/**
* {@inheritdoc}
*/
public static function preDelete(EntityStorageInterface $storage, array $entities) {
parent::preDelete($storage, $entities);
$workspace_tree = \Drupal::service('workspaces.repository')->loadTree();
// Ensure that workspaces that have descendants can not be deleted.
foreach ($entities as $entity) {
if (!empty($workspace_tree[$entity->id()]['descendants'])) {
throw new \InvalidArgumentException("The {$entity->label()} workspace can not be deleted because it has child workspaces.");
}
}
}
/**
* {@inheritdoc}
*/
......
......@@ -9,6 +9,7 @@
use Drupal\Core\Entity\RevisionableInterface;
use Drupal\Core\Form\FormStateInterface;
use Drupal\Core\StringTranslation\StringTranslationTrait;
use Drupal\workspaces\Plugin\Validation\Constraint\EntityWorkspaceConflictConstraint;
use Symfony\Component\DependencyInjection\ContainerInterface;
/**
......@@ -270,18 +271,16 @@ public function entityFormAlter(array &$form, FormStateInterface $form_state, $f
$form['#entity_builders'][] = [get_called_class(), 'entityFormEntityBuild'];
}
if ($workspace_ids = $this->workspaceAssociation->getEntityTrackingWorkspaceIds($entity)) {
// An entity can only be edited in one workspace.
$workspace_id = reset($workspace_ids);
$active_workspace = $this->workspaceManager->getActiveWorkspace();
if ($workspace_id && (!$active_workspace || $workspace_id !== $active_workspace->id())) {
$workspace = $this->entityTypeManager->getStorage('workspace')->load($workspace_id);
$form['#markup'] = $this->t('The content is being edited in the %label workspace.', ['%label' => $workspace->label()]);
// Run the workspace conflict validation constraint when the entity form is
// being built so we can "disable" it early and display a message to the
// user, instead of allowing them to enter data that can never be saved.
foreach ($entity->validate()->getEntityViolations() as $violation) {
if ($violation->getConstraint() instanceof EntityWorkspaceConflictConstraint) {
$form['#markup'] = $violation->getMessage();
$form['#access'] = FALSE;
continue;
}
}
};
}
/**
......
......@@ -68,7 +68,18 @@ public static function getSubscribedEvents() {
* {@inheritdoc}
*/
public function onEntityTypeCreate(EntityTypeInterface $entity_type) {
// Nothing to do here.
// If the entity type is supported by Workspaces, add the revision metadata
// field.
if ($this->workspaceManager->isEntityTypeSupported($entity_type)) {
$this->addRevisionMetadataField($entity_type);
}
}
/**
* {@inheritdoc}
*/
public function onFieldableEntityTypeCreate(EntityTypeInterface $entity_type, array $field_storage_definitions) {
$this->onEntityTypeCreate($entity_type);
}
/**
......@@ -78,23 +89,7 @@ public function onEntityTypeUpdate(EntityTypeInterface $entity_type, EntityTypeI
// If the entity type is now supported by Workspaces, add the revision
// metadata field.
if ($this->workspaceManager->isEntityTypeSupported($entity_type) && !$this->workspaceManager->isEntityTypeSupported($original)) {
$revision_metadata_keys = $entity_type->get('revision_metadata_keys');
if (!isset($revision_metadata_keys['workspace'])) {
// Bail out if there's an existing field called 'workspace'.
if ($this->entityDefinitionUpdateManager->getFieldStorageDefinition('workspace', $entity_type->id())) {
throw new \RuntimeException("An existing 'workspace' field was found for the '{$entity_type->id()}' entity type. Set the 'workspace' revision metadata key to use a different field name and run this update function again.");
}
$revision_metadata_keys['workspace'] = 'workspace';
$entity_type->set('revision_metadata_keys', $revision_metadata_keys);
// We are only adding a revision metadata key so we don't need to go
// through the entity update process.
$this->entityLastInstalledSchemaRepository->setLastInstalledDefinition($entity_type);
}
$this->entityDefinitionUpdateManager->installFieldStorageDefinition($revision_metadata_keys['workspace'], $entity_type->id(), 'workspaces', $this->getWorkspaceFieldDefinition());
$this->addRevisionMetadataField($entity_type);
}
// If the entity type is no longer supported by Workspaces, remove the
......@@ -128,6 +123,32 @@ public function onEntityTypeDelete(EntityTypeInterface $entity_type) {
// Nothing to do here.
}
/**
* Adds the 'workspace' revision metadata field to an entity type.
*
* @param \Drupal\Core\Entity\EntityTypeInterface $entity_type
* The entity type that has been installed or updated.
*/
protected function addRevisionMetadataField(EntityTypeInterface $entity_type) {
$revision_metadata_keys = $entity_type->get('revision_metadata_keys');
if (!isset($revision_metadata_keys['workspace'])) {
// Bail out if there's an existing field called 'workspace'.
if ($this->entityDefinitionUpdateManager->getFieldStorageDefinition('workspace', $entity_type->id())) {
throw new \RuntimeException("An existing 'workspace' field was found for the '{$entity_type->id()}' entity type. Set the 'workspace' revision metadata key to use a different field name and run this update function again.");
}
$revision_metadata_keys['workspace'] = 'workspace';
$entity_type->set('revision_metadata_keys', $revision_metadata_keys);
// We are only adding a revision metadata key so we don't need to go
// through the entity update process.
$this->entityLastInstalledSchemaRepository->setLastInstalledDefinition($entity_type);
}
$this->entityDefinitionUpdateManager->installFieldStorageDefinition($revision_metadata_keys['workspace'], $entity_type->id(), 'workspaces', $this->getWorkspaceFieldDefinition());
}
/**
* Gets the base field definition for the 'workspace' revision metadata field.
*
......
......@@ -8,6 +8,7 @@
use Drupal\Core\Entity\EntityTypeBundleInfoInterface;
use Drupal\Core\Form\FormStateInterface;
use Drupal\workspaces\WorkspaceAssociationInterface;
use Drupal\workspaces\WorkspaceRepositoryInterface;
use Symfony\Component\DependencyInjection\ContainerInterface;
/**
......@@ -31,6 +32,13 @@ class WorkspaceDeleteForm extends ContentEntityDeleteForm implements WorkspaceFo
*/
protected $workspaceAssociation;
/**
* The workspace repository service.
*
* @var \Drupal\workspaces\WorkspaceRepositoryInterface
*/
protected $workspaceRepository;
/**
* {@inheritdoc}
*/
......@@ -38,6 +46,7 @@ public static function create(ContainerInterface $container) {
return new static(
$container->get('entity.repository'),
$container->get('workspaces.association'),
$container->get('workspaces.repository'),
$container->get('entity_type.bundle.info'),
$container->get('datetime.time')
);
......@@ -51,14 +60,17 @@ public static function create(ContainerInterface $container) {
* @param \Drupal\workspaces\WorkspaceAssociationInterface $workspace_association
* The workspace association service to check how many revisions will be
* deleted.
* @param \Drupal\workspaces\WorkspaceRepositoryInterface $workspace_repository
* The workspace repository service.
* @param \Drupal\Core\Entity\EntityTypeBundleInfoInterface $entity_type_bundle_info
* The entity type bundle service.
* @param \Drupal\Component\Datetime\TimeInterface $time
* The time service.
*/
public function __construct(EntityRepositoryInterface $entity_repository, WorkspaceAssociationInterface $workspace_association, EntityTypeBundleInfoInterface $entity_type_bundle_info = NULL, TimeInterface $time = NULL) {
public function __construct(EntityRepositoryInterface $entity_repository, WorkspaceAssociationInterface $workspace_association, WorkspaceRepositoryInterface $workspace_repository, EntityTypeBundleInfoInterface $entity_type_bundle_info = NULL, TimeInterface $time = NULL) {
parent::__construct($entity_repository, $entity_type_bundle_info, $time);
$this->workspaceAssociation = $workspace_association;
$this->workspaceRepository = $workspace_repository;
}
/**
......@@ -66,6 +78,17 @@ public function __construct(EntityRepositoryInterface $entity_repository, Worksp
*/
public function buildForm(array $form, FormStateInterface $form_state) {
$form = parent::buildForm($form, $form_state);
$workspace_tree = $this->workspaceRepository->loadTree();
if (!empty($workspace_tree[$this->entity->id()]['descendants'])) {
$form['description']['#markup'] = $this->t('The %label workspace can not be deleted because it has child workspaces.', [
'%label' => $this->entity->label(),
]);
$form['actions']['submit']['#disabled'] = TRUE;
return $form;
}
$tracked_entities = $this->workspaceAssociation->getTrackedEntities($this->entity->id());
$items = [];
foreach (array_keys($tracked_entities) as $entity_type_id => $entity_ids) {
......
<?php
namespace Drupal\workspaces\Form;
use Drupal\Core\DependencyInjection\ContainerInjectionInterface;
use Drupal\Core\Entity\EntityTypeManagerInterface;
use Drupal\Core\Form\ConfirmFormBase;
use Drupal\Core\Form\FormStateInterface;
use Drupal\Core\Url;
use Drupal\workspaces\WorkspaceInterface;
use Drupal\workspaces\WorkspaceOperationFactory;
use Symfony\Component\DependencyInjection\ContainerInterface;
/**
* Provides a form that merges the contents for a workspace into another one.
*/
class WorkspaceMergeForm extends ConfirmFormBase implements WorkspaceFormInterface, ContainerInjectionInterface {
/**
* The source workspace entity.
*
* @var \Drupal\workspaces\WorkspaceInterface
*/
protected $sourceWorkspace;
/**
* The target workspace entity.
*
* @var \Drupal\workspaces\WorkspaceInterface
*/
protected $targetWorkspace;
/**
* The workspace operation factory.
*
* @var \Drupal\workspaces\WorkspaceOperationFactory
*/
protected $workspaceOperationFactory;
/**
* The entity type manager.
*
* @var \Drupal\Core\Entity\EntityTypeManagerInterface
*/
protected $entityTypeManager;
/**
* Constructs a new WorkspaceMergeForm.
*
* @param \Drupal\workspaces\WorkspaceOperationFactory $workspace_operation_factory
* The workspace operation factory service.
* @param \Drupal\Core\Entity\EntityTypeManagerInterface $entity_type_manager
* The entity type manager.
*/
public function __construct(WorkspaceOperationFactory $workspace_operation_factory, EntityTypeManagerInterface $entity_type_manager) {
$this->workspaceOperationFactory = $workspace_operation_factory;
$this->entityTypeManager = $entity_type_manager;
}
/**
* {@inheritdoc}
*/
public static function create(ContainerInterface $container) {
return new static(
$container->get('workspaces.operation_factory'),
$container->get('entity_type.manager')
);
}
/**
* {@inheritdoc}
*/
public function getFormId() {
return 'workspace_merge_form';
}
/**
* {@inheritdoc}
*/
public function buildForm(array $form, FormStateInterface $form_state, WorkspaceInterface $source_workspace = NULL, WorkspaceInterface $target_workspace = NULL) {
$this->sourceWorkspace = $source_workspace;
$this->targetWorkspace = $target_workspace;
$form = parent::buildForm($form, $form_state);
$workspace_merger = $this->workspaceOperationFactory->getMerger($this->sourceWorkspace, $this->targetWorkspace);
$args = [
'%source_label' => $this->sourceWorkspace->label(),
'%target_label' => $this->targetWorkspace->label(),
];
// List the changes that can be merged into the target.
if ($source_rev_diff = $workspace_merger->getDifferringRevisionIdsOnSource()) {
$total_count = $workspace_merger->getNumberOfChangesOnSource();
$form['merge'] = [
'#theme' => 'item_list',
'#title' => $this->formatPlural($total_count, 'There is @count item that can be merged from %source_label to %target_label', 'There are @count items that can be merged from %source_label to %target_label', $args),
'#items' => [],
'#total_count' => $total_count,
];
foreach ($source_rev_diff as $entity_type_id => $revision_difference) {
$form['merge']['#items'][$entity_type_id] = $this->entityTypeManager->getDefinition($entity_type_id)->getCountLabel(count($revision_difference));
}
}
// If there are no changes to merge, show an informational message.
if (!isset($form['merge'])) {
$form['description'] = [
'#markup' => $this->t('There are no changes that can be merged from %source_label to %target_label.', $args),
];
$form['actions']['submit']['#disabled'] = TRUE;
}
return $form;
}
/**
* {@inheritdoc}
*/
public function getQuestion() {
return $this->t('Would you like to merge the contents of the %source_label workspace into %target_label?', [
'%source_label' => $this->sourceWorkspace->label(),
'%target_label' => $this->targetWorkspace->label(),
]);
}
/**
* {@inheritdoc}
*/
public function getDescription() {
return $this->t('Merge workspace contents.');
}
/**
* {@inheritdoc}
*/
public function getCancelUrl() {
return Url::fromRoute('entity.workspace.collection', [], ['query' => \Drupal::destination()->getAsArray()]);
}
/**
* {@inheritdoc}
*/
public function submitForm(array &$form, FormStateInterface $form_state) {
$this->workspaceOperationFactory->getMerger($this->sourceWorkspace, $this->targetWorkspace)->merge();
$this->messenger()->addMessage($this->t('The contents of the %source_label workspace have been merged into %target_label.', [
'%source_label' => $this->sourceWorkspace->label(),
'%target_label' => $this->targetWorkspace->label(),
]));
}
}
<?php
namespace Drupal\workspaces\Plugin\EntityReferenceSelection;
use Drupal\Component\Utility\Html;
use Drupal\Core\Entity\Plugin\EntityReferenceSelection\DefaultSelection;
use Drupal\Core\Form\FormStateInterface;
use Symfony\Component\DependencyInjection\ContainerInterface;
/**
* Provides specific access control for the workspace entity type.
*
* @EntityReferenceSelection(
* id = "default:workspace",
* label = @Translation("Workspace selection"),
* entity_types = {"workspace"},
* group = "default",
* weight = 1
* )
*/
class WorkspaceSelection extends DefaultSelection {
/**
* The workspace repository service.
*
* @var \Drupal\workspaces\WorkspaceRepositoryInterface
*/
protected $workspaceRepository;
/**
* {@inheritdoc}
*/
public static function create(ContainerInterface $container, array $configuration, $plugin_id, $plugin_definition) {
$instance = parent::create($container, $configuration, $plugin_id, $plugin_definition);
$instance->workspaceRepository = $container->get('workspaces.repository');
return $instance;
}
/**
* {@inheritdoc}
*/
public function defaultConfiguration() {
return [
'sort' => [
'field' => 'label',
'direction' => 'asc',
],
] + parent::defaultConfiguration();
}
/**
* {@inheritdoc}
*/
public function buildConfigurationForm(array $form, FormStateInterface $form_state) {
$form = parent::buildConfigurationForm($form, $form_state);
// Sorting is not possible for workspaces because we always sort them by
// depth and label.
$form['sort']['#access'] = FALSE;
return $form;
}
/**
* {@inheritdoc}
*/
public function getReferenceableEntities($match = NULL, $match_operator = 'CONTAINS', $limit = 0) {
// Get all the workspace entities and sort them in tree order.
$storage = $this->entityTypeManager->getStorage('workspace');
$workspace_tree = $this->workspaceRepository->loadTree();
$entities = array_replace($workspace_tree, $storage->loadMultiple());
// If we need to restrict the list of workspaces by searching only a part of
// their label ($match) or by a number of results ($limit), the workspace
// tree would be mangled because it wouldn't contain all the tree items.
if ($match || $limit) {
$options = parent::getReferenceableEntities($match, $match_operator, $limit);
}
else {
$options = [];
foreach ($entities as $entity) {
$options[$entity->bundle()][$entity->id()] = str_repeat('-', $workspace_tree[$entity->id()]['depth']) . Html::escape($this->entityRepository->getTranslationFromContext($entity)->label());
}
}
$restricted_access_entities = [];
foreach ($options as $bundle => $bundle_options) {
foreach (array_keys($bundle_options) as $id) {
// If a user can not view a workspace, we need to prevent them from
// referencing that workspace as well as its descendants.
if (in_array($id, $restricted_access_entities) || !$entities[$id]->access('view', $this->currentUser)) {
$restricted_access_entities += $workspace_tree[$id]['descendants'];
unset($options[$bundle][$id]);
}
}
}
return $options;
}
}
......@@ -6,6 +6,7 @@
use Drupal\Core\Entity\EntityTypeManagerInterface;
use Drupal\workspaces\WorkspaceAssociationInterface;
use Drupal\workspaces\WorkspaceManagerInterface;
use Drupal\workspaces\WorkspaceRepositoryInterface;
use Symfony\Component\DependencyInjection\ContainerInterface;
use Symfony\Component\Validator\Constraint;
use Symfony\Component\Validator\ConstraintValidator;
......@@ -36,6 +37,13 @@ class EntityWorkspaceConflictConstraintValidator extends ConstraintValidator imp
*/
protected $workspaceAssociation;
/**
* The workspace repository service.
*
* @var \Drupal\workspaces\WorkspaceRepositoryInterface
*/
protected $workspaceRepository;
/**
* Constructs an EntityUntranslatableFieldsConstraintValidator object.
*
......@@ -45,11 +53,14 @@ class EntityWorkspaceConflictConstraintValidator extends ConstraintValidator imp
* The workspace manager service.
* @param \Drupal\workspaces\WorkspaceAssociationInterface $workspace_association
* The workspace association service.
* @param \Drupal\workspaces\WorkspaceRepositoryInterface $workspace_repository
* The Workspace repository service.
*/
public function __construct(EntityTypeManagerInterface $entity_type_manager, WorkspaceManagerInterface $workspace_manager, WorkspaceAssociationInterface $workspace_association) {
public function __construct(EntityTypeManagerInterface $entity_type_manager, WorkspaceManagerInterface $workspace_manager, WorkspaceAssociationInterface $workspace_association, WorkspaceRepositoryInterface $workspace_repository) {
$this->entityTypeManager = $entity_type_manager;
$this->workspaceManager = $workspace_manager;
$this->workspaceAssociation = $workspace_association;
$this->workspaceRepository = $workspace_repository;
}
/**
......@@ -59,7 +70,8 @@ public static function create(ContainerInterface $container) {
return new static(
$container->get('entity_type.manager'),
$container->get('workspaces.manager'),
$container->get('workspaces.association')
$container->get('workspaces.association'),
$container->get('workspaces.repository')
);
}
......@@ -69,17 +81,25 @@ public static function create(ContainerInterface $container) {
public function validate($entity, Constraint $constraint) {
/** @var \Drupal\Core\Entity\EntityInterface $entity */
if (isset($entity) && !$entity->isNew()) {
$workspace_ids = $this->workspaceAssociation->getEntityTrackingWorkspaceIds($entity);
$active_workspace = $this->workspaceManager->getActiveWorkspace();
if ($workspace_ids && (!$active_workspace || !in_array($active_workspace->id(), $workspace_ids, TRUE))) {
// An entity can only be edited in one workspace.
$workspace_id = reset($workspace_ids);
$workspace = $this->entityTypeManager->getStorage('workspace')->load($workspace_id);
// Get the latest revision of the entity in order to check if it's being
// edited in a different workspace.
$latest_revision = $this->workspaceManager->executeOutsideWorkspace(function () use ($entity) {
$storage = $this->entityTypeManager->getStorage($entity->getEntityTypeId());
return $storage->loadRevision($storage->getLatestRevisionId($entity->id()));
});
// If the latest revision of the entity is tracked in a workspace, it can
// only be edited in that workspace or one of its descendants.
if ($latest_revision_workspace = $latest_revision->workspace->entity) {
$descendants_and_self = $this->workspaceRepository->getDescendantsAndSelf($latest_revision_workspace->id());
$this->context->buildViolation($constraint->message)
->setParameter('%label', $workspace->label())
->addViolation();
if (!$active_workspace || !in_array($active_workspace->id(), $descendants_and_self, TRUE)) {
$this->context->buildViolation($constraint->message)
->setParameter('%label', $latest_revision_workspace->label())
->addViolation();
}
}
}
}
......
......@@ -19,6 +19,11 @@ class WorkspaceAccessControlHandler extends EntityAccessControlHandler {
*/
protected function checkAccess(EntityInterface $entity, $operation, AccountInterface $account) {
/** @var \Drupal\workspaces\WorkspaceInterface $entity */
if ($operation === 'publish' && $entity->hasParent()) {
$message = $this->t('Only top-level workspaces can be published.');
return AccessResult::forbidden((string) $message)->addCacheableDependency($entity);
}
if ($account->hasPermission('administer workspaces')) {
return AccessResult::allowed()->cachePerPermissions();
}
......
......@@ -31,6 +31,13 @@ class WorkspaceAssociation implements WorkspaceAssociationInterface {
*/
protected $entityTypeManager;
/**
* The workspace repository service.
*
* @var \Drupal\workspaces\WorkspaceRepositoryInterface
*/
protected $workspaceRepository;
/**
* Constructs a WorkspaceAssociation object.
*
......@@ -38,26 +45,86 @@ class WorkspaceAssociation implements WorkspaceAssociationInterface {
* A database connection for reading and writing path aliases.
* @param \Drupal\Core\Entity\EntityTypeManagerInterface $entity_type_manager
* The entity type manager for querying revisions.
* @param \Drupal\workspaces\WorkspaceRepositoryInterface $workspace_repository
* The Workspace repository service.
*/
public function __construct(Connection $connection, EntityTypeManagerInterface $entity_type_manager) {
public function __construct(Connection $connection, EntityTypeManagerInterface $entity_type_manager, WorkspaceRepositoryInterface $workspace_repository) {
$this->database = $connection;
$this->entityTypeManager = $entity_type_manager;
$this->workspaceRepository = $workspace_repository;
}
/**
* {@inheritdoc}
*/
public function trackEntity(RevisionableInterface $entity, WorkspaceInterface $workspace) {
$this->database->merge(static::TABLE)
->fields([
'target_entity_revision_id' => $entity->getRevisionId(),
])
->keys([
'workspace' => $workspace->id(),
'target_entity_type_id' => $entity->getEntityTypeId(),
'target_entity_id' => $entity->id(),
])
->execute();
// Determine all workspaces that might be affected by this change.
$affected_workspaces = $this->workspaceRepository->getDescendantsAndSelf($workspace->id());
// Get the currently tracked revision for this workspace.
$tracked = $this->getTrackedEntities($workspace->id(), $entity->getEntityTypeId(), [$entity->id()]);
$tracked_revision_id = NULL;
if (isset($tracked[$entity->getEntityTypeId()])) {
$tracked_revision_id = key($tracked[$entity->getEntityTypeId()]);
}
$transaction = $this->database->startTransaction();
try {
// Update all affected workspaces that were tracking the current revision.
// This means they are inheriting content and should be updated.
if ($tracked_revision_id) {
$this->database->update(static::TABLE)
->fields([
'target_entity_revision_id' => $entity->getRevisionId(),
])
->condition('workspace', $affected_workspaces, 'IN')
->condition('target_entity_type_id', $entity->getEntityTypeId())
->condition('target_entity_id', $entity->id())
// Only update descendant workspaces if they have the same initial
// revision, which means they are currently inheriting content.
->condition('target_entity_revision_id', $tracked_revision_id)
->execute();
}
// Insert a new index entry for each workspace that is not tracking this
// entity yet.
$missing_workspaces = array_diff($affected_workspaces, $this->getEntityTrackingWorkspaceIds($entity));
if ($missing_workspaces) {
$insert_query = $this->database->insert(static::TABLE)
->fields([
'workspace',
'target_entity_revision_id',
'target_entity_type_id',
'target_entity_id',
]);
foreach ($missing_workspaces as $workspace_id) {
$insert_query->values([
'workspace' => $workspace_id,
'target_entity_type_id' => $entity->getEntityTypeId(),
'target_entity_id' => $entity->id(),
'target_entity_revision_id' => $entity->getRevisionId(),
]);
}
$insert_query->execute();
}
}
catch (\Exception $e) {
$transaction->rollBack();
watchdog_exception('workspaces', $e);
throw $e;
}
}
/**
* {@inheritdoc}