From 69684c211e60ce29f972b2338b619fdbcfa7e415 Mon Sep 17 00:00:00 2001 From: Priyadi Iman Nurcahyo <1102197+priyadi@users.noreply.github.com> Date: Wed, 21 Feb 2024 12:21:43 +0700 Subject: [PATCH] feat: Mapping to existing values in a dynamic property. --- CHANGELOG.md | 1 + .../ObjectToObjectTransformer.php | 31 +++++- .../ObjectToObjectMetadataFactory.php | 2 + .../ObjectToObjectMetadata.php | 2 +- ...ngStdClassWithExplicitScalarProperties.php | 70 ++++++++++++ tests/IntegrationTest/DynamicPropertyTest.php | 104 ++++++++++++++---- 6 files changed, 185 insertions(+), 25 deletions(-) create mode 100644 tests/Fixtures/DynamicProperty/ObjectExtendingStdClassWithExplicitScalarProperties.php diff --git a/CHANGELOG.md b/CHANGELOG.md index 5e7e5a1..303a52b 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -18,6 +18,7 @@ * fix: Fix `PresetTransformer`. * fix: mapping to object extending `stdClass` to property with no setter. * feat: `stdClass` to `stdClass` mapping should work correctly. +* feat: Mapping to existing values in a dynamic property. ## 1.0.0 diff --git a/src/Transformer/Implementation/ObjectToObjectTransformer.php b/src/Transformer/Implementation/ObjectToObjectTransformer.php index 3835a76..ff9aaf6 100644 --- a/src/Transformer/Implementation/ObjectToObjectTransformer.php +++ b/src/Transformer/Implementation/ObjectToObjectTransformer.php @@ -97,7 +97,7 @@ public function transform( // if sourceType and targetType are the same, just return the source - if (null === $target && TypeCheck::isSomewhatIdentical($sourceType, $targetType)) { + if (null === $target && TypeCheck::isSomewhatIdentical($sourceType, $targetType) && !$source instanceof \stdClass) { return $source; } @@ -579,10 +579,33 @@ private function mapDynamicProperties( foreach (get_object_vars($source) as $sourceProperty => $sourcePropertyValue) { if (!in_array($sourceProperty, $sourceProperties, true)) { try { - $target->{$sourceProperty} = $sourcePropertyValue; - } catch (\Error) { - // ignore + if (isset($target->{$sourceProperty})) { + /** @psalm-suppress MixedAssignment */ + $currentTargetPropertyValue = $target->{$sourceProperty}; + } else { + $currentTargetPropertyValue = null; + } + + + if ($currentTargetPropertyValue === null) { + /** @psalm-suppress MixedAssignment */ + $targetPropertyValue = $sourcePropertyValue; + } else { + /** @var mixed */ + $targetPropertyValue = $this->getMainTransformer()->transform( + source: $sourcePropertyValue, + target: $currentTargetPropertyValue, + sourceType: null, + targetTypes: [], + context: $context, + path: $sourceProperty, + ); + } + } catch (\Throwable $e) { + $targetPropertyValue = null; } + + $target->{$sourceProperty} = $targetPropertyValue; } } } diff --git a/src/Transformer/ObjectToObjectMetadata/Implementation/ObjectToObjectMetadataFactory.php b/src/Transformer/ObjectToObjectMetadata/Implementation/ObjectToObjectMetadataFactory.php index 5733f46..8b0f3d1 100644 --- a/src/Transformer/ObjectToObjectMetadata/Implementation/ObjectToObjectMetadataFactory.php +++ b/src/Transformer/ObjectToObjectMetadata/Implementation/ObjectToObjectMetadataFactory.php @@ -248,6 +248,8 @@ public function createObjectToObjectMetadata( $targetWriteName = $targetProperty; $targetWriteVisibility = Visibility::Public; } else { + $effectivePropertiesToMap[] = $targetProperty; + continue; } } else { diff --git a/src/Transformer/ObjectToObjectMetadata/ObjectToObjectMetadata.php b/src/Transformer/ObjectToObjectMetadata/ObjectToObjectMetadata.php index abc7a1a..90020bb 100644 --- a/src/Transformer/ObjectToObjectMetadata/ObjectToObjectMetadata.php +++ b/src/Transformer/ObjectToObjectMetadata/ObjectToObjectMetadata.php @@ -53,7 +53,7 @@ * @param array $allPropertyMappings * @param array $initializableTargetPropertiesNotInSource * @param array $targetProxySkippedProperties - * @param array $sourceProperties + * @param array $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. */ public function __construct( private string $sourceClass, diff --git a/tests/Fixtures/DynamicProperty/ObjectExtendingStdClassWithExplicitScalarProperties.php b/tests/Fixtures/DynamicProperty/ObjectExtendingStdClassWithExplicitScalarProperties.php new file mode 100644 index 0000000..5294f57 --- /dev/null +++ b/tests/Fixtures/DynamicProperty/ObjectExtendingStdClassWithExplicitScalarProperties.php @@ -0,0 +1,70 @@ + + * + * For the full copyright and license information, please view the LICENSE file + * that was distributed with this source code. + */ + +namespace Rekalogika\Mapper\Tests\Fixtures\DynamicProperty; + +class ObjectExtendingStdClassWithExplicitScalarProperties extends \stdClass +{ + private int $a = 1; + private string $b = 'string'; + private bool $c = true; + private float $d = 1.1; + + public function getA(): int + { + return $this->a; + } + + public function setA(int $a): self + { + $this->a = $a; + + return $this; + } + + public function getB(): string + { + return $this->b; + } + + public function setB(string $b): self + { + $this->b = $b; + + return $this; + } + + public function isC(): bool + { + return $this->c; + } + + public function setC(bool $c): self + { + $this->c = $c; + + return $this; + } + + public function getD(): float + { + return $this->d; + } + + public function setD(float $d): self + { + $this->d = $d; + + return $this; + } +} diff --git a/tests/IntegrationTest/DynamicPropertyTest.php b/tests/IntegrationTest/DynamicPropertyTest.php index 21a0562..2295f8a 100644 --- a/tests/IntegrationTest/DynamicPropertyTest.php +++ b/tests/IntegrationTest/DynamicPropertyTest.php @@ -16,6 +16,7 @@ use Rekalogika\Mapper\Tests\Common\FrameworkTestCase; use Rekalogika\Mapper\Tests\Fixtures\DynamicProperty\AnotherObjectExtendingStdClass; use Rekalogika\Mapper\Tests\Fixtures\DynamicProperty\ObjectExtendingStdClass; +use Rekalogika\Mapper\Tests\Fixtures\DynamicProperty\ObjectExtendingStdClassWithExplicitScalarProperties; use Rekalogika\Mapper\Tests\Fixtures\DynamicProperty\ObjectExtendingStdClassWithProperties; use Rekalogika\Mapper\Tests\Fixtures\Scalar\ObjectWithScalarProperties; use Rekalogika\Mapper\Tests\Fixtures\ScalarDto\ObjectWithScalarPropertiesDto; @@ -35,10 +36,10 @@ public function testStdClassToObject(): void $target = $this->mapper->map($source, ObjectWithScalarPropertiesDto::class); $this->assertInstanceOf(ObjectWithScalarPropertiesDto::class, $target); - $this->assertSame(1, $target->a); - $this->assertSame('string', $target->b); + $this->assertEquals(1, $target->a); + $this->assertEquals('string', $target->b); $this->assertTrue($target->c); - $this->assertSame(1.1, $target->d); + $this->assertEquals(1.1, $target->d); } public function testObjectExtendingStdClassToObject(): void @@ -56,10 +57,10 @@ public function testObjectExtendingStdClassToObject(): void $target = $this->mapper->map($source, ObjectWithScalarPropertiesDto::class); $this->assertInstanceOf(ObjectWithScalarPropertiesDto::class, $target); - $this->assertSame(1, $target->a); - $this->assertSame('string', $target->b); + $this->assertEquals(1, $target->a); + $this->assertEquals('string', $target->b); $this->assertTrue($target->c); - $this->assertSame(1.1, $target->d); + $this->assertEquals(1.1, $target->d); } public function testArrayCastToObjectToObject(): void @@ -74,10 +75,10 @@ public function testArrayCastToObjectToObject(): void $target = $this->mapper->map((object) $source, ObjectWithScalarPropertiesDto::class); $this->assertInstanceOf(ObjectWithScalarPropertiesDto::class, $target); - $this->assertSame(1, $target->a); - $this->assertSame('string', $target->b); + $this->assertEquals(1, $target->a); + $this->assertEquals('string', $target->b); $this->assertTrue($target->c); - $this->assertSame(1.1, $target->d); + $this->assertEquals(1.1, $target->d); } public function testStdClassWithoutPropertiesToObject(): void @@ -92,6 +93,38 @@ public function testStdClassWithoutPropertiesToObject(): void $this->assertNull($target->d); } + public function testStdClassWithExtraPropertyToObject(): void + { + $source = new \stdClass(); + $source->a = 1; + $source->b = 'string'; + $source->c = true; + $source->d = 1.1; + $source->e = 'extra'; + + $target = $this->mapper->map($source, ObjectWithScalarPropertiesDto::class); + + $this->assertInstanceOf(ObjectWithScalarPropertiesDto::class, $target); + $this->assertEquals(1, $target->a); + $this->assertEquals('string', $target->b); + $this->assertTrue($target->c); + $this->assertEquals(1.1, $target->d); + } + + public function testObjectExtendingStdClassWithExplicitScalarPropertiesToObject(): void + { + $source = new ObjectExtendingStdClassWithExplicitScalarProperties(); + /** @psalm-suppress UndefinedPropertyAssignment */ + $source->e = 'extra'; + $target = $this->mapper->map($source, ObjectWithScalarPropertiesDto::class); + + $this->assertInstanceOf(ObjectWithScalarPropertiesDto::class, $target); + $this->assertEquals(1, $target->a); + $this->assertEquals('string', $target->b); + $this->assertTrue($target->c); + $this->assertEquals(1.1, $target->d); + } + // to stdClass public function testObjectToStdClass(): void @@ -101,10 +134,10 @@ public function testObjectToStdClass(): void $this->assertInstanceOf(\stdClass::class, $target); - $this->assertSame(1, $target->a); - $this->assertSame('string', $target->b); + $this->assertEquals(1, $target->a); + $this->assertEquals('string', $target->b); $this->assertTrue($target->c); - $this->assertSame(1.1, $target->d); + $this->assertEquals(1.1, $target->d); } public function testObjectToObjectExtendingStdClass(): void @@ -115,13 +148,13 @@ public function testObjectToObjectExtendingStdClass(): void $this->assertInstanceOf(ObjectExtendingStdClass::class, $target); /** @psalm-suppress UndefinedPropertyFetch */ - $this->assertSame(1, $target->a); + $this->assertEquals(1, $target->a); /** @psalm-suppress UndefinedPropertyFetch */ - $this->assertSame('string', $target->b); + $this->assertEquals('string', $target->b); /** @psalm-suppress UndefinedPropertyFetch */ $this->assertTrue($target->c); /** @psalm-suppress UndefinedPropertyFetch */ - $this->assertSame(1.1, $target->d); + $this->assertEquals(1.1, $target->d); } // stdclass to stdclass @@ -142,13 +175,13 @@ public function testStdClassToStdClass(): void $this->assertInstanceOf(\stdClass::class, $target); /** @psalm-suppress UndefinedPropertyFetch */ - $this->assertSame(1, $target->a); + $this->assertEquals(1, $target->a); /** @psalm-suppress UndefinedPropertyFetch */ - $this->assertSame('string', $target->b); + $this->assertEquals('string', $target->b); /** @psalm-suppress UndefinedPropertyFetch */ $this->assertTrue($target->c); /** @psalm-suppress UndefinedPropertyFetch */ - $this->assertSame(1.1, $target->d); + $this->assertEquals(1.1, $target->d); } public function testStdClassToStdClassWithExplicitProperties(): void @@ -162,10 +195,41 @@ public function testStdClassToStdClassWithExplicitProperties(): void $target = $this->mapper->map($source, ObjectExtendingStdClassWithProperties::class); $this->assertInstanceOf(\stdClass::class, $target); - $this->assertSame('public', $target->public); + $this->assertEquals('public', $target->public); $this->assertNull($target->getPrivate()); $this->assertEquals('constructor', $target->getConstructor()); /** @psalm-suppress UndefinedPropertyFetch */ - $this->assertSame('dynamic', $target->dynamic); + $this->assertEquals('dynamic', $target->dynamic); + } + + public function testStdClassToStdClassWithExistingValue(): void + { + $source = new \stdClass(); + $source->property = new ObjectWithScalarProperties(); + + $target = new \stdClass(); + $targetProperty = new ObjectWithScalarPropertiesDto(); + $target->property = $targetProperty; + + $this->mapper->map($source, $target); + + $this->assertSame($targetProperty, $target->property); + } + + public function testStdClassToStdClassWithExistingNullValue(): void + { + $source = new \stdClass(); + $source->property = new ObjectWithScalarProperties(); + + $target = new \stdClass(); + $target->property = null; + + $this->mapper->map($source, $target); + + /** + * @psalm-suppress TypeDoesNotContainType + * @phpstan-ignore-next-line + */ + $this->assertInstanceOf(ObjectWithScalarProperties::class, $target->property); } }