diff --git a/src/Sulu/Bundle/MediaBundle/Infrastructure/Sulu/Content/PropertyResolver/ImageMapPropertyResolver.php b/src/Sulu/Bundle/MediaBundle/Infrastructure/Sulu/Content/PropertyResolver/ImageMapPropertyResolver.php new file mode 100644 index 00000000000..b9d2f7c109f --- /dev/null +++ b/src/Sulu/Bundle/MediaBundle/Infrastructure/Sulu/Content/PropertyResolver/ImageMapPropertyResolver.php @@ -0,0 +1,145 @@ +metadataResolver = $metadataResolver; + } + + public function resolve(mixed $data, string $locale, array $params = []): ContentView + { + $hotspots = (\is_array($data) && isset($data['hotspots']) && \is_array($data['hotspots'])) && \array_is_list($data['hotspots']) + ? $data['hotspots'] + : []; + + $hotspots = [] !== $hotspots ? $this->resolveHotspots($hotspots, $locale, $params) : []; + + $returnedParams = $params; + unset($returnedParams['metadata']); // TODO we may should implement a PropertyResolverAwareMetadataInterface + + if (!\is_array($data) + || !isset($data['imageId']) + || !\is_numeric($data['imageId']) + ) { + return ContentView::create([ + 'image' => null, + 'hotspots' => $hotspots, + ], ['imageId' => null, ...$returnedParams]); + } + + /** @var string $resourceLoaderKey */ + $resourceLoaderKey = $params['resourceLoader'] ?? MediaResourceLoader::getKey(); + $imageId = (int) $data['imageId']; + + return ContentView::create( + [ + 'image' => new ResolvableResource($imageId, $resourceLoaderKey), + 'hotspots' => $hotspots, + ], + [ + 'imageId' => $imageId, + ...$returnedParams, + ], + ); + } + + /** + * @param non-empty-array> $hotspots + * @param array $params + * + * @return array> + */ + private function resolveHotspots(array $hotspots, string $locale, array $params): array + { + $metadata = $params['metadata'] ?? null; + \assert($metadata instanceof FieldMetadata, 'Metadata must be set to resolve hotspots.'); + $metadataTypes = $metadata->getTypes(); + $contentViews = []; + foreach ($hotspots as $block) { + if (!\is_array($block) || !isset($block['type']) || !\is_string($block['type'])) { + continue; + } + if (!isset($block['hotspot']) || !\is_array($block['hotspot'])) { + continue; + } + + $type = $block['type']; + $formMetadata = $metadataTypes[$type] ?? null; + + if (!$formMetadata instanceof FormMetadata) { + $errorMessage = \sprintf( + 'Metadata type "%s" in "%s" not found, founded types are: "%s"', + $type, + $metadata->getName(), + \implode('", "', \array_keys($metadataTypes)), + ); + + $this->logger->error($errorMessage); + + if ($this->debug) { + throw new \UnexpectedValueException($errorMessage); + } + + // TODO discuss this with the team + $formMetadata = $metadataTypes[$metadata->getDefaultType()] ?? null; + if (!$formMetadata instanceof FormMetadata) { + continue; + } + } + + $contentViews[] = [ + 'type' => $type, + 'hotspot' => $block['hotspot'], + ...$this->metadataResolver->resolveItems($formMetadata->getItems(), $block, $locale), + ]; + } + + return $contentViews; + } + + public static function getType(): string + { + return 'image_map'; + } +} diff --git a/src/Sulu/Bundle/MediaBundle/Tests/Unit/Infrastructure/Sulu/Content/PropertyResolver/ImageMapPropertyResolverTest.php b/src/Sulu/Bundle/MediaBundle/Tests/Unit/Infrastructure/Sulu/Content/PropertyResolver/ImageMapPropertyResolverTest.php new file mode 100644 index 00000000000..0223089cc56 --- /dev/null +++ b/src/Sulu/Bundle/MediaBundle/Tests/Unit/Infrastructure/Sulu/Content/PropertyResolver/ImageMapPropertyResolverTest.php @@ -0,0 +1,219 @@ +logger = new BufferingLogger(); + $this->resolver = new ImageMapPropertyResolver( + $this->logger, + debug: false, + ); + $metadataResolverProperty = new PropertyResolverProvider([ + 'default' => new DefaultPropertyResolver(), + ]); + $metadataResolver = new MetadataResolver($metadataResolverProperty); + $this->resolver->setMetadataResolver($metadataResolver); + } + + public function testResolveEmpty(): void + { + $contentView = $this->resolver->resolve(null, 'en'); + + $this->assertSame(['image' => null, 'hotspots' => []], $contentView->getContent()); + $this->assertSame(['imageId' => null], $contentView->getView()); + } + + public function testResolveParams(): void + { + $contentView = $this->resolver->resolve(null, 'en', ['custom' => 'params']); + + $this->assertSame(['image' => null, 'hotspots' => []], $contentView->getContent()); + $this->assertSame([ + 'imageId' => null, + 'custom' => 'params', + ], $contentView->getView()); + } + + #[DataProvider('provideUnresolvableData')] + public function testResolveUnresolvableData(mixed $data): void + { + $contentView = $this->resolver->resolve($data, 'en'); + + $this->assertSame(['image' => null, 'hotspots' => []], $contentView->getContent()); + $this->assertSame(['imageId' => null], $contentView->getView()); + + $this->assertCount(0, $this->logger->cleanLogs()); + } + + /** + * @return iterable + */ + public static function provideUnresolvableData(): iterable + { + yield 'null' => [null]; + yield 'smart_content' => [['source' => '123']]; + yield 'single_value' => [1]; + yield 'object' => [(object) [1, 2]]; + yield 'int_list_not_in_ids' => [[1, 2]]; + yield 'ids_null' => [['ids' => null]]; + yield 'ids_list' => [['ids' => [1, 2]]]; + yield 'id_list' => [['id' => [1, 2]]]; + yield 'non_numeric_image_id' => [['imageId' => 'a']]; + } + + /** + * @param array{ + * imageId?: string|int, + * hotspots?: array, + * } $data + */ + #[DataProvider('provideResolvableData')] + public function testResolveResolvableData(array $data): void + { + $contentView = $this->resolver->resolve($data, 'en', ['metadata' => $this->createMetadata()]); + + $content = $contentView->getContent(); + $this->assertIsArray($content); + $imageId = $data['imageId'] ?? null; + if (null !== $imageId) { + $imageId = (int) $imageId; + $image = $content['image'] ?? null; + $this->assertInstanceOf(ResolvableResource::class, $image); + $this->assertSame($imageId, $image->getId()); + $this->assertSame('media', $image->getResourceLoaderKey()); + } + + $hotspots = $content['hotspots'] ?? []; + + $this->assertIsArray($hotspots); + foreach ($data['hotspots'] as $key => $hotspot) { + $hotspot = $hotspots[$key] ?? null; + $this->assertIsArray($hotspot); + $this->assertSame($data['hotspots'][$key], $hotspot); + } + + $this->assertSame([ + 'imageId' => $imageId, + ], $contentView->getView()); + + $this->assertCount(0, $this->logger->cleanLogs()); + } + + /** + * @return iterable + */ + public static function provideResolvableData(): iterable + { + yield 'empty' => [[]]; + yield 'int_id' => [['imageId' => 1]]; + yield 'int_id_with_hotspots' => [['imageId' => 1, 'hotspots' => [['type' => 'text', 'hotspot' => ['type' => 'circle'], 'title' => 'Title'], ['type' => 'text', 'title' => 'Title']]]]; + yield 'string_id' => [['imageId' => '1']]; + yield 'string_id_with_hotspots' => [['imageId' => '1', 'hotspots' => [['type' => 'text', 'hotspot' => ['type' => 'circle'], 'title' => 'Title'], ['type' => 'text', 'title' => 'Title']]]]; + } + + public function testCustomResourceLoader(): void + { + $contentView = $this->resolver->resolve( + ['imageId' => 1, 'hotspots' => [['type' => 'text', 'title' => 'Title'], ['type' => 'text', 'title' => 'Title']]], + 'en', + [ + 'metadata' => $this->createMetadata(), + 'resourceLoader' => 'custom_media', + ] + ); + + $content = $contentView->getContent(); + $this->assertIsArray($content); + $image = $content['image'] ?? null; + $this->assertInstanceOf(ResolvableResource::class, $image); + $this->assertSame(1, $image->getId()); + $this->assertSame('custom_media', $image->getResourceLoaderKey()); + + $this->assertSame([ + 'imageId' => 1, + 'resourceLoader' => 'custom_media', + ], $contentView->getView()); + } + + #[DataProvider('provideUnresolvableHotspotData')] + public function testResolveUnresolvableHotspotData(mixed $data): void + { + $contentView = $this->resolver->resolve($data, 'en', ['metadata' => $this->createMetadata()]); + + $content = $contentView->getContent(); + $this->assertIsArray($content); + $image = $content['image'] ?? null; + $this->assertInstanceOf(ResolvableResource::class, $image); + $this->assertSame(1, $image->getId()); + $this->assertSame('media', $image->getResourceLoaderKey()); + + $this->assertSame(['image' => null, 'hotspots' => []], $contentView->getContent()); + $this->assertSame(['imageId' => null], $contentView->getView()); + } + + /** + * @return iterable + */ + public static function provideUnresolvableHotspotData(): iterable + { + yield 'hotspot_with_not_exist_type' => [['imageId' => 1, 'hotspots' => [['type' => 'not_exist', 'title' => 'Title']]]]; + } + + private function createMetadata(): FieldMetadata + { + $fieldMetadata = new FieldMetadata('image'); + $fieldMetadata->setType('image_map'); + $fieldMetadata->setDefaultType('text'); + + $textFormMetadata = new FormMetadata(); + $textFormMetadata->setName('text'); + $itemMetadata = new FieldMetadata('title'); + $itemMetadata->setType('text_line'); + $textFormMetadata->addItem($itemMetadata); + + $fieldMetadata->addType($textFormMetadata); + + return $fieldMetadata; + } +}