diff --git a/CHANGELOG.md b/CHANGELOG.md index f0296ba..77e03b5 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,13 +4,14 @@ * feat: `PresetTransformer`. * fix: Typo in `RemoveOptionalDefinitionPass` -* feat: Supports dynamic properties (including `stdClass`) on the target side. +* feat: Supports dynamic properties (including `stdClass`) on the source side. * fix(`Mapper`): Fix typehint. * test: test array cast to object mapping * feat(`Context`): `with()` not accepts multiple argument. * build: Deinternalize `ObjectCacheFactory` * fix(`PresetMapping`): Support proxied classes, add tests. * fix: Disallow proxy for objects with dynamic properties, including `stdClass`. +* feat: Dynamic properties (`stdClass` & co) on the target side. ## 1.0.0 diff --git a/src/Transformer/Implementation/ObjectToObjectTransformer.php b/src/Transformer/Implementation/ObjectToObjectTransformer.php index 78b7d85..22f931f 100644 --- a/src/Transformer/Implementation/ObjectToObjectTransformer.php +++ b/src/Transformer/Implementation/ObjectToObjectTransformer.php @@ -377,9 +377,10 @@ private function readSourcePropertyAndWriteTargetProperty( $targetWriteVisibility = $propertyMapping->getTargetWriteVisibility(); if ( - $targetWriteMode !== WriteMode::Method - && $targetWriteMode !== WriteMode::Property - && $targetWriteMode !== WriteMode::AdderRemover + in_array($targetWriteMode, [ + WriteMode::None, + WriteMode::Constructor, + ], true) ) { return; } diff --git a/src/Transformer/ObjectToObjectMetadata/Implementation/ObjectToObjectMetadataFactory.php b/src/Transformer/ObjectToObjectMetadata/Implementation/ObjectToObjectMetadataFactory.php index 6aeded2..071d430 100644 --- a/src/Transformer/ObjectToObjectMetadata/Implementation/ObjectToObjectMetadataFactory.php +++ b/src/Transformer/ObjectToObjectMetadata/Implementation/ObjectToObjectMetadataFactory.php @@ -90,7 +90,7 @@ public function createObjectToObjectMetadata( throw new InternalClassUnsupportedException($sourceClass); } - if ($targetReflection->isInternal()) { + if (!$targetAllowsDynamicProperties && $targetReflection->isInternal()) { throw new InternalClassUnsupportedException($targetClass); } @@ -126,7 +126,18 @@ public function createObjectToObjectMetadata( $propertyMappings = []; - foreach ($targetProperties as $targetProperty) { + // determine properties to map + + if ($targetAllowsDynamicProperties) { + $sourceProperties = $this->listProperties($sourceClass); + $propertiesToMap = array_unique(array_merge($sourceProperties, $targetProperties)); + } else { + $propertiesToMap = $targetProperties; + } + + // iterate over properties to map + + foreach ($propertiesToMap as $targetProperty) { $sourceProperty = $targetProperty; // determine if a property mapper is defined for the property @@ -176,9 +187,16 @@ public function createObjectToObjectMetadata( // process target read mode if ($targetReadInfo === null) { - $targetReadMode = ReadMode::None; - $targetReadName = null; - $targetReadVisibility = Visibility::None; + // if source allows dynamic properties, including stdClass + if ($targetAllowsDynamicProperties) { + $targetReadMode = ReadMode::DynamicProperty; + $targetReadName = $targetProperty; + $targetReadVisibility = Visibility::Public; + } else { + $targetReadMode = ReadMode::None; + $targetReadName = null; + $targetReadVisibility = Visibility::None; + } } else { $targetReadMode = match ($targetReadInfo->getType()) { PropertyReadInfo::TYPE_METHOD => ReadMode::Method, @@ -223,15 +241,22 @@ public function createObjectToObjectMetadata( }; if ($targetWriteMode === WriteMode::None) { - continue; + if (!$targetAllowsDynamicProperties) { + continue; + } + $targetWriteMode = WriteMode::DynamicProperty; + $targetWriteName = $targetProperty; + $targetWriteVisibility = Visibility::Public; + + } else { + $targetWriteName = $targetWriteInfo->getName(); + $targetWriteVisibility = match ($targetWriteInfo->getVisibility()) { + PropertyWriteInfo::VISIBILITY_PUBLIC => Visibility::Public, + PropertyWriteInfo::VISIBILITY_PROTECTED => Visibility::Protected, + PropertyWriteInfo::VISIBILITY_PRIVATE => Visibility::Private, + default => Visibility::None, + }; } - $targetWriteName = $targetWriteInfo->getName(); - $targetWriteVisibility = match ($targetWriteInfo->getVisibility()) { - PropertyWriteInfo::VISIBILITY_PUBLIC => Visibility::Public, - PropertyWriteInfo::VISIBILITY_PROTECTED => Visibility::Protected, - PropertyWriteInfo::VISIBILITY_PRIVATE => Visibility::Private, - default => Visibility::None, - }; } // get source property types diff --git a/src/Transformer/ObjectToObjectMetadata/WriteMode.php b/src/Transformer/ObjectToObjectMetadata/WriteMode.php index 646b91c..5f24ce5 100644 --- a/src/Transformer/ObjectToObjectMetadata/WriteMode.php +++ b/src/Transformer/ObjectToObjectMetadata/WriteMode.php @@ -23,4 +23,5 @@ enum WriteMode case Property; case AdderRemover; case Constructor; + case DynamicProperty; } diff --git a/src/Transformer/Util/ReaderWriter.php b/src/Transformer/Util/ReaderWriter.php index 6d2c766..faaa429 100644 --- a/src/Transformer/Util/ReaderWriter.php +++ b/src/Transformer/Util/ReaderWriter.php @@ -64,9 +64,10 @@ public function readSourceProperty( if (isset($source->{$accessorName})) { return $source->{$accessorName}; } - return null; + return null; } + return null; } catch (\Error $e) { $message = $e->getMessage(); @@ -119,7 +120,14 @@ public function readTargetProperty( } elseif ($readMode === ReadMode::Method) { /** @psalm-suppress MixedMethodCall */ return $target->{$accessorName}(); + } elseif ($readMode === ReadMode::DynamicProperty) { + if (isset($target->{$accessorName})) { + return $target->{$accessorName}; + } + + return null; } + return null; } catch (\Error $e) { $message = $e->getMessage(); @@ -164,6 +172,8 @@ public function writeTargetProperty( $target->{$accessorName}($value); } elseif ($writeMode === WriteMode::AdderRemover) { // noop + } elseif ($writeMode === WriteMode::DynamicProperty) { + $target->{$accessorName} = $value; } } catch (\Throwable $e) { throw new UnableToWriteException( diff --git a/tests/IntegrationTest/DynamicPropertyTest.php b/tests/IntegrationTest/DynamicPropertyTest.php index 6085de7..a65b388 100644 --- a/tests/IntegrationTest/DynamicPropertyTest.php +++ b/tests/IntegrationTest/DynamicPropertyTest.php @@ -15,10 +15,13 @@ use Rekalogika\Mapper\Tests\Common\FrameworkTestCase; use Rekalogika\Mapper\Tests\Fixtures\DynamicProperty\ObjectExtendingStdClass; +use Rekalogika\Mapper\Tests\Fixtures\Scalar\ObjectWithScalarProperties; use Rekalogika\Mapper\Tests\Fixtures\ScalarDto\ObjectWithScalarPropertiesDto; class DynamicPropertyTest extends FrameworkTestCase { + // from stdclass to object + public function testStdClassToObject(): void { $source = new \stdClass(); @@ -86,4 +89,55 @@ public function testStdClassWithoutPropertiesToObject(): void $this->assertNull($target->c); $this->assertNull($target->d); } + + // to stdClass + + public function testObjectToStdClass(): void + { + $source = new ObjectWithScalarProperties(); + $target = $this->mapper->map($source, \stdClass::class); + + $this->assertInstanceOf(\stdClass::class, $target); + + $this->assertSame(1, $target->a); + $this->assertSame('string', $target->b); + $this->assertTrue($target->c); + $this->assertSame(1.1, $target->d); + } + + public function testObjectToObjectExtendingStdClass(): void + { + $source = new ObjectWithScalarProperties(); + $target = $this->mapper->map($source, ObjectExtendingStdClass::class); + + $this->assertInstanceOf(ObjectExtendingStdClass::class, $target); + + /** @psalm-suppress UndefinedPropertyFetch */ + $this->assertSame(1, $target->a); + /** @psalm-suppress UndefinedPropertyFetch */ + $this->assertSame('string', $target->b); + /** @psalm-suppress UndefinedPropertyFetch */ + $this->assertTrue($target->c); + /** @psalm-suppress UndefinedPropertyFetch */ + $this->assertSame(1.1, $target->d); + } + + // stdclass to stdclass + + public function testStdClassToStdClass(): void + { + $source = new \stdClass(); + $source->a = 1; + $source->b = 'string'; + $source->c = true; + $source->d = 1.1; + + $target = $this->mapper->map($source, \stdClass::class); + + $this->assertInstanceOf(\stdClass::class, $target); + $this->assertSame(1, $target->a); + $this->assertSame('string', $target->b); + $this->assertTrue($target->c); + $this->assertSame(1.1, $target->d); + } }