diff --git a/core/modules/content_moderation/src/Entity/ContentModerationState.php b/core/modules/content_moderation/src/Entity/ContentModerationState.php index 636483c..f495355 100644 --- a/core/modules/content_moderation/src/Entity/ContentModerationState.php +++ b/core/modules/content_moderation/src/Entity/ContentModerationState.php @@ -3,6 +3,8 @@ namespace Drupal\content_moderation\Entity; use Drupal\content_moderation\ContentModerationStateInterface; +use Drupal\content_moderation\Event\ContentModerationEvents; +use Drupal\content_moderation\Event\ContentModerationStateChangedEvent; use Drupal\Core\Entity\ContentEntityBase; use Drupal\Core\Entity\EntityTypeInterface; use Drupal\Core\Field\BaseFieldDefinition; @@ -149,15 +151,26 @@ public static function getCurrentUserId() { } /** - * {@inheritdoc} + * Load the entity whose state is being tracked by this entity. + * + * @return \Drupal\Core\Entity\ContentEntityInterface + * The entity whose state is being tracked. */ - public function save() { + protected function loadRelatedEntity() { $related_entity = \Drupal::entityTypeManager() ->getStorage($this->content_entity_type_id->value) ->loadRevision($this->content_entity_revision_id->value); if ($related_entity instanceof TranslatableInterface) { $related_entity = $related_entity->getTranslation($this->activeLangcode); } + return $related_entity; + } + + /** + * {@inheritdoc} + */ + public function save() { + $related_entity = $this->loadRelatedEntity(); $related_entity->moderation_state = $this->moderation_state; return $related_entity->save(); } @@ -175,7 +188,25 @@ public function save() { * In case of failures an exception is thrown. */ protected function realSave() { - return parent::save(); + if (!$this->getLoadedRevisionId()) { + $original_state = FALSE; + } + else { + $original_content_moderation_state = \Drupal::entityTypeManager() + ->getStorage($this->getEntityTypeId()) + ->loadRevision($this->getLoadedRevisionId()); + if (!$this->isDefaultTranslation() && $original_content_moderation_state->hasTranslation($this->activeLangcode)) { + $original_content_moderation_state = $original_content_moderation_state->getTranslation($this->activeLangcode); + } + $original_state = $original_content_moderation_state->moderation_state->value; + } + + $result = parent::save(); + + $event = new ContentModerationStateChangedEvent($this->loadRelatedEntity(), $this->moderation_state->value, $original_state, $this->workflow->target_id); + \Drupal::service('event_dispatcher')->dispatch(ContentModerationEvents::STATE_CHANGED, $event); + + return $result; } } diff --git a/core/modules/content_moderation/src/Event/ContentModerationEvents.php b/core/modules/content_moderation/src/Event/ContentModerationEvents.php new file mode 100644 index 0000000..4c0b58e --- /dev/null +++ b/core/modules/content_moderation/src/Event/ContentModerationEvents.php @@ -0,0 +1,20 @@ +moderatedEntity = $moderated_entity; + $this->newState = $new_state; + $this->originalState = $original_state; + $this->workflow = $workflow; + } + + /** + * Get the entity that is being moderated. + * + * @return \Drupal\Core\Entity\ContentEntityInterface + * The entity that is being moderated. + */ + public function getModeratedEntity() { + return $this->moderatedEntity; + } + + /** + * Get the new state of the content. + * + * @return string + * The state the content has been changed to. + */ + public function getNewState() { + return $this->newState; + } + + /** + * Get the original state of the content. + * + * @return string + * The state the content was before. + */ + public function getOriginalState() { + return $this->originalState; + } + + /** + * Get the ID of the workflow which allowed this state change. + * + * @return string + * The ID of the workflow. + */ + public function getWorkflow() { + return $this->workflow; + } + +} diff --git a/core/modules/content_moderation/tests/src/Kernel/ContentModerationStateChangedEventTest.php b/core/modules/content_moderation/tests/src/Kernel/ContentModerationStateChangedEventTest.php new file mode 100644 index 0000000..e5bfcbf --- /dev/null +++ b/core/modules/content_moderation/tests/src/Kernel/ContentModerationStateChangedEventTest.php @@ -0,0 +1,157 @@ +installEntitySchema('content_moderation_state'); + $this->installEntitySchema('user'); + $this->installEntitySchema('node'); + $this->installConfig('workflows'); + $this->installSchema('node', ['node_access']); + $this->installConfig('content_moderation'); + + NodeType::create([ + 'title' => 'Test node', + 'type' => 'example', + ])->save(); + + $workflow = Workflow::load('editorial'); + $workflow->getTypePlugin()->addEntityTypeAndBundle('node', 'example'); + $workflow->save(); + + $this->eventDispatcher = $this->getMock(EventDispatcherInterface::class); + $this->container->set('event_dispatcher', $this->eventDispatcher); + + ConfigurableLanguage::createFromLangcode('fr')->save(); + } + + /** + * Tests events for adding and updating moderation state entities. + */ + public function testCreateUpdateStates() { + + $this->assertEventDispatchedAtIndex(0, function($event_name, ContentModerationStateChangedEvent $event) { + $this->assertEquals('content_moderation.state_changed', $event_name); + $this->assertEquals('editorial', $event->getWorkflow()); + $this->assertEquals(FALSE, $event->getOriginalState()); + $this->assertEquals('draft', $event->getNewState()); + $this->assertEquals('node', $event->getModeratedEntity()->getEntityTypeId()); + $this->assertEquals(1, $event->getModeratedEntity()->getRevisionId()); + }); + + $this->assertEventDispatchedAtIndex(1, function($event_name, ContentModerationStateChangedEvent $event) { + $this->assertEquals('content_moderation.state_changed', $event_name); + $this->assertEquals('editorial', $event->getWorkflow()); + $this->assertEquals('draft', $event->getOriginalState()); + $this->assertEquals('published', $event->getNewState()); + $this->assertEquals('node', $event->getModeratedEntity()->getEntityTypeId()); + $this->assertEquals(2, $event->getModeratedEntity()->getRevisionId()); + }); + + $this->assertEventDispatchedAtIndex(2, function($event_name, ContentModerationStateChangedEvent $event) { + $this->assertEquals('content_moderation.state_changed', $event_name); + $this->assertEquals('editorial', $event->getWorkflow()); + $this->assertEquals('published', $event->getOriginalState()); + $this->assertEquals('archived', $event->getNewState()); + $this->assertEquals('node', $event->getModeratedEntity()->getEntityTypeId()); + $this->assertEquals(3, $event->getModeratedEntity()->getRevisionId()); + }); + + $this->assertEventDispatchedAtIndex(3, function($event_name, ContentModerationStateChangedEvent $event) { + $this->assertEquals('content_moderation.state_changed', $event_name); + $this->assertEquals('editorial', $event->getWorkflow()); + $this->assertEquals('archived', $event->getOriginalState()); + $this->assertEquals('published', $event->getNewState()); + $this->assertEquals('node', $event->getModeratedEntity()->getEntityTypeId()); + $this->assertEquals(4, $event->getModeratedEntity()->getRevisionId()); + $this->assertEquals('fr', $event->getModeratedEntity()->language()->getId()); + }); + + $this->assertEventDispatchedAtIndex(4, function($event_name, ContentModerationStateChangedEvent $event) { + $this->assertEquals('content_moderation.state_changed', $event_name); + $this->assertEquals('editorial', $event->getWorkflow()); + $this->assertEquals('published', $event->getOriginalState()); + $this->assertEquals('draft', $event->getNewState()); + $this->assertEquals('node', $event->getModeratedEntity()->getEntityTypeId()); + $this->assertEquals(5, $event->getModeratedEntity()->getRevisionId()); + $this->assertEquals('fr', $event->getModeratedEntity()->language()->getId()); + }); + + $node = Node::create([ + 'type' => 'example', + 'title' => 'Foo', + 'moderation_state' => 'draft', + ]); + $node->save(); + + $node->moderation_state = 'published'; + $node->save(); + + $node->moderation_state = 'archived'; + $node->save(); + + $french_node = $node->addTranslation('fr'); + $french_node->title = 'French node'; + $french_node->moderation_state = 'published'; + $french_node->save(); + + $french_node->moderation_state = 'draft'; + $french_node->save(); + } + + /** + * Assert the event information dispatched at a particular index. + * + * @param int $index + * The index. + * @param callable $callback + * A callback passed two arguments, the event name and event. + */ + protected function assertEventDispatchedAtIndex($index, $callback) { + $this->eventDispatcher + ->expects($this->at($index)) + ->method('dispatch') + ->willReturnCallback($callback); + } + +} diff --git a/core/modules/content_moderation/tests/src/Kernel/ContentModerationStateStorageSchemaTest.php b/core/modules/content_moderation/tests/src/Kernel/ContentModerationStateStorageSchemaTest.php index af59651..38de772 100644 --- a/core/modules/content_moderation/tests/src/Kernel/ContentModerationStateStorageSchemaTest.php +++ b/core/modules/content_moderation/tests/src/Kernel/ContentModerationStateStorageSchemaTest.php @@ -2,6 +2,8 @@ namespace Drupal\Tests\content_moderation\Kernel; +use Drupal\block_content\Entity\BlockContent; +use Drupal\block_content\Entity\BlockContentType; use Drupal\content_moderation\Entity\ContentModerationState; use Drupal\KernelTests\KernelTestBase; use Drupal\node\Entity\Node; @@ -26,7 +28,7 @@ class ContentModerationStateStorageSchemaTest extends KernelTestBase { 'system', 'text', 'workflows', - 'entity_test', + 'block_content', ]; /** @@ -37,7 +39,7 @@ protected function setUp() { $this->installSchema('node', 'node_access'); $this->installEntitySchema('node'); - $this->installEntitySchema('entity_test'); + $this->installEntitySchema('block_content'); $this->installEntitySchema('user'); $this->installEntitySchema('content_moderation_state'); $this->installConfig('content_moderation'); @@ -45,8 +47,15 @@ protected function setUp() { NodeType::create([ 'type' => 'example', ])->save(); + + BlockContentType::create([ + 'label' => 'Test', + 'id' => 'example', + ])->save(); + $workflow = Workflow::load('editorial'); $workflow->getTypePlugin()->addEntityTypeAndBundle('node', 'example'); + $workflow->getTypePlugin()->addEntityTypeAndBundle('block_content', 'example'); $workflow->save(); } @@ -88,18 +97,22 @@ public function testUniqueKeys() { ], FALSE); // Different entity types should not trigger an exception. - $this->assertStorageException([ - 'content_entity_type_id' => 'entity_test', - 'content_entity_id' => $node->id(), - 'content_entity_revision_id' => $node->getRevisionId(), - ], FALSE); + $block_content = BlockContent::create([ + 'info' => 'Test block', + 'type' => 'example', + 'moderation_state' => 'draft', + ]); + $block_content->save(); + $this->assertEquals($block_content->id(), $node->id()); + $this->assertEquals($block_content->getRevisionId(), $node->getRevisionId()); // Different entity and revision IDs should not trigger an exception. - $this->assertStorageException([ - 'content_entity_type_id' => $node->getEntityTypeId(), - 'content_entity_id' => 9999, - 'content_entity_revision_id' => 9999, - ], FALSE); + $second_node = Node::create([ + 'title' => 'Test title', + 'type' => 'example', + 'moderation_state' => 'draft', + ]); + $second_node->save(); // Creating a version of the entity with a previously used, but not current // revision ID should trigger an exception.