Skip to content

Commit 3b71652

Browse files
committed
feat(object-mapper): support nested objects when mapping
1 parent 6790e1b commit 3b71652

File tree

9 files changed

+242
-0
lines changed

9 files changed

+242
-0
lines changed

src/Symfony/Component/ObjectMapper/CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ CHANGELOG
77
* The component is not marked as `@experimental` anymore
88
* Add `ObjectMapperAwareInterface` to set the owning object mapper instance
99
* Add a `MapCollection` transform that calls the Mapper over iterable properties
10+
* Add support for mapping nested objects
1011

1112
7.3
1213
---

src/Symfony/Component/ObjectMapper/ObjectMapper.php

Lines changed: 59 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -167,6 +167,65 @@ private function doMap(object $source, object|string|null $target, \WeakMap $obj
167167
$value = $this->getSourceValue($source, $mappedTarget, $this->getRawValue($source, $propertyName), $objectMap);
168168
$this->storeValue($propertyName, $mapToProperties, $ctorArguments, $value);
169169
}
170+
171+
if (!$mappings && !$targetRefl->hasProperty($propertyName)) {
172+
$sourceProperty = $refl->getProperty($propertyName);
173+
if ($refl->isInstance($source) && !$sourceProperty->isInitialized($source)) {
174+
continue;
175+
}
176+
177+
try {
178+
$value = $this->getRawValue($source, $propertyName);
179+
} catch (NoSuchPropertyException) {
180+
continue;
181+
}
182+
183+
if (!\is_object($value)) {
184+
continue;
185+
}
186+
187+
try {
188+
$nestedRefl = new \ReflectionClass($value);
189+
} catch (\ReflectionException) {
190+
continue;
191+
}
192+
193+
foreach ($nestedRefl->getProperties() as $nestedProperty) {
194+
if ($nestedProperty->isStatic()) {
195+
continue;
196+
}
197+
198+
$nestedPropertyName = $nestedProperty->getName();
199+
$nestedMappings = $this->metadataFactory->create($value, $nestedPropertyName);
200+
201+
foreach ($nestedMappings as $nestedMapping) {
202+
$nestedTargetPropertyName = $nestedMapping->target ?? $nestedPropertyName;
203+
204+
if (!$targetRefl->hasProperty($nestedTargetPropertyName)) {
205+
continue;
206+
}
207+
208+
if (false === $nestedMapping->if) {
209+
continue;
210+
}
211+
212+
if (!$nestedProperty->isInitialized($value)) {
213+
continue;
214+
}
215+
216+
$nestedValue = $this->getRawValue($value, $nestedPropertyName);
217+
218+
if ($nestedMapping->if && ($fn = $this->getCallable($nestedMapping->if, $this->conditionCallableLocator))) {
219+
if (!$this->call($fn, $nestedValue, $source, $mappedTarget)) {
220+
continue;
221+
}
222+
}
223+
224+
$nestedValue = $this->getSourceValue($value, $mappedTarget, $nestedValue, $objectMap, $nestedMapping);
225+
$this->storeValue($nestedTargetPropertyName, $mapToProperties, $ctorArguments, $nestedValue);
226+
}
227+
}
228+
}
170229
}
171230

172231
if ((!$mappingToObject || !$rootCall) && !$map?->transform && $targetConstructor) {
Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
<?php
2+
3+
/*
4+
* This file is part of the Symfony package.
5+
*
6+
* (c) Fabien Potencier <fabien@symfony.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+
namespace Symfony\Component\ObjectMapper\Tests\Fixtures\NestedObjectMapping;
13+
14+
use Symfony\Component\ObjectMapper\Attribute\Map;
15+
16+
class AddressDto
17+
{
18+
#[Map(target: 'streetAddress')]
19+
public string $street;
20+
21+
#[Map(target: 'city')]
22+
public string $city;
23+
24+
public string $internalCode;
25+
}
Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
<?php
2+
3+
/*
4+
* This file is part of the Symfony package.
5+
*
6+
* (c) Fabien Potencier <fabien@symfony.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+
namespace Symfony\Component\ObjectMapper\Tests\Fixtures\NestedObjectMapping;
13+
14+
use Symfony\Component\ObjectMapper\Attribute\Map;
15+
16+
#[Map(target: BankDataResource::class)]
17+
class BankDataDto
18+
{
19+
#[Map(target: 'iban')]
20+
public string $iban;
21+
22+
public BankDto $bank;
23+
}
Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
<?php
2+
3+
/*
4+
* This file is part of the Symfony package.
5+
*
6+
* (c) Fabien Potencier <fabien@symfony.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+
namespace Symfony\Component\ObjectMapper\Tests\Fixtures\NestedObjectMapping;
13+
14+
class BankDataResource
15+
{
16+
public string $iban;
17+
public string $bic;
18+
public string $bankCode;
19+
public string $bankName;
20+
}
Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
<?php
2+
3+
/*
4+
* This file is part of the Symfony package.
5+
*
6+
* (c) Fabien Potencier <fabien@symfony.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+
namespace Symfony\Component\ObjectMapper\Tests\Fixtures\NestedObjectMapping;
13+
14+
use Symfony\Component\ObjectMapper\Attribute\Map;
15+
16+
class BankDto
17+
{
18+
#[Map(target: 'bic')]
19+
public string $bic;
20+
21+
#[Map(target: 'bankCode')]
22+
public string $code;
23+
24+
#[Map(target: 'bankName')]
25+
public string $name;
26+
}
Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
<?php
2+
3+
/*
4+
* This file is part of the Symfony package.
5+
*
6+
* (c) Fabien Potencier <fabien@symfony.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+
namespace Symfony\Component\ObjectMapper\Tests\Fixtures\NestedObjectMapping;
13+
14+
use Symfony\Component\ObjectMapper\Attribute\Map;
15+
16+
#[Map(target: PersonResource::class)]
17+
class PersonDto
18+
{
19+
public string $name;
20+
public AddressDto $address;
21+
}
Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
<?php
2+
3+
/*
4+
* This file is part of the Symfony package.
5+
*
6+
* (c) Fabien Potencier <fabien@symfony.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+
namespace Symfony\Component\ObjectMapper\Tests\Fixtures\NestedObjectMapping;
13+
14+
class PersonResource
15+
{
16+
public string $name;
17+
public string $streetAddress;
18+
public string $city;
19+
}

src/Symfony/Component/ObjectMapper/Tests/ObjectMapperTest.php

Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -62,6 +62,12 @@
6262
use Symfony\Component\ObjectMapper\Tests\Fixtures\MultipleTargets\A as MultipleTargetsA;
6363
use Symfony\Component\ObjectMapper\Tests\Fixtures\MultipleTargets\C as MultipleTargetsC;
6464
use Symfony\Component\ObjectMapper\Tests\Fixtures\MyProxy;
65+
use Symfony\Component\ObjectMapper\Tests\Fixtures\NestedObjectMapping\AddressDto;
66+
use Symfony\Component\ObjectMapper\Tests\Fixtures\NestedObjectMapping\BankDataDto;
67+
use Symfony\Component\ObjectMapper\Tests\Fixtures\NestedObjectMapping\BankDataResource;
68+
use Symfony\Component\ObjectMapper\Tests\Fixtures\NestedObjectMapping\BankDto;
69+
use Symfony\Component\ObjectMapper\Tests\Fixtures\NestedObjectMapping\PersonDto;
70+
use Symfony\Component\ObjectMapper\Tests\Fixtures\NestedObjectMapping\PersonResource;
6571
use Symfony\Component\ObjectMapper\Tests\Fixtures\PartialInput\FinalInput;
6672
use Symfony\Component\ObjectMapper\Tests\Fixtures\PartialInput\PartialInput;
6773
use Symfony\Component\ObjectMapper\Tests\Fixtures\PromotedConstructor\Source as PromotedConstructorSource;
@@ -583,4 +589,46 @@ public function testEmbedsAreLazyLoadedByDefault()
583589
$this->assertSame('Test User', $target->user->name);
584590
$this->assertFalse($refl->isUninitializedLazyObject($target->user));
585591
}
592+
593+
public function testNestedObjectMappingWithAttributes()
594+
{
595+
$bankDto = new BankDto();
596+
$bankDto->bic = 'ABCDEFGH';
597+
$bankDto->code = '12345678';
598+
$bankDto->name = 'Test Bank';
599+
600+
$bankDataDto = new BankDataDto();
601+
$bankDataDto->iban = 'DE89370400440532013000';
602+
$bankDataDto->bank = $bankDto;
603+
604+
$mapper = new ObjectMapper();
605+
$result = $mapper->map($bankDataDto, BankDataResource::class);
606+
607+
$this->assertInstanceOf(BankDataResource::class, $result);
608+
$this->assertSame('DE89370400440532013000', $result->iban);
609+
$this->assertSame('ABCDEFGH', $result->bic);
610+
$this->assertSame('12345678', $result->bankCode);
611+
$this->assertSame('Test Bank', $result->bankName);
612+
}
613+
614+
public function testNestedObjectMappingOnlyMapsMappedProperties()
615+
{
616+
$addressDto = new AddressDto();
617+
$addressDto->street = '123 Main St';
618+
$addressDto->city = 'Springfield';
619+
$addressDto->internalCode = 'INTERNAL123';
620+
621+
$personDto = new PersonDto();
622+
$personDto->name = 'John Doe';
623+
$personDto->address = $addressDto;
624+
625+
$mapper = new ObjectMapper();
626+
$result = $mapper->map($personDto);
627+
628+
$this->assertInstanceOf(PersonResource::class, $result);
629+
$this->assertSame('John Doe', $result->name);
630+
$this->assertSame('123 Main St', $result->streetAddress);
631+
$this->assertSame('Springfield', $result->city);
632+
$this->assertObjectNotHasProperty('internalCode', $result);
633+
}
586634
}

0 commit comments

Comments
 (0)