Skip to content

Commit

Permalink
Add ImageMap Property Resolver
Browse files Browse the repository at this point in the history
  • Loading branch information
alexander-schranz committed Oct 17, 2024
1 parent 69c006e commit f559652
Show file tree
Hide file tree
Showing 2 changed files with 364 additions and 0 deletions.
Original file line number Diff line number Diff line change
@@ -0,0 +1,145 @@
<?php

declare(strict_types=1);

/*
* This file is part of Sulu.
*
* (c) Sulu GmbH
*
* This source file is subject to the MIT license that is bundled
* with this source code in the file LICENSE.
*/

namespace Sulu\Bundle\MediaBundle\Infrastructure\Sulu\Content\PropertyResolver;

use Psr\Log\LoggerInterface;
use Sulu\Bundle\AdminBundle\Metadata\FormMetadata\FieldMetadata;
use Sulu\Bundle\AdminBundle\Metadata\FormMetadata\FormMetadata;
use Sulu\Bundle\ContentBundle\Content\Application\ContentResolver\Value\ContentView;
use Sulu\Bundle\ContentBundle\Content\Application\ContentResolver\Value\ResolvableResource;
use Sulu\Bundle\ContentBundle\Content\Application\MetadataResolver\MetadataResolver;
use Sulu\Bundle\ContentBundle\Content\Application\PropertyResolver\PropertyResolverInterface;
use Sulu\Bundle\MediaBundle\Infrastructure\Sulu\Content\ResourceLoader\MediaResourceLoader;

/**
* @internal if you need to override this service, create a new service with based on ResourceLoaderInterface instead of extending this class
*
* @final
*/
class ImageMapPropertyResolver implements PropertyResolverInterface // TODO we may should implement a PropertyResolverAwareMetadataInterface
{
private MetadataResolver $metadataResolver;

public function __construct(
private readonly LoggerInterface $logger,
private readonly bool $debug = false,
) {
}

/**
* @internal
*
* Prevent circular dependency by injecting the MetadataResolver after instantiation
*/
public function setMetadataResolver(MetadataResolver $metadataResolver): void
{
$this->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<array<mixed>> $hotspots
* @param array<string, mixed> $params
*
* @return array<array<mixed>>
*/
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';
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,219 @@
<?php

/*
* This file is part of Sulu.
*
* (c) Sulu GmbH
*
* This source file is subject to the MIT license that is bundled
* with this source code in the file LICENSE.
*/

namespace Sulu\Bundle\MediaBundle\Tests\Unit\Infrastructure\Sulu\Content\PropertyResolver;

use PHPUnit\Framework\Attributes\CoversClass;
use PHPUnit\Framework\Attributes\DataProvider;
use PHPUnit\Framework\TestCase;
use Sulu\Bundle\AdminBundle\Metadata\FormMetadata\FieldMetadata;
use Sulu\Bundle\AdminBundle\Metadata\FormMetadata\FormMetadata;
use Sulu\Bundle\ContentBundle\Content\Application\ContentResolver\Value\ResolvableResource;
use Sulu\Bundle\ContentBundle\Content\Application\MetadataResolver\MetadataResolver;
use Sulu\Bundle\ContentBundle\Content\Application\PropertyResolver\PropertyResolverProvider;
use Sulu\Bundle\ContentBundle\Content\Application\PropertyResolver\Resolver\DefaultPropertyResolver;
use Sulu\Bundle\MediaBundle\Infrastructure\Sulu\Content\PropertyResolver\ImageMapPropertyResolver;
use Symfony\Component\ErrorHandler\BufferingLogger;

#[CoversClass(ImageMapPropertyResolver::class)]
class ImageMapPropertyResolverTest extends TestCase
{
private ImageMapPropertyResolver $resolver;

private BufferingLogger $logger;

public function setUp(): void
{
$this->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<array{
* 0: mixed,
* }>
*/
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<array{
* type: string,
* ...
* }>,
* } $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<array{
* 0: array{
* id: string|int,
* displayOption?: string|null,
* },
* }>
*/
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<array{
* 0: mixed,
* }>
*/
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;
}
}

0 comments on commit f559652

Please sign in to comment.