Skip to content

Commit 62752b8

Browse files
rvanlaakfabpot
authored andcommitted
[ObjectMapper] Preserve non-promoted constructor parameters
1 parent a11e768 commit 62752b8

File tree

4 files changed

+106
-12
lines changed

4 files changed

+106
-12
lines changed

ObjectMapper.php

Lines changed: 27 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -92,21 +92,23 @@ public function map(object $source, object|string|null $target = null): object
9292

9393
$this->objectMap[$source] = $mappedTarget;
9494
$ctorArguments = [];
95-
$constructor = $targetRefl->getConstructor();
96-
foreach ($constructor?->getParameters() ?? [] as $parameter) {
97-
if (!$parameter->isPromoted()) {
98-
continue;
99-
}
100-
95+
$targetConstructor = $targetRefl->getConstructor();
96+
foreach ($targetConstructor?->getParameters() ?? [] as $parameter) {
10197
$parameterName = $parameter->getName();
102-
$property = $targetRefl->getProperty($parameterName);
10398

104-
if ($property->isReadOnly() && $property->isInitialized($mappedTarget)) {
105-
continue;
99+
if ($targetRefl->hasProperty($parameterName)) {
100+
$property = $targetRefl->getProperty($parameterName);
101+
102+
if ($property->isReadOnly() && $property->isInitialized($mappedTarget)) {
103+
continue;
104+
}
106105
}
107106

108-
// this may be filled later on see storeValue
109-
$ctorArguments[$parameterName] = $parameter->isDefaultValueAvailable() ? $parameter->getDefaultValue() : null;
107+
if ($this->isReadable($source, $parameterName)) {
108+
$ctorArguments[$parameterName] = $this->getRawValue($source, $parameterName);
109+
} else {
110+
$ctorArguments[$parameterName] = $parameter->isDefaultValueAvailable() ? $parameter->getDefaultValue() : null;
111+
}
110112
}
111113

112114
$readMetadataFrom = $source;
@@ -160,7 +162,7 @@ public function map(object $source, object|string|null $target = null): object
160162
}
161163
}
162164

163-
if (!$mappingToObject && !$map?->transform && $constructor) {
165+
if (!$mappingToObject && !$map?->transform && $targetConstructor) {
164166
try {
165167
$mappedTarget->__construct(...$ctorArguments);
166168
} catch (\ReflectionException $e) {
@@ -187,6 +189,19 @@ public function map(object $source, object|string|null $target = null): object
187189
return $mappedTarget;
188190
}
189191

192+
private function isReadable(object $source, string $propertyName): bool
193+
{
194+
if ($this->propertyAccessor) {
195+
return $this->propertyAccessor->isReadable($source, $propertyName);
196+
}
197+
198+
if (!property_exists($source, $propertyName) && !isset($source->{$propertyName})) {
199+
return false;
200+
}
201+
202+
return true;
203+
}
204+
190205
private function getRawValue(object $source, string $propertyName): mixed
191206
{
192207
if ($this->propertyAccessor) {
Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
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\InitializedConstructor;
13+
14+
class C
15+
{
16+
public string $bar;
17+
18+
public function __construct(string $bar)
19+
{
20+
$this->bar = $bar;
21+
}
22+
}
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\InitializedConstructor;
13+
14+
use Symfony\Component\ObjectMapper\Attribute\Map;
15+
16+
class D
17+
{
18+
#[Map(if: false)]
19+
public string $barUpperCase;
20+
21+
public function __construct(string $bar)
22+
{
23+
$this->barUpperCase = strtoupper($bar);
24+
}
25+
}

Tests/ObjectMapperTest.php

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -37,6 +37,8 @@
3737
use Symfony\Component\ObjectMapper\Tests\Fixtures\HydrateObject\SourceOnly;
3838
use Symfony\Component\ObjectMapper\Tests\Fixtures\InitializedConstructor\A as InitializedConstructorA;
3939
use Symfony\Component\ObjectMapper\Tests\Fixtures\InitializedConstructor\B as InitializedConstructorB;
40+
use Symfony\Component\ObjectMapper\Tests\Fixtures\InitializedConstructor\C as InitializedConstructorC;
41+
use Symfony\Component\ObjectMapper\Tests\Fixtures\InitializedConstructor\D as InitializedConstructorD;
4042
use Symfony\Component\ObjectMapper\Tests\Fixtures\InstanceCallback\A as InstanceCallbackA;
4143
use Symfony\Component\ObjectMapper\Tests\Fixtures\InstanceCallback\B as InstanceCallbackB;
4244
use Symfony\Component\ObjectMapper\Tests\Fixtures\InstanceCallbackWithArguments\A as InstanceCallbackWithArgumentsA;
@@ -171,6 +173,36 @@ public function testMapWithInitializedConstructor()
171173
$this->assertEquals($b->tags, ['foo', 'bar']);
172174
}
173175

176+
public function testMapReliesOnConstructorsOwnInitialization()
177+
{
178+
$expected = 'bar';
179+
180+
$mapper = new ObjectMapper(propertyAccessor: PropertyAccess::createPropertyAccessor());
181+
182+
$source = new \stdClass();
183+
$source->bar = $expected;
184+
185+
$c = $mapper->map($source, InitializedConstructorC::class);
186+
187+
$this->assertInstanceOf(InitializedConstructorC::class, $c);
188+
$this->assertEquals($expected, $c->bar);
189+
}
190+
191+
public function testMapConstructorArgumentsDifferFromClassFields()
192+
{
193+
$expected = 'bar';
194+
195+
$mapper = new ObjectMapper(propertyAccessor: PropertyAccess::createPropertyAccessor());
196+
197+
$source = new \stdClass();
198+
$source->bar = $expected;
199+
200+
$actual = $mapper->map($source, InitializedConstructorD::class);
201+
202+
$this->assertInstanceOf(InitializedConstructorD::class, $actual);
203+
$this->assertStringContainsStringIgnoringCase($expected, $actual->barUpperCase);
204+
}
205+
174206
public function testMapToWithInstanceHook()
175207
{
176208
$a = new InstanceCallbackA();

0 commit comments

Comments
 (0)