From c820c440b8d3a1cb65f8c2354e7463f89f6904b3 Mon Sep 17 00:00:00 2001 From: daFish81 Date: Thu, 13 Nov 2025 10:53:33 +0100 Subject: [PATCH 1/2] test(object-mapper): add test cases for nested object mapping --- .../NestedObjectMapping/AddressDto.php | 25 ++++++++++ .../NestedObjectMapping/BankDataDto.php | 23 +++++++++ .../NestedObjectMapping/BankDataResource.php | 20 ++++++++ .../Fixtures/NestedObjectMapping/BankDto.php | 26 ++++++++++ .../NestedObjectMapping/PersonDto.php | 21 ++++++++ .../NestedObjectMapping/PersonResource.php | 19 ++++++++ .../ObjectMapper/Tests/ObjectMapperTest.php | 48 +++++++++++++++++++ 7 files changed, 182 insertions(+) create mode 100644 src/Symfony/Component/ObjectMapper/Tests/Fixtures/NestedObjectMapping/AddressDto.php create mode 100644 src/Symfony/Component/ObjectMapper/Tests/Fixtures/NestedObjectMapping/BankDataDto.php create mode 100644 src/Symfony/Component/ObjectMapper/Tests/Fixtures/NestedObjectMapping/BankDataResource.php create mode 100644 src/Symfony/Component/ObjectMapper/Tests/Fixtures/NestedObjectMapping/BankDto.php create mode 100644 src/Symfony/Component/ObjectMapper/Tests/Fixtures/NestedObjectMapping/PersonDto.php create mode 100644 src/Symfony/Component/ObjectMapper/Tests/Fixtures/NestedObjectMapping/PersonResource.php diff --git a/src/Symfony/Component/ObjectMapper/Tests/Fixtures/NestedObjectMapping/AddressDto.php b/src/Symfony/Component/ObjectMapper/Tests/Fixtures/NestedObjectMapping/AddressDto.php new file mode 100644 index 0000000000000..bf15f666bf15f --- /dev/null +++ b/src/Symfony/Component/ObjectMapper/Tests/Fixtures/NestedObjectMapping/AddressDto.php @@ -0,0 +1,25 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\ObjectMapper\Tests\Fixtures\NestedObjectMapping; + +use Symfony\Component\ObjectMapper\Attribute\Map; + +class AddressDto +{ + #[Map(target: 'streetAddress')] + public string $street; + + #[Map(target: 'city')] + public string $city; + + public string $internalCode; +} diff --git a/src/Symfony/Component/ObjectMapper/Tests/Fixtures/NestedObjectMapping/BankDataDto.php b/src/Symfony/Component/ObjectMapper/Tests/Fixtures/NestedObjectMapping/BankDataDto.php new file mode 100644 index 0000000000000..fa9522712e509 --- /dev/null +++ b/src/Symfony/Component/ObjectMapper/Tests/Fixtures/NestedObjectMapping/BankDataDto.php @@ -0,0 +1,23 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\ObjectMapper\Tests\Fixtures\NestedObjectMapping; + +use Symfony\Component\ObjectMapper\Attribute\Map; + +#[Map(target: BankDataResource::class)] +class BankDataDto +{ + #[Map(target: 'iban')] + public string $iban; + + public BankDto $bank; +} diff --git a/src/Symfony/Component/ObjectMapper/Tests/Fixtures/NestedObjectMapping/BankDataResource.php b/src/Symfony/Component/ObjectMapper/Tests/Fixtures/NestedObjectMapping/BankDataResource.php new file mode 100644 index 0000000000000..a18d15b239daa --- /dev/null +++ b/src/Symfony/Component/ObjectMapper/Tests/Fixtures/NestedObjectMapping/BankDataResource.php @@ -0,0 +1,20 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\ObjectMapper\Tests\Fixtures\NestedObjectMapping; + +class BankDataResource +{ + public string $iban; + public string $bic; + public string $bankCode; + public string $bankName; +} diff --git a/src/Symfony/Component/ObjectMapper/Tests/Fixtures/NestedObjectMapping/BankDto.php b/src/Symfony/Component/ObjectMapper/Tests/Fixtures/NestedObjectMapping/BankDto.php new file mode 100644 index 0000000000000..2831a76f227f1 --- /dev/null +++ b/src/Symfony/Component/ObjectMapper/Tests/Fixtures/NestedObjectMapping/BankDto.php @@ -0,0 +1,26 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\ObjectMapper\Tests\Fixtures\NestedObjectMapping; + +use Symfony\Component\ObjectMapper\Attribute\Map; + +class BankDto +{ + #[Map(target: 'bic')] + public string $bic; + + #[Map(target: 'bankCode')] + public string $code; + + #[Map(target: 'bankName')] + public string $name; +} diff --git a/src/Symfony/Component/ObjectMapper/Tests/Fixtures/NestedObjectMapping/PersonDto.php b/src/Symfony/Component/ObjectMapper/Tests/Fixtures/NestedObjectMapping/PersonDto.php new file mode 100644 index 0000000000000..a8a1d681fd5c6 --- /dev/null +++ b/src/Symfony/Component/ObjectMapper/Tests/Fixtures/NestedObjectMapping/PersonDto.php @@ -0,0 +1,21 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\ObjectMapper\Tests\Fixtures\NestedObjectMapping; + +use Symfony\Component\ObjectMapper\Attribute\Map; + +#[Map(target: PersonResource::class)] +class PersonDto +{ + public string $name; + public AddressDto $address; +} diff --git a/src/Symfony/Component/ObjectMapper/Tests/Fixtures/NestedObjectMapping/PersonResource.php b/src/Symfony/Component/ObjectMapper/Tests/Fixtures/NestedObjectMapping/PersonResource.php new file mode 100644 index 0000000000000..47d7cbc84edde --- /dev/null +++ b/src/Symfony/Component/ObjectMapper/Tests/Fixtures/NestedObjectMapping/PersonResource.php @@ -0,0 +1,19 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\ObjectMapper\Tests\Fixtures\NestedObjectMapping; + +class PersonResource +{ + public string $name; + public string $streetAddress; + public string $city; +} diff --git a/src/Symfony/Component/ObjectMapper/Tests/ObjectMapperTest.php b/src/Symfony/Component/ObjectMapper/Tests/ObjectMapperTest.php index 566ba7d0021a6..8fe35ac1ae6c2 100644 --- a/src/Symfony/Component/ObjectMapper/Tests/ObjectMapperTest.php +++ b/src/Symfony/Component/ObjectMapper/Tests/ObjectMapperTest.php @@ -62,6 +62,12 @@ use Symfony\Component\ObjectMapper\Tests\Fixtures\MultipleTargets\A as MultipleTargetsA; use Symfony\Component\ObjectMapper\Tests\Fixtures\MultipleTargets\C as MultipleTargetsC; use Symfony\Component\ObjectMapper\Tests\Fixtures\MyProxy; +use Symfony\Component\ObjectMapper\Tests\Fixtures\NestedObjectMapping\AddressDto; +use Symfony\Component\ObjectMapper\Tests\Fixtures\NestedObjectMapping\BankDataDto; +use Symfony\Component\ObjectMapper\Tests\Fixtures\NestedObjectMapping\BankDataResource; +use Symfony\Component\ObjectMapper\Tests\Fixtures\NestedObjectMapping\BankDto; +use Symfony\Component\ObjectMapper\Tests\Fixtures\NestedObjectMapping\PersonDto; +use Symfony\Component\ObjectMapper\Tests\Fixtures\NestedObjectMapping\PersonResource; use Symfony\Component\ObjectMapper\Tests\Fixtures\PartialInput\FinalInput; use Symfony\Component\ObjectMapper\Tests\Fixtures\PartialInput\PartialInput; use Symfony\Component\ObjectMapper\Tests\Fixtures\PromotedConstructor\Source as PromotedConstructorSource; @@ -583,4 +589,46 @@ public function testEmbedsAreLazyLoadedByDefault() $this->assertSame('Test User', $target->user->name); $this->assertFalse($refl->isUninitializedLazyObject($target->user)); } + + public function testNestedObjectMappingWithAttributes() + { + $bankDto = new BankDto(); + $bankDto->bic = 'ABCDEFGH'; + $bankDto->code = '12345678'; + $bankDto->name = 'Test Bank'; + + $bankDataDto = new BankDataDto(); + $bankDataDto->iban = 'DE89370400440532013000'; + $bankDataDto->bank = $bankDto; + + $mapper = new ObjectMapper(); + $result = $mapper->map($bankDataDto, BankDataResource::class); + + $this->assertInstanceOf(BankDataResource::class, $result); + $this->assertSame('DE89370400440532013000', $result->iban); + $this->assertSame('ABCDEFGH', $result->bic); + $this->assertSame('12345678', $result->bankCode); + $this->assertSame('Test Bank', $result->bankName); + } + + public function testNestedObjectMappingOnlyMapsMappedProperties() + { + $addressDto = new AddressDto(); + $addressDto->street = '123 Main St'; + $addressDto->city = 'Springfield'; + $addressDto->internalCode = 'INTERNAL123'; + + $personDto = new PersonDto(); + $personDto->name = 'John Doe'; + $personDto->address = $addressDto; + + $mapper = new ObjectMapper(); + $result = $mapper->map($personDto); + + $this->assertInstanceOf(PersonResource::class, $result); + $this->assertSame('John Doe', $result->name); + $this->assertSame('123 Main St', $result->streetAddress); + $this->assertSame('Springfield', $result->city); + $this->assertObjectNotHasProperty('internalCode', $result); + } } From 3794a99fc7d5f443d7823093830d67bce247a3df Mon Sep 17 00:00:00 2001 From: daFish81 Date: Thu, 13 Nov 2025 10:54:12 +0100 Subject: [PATCH 2/2] feat(object-mapper): support nested objects when mapping --- .../Component/ObjectMapper/CHANGELOG.md | 1 + .../Component/ObjectMapper/ObjectMapper.php | 59 +++++++++++++++++++ 2 files changed, 60 insertions(+) diff --git a/src/Symfony/Component/ObjectMapper/CHANGELOG.md b/src/Symfony/Component/ObjectMapper/CHANGELOG.md index ffda03ca0d8c7..c5f21e3be89b6 100644 --- a/src/Symfony/Component/ObjectMapper/CHANGELOG.md +++ b/src/Symfony/Component/ObjectMapper/CHANGELOG.md @@ -7,6 +7,7 @@ CHANGELOG * The component is not marked as `@experimental` anymore * Add `ObjectMapperAwareInterface` to set the owning object mapper instance * Add a `MapCollection` transform that calls the Mapper over iterable properties + * Add support for mapping nested objects 7.3 --- diff --git a/src/Symfony/Component/ObjectMapper/ObjectMapper.php b/src/Symfony/Component/ObjectMapper/ObjectMapper.php index bb14c9e7191b1..2efeae1e82344 100644 --- a/src/Symfony/Component/ObjectMapper/ObjectMapper.php +++ b/src/Symfony/Component/ObjectMapper/ObjectMapper.php @@ -167,6 +167,65 @@ private function doMap(object $source, object|string|null $target, \WeakMap $obj $value = $this->getSourceValue($source, $mappedTarget, $this->getRawValue($source, $propertyName), $objectMap); $this->storeValue($propertyName, $mapToProperties, $ctorArguments, $value); } + + if (!$mappings && !$targetRefl->hasProperty($propertyName)) { + $sourceProperty = $refl->getProperty($propertyName); + if ($refl->isInstance($source) && !$sourceProperty->isInitialized($source)) { + continue; + } + + try { + $value = $this->getRawValue($source, $propertyName); + } catch (NoSuchPropertyException) { + continue; + } + + if (!\is_object($value)) { + continue; + } + + try { + $nestedRefl = new \ReflectionClass($value); + } catch (\ReflectionException) { + continue; + } + + foreach ($nestedRefl->getProperties() as $nestedProperty) { + if ($nestedProperty->isStatic()) { + continue; + } + + $nestedPropertyName = $nestedProperty->getName(); + $nestedMappings = $this->metadataFactory->create($value, $nestedPropertyName); + + foreach ($nestedMappings as $nestedMapping) { + $nestedTargetPropertyName = $nestedMapping->target ?? $nestedPropertyName; + + if (!$targetRefl->hasProperty($nestedTargetPropertyName)) { + continue; + } + + if (false === $nestedMapping->if) { + continue; + } + + if (!$nestedProperty->isInitialized($value)) { + continue; + } + + $nestedValue = $this->getRawValue($value, $nestedPropertyName); + + if ($nestedMapping->if && ($fn = $this->getCallable($nestedMapping->if, $this->conditionCallableLocator))) { + if (!$this->call($fn, $nestedValue, $source, $mappedTarget)) { + continue; + } + } + + $nestedValue = $this->getSourceValue($value, $mappedTarget, $nestedValue, $objectMap, $nestedMapping); + $this->storeValue($nestedTargetPropertyName, $mapToProperties, $ctorArguments, $nestedValue); + } + } + } } if ((!$mappingToObject || !$rootCall) && !$map?->transform && $targetConstructor) {