Skip to content

Commit 50cd48c

Browse files
bug #61310 [ObjectMapper] read source metadata before transform (soyuka, rvanlaak)
This PR was merged into the 7.3 branch. Discussion ---------- [ObjectMapper] read source metadata before transform | Q | A | ------------- | --- | Branch? | 7.3 | Bug fix? | yes | New feature? | no | Deprecations? | no | Issues | fixes #61027 | License | MIT Commits ------- c1e4adf [ObjectMapper] do not require mapping a target's required promoted property when not on source (#2) 50e177d [ObjectMapper] read source metadata before transform
2 parents abdd9e1 + c1e4adf commit 50cd48c

File tree

6 files changed

+143
-7
lines changed

6 files changed

+143
-7
lines changed

src/Symfony/Component/ObjectMapper/ObjectMapper.php

Lines changed: 16 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -72,6 +72,12 @@ public function map(object $source, object|string|null $target = null): object
7272
}
7373

7474
$mappedTarget = $mappingToObject ? $target : $targetRefl->newInstanceWithoutConstructor();
75+
76+
if (!$metadata && $targetMetadata = $this->metadataFactory->create($mappedTarget)) {
77+
$metadata = $targetMetadata;
78+
$map = $this->getMapTarget($metadata, null, $source, null);
79+
}
80+
7581
if ($map && $map->transform) {
7682
$mappedTarget = $this->applyTransforms($map, $mappedTarget, $source, null);
7783

@@ -104,7 +110,7 @@ public function map(object $source, object|string|null $target = null): object
104110
}
105111

106112
$readMetadataFrom = $source;
107-
$refl = $this->getSourceReflectionClass($source, $targetRefl);
113+
$refl = $this->getSourceReflectionClass($source) ?? $targetRefl;
108114

109115
// When source contains no metadata, we read metadata on the target instead
110116
if ($refl === $targetRefl) {
@@ -164,7 +170,7 @@ public function map(object $source, object|string|null $target = null): object
164170

165171
if ($mappingToObject && $ctorArguments) {
166172
foreach ($ctorArguments as $property => $value) {
167-
if ($targetRefl->hasProperty($property) && $targetRefl->getProperty($property)->isPublic()) {
173+
if ($this->propertyIsMappable($refl, $property) && $this->propertyIsMappable($targetRefl, $property)) {
168174
$mapToProperties[$property] = $value;
169175
}
170176
}
@@ -308,11 +314,9 @@ private function getCallable(string|callable $fn, ?ContainerInterface $locator =
308314
}
309315

310316
/**
311-
* @param \ReflectionClass<object> $targetRefl
312-
*
313-
* @return \ReflectionClass<object|T>
317+
* @return ?\ReflectionClass<object|T>
314318
*/
315-
private function getSourceReflectionClass(object $source, \ReflectionClass $targetRefl): \ReflectionClass
319+
private function getSourceReflectionClass(object $source): ?\ReflectionClass
316320
{
317321
$metadata = $this->metadataFactory->create($source);
318322
try {
@@ -337,6 +341,11 @@ private function getSourceReflectionClass(object $source, \ReflectionClass $targ
337341
}
338342
}
339343

340-
return $targetRefl;
344+
return null;
345+
}
346+
347+
private function propertyIsMappable(\ReflectionClass $targetRefl, int|string $property): bool
348+
{
349+
return $targetRefl->hasProperty($property) && $targetRefl->getProperty($property)->isPublic();
341350
}
342351
}
Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
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\PromotedConstructorWithMetadata;
13+
14+
use Symfony\Component\ObjectMapper\Tests\Fixtures\MapStruct\Map;
15+
16+
#[Map(target: Target::class)]
17+
class Source
18+
{
19+
public function __construct(
20+
public int $number,
21+
public string $name,
22+
) {
23+
}
24+
}
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\PromotedConstructorWithMetadata;
13+
14+
class Target
15+
{
16+
public function __construct(
17+
/**
18+
* This promoted property is required but should not lead to an exception on the object mapping as instantiation
19+
* happened earlier already.
20+
*/
21+
public string $notOnSourceButRequired,
22+
public int $number,
23+
public string $name,
24+
) {
25+
}
26+
}
Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
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\TargetTransform;
13+
14+
class SourceEntity
15+
{
16+
public string $name;
17+
}
Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
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\TargetTransform;
13+
14+
use Symfony\Component\ObjectMapper\Attribute\Map;
15+
16+
#[Map(source: SourceEntity::class, transform: [self::class, 't'])]
17+
class TargetDto
18+
{
19+
#[Map(if: false)]
20+
public bool $transformed;
21+
public string $name;
22+
23+
public static function t(mixed $value, object $source, ?object $target)
24+
{
25+
$value->transformed = true;
26+
27+
return $value;
28+
}
29+
}

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

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -58,12 +58,16 @@
5858
use Symfony\Component\ObjectMapper\Tests\Fixtures\PartialInput\PartialInput;
5959
use Symfony\Component\ObjectMapper\Tests\Fixtures\PromotedConstructor\Source as PromotedConstructorSource;
6060
use Symfony\Component\ObjectMapper\Tests\Fixtures\PromotedConstructor\Target as PromotedConstructorTarget;
61+
use Symfony\Component\ObjectMapper\Tests\Fixtures\PromotedConstructorWithMetadata\Source as PromotedConstructorWithMetadataSource;
62+
use Symfony\Component\ObjectMapper\Tests\Fixtures\PromotedConstructorWithMetadata\Target as PromotedConstructorWithMetadataTarget;
6163
use Symfony\Component\ObjectMapper\Tests\Fixtures\Recursion\AB;
6264
use Symfony\Component\ObjectMapper\Tests\Fixtures\Recursion\Dto;
6365
use Symfony\Component\ObjectMapper\Tests\Fixtures\ServiceLocator\A as ServiceLocatorA;
6466
use Symfony\Component\ObjectMapper\Tests\Fixtures\ServiceLocator\B as ServiceLocatorB;
6567
use Symfony\Component\ObjectMapper\Tests\Fixtures\ServiceLocator\ConditionCallable;
6668
use Symfony\Component\ObjectMapper\Tests\Fixtures\ServiceLocator\TransformCallable;
69+
use Symfony\Component\ObjectMapper\Tests\Fixtures\TargetTransform\SourceEntity;
70+
use Symfony\Component\ObjectMapper\Tests\Fixtures\TargetTransform\TargetDto as TargetTransformTargetDto;
6771
use Symfony\Component\PropertyAccess\PropertyAccess;
6872

6973
final class ObjectMapperTest extends TestCase
@@ -364,6 +368,20 @@ public function testUpdateObjectWithConstructorPromotedProperties(ObjectMapperIn
364368
$this->assertSame($v->name, 'foo');
365369
}
366370

371+
/**
372+
* @dataProvider objectMapperProvider
373+
*/
374+
public function testUpdateMappedObjectWithAdditionalConstructorPromotedProperties(ObjectMapperInterface $mapper)
375+
{
376+
$a = new PromotedConstructorWithMetadataSource(3, 'foo-will-get-updated');
377+
$b = new PromotedConstructorWithMetadataTarget('notOnSourceButRequired', 1, 'bar');
378+
379+
$v = $mapper->map($a, $b);
380+
381+
$this->assertSame($v->name, $a->name);
382+
$this->assertSame($v->number, $a->number);
383+
}
384+
367385
/**
368386
* @return iterable<array{0: ObjectMapperInterface}>
369387
*/
@@ -447,4 +465,17 @@ public static function validPartialInputProvider(): iterable
447465

448466
yield [$p, $f];
449467
}
468+
469+
public function testMapWithSourceTransform()
470+
{
471+
$source = new SourceEntity();
472+
$source->name = 'test';
473+
474+
$mapper = new ObjectMapper();
475+
$target = $mapper->map($source, TargetTransformTargetDto::class);
476+
477+
$this->assertInstanceOf(TargetTransformTargetDto::class, $target);
478+
$this->assertTrue($target->transformed);
479+
$this->assertSame('test', $target->name);
480+
}
450481
}

0 commit comments

Comments
 (0)