Skip to content

Commit f400290

Browse files
Cafeine42Thibaut Cholleysoyuka
authored
fix(state): on creation, give expected class to object mapper (#7795)
Co-authored-by: Thibaut Cholley <thibaut.cholley@elsie-sante.fr> Co-authored-by: soyuka <soyuka@users.noreply.github.com>
1 parent 42a2d7f commit f400290

6 files changed

Lines changed: 70 additions & 2 deletions

File tree

src/State/Processor/ObjectMapperProcessor.php

Lines changed: 13 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@
1515

1616
use ApiPlatform\Metadata\Operation;
1717
use ApiPlatform\State\ProcessorInterface;
18+
use ApiPlatform\State\Util\StateOptionsTrait;
1819
use Symfony\Component\HttpFoundation\Response;
1920
use Symfony\Component\ObjectMapper\ObjectMapperInterface;
2021

@@ -23,6 +24,8 @@
2324
*/
2425
final class ObjectMapperProcessor implements ProcessorInterface
2526
{
27+
use StateOptionsTrait;
28+
2629
/**
2730
* @param ProcessorInterface<mixed,mixed> $decorated
2831
*/
@@ -48,9 +51,17 @@ public function process(mixed $data, Operation $operation, array $uriVariables =
4851
}
4952

5053
$request = $context['request'] ?? null;
54+
55+
// maps the Resource to an Entity
56+
if ($request?->attributes->get('mapped_data')) {
57+
$mappedData = $this->objectMapper->map($data, $request->attributes->get('mapped_data'));
58+
} else {
59+
$mappedData = $this->objectMapper->map($data, $this->getStateOptionsClass($operation, $operation->getClass()));
60+
}
61+
$request?->attributes->set('mapped_data', $mappedData);
62+
5163
$persisted = $this->decorated->process(
52-
// maps the Resource to an Entity
53-
$this->objectMapper->map($data, $request?->attributes->get('mapped_data')),
64+
$mappedData,
5465
$operation,
5566
$uriVariables,
5667
$context,
Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
<?php
2+
3+
/*
4+
* This file is part of the API Platform project.
5+
*
6+
* (c) Kévin Dunglas <dunglas@gmail.com>
7+
*
8+
* For the full copyright and license information, please view the LICENSE
9+
* file that was distributed with this source code.
10+
*/
11+
12+
declare(strict_types=1);
13+
14+
namespace ApiPlatform\Symfony\Tests\Fixtures;
15+
16+
class AnotherMappedObject
17+
{
18+
}

tests/Fixtures/TestBundle/ApiResource/FirstResource.php

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@
1616
use ApiPlatform\Doctrine\Orm\State\Options;
1717
use ApiPlatform\Metadata\ApiResource;
1818
use ApiPlatform\Metadata\GetCollection;
19+
use ApiPlatform\Symfony\Tests\Fixtures\AnotherMappedObject;
1920
use ApiPlatform\Tests\Fixtures\TestBundle\Entity\SameEntity;
2021
use Symfony\Component\ObjectMapper\Attribute\Map;
2122

@@ -24,6 +25,7 @@
2425
operations: [new GetCollection()],
2526
stateOptions: new Options(entityClass: SameEntity::class)
2627
)]
28+
#[Map(target: AnotherMappedObject::class)]
2729
#[Map(target: SameEntity::class)]
2830
final class FirstResource
2931
{

tests/Fixtures/TestBundle/ApiResource/MappedResource.php

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,13 +16,15 @@
1616
use ApiPlatform\Doctrine\Orm\State\Options;
1717
use ApiPlatform\JsonLd\ContextBuilder;
1818
use ApiPlatform\Metadata\ApiResource;
19+
use ApiPlatform\Symfony\Tests\Fixtures\AnotherMappedObject;
1920
use ApiPlatform\Tests\Fixtures\TestBundle\Entity\MappedEntity;
2021
use Symfony\Component\ObjectMapper\Attribute\Map;
2122

2223
#[ApiResource(
2324
stateOptions: new Options(entityClass: MappedEntity::class),
2425
normalizationContext: [ContextBuilder::HYDRA_CONTEXT_HAS_PREFIX => false],
2526
)]
27+
#[Map(target: AnotherMappedObject::class)]
2628
#[Map(target: MappedEntity::class)]
2729
final class MappedResource
2830
{

tests/Fixtures/TestBundle/ApiResource/MappedResourceOdm.php

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,13 +16,15 @@
1616
use ApiPlatform\Doctrine\Odm\State\Options;
1717
use ApiPlatform\JsonLd\ContextBuilder;
1818
use ApiPlatform\Metadata\ApiResource;
19+
use ApiPlatform\Symfony\Tests\Fixtures\AnotherMappedObject;
1920
use ApiPlatform\Tests\Fixtures\TestBundle\Document\MappedDocument;
2021
use Symfony\Component\ObjectMapper\Attribute\Map;
2122

2223
#[ApiResource(
2324
stateOptions: new Options(documentClass: MappedDocument::class),
2425
normalizationContext: [ContextBuilder::HYDRA_CONTEXT_HAS_PREFIX => false],
2526
)]
27+
#[Map(target: AnotherMappedObject::class)]
2628
#[Map(target: MappedDocument::class)]
2729
final class MappedResourceOdm
2830
{

tests/Functional/MappingTest.php

Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -103,6 +103,39 @@ public function testShouldMapBetweenResourceAndEntity(): void
103103
$this->assertResponseStatusCodeSame(204);
104104
}
105105

106+
/**
107+
* When an API resource has multiple #[Map] targets (e.g. MappedEntity + AnotherMappedObject),
108+
* the ObjectMapperProcessor must resolve the correct target using stateOptions during POST.
109+
*/
110+
public function testPostWithMultipleMapTargetsResolvesCorrectEntity(): void
111+
{
112+
if ($this->isMongoDB()) {
113+
$this->markTestSkipped('MongoDB not tested.');
114+
}
115+
116+
if (!$this->getContainer()->has('api_platform.object_mapper')) {
117+
$this->markTestSkipped('ObjectMapper not installed');
118+
}
119+
120+
$this->recreateSchema([MappedEntity::class]);
121+
$client = self::createClient();
122+
$r = $client->request('POST', 'mapped_resources', ['json' => ['username' => 'multi target']]);
123+
124+
$this->assertResponseStatusCodeSame(201);
125+
$this->assertJsonContains(['username' => 'multi target']);
126+
127+
// Verify the mapped_data is the entity from stateOptions, not AnotherMappedObject
128+
$mappedData = $client->getKernelBrowser()->getRequest()->attributes->get('mapped_data');
129+
$this->assertInstanceOf(MappedEntity::class, $mappedData, 'ObjectMapper should resolve to the stateOptions entity class, not the first #[Map] target.');
130+
131+
// Verify persistence
132+
$repo = $this->getManager()->getRepository(MappedEntity::class);
133+
$persisted = $repo->findOneBy(['id' => $r->toArray()['id']]);
134+
$this->assertNotNull($persisted);
135+
$this->assertSame('multi', $persisted->getFirstName());
136+
$this->assertSame('target', $persisted->getLastName());
137+
}
138+
106139
public function testShouldMapToTheCorrectResource(): void
107140
{
108141
if ($this->isMongoDB()) {

0 commit comments

Comments
 (0)