From 3058b8382a49f5814d464519a8edf8b8bd251d73 Mon Sep 17 00:00:00 2001 From: mrossard Date: Thu, 24 Jul 2025 14:24:13 +0200 Subject: [PATCH] fix(symfony): explicitly set the target class when mapping entities to resources --- src/State/Processor/ObjectMapperProcessor.php | 2 +- src/State/Provider/ObjectMapperProvider.php | 4 +- .../TestBundle/ApiResource/FirstResource.php | 34 +++++++++++++ .../TestBundle/ApiResource/SecondResource.php | 36 ++++++++++++++ .../Fixtures/TestBundle/Entity/SameEntity.php | 48 +++++++++++++++++++ tests/Functional/MappingTest.php | 28 ++++++++++- 6 files changed, 148 insertions(+), 4 deletions(-) create mode 100644 tests/Fixtures/TestBundle/ApiResource/FirstResource.php create mode 100644 tests/Fixtures/TestBundle/ApiResource/SecondResource.php create mode 100644 tests/Fixtures/TestBundle/Entity/SameEntity.php diff --git a/src/State/Processor/ObjectMapperProcessor.php b/src/State/Processor/ObjectMapperProcessor.php index 2c379a68bc2..0d926c573c2 100644 --- a/src/State/Processor/ObjectMapperProcessor.php +++ b/src/State/Processor/ObjectMapperProcessor.php @@ -42,6 +42,6 @@ public function process(mixed $data, Operation $operation, array $uriVariables = return $this->decorated->process($data, $operation, $uriVariables, $context); } - return $this->objectMapper->map($this->decorated->process($this->objectMapper->map($data), $operation, $uriVariables, $context)); + return $this->objectMapper->map($this->decorated->process($this->objectMapper->map($data), $operation, $uriVariables, $context), $operation->getClass()); } } diff --git a/src/State/Provider/ObjectMapperProvider.php b/src/State/Provider/ObjectMapperProvider.php index bd9747e11ac..b19d9758b3e 100644 --- a/src/State/Provider/ObjectMapperProvider.php +++ b/src/State/Provider/ObjectMapperProvider.php @@ -64,9 +64,9 @@ public function provide(Operation $operation, array $uriVariables = [], array $c } if ($data instanceof PaginatorInterface) { - $data = new ArrayPaginator(array_map(fn ($v) => $this->objectMapper->map($v), iterator_to_array($data)), 0, \count($data)); + $data = new ArrayPaginator(array_map(fn ($v) => $this->objectMapper->map($v, $operation->getClass()), iterator_to_array($data)), 0, \count($data)); } else { - $data = $this->objectMapper->map($data); + $data = $this->objectMapper->map($data, $operation->getClass()); } $request?->attributes->set('data', $data); diff --git a/tests/Fixtures/TestBundle/ApiResource/FirstResource.php b/tests/Fixtures/TestBundle/ApiResource/FirstResource.php new file mode 100644 index 00000000000..5390bea4777 --- /dev/null +++ b/tests/Fixtures/TestBundle/ApiResource/FirstResource.php @@ -0,0 +1,34 @@ + + * + * 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\Tests\Fixtures\TestBundle\ApiResource; + +use ApiPlatform\Doctrine\Orm\State\Options; +use ApiPlatform\Metadata\ApiResource; +use ApiPlatform\Metadata\GetCollection; +use ApiPlatform\Tests\Fixtures\TestBundle\Entity\SameEntity; +use Symfony\Component\ObjectMapper\Attribute\Map; + +#[ApiResource( + shortName: 'First', + operations: [new GetCollection()], + stateOptions: new Options(entityClass: SameEntity::class) +)] +#[Map(target: SameEntity::class)] +final class FirstResource +{ + #[Map(if: false)] + public ?int $id = null; + + public string $name; +} diff --git a/tests/Fixtures/TestBundle/ApiResource/SecondResource.php b/tests/Fixtures/TestBundle/ApiResource/SecondResource.php new file mode 100644 index 00000000000..9d13f473631 --- /dev/null +++ b/tests/Fixtures/TestBundle/ApiResource/SecondResource.php @@ -0,0 +1,36 @@ + + * + * 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\Tests\Fixtures\TestBundle\ApiResource; + +use ApiPlatform\Doctrine\Orm\State\Options; +use ApiPlatform\Metadata\ApiResource; +use ApiPlatform\Metadata\GetCollection; +use ApiPlatform\Tests\Fixtures\TestBundle\Entity\SameEntity; +use Symfony\Component\ObjectMapper\Attribute\Map; + +#[ApiResource( + shortName: 'Second', + operations: [new GetCollection()], + stateOptions: new Options(entityClass: SameEntity::class) +)] +#[Map(target: SameEntity::class)] +final class SecondResource +{ + #[Map(if: false)] + public ?int $id = null; + + public string $name; + + public string $extra = 'field'; +} diff --git a/tests/Fixtures/TestBundle/Entity/SameEntity.php b/tests/Fixtures/TestBundle/Entity/SameEntity.php new file mode 100644 index 00000000000..58d6db2e362 --- /dev/null +++ b/tests/Fixtures/TestBundle/Entity/SameEntity.php @@ -0,0 +1,48 @@ + + * + * 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\Tests\Fixtures\TestBundle\Entity; + +use ApiPlatform\Tests\Fixtures\TestBundle\ApiResource\FirstResource; +use ApiPlatform\Tests\Fixtures\TestBundle\ApiResource\SecondResource; +use Doctrine\ORM\Mapping as ORM; +use Symfony\Component\ObjectMapper\Attribute\Map; + +#[ORM\Entity] +#[Map(target: FirstResource::class)] +#[Map(target: SecondResource::class)] +class SameEntity +{ + #[ORM\Column(type: 'integer')] + #[ORM\Id] + #[ORM\GeneratedValue(strategy: 'AUTO')] + private ?int $id = null; + + #[ORM\Column] + private string $name; + + public function getId(): ?int + { + return $this->id; + } + + public function setName(string $name): void + { + $this->name = $name; + } + + public function getName(): string + { + return $this->name; + } +} diff --git a/tests/Functional/MappingTest.php b/tests/Functional/MappingTest.php index c032e6822cd..0075cd996dc 100644 --- a/tests/Functional/MappingTest.php +++ b/tests/Functional/MappingTest.php @@ -14,10 +14,13 @@ namespace ApiPlatform\Tests\Functional; use ApiPlatform\Symfony\Bundle\Test\ApiTestCase; +use ApiPlatform\Tests\Fixtures\TestBundle\ApiResource\FirstResource; use ApiPlatform\Tests\Fixtures\TestBundle\ApiResource\MappedResource; use ApiPlatform\Tests\Fixtures\TestBundle\ApiResource\MappedResourceOdm; +use ApiPlatform\Tests\Fixtures\TestBundle\ApiResource\SecondResource; use ApiPlatform\Tests\Fixtures\TestBundle\Document\MappedDocument; use ApiPlatform\Tests\Fixtures\TestBundle\Entity\MappedEntity; +use ApiPlatform\Tests\Fixtures\TestBundle\Entity\SameEntity; use ApiPlatform\Tests\RecreateSchemaTrait; use ApiPlatform\Tests\SetupClassResourcesTrait; use Doctrine\ODM\MongoDB\DocumentManager; @@ -33,7 +36,7 @@ final class MappingTest extends ApiTestCase */ public static function getResources(): array { - return [MappedResource::class, MappedResourceOdm::class]; + return [MappedResource::class, MappedResourceOdm::class, FirstResource::class, SecondResource::class]; } public function testShouldMapBetweenResourceAndEntity(): void @@ -68,6 +71,29 @@ public function testShouldMapBetweenResourceAndEntity(): void $this->assertJsonContains(['username' => 'ba zar']); } + public function testShouldMapToTheCorrectResource(): void + { + if ($this->isMongoDB()) { + $this->markTestSkipped('MongoDB not tested.'); + } + + if (!$this->getContainer()->has('object_mapper')) { + $this->markTestSkipped('ObjectMapper not installed'); + } + + $this->recreateSchema([SameEntity::class]); + $manager = $this->getManager(); + $e = new SameEntity(); + $e->setName('foo'); + $manager->persist($e); + $manager->flush(); + + self::createClient()->request('GET', '/seconds'); + $this->assertJsonContains(['hydra:member' => [ + ['name' => 'foo', 'extra' => 'field'], + ]]); + } + private function loadFixtures(): void { $manager = $this->getManager();