Skip to content

Commit ec57f9a

Browse files
feat(serializer): option to preserve key in CollectionNormalizer
1 parent 05a5d4d commit ec57f9a

2 files changed

Lines changed: 84 additions & 3 deletions

File tree

src/Hydra/Serializer/CollectionNormalizer.php

Lines changed: 12 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -36,8 +36,10 @@ final class CollectionNormalizer extends AbstractCollectionNormalizer
3636

3737
public const FORMAT = 'jsonld';
3838
public const IRI_ONLY = 'iri_only';
39+
public const PRESERVE_COLLECTION_KEYS = 'preserve_collection_keys';
3940
private array $defaultContext = [
4041
self::IRI_ONLY => false,
42+
self::PRESERVE_COLLECTION_KEYS => false,
4143
];
4244

4345
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
7981
$hydraPrefix = $this->getHydraPrefix($context + $this->defaultContext);
8082
$data = [$hydraPrefix.'member' => []];
8183
$iriOnly = $context[self::IRI_ONLY] ?? $this->defaultContext[self::IRI_ONLY];
84+
$preserveCollectionKey = $context[self::PRESERVE_COLLECTION_KEYS] ?? $this->defaultContext[self::PRESERVE_COLLECTION_KEYS];
8285

83-
foreach ($object as $obj) {
86+
foreach ($object as $key => $obj) {
8487
if ($iriOnly) {
85-
$data[$hydraPrefix.'member'][] = $this->iriConverter->getIriFromResource($obj, UrlGeneratorInterface::ABS_PATH, null, $context);
88+
$normalizedItem = $this->iriConverter->getIriFromResource($obj, UrlGeneratorInterface::ABS_PATH, null, $context);
8689
} else {
87-
$data[$hydraPrefix.'member'][] = $this->normalizer->normalize($obj, $format, $context + ['jsonld_has_context' => true]);
90+
$normalizedItem = $this->normalizer->normalize($obj, $format, $context + ['jsonld_has_context' => true]);
91+
}
92+
93+
if ($preserveCollectionKey) {
94+
$data[$hydraPrefix.'member'][$key] = $normalizedItem;
95+
} else {
96+
$data[$hydraPrefix.'member'][] = $normalizedItem;
8897
}
8998
}
9099

src/Hydra/Tests/Serializer/CollectionNormalizerTest.php

Lines changed: 72 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,7 @@
2323
use ApiPlatform\Serializer\AbstractItemNormalizer;
2424
use ApiPlatform\State\Pagination\PaginatorInterface;
2525
use ApiPlatform\State\Pagination\PartialPaginatorInterface;
26+
use PHPUnit\Framework\Attributes\DataProvider;
2627
use PHPUnit\Framework\TestCase;
2728
use Prophecy\Argument;
2829
use Prophecy\PhpUnit\ProphecyTrait;
@@ -123,6 +124,77 @@ public function testNormalizeResourceCollection(): void
123124
], $actual);
124125
}
125126

127+
#[DataProvider('normalizeWithKeyDataProvider')]
128+
public function testNormalizeResourceWithKeysCollection(bool $preserveKeys): void
129+
{
130+
$fooOne = new Foo();
131+
$fooOne->id = 1;
132+
$fooOne->bar = 'baz';
133+
134+
$fooThree = new Foo();
135+
$fooThree->id = 3;
136+
$fooThree->bar = 'bzz';
137+
138+
$data = [$fooOne, 3 => $fooThree];
139+
140+
$normalizedFooOne = [
141+
'@id' => '/foos/1',
142+
'@type' => 'Foo',
143+
'bar' => 'baz',
144+
];
145+
146+
$normalizedFooThree = [
147+
'@id' => '/foos/3',
148+
'@type' => 'Foo',
149+
'bar' => 'bzz',
150+
];
151+
152+
$contextBuilderProphecy = $this->createMock(ContextBuilderInterface::class);
153+
$contextBuilderProphecy->method('getResourceContextUri')->with(Foo::class)->willReturn('/contexts/Foo');
154+
155+
$resourceClassResolverProphecy = $this->createMock(ResourceClassResolverInterface::class);
156+
$resourceClassResolverProphecy->method('getResourceClass')->with($data, Foo::class)->willReturn(Foo::class);
157+
158+
$iriConverterProphecy = $this->createMock(IriConverterInterface::class);
159+
$iriConverterProphecy->method('getIriFromResource')->with(Foo::class, UrlGeneratorInterface::ABS_PATH, null)->willReturn('/foos');
160+
161+
$delegateNormalizerProphecy = $this->createMock(NormalizerInterface::class);
162+
$delegateNormalizerProphecy->method('normalize')->willReturnCallback(
163+
static fn (Foo $item) => 1 === $item->id ? $normalizedFooOne : $normalizedFooThree
164+
);
165+
166+
$normalizer = new CollectionNormalizer($contextBuilderProphecy, $resourceClassResolverProphecy, $iriConverterProphecy);
167+
$normalizer->setNormalizer($delegateNormalizerProphecy);
168+
169+
$actual = $normalizer->normalize($data, CollectionNormalizer::FORMAT, [
170+
'operation_name' => 'get',
171+
'resource_class' => Foo::class,
172+
CollectionNormalizer::PRESERVE_COLLECTION_KEYS => $preserveKeys,
173+
]);
174+
175+
$this->assertEquals([
176+
'@context' => '/contexts/Foo',
177+
'@id' => '/foos',
178+
'@type' => 'hydra:Collection',
179+
'hydra:member' => $preserveKeys ? [
180+
$normalizedFooOne,
181+
3 => $normalizedFooThree,
182+
] : [
183+
$normalizedFooOne,
184+
$normalizedFooThree,
185+
],
186+
'hydra:totalItems' => 2,
187+
], $actual);
188+
}
189+
190+
/**
191+
* @return array<array{bool}>
192+
*/
193+
public static function normalizeWithKeyDataProvider(): array
194+
{
195+
return [[false], [true]];
196+
}
197+
126198
public function testNormalizePaginator(): void
127199
{
128200
$this->assertEquals(

0 commit comments

Comments
 (0)