diff --git a/src/State/Processor/ObjectMapperProcessor.php b/src/State/Processor/ObjectMapperProcessor.php index 7c71d3e3f0f..032a141749f 100644 --- a/src/State/Processor/ObjectMapperProcessor.php +++ b/src/State/Processor/ObjectMapperProcessor.php @@ -15,6 +15,7 @@ use ApiPlatform\Metadata\Operation; use ApiPlatform\State\ProcessorInterface; +use ApiPlatform\State\Util\StateOptionsTrait; use Symfony\Component\HttpFoundation\Response; use Symfony\Component\ObjectMapper\ObjectMapperInterface; @@ -23,6 +24,8 @@ */ final class ObjectMapperProcessor implements ProcessorInterface { + use StateOptionsTrait; + /** * @param ProcessorInterface $decorated */ @@ -48,9 +51,17 @@ public function process(mixed $data, Operation $operation, array $uriVariables = } $request = $context['request'] ?? null; + + // maps the Resource to an Entity + if ($request?->attributes->get('mapped_data')) { + $mappedData = $this->objectMapper->map($data, $request->attributes->get('mapped_data')); + } else { + $mappedData = $this->objectMapper->map($data, $this->getStateOptionsClass($operation, $operation->getClass())); + } + $request?->attributes->set('mapped_data', $mappedData); + $persisted = $this->decorated->process( - // maps the Resource to an Entity - $this->objectMapper->map($data, $request?->attributes->get('mapped_data')), + $mappedData, $operation, $uriVariables, $context, diff --git a/src/Symfony/Tests/Fixtures/AnotherMappedObject.php b/src/Symfony/Tests/Fixtures/AnotherMappedObject.php new file mode 100644 index 00000000000..f91178e05a4 --- /dev/null +++ b/src/Symfony/Tests/Fixtures/AnotherMappedObject.php @@ -0,0 +1,18 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +declare(strict_types=1); + +namespace ApiPlatform\Symfony\Tests\Fixtures; + +class AnotherMappedObject +{ +} diff --git a/tests/Fixtures/TestBundle/ApiResource/FirstResource.php b/tests/Fixtures/TestBundle/ApiResource/FirstResource.php index 5390bea4777..3bc2e67b9cd 100644 --- a/tests/Fixtures/TestBundle/ApiResource/FirstResource.php +++ b/tests/Fixtures/TestBundle/ApiResource/FirstResource.php @@ -16,6 +16,7 @@ use ApiPlatform\Doctrine\Orm\State\Options; use ApiPlatform\Metadata\ApiResource; use ApiPlatform\Metadata\GetCollection; +use ApiPlatform\Symfony\Tests\Fixtures\AnotherMappedObject; use ApiPlatform\Tests\Fixtures\TestBundle\Entity\SameEntity; use Symfony\Component\ObjectMapper\Attribute\Map; @@ -24,6 +25,7 @@ operations: [new GetCollection()], stateOptions: new Options(entityClass: SameEntity::class) )] +#[Map(target: AnotherMappedObject::class)] #[Map(target: SameEntity::class)] final class FirstResource { diff --git a/tests/Fixtures/TestBundle/ApiResource/MappedResource.php b/tests/Fixtures/TestBundle/ApiResource/MappedResource.php index 8b4092eabab..1a44df44f26 100644 --- a/tests/Fixtures/TestBundle/ApiResource/MappedResource.php +++ b/tests/Fixtures/TestBundle/ApiResource/MappedResource.php @@ -16,6 +16,7 @@ use ApiPlatform\Doctrine\Orm\State\Options; use ApiPlatform\JsonLd\ContextBuilder; use ApiPlatform\Metadata\ApiResource; +use ApiPlatform\Symfony\Tests\Fixtures\AnotherMappedObject; use ApiPlatform\Tests\Fixtures\TestBundle\Entity\MappedEntity; use Symfony\Component\ObjectMapper\Attribute\Map; @@ -23,6 +24,7 @@ stateOptions: new Options(entityClass: MappedEntity::class), normalizationContext: [ContextBuilder::HYDRA_CONTEXT_HAS_PREFIX => false], )] +#[Map(target: AnotherMappedObject::class)] #[Map(target: MappedEntity::class)] final class MappedResource { diff --git a/tests/Fixtures/TestBundle/ApiResource/MappedResourceOdm.php b/tests/Fixtures/TestBundle/ApiResource/MappedResourceOdm.php index 2a61cabab7b..3776bfc5de7 100644 --- a/tests/Fixtures/TestBundle/ApiResource/MappedResourceOdm.php +++ b/tests/Fixtures/TestBundle/ApiResource/MappedResourceOdm.php @@ -16,6 +16,7 @@ use ApiPlatform\Doctrine\Odm\State\Options; use ApiPlatform\JsonLd\ContextBuilder; use ApiPlatform\Metadata\ApiResource; +use ApiPlatform\Symfony\Tests\Fixtures\AnotherMappedObject; use ApiPlatform\Tests\Fixtures\TestBundle\Document\MappedDocument; use Symfony\Component\ObjectMapper\Attribute\Map; @@ -23,6 +24,7 @@ stateOptions: new Options(documentClass: MappedDocument::class), normalizationContext: [ContextBuilder::HYDRA_CONTEXT_HAS_PREFIX => false], )] +#[Map(target: AnotherMappedObject::class)] #[Map(target: MappedDocument::class)] final class MappedResourceOdm { diff --git a/tests/Functional/MappingTest.php b/tests/Functional/MappingTest.php index b79db81672a..5babb70fdb5 100644 --- a/tests/Functional/MappingTest.php +++ b/tests/Functional/MappingTest.php @@ -103,6 +103,39 @@ public function testShouldMapBetweenResourceAndEntity(): void $this->assertResponseStatusCodeSame(204); } + /** + * When an API resource has multiple #[Map] targets (e.g. MappedEntity + AnotherMappedObject), + * the ObjectMapperProcessor must resolve the correct target using stateOptions during POST. + */ + public function testPostWithMultipleMapTargetsResolvesCorrectEntity(): void + { + if ($this->isMongoDB()) { + $this->markTestSkipped('MongoDB not tested.'); + } + + if (!$this->getContainer()->has('api_platform.object_mapper')) { + $this->markTestSkipped('ObjectMapper not installed'); + } + + $this->recreateSchema([MappedEntity::class]); + $client = self::createClient(); + $r = $client->request('POST', 'mapped_resources', ['json' => ['username' => 'multi target']]); + + $this->assertResponseStatusCodeSame(201); + $this->assertJsonContains(['username' => 'multi target']); + + // Verify the mapped_data is the entity from stateOptions, not AnotherMappedObject + $mappedData = $client->getKernelBrowser()->getRequest()->attributes->get('mapped_data'); + $this->assertInstanceOf(MappedEntity::class, $mappedData, 'ObjectMapper should resolve to the stateOptions entity class, not the first #[Map] target.'); + + // Verify persistence + $repo = $this->getManager()->getRepository(MappedEntity::class); + $persisted = $repo->findOneBy(['id' => $r->toArray()['id']]); + $this->assertNotNull($persisted); + $this->assertSame('multi', $persisted->getFirstName()); + $this->assertSame('target', $persisted->getLastName()); + } + public function testShouldMapToTheCorrectResource(): void { if ($this->isMongoDB()) {