diff --git a/composer.json b/composer.json index 990228b36..b6b1df92c 100644 --- a/composer.json +++ b/composer.json @@ -42,9 +42,12 @@ "symfony/http-kernel": "^4.4 || ^5.4 || ^6.0", "symfony/options-resolver": "^4.4 || ^5.4 || ^6.0", "symfony/process": "^4.4 || ^5.4 || ^6.0", + "symfony/property-access": "^4.4 || ^5.4 || ^6.0", + "symfony/property-info": "^4.4 || ^5.4 || ^6.0", "symfony/routing": "^4.4 || ^5.4 || ^6.0", "symfony/security-core": "^4.4 || ^5.4 || ^6.0", "symfony/security-http": "^4.4 || ^5.4 || ^6.0", + "symfony/serializer": "^4.4 || ^5.4 || ^6.0", "symfony/validator": "^4.4 || ^5.4 || ^6.0", "twig/string-extra": "^3.0", "twig/twig": "^2.12.1 || ^3.0" diff --git a/src/Entity/Transformer.php b/src/Entity/Transformer.php index 830734cd4..052da88ba 100644 --- a/src/Entity/Transformer.php +++ b/src/Entity/Transformer.php @@ -25,6 +25,12 @@ use Sonata\PageBundle\Model\SnapshotInterface; use Sonata\PageBundle\Model\SnapshotManagerInterface; use Sonata\PageBundle\Model\TransformerInterface; +use Sonata\PageBundle\Serializer\InterfaceDenormalizer; +use Symfony\Component\Serializer\Normalizer\AbstractNormalizer; +use Symfony\Component\Serializer\Normalizer\DateTimeNormalizer; +use Symfony\Component\Serializer\Normalizer\DenormalizerInterface; +use Symfony\Component\Serializer\Normalizer\NormalizerInterface; +use Symfony\Component\Serializer\SerializerInterface; /** * This class transform a SnapshotInterface into PageInterface. @@ -51,18 +57,26 @@ final class Transformer implements TransformerInterface private ManagerRegistry $registry; /** - * @param ManagerInterface $blockManager + * @var SerializerInterface&NormalizerInterface&DenormalizerInterface + */ + private $serializer; + + /** + * @param ManagerInterface $blockManager + * @param SerializerInterface&NormalizerInterface&DenormalizerInterface $serializer */ public function __construct( SnapshotManagerInterface $snapshotManager, PageManagerInterface $pageManager, ManagerInterface $blockManager, - ManagerRegistry $registry + ManagerRegistry $registry, + SerializerInterface $serializer ) { $this->snapshotManager = $snapshotManager; $this->pageManager = $pageManager; $this->blockManager = $blockManager; $this->registry = $registry; + $this->serializer = $serializer; } public function create(PageInterface $page) @@ -89,33 +103,22 @@ public function create(PageInterface $page) $snapshot->setParentId($page->getParent()->getId()); } - $blocks = []; - foreach ($page->getBlocks() as $block) { - if ($block->getParent()) { // ignore block with a parent => must be a child of a main - continue; - } - - $blocks[] = $this->createBlock($block); - } - - $snapshot->setContent([ - 'id' => $page->getId(), - 'name' => $page->getName(), - 'javascript' => $page->getJavascript(), - 'stylesheet' => $page->getStylesheet(), - 'raw_headers' => $page->getRawHeaders(), - 'title' => $page->getTitle(), - 'meta_description' => $page->getMetaDescription(), - 'meta_keyword' => $page->getMetaKeyword(), - 'template_code' => $page->getTemplateCode(), - 'request_method' => $page->getRequestMethod(), - 'created_at' => null !== $page->getCreatedAt() ? (int) $page->getCreatedAt()->format('U') : null, - 'updated_at' => null !== $page->getUpdatedAt() ? (int) $page->getUpdatedAt()->format('U') : null, - 'slug' => $page->getSlug(), - 'parent_id' => $page->getParent() ? $page->getParent()->getId() : null, - 'blocks' => $blocks, + /** + * @var PageContent $content + */ + $content = $this->serializer->normalize($page, null, [ + DateTimeNormalizer::FORMAT_KEY => 'U', + AbstractNormalizer::GROUPS => ['page_transformer'], + AbstractNormalizer::CALLBACKS => [ + 'blocks' => static fn (Collection $innerObject, PageInterface $outerObject, string $attributeName, ?string $format = null, array $context = []) => $innerObject->filter(static fn (BlockInterface $block) => !$block->hasParent()), + 'parent' => static fn ($innerObject, $outerObject, string $attributeName, ?string $format = null, array $context = []) => $innerObject instanceof PageInterface ? $innerObject->getId() : $innerObject, + // remove NEXT_MAYOR + 'target' => static fn ($innerObject, $outerObject, string $attributeName, ?string $format = null, array $context = []) => $innerObject instanceof PageInterface ? $innerObject->getId() : $innerObject, + ], ]); + $snapshot->setContent($content); + return $snapshot; } @@ -135,27 +138,19 @@ public function load(SnapshotInterface $snapshot) $content = $snapshot->getContent(); - if (null !== $content) { - $page->setId($content['id']); - $page->setJavascript($content['javascript']); - $page->setStylesheet($content['stylesheet']); - $page->setRawHeaders($content['raw_headers']); - $page->setTitle($content['title'] ?? null); - $page->setMetaDescription($content['meta_description']); - $page->setMetaKeyword($content['meta_keyword']); - $page->setName($content['name']); - $page->setSlug($content['slug']); - $page->setTemplateCode($content['template_code']); - $page->setRequestMethod($content['request_method']); - - $createdAt = new \DateTime(); - $createdAt->setTimestamp((int) $content['created_at']); - $page->setCreatedAt($createdAt); - - $updatedAt = new \DateTime(); - $updatedAt->setTimestamp((int) $content['updated_at']); - $page->setUpdatedAt($updatedAt); - } + $pageClass = $this->pageManager->getClass(); + $blockClass = $this->blockManager->getClass(); + + $this->serializer->denormalize($content, $pageClass, null, [ + DateTimeNormalizer::FORMAT_KEY => 'U', + AbstractNormalizer::GROUPS => ['page_transformer'], + AbstractNormalizer::OBJECT_TO_POPULATE => $page, + InterfaceDenormalizer::SUPPORTED_INTERFACES_KEY => [ + PageInterface::class => $pageClass, + BlockInterface::class => $blockClass, + PageBlockInterface::class => $blockClass, + ], + ]); return $page; } @@ -166,37 +161,17 @@ public function loadBlock(array $content, PageInterface $page) $block->setPage($page); - if (isset($content['id'])) { - $block->setId($content['id']); - } - - if (isset($content['name'])) { - $block->setName($content['name']); - } - - $block->setEnabled($content['enabled']); - - if (isset($content['position'])) { - $block->setPosition($content['position']); - } + $blockClass = $this->blockManager->getClass(); - $block->setSettings($content['settings']); - - if (isset($content['type'])) { - $block->setType($content['type']); - } - - $createdAt = new \DateTime(); - $createdAt->setTimestamp((int) $content['created_at']); - $block->setCreatedAt($createdAt); - - $updatedAt = new \DateTime(); - $updatedAt->setTimestamp((int) $content['updated_at']); - $block->setUpdatedAt($updatedAt); - - foreach ($content['blocks'] as $child) { - $block->addChild($this->loadBlock($child, $page)); - } + $this->serializer->denormalize($content, $blockClass, null, [ + DateTimeNormalizer::FORMAT_KEY => 'U', + AbstractNormalizer::GROUPS => ['page_transformer'], + AbstractNormalizer::OBJECT_TO_POPULATE => $block, + InterfaceDenormalizer::SUPPORTED_INTERFACES_KEY => [ + BlockInterface::class => $blockClass, + PageBlockInterface::class => $blockClass, + ], + ]); return $block; } @@ -241,30 +216,4 @@ public function getChildren(PageInterface $page) return $this->children[$page->getId()]; } - - /** - * @return array - * - * @phpstan-return BlockContent - */ - private function createBlock(BlockInterface $block) - { - $childBlocks = []; - - foreach ($block->getChildren() as $child) { - $childBlocks[] = $this->createBlock($child); - } - - return [ - 'id' => $block->getId(), - 'name' => $block->getName(), - 'enabled' => $block->getEnabled(), - 'position' => $block->getPosition(), - 'settings' => $block->getSettings(), - 'type' => $block->getType(), - 'created_at' => null !== $block->getCreatedAt() ? (int) $block->getCreatedAt()->format('U') : null, - 'updated_at' => null !== $block->getUpdatedAt() ? (int) $block->getUpdatedAt()->format('U') : null, - 'blocks' => $childBlocks, - ]; - } } diff --git a/src/Model/TransformerInterface.php b/src/Model/TransformerInterface.php index bfe510f2b..fe3bfe5a3 100644 --- a/src/Model/TransformerInterface.php +++ b/src/Model/TransformerInterface.php @@ -54,8 +54,8 @@ * slug: string|null, * template_code: string|null, * request_method: string|null, - * created_at: int|null, - * updated_at: int|null, + * created_at: int|string|null, + * updated_at: int|string|null, * blocks: array, * } */ diff --git a/src/Resources/config/orm.xml b/src/Resources/config/orm.xml index 429fe8ccd..6b681fa32 100644 --- a/src/Resources/config/orm.xml +++ b/src/Resources/config/orm.xml @@ -47,6 +47,7 @@ + diff --git a/src/Resources/config/serialization/Model.Block.xml b/src/Resources/config/serialization/Model.Block.xml new file mode 100644 index 000000000..aaad43abf --- /dev/null +++ b/src/Resources/config/serialization/Model.Block.xml @@ -0,0 +1,36 @@ + + + + + page_transformer + + + page_transformer + + + page_transformer + + + page_transformer + + + page_transformer + + + page_transformer + + + page_transformer + + + page_transformer + + + page_transformer + + + + + + + diff --git a/src/Resources/config/serialization/Model.Page.xml b/src/Resources/config/serialization/Model.Page.xml new file mode 100644 index 000000000..15f2f4258 --- /dev/null +++ b/src/Resources/config/serialization/Model.Page.xml @@ -0,0 +1,55 @@ + + + + + page_transformer + + + page_transformer + + + page_transformer + + + page_transformer + + + page_transformer + + + page_transformer + + + page_transformer + + + page_transformer + + + page_transformer + + + page_transformer + + + page_transformer + + + page_transformer + + + page_transformer + + + page_transformer + + + page_transformer + + + + + + + + diff --git a/src/Serializer/InterfaceDenormalizer.php b/src/Serializer/InterfaceDenormalizer.php new file mode 100644 index 000000000..c096155d6 --- /dev/null +++ b/src/Serializer/InterfaceDenormalizer.php @@ -0,0 +1,62 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Sonata\PageBundle\Serializer; + +use Symfony\Component\Serializer\Exception\BadMethodCallException; +use Symfony\Component\Serializer\Exception\InvalidArgumentException; +use Symfony\Component\Serializer\Normalizer\ContextAwareDenormalizerInterface; +use Symfony\Component\Serializer\Normalizer\DenormalizerInterface; +use Symfony\Component\Serializer\SerializerAwareInterface; +use Symfony\Component\Serializer\SerializerInterface; + +class InterfaceDenormalizer implements ContextAwareDenormalizerInterface, SerializerAwareInterface +{ + public const SUPPORTED_INTERFACES_KEY = 'supported_interfaces'; + + /** + * @var SerializerInterface|DenormalizerInterface + */ + private $serializer; + + public function denormalize($data, $type, $format = null, array $context = []) + { + if (null === $this->serializer) { + throw new BadMethodCallException('Please set a serializer before calling denormalize()!'); + } + + if (!isset($context[self::SUPPORTED_INTERFACES_KEY][$type])) { + throw new InvalidArgumentException('Unsupported class: '.$type); + } + + return $this->serializer->denormalize($data, $context[self::SUPPORTED_INTERFACES_KEY][$type], $format, $context); + } + + public function supportsDenormalization($data, $type, $format = null, array $context = []): bool + { + if (!interface_exists($type, false)) { + return false; + } + + if (!isset($context[self::SUPPORTED_INTERFACES_KEY][$type])) { + return false; + } + + return $this->serializer->supportsDenormalization($data, $context[self::SUPPORTED_INTERFACES_KEY][$type], $format, $context); + } + + public function setSerializer(SerializerInterface $serializer) + { + $this->serializer = $serializer; + } +} diff --git a/tests/Entity/TransformerTest.php b/tests/Entity/TransformerTest.php index d7ab6c772..b57b9d0ce 100644 --- a/tests/Entity/TransformerTest.php +++ b/tests/Entity/TransformerTest.php @@ -22,10 +22,21 @@ use Sonata\PageBundle\Model\PageManagerInterface; use Sonata\PageBundle\Model\SnapshotManagerInterface; use Sonata\PageBundle\Model\TransformerInterface; +use Sonata\PageBundle\Serializer\InterfaceDenormalizer; use Sonata\PageBundle\Tests\App\Entity\SonataPageBlock; use Sonata\PageBundle\Tests\App\Entity\SonataPagePage; use Sonata\PageBundle\Tests\App\Entity\SonataPageSite; use Sonata\PageBundle\Tests\App\Entity\SonataPageSnapshot; +use Symfony\Component\PropertyInfo\Extractor\ReflectionExtractor; +use Symfony\Component\Serializer\Encoder\JsonEncoder; +use Symfony\Component\Serializer\Mapping\Factory\ClassMetadataFactory; +use Symfony\Component\Serializer\Mapping\Loader\LoaderChain; +use Symfony\Component\Serializer\Mapping\Loader\XmlFileLoader; +use Symfony\Component\Serializer\NameConverter\MetadataAwareNameConverter; +use Symfony\Component\Serializer\Normalizer\ArrayDenormalizer; +use Symfony\Component\Serializer\Normalizer\DateTimeNormalizer; +use Symfony\Component\Serializer\Normalizer\ObjectNormalizer; +use Symfony\Component\Serializer\Serializer; /** * @phpstan-import-type PageContent from TransformerInterface @@ -56,14 +67,40 @@ protected function setUp(): void $this->snapshotManager = $this->createMock(SnapshotManagerInterface::class); $this->pageManager = $this->createMock(PageManagerInterface::class); + $this->pageManager->method('createWithDefaults')->willReturn(new SonataPagePage()); + $this->pageManager->method('getClass')->willReturn(SonataPagePage::class); + $this->blockManager = $this->createMock(ManagerInterface::class); + $this->blockManager->method('create')->willReturnCallback(static fn () => new SonataPageBlock()); + $this->blockManager->method('getClass')->willReturn(SonataPageBlock::class); + $registry = $this->createMock(ManagerRegistry::class); + $loaders = new LoaderChain([ + new XmlFileLoader(__DIR__.'/../../src/Resources/config/serialization/Model.Block.xml'), + new XmlFileLoader(__DIR__.'/../../src/Resources/config/serialization/Model.Page.xml'), + ]); + + $classMetadataFactory = new ClassMetadataFactory($loaders); + $nameConverter = new MetadataAwareNameConverter($classMetadataFactory); + + $objectNormalizer = new ObjectNormalizer($classMetadataFactory, $nameConverter, null, new ReflectionExtractor()); + + $encoders = [new JsonEncoder()]; + $normalizers = [ + new DateTimeNormalizer(), + new ArrayDenormalizer(), + new InterfaceDenormalizer(), + $objectNormalizer, + ]; + $serializer = new Serializer($normalizers, $encoders); + $this->transformer = new Transformer( $this->snapshotManager, $this->pageManager, $this->blockManager, $registry, + $serializer, ); } @@ -143,10 +180,6 @@ public function testTransformerPageToSnapshot(): void public function testLoadSnapshotToPage(): void { - $method = method_exists($this->pageManager, 'createWithDefaults') ? 'createWithDefaults' : 'create'; - $this->pageManager->method($method)->willReturn(new SonataPagePage()); - $this->pageManager->method('getClass')->willReturn(SonataPagePage::class); - $dateTime = new \DateTime(); $snapshot = new SonataPageSnapshot(); $snapshot->setContent($this->getTestContent($dateTime)); @@ -161,8 +194,6 @@ public function testLoadSnapshotToPage(): void public function testLoadBlock(): void { - $this->blockManager->method('create')->willReturnCallback(static fn () => new SonataPageBlock()); - $dateTime = new \DateTime(); $page = new SonataPagePage(); @@ -188,8 +219,8 @@ protected function getTestContent(\DateTimeInterface $datetime): array 'meta_keyword' => null, 'template_code' => null, 'request_method' => 'GET|POST|HEAD|DELETE|PUT', - 'created_at' => (int) $datetime->format('U'), - 'updated_at' => (int) $datetime->format('U'), + 'created_at' => $datetime->format('U'), + 'updated_at' => $datetime->format('U'), 'slug' => null, 'parent_id' => 'page_parent', 'blocks' => [ @@ -210,8 +241,8 @@ protected function getTestBlockArray(\DateTimeInterface $datetime): array 'position' => 0, 'settings' => [], 'type' => 'type', - 'created_at' => (int) $datetime->format('U'), - 'updated_at' => (int) $datetime->format('U'), + 'created_at' => $datetime->format('U'), + 'updated_at' => $datetime->format('U'), 'blocks' => [ [ 'id' => 'block234', @@ -220,8 +251,8 @@ protected function getTestBlockArray(\DateTimeInterface $datetime): array 'position' => 0, 'settings' => [], 'type' => 'type', - 'created_at' => (int) $datetime->format('U'), - 'updated_at' => (int) $datetime->format('U'), + 'created_at' => $datetime->format('U'), + 'updated_at' => $datetime->format('U'), 'blocks' => [], ], ],