Skip to content

Commit

Permalink
fix(serializer): Guess uri variables with the operation and the data …
Browse files Browse the repository at this point in the history
…instead of hardcoding id (#5546)

* fix(serializer): compute uri variables on complex operations

* tests

---------

Co-authored-by: soyuka <soyuka@users.noreply.github.com>
  • Loading branch information
Aerendir and soyuka committed May 5, 2023
1 parent 8bede6d commit ed4bca9
Show file tree
Hide file tree
Showing 2 changed files with 98 additions and 2 deletions.
29 changes: 27 additions & 2 deletions src/Serializer/ItemNormalizer.php
Expand Up @@ -17,6 +17,7 @@
use ApiPlatform\Api\ResourceClassResolverInterface;
use ApiPlatform\Api\UrlGeneratorInterface;
use ApiPlatform\Exception\InvalidArgumentException;
use ApiPlatform\Metadata\Link;
use ApiPlatform\Metadata\Property\Factory\PropertyMetadataFactoryInterface;
use ApiPlatform\Metadata\Property\Factory\PropertyNameCollectionFactoryInterface;
use ApiPlatform\Metadata\Resource\Factory\ResourceMetadataCollectionFactoryInterface;
Expand Down Expand Up @@ -76,10 +77,34 @@ private function updateObjectToPopulate(array $data, array &$context): void
$context[self::OBJECT_TO_POPULATE] = $this->iriConverter->getResourceFromIri((string) $data['id'], $context + ['fetch_data' => true]);
} catch (InvalidArgumentException) {
$operation = $this->resourceMetadataCollectionFactory->create($context['resource_class'])->getOperation();
// todo: we could guess uri variables with the operation and the data instead of hardcoding id
$iri = $this->iriConverter->getIriFromResource($context['resource_class'], UrlGeneratorInterface::ABS_PATH, $operation, ['uri_variables' => ['id' => $data['id']]]);
$uriVariables = $this->getContextUriVariables($data, $operation, $context);
$iri = $this->iriConverter->getIriFromResource($context['resource_class'], UrlGeneratorInterface::ABS_PATH, $operation, ['uri_variables' => $uriVariables]);

$context[self::OBJECT_TO_POPULATE] = $this->iriConverter->getResourceFromIri($iri, ['fetch_data' => true]);
}
}

private function getContextUriVariables(array $data, $operation, array $context): array
{
if (!isset($context['uri_variables'])) {
return ['id' => $data['id']];
}

$uriVariables = $context['uri_variables'];

/** @var Link $uriVariable */
foreach ($operation->getUriVariables() as $uriVariable) {
if (isset($uriVariables[$uriVariable->getParameterName()])) {
continue;
}

foreach ($uriVariable->getIdentifiers() as $identifier) {
if (isset($data[$identifier])) {
$uriVariables[$uriVariable->getParameterName()] = $data[$identifier];
}
}
}

return $uriVariables;
}
}
71 changes: 71 additions & 0 deletions tests/Serializer/ItemNormalizerTest.php
Expand Up @@ -15,10 +15,18 @@

use ApiPlatform\Api\IriConverterInterface;
use ApiPlatform\Api\ResourceClassResolverInterface;
use ApiPlatform\Api\UrlGeneratorInterface;
use ApiPlatform\Exception\InvalidArgumentException;
use ApiPlatform\Metadata\ApiProperty;
use ApiPlatform\Metadata\ApiResource;
use ApiPlatform\Metadata\Get;
use ApiPlatform\Metadata\Link;
use ApiPlatform\Metadata\Operations;
use ApiPlatform\Metadata\Property\Factory\PropertyMetadataFactoryInterface;
use ApiPlatform\Metadata\Property\Factory\PropertyNameCollectionFactoryInterface;
use ApiPlatform\Metadata\Property\PropertyNameCollection;
use ApiPlatform\Metadata\Resource\Factory\ResourceMetadataCollectionFactoryInterface;
use ApiPlatform\Metadata\Resource\ResourceMetadataCollection;
use ApiPlatform\Serializer\ItemNormalizer;
use ApiPlatform\Tests\Fixtures\TestBundle\Entity\Dummy;
use PHPUnit\Framework\TestCase;
Expand Down Expand Up @@ -173,6 +181,69 @@ public function testDenormalizeWithIri(): void
$this->assertInstanceOf(Dummy::class, $normalizer->denormalize(['id' => '/dummies/12', 'name' => 'hello'], Dummy::class, null, $context));
}

public function testDenormalizeGuessingUriVariables(): void
{
$context = ['resource_class' => Dummy::class, 'api_allow_update' => true, 'uri_variables' => [
'parent_resource' => '1',
'resource' => '1',
]];

$propertyNameCollection = new PropertyNameCollection(['name']);
$propertyNameCollectionFactoryProphecy = $this->prophesize(PropertyNameCollectionFactoryInterface::class);
$propertyNameCollectionFactoryProphecy->create(Dummy::class, Argument::cetera())->willReturn($propertyNameCollection)->shouldBeCalled();

$propertyMetadata = (new ApiProperty())->withReadable(true)->withWritable(true);
$propertyMetadataFactoryProphecy = $this->prophesize(PropertyMetadataFactoryInterface::class);
$propertyMetadataFactoryProphecy->create(Dummy::class, 'name', Argument::cetera())->willReturn($propertyMetadata)->shouldBeCalled();

$uriVariables = [
'parent_resource' => new Link('parent_resource', identifiers: ['id']),
'resource' => new Link('resource', identifiers: ['id']),
'sub_resource' => new Link('sub_resource', identifiers: ['id']),
];
$resourceMetadataCollectionFactoryProphecy = $this->prophesize(ResourceMetadataCollectionFactoryInterface::class);
$resourceMetadataCollectionFactoryProphecy->create(Dummy::class)->willReturn(new ResourceMetadataCollection(Dummy::class, [
(new ApiResource())->withShortName('Dummy')->withOperations(new Operations([
'sub_resource' => (new Get(uriVariables: $uriVariables))->withShortName('Dummy'),
])),
]));

$resourceClassResolverProphecy = $this->prophesize(ResourceClassResolverInterface::class);
$resourceClassResolverProphecy->getResourceClass(null, Dummy::class)->willReturn(Dummy::class);
$resourceClassResolverProphecy->isResourceClass(Dummy::class)->willReturn(true);

$serializerProphecy = $this->prophesize(SerializerInterface::class);
$serializerProphecy->willImplement(DenormalizerInterface::class);

$iriConverterProphecy = $this->prophesize(IriConverterInterface::class);
$iriConverterProphecy->getResourceFromIri(Argument::is('12'), Argument::cetera())->willThrow(InvalidArgumentException::class);
$iriConverterProphecy
->getIriFromResource(
Dummy::class,
UrlGeneratorInterface::ABS_PATH,
Argument::type(Get::class),
Argument::withEntry('uri_variables', Argument::allOf(
Argument::withEntry('parent_resource', '1'),
Argument::withEntry('resource', '1'),
Argument::withEntry('sub_resource', '12')
))
)
->willReturn('parent_resource/1/resource/1/sub_resource/2')
->shouldBeCalledOnce();
$iriConverterProphecy->getResourceFromIri('parent_resource/1/resource/1/sub_resource/2', ['fetch_data' => true])->shouldBeCalledOnce();

$normalizer = new ItemNormalizer(
$propertyNameCollectionFactoryProphecy->reveal(),
$propertyMetadataFactoryProphecy->reveal(),
$iriConverterProphecy->reveal(),
$resourceClassResolverProphecy->reveal(),
resourceMetadataFactory: $resourceMetadataCollectionFactoryProphecy->reveal(),
);
$normalizer->setSerializer($serializerProphecy->reveal());

$this->assertInstanceOf(Dummy::class, $normalizer->denormalize(['id' => '12', 'name' => 'hello'], Dummy::class, null, $context));
}

public function testDenormalizeWithIdAndUpdateNotAllowed(): void
{
$this->expectException(NotNormalizableValueException::class);
Expand Down

0 comments on commit ed4bca9

Please sign in to comment.