diff --git a/src/Hydra/Serializer/CollectionNormalizer.php b/src/Hydra/Serializer/CollectionNormalizer.php index 0ee28252ce..e882d3aef0 100644 --- a/src/Hydra/Serializer/CollectionNormalizer.php +++ b/src/Hydra/Serializer/CollectionNormalizer.php @@ -36,8 +36,10 @@ final class CollectionNormalizer extends AbstractCollectionNormalizer public const FORMAT = 'jsonld'; public const IRI_ONLY = 'iri_only'; + public const PRESERVE_COLLECTION_KEYS = 'preserve_collection_keys'; private array $defaultContext = [ self::IRI_ONLY => false, + self::PRESERVE_COLLECTION_KEYS => false, ]; public function __construct(private readonly ContextBuilderInterface $contextBuilder, ResourceClassResolverInterface $resourceClassResolver, private readonly IriConverterInterface $iriConverter, array $defaultContext = []) @@ -79,12 +81,19 @@ protected function getItemsData(iterable $object, ?string $format = null, array $hydraPrefix = $this->getHydraPrefix($context + $this->defaultContext); $data = [$hydraPrefix.'member' => []]; $iriOnly = $context[self::IRI_ONLY] ?? $this->defaultContext[self::IRI_ONLY]; + $preserveCollectionKey = $context[self::PRESERVE_COLLECTION_KEYS] ?? $this->defaultContext[self::PRESERVE_COLLECTION_KEYS]; - foreach ($object as $obj) { + foreach ($object as $key => $obj) { if ($iriOnly) { - $data[$hydraPrefix.'member'][] = $this->iriConverter->getIriFromResource($obj, UrlGeneratorInterface::ABS_PATH, null, $context); + $normalizedItem = $this->iriConverter->getIriFromResource($obj, UrlGeneratorInterface::ABS_PATH, null, $context); } else { - $data[$hydraPrefix.'member'][] = $this->normalizer->normalize($obj, $format, $context + ['jsonld_has_context' => true]); + $normalizedItem = $this->normalizer->normalize($obj, $format, $context + ['jsonld_has_context' => true]); + } + + if ($preserveCollectionKey) { + $data[$hydraPrefix.'member'][$key] = $normalizedItem; + } else { + $data[$hydraPrefix.'member'][] = $normalizedItem; } } diff --git a/src/Hydra/Tests/Serializer/CollectionNormalizerTest.php b/src/Hydra/Tests/Serializer/CollectionNormalizerTest.php index 7fdedbdc19..3512d2d65c 100644 --- a/src/Hydra/Tests/Serializer/CollectionNormalizerTest.php +++ b/src/Hydra/Tests/Serializer/CollectionNormalizerTest.php @@ -23,6 +23,7 @@ use ApiPlatform\Serializer\AbstractItemNormalizer; use ApiPlatform\State\Pagination\PaginatorInterface; use ApiPlatform\State\Pagination\PartialPaginatorInterface; +use PHPUnit\Framework\Attributes\DataProvider; use PHPUnit\Framework\TestCase; use Prophecy\Argument; use Prophecy\PhpUnit\ProphecyTrait; @@ -123,6 +124,77 @@ public function testNormalizeResourceCollection(): void ], $actual); } + #[DataProvider('normalizeWithKeyDataProvider')] + public function testNormalizeResourceWithKeysCollection(bool $preserveKeys): void + { + $fooOne = new Foo(); + $fooOne->id = 1; + $fooOne->bar = 'baz'; + + $fooThree = new Foo(); + $fooThree->id = 3; + $fooThree->bar = 'bzz'; + + $data = [$fooOne, 3 => $fooThree]; + + $normalizedFooOne = [ + '@id' => '/foos/1', + '@type' => 'Foo', + 'bar' => 'baz', + ]; + + $normalizedFooThree = [ + '@id' => '/foos/3', + '@type' => 'Foo', + 'bar' => 'bzz', + ]; + + $contextBuilderProphecy = $this->createMock(ContextBuilderInterface::class); + $contextBuilderProphecy->method('getResourceContextUri')->with(Foo::class)->willReturn('/contexts/Foo'); + + $resourceClassResolverProphecy = $this->createMock(ResourceClassResolverInterface::class); + $resourceClassResolverProphecy->method('getResourceClass')->with($data, Foo::class)->willReturn(Foo::class); + + $iriConverterProphecy = $this->createMock(IriConverterInterface::class); + $iriConverterProphecy->method('getIriFromResource')->with(Foo::class, UrlGeneratorInterface::ABS_PATH, null)->willReturn('/foos'); + + $delegateNormalizerProphecy = $this->createMock(NormalizerInterface::class); + $delegateNormalizerProphecy->method('normalize')->willReturnCallback( + static fn (Foo $item) => 1 === $item->id ? $normalizedFooOne : $normalizedFooThree + ); + + $normalizer = new CollectionNormalizer($contextBuilderProphecy, $resourceClassResolverProphecy, $iriConverterProphecy); + $normalizer->setNormalizer($delegateNormalizerProphecy); + + $actual = $normalizer->normalize($data, CollectionNormalizer::FORMAT, [ + 'operation_name' => 'get', + 'resource_class' => Foo::class, + CollectionNormalizer::PRESERVE_COLLECTION_KEYS => $preserveKeys, + ]); + + $this->assertEquals([ + '@context' => '/contexts/Foo', + '@id' => '/foos', + '@type' => 'hydra:Collection', + 'hydra:member' => $preserveKeys ? [ + $normalizedFooOne, + 3 => $normalizedFooThree, + ] : [ + $normalizedFooOne, + $normalizedFooThree, + ], + 'hydra:totalItems' => 2, + ], $actual); + } + + /** + * @return array + */ + public static function normalizeWithKeyDataProvider(): array + { + return [[false], [true]]; + } + public function testNormalizePaginator(): void { $this->assertEquals(