diff --git a/Content/Application/ContentWorkflow/ContentWorkflow.php b/Content/Application/ContentWorkflow/ContentWorkflow.php index 5b18ab23..3eef504b 100644 --- a/Content/Application/ContentWorkflow/ContentWorkflow.php +++ b/Content/Application/ContentWorkflow/ContentWorkflow.php @@ -135,9 +135,11 @@ private function getWorkflow(): SymfonyWorkflowInterface // | New |--------->| Unpublished | | Review |---------->| Published | | draft | | Review draft | // | | | |<---------------------| | | |--------------->| |<----------------------------| | // +-----+ +-------------+ reject +--------+ +------------+ create draft +-------+ reject draft +---------------+ - // A | A | - // +---+ | publish | - // publish +----------------------------------------------------------------+ + // A | A | A | A | + // +---+ +---+ | | restore | | + // restore publish | +----------------------+ | + // | publish | + // +--------------------------------------------------------------------+ // Configures places $definition = $definitionBuilder diff --git a/Content/Application/ContentWorkflow/Subscriber/PublishTransitionSubscriber.php b/Content/Application/ContentWorkflow/Subscriber/PublishTransitionSubscriber.php index 7ed5e537..9001a878 100644 --- a/Content/Application/ContentWorkflow/Subscriber/PublishTransitionSubscriber.php +++ b/Content/Application/ContentWorkflow/Subscriber/PublishTransitionSubscriber.php @@ -19,6 +19,7 @@ use Sulu\Bundle\ContentBundle\Content\Domain\Model\DimensionContentCollectionInterface; use Sulu\Bundle\ContentBundle\Content\Domain\Model\DimensionContentInterface; use Sulu\Bundle\ContentBundle\Content\Domain\Model\WorkflowInterface; +use Sulu\Bundle\ContentBundle\Content\Domain\Repository\DimensionContentRepositoryInterface; use Symfony\Component\EventDispatcher\EventSubscriberInterface; use Symfony\Component\Workflow\Event\TransitionEvent; @@ -29,9 +30,17 @@ class PublishTransitionSubscriber implements EventSubscriberInterface */ private $contentCopier; - public function __construct(ContentCopierInterface $contentCopier) - { + /** + * @var DimensionContentRepositoryInterface + */ + private $dimensionContentRepository; + + public function __construct( + ContentCopierInterface $contentCopier, + DimensionContentRepositoryInterface $dimensionContentRepository + ) { $this->contentCopier = $contentCopier; + $this->dimensionContentRepository = $dimensionContentRepository; } public function onPublish(TransitionEvent $transitionEvent): void @@ -68,6 +77,21 @@ public function onPublish(TransitionEvent $transitionEvent): void $dimensionAttributes['stage'] = DimensionContentInterface::STAGE_LIVE; + // create new version + // TODO optimize latest version and publish locales on write process to avoid loading them here? + $version = 1 + $this->dimensionContentRepository->getLatestVersion($contentRichEntity); + $publishLocales = $this->dimensionContentRepository->getLocales($contentRichEntity, $dimensionAttributes); + + foreach ($publishLocales as $publishLocale) { + $this->contentCopier->copy( + $contentRichEntity, + \array_merge($dimensionAttributes, ['locale' => $publishLocale]), + $contentRichEntity, + \array_merge($dimensionAttributes, ['locale' => $publishLocale, 'version' => $version]) + ); + } + + // publish content into live workspace $this->contentCopier->copyFromDimensionContentCollection( $dimensionContentCollection, $contentRichEntity, diff --git a/Content/Domain/Model/DimensionContentInterface.php b/Content/Domain/Model/DimensionContentInterface.php index b6450ca2..15140da3 100644 --- a/Content/Domain/Model/DimensionContentInterface.php +++ b/Content/Domain/Model/DimensionContentInterface.php @@ -18,6 +18,8 @@ interface DimensionContentInterface public const STAGE_DRAFT = 'draft'; public const STAGE_LIVE = 'live'; + public const DEFAULT_VERSION = 0; + public static function getResourceKey(): string; public function getLocale(): ?string; @@ -28,6 +30,10 @@ public function getStage(): string; public function setStage(string $stage): void; + public function getVersion(): int; + + public function setVersion(int $version): void; + public function getResource(): ContentRichEntityInterface; public function isMerged(): bool; diff --git a/Content/Domain/Model/DimensionContentTrait.php b/Content/Domain/Model/DimensionContentTrait.php index e485d22e..e11e6baf 100644 --- a/Content/Domain/Model/DimensionContentTrait.php +++ b/Content/Domain/Model/DimensionContentTrait.php @@ -25,6 +25,11 @@ trait DimensionContentTrait */ protected $stage = DimensionContentInterface::STAGE_DRAFT; + /** + * @var int + */ + protected $version = DimensionContentInterface::DEFAULT_VERSION; + /** * @var bool */ @@ -50,6 +55,16 @@ public function getStage(): string return $this->stage; } + public function setVersion(int $version): void + { + $this->version = $version; + } + + public function getVersion(): int + { + return $this->version; + } + public function isMerged(): bool { return $this->isMerged; @@ -65,6 +80,7 @@ public static function getDefaultDimensionAttributes(): array return [ 'locale' => null, 'stage' => DimensionContentInterface::STAGE_DRAFT, + 'version' => DimensionContentInterface::DEFAULT_VERSION, ]; } diff --git a/Content/Domain/Repository/DimensionContentRepositoryInterface.php b/Content/Domain/Repository/DimensionContentRepositoryInterface.php index e31e9d51..562e1877 100644 --- a/Content/Domain/Repository/DimensionContentRepositoryInterface.php +++ b/Content/Domain/Repository/DimensionContentRepositoryInterface.php @@ -25,4 +25,16 @@ public function load( ContentRichEntityInterface $contentRichEntity, array $dimensionAttributes ): DimensionContentCollectionInterface; + + public function getLatestVersion(ContentRichEntityInterface $contentRichEntity): int; + + /** + * @param mixed[] $dimensionAttributes + * + * @return string[] + */ + public function getLocales( + ContentRichEntityInterface $contentRichEntity, + array $dimensionAttributes + ): array; } diff --git a/Content/Infrastructure/Doctrine/DimensionContentRepository.php b/Content/Infrastructure/Doctrine/DimensionContentRepository.php index 2440b92b..941925a3 100644 --- a/Content/Infrastructure/Doctrine/DimensionContentRepository.php +++ b/Content/Infrastructure/Doctrine/DimensionContentRepository.php @@ -77,4 +77,43 @@ public function load( $dimensionContentClass ); } + + public function getLatestVersion(ContentRichEntityInterface $contentRichEntity): int + { + $dimensionContentClass = $this->contentMetadataInspector->getDimensionContentClass(\get_class($contentRichEntity)); + + $queryBuilder = $this->entityManager->createQueryBuilder() + ->from($dimensionContentClass, 'dimensionContent') + ->select('dimensionContent.version') + ->orderBy('dimensionContent.version', 'DESC') + ->setMaxResults(1) + ->where('content.id = :id') + ->setParameter('id', $contentRichEntity->getId()); + + return (int) $queryBuilder->getQuery()->getSingleScalarResult(); + } + + public function getLocales( + ContentRichEntityInterface $contentRichEntity, + array $dimensionAttributes + ): array { + $dimensionContentClass = $this->contentMetadataInspector->getDimensionContentClass(\get_class($contentRichEntity)); + + $queryBuilder = $this->entityManager->createQueryBuilder() + ->from($dimensionContentClass, 'dimensionContent') + ->select('dimensionContent.locale') + ->where('content.id = :id') + ->andWhere('dimensionContent.locale IS NOT NULL') + ->setParameter('id', $contentRichEntity->getId()); + + unset($dimensionAttributes['locale']); + foreach ($dimensionAttributes as $key => $value) { + $queryBuilder->andWhere('dimensionContent.' . $key, ':' . $key) + ->setParameter(':' . $key, $value); + } + + return \array_map(function($row) { + return $row['locale']; + }, $queryBuilder->getQuery()->getArrayResult()); + } } diff --git a/Content/Infrastructure/Doctrine/MetadataLoader.php b/Content/Infrastructure/Doctrine/MetadataLoader.php index 54601194..3de1054e 100644 --- a/Content/Infrastructure/Doctrine/MetadataLoader.php +++ b/Content/Infrastructure/Doctrine/MetadataLoader.php @@ -47,6 +47,7 @@ public function loadClassMetadata(LoadClassMetadataEventArgs $event): void if ($reflection->implementsInterface(DimensionContentInterface::class)) { $this->addField($metadata, 'stage', 'string', ['length' => 16, 'nullable' => false]); $this->addField($metadata, 'locale', 'string', ['length' => 7, 'nullable' => true]); + $this->addField($metadata, 'version', 'integer', ['nullable' => false, 'default' => 0]); } if ($reflection->implementsInterface(SeoInterface::class)) { diff --git a/Tests/Unit/Content/Application/ContentWorkflow/Subscriber/PublishTransitionSubscriberTest.php b/Tests/Unit/Content/Application/ContentWorkflow/Subscriber/PublishTransitionSubscriberTest.php index 21b6a5e2..0102e61a 100644 --- a/Tests/Unit/Content/Application/ContentWorkflow/Subscriber/PublishTransitionSubscriberTest.php +++ b/Tests/Unit/Content/Application/ContentWorkflow/Subscriber/PublishTransitionSubscriberTest.php @@ -22,21 +22,30 @@ use Sulu\Bundle\ContentBundle\Content\Domain\Model\DimensionContentCollectionInterface; use Sulu\Bundle\ContentBundle\Content\Domain\Model\DimensionContentInterface; use Sulu\Bundle\ContentBundle\Content\Domain\Model\WorkflowInterface; +use Sulu\Bundle\ContentBundle\Content\Domain\Repository\DimensionContentRepositoryInterface; use Symfony\Component\Workflow\Event\TransitionEvent; use Symfony\Component\Workflow\Marking; class PublishTransitionSubscriberTest extends TestCase { public function createContentPublisherSubscriberInstance( - ContentCopierInterface $contentCopier + ContentCopierInterface $contentCopier, + DimensionContentRepositoryInterface $dimensionContentRepository ): PublishTransitionSubscriber { - return new PublishTransitionSubscriber($contentCopier); + return new PublishTransitionSubscriber( + $contentCopier, + $dimensionContentRepository + ); } public function testGetSubscribedEvents(): void { $contentCopier = $this->prophesize(ContentCopierInterface::class); - $contentPublishSubscriber = $this->createContentPublisherSubscriberInstance($contentCopier->reveal()); + $dimensionContentRepository = $this->prophesize(DimensionContentRepositoryInterface::class); + $contentPublishSubscriber = $this->createContentPublisherSubscriberInstance( + $contentCopier->reveal(), + $dimensionContentRepository->reveal() + ); $this->assertSame([ 'workflow.content_workflow.transition.publish' => 'onPublish', @@ -54,7 +63,11 @@ public function testOnPublishNoDimensionContentInterface(): void $contentCopier = $this->prophesize(ContentCopierInterface::class); $contentCopier->copyFromDimensionContentCollection(Argument::cetera())->shouldNotBeCalled(); - $contentPublishSubscriber = $this->createContentPublisherSubscriberInstance($contentCopier->reveal()); + $dimensionContentRepository = $this->prophesize(DimensionContentRepositoryInterface::class); + $contentPublishSubscriber = $this->createContentPublisherSubscriberInstance( + $contentCopier->reveal(), + $dimensionContentRepository->reveal() + ); $contentPublishSubscriber->onPublish($event); } @@ -81,7 +94,11 @@ public function testOnPublishNoDimensionContentCollection(): void $contentCopier->copyFromDimensionContentCollection(Argument::any(), Argument::any(), Argument::any()) ->shouldNotBeCalled(); - $contentPublishSubscriber = $this->createContentPublisherSubscriberInstance($contentCopier->reveal()); + $dimensionContentRepository = $this->prophesize(DimensionContentRepositoryInterface::class); + $contentPublishSubscriber = $this->createContentPublisherSubscriberInstance( + $contentCopier->reveal(), + $dimensionContentRepository->reveal() + ); $contentPublishSubscriber->onPublish($event); } @@ -108,7 +125,11 @@ public function testOnPublishNoContentRichEntity(): void $contentCopier->copyFromDimensionContentCollection(Argument::any(), Argument::any(), Argument::any()) ->shouldNotBeCalled(); - $contentPublishSubscriber = $this->createContentPublisherSubscriberInstance($contentCopier->reveal()); + $dimensionContentRepository = $this->prophesize(DimensionContentRepositoryInterface::class); + $contentPublishSubscriber = $this->createContentPublisherSubscriberInstance( + $contentCopier->reveal(), + $dimensionContentRepository->reveal() + ); $contentPublishSubscriber->onPublish($event); } @@ -135,7 +156,11 @@ public function testOnPublishNoDimensionAttributes(): void $contentCopier->copyFromDimensionContentCollection(Argument::any(), Argument::any(), Argument::any()) ->shouldNotBeCalled(); - $contentPublishSubscriber = $this->createContentPublisherSubscriberInstance($contentCopier->reveal()); + $dimensionContentRepository = $this->prophesize(DimensionContentRepositoryInterface::class); + $contentPublishSubscriber = $this->createContentPublisherSubscriberInstance( + $contentCopier->reveal(), + $dimensionContentRepository->reveal() + ); $contentPublishSubscriber->onPublish($event); } @@ -162,19 +187,30 @@ public function testOnPublish(): void ]); $contentCopier = $this->prophesize(ContentCopierInterface::class); - $sourceDimensionAttributes = $dimensionAttributes; - $sourceDimensionAttributes['stage'] = 'live'; + $targetDimensionAttributes = $dimensionAttributes; + $targetDimensionAttributes['stage'] = 'live'; $resolvedCopiedContent = $this->prophesize(DimensionContentInterface::class); $contentCopier->copyFromDimensionContentCollection( $dimensionContentCollection->reveal(), $contentRichEntity->reveal(), - $sourceDimensionAttributes + $targetDimensionAttributes ) ->willReturn($resolvedCopiedContent->reveal()) ->shouldBeCalled(); - $contentPublishSubscriber = $this->createContentPublisherSubscriberInstance($contentCopier->reveal()); + $dimensionContentRepository = $this->prophesize(DimensionContentRepositoryInterface::class); + $dimensionContentRepository->getLatestVersion($contentRichEntity->reveal()) + ->willReturn(0) + ->shouldBeCalled(); + $dimensionContentRepository->getLocales($contentRichEntity->reveal(), $targetDimensionAttributes) + ->willReturn([]) + ->shouldBeCalled(); + + $contentPublishSubscriber = $this->createContentPublisherSubscriberInstance( + $contentCopier->reveal(), + $dimensionContentRepository->reveal() + ); $contentPublishSubscriber->onPublish($event); } @@ -201,19 +237,44 @@ public function testOnPublishExistingPublished(): void ]); $contentCopier = $this->prophesize(ContentCopierInterface::class); - $sourceDimensionAttributes = $dimensionAttributes; - $sourceDimensionAttributes['stage'] = 'live'; + $targetDimensionAttributes = $dimensionAttributes; + $targetDimensionAttributes['stage'] = 'live'; $resolvedCopiedContent = $this->prophesize(DimensionContentInterface::class); $contentCopier->copyFromDimensionContentCollection( $dimensionContentCollection->reveal(), $contentRichEntity->reveal(), - $sourceDimensionAttributes + $targetDimensionAttributes ) ->willReturn($resolvedCopiedContent->reveal()) ->shouldBeCalled(); - $contentPublishSubscriber = $this->createContentPublisherSubscriberInstance($contentCopier->reveal()); + $dimensionContentRepository = $this->prophesize(DimensionContentRepositoryInterface::class); + $dimensionContentRepository->getLatestVersion($contentRichEntity->reveal()) + ->willReturn(0) + ->shouldBeCalled(); + $dimensionContentRepository->getLocales($contentRichEntity->reveal(), $targetDimensionAttributes) + ->willReturn(['en', 'de']) + ->shouldBeCalled(); + + $contentCopier->copy( + $contentRichEntity->reveal(), + \array_merge($targetDimensionAttributes, ['locale' => 'en']), + $contentRichEntity->reveal(), + \array_merge($targetDimensionAttributes, ['locale' => 'en', 'version' => 1]) + )->shouldBeCalled(); + + $contentCopier->copy( + $contentRichEntity->reveal(), + \array_merge($targetDimensionAttributes, ['locale' => 'de']), + $contentRichEntity->reveal(), + \array_merge($targetDimensionAttributes, ['locale' => 'de', 'version' => 1]) + )->shouldBeCalled(); + + $contentPublishSubscriber = $this->createContentPublisherSubscriberInstance( + $contentCopier->reveal(), + $dimensionContentRepository->reveal() + ); $contentPublishSubscriber->onPublish($event); }