Skip to content

Commit 9d8e927

Browse files
authored
Merge pull request #34 from rekalogika:feat/mapping-stdclass-to-existing-property
feat: Mapping to existing values in a dynamic property.
2 parents 5f787bb + 69684c2 commit 9d8e927

File tree

6 files changed

+185
-25
lines changed

6 files changed

+185
-25
lines changed

CHANGELOG.md

+1
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@
1818
* fix: Fix `PresetTransformer`.
1919
* fix: mapping to object extending `stdClass` to property with no setter.
2020
* feat: `stdClass` to `stdClass` mapping should work correctly.
21+
* feat: Mapping to existing values in a dynamic property.
2122

2223
## 1.0.0
2324

src/Transformer/Implementation/ObjectToObjectTransformer.php

+27-4
Original file line numberDiff line numberDiff line change
@@ -97,7 +97,7 @@ public function transform(
9797

9898
// if sourceType and targetType are the same, just return the source
9999

100-
if (null === $target && TypeCheck::isSomewhatIdentical($sourceType, $targetType)) {
100+
if (null === $target && TypeCheck::isSomewhatIdentical($sourceType, $targetType) && !$source instanceof \stdClass) {
101101
return $source;
102102
}
103103

@@ -579,10 +579,33 @@ private function mapDynamicProperties(
579579
foreach (get_object_vars($source) as $sourceProperty => $sourcePropertyValue) {
580580
if (!in_array($sourceProperty, $sourceProperties, true)) {
581581
try {
582-
$target->{$sourceProperty} = $sourcePropertyValue;
583-
} catch (\Error) {
584-
// ignore
582+
if (isset($target->{$sourceProperty})) {
583+
/** @psalm-suppress MixedAssignment */
584+
$currentTargetPropertyValue = $target->{$sourceProperty};
585+
} else {
586+
$currentTargetPropertyValue = null;
587+
}
588+
589+
590+
if ($currentTargetPropertyValue === null) {
591+
/** @psalm-suppress MixedAssignment */
592+
$targetPropertyValue = $sourcePropertyValue;
593+
} else {
594+
/** @var mixed */
595+
$targetPropertyValue = $this->getMainTransformer()->transform(
596+
source: $sourcePropertyValue,
597+
target: $currentTargetPropertyValue,
598+
sourceType: null,
599+
targetTypes: [],
600+
context: $context,
601+
path: $sourceProperty,
602+
);
603+
}
604+
} catch (\Throwable $e) {
605+
$targetPropertyValue = null;
585606
}
607+
608+
$target->{$sourceProperty} = $targetPropertyValue;
586609
}
587610
}
588611
}

src/Transformer/ObjectToObjectMetadata/Implementation/ObjectToObjectMetadataFactory.php

+2
Original file line numberDiff line numberDiff line change
@@ -248,6 +248,8 @@ public function createObjectToObjectMetadata(
248248
$targetWriteName = $targetProperty;
249249
$targetWriteVisibility = Visibility::Public;
250250
} else {
251+
$effectivePropertiesToMap[] = $targetProperty;
252+
251253
continue;
252254
}
253255
} else {

src/Transformer/ObjectToObjectMetadata/ObjectToObjectMetadata.php

+1-1
Original file line numberDiff line numberDiff line change
@@ -53,7 +53,7 @@
5353
* @param array<int,PropertyMapping> $allPropertyMappings
5454
* @param array<int,string> $initializableTargetPropertiesNotInSource
5555
* @param array<string,true> $targetProxySkippedProperties
56-
* @param array<int,string> $sourceProperties
56+
* @param array<int,string> $sourceProperties List of the source properties. Used by `ObjectToObjectTransformer` to determine if a property is a dynamic property. A property not listed here is considered dynamic.
5757
*/
5858
public function __construct(
5959
private string $sourceClass,
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,70 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
/*
6+
* This file is part of rekalogika/mapper package.
7+
*
8+
* (c) Priyadi Iman Nurcahyo <https://rekalogika.dev>
9+
*
10+
* For the full copyright and license information, please view the LICENSE file
11+
* that was distributed with this source code.
12+
*/
13+
14+
namespace Rekalogika\Mapper\Tests\Fixtures\DynamicProperty;
15+
16+
class ObjectExtendingStdClassWithExplicitScalarProperties extends \stdClass
17+
{
18+
private int $a = 1;
19+
private string $b = 'string';
20+
private bool $c = true;
21+
private float $d = 1.1;
22+
23+
public function getA(): int
24+
{
25+
return $this->a;
26+
}
27+
28+
public function setA(int $a): self
29+
{
30+
$this->a = $a;
31+
32+
return $this;
33+
}
34+
35+
public function getB(): string
36+
{
37+
return $this->b;
38+
}
39+
40+
public function setB(string $b): self
41+
{
42+
$this->b = $b;
43+
44+
return $this;
45+
}
46+
47+
public function isC(): bool
48+
{
49+
return $this->c;
50+
}
51+
52+
public function setC(bool $c): self
53+
{
54+
$this->c = $c;
55+
56+
return $this;
57+
}
58+
59+
public function getD(): float
60+
{
61+
return $this->d;
62+
}
63+
64+
public function setD(float $d): self
65+
{
66+
$this->d = $d;
67+
68+
return $this;
69+
}
70+
}

tests/IntegrationTest/DynamicPropertyTest.php

+84-20
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@
1616
use Rekalogika\Mapper\Tests\Common\FrameworkTestCase;
1717
use Rekalogika\Mapper\Tests\Fixtures\DynamicProperty\AnotherObjectExtendingStdClass;
1818
use Rekalogika\Mapper\Tests\Fixtures\DynamicProperty\ObjectExtendingStdClass;
19+
use Rekalogika\Mapper\Tests\Fixtures\DynamicProperty\ObjectExtendingStdClassWithExplicitScalarProperties;
1920
use Rekalogika\Mapper\Tests\Fixtures\DynamicProperty\ObjectExtendingStdClassWithProperties;
2021
use Rekalogika\Mapper\Tests\Fixtures\Scalar\ObjectWithScalarProperties;
2122
use Rekalogika\Mapper\Tests\Fixtures\ScalarDto\ObjectWithScalarPropertiesDto;
@@ -35,10 +36,10 @@ public function testStdClassToObject(): void
3536
$target = $this->mapper->map($source, ObjectWithScalarPropertiesDto::class);
3637

3738
$this->assertInstanceOf(ObjectWithScalarPropertiesDto::class, $target);
38-
$this->assertSame(1, $target->a);
39-
$this->assertSame('string', $target->b);
39+
$this->assertEquals(1, $target->a);
40+
$this->assertEquals('string', $target->b);
4041
$this->assertTrue($target->c);
41-
$this->assertSame(1.1, $target->d);
42+
$this->assertEquals(1.1, $target->d);
4243
}
4344

4445
public function testObjectExtendingStdClassToObject(): void
@@ -56,10 +57,10 @@ public function testObjectExtendingStdClassToObject(): void
5657
$target = $this->mapper->map($source, ObjectWithScalarPropertiesDto::class);
5758

5859
$this->assertInstanceOf(ObjectWithScalarPropertiesDto::class, $target);
59-
$this->assertSame(1, $target->a);
60-
$this->assertSame('string', $target->b);
60+
$this->assertEquals(1, $target->a);
61+
$this->assertEquals('string', $target->b);
6162
$this->assertTrue($target->c);
62-
$this->assertSame(1.1, $target->d);
63+
$this->assertEquals(1.1, $target->d);
6364
}
6465

6566
public function testArrayCastToObjectToObject(): void
@@ -74,10 +75,10 @@ public function testArrayCastToObjectToObject(): void
7475
$target = $this->mapper->map((object) $source, ObjectWithScalarPropertiesDto::class);
7576

7677
$this->assertInstanceOf(ObjectWithScalarPropertiesDto::class, $target);
77-
$this->assertSame(1, $target->a);
78-
$this->assertSame('string', $target->b);
78+
$this->assertEquals(1, $target->a);
79+
$this->assertEquals('string', $target->b);
7980
$this->assertTrue($target->c);
80-
$this->assertSame(1.1, $target->d);
81+
$this->assertEquals(1.1, $target->d);
8182
}
8283

8384
public function testStdClassWithoutPropertiesToObject(): void
@@ -92,6 +93,38 @@ public function testStdClassWithoutPropertiesToObject(): void
9293
$this->assertNull($target->d);
9394
}
9495

96+
public function testStdClassWithExtraPropertyToObject(): void
97+
{
98+
$source = new \stdClass();
99+
$source->a = 1;
100+
$source->b = 'string';
101+
$source->c = true;
102+
$source->d = 1.1;
103+
$source->e = 'extra';
104+
105+
$target = $this->mapper->map($source, ObjectWithScalarPropertiesDto::class);
106+
107+
$this->assertInstanceOf(ObjectWithScalarPropertiesDto::class, $target);
108+
$this->assertEquals(1, $target->a);
109+
$this->assertEquals('string', $target->b);
110+
$this->assertTrue($target->c);
111+
$this->assertEquals(1.1, $target->d);
112+
}
113+
114+
public function testObjectExtendingStdClassWithExplicitScalarPropertiesToObject(): void
115+
{
116+
$source = new ObjectExtendingStdClassWithExplicitScalarProperties();
117+
/** @psalm-suppress UndefinedPropertyAssignment */
118+
$source->e = 'extra';
119+
$target = $this->mapper->map($source, ObjectWithScalarPropertiesDto::class);
120+
121+
$this->assertInstanceOf(ObjectWithScalarPropertiesDto::class, $target);
122+
$this->assertEquals(1, $target->a);
123+
$this->assertEquals('string', $target->b);
124+
$this->assertTrue($target->c);
125+
$this->assertEquals(1.1, $target->d);
126+
}
127+
95128
// to stdClass
96129

97130
public function testObjectToStdClass(): void
@@ -101,10 +134,10 @@ public function testObjectToStdClass(): void
101134

102135
$this->assertInstanceOf(\stdClass::class, $target);
103136

104-
$this->assertSame(1, $target->a);
105-
$this->assertSame('string', $target->b);
137+
$this->assertEquals(1, $target->a);
138+
$this->assertEquals('string', $target->b);
106139
$this->assertTrue($target->c);
107-
$this->assertSame(1.1, $target->d);
140+
$this->assertEquals(1.1, $target->d);
108141
}
109142

110143
public function testObjectToObjectExtendingStdClass(): void
@@ -115,13 +148,13 @@ public function testObjectToObjectExtendingStdClass(): void
115148
$this->assertInstanceOf(ObjectExtendingStdClass::class, $target);
116149

117150
/** @psalm-suppress UndefinedPropertyFetch */
118-
$this->assertSame(1, $target->a);
151+
$this->assertEquals(1, $target->a);
119152
/** @psalm-suppress UndefinedPropertyFetch */
120-
$this->assertSame('string', $target->b);
153+
$this->assertEquals('string', $target->b);
121154
/** @psalm-suppress UndefinedPropertyFetch */
122155
$this->assertTrue($target->c);
123156
/** @psalm-suppress UndefinedPropertyFetch */
124-
$this->assertSame(1.1, $target->d);
157+
$this->assertEquals(1.1, $target->d);
125158
}
126159

127160
// stdclass to stdclass
@@ -142,13 +175,13 @@ public function testStdClassToStdClass(): void
142175

143176
$this->assertInstanceOf(\stdClass::class, $target);
144177
/** @psalm-suppress UndefinedPropertyFetch */
145-
$this->assertSame(1, $target->a);
178+
$this->assertEquals(1, $target->a);
146179
/** @psalm-suppress UndefinedPropertyFetch */
147-
$this->assertSame('string', $target->b);
180+
$this->assertEquals('string', $target->b);
148181
/** @psalm-suppress UndefinedPropertyFetch */
149182
$this->assertTrue($target->c);
150183
/** @psalm-suppress UndefinedPropertyFetch */
151-
$this->assertSame(1.1, $target->d);
184+
$this->assertEquals(1.1, $target->d);
152185
}
153186

154187
public function testStdClassToStdClassWithExplicitProperties(): void
@@ -162,10 +195,41 @@ public function testStdClassToStdClassWithExplicitProperties(): void
162195
$target = $this->mapper->map($source, ObjectExtendingStdClassWithProperties::class);
163196

164197
$this->assertInstanceOf(\stdClass::class, $target);
165-
$this->assertSame('public', $target->public);
198+
$this->assertEquals('public', $target->public);
166199
$this->assertNull($target->getPrivate());
167200
$this->assertEquals('constructor', $target->getConstructor());
168201
/** @psalm-suppress UndefinedPropertyFetch */
169-
$this->assertSame('dynamic', $target->dynamic);
202+
$this->assertEquals('dynamic', $target->dynamic);
203+
}
204+
205+
public function testStdClassToStdClassWithExistingValue(): void
206+
{
207+
$source = new \stdClass();
208+
$source->property = new ObjectWithScalarProperties();
209+
210+
$target = new \stdClass();
211+
$targetProperty = new ObjectWithScalarPropertiesDto();
212+
$target->property = $targetProperty;
213+
214+
$this->mapper->map($source, $target);
215+
216+
$this->assertSame($targetProperty, $target->property);
217+
}
218+
219+
public function testStdClassToStdClassWithExistingNullValue(): void
220+
{
221+
$source = new \stdClass();
222+
$source->property = new ObjectWithScalarProperties();
223+
224+
$target = new \stdClass();
225+
$target->property = null;
226+
227+
$this->mapper->map($source, $target);
228+
229+
/**
230+
* @psalm-suppress TypeDoesNotContainType
231+
* @phpstan-ignore-next-line
232+
*/
233+
$this->assertInstanceOf(ObjectWithScalarProperties::class, $target->property);
170234
}
171235
}

0 commit comments

Comments
 (0)