From c1d2be3c6bb52f8a9689d573e080c7d1c6987fca Mon Sep 17 00:00:00 2001 From: soyuka Date: Wed, 25 Mar 2026 16:22:15 +0100 Subject: [PATCH] test(state): add failing test for nested DTO mapping with source-only Map attribute (#7801) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Reproduces the issue where nested entity→DTO mapping fails on GET when DTOs use #[Map(source:...)] without #[Map(target:...)] on entities. Requires symfony/object-mapper >= 8.1 (skipped otherwise). Co-Authored-By: Claude Opus 4.6 (1M context) --- .../Issue7801/Issue7801CategoryDto.php | 35 ++++++++++++++++ .../Issue7801/Issue7801ProductDto.php | 39 ++++++++++++++++++ .../Entity/Issue7801/Issue7801Category.php | 38 +++++++++++++++++ .../Entity/Issue7801/Issue7801Product.php | 41 +++++++++++++++++++ tests/Functional/Doctrine/StateOptionTest.php | 38 ++++++++++++++++- 5 files changed, 190 insertions(+), 1 deletion(-) create mode 100644 tests/Fixtures/TestBundle/ApiResource/Issue7801/Issue7801CategoryDto.php create mode 100644 tests/Fixtures/TestBundle/ApiResource/Issue7801/Issue7801ProductDto.php create mode 100644 tests/Fixtures/TestBundle/Entity/Issue7801/Issue7801Category.php create mode 100644 tests/Fixtures/TestBundle/Entity/Issue7801/Issue7801Product.php diff --git a/tests/Fixtures/TestBundle/ApiResource/Issue7801/Issue7801CategoryDto.php b/tests/Fixtures/TestBundle/ApiResource/Issue7801/Issue7801CategoryDto.php new file mode 100644 index 00000000000..bd98a1a04d5 --- /dev/null +++ b/tests/Fixtures/TestBundle/ApiResource/Issue7801/Issue7801CategoryDto.php @@ -0,0 +1,35 @@ + + * + * 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\Issue7801; + +use ApiPlatform\Doctrine\Orm\State\Options; +use ApiPlatform\Metadata\ApiResource; +use ApiPlatform\Metadata\Get; +use ApiPlatform\Tests\Fixtures\TestBundle\Entity\Issue7801\Issue7801Category; +use Symfony\Component\ObjectMapper\Attribute\Map; + +#[ApiResource( + operations: [ + new Get(), + ], + shortName: 'Issue7801Category', + stateOptions: new Options(entityClass: Issue7801Category::class) +)] +#[Map(source: Issue7801Category::class)] +class Issue7801CategoryDto +{ + public ?int $id = null; + + public string $name; +} diff --git a/tests/Fixtures/TestBundle/ApiResource/Issue7801/Issue7801ProductDto.php b/tests/Fixtures/TestBundle/ApiResource/Issue7801/Issue7801ProductDto.php new file mode 100644 index 00000000000..2109439a54c --- /dev/null +++ b/tests/Fixtures/TestBundle/ApiResource/Issue7801/Issue7801ProductDto.php @@ -0,0 +1,39 @@ + + * + * 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\Issue7801; + +use ApiPlatform\Doctrine\Orm\State\Options; +use ApiPlatform\Metadata\ApiResource; +use ApiPlatform\Metadata\Get; +use ApiPlatform\Metadata\GetCollection; +use ApiPlatform\Tests\Fixtures\TestBundle\Entity\Issue7801\Issue7801Product; +use Symfony\Component\ObjectMapper\Attribute\Map; + +#[ApiResource( + operations: [ + new Get(), + new GetCollection(), + ], + shortName: 'Issue7801Product', + stateOptions: new Options(entityClass: Issue7801Product::class) +)] +#[Map(source: Issue7801Product::class)] +class Issue7801ProductDto +{ + public ?int $id = null; + + public string $name; + + public ?Issue7801CategoryDto $category = null; +} diff --git a/tests/Fixtures/TestBundle/Entity/Issue7801/Issue7801Category.php b/tests/Fixtures/TestBundle/Entity/Issue7801/Issue7801Category.php new file mode 100644 index 00000000000..842a299ef09 --- /dev/null +++ b/tests/Fixtures/TestBundle/Entity/Issue7801/Issue7801Category.php @@ -0,0 +1,38 @@ + + * + * 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\Issue7801; + +use Doctrine\ORM\Mapping as ORM; + +#[ORM\Entity] +class Issue7801Category +{ + #[ORM\Id] + #[ORM\GeneratedValue] + #[ORM\Column(type: 'integer')] + private ?int $id = null; + + #[ORM\Column(type: 'string', length: 255)] + public string $name; + + public function getId(): ?int + { + return $this->id; + } + + public function setId(?int $id): void + { + $this->id = $id; + } +} diff --git a/tests/Fixtures/TestBundle/Entity/Issue7801/Issue7801Product.php b/tests/Fixtures/TestBundle/Entity/Issue7801/Issue7801Product.php new file mode 100644 index 00000000000..447d8011d9c --- /dev/null +++ b/tests/Fixtures/TestBundle/Entity/Issue7801/Issue7801Product.php @@ -0,0 +1,41 @@ + + * + * 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\Issue7801; + +use Doctrine\ORM\Mapping as ORM; + +#[ORM\Entity] +class Issue7801Product +{ + #[ORM\Id] + #[ORM\GeneratedValue] + #[ORM\Column(type: 'integer')] + private ?int $id = null; + + #[ORM\Column(type: 'string', length: 255)] + public string $name; + + #[ORM\ManyToOne(targetEntity: Issue7801Category::class)] + public ?Issue7801Category $category = null; + + public function getId(): ?int + { + return $this->id; + } + + public function setId(?int $id): void + { + $this->id = $id; + } +} diff --git a/tests/Functional/Doctrine/StateOptionTest.php b/tests/Functional/Doctrine/StateOptionTest.php index b77694aa74c..a986538be0c 100644 --- a/tests/Functional/Doctrine/StateOptionTest.php +++ b/tests/Functional/Doctrine/StateOptionTest.php @@ -17,9 +17,13 @@ use ApiPlatform\Tests\Fixtures\TestBundle\ApiResource\Issue6039\UserApi; use ApiPlatform\Tests\Fixtures\TestBundle\ApiResource\Issue7689\Issue7689CategoryDto; use ApiPlatform\Tests\Fixtures\TestBundle\ApiResource\Issue7689\Issue7689ProductDto; +use ApiPlatform\Tests\Fixtures\TestBundle\ApiResource\Issue7801\Issue7801CategoryDto; +use ApiPlatform\Tests\Fixtures\TestBundle\ApiResource\Issue7801\Issue7801ProductDto; use ApiPlatform\Tests\Fixtures\TestBundle\Entity\Issue6039\Issue6039EntityUser; use ApiPlatform\Tests\Fixtures\TestBundle\Entity\Issue7689\Issue7689Category; use ApiPlatform\Tests\Fixtures\TestBundle\Entity\Issue7689\Issue7689Product; +use ApiPlatform\Tests\Fixtures\TestBundle\Entity\Issue7801\Issue7801Category; +use ApiPlatform\Tests\Fixtures\TestBundle\Entity\Issue7801\Issue7801Product; use ApiPlatform\Tests\RecreateSchemaTrait; use ApiPlatform\Tests\SetupClassResourcesTrait; use Symfony\Component\ObjectMapper\Metadata\ReverseClassObjectMapperMetadataFactory; @@ -36,7 +40,7 @@ final class StateOptionTest extends ApiTestCase */ public static function getResources(): array { - return [UserApi::class, Issue7689ProductDto::class, Issue7689CategoryDto::class]; + return [UserApi::class, Issue7689ProductDto::class, Issue7689CategoryDto::class, Issue7801ProductDto::class, Issue7801CategoryDto::class]; } public function testDtoWithEntityClassOptionCollection(): void @@ -91,4 +95,36 @@ public function testPostWithEntityClassOption(): void $this->assertNotNull($product->category); $this->assertEquals(1, $product->category->getId()); } + + public function testGetWithNestedDtoMapping(): void + { + if ($this->isMongoDB()) { + $this->markTestSkipped('MongoDB not tested.'); + } + + if (!class_exists(ReverseClassObjectMapperMetadataFactory::class)) { + $this->markTestSkipped('This test requires symfony/object-mapper >= 8.1'); + } + + $this->recreateSchema([Issue7801Product::class, Issue7801Category::class]); + $manager = static::getContainer()->get('doctrine')->getManager(); + + $category = new Issue7801Category(); + $category->name = 'electronics'; + $manager->persist($category); + + $product = new Issue7801Product(); + $product->name = 'laptop'; + $product->category = $category; + $manager->persist($product); + $manager->flush(); + + $response = static::createClient()->request('GET', '/issue7801_products/'.$product->getId(), ['headers' => ['Accept' => 'application/ld+json']]); + $this->assertResponseStatusCodeSame(200); + + $json = $response->toArray(); + $this->assertSame('laptop', $json['name']); + $this->assertArrayHasKey('category', $json); + $this->assertSame('electronics', $json['category']['name']); + } }