Skip to content

Commit ed4bca9

Browse files
Aerendirsoyuka
andauthored
fix(serializer): Guess uri variables with the operation and the data instead of hardcoding id (#5546)
* fix(serializer): compute uri variables on complex operations * tests --------- Co-authored-by: soyuka <[email protected]>
1 parent 8bede6d commit ed4bca9

File tree

2 files changed

+98
-2
lines changed

2 files changed

+98
-2
lines changed

src/Serializer/ItemNormalizer.php

+27-2
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@
1717
use ApiPlatform\Api\ResourceClassResolverInterface;
1818
use ApiPlatform\Api\UrlGeneratorInterface;
1919
use ApiPlatform\Exception\InvalidArgumentException;
20+
use ApiPlatform\Metadata\Link;
2021
use ApiPlatform\Metadata\Property\Factory\PropertyMetadataFactoryInterface;
2122
use ApiPlatform\Metadata\Property\Factory\PropertyNameCollectionFactoryInterface;
2223
use ApiPlatform\Metadata\Resource\Factory\ResourceMetadataCollectionFactoryInterface;
@@ -76,10 +77,34 @@ private function updateObjectToPopulate(array $data, array &$context): void
7677
$context[self::OBJECT_TO_POPULATE] = $this->iriConverter->getResourceFromIri((string) $data['id'], $context + ['fetch_data' => true]);
7778
} catch (InvalidArgumentException) {
7879
$operation = $this->resourceMetadataCollectionFactory->create($context['resource_class'])->getOperation();
79-
// todo: we could guess uri variables with the operation and the data instead of hardcoding id
80-
$iri = $this->iriConverter->getIriFromResource($context['resource_class'], UrlGeneratorInterface::ABS_PATH, $operation, ['uri_variables' => ['id' => $data['id']]]);
80+
$uriVariables = $this->getContextUriVariables($data, $operation, $context);
81+
$iri = $this->iriConverter->getIriFromResource($context['resource_class'], UrlGeneratorInterface::ABS_PATH, $operation, ['uri_variables' => $uriVariables]);
8182

8283
$context[self::OBJECT_TO_POPULATE] = $this->iriConverter->getResourceFromIri($iri, ['fetch_data' => true]);
8384
}
8485
}
86+
87+
private function getContextUriVariables(array $data, $operation, array $context): array
88+
{
89+
if (!isset($context['uri_variables'])) {
90+
return ['id' => $data['id']];
91+
}
92+
93+
$uriVariables = $context['uri_variables'];
94+
95+
/** @var Link $uriVariable */
96+
foreach ($operation->getUriVariables() as $uriVariable) {
97+
if (isset($uriVariables[$uriVariable->getParameterName()])) {
98+
continue;
99+
}
100+
101+
foreach ($uriVariable->getIdentifiers() as $identifier) {
102+
if (isset($data[$identifier])) {
103+
$uriVariables[$uriVariable->getParameterName()] = $data[$identifier];
104+
}
105+
}
106+
}
107+
108+
return $uriVariables;
109+
}
85110
}

tests/Serializer/ItemNormalizerTest.php

+71
Original file line numberDiff line numberDiff line change
@@ -15,10 +15,18 @@
1515

1616
use ApiPlatform\Api\IriConverterInterface;
1717
use ApiPlatform\Api\ResourceClassResolverInterface;
18+
use ApiPlatform\Api\UrlGeneratorInterface;
19+
use ApiPlatform\Exception\InvalidArgumentException;
1820
use ApiPlatform\Metadata\ApiProperty;
21+
use ApiPlatform\Metadata\ApiResource;
22+
use ApiPlatform\Metadata\Get;
23+
use ApiPlatform\Metadata\Link;
24+
use ApiPlatform\Metadata\Operations;
1925
use ApiPlatform\Metadata\Property\Factory\PropertyMetadataFactoryInterface;
2026
use ApiPlatform\Metadata\Property\Factory\PropertyNameCollectionFactoryInterface;
2127
use ApiPlatform\Metadata\Property\PropertyNameCollection;
28+
use ApiPlatform\Metadata\Resource\Factory\ResourceMetadataCollectionFactoryInterface;
29+
use ApiPlatform\Metadata\Resource\ResourceMetadataCollection;
2230
use ApiPlatform\Serializer\ItemNormalizer;
2331
use ApiPlatform\Tests\Fixtures\TestBundle\Entity\Dummy;
2432
use PHPUnit\Framework\TestCase;
@@ -173,6 +181,69 @@ public function testDenormalizeWithIri(): void
173181
$this->assertInstanceOf(Dummy::class, $normalizer->denormalize(['id' => '/dummies/12', 'name' => 'hello'], Dummy::class, null, $context));
174182
}
175183

184+
public function testDenormalizeGuessingUriVariables(): void
185+
{
186+
$context = ['resource_class' => Dummy::class, 'api_allow_update' => true, 'uri_variables' => [
187+
'parent_resource' => '1',
188+
'resource' => '1',
189+
]];
190+
191+
$propertyNameCollection = new PropertyNameCollection(['name']);
192+
$propertyNameCollectionFactoryProphecy = $this->prophesize(PropertyNameCollectionFactoryInterface::class);
193+
$propertyNameCollectionFactoryProphecy->create(Dummy::class, Argument::cetera())->willReturn($propertyNameCollection)->shouldBeCalled();
194+
195+
$propertyMetadata = (new ApiProperty())->withReadable(true)->withWritable(true);
196+
$propertyMetadataFactoryProphecy = $this->prophesize(PropertyMetadataFactoryInterface::class);
197+
$propertyMetadataFactoryProphecy->create(Dummy::class, 'name', Argument::cetera())->willReturn($propertyMetadata)->shouldBeCalled();
198+
199+
$uriVariables = [
200+
'parent_resource' => new Link('parent_resource', identifiers: ['id']),
201+
'resource' => new Link('resource', identifiers: ['id']),
202+
'sub_resource' => new Link('sub_resource', identifiers: ['id']),
203+
];
204+
$resourceMetadataCollectionFactoryProphecy = $this->prophesize(ResourceMetadataCollectionFactoryInterface::class);
205+
$resourceMetadataCollectionFactoryProphecy->create(Dummy::class)->willReturn(new ResourceMetadataCollection(Dummy::class, [
206+
(new ApiResource())->withShortName('Dummy')->withOperations(new Operations([
207+
'sub_resource' => (new Get(uriVariables: $uriVariables))->withShortName('Dummy'),
208+
])),
209+
]));
210+
211+
$resourceClassResolverProphecy = $this->prophesize(ResourceClassResolverInterface::class);
212+
$resourceClassResolverProphecy->getResourceClass(null, Dummy::class)->willReturn(Dummy::class);
213+
$resourceClassResolverProphecy->isResourceClass(Dummy::class)->willReturn(true);
214+
215+
$serializerProphecy = $this->prophesize(SerializerInterface::class);
216+
$serializerProphecy->willImplement(DenormalizerInterface::class);
217+
218+
$iriConverterProphecy = $this->prophesize(IriConverterInterface::class);
219+
$iriConverterProphecy->getResourceFromIri(Argument::is('12'), Argument::cetera())->willThrow(InvalidArgumentException::class);
220+
$iriConverterProphecy
221+
->getIriFromResource(
222+
Dummy::class,
223+
UrlGeneratorInterface::ABS_PATH,
224+
Argument::type(Get::class),
225+
Argument::withEntry('uri_variables', Argument::allOf(
226+
Argument::withEntry('parent_resource', '1'),
227+
Argument::withEntry('resource', '1'),
228+
Argument::withEntry('sub_resource', '12')
229+
))
230+
)
231+
->willReturn('parent_resource/1/resource/1/sub_resource/2')
232+
->shouldBeCalledOnce();
233+
$iriConverterProphecy->getResourceFromIri('parent_resource/1/resource/1/sub_resource/2', ['fetch_data' => true])->shouldBeCalledOnce();
234+
235+
$normalizer = new ItemNormalizer(
236+
$propertyNameCollectionFactoryProphecy->reveal(),
237+
$propertyMetadataFactoryProphecy->reveal(),
238+
$iriConverterProphecy->reveal(),
239+
$resourceClassResolverProphecy->reveal(),
240+
resourceMetadataFactory: $resourceMetadataCollectionFactoryProphecy->reveal(),
241+
);
242+
$normalizer->setSerializer($serializerProphecy->reveal());
243+
244+
$this->assertInstanceOf(Dummy::class, $normalizer->denormalize(['id' => '12', 'name' => 'hello'], Dummy::class, null, $context));
245+
}
246+
176247
public function testDenormalizeWithIdAndUpdateNotAllowed(): void
177248
{
178249
$this->expectException(NotNormalizableValueException::class);

0 commit comments

Comments
 (0)