diff --git a/Content/Application/ContentMerger/Merger/DimensionContentMerger.php b/Content/Application/ContentMerger/Merger/DimensionContentMerger.php new file mode 100644 index 00000000..33b3027e --- /dev/null +++ b/Content/Application/ContentMerger/Merger/DimensionContentMerger.php @@ -0,0 +1,38 @@ +getGhostLocale()) { + $targetObject->setGhostLocale($ghostLocale); + } + + foreach ($sourceObject->getAvailableLocales() ?: [] as $availableLocale) { + $targetObject->addAvailableLocale($availableLocale); + } + } +} diff --git a/Content/Application/DimensionContentCollectionFactory/DimensionContentCollectionFactory.php b/Content/Application/DimensionContentCollectionFactory/DimensionContentCollectionFactory.php index 18964c14..4b2e45d1 100644 --- a/Content/Application/DimensionContentCollectionFactory/DimensionContentCollectionFactory.php +++ b/Content/Application/DimensionContentCollectionFactory/DimensionContentCollectionFactory.php @@ -13,8 +13,6 @@ namespace Sulu\Bundle\ContentBundle\Content\Application\DimensionContentCollectionFactory; -use Doctrine\Common\Collections\ArrayCollection; -use Doctrine\Common\Collections\Collection; use Sulu\Bundle\ContentBundle\Content\Application\ContentDataMapper\ContentDataMapperInterface; use Sulu\Bundle\ContentBundle\Content\Domain\Factory\DimensionContentCollectionFactoryInterface; use Sulu\Bundle\ContentBundle\Content\Domain\Model\ContentRichEntityInterface; @@ -60,41 +58,40 @@ public function create( $dimensionContentCollection = $this->dimensionContentRepository->load($contentRichEntity, $dimensionAttributes); $dimensionAttributes = $dimensionContentCollection->getDimensionAttributes(); - $orderedContentDimensions = \iterator_to_array($dimensionContentCollection); - $dimensionContents = new ArrayCollection($orderedContentDimensions); - $unlocalizedAttributes = $dimensionAttributes; $unlocalizedAttributes['locale'] = null; - // get or create unlocalized dimension content $unlocalizedDimensionContent = $dimensionContentCollection->getDimensionContent($unlocalizedAttributes); if (!$unlocalizedDimensionContent) { $unlocalizedDimensionContent = $this->createContentDimension( $contentRichEntity, - $dimensionContents, $unlocalizedAttributes ); - $orderedContentDimensions[] = $unlocalizedDimensionContent; } $localizedDimensionContent = null; if (isset($dimensionAttributes['locale'])) { - // get or create localized dimension content $localizedDimensionContent = $dimensionContentCollection->getDimensionContent($dimensionAttributes); if (!$localizedDimensionContent) { $localizedDimensionContent = $this->createContentDimension( $contentRichEntity, - $dimensionContents, $dimensionAttributes ); - $orderedContentDimensions[] = $localizedDimensionContent; + $unlocalizedDimensionContent->addAvailableLocale($localizedDimensionContent->getLocale()); + + if (!$unlocalizedDimensionContent->getGhostLocale() && $localizedDimensionContent) { + $unlocalizedDimensionContent->setGhostLocale($localizedDimensionContent->getLocale()); + } } } $dimensionContentCollection = new DimensionContentCollection( - $orderedContentDimensions, + \array_filter([ + $unlocalizedDimensionContent, + $localizedDimensionContent, + ]), $dimensionAttributes, $dimensionContentCollection->getDimensionContentClass() ); @@ -105,12 +102,10 @@ public function create( } /** - * @param Collection $dimensionContents * @param mixed[] $attributes */ private function createContentDimension( ContentRichEntityInterface $contentRichEntity, - Collection $dimensionContents, array $attributes ): DimensionContentInterface { $dimensionContent = $contentRichEntity->createDimensionContent(); @@ -120,7 +115,6 @@ private function createContentDimension( } $contentRichEntity->addDimensionContent($dimensionContent); - $dimensionContents->add($dimensionContent); return $dimensionContent; } diff --git a/Content/Domain/Model/DimensionContentInterface.php b/Content/Domain/Model/DimensionContentInterface.php index b6450ca2..e0447167 100644 --- a/Content/Domain/Model/DimensionContentInterface.php +++ b/Content/Domain/Model/DimensionContentInterface.php @@ -22,16 +22,39 @@ public static function getResourceKey(): string; public function getLocale(): ?string; + /** + * @internal should only be set by content bundle services not from outside + */ public function setLocale(?string $locale): void; + /** + * @internal should only be set by content bundle services not from outside + */ + public function setGhostLocale(?string $ghostLocale): void; + + public function getGhostLocale(): ?string; + + /** + * @internal should only be set by content bundle services not from outside + */ + public function addAvailableLocale(string $availableLocale): void; + + public function getAvailableLocales(): ?array; + public function getStage(): string; + /** + * @internal should only be set by content bundle services not from outside + */ public function setStage(string $stage): void; public function getResource(): ContentRichEntityInterface; public function isMerged(): bool; + /** + * @internal should only be set by content bundle services not from outside + */ public function markAsMerged(): void; /** diff --git a/Content/Domain/Model/DimensionContentTrait.php b/Content/Domain/Model/DimensionContentTrait.php index e485d22e..33005433 100644 --- a/Content/Domain/Model/DimensionContentTrait.php +++ b/Content/Domain/Model/DimensionContentTrait.php @@ -20,6 +20,16 @@ trait DimensionContentTrait */ protected $locale; + /** + * @var string|null + */ + protected $ghostLocale; + + /** + * @var string[]|null + */ + protected $availableLocales; + /** * @var string */ @@ -30,6 +40,9 @@ trait DimensionContentTrait */ private $isMerged = false; + /** + * @internal should only be set by content bundle services not from outside + */ public function setLocale(?string $locale): void { $this->locale = $locale; @@ -40,6 +53,41 @@ public function getLocale(): ?string return $this->locale; } + /** + * @internal should only be set by content bundle services not from outside + */ + public function setGhostLocale(?string $ghostLocale): void + { + $this->ghostLocale = $ghostLocale; + } + + public function getGhostLocale(): ?string + { + return $this->ghostLocale; + } + + /** + * @internal should only be set by content bundle services not from outside + */ + public function addAvailableLocale(string $availableLocale): void + { + if (null === $this->availableLocales) { + $this->availableLocales = []; + } + + if (!\in_array($availableLocale, $this->availableLocales, true)) { + $this->availableLocales[] = $availableLocale; + } + } + + public function getAvailableLocales(): ?array + { + return $this->availableLocales; + } + + /** + * @internal should only be set by content bundle services not from outside + */ public function setStage(string $stage): void { $this->stage = $stage; @@ -55,6 +103,9 @@ public function isMerged(): bool return $this->isMerged; } + /** + * @internal should only be set by content bundle services not from outside + */ public function markAsMerged(): void { $this->isMerged = true; diff --git a/Content/Infrastructure/Doctrine/DimensionContentQueryEnhancer.php b/Content/Infrastructure/Doctrine/DimensionContentQueryEnhancer.php index da3edd89..69eedc22 100644 --- a/Content/Infrastructure/Doctrine/DimensionContentQueryEnhancer.php +++ b/Content/Infrastructure/Doctrine/DimensionContentQueryEnhancer.php @@ -23,8 +23,7 @@ use Webmozart\Assert\Assert; /** - * TODO add loadGhost functionality - * add loadShadow functionality. + * TODO add loadShadow functionality. * * @final */ @@ -88,6 +87,7 @@ public function __construct( * tagNames?: string[], * tagOperator?: 'AND'|'OR', * templateKeys?: string[], + * loadGhost?: bool, * } $filters */ public function addFilters( @@ -114,6 +114,11 @@ public function addFilters( continue; } + if ('locale' === $key && ($filters['loadGhost'] ?? false)) { + // do not filter by locale when loadGhost is active + continue; + } + $queryBuilder->andWhere('filterDimensionContent.' . $key . '= :' . $key) ->setParameter($key, $value); } diff --git a/Content/Infrastructure/Doctrine/MetadataLoader.php b/Content/Infrastructure/Doctrine/MetadataLoader.php index 54601194..a21cc812 100644 --- a/Content/Infrastructure/Doctrine/MetadataLoader.php +++ b/Content/Infrastructure/Doctrine/MetadataLoader.php @@ -47,6 +47,8 @@ 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, 'ghostLocale', 'string', ['length' => 7, 'nullable' => true]); + $this->addField($metadata, 'availableLocales', 'json', ['nullable' => true]); } if ($reflection->implementsInterface(SeoInterface::class)) { diff --git a/Resources/config/merger.xml b/Resources/config/merger.xml index 4b33debc..9a6928f3 100644 --- a/Resources/config/merger.xml +++ b/Resources/config/merger.xml @@ -4,6 +4,10 @@ xsi:schemaLocation="http://symfony.com/schema/dic/services http://symfony.com/schema/dic/services/services-1.0.xsd"> + + + + diff --git a/Tests/Application/ExampleTestBundle/Controller/ExampleController.php b/Tests/Application/ExampleTestBundle/Controller/ExampleController.php index 06b8c65d..3ec59dbb 100644 --- a/Tests/Application/ExampleTestBundle/Controller/ExampleController.php +++ b/Tests/Application/ExampleTestBundle/Controller/ExampleController.php @@ -97,6 +97,8 @@ public function cgetAction(Request $request): Response $fieldDescriptors = $this->fieldDescriptorFactory->getFieldDescriptors(Example::RESOURCE_KEY); /** @var DoctrineListBuilder $listBuilder */ $listBuilder = $this->listBuilderFactory->create(Example::class); + $listBuilder->addSelectField($fieldDescriptors['locale']); + $listBuilder->addSelectField($fieldDescriptors['ghostLocale']); $listBuilder->setParameter('locale', $request->query->get('locale')); $this->restHelper->initializeListBuilder($listBuilder, $fieldDescriptors); @@ -183,6 +185,25 @@ public function postTriggerAction(string $id, Request $request): Response $action = $request->query->get('action'); switch ($action) { + case 'copy-locale': + $dimensionContent = $this->contentManager->copy( + $example, + [ + 'stage' => DimensionContentInterface::STAGE_DRAFT, + 'locale' => $request->query->get('src'), + ], + $example, + [ + 'stage' => DimensionContentInterface::STAGE_DRAFT, + 'locale' => $request->query->get('dest'), + ], + ); + + $this->entityManager->flush(); + + return $this->handleView($this->view($this->normalize($example, $dimensionContent))); + + break; case 'unpublish': $dimensionContent = $this->contentManager->applyTransition( $example, diff --git a/Tests/Application/ExampleTestBundle/Resources/config/lists/examples.xml b/Tests/Application/ExampleTestBundle/Resources/config/lists/examples.xml index b96c3362..e082637d 100644 --- a/Tests/Application/ExampleTestBundle/Resources/config/lists/examples.xml +++ b/Tests/Application/ExampleTestBundle/Resources/config/lists/examples.xml @@ -11,17 +11,56 @@ + + + unlocalizedDimensionContent + Sulu\Bundle\ContentBundle\Tests\Application\ExampleTestBundle\Entity\Example.dimensionContents + LEFT + unlocalizedDimensionContent.locale IS NULL AND unlocalizedDimensionContent.stage = 'draft' + + + + + + ghostDimensionContent + Sulu\Bundle\ContentBundle\Tests\Application\ExampleTestBundle\Entity\Example.dimensionContents + LEFT + ghostDimensionContent.locale = unlocalizedDimensionContent.ghostLocale AND ghostDimensionContent.stage = 'draft' + + + id Sulu\Bundle\ContentBundle\Tests\Application\ExampleTestBundle\Entity\Example - - title + + + title + dimensionContent + + + + + + title + ghostDimensionContent + + + + + + + + + locale dimensionContent + - + + ghostLocale + unlocalizedDimensionContent diff --git a/Tests/Functional/Integration/ExampleControllerTest.php b/Tests/Functional/Integration/ExampleControllerTest.php index cfeac399..b4123777 100644 --- a/Tests/Functional/Integration/ExampleControllerTest.php +++ b/Tests/Functional/Integration/ExampleControllerTest.php @@ -165,6 +165,36 @@ public function testGet(int $id): void $this->assertHttpStatusCode(404, $response); } + /** + * @depends testPost + */ + public function testGetGhostLocale(int $id): void + { + $this->client->request('GET', '/admin/api/examples/' . $id . '?locale=de'); + $response = $this->client->getResponse(); + $this->assertResponseSnapshot('example_get_ghost_locale.json', $response, 200); + + self::ensureKernelShutdown(); + + $websiteClient = $this->createWebsiteClient(); + $websiteClient->request('GET', '/de/my-example'); + + $response = $websiteClient->getResponse(); + $this->assertHttpStatusCode(404, $response); + } + + /** + * @depends testPost + */ + public function testPostTriggerCopyLocale(int $id): void + { + $this->client->request('POST', '/admin/api/examples/' . $id . '?locale=de&action=copy-locale&src=en&dest=de'); + + $response = $this->client->getResponse(); + + $this->assertResponseSnapshot('example_post_trigger_copy_locale.json', $response, 200); + } + /** * @depends testPost * @depends testGet diff --git a/Tests/Functional/Integration/responses/example_get.json b/Tests/Functional/Integration/responses/example_get.json index 4ea0b4a5..e5d295ca 100644 --- a/Tests/Functional/Integration/responses/example_get.json +++ b/Tests/Functional/Integration/responses/example_get.json @@ -1,6 +1,9 @@ { "author": null, "authored": "2020-05-08T00:00:00+00:00", + "availableLocales": [ + "en" + ], "excerptCategories": [], "excerptDescription": "Excerpt Description", "excerptIcon": null, @@ -11,6 +14,7 @@ "Tag 2" ], "excerptTitle": "Excerpt Title", + "ghostLocale": "en", "id": "@integer@", "images": null, "locale": "en", diff --git a/Tests/Functional/Integration/responses/example_get_ghost_locale.json b/Tests/Functional/Integration/responses/example_get_ghost_locale.json new file mode 100644 index 00000000..51cc5e11 --- /dev/null +++ b/Tests/Functional/Integration/responses/example_get_ghost_locale.json @@ -0,0 +1,28 @@ +{ + "author": null, + "authored": null, + "availableLocales": ["en"], + "excerptCategories": [], + "excerptDescription": null, + "excerptIcon": null, + "excerptImage": null, + "excerptMore": null, + "excerptTags": [], + "excerptTitle":null, + "ghostLocale": "en", + "id": "@integer@", + "locale": null, + "published": null, + "publishedState": false, + "seoCanonicalUrl": null, + "seoDescription": null, + "seoHideInSitemap": false, + "seoKeywords": null, + "seoNoFollow": false, + "seoNoIndex": false, + "seoTitle": null, + "stage": "draft", + "template": null, + "title": null, + "workflowPlace": null +} diff --git a/Tests/Functional/Integration/responses/example_post.json b/Tests/Functional/Integration/responses/example_post.json index 4ea0b4a5..e5d295ca 100644 --- a/Tests/Functional/Integration/responses/example_post.json +++ b/Tests/Functional/Integration/responses/example_post.json @@ -1,6 +1,9 @@ { "author": null, "authored": "2020-05-08T00:00:00+00:00", + "availableLocales": [ + "en" + ], "excerptCategories": [], "excerptDescription": "Excerpt Description", "excerptIcon": null, @@ -11,6 +14,7 @@ "Tag 2" ], "excerptTitle": "Excerpt Title", + "ghostLocale": "en", "id": "@integer@", "images": null, "locale": "en", diff --git a/Tests/Functional/Integration/responses/example_post_publish.json b/Tests/Functional/Integration/responses/example_post_publish.json index 3a862ceb..55465fab 100644 --- a/Tests/Functional/Integration/responses/example_post_publish.json +++ b/Tests/Functional/Integration/responses/example_post_publish.json @@ -1,6 +1,9 @@ { "author": null, "authored": "2020-05-08T00:00:00+00:00", + "availableLocales": [ + "en" + ], "excerptCategories": [], "excerptDescription": "Excerpt Description", "excerptIcon": null, @@ -11,6 +14,7 @@ "Tag 2" ], "excerptTitle": "Excerpt Title", + "ghostLocale": "en", "id": "@integer@", "images": null, "locale": "en", diff --git a/Tests/Functional/Integration/responses/example_post_trigger_copy_locale.json b/Tests/Functional/Integration/responses/example_post_trigger_copy_locale.json new file mode 100644 index 00000000..39bd1e1a --- /dev/null +++ b/Tests/Functional/Integration/responses/example_post_trigger_copy_locale.json @@ -0,0 +1,36 @@ +{ + "author": null, + "authored": "2020-05-08T00:00:00+00:00", + "availableLocales": [ + "en", + "de" + ], + "excerptCategories": [], + "excerptDescription": "Excerpt Description", + "excerptIcon": null, + "excerptImage": null, + "excerptMore": "Excerpt More", + "excerptTags": [ + "Tag 1", + "Tag 2" + ], + "excerptTitle": "Excerpt Title", + "ghostLocale": "en", + "id": "@integer@", + "images": null, + "locale": "de", + "published": null, + "publishedState": false, + "seoCanonicalUrl": "https://sulu.io/", + "seoDescription": "Seo Description", + "seoHideInSitemap": true, + "seoKeywords": "Seo Keyword 1, Seo Keyword 2", + "seoNoFollow": true, + "seoNoIndex": true, + "seoTitle": "Seo Title", + "stage": "draft", + "template": "example-2", + "title": "Test Example", + "url": "/my-example", + "workflowPlace": "unpublished" +} diff --git a/Tests/Functional/Integration/responses/example_post_trigger_unpublish.json b/Tests/Functional/Integration/responses/example_post_trigger_unpublish.json index 963a1022..810f5d51 100644 --- a/Tests/Functional/Integration/responses/example_post_trigger_unpublish.json +++ b/Tests/Functional/Integration/responses/example_post_trigger_unpublish.json @@ -1,6 +1,9 @@ { "author": null, "authored": "2020-05-08T00:00:00+00:00", + "availableLocales": [ + "en" + ], "excerptCategories": [], "excerptDescription": "Excerpt Description", "excerptIcon": null, @@ -11,6 +14,7 @@ "Tag 2" ], "excerptTitle": "Excerpt Title", + "ghostLocale": "en", "id": "@integer@", "images": null, "locale": "en", diff --git a/Tests/Functional/Integration/responses/example_put.json b/Tests/Functional/Integration/responses/example_put.json index 2e89dd25..aa034e86 100644 --- a/Tests/Functional/Integration/responses/example_put.json +++ b/Tests/Functional/Integration/responses/example_put.json @@ -2,6 +2,10 @@ "author": null, "authored": "2020-05-08T00:00:00+00:00", "article": "

Test Article 2

", + "availableLocales": [ + "en", + "de" + ], "blocks": null, "description": null, "excerptCategories": [], @@ -14,6 +18,7 @@ "Tag 4" ], "excerptTitle": "Excerpt Title 2", + "ghostLocale": "en", "id": "@integer@", "image": null, "locale": "en", diff --git a/Tests/Unit/Content/Application/ContentNormalizer/ContentNormalizerTest.php b/Tests/Unit/Content/Application/ContentNormalizer/ContentNormalizerTest.php index 12dbd407..4809454b 100644 --- a/Tests/Unit/Content/Application/ContentNormalizer/ContentNormalizerTest.php +++ b/Tests/Unit/Content/Application/ContentNormalizer/ContentNormalizerTest.php @@ -68,6 +68,8 @@ public function __construct(ContentRichEntityInterface $resource) $this->resource = $resource; $this->locale = 'de'; $this->stage = 'live'; + $this->ghostLocale = 'de'; + $this->availableLocales = ['de']; } public static function getResourceKey(): string @@ -83,6 +85,8 @@ public function getResource(): ContentRichEntityInterface $contentNormalizer = $this->createContentNormalizerInstance(); $this->assertSame([ + 'availableLocales' => ['de'], + 'ghostLocale' => 'de', 'id' => 5, 'locale' => 'de', 'stage' => 'live', @@ -130,6 +134,10 @@ public function getResource(): ContentRichEntityInterface } }; + $object->setGhostLocale('en'); + $object->addAvailableLocale('en'); + $object->addAvailableLocale('de'); + $tag1 = $this->prophesize(TagInterface::class); $tag1->getId()->willReturn(1); $tag1->getName()->willReturn('Tag 1'); @@ -168,6 +176,7 @@ public function getResource(): ContentRichEntityInterface $contentNormalizer = $this->createContentNormalizerInstance(); $this->assertSame([ + 'availableLocales' => ['en', 'de'], 'excerptCategories' => [ 3, 4, @@ -181,6 +190,7 @@ public function getResource(): ContentRichEntityInterface 'Tag 2', ], 'excerptTitle' => 'Excerpt Title', + 'ghostLocale' => 'en', 'id' => 5, 'locale' => 'de', 'published' => '2020-02-02T12:30:00+00:00', diff --git a/Tests/Unit/Content/Application/DimensionContentCollectionFactory/DimensionContentCollectionFactoryTest.php b/Tests/Unit/Content/Application/DimensionContentCollectionFactory/DimensionContentCollectionFactoryTest.php index fa48971c..7f0bc813 100644 --- a/Tests/Unit/Content/Application/DimensionContentCollectionFactory/DimensionContentCollectionFactoryTest.php +++ b/Tests/Unit/Content/Application/DimensionContentCollectionFactory/DimensionContentCollectionFactoryTest.php @@ -22,6 +22,7 @@ use Sulu\Bundle\ContentBundle\Content\Domain\Model\DimensionContentCollectionInterface; use Sulu\Bundle\ContentBundle\Content\Domain\Model\DimensionContentInterface; use Sulu\Bundle\ContentBundle\Content\Domain\Repository\DimensionContentRepositoryInterface; +use Sulu\Bundle\ContentBundle\Tests\Application\ExampleTestBundle\Entity\Example; use Sulu\Bundle\ContentBundle\Tests\Application\ExampleTestBundle\Entity\ExampleDimensionContent; use Symfony\Component\PropertyAccess\PropertyAccessor; @@ -54,12 +55,12 @@ protected function createDimensionContentCollectionFactoryInstance( public function testCreateWithExistingDimensionContent(): void { - $dimensionContent1 = $this->prophesize(DimensionContentInterface::class); - $dimensionContent1->getLocale()->willReturn(null); - $dimensionContent1->getStage()->willReturn('draft'); - $dimensionContent2 = $this->prophesize(DimensionContentInterface::class); - $dimensionContent2->getLocale()->willReturn('de'); - $dimensionContent2->getStage()->willReturn('draft'); + $contentRichEntity = $this->prophesize(Example::class); + $dimensionContent1 = new ExampleDimensionContent($contentRichEntity->reveal()); + $dimensionContent1->setStage('draft'); + $dimensionContent2 = new ExampleDimensionContent($contentRichEntity->reveal()); + $dimensionContent2->setStage('draft'); + $dimensionContent2->setLocale('de'); $contentRichEntity = $this->prophesize(ContentRichEntityInterface::class); @@ -76,7 +77,7 @@ public function testCreateWithExistingDimensionContent(): void $contentDataMapper->map( Argument::that( function(DimensionContentCollectionInterface $collection) use ($dimensionContent1, $dimensionContent2) { - return [$dimensionContent1->reveal(), $dimensionContent2->reveal()] === \iterator_to_array($collection); + return [$dimensionContent1, $dimensionContent2] === \iterator_to_array($collection); } ), $attributes, @@ -86,8 +87,8 @@ function(DimensionContentCollectionInterface $collection) use ($dimensionContent $dimensionContentCollectionFactoryInstance = $this->createDimensionContentCollectionFactoryInstance( $attributes, [ - $dimensionContent1->reveal(), - $dimensionContent2->reveal(), + $dimensionContent1, + $dimensionContent2, ], $contentDataMapper->reveal() ); @@ -102,33 +103,24 @@ function(DimensionContentCollectionInterface $collection) use ($dimensionContent $this->assertSame(ExampleDimensionContent::class, $dimensionContentCollection->getDimensionContentClass()); $this->assertSame($attributes, $dimensionContentCollection->getDimensionAttributes()); $this->assertSame( - [$dimensionContent1->reveal(), $dimensionContent2->reveal()], + [$dimensionContent1, $dimensionContent2], \iterator_to_array($dimensionContentCollection) ); } public function testCreateWithoutExistingDimensionContent(): void { - $dimensionContent1 = $this->prophesize(DimensionContentInterface::class); - $dimensionContent1->setLocale(null) - ->shouldBeCalled(); - $dimensionContent1->setStage('draft') - ->shouldBeCalled(); - $dimensionContent1->getLocale()->willReturn(null); - $dimensionContent1->getStage()->willReturn('draft'); - $dimensionContent2 = $this->prophesize(DimensionContentInterface::class); - $dimensionContent2->setLocale('de') - ->shouldBeCalled(); - $dimensionContent2->setStage('draft') - ->shouldBeCalled(); - $dimensionContent2->getLocale()->willReturn('de'); - $dimensionContent2->getStage()->willReturn('draft'); + $contentRichEntity = $this->prophesize(Example::class); + $dimensionContent1 = new ExampleDimensionContent($contentRichEntity->reveal()); + $dimensionContent1->setStage('draft'); + $dimensionContent2 = new ExampleDimensionContent($contentRichEntity->reveal()); + $dimensionContent2->setStage('draft'); + $dimensionContent2->setLocale('de'); - $contentRichEntity = $this->prophesize(ContentRichEntityInterface::class); $contentRichEntity->createDimensionContent()->shouldBeCalledTimes(2) - ->willReturn($dimensionContent1->reveal(), $dimensionContent2->reveal()); - $contentRichEntity->addDimensionContent($dimensionContent1->reveal())->shouldBeCalled(); - $contentRichEntity->addDimensionContent($dimensionContent2->reveal())->shouldBeCalled(); + ->willReturn($dimensionContent1, $dimensionContent2); + $contentRichEntity->addDimensionContent($dimensionContent1)->shouldBeCalled(); + $contentRichEntity->addDimensionContent($dimensionContent2)->shouldBeCalled(); $attributes = [ 'locale' => 'de', @@ -143,7 +135,7 @@ public function testCreateWithoutExistingDimensionContent(): void $contentDataMapper->map( Argument::that( function(DimensionContentCollectionInterface $collection) use ($dimensionContent1, $dimensionContent2) { - return [$dimensionContent1->reveal(), $dimensionContent2->reveal()] === \iterator_to_array($collection); + return [$dimensionContent1, $dimensionContent2] === \iterator_to_array($collection); } ), $attributes, @@ -167,27 +159,26 @@ function(DimensionContentCollectionInterface $collection) use ($dimensionContent $this->assertSame(ExampleDimensionContent::class, $dimensionContentCollection->getDimensionContentClass()); $this->assertSame($attributes, $dimensionContentCollection->getDimensionAttributes()); $this->assertSame( - [$dimensionContent1->reveal(), $dimensionContent2->reveal()], + [$dimensionContent1, $dimensionContent2], \iterator_to_array($dimensionContentCollection) ); + $this->assertSame('de', $dimensionContent1->getGhostLocale()); + $this->assertSame(['de'], $dimensionContent1->getAvailableLocales()); } public function testCreateWithoutExistingLocalizedDimensionContent(): void { - $dimensionContent1 = $this->prophesize(DimensionContentInterface::class); - $dimensionContent1->getLocale()->willReturn(null); - $dimensionContent1->getStage()->willReturn('draft'); - $dimensionContent2 = $this->prophesize(DimensionContentInterface::class); - $dimensionContent2->setLocale('de') - ->shouldBeCalled(); - $dimensionContent2->setStage('draft') - ->shouldBeCalled(); - $dimensionContent2->getLocale()->willReturn('de'); - $dimensionContent2->getStage()->willReturn('draft'); - - $contentRichEntity = $this->prophesize(ContentRichEntityInterface::class); - $contentRichEntity->createDimensionContent()->shouldBeCalled()->willReturn($dimensionContent2->reveal()); - $contentRichEntity->addDimensionContent($dimensionContent2->reveal())->shouldBeCalled(); + $contentRichEntity = $this->prophesize(Example::class); + $dimensionContent1 = new ExampleDimensionContent($contentRichEntity->reveal()); + $dimensionContent1->setStage('draft'); + $dimensionContent2 = new ExampleDimensionContent($contentRichEntity->reveal()); + $dimensionContent2->setStage('draft'); + $dimensionContent2->setLocale('de'); + + $contentRichEntity->createDimensionContent() + ->shouldBeCalled() + ->willReturn($dimensionContent2); + $contentRichEntity->addDimensionContent($dimensionContent2)->shouldBeCalled(); $attributes = [ 'locale' => 'de', @@ -202,7 +193,7 @@ public function testCreateWithoutExistingLocalizedDimensionContent(): void $contentDataMapper->map( Argument::that( function(DimensionContentCollectionInterface $collection) use ($dimensionContent1, $dimensionContent2) { - return [$dimensionContent1->reveal(), $dimensionContent2->reveal()] === \iterator_to_array($collection); + return [$dimensionContent1, $dimensionContent2] === \iterator_to_array($collection); } ), $attributes, @@ -211,7 +202,7 @@ function(DimensionContentCollectionInterface $collection) use ($dimensionContent $dimensionContentCollectionFactoryInstance = $this->createDimensionContentCollectionFactoryInstance( $attributes, [ - $dimensionContent1->reveal(), + $dimensionContent1, ], $contentDataMapper->reveal() ); @@ -226,7 +217,7 @@ function(DimensionContentCollectionInterface $collection) use ($dimensionContent $this->assertSame(ExampleDimensionContent::class, $dimensionContentCollection->getDimensionContentClass()); $this->assertSame($attributes, $dimensionContentCollection->getDimensionAttributes()); $this->assertSame( - [$dimensionContent1->reveal(), $dimensionContent2->reveal()], + [$dimensionContent1, $dimensionContent2], \iterator_to_array($dimensionContentCollection) ); } diff --git a/Tests/Unit/Content/Domain/Model/DimensionContentTraitTest.php b/Tests/Unit/Content/Domain/Model/DimensionContentTraitTest.php index 68fcc934..0c98771f 100644 --- a/Tests/Unit/Content/Domain/Model/DimensionContentTraitTest.php +++ b/Tests/Unit/Content/Domain/Model/DimensionContentTraitTest.php @@ -45,6 +45,31 @@ public function testGetSetLocale(): void $this->assertSame('de', $model->getLocale()); } + public function testGetSetGhostLocale(): void + { + $model = $this->getDimensionContentInstance(); + $model->setGhostLocale('de'); + $this->assertSame('de', $model->getGhostLocale()); + } + + public function testAddGetAvailableLocales(): void + { + $model = $this->getDimensionContentInstance(); + $this->assertNull($model->getAvailableLocales()); + $model->addAvailableLocale('en'); + $model->addAvailableLocale('de'); + $this->assertSame(['en', 'de'], $model->getAvailableLocales()); + } + + public function testAddSameAvailableLocale(): void + { + $model = $this->getDimensionContentInstance(); + $this->assertNull($model->getAvailableLocales()); + $model->addAvailableLocale('de'); + $model->addAvailableLocale('de'); + $this->assertSame(['de'], $model->getAvailableLocales()); + } + public function testGetSetStage(): void { $model = $this->getDimensionContentInstance(); diff --git a/Tests/Unit/Content/Infrastructure/Doctrine/MetadataLoaderTest.php b/Tests/Unit/Content/Infrastructure/Doctrine/MetadataLoaderTest.php index 74dfe4a8..0062d300 100644 --- a/Tests/Unit/Content/Infrastructure/Doctrine/MetadataLoaderTest.php +++ b/Tests/Unit/Content/Infrastructure/Doctrine/MetadataLoaderTest.php @@ -133,6 +133,8 @@ public function dataProvider(): \Generator [ 'locale' => false, 'stage' => false, + 'ghostLocale' => false, + 'availableLocales' => false, ], [], [ @@ -146,6 +148,8 @@ public function dataProvider(): \Generator [ 'locale' => true, 'stage' => true, + 'ghostLocale' => true, + 'availableLocales' => true, ], [], [ diff --git a/Tests/Unit/Mocks/DimensionContentMockWrapperTrait.php b/Tests/Unit/Mocks/DimensionContentMockWrapperTrait.php index 25e4b8dd..65ff69f5 100644 --- a/Tests/Unit/Mocks/DimensionContentMockWrapperTrait.php +++ b/Tests/Unit/Mocks/DimensionContentMockWrapperTrait.php @@ -37,6 +37,26 @@ public function setLocale(?string $locale): void $this->instance->setLocale($locale); } + public function getGhostLocale(): ?string + { + return $this->instance->getGhostLocale(); + } + + public function setGhostLocale(?string $ghostLocale): void + { + $this->instance->setGhostLocale($ghostLocale); + } + + public function getAvailableLocales(): ?array + { + return $this->instance->getAvailableLocales(); + } + + public function addAvailableLocale(string $availableLocale): void + { + $this->instance->addAvailableLocale($availableLocale); + } + public function getStage(): string { return $this->instance->getStage(); diff --git a/UPGRADE.md b/UPGRADE.md index 62a49bec..cbab3abc 100644 --- a/UPGRADE.md +++ b/UPGRADE.md @@ -2,6 +2,46 @@ ## 0.7.0 +### Add ghostLocale and availableLocales field + +To support multi localization feature of sulu a ghostLocale need to be added to +the dimension content tables. + +```sql +ALTER TABLE test_example_dimension_contents ADD ghostLocale VARCHAR(7) DEFAULT NULL, ADD availableLocales JSON DEFAULT NULL; -- replace `test_example_dimension_contents` with your table + +-- Update ghostLocale: +UPDATE test_example_dimension_contents dc -- replace `test_example_dimension_contents` with your table +INNER JOIN test_example_dimension_contents dc2 -- replace `test_example_dimension_contents` with your table + ON dc2.stage = dc.stage + AND dc2.example_id = dc.example_id -- replace `example_id` with your relation + AND dc2.locale IS NOT NULL +SET dc.ghostLocale = dc2.locale +WHERE + dc.ghostLocale IS NULL + AND dc.locale IS NULL; + +-- Update availableLocales: +UPDATE test_example_dimension_contents dc -- replace `test_example_dimension_contents` with your table +LEFT JOIN ( + SELECT + dc3.stage, + dc3.example_id, -- replace `example_id` with your relation + CONCAT('["', REPLACE(GROUP_CONCAT(dc3.locale), ',', '","'), '"]') as availableLocales + FROM test_example_dimension_contents dc3 -- replace `test_example_dimension_contents` with your table + WHERE locale IS NOT NULL + GROUP BY + dc3.example_id, -- replace `example_id` with your relation + dc3.stage + ) as dc4 ON + dc4.example_id = dc.example_id -- replace `example_id` with your relation + AND dc4.stage = dc.stage +SET dc.availableLocales = dc4.availableLocales +WHERE + dc.availableLocales IS NULL + AND dc.locale IS NULL; +``` + ### ContentMapperInterface changed The `ContentMapperInterface` was changed as a preparation for refactoring the `DimensionContentCollection`: