diff --git a/Content/Application/ContentCopier/ContentCopier.php b/Content/Application/ContentCopier/ContentCopier.php index 8056681a..b98b6881 100644 --- a/Content/Application/ContentCopier/ContentCopier.php +++ b/Content/Application/ContentCopier/ContentCopier.php @@ -59,29 +59,36 @@ public function copy( ContentRichEntityInterface $sourceContentRichEntity, array $sourceDimensionAttributes, ContentRichEntityInterface $targetContentRichEntity, - array $targetDimensionAttributes + array $targetDimensionAttributes, + array $options = [] ): DimensionContentInterface { $sourceDimensionContent = $this->contentResolver->resolve($sourceContentRichEntity, $sourceDimensionAttributes); - return $this->copyFromDimensionContent($sourceDimensionContent, $targetContentRichEntity, $targetDimensionAttributes); + return $this->copyFromDimensionContent($sourceDimensionContent, $targetContentRichEntity, $targetDimensionAttributes, $options); } public function copyFromDimensionContentCollection( DimensionContentCollectionInterface $dimensionContentCollection, ContentRichEntityInterface $targetContentRichEntity, - array $targetDimensionAttributes + array $targetDimensionAttributes, + array $options = [] ): DimensionContentInterface { $sourceDimensionContent = $this->contentMerger->merge($dimensionContentCollection); - return $this->copyFromDimensionContent($sourceDimensionContent, $targetContentRichEntity, $targetDimensionAttributes); + return $this->copyFromDimensionContent($sourceDimensionContent, $targetContentRichEntity, $targetDimensionAttributes, $options); } public function copyFromDimensionContent( DimensionContentInterface $dimensionContent, ContentRichEntityInterface $targetContentRichEntity, - array $targetDimensionAttributes + array $targetDimensionAttributes, + array $options = [] ): DimensionContentInterface { - $data = $this->contentNormalizer->normalize($dimensionContent); + $data = \array_replace($this->contentNormalizer->normalize($dimensionContent), $options['data'] ?? []); + + foreach (($options['ignoredAttributes'] ?? []) as $ignoredAttribute) { + unset($data[$ignoredAttribute]); + } return $this->contentPersister->persist($targetContentRichEntity, $data, $targetDimensionAttributes); } diff --git a/Content/Application/ContentCopier/ContentCopierInterface.php b/Content/Application/ContentCopier/ContentCopierInterface.php index 287b6db3..44935681 100644 --- a/Content/Application/ContentCopier/ContentCopierInterface.php +++ b/Content/Application/ContentCopier/ContentCopierInterface.php @@ -26,6 +26,7 @@ interface ContentCopierInterface * @param mixed[] $sourceDimensionAttributes * @param ContentRichEntityInterface $targetContentRichEntity * @param mixed[] $targetDimensionAttributes + * @param array{data?: mixed[], ignoredAttributes?: string[]} $options the "data" allows given custom data to the target and "ignoredAttributes" avoids specific attributes to be copied * * @return T */ @@ -33,7 +34,8 @@ public function copy( ContentRichEntityInterface $sourceContentRichEntity, array $sourceDimensionAttributes, ContentRichEntityInterface $targetContentRichEntity, - array $targetDimensionAttributes + array $targetDimensionAttributes, + array $options = [] ): DimensionContentInterface; /** @@ -42,13 +44,15 @@ public function copy( * @param DimensionContentCollectionInterface $dimensionContentCollection * @param ContentRichEntityInterface $targetContentRichEntity * @param mixed[] $targetDimensionAttributes + * @param array{data?: mixed[], ignoredAttributes?: string[]} $options the "data" allows given custom data to the target and "ignoredAttributes" avoids specific attributes to be copied * * @return T */ public function copyFromDimensionContentCollection( DimensionContentCollectionInterface $dimensionContentCollection, ContentRichEntityInterface $targetContentRichEntity, - array $targetDimensionAttributes + array $targetDimensionAttributes, + array $options = [] ): DimensionContentInterface; /** @@ -57,12 +61,14 @@ public function copyFromDimensionContentCollection( * @param T $dimensionContent * @param ContentRichEntityInterface $targetContentRichEntity * @param mixed[] $targetDimensionAttributes + * @param array{data?: mixed[], ignoredAttributes?: string[]} $options the "data" allows given custom data to the target and "ignoredAttributes" avoids specific attributes to be copied * * @return T */ public function copyFromDimensionContent( DimensionContentInterface $dimensionContent, ContentRichEntityInterface $targetContentRichEntity, - array $targetDimensionAttributes + array $targetDimensionAttributes, + array $options = [] ): DimensionContentInterface; } diff --git a/Content/Application/ContentDataMapper/DataMapper/RoutableDataMapper.php b/Content/Application/ContentDataMapper/DataMapper/RoutableDataMapper.php index 53939fff..3c499a5e 100644 --- a/Content/Application/ContentDataMapper/DataMapper/RoutableDataMapper.php +++ b/Content/Application/ContentDataMapper/DataMapper/RoutableDataMapper.php @@ -108,6 +108,12 @@ public function map( /** @var string $name */ $name = $property->getName(); + if ('url' !== $name) { + throw new \RuntimeException(\sprintf( + 'Expected a property with the name "url" but "%s" given.', + $name + )); // TODO move this validation to a compiler pass see also direct access of 'url' in PublishTransitionSubscriber class. + } $currentRoutePath = $localizedDimensionContent->getTemplateData()[$name] ?? null; if (!\array_key_exists($name, $data) && null !== $currentRoutePath) { @@ -194,6 +200,7 @@ public function map( private function getRouteProperty(StructureMetadata $metadata): ?PropertyMetadata { foreach ($metadata->getProperties() as $property) { + // TODO add support for page_tree_route field type: https://github.com/sulu/SuluContentBundle/issues/242 if ('route' === $property->getType()) { return $property; } diff --git a/Content/Application/ContentDataMapper/DataMapper/ShadowDataMapper.php b/Content/Application/ContentDataMapper/DataMapper/ShadowDataMapper.php index ec703703..ac397a71 100644 --- a/Content/Application/ContentDataMapper/DataMapper/ShadowDataMapper.php +++ b/Content/Application/ContentDataMapper/DataMapper/ShadowDataMapper.php @@ -23,7 +23,9 @@ public function map( DimensionContentInterface $localizedDimensionContent, array $data ): void { - if (!$localizedDimensionContent instanceof ShadowInterface) { + if (!$unlocalizedDimensionContent instanceof ShadowInterface + || !$localizedDimensionContent instanceof ShadowInterface + ) { return; } @@ -33,11 +35,19 @@ public function map( /** @var string|null $shadowLocale */ $shadowLocale = $data['shadowLocale'] ?? null; + $locale = $localizedDimensionContent->getLocale(); + $localizedDimensionContent->setShadowLocale( - $shadowOn + $shadowOn && $locale ? $shadowLocale : null ); + + if ($locale && $shadowLocale) { + $unlocalizedDimensionContent->addShadowLocale($locale, $shadowLocale); + } elseif ($locale) { + $unlocalizedDimensionContent->removeShadowLocale($locale); + } } } } diff --git a/Content/Application/ContentDataMapper/DataMapper/TemplateDataMapper.php b/Content/Application/ContentDataMapper/DataMapper/TemplateDataMapper.php index 4fcd7249..6a2cd042 100644 --- a/Content/Application/ContentDataMapper/DataMapper/TemplateDataMapper.php +++ b/Content/Application/ContentDataMapper/DataMapper/TemplateDataMapper.php @@ -62,10 +62,12 @@ public function map( throw new \RuntimeException('Expected "template" to be set in the data array.'); } - list($unlocalizedData, $localizedData, $hasAnyValue) = $this->getTemplateData( + [$unlocalizedData, $localizedData, $hasAnyValue] = $this->getTemplateData( $data, $type, - $template + $template, + $unlocalizedDimensionContent->getTemplateData(), + $localizedDimensionContent->getTemplateData() ); if (!isset($data['template']) && !$hasAnyValue) { @@ -75,36 +77,38 @@ public function map( $localizedDimensionContent->setTemplateKey($template); $localizedDimensionContent->setTemplateData($localizedData); - - $unlocalizedDimensionContent->setTemplateData(\array_merge( - $unlocalizedDimensionContent->getTemplateData(), - $unlocalizedData - )); + $unlocalizedDimensionContent->setTemplateData($unlocalizedData); } /** * @param mixed[] $data + * @param mixed[] $unlocalizedData + * @param mixed[] $localizedData * * @return array{ - * 0: mixed[], - * 1: mixed[], - * 2: bool, + * 0: mixed[], + * 1: mixed[], + * 2: bool, * } */ - private function getTemplateData(array $data, string $type, string $template): array - { + private function getTemplateData( + array $data, + string $type, + string $template, + array $unlocalizedData, + array $localizedData + ): array { $metadata = $this->factory->getStructureMetadata($type, $template); if (!$metadata) { throw new \RuntimeException(\sprintf('Could not find structure "%s" of type "%s".', $template, $type)); } - $unlocalizedData = []; - $localizedData = []; $hasAnyValue = false; + $defaultLocalizedData = $localizedData; // use existing localizedData only as default to remove not longer existing properties of the template + $localizedData = []; foreach ($metadata->getProperties() as $property) { - $value = null; $name = $property->getName(); // Float are converted to ints in php array as key so we need convert it to string @@ -112,7 +116,8 @@ private function getTemplateData(array $data, string $type, string $template): a $name = (string) $name; } - if (\array_key_exists($name, $data)) { + $value = $property->isLocalized() ? $defaultLocalizedData[$name] ?? null : $defaultLocalizedData[$name] ?? null; + if (\array_key_exists($name, $data)) { // values not explicitly given need to stay untouched for e.g. for shadow pages urls $hasAnyValue = true; $value = $data[$name]; } diff --git a/Content/Application/ContentMerger/Merger/ShadowMerger.php b/Content/Application/ContentMerger/Merger/ShadowMerger.php index 4fd84169..3831b97b 100644 --- a/Content/Application/ContentMerger/Merger/ShadowMerger.php +++ b/Content/Application/ContentMerger/Merger/ShadowMerger.php @@ -35,7 +35,7 @@ public function merge(object $targetObject, object $sourceObject): void $targetObject->setShadowLocale($shadowLocale); } - foreach ($sourceObject->getShadowLocales() ?: [] as $locale => $shadowLocale) { + foreach (($sourceObject->getShadowLocales() ?? []) as $locale => $shadowLocale) { $targetObject->addShadowLocale($locale, $shadowLocale); } } diff --git a/Content/Application/ContentWorkflow/Subscriber/PublishTransitionSubscriber.php b/Content/Application/ContentWorkflow/Subscriber/PublishTransitionSubscriber.php index 7ed5e537..69c89f29 100644 --- a/Content/Application/ContentWorkflow/Subscriber/PublishTransitionSubscriber.php +++ b/Content/Application/ContentWorkflow/Subscriber/PublishTransitionSubscriber.php @@ -18,10 +18,17 @@ use Sulu\Bundle\ContentBundle\Content\Domain\Model\ContentRichEntityInterface; use Sulu\Bundle\ContentBundle\Content\Domain\Model\DimensionContentCollectionInterface; use Sulu\Bundle\ContentBundle\Content\Domain\Model\DimensionContentInterface; +use Sulu\Bundle\ContentBundle\Content\Domain\Model\ShadowInterface; +use Sulu\Bundle\ContentBundle\Content\Domain\Model\TemplateInterface; use Sulu\Bundle\ContentBundle\Content\Domain\Model\WorkflowInterface; use Symfony\Component\EventDispatcher\EventSubscriberInterface; use Symfony\Component\Workflow\Event\TransitionEvent; +/** + * @final + * + * @internal this class is internal and should not be extended from or used in another context + */ class PublishTransitionSubscriber implements EventSubscriberInterface { /** @@ -66,12 +73,69 @@ public function onPublish(TransitionEvent $transitionEvent): void throw new \RuntimeException('No "contentRichEntity" given.'); } - $dimensionAttributes['stage'] = DimensionContentInterface::STAGE_LIVE; + $sourceDimensionAttributes = $dimensionAttributes; + $targetDimensionAttributes = $dimensionAttributes; + $targetDimensionAttributes['stage'] = DimensionContentInterface::STAGE_LIVE; + + $shadowLocale = $dimensionContent instanceof ShadowInterface + ? $dimensionContent->getShadowLocale() + : null; + + /** @var string $locale */ + $locale = $dimensionContent->getLocale(); + + if (!$shadowLocale) { + $publishedDimensionContent = $this->contentCopier->copyFromDimensionContentCollection( + $dimensionContentCollection, + $contentRichEntity, + $targetDimensionAttributes + ); + + if (!$publishedDimensionContent instanceof ShadowInterface) { + return; + } + + $shadowLocales = $publishedDimensionContent->getShadowLocalesForLocale($locale); + + foreach ($shadowLocales as $shadowLocale) { + $targetDimensionAttributes['locale'] = $shadowLocale; + + $this->contentCopier->copyFromDimensionContentCollection( + $dimensionContentCollection, + $contentRichEntity, + $targetDimensionAttributes, + [ + 'ignoredAttributes' => [ + 'shadowOn', + 'shadowLocale', + 'url', + ], + ] + ); + } + + return; + } + + $sourceDimensionAttributes['locale'] = $shadowLocale; + $sourceDimensionAttributes['stage'] = DimensionContentInterface::STAGE_LIVE; - $this->contentCopier->copyFromDimensionContentCollection( - $dimensionContentCollection, + $data = [ + // @see \Sulu\Bundle\ContentBundle\Content\Application\ContentDataMapper\DataMapper\ShadowDataMapper::map + 'shadowOn' => true, + 'shadowLocale' => $shadowLocale, + ]; + + if ($dimensionContent instanceof TemplateInterface) { + $data['url'] = $dimensionContent->getTemplateData()['url'] ?? null; // TODO get correct route property + } + + $this->contentCopier->copy( + $contentRichEntity, + $sourceDimensionAttributes, $contentRichEntity, - $dimensionAttributes + $targetDimensionAttributes, + ['data' => $data] ); } diff --git a/Content/Application/ContentWorkflow/Subscriber/RemoveDraftTransitionSubscriber.php b/Content/Application/ContentWorkflow/Subscriber/RemoveDraftTransitionSubscriber.php index 3e0f7402..f7737f11 100644 --- a/Content/Application/ContentWorkflow/Subscriber/RemoveDraftTransitionSubscriber.php +++ b/Content/Application/ContentWorkflow/Subscriber/RemoveDraftTransitionSubscriber.php @@ -21,6 +21,11 @@ use Symfony\Component\EventDispatcher\EventSubscriberInterface; use Symfony\Component\Workflow\Event\TransitionEvent; +/** + * @final + * + * @internal this class is internal and should not be extended from or used in another context + */ class RemoveDraftTransitionSubscriber implements EventSubscriberInterface { /** diff --git a/Content/Application/ContentWorkflow/Subscriber/UnpublishTransitionSubscriber.php b/Content/Application/ContentWorkflow/Subscriber/UnpublishTransitionSubscriber.php index 77b66a4b..b361f7e2 100644 --- a/Content/Application/ContentWorkflow/Subscriber/UnpublishTransitionSubscriber.php +++ b/Content/Application/ContentWorkflow/Subscriber/UnpublishTransitionSubscriber.php @@ -18,11 +18,17 @@ use Sulu\Bundle\ContentBundle\Content\Domain\Exception\ContentNotFoundException; use Sulu\Bundle\ContentBundle\Content\Domain\Model\ContentRichEntityInterface; use Sulu\Bundle\ContentBundle\Content\Domain\Model\DimensionContentInterface; +use Sulu\Bundle\ContentBundle\Content\Domain\Model\ShadowInterface; 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; +/** + * @final + * + * @internal this class is internal and should not be extended from or used in another context + */ class UnpublishTransitionSubscriber implements EventSubscriberInterface { /** @@ -83,6 +89,10 @@ public function onUnpublish(TransitionEvent $transitionEvent): void /** @var DimensionContentInterface $unlocalizedLiveDimensionContent */ $unlocalizedLiveDimensionContent = $dimensionContentCollection->getDimensionContent($unlocalizedLiveDimensionAttributes); // @phpstan-ignore-line we can not define the generic of DimensionContentInterface here $unlocalizedLiveDimensionContent->removeAvailableLocale($locale); + + if ($unlocalizedLiveDimensionContent instanceof ShadowInterface) { + $unlocalizedLiveDimensionContent->removeShadowLocale($locale); + } } $this->entityManager->remove($localizedLiveDimensionContent); diff --git a/Content/Domain/Model/RoutableInterface.php b/Content/Domain/Model/RoutableInterface.php index a37ce3b8..d08db9c1 100644 --- a/Content/Domain/Model/RoutableInterface.php +++ b/Content/Domain/Model/RoutableInterface.php @@ -13,9 +13,6 @@ namespace Sulu\Bundle\ContentBundle\Content\Domain\Model; -/** - * Marker interface for autoloading the doctrine metadata for routables. - */ interface RoutableInterface { public static function getResourceKey(): string; diff --git a/Content/Domain/Model/ShadowInterface.php b/Content/Domain/Model/ShadowInterface.php index 49413997..e119546c 100644 --- a/Content/Domain/Model/ShadowInterface.php +++ b/Content/Domain/Model/ShadowInterface.php @@ -33,7 +33,18 @@ public function addShadowLocale(string $locale, string $shadowLocale): void; public function removeShadowLocale(string $locale): void; /** + * Returns the locales which shadow the given locale. + * * @return array|null */ public function getShadowLocales(): ?array; + + /** + * @internal should only be set by content bundle services not from outside + * + * Returns the locales which shadow the given locale + * + * @return string[] + */ + public function getShadowLocalesForLocale(string $shadowLocale): array; } diff --git a/Content/Domain/Model/ShadowTrait.php b/Content/Domain/Model/ShadowTrait.php index fb0b436f..3425cd29 100644 --- a/Content/Domain/Model/ShadowTrait.php +++ b/Content/Domain/Model/ShadowTrait.php @@ -55,14 +55,39 @@ public function addShadowLocale(string $locale, string $shadowLocale): void */ public function removeShadowLocale(string $locale): void { + if (!$this->shadowLocales) { + return; + } + unset($this->shadowLocales[$locale]); + + if (0 === \count($this->shadowLocales)) { + $this->shadowLocales = null; + } } /** - * @return array + * @internal should only be set by content bundle services not from outside */ public function getShadowLocales(): ?array { return $this->shadowLocales; } + + /** + * @internal should only be set by content bundle services not from outside + * + * @return string[] + */ + public function getShadowLocalesForLocale(string $shadowLocale): array + { + $locales = []; + foreach (($this->shadowLocales ?? []) as $locale => $localeShadowLocale) { + if ($localeShadowLocale === $shadowLocale) { + $locales[] = $locale; + } + } + + return $locales; + } } diff --git a/Content/Infrastructure/Doctrine/DimensionContentQueryEnhancer.php b/Content/Infrastructure/Doctrine/DimensionContentQueryEnhancer.php index 9c64c46b..6b6f310f 100644 --- a/Content/Infrastructure/Doctrine/DimensionContentQueryEnhancer.php +++ b/Content/Infrastructure/Doctrine/DimensionContentQueryEnhancer.php @@ -95,8 +95,6 @@ public function addFilters( 'filterDimensionContent.' . $contentRichEntityAlias . ' = ' . $contentRichEntityAlias . '' ); - // TODO filter to shadow dimension - foreach ($effectiveAttributes as $key => $value) { if (null === $value) { $queryBuilder->andWhere('filterDimensionContent.' . $key . ' IS NULL'); diff --git a/Content/Infrastructure/Doctrine/MetadataLoader.php b/Content/Infrastructure/Doctrine/MetadataLoader.php index 477da89f..ad58ef6c 100644 --- a/Content/Infrastructure/Doctrine/MetadataLoader.php +++ b/Content/Infrastructure/Doctrine/MetadataLoader.php @@ -60,6 +60,18 @@ public function loadClassMetadata(LoadClassMetadataEventArgs $event): void $this->addIndex($metadata, 'idx_stage', ['stage']); } + if ($reflection->implementsInterface(ShadowInterface::class)) { + $this->addField($metadata, 'shadowLocale', 'string', ['length' => 7, 'nullable' => true]); + $this->addField($metadata, 'shadowLocales', 'json', ['nullable' => true, 'options' => ['jsonb' => true]]); + } + + if ($reflection->implementsInterface(TemplateInterface::class)) { + $this->addField($metadata, 'templateKey', 'string', ['length' => 32]); + $this->addField($metadata, 'templateData', 'json', ['nullable' => false, 'options' => ['jsonb' => true]]); + + $this->addIndex($metadata, 'idx_template_key', ['templateKey']); + } + if ($reflection->implementsInterface(SeoInterface::class)) { $this->addField($metadata, 'seoTitle'); $this->addField($metadata, 'seoDescription', 'text'); @@ -70,13 +82,6 @@ public function loadClassMetadata(LoadClassMetadataEventArgs $event): void $this->addField($metadata, 'seoHideInSitemap', 'boolean'); } - if ($reflection->implementsInterface(TemplateInterface::class)) { - $this->addField($metadata, 'templateKey', 'string', ['length' => 32]); - $this->addField($metadata, 'templateData', 'json', ['nullable' => false, 'options' => ['jsonb' => true]]); - - $this->addIndex($metadata, 'idx_template_key', ['templateKey']); - } - if ($reflection->implementsInterface(ExcerptInterface::class)) { $this->addField($metadata, 'excerptTitle'); $this->addField($metadata, 'excerptMore', 'string', ['length' => 64]); @@ -111,11 +116,6 @@ public function loadClassMetadata(LoadClassMetadataEventArgs $event): void $this->addField($metadata, 'mainWebspace', 'string', ['nullable' => true]); } - if ($reflection->implementsInterface(ShadowInterface::class)) { - $this->addField($metadata, 'shadowLocale', 'string', ['length' => 7, 'nullable' => true]); - $this->addField($metadata, 'shadowLocales', 'json', ['nullable' => true, 'options' => ['jsonb' => true]]); - } - if ($reflection->implementsInterface(AuthorInterface::class)) { $this->addField($metadata, 'authored', 'datetime_immutable', ['nullable' => true]); $this->addManyToOne($event, $metadata, 'author', ContactInterface::class, true); diff --git a/Content/Infrastructure/Sulu/Admin/ContentViewBuilderFactory.php b/Content/Infrastructure/Sulu/Admin/ContentViewBuilderFactory.php index 6f79fc54..7819f08c 100644 --- a/Content/Infrastructure/Sulu/Admin/ContentViewBuilderFactory.php +++ b/Content/Infrastructure/Sulu/Admin/ContentViewBuilderFactory.php @@ -158,6 +158,10 @@ public function createViews( $seoAndExcerptToolbarActions = ['save' => $toolbarActions['save']]; $settingsToolbarActions = ['save' => $toolbarActions['save']]; } + if (isset($toolbarActions['edit'])) { + $seoAndExcerptToolbarActions['edit'] = $toolbarActions['edit']; + $settingsToolbarActions['edit'] = $toolbarActions['edit']; + } if (!$this->hasPermission($securityContext, PermissionTypes::EDIT)) { unset($toolbarActions['save'], $seoAndExcerptToolbarActions['save'], $settingsToolbarActions['save']); @@ -236,6 +240,12 @@ public function createViews( $settingsToolbarActions, $dimensionContentClass ); + + foreach ($views as $view) { + if ($view instanceof PreviewFormViewBuilderInterface) { + $view->setPreviewCondition('shadowOn != true'); + } + } } return $views; diff --git a/Tests/Functional/Integration/ExampleControllerAvailableAndShadowLocalesTest.php b/Tests/Functional/Integration/ExampleControllerAvailableAndShadowLocalesTest.php new file mode 100644 index 00000000..ee643603 --- /dev/null +++ b/Tests/Functional/Integration/ExampleControllerAvailableAndShadowLocalesTest.php @@ -0,0 +1,582 @@ +client = $this->createAuthenticatedClient( + [], + ['CONTENT_TYPE' => 'application/json', 'HTTP_ACCEPT' => 'application/json'] + ); + } + + public function testPostCreateEnDraft(): int + { + self::purgeDatabase(); + self::initPhpcr(); + + $this->client->request('POST', '/admin/api/examples?locale=en', [], [], [], \json_encode([ + 'template' => 'example-2', + 'title' => 'Test EN', + 'url' => '/test-en', + ]) ?: null); + + $response = $this->client->getResponse(); + $this->assertHttpStatusCode(201, $response); + $content = \json_decode((string) $response->getContent(), true); + /** @var int $id */ + $id = $content['id'] ?? null; // @phpstan-ignore-line + + $data = $this->getDimensionContent($id); + + $this->assertSame([ + [ + 'stage' => 'draft', + 'locale' => null, + 'availableLocales' => ['en'], + 'shadowLocale' => null, + 'shadowLocales' => null, + 'templateData' => [], + ], + [ + 'stage' => 'draft', + 'locale' => 'en', + 'availableLocales' => null, + 'shadowLocale' => null, + 'shadowLocales' => null, + 'templateData' => [ + 'url' => '/test-en', + 'title' => 'Test EN', + 'images' => null, + ], + ], + ], $data); + + return $id; + } + + /** + * @depends testPostCreateEnDraft + */ + public function testPostCreateDeDraft(int $id): int + { + $this->client->request('PUT', '/admin/api/examples/' . $id . '?locale=de', [], [], [], \json_encode([ + 'template' => 'example-2', + 'title' => 'Test DE', + 'url' => '/test-de', + 'shadowOn' => true, + 'shadowLocale' => 'en', + ]) ?: null); + + $response = $this->client->getResponse(); + $this->assertHttpStatusCode(200, $response); + $content = \json_decode((string) $response->getContent(), true); + /** @var int $id */ + $id = $content['id'] ?? null; // @phpstan-ignore-line + + $data = $this->getDimensionContent($id); + + $this->assertSame([ + [ + 'stage' => 'draft', + 'locale' => null, + 'availableLocales' => ['en', 'de'], + 'shadowLocale' => null, + 'shadowLocales' => ['de' => 'en'], + 'templateData' => [], + ], + [ + 'stage' => 'draft', + 'locale' => 'en', + 'availableLocales' => null, + 'shadowLocale' => null, + 'shadowLocales' => null, + 'templateData' => [ + 'url' => '/test-en', + 'title' => 'Test EN', + 'images' => null, + ], + ], + [ + 'stage' => 'draft', + 'locale' => 'de', + 'availableLocales' => null, + 'shadowLocale' => 'en', + 'shadowLocales' => null, + 'templateData' => [ + 'url' => '/test-de', + 'title' => 'Test DE', + 'images' => null, + ], + ], + ], $data); + + return $id; + } + + /** + * @depends testPostCreateDeDraft + */ + public function testPostPublishEn(int $id): int + { + $this->client->request('PUT', '/admin/api/examples/' . $id . '?locale=en&action=publish', [], [], [], \json_encode([ + 'template' => 'example-2', + 'title' => 'Test EN', + 'url' => '/test-en', + ]) ?: null); + + $response = $this->client->getResponse(); + $this->assertHttpStatusCode(200, $response); + $content = \json_decode((string) $response->getContent(), true); + /** @var int $id */ + $id = $content['id'] ?? null; // @phpstan-ignore-line + + $data = $this->getDimensionContent($id); + + $this->assertSame([ + [ + 'stage' => 'draft', + 'locale' => null, + 'availableLocales' => ['en', 'de'], + 'shadowLocale' => null, + 'shadowLocales' => ['de' => 'en'], + 'templateData' => [], + ], + [ + 'stage' => 'draft', + 'locale' => 'en', + 'availableLocales' => null, + 'shadowLocale' => null, + 'shadowLocales' => null, + 'templateData' => [ + 'url' => '/test-en', + 'title' => 'Test EN', + 'images' => null, + ], + ], + [ + 'stage' => 'draft', + 'locale' => 'de', + 'availableLocales' => null, + 'shadowLocale' => 'en', + 'shadowLocales' => null, + 'templateData' => [ + 'url' => '/test-de', + 'title' => 'Test DE', + 'images' => null, + ], + ], + [ + 'stage' => 'live', + 'locale' => null, + 'availableLocales' => ['en'], + 'shadowLocale' => null, + 'shadowLocales' => null, + 'templateData' => [], + ], + [ + 'stage' => 'live', + 'locale' => 'en', + 'availableLocales' => null, + 'shadowLocale' => null, + 'shadowLocales' => null, + 'templateData' => [ + 'url' => '/test-en', + 'title' => 'Test EN', + 'images' => null, + ], + ], + ], $data); + + return $id; + } + + /** + * @depends testPostPublishEn + */ + public function testPostPublishDe(int $id): int + { + $this->client->request('PUT', '/admin/api/examples/' . $id . '?locale=de&action=publish', [], [], [], \json_encode([ + 'template' => 'example-2', + 'title' => 'Test DE', + 'url' => '/test-de', + 'shadowOn' => true, + 'shadowLocale' => 'en', + ]) ?: null); + + $response = $this->client->getResponse(); + $this->assertHttpStatusCode(200, $response); + $content = \json_decode((string) $response->getContent(), true); + /** @var int $id */ + $id = $content['id'] ?? null; // @phpstan-ignore-line + + $data = $this->getDimensionContent($id); + + $this->assertSame([ + [ + 'stage' => 'draft', + 'locale' => null, + 'availableLocales' => ['en', 'de'], + 'shadowLocale' => null, + 'shadowLocales' => ['de' => 'en'], + 'templateData' => [], + ], + [ + 'stage' => 'draft', + 'locale' => 'en', + 'availableLocales' => null, + 'shadowLocale' => null, + 'shadowLocales' => null, + 'templateData' => [ + 'url' => '/test-en', + 'title' => 'Test EN', + 'images' => null, + ], + ], + [ + 'stage' => 'draft', + 'locale' => 'de', + 'availableLocales' => null, + 'shadowLocale' => 'en', + 'shadowLocales' => null, + 'templateData' => [ + 'url' => '/test-de', + 'title' => 'Test DE', + 'images' => null, + ], + ], + [ + 'stage' => 'live', + 'locale' => null, + 'availableLocales' => ['en', 'de'], + 'shadowLocale' => null, + 'shadowLocales' => ['de' => 'en'], + 'templateData' => [], + ], + [ + 'stage' => 'live', + 'locale' => 'en', + 'availableLocales' => null, + 'shadowLocale' => null, + 'shadowLocales' => null, + 'templateData' => [ + 'url' => '/test-en', + 'title' => 'Test EN', + 'images' => null, + ], + ], + [ + 'stage' => 'live', + 'locale' => 'de', + 'availableLocales' => null, + 'shadowLocale' => 'en', + 'shadowLocales' => null, + 'templateData' => [ + 'url' => '/test-de', + 'title' => 'Test EN', + 'images' => null, + ], + ], + ], $data); + + return $id; + } + + /** + * @depends testPostPublishDe + */ + public function testPostRepublishEn(int $id): int + { + $this->client->request('PUT', '/admin/api/examples/' . $id . '?locale=en&action=publish', [], [], [], \json_encode([ + 'template' => 'example-2', + 'title' => 'Test EN New', + 'url' => '/test-en', + ]) ?: null); + + $response = $this->client->getResponse(); + $this->assertHttpStatusCode(200, $response); + $content = \json_decode((string) $response->getContent(), true); + /** @var int $id */ + $id = $content['id'] ?? null; // @phpstan-ignore-line + + $data = $this->getDimensionContent($id); + + $this->assertSame([ + [ + 'stage' => 'draft', + 'locale' => null, + 'availableLocales' => ['en', 'de'], + 'shadowLocale' => null, + 'shadowLocales' => ['de' => 'en'], + 'templateData' => [], + ], + [ + 'stage' => 'draft', + 'locale' => 'en', + 'availableLocales' => null, + 'shadowLocale' => null, + 'shadowLocales' => null, + 'templateData' => [ + 'url' => '/test-en', + 'title' => 'Test EN New', + 'images' => null, + ], + ], + [ + 'stage' => 'draft', + 'locale' => 'de', + 'availableLocales' => null, + 'shadowLocale' => 'en', + 'shadowLocales' => null, + 'templateData' => [ + 'url' => '/test-de', + 'title' => 'Test DE', + 'images' => null, + ], + ], + [ + 'stage' => 'live', + 'locale' => null, + 'availableLocales' => ['en', 'de'], + 'shadowLocale' => null, + 'shadowLocales' => ['de' => 'en'], + 'templateData' => [], + ], + [ + 'stage' => 'live', + 'locale' => 'en', + 'availableLocales' => null, + 'shadowLocale' => null, + 'shadowLocales' => null, + 'templateData' => [ + 'url' => '/test-en', + 'title' => 'Test EN New', + 'images' => null, + ], + ], + [ + 'stage' => 'live', + 'locale' => 'de', + 'availableLocales' => null, + 'shadowLocale' => 'en', + 'shadowLocales' => null, + 'templateData' => [ + 'url' => '/test-de', + 'title' => 'Test EN New', + 'images' => null, + ], + ], + ], $data); + + return $id; + } + + /** + * @depends testPostRepublishEn + */ + public function testPostUnpublishDe(int $id): int + { + $this->client->request('POST', '/admin/api/examples/' . $id . '?locale=de&action=unpublish', [], [], [], \json_encode([ + 'template' => 'example-2', + 'title' => 'Test DE', + 'url' => '/test-de', + 'shadowOn' => true, + 'shadowLocale' => 'en', + ]) ?: null); + + $response = $this->client->getResponse(); + $this->assertHttpStatusCode(200, $response); + $content = \json_decode((string) $response->getContent(), true); + /** @var int $id */ + $id = $content['id'] ?? null; // @phpstan-ignore-line + + $data = $this->getDimensionContent($id); + + $this->assertSame([ + [ + 'stage' => 'draft', + 'locale' => null, + 'availableLocales' => ['en', 'de'], + 'shadowLocale' => null, + 'shadowLocales' => ['de' => 'en'], + 'templateData' => [], + ], + [ + 'stage' => 'draft', + 'locale' => 'en', + 'availableLocales' => null, + 'shadowLocale' => null, + 'shadowLocales' => null, + 'templateData' => [ + 'url' => '/test-en', + 'title' => 'Test EN New', + 'images' => null, + ], + ], + [ + 'stage' => 'draft', + 'locale' => 'de', + 'availableLocales' => null, + 'shadowLocale' => 'en', + 'shadowLocales' => null, + 'templateData' => [ + 'url' => '/test-de', + 'title' => 'Test DE', + 'images' => null, + ], + ], + [ + 'stage' => 'live', + 'locale' => null, + 'availableLocales' => ['en'], + 'shadowLocale' => null, + 'shadowLocales' => null, + 'templateData' => [], + ], + [ + 'stage' => 'live', + 'locale' => 'en', + 'availableLocales' => null, + 'shadowLocale' => null, + 'shadowLocales' => null, + 'templateData' => [ + 'url' => '/test-en', + 'title' => 'Test EN New', + 'images' => null, + ], + ], + ], $data); + + return $id; + } + + /** + * @depends testPostRepublishEn + */ + public function testPostRepublishEnAgain(int $id): int + { + $this->client->request('PUT', '/admin/api/examples/' . $id . '?locale=en&action=publish', [], [], [], \json_encode([ + 'template' => 'example-2', + 'title' => 'Test EN New 2', + 'url' => '/test-en', + ]) ?: null); + + $response = $this->client->getResponse(); + $this->assertHttpStatusCode(200, $response); + $content = \json_decode((string) $response->getContent(), true); + /** @var int $id */ + $id = $content['id'] ?? null; // @phpstan-ignore-line + + $data = $this->getDimensionContent($id); + + $this->assertSame([ + [ + 'stage' => 'draft', + 'locale' => null, + 'availableLocales' => ['en', 'de'], + 'shadowLocale' => null, + 'shadowLocales' => ['de' => 'en'], + 'templateData' => [], + ], + [ + 'stage' => 'draft', + 'locale' => 'en', + 'availableLocales' => null, + 'shadowLocale' => null, + 'shadowLocales' => null, + 'templateData' => [ + 'url' => '/test-en', + 'title' => 'Test EN New 2', + 'images' => null, + ], + ], + [ + 'stage' => 'draft', + 'locale' => 'de', + 'availableLocales' => null, + 'shadowLocale' => 'en', + 'shadowLocales' => null, + 'templateData' => [ + 'url' => '/test-de', + 'title' => 'Test DE', + 'images' => null, + ], + ], + [ + 'stage' => 'live', + 'locale' => null, + 'availableLocales' => ['en'], + 'shadowLocale' => null, + 'shadowLocales' => null, + 'templateData' => [], + ], + [ + 'stage' => 'live', + 'locale' => 'en', + 'availableLocales' => null, + 'shadowLocale' => null, + 'shadowLocales' => null, + 'templateData' => [ + 'url' => '/test-en', + 'title' => 'Test EN New 2', + 'images' => null, + ], + ], + ], $data); + + return $id; + } + + /** + * @return mixed[] + */ + private function getDimensionContent(int $id): array + { + /** @var EntityManagerInterface $entityManager */ + $entityManager = $this->getContainer()->get(EntityManagerInterface::class); + $queryBuilder = $entityManager->createQueryBuilder() + ->from(ExampleDimensionContent::class, 'dimensionContent') + ->select('dimensionContent.stage') + ->addSelect('dimensionContent.locale') + ->addSelect('dimensionContent.availableLocales') + ->addSelect('dimensionContent.shadowLocale') + ->addSelect('dimensionContent.shadowLocales') + ->addSelect('dimensionContent.templateData') + ->where('IDENTITY(dimensionContent.example) = :id') + ->orderBy('dimensionContent.id') + ->setParameter('id', $id); + + return $queryBuilder->getQuery()->getArrayResult(); + } +} diff --git a/Tests/Unit/Content/Application/ContentCopier/ContentCopierTest.php b/Tests/Unit/Content/Application/ContentCopier/ContentCopierTest.php index 51732670..fd1b3f37 100644 --- a/Tests/Unit/Content/Application/ContentCopier/ContentCopierTest.php +++ b/Tests/Unit/Content/Application/ContentCopier/ContentCopierTest.php @@ -14,6 +14,7 @@ namespace Sulu\Bundle\ContentBundle\Tests\Unit\Content\Application\ContentCopier; use PHPUnit\Framework\TestCase; +use Prophecy\PhpUnit\ProphecyTrait; use Sulu\Bundle\ContentBundle\Content\Application\ContentCopier\ContentCopier; use Sulu\Bundle\ContentBundle\Content\Application\ContentCopier\ContentCopierInterface; use Sulu\Bundle\ContentBundle\Content\Application\ContentMerger\ContentMergerInterface; @@ -26,7 +27,7 @@ class ContentCopierTest extends TestCase { - use \Prophecy\PhpUnit\ProphecyTrait; + use ProphecyTrait; protected function createContentCopierInstance( ContentResolverInterface $contentResolver, @@ -167,4 +168,57 @@ public function testCopyFromDimensionContent(): void ) ); } + + public function testCopyFromDimensionContentWithIgnoredAttributesAndData(): void + { + $resolvedSourceContent = $this->prophesize(DimensionContentInterface::class); + $resolvedTargetContent = $this->prophesize(DimensionContentInterface::class); + + $targetContentRichEntity = $this->prophesize(ContentRichEntityInterface::class); + $targetDimensionAttributes = ['locale' => 'de']; + + $contentResolver = $this->prophesize(ContentResolverInterface::class); + $contentMerger = $this->prophesize(ContentMergerInterface::class); + $contentPersister = $this->prophesize(ContentPersisterInterface::class); + $contentNormalizer = $this->prophesize(ContentNormalizerInterface::class); + + $contentNormalizer->normalize($resolvedSourceContent->reveal()) + ->willReturn([ + 'resolved' => 'content', + 'overwritten' => 'old', + 'ignored' => 'value', + ]) + ->shouldBeCalled(); + + $contentPersister->persist($targetContentRichEntity, [ + 'resolved' => 'content', + 'overwritten' => 'new', + ], $targetDimensionAttributes) + ->willReturn($resolvedTargetContent->reveal()) + ->shouldBeCalled(); + + $contentCopier = $this->createContentCopierInstance( + $contentResolver->reveal(), + $contentMerger->reveal(), + $contentPersister->reveal(), + $contentNormalizer->reveal() + ); + + $this->assertSame( + $resolvedTargetContent->reveal(), + $contentCopier->copyFromDimensionContent( + $resolvedSourceContent->reveal(), + $targetContentRichEntity->reveal(), + $targetDimensionAttributes, + [ + 'data' => [ + 'overwritten' => 'new', + ], + 'ignoredAttributes' => [ + 'ignored', + ], + ], + ) + ); + } } diff --git a/Tests/Unit/Content/Application/ContentDataMapper/DataMapper/RoutableDataMapperTest.php b/Tests/Unit/Content/Application/ContentDataMapper/DataMapper/RoutableDataMapperTest.php index c695786e..b9b07bca 100644 --- a/Tests/Unit/Content/Application/ContentDataMapper/DataMapper/RoutableDataMapperTest.php +++ b/Tests/Unit/Content/Application/ContentDataMapper/DataMapper/RoutableDataMapperTest.php @@ -365,6 +365,35 @@ public function testMapRouteProperty(): void ], $localizedDimensionContent->getTemplateData()); } + public function testMapRoutePropertyFalseName(): void + { + $this->expectException(\RuntimeException::class); + $this->expectExceptionMessage('Expected a property with the name "url" but "route" given.'); + + $data = [ + 'route' => '/test', + ]; + + $route = new Route(); + $route->setPath('/test-1'); + + $example = new Example(); + static::setPrivateProperty($example, 'id', 1); + $unlocalizedDimensionContent = new ExampleDimensionContent($example); + $unlocalizedDimensionContent->setStage('live'); + $localizedDimensionContent = new ExampleDimensionContent($example); + $localizedDimensionContent->setTemplateKey('default'); + $localizedDimensionContent->setStage('live'); + $localizedDimensionContent->setLocale('en'); + + $this->structureMetadataFactory->getStructureMetadata('example', 'default') + ->shouldBeCalled() + ->willReturn($this->createRouteStructureMetadata('route')); + + $mapper = $this->createRouteDataMapperInstance(); + $mapper->map($unlocalizedDimensionContent, $localizedDimensionContent, $data); + } + public function testMapRouteDraftDimension(): void { $data = [ @@ -570,11 +599,11 @@ public function testMapOnlySlash(): void ], $localizedDimensionContent->getTemplateData()); } - private function createRouteStructureMetadata(): StructureMetadata + private function createRouteStructureMetadata(string $propertyName = 'url'): StructureMetadata { $property = $this->prophesize(PropertyMetadata::class); $property->getType()->willReturn('route'); - $property->getName()->willReturn('url'); + $property->getName()->willReturn($propertyName); $structureMetadata = $this->prophesize(StructureMetadata::class); $structureMetadata->getProperties()->willReturn([ diff --git a/Tests/Unit/Content/Application/ContentDataMapper/DataMapper/ShadowDataMapperTest.php b/Tests/Unit/Content/Application/ContentDataMapper/DataMapper/ShadowDataMapperTest.php index a82b46f6..263be5fd 100644 --- a/Tests/Unit/Content/Application/ContentDataMapper/DataMapper/ShadowDataMapperTest.php +++ b/Tests/Unit/Content/Application/ContentDataMapper/DataMapper/ShadowDataMapperTest.php @@ -38,6 +38,7 @@ public function testMapNoAuthorInterface(): void $unlocalizedDimensionContent = $this->prophesize(DimensionContentInterface::class); $localizedDimensionContent = $this->prophesize(DimensionContentInterface::class); + $localizedDimensionContent->getLocale()->willReturn('en'); $shadowMapper = $this->createShadowDataMapperInstance(); $shadowMapper->map($unlocalizedDimensionContent->reveal(), $localizedDimensionContent->reveal(), $data); @@ -52,6 +53,7 @@ public function testMapShadowNoData(): void $example = new Example(); $unlocalizedDimensionContent = new ExampleDimensionContent($example); $localizedDimensionContent = new ExampleDimensionContent($example); + $localizedDimensionContent->setLocale('de'); $shadowMapper = $this->createShadowDataMapperInstance(); $shadowMapper->map($unlocalizedDimensionContent, $localizedDimensionContent, $data); @@ -66,6 +68,7 @@ public function testMapShadowNoDataExistData(): void $example = new Example(); $unlocalizedDimensionContent = new ExampleDimensionContent($example); $localizedDimensionContent = new ExampleDimensionContent($example); + $localizedDimensionContent->setLocale('de'); $localizedDimensionContent->setShadowLocale('en'); $shadowMapper = $this->createShadowDataMapperInstance(); @@ -84,11 +87,34 @@ public function testMapData(): void $example = new Example(); $unlocalizedDimensionContent = new ExampleDimensionContent($example); $localizedDimensionContent = new ExampleDimensionContent($example); + $localizedDimensionContent->setLocale('de'); $shadowMapper = $this->createShadowDataMapperInstance(); $shadowMapper->map($unlocalizedDimensionContent, $localizedDimensionContent, $data); $this->assertSame('en', $localizedDimensionContent->getShadowLocale()); + $this->assertSame(['de' => 'en'], $unlocalizedDimensionContent->getShadowLocales()); + } + + public function testMapDataRemoveShadow(): void + { + $data = [ + 'shadowOn' => false, + 'shadowLocale' => null, + ]; + + $example = new Example(); + $unlocalizedDimensionContent = new ExampleDimensionContent($example); + $unlocalizedDimensionContent->addShadowLocale('de', 'en'); + $localizedDimensionContent = new ExampleDimensionContent($example); + $localizedDimensionContent->setLocale('de'); + $localizedDimensionContent->setShadowLocale('en'); + + $shadowMapper = $this->createShadowDataMapperInstance(); + $shadowMapper->map($unlocalizedDimensionContent, $localizedDimensionContent, $data); + + $this->assertNull($localizedDimensionContent->getShadowLocale()); + $this->assertNull($unlocalizedDimensionContent->getShadowLocales()); } public function testMapDataNull(): void @@ -100,12 +126,15 @@ public function testMapDataNull(): void $example = new Example(); $unlocalizedDimensionContent = new ExampleDimensionContent($example); + $unlocalizedDimensionContent->addShadowLocale('de', 'en'); $localizedDimensionContent = new ExampleDimensionContent($example); - $localizedDimensionContent->setShadowLocale(null); + $localizedDimensionContent->setLocale('de'); + $localizedDimensionContent->setShadowLocale('en'); $shadowMapper = $this->createShadowDataMapperInstance(); $shadowMapper->map($unlocalizedDimensionContent, $localizedDimensionContent, $data); $this->assertNull($localizedDimensionContent->getShadowLocale()); + $this->assertNull($unlocalizedDimensionContent->getShadowLocales()); } } diff --git a/Tests/Unit/Content/Application/ContentWorkflow/Subscriber/PublishTransitionSubscriberTest.php b/Tests/Unit/Content/Application/ContentWorkflow/Subscriber/PublishTransitionSubscriberTest.php index 0bbb2512..f734908e 100644 --- a/Tests/Unit/Content/Application/ContentWorkflow/Subscriber/PublishTransitionSubscriberTest.php +++ b/Tests/Unit/Content/Application/ContentWorkflow/Subscriber/PublishTransitionSubscriberTest.php @@ -15,19 +15,22 @@ use PHPUnit\Framework\TestCase; use Prophecy\Argument; +use Prophecy\PhpUnit\ProphecyTrait; use Sulu\Bundle\ContentBundle\Content\Application\ContentCopier\ContentCopierInterface; use Sulu\Bundle\ContentBundle\Content\Application\ContentWorkflow\ContentWorkflowInterface; use Sulu\Bundle\ContentBundle\Content\Application\ContentWorkflow\Subscriber\PublishTransitionSubscriber; use Sulu\Bundle\ContentBundle\Content\Domain\Model\ContentRichEntityInterface; use Sulu\Bundle\ContentBundle\Content\Domain\Model\DimensionContentCollectionInterface; use Sulu\Bundle\ContentBundle\Content\Domain\Model\DimensionContentInterface; +use Sulu\Bundle\ContentBundle\Content\Domain\Model\ShadowInterface; +use Sulu\Bundle\ContentBundle\Content\Domain\Model\TemplateInterface; use Sulu\Bundle\ContentBundle\Content\Domain\Model\WorkflowInterface; use Symfony\Component\Workflow\Event\TransitionEvent; use Symfony\Component\Workflow\Marking; class PublishTransitionSubscriberTest extends TestCase { - use \Prophecy\PhpUnit\ProphecyTrait; + use ProphecyTrait; public function createContentPublisherSubscriberInstance( ContentCopierInterface $contentCopier @@ -150,6 +153,7 @@ public function testOnPublish(): void $contentRichEntity = $this->prophesize(ContentRichEntityInterface::class); $dimensionAttributes = ['locale' => 'en', 'stage' => 'draft']; + $dimensionContent->getLocale()->willReturn('en'); $dimensionContent->getWorkflowPublished()->willReturn(null); $dimensionContent->setWorkflowPublished(Argument::cetera())->shouldBeCalled(); @@ -189,6 +193,7 @@ public function testOnPublishExistingPublished(): void $contentRichEntity = $this->prophesize(ContentRichEntityInterface::class); $dimensionAttributes = ['locale' => 'en', 'stage' => 'draft']; + $dimensionContent->getLocale()->willReturn('en'); $dimensionContent->getWorkflowPublished()->willReturn(new \DateTimeImmutable()); $dimensionContent->setWorkflowPublished(Argument::any())->shouldNotBeCalled(); @@ -202,15 +207,131 @@ public function testOnPublishExistingPublished(): void ContentWorkflowInterface::CONTENT_RICH_ENTITY_CONTEXT_KEY => $contentRichEntity->reveal(), ]); + $contentCopier = $this->prophesize(ContentCopierInterface::class); + $targetDimensionAttributes = $dimensionAttributes; + $targetDimensionAttributes['stage'] = 'live'; + + $resolvedCopiedContent = $this->prophesize(DimensionContentInterface::class); + $contentCopier->copyFromDimensionContentCollection( + $dimensionContentCollection->reveal(), + $contentRichEntity->reveal(), + $targetDimensionAttributes + ) + ->willReturn($resolvedCopiedContent->reveal()) + ->shouldBeCalled(); + + $contentPublishSubscriber = $this->createContentPublisherSubscriberInstance($contentCopier->reveal()); + + $contentPublishSubscriber->onPublish($event); + } + + public function testOnPublishShadow(): void + { + $dimensionContent = $this->prophesize(DimensionContentInterface::class); + $dimensionContent->willImplement(WorkflowInterface::class); + $dimensionContent->willImplement(ShadowInterface::class); + $dimensionContent->willImplement(TemplateInterface::class); + $dimensionContentCollection = $this->prophesize(DimensionContentCollectionInterface::class); + $contentRichEntity = $this->prophesize(ContentRichEntityInterface::class); + $dimensionAttributes = ['locale' => 'en', 'stage' => 'draft']; + + $dimensionContent->getLocale()->willReturn('en'); + $dimensionContent->getShadowLocale()->willReturn('de'); + $dimensionContent->getTemplateData()->willReturn(['url' => '/test-de']); + $dimensionContent->getWorkflowPublished()->willReturn(null); + $dimensionContent->setWorkflowPublished(Argument::cetera())->shouldBeCalled(); + + $event = new TransitionEvent( + $dimensionContent->reveal(), + new Marking() + ); + $event->setContext([ + ContentWorkflowInterface::DIMENSION_CONTENT_COLLECTION_CONTEXT_KEY => $dimensionContentCollection->reveal(), + ContentWorkflowInterface::DIMENSION_ATTRIBUTES_CONTEXT_KEY => $dimensionAttributes, + ContentWorkflowInterface::CONTENT_RICH_ENTITY_CONTEXT_KEY => $contentRichEntity->reveal(), + ]); + $contentCopier = $this->prophesize(ContentCopierInterface::class); $sourceDimensionAttributes = $dimensionAttributes; + $sourceDimensionAttributes['locale'] = 'de'; $sourceDimensionAttributes['stage'] = 'live'; + $targetDimensionAttributes = $dimensionAttributes; + $targetDimensionAttributes['stage'] = 'live'; $resolvedCopiedContent = $this->prophesize(DimensionContentInterface::class); + $contentCopier->copy( + $contentRichEntity->reveal(), + $sourceDimensionAttributes, + $contentRichEntity->reveal(), + $targetDimensionAttributes, + [ + 'data' => [ + 'shadowOn' => true, + 'shadowLocale' => 'de', + 'url' => '/test-de', + ], + ] + ) + ->willReturn($resolvedCopiedContent->reveal()) + ->shouldBeCalled(); + + $contentPublishSubscriber = $this->createContentPublisherSubscriberInstance($contentCopier->reveal()); + + $contentPublishSubscriber->onPublish($event); + } + + public function testOnPublishHasShadow(): void + { + $dimensionContent = $this->prophesize(DimensionContentInterface::class); + $dimensionContent->willImplement(WorkflowInterface::class); + $dimensionContent->willImplement(ShadowInterface::class); + $dimensionContent->willImplement(TemplateInterface::class); + $dimensionContentCollection = $this->prophesize(DimensionContentCollectionInterface::class); + $contentRichEntity = $this->prophesize(ContentRichEntityInterface::class); + $dimensionAttributes = ['locale' => 'en', 'stage' => 'draft']; + + $dimensionContent->getLocale()->willReturn('en'); + $dimensionContent->getShadowLocale()->willReturn(null); + $dimensionContent->getWorkflowPublished()->willReturn(null); + $dimensionContent->setWorkflowPublished(Argument::cetera())->shouldBeCalled(); + + $event = new TransitionEvent( + $dimensionContent->reveal(), + new Marking() + ); + $event->setContext([ + ContentWorkflowInterface::DIMENSION_CONTENT_COLLECTION_CONTEXT_KEY => $dimensionContentCollection->reveal(), + ContentWorkflowInterface::DIMENSION_ATTRIBUTES_CONTEXT_KEY => $dimensionAttributes, + ContentWorkflowInterface::CONTENT_RICH_ENTITY_CONTEXT_KEY => $contentRichEntity->reveal(), + ]); + + $contentCopier = $this->prophesize(ContentCopierInterface::class); + $targetDimensionAttributes = $dimensionAttributes; + $targetDimensionAttributes['stage'] = 'live'; + + $resolvedCopiedContent = $this->prophesize(DimensionContentInterface::class); + $resolvedCopiedContent->willImplement(ShadowInterface::class); + $resolvedCopiedContent->getShadowLocalesForLocale('en')->willReturn(['de'])->shouldBeCalled(); $contentCopier->copyFromDimensionContentCollection( $dimensionContentCollection->reveal(), $contentRichEntity->reveal(), - $sourceDimensionAttributes + $targetDimensionAttributes + ) + ->willReturn($resolvedCopiedContent->reveal()) + ->shouldBeCalled(); + + $targetDimensionAttributes['locale'] = 'de'; + $contentCopier->copyFromDimensionContentCollection( + $dimensionContentCollection->reveal(), + $contentRichEntity->reveal(), + $targetDimensionAttributes, + [ + 'ignoredAttributes' => [ + 'shadowOn', + 'shadowLocale', + 'url', + ], + ] ) ->willReturn($resolvedCopiedContent->reveal()) ->shouldBeCalled(); diff --git a/Tests/Unit/Content/Application/ContentWorkflow/Subscriber/UnpublishTransitionSubscriberTest.php b/Tests/Unit/Content/Application/ContentWorkflow/Subscriber/UnpublishTransitionSubscriberTest.php index f84e111c..e7cdc0b4 100644 --- a/Tests/Unit/Content/Application/ContentWorkflow/Subscriber/UnpublishTransitionSubscriberTest.php +++ b/Tests/Unit/Content/Application/ContentWorkflow/Subscriber/UnpublishTransitionSubscriberTest.php @@ -177,20 +177,22 @@ public function testOnUnpublishNoLocalizedDimensionContent(): void public function testOnUnpublish(): void { - $dimensionContent = $this->prophesize(DimensionContentInterface::class); - $dimensionContent->willImplement(WorkflowInterface::class); - $contentRichEntity = $this->prophesize(ContentRichEntityInterface::class); - $dimensionAttributes = ['locale' => 'en', 'stage' => 'draft']; + $example = new Example(); + $dimensionContent = new ExampleDimensionContent($example); + $dimensionContent->setStage('stage'); + $dimensionContent->setLocale('en'); + $dimensionContent->setWorkflowPublished(new \DateTimeImmutable()); + $dimensionContent->setShadowLocale('de'); - $dimensionContent->setWorkflowPublished(null)->shouldBeCalled(); + $dimensionAttributes = ['locale' => 'en', 'stage' => 'draft']; $event = new TransitionEvent( - $dimensionContent->reveal(), + $dimensionContent, new Marking() ); $event->setContext([ ContentWorkflowInterface::DIMENSION_ATTRIBUTES_CONTEXT_KEY => $dimensionAttributes, - ContentWorkflowInterface::CONTENT_RICH_ENTITY_CONTEXT_KEY => $contentRichEntity->reveal(), + ContentWorkflowInterface::CONTENT_RICH_ENTITY_CONTEXT_KEY => $example, ]); $dimensionContentRepository = $this->prophesize(DimensionContentRepositoryInterface::class); @@ -201,7 +203,6 @@ public function testOnUnpublish(): void $entityManager->reveal() ); - $example = new Example(); $localizedLiveDimensionContent = new ExampleDimensionContent($example); $localizedLiveDimensionContent->setStage('live'); $localizedLiveDimensionContent->setLocale('en'); @@ -214,13 +215,14 @@ public function testOnUnpublish(): void $unlocalizedLiveDimensionContent->setStage('live'); $unlocalizedLiveDimensionContent->addAvailableLocale('en'); $unlocalizedLiveDimensionContent->addAvailableLocale('de'); + $unlocalizedLiveDimensionContent->addShadowLocale('en', 'de'); $dimensionContentCollection->getDimensionContent(['locale' => null, 'stage' => 'live']) ->willReturn($unlocalizedLiveDimensionContent) ->shouldBeCalled(); $liveDimensionAttributes = \array_merge($dimensionAttributes, ['stage' => DimensionContentInterface::STAGE_LIVE]); - $dimensionContentRepository->load($contentRichEntity->reveal(), $liveDimensionAttributes) + $dimensionContentRepository->load($example, $liveDimensionAttributes) ->willReturn($dimensionContentCollection) ->shouldBeCalled(); @@ -228,5 +230,7 @@ public function testOnUnpublish(): void $contentUnpublishSubscriber->onUnpublish($event); $this->assertSame(['de'], $unlocalizedLiveDimensionContent->getAvailableLocales()); + $this->assertNull($dimensionContent->getWorkflowPublished()); + $this->assertNull($unlocalizedLiveDimensionContent->getShadowLocales()); } } diff --git a/Tests/Unit/Content/Domain/Model/ShadowTraitTest.php b/Tests/Unit/Content/Domain/Model/ShadowTraitTest.php index b0417a29..18bc290a 100644 --- a/Tests/Unit/Content/Domain/Model/ShadowTraitTest.php +++ b/Tests/Unit/Content/Domain/Model/ShadowTraitTest.php @@ -40,11 +40,20 @@ public function testGetSetShadowLocale(): void public function testAddRemoveShadowLocales(): void { $model = $this->getShadowInstance(); - $Shadowed = new \DateTimeImmutable('2020-05-08T00:00:00+00:00'); + $this->assertNull($model->getShadowLocales()); + $model->removeShadowLocale('de'); $this->assertNull($model->getShadowLocales()); $model->addShadowLocale('de', 'en'); $this->assertSame(['de' => 'en'], $model->getShadowLocales()); $model->removeShadowLocale('de'); - $this->assertSame([], $model->getShadowLocales()); + $this->assertNull($model->getShadowLocales()); + } + + public function testGetShadowLocalesForLocale(): void + { + $model = $this->getShadowInstance(); + $this->assertSame([], $model->getShadowLocalesForLocale('en')); + $model->addShadowLocale('de', 'en'); + $this->assertSame(['de'], $model->getShadowLocalesForLocale('en')); } } diff --git a/Tests/Unit/Content/Infrastructure/Sulu/Admin/ContentViewBuilderFactoryTest.php b/Tests/Unit/Content/Infrastructure/Sulu/Admin/ContentViewBuilderFactoryTest.php index 0141cc45..ff57832c 100644 --- a/Tests/Unit/Content/Infrastructure/Sulu/Admin/ContentViewBuilderFactoryTest.php +++ b/Tests/Unit/Content/Infrastructure/Sulu/Admin/ContentViewBuilderFactoryTest.php @@ -215,9 +215,9 @@ public function getSecurityContextData(): array [ ['sulu_admin.save_with_publishing', 'sulu_admin.type', 'sulu_admin.delete', 'sulu_admin.dropdown'], ['sulu_admin.save_with_publishing', 'sulu_admin.type', 'sulu_admin.delete', 'sulu_admin.dropdown'], - ['sulu_admin.save_with_publishing'], - ['sulu_admin.save_with_publishing'], - ['sulu_admin.save_with_publishing'], + ['sulu_admin.save_with_publishing', 'sulu_admin.dropdown'], + ['sulu_admin.save_with_publishing', 'sulu_admin.dropdown'], + ['sulu_admin.save_with_publishing', 'sulu_admin.dropdown'], ], ], [ @@ -229,9 +229,9 @@ public function getSecurityContextData(): array ], [ ['sulu_admin.save_with_publishing', 'sulu_admin.type', 'sulu_admin.delete', 'sulu_admin.dropdown'], - ['sulu_admin.save_with_publishing'], - ['sulu_admin.save_with_publishing'], - ['sulu_admin.save_with_publishing'], + ['sulu_admin.save_with_publishing', 'sulu_admin.dropdown'], + ['sulu_admin.save_with_publishing', 'sulu_admin.dropdown'], + ['sulu_admin.save_with_publishing', 'sulu_admin.dropdown'], ], ], [ @@ -243,9 +243,9 @@ public function getSecurityContextData(): array ], [ ['sulu_admin.save_with_publishing', 'sulu_admin.type'], - ['sulu_admin.save_with_publishing'], - ['sulu_admin.save_with_publishing'], - ['sulu_admin.save_with_publishing'], + ['sulu_admin.save_with_publishing', 'sulu_admin.dropdown'], + ['sulu_admin.save_with_publishing', 'sulu_admin.dropdown'], + ['sulu_admin.save_with_publishing', 'sulu_admin.dropdown'], ], ], [ @@ -269,9 +269,9 @@ public function getSecurityContextData(): array [ ['sulu_admin.save_with_publishing', 'sulu_admin.type', 'sulu_admin.delete'], ['sulu_admin.save_with_publishing', 'sulu_admin.type', 'sulu_admin.delete'], - ['sulu_admin.save_with_publishing'], - ['sulu_admin.save_with_publishing'], - ['sulu_admin.save_with_publishing'], + ['sulu_admin.save_with_publishing', 'sulu_admin.dropdown'], + ['sulu_admin.save_with_publishing', 'sulu_admin.dropdown'], + ['sulu_admin.save_with_publishing', 'sulu_admin.dropdown'], ], ], [ @@ -284,9 +284,9 @@ public function getSecurityContextData(): array [ ['sulu_admin.save_with_publishing', 'sulu_admin.type', 'sulu_admin.dropdown'], ['sulu_admin.save_with_publishing', 'sulu_admin.type', 'sulu_admin.dropdown'], - ['sulu_admin.save_with_publishing'], - ['sulu_admin.save_with_publishing'], - ['sulu_admin.save_with_publishing'], + ['sulu_admin.save_with_publishing', 'sulu_admin.dropdown'], + ['sulu_admin.save_with_publishing', 'sulu_admin.dropdown'], + ['sulu_admin.save_with_publishing', 'sulu_admin.dropdown'], ], ], ]; @@ -424,9 +424,9 @@ public static function getResourceKey(): string [ ['sulu_admin.save_with_publishing', 'sulu_admin.type', 'sulu_admin.delete', 'sulu_admin.dropdown'], ['sulu_admin.save_with_publishing', 'sulu_admin.type', 'sulu_admin.delete', 'sulu_admin.dropdown'], - ['sulu_admin.save_with_publishing'], - ['sulu_admin.save_with_publishing'], - ['sulu_admin.save_with_publishing'], + ['sulu_admin.save_with_publishing', 'sulu_admin.dropdown'], + ['sulu_admin.save_with_publishing', 'sulu_admin.dropdown'], + ['sulu_admin.save_with_publishing', 'sulu_admin.dropdown'], ], ], ]; diff --git a/UPGRADE.md b/UPGRADE.md index 1bf0b47c..2e88b491 100644 --- a/UPGRADE.md +++ b/UPGRADE.md @@ -2,6 +2,11 @@ ## 0.7.0 +### Route property name forced to `url` + +The `route` property requires now to be named `url` to make things easier accessible +for different parts of the system. + ### Add shadowLocale and shadowLocales fields To support shadow feature of sulu shadowLocale and shadowLocales fields need to be added to diff --git a/composer.json b/composer.json index 04958c8f..afed83e3 100644 --- a/composer.json +++ b/composer.json @@ -65,6 +65,7 @@ "thecodingmachine/phpstan-strict-rules": "^1.0" }, "conflict": { + "coduo/php-matcher": "6.0.12", "doctrine/persistence": "1.3.2" }, "config": {