From 2a9c2a7118e0eed3a6c5312a3e5264eee36d0cdd Mon Sep 17 00:00:00 2001 From: Priyadi Iman Nurcahyo <1102197+priyadi@users.noreply.github.com> Date: Sun, 13 Oct 2024 00:32:19 +0700 Subject: [PATCH 1/6] feat: extra target values --- CHANGELOG.md | 1 + src/Context/ExtraTargetValues.php | 44 +++++++++++++++++++ .../ObjectToObjectTransformer.php | 12 +++++ src/Util/ClassUtil.php | 2 +- .../rekalogika-mapper/generated-mappings.php | 6 +++ .../Fixtures/ExtraTargetValues/SomeObject.php | 19 ++++++++ .../SomeObjectWithConstructorDto.php | 22 ++++++++++ .../IntegrationTest/ExtraTargetValuesTest.php | 42 ++++++++++++++++++ 8 files changed, 147 insertions(+), 1 deletion(-) create mode 100644 src/Context/ExtraTargetValues.php create mode 100644 tests/src/Fixtures/ExtraTargetValues/SomeObject.php create mode 100644 tests/src/Fixtures/ExtraTargetValues/SomeObjectWithConstructorDto.php create mode 100644 tests/src/IntegrationTest/ExtraTargetValuesTest.php diff --git a/CHANGELOG.md b/CHANGELOG.md index b6a10931..019ad9c0 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -11,6 +11,7 @@ * perf: `ObjectToObjectTransformer` optimization * perf: move `TypeResolver` outside of the hot path * perf(`ObjectCache`): use sentinel value instead of exception +* feat: extra target values ## 1.10.0 diff --git a/src/Context/ExtraTargetValues.php b/src/Context/ExtraTargetValues.php new file mode 100644 index 00000000..73fece01 --- /dev/null +++ b/src/Context/ExtraTargetValues.php @@ -0,0 +1,44 @@ + + * + * For the full copyright and license information, please view the LICENSE file + * that was distributed with this source code. + */ + +namespace Rekalogika\Mapper\Context; + +/** + * Adds additional values to the target object + */ +final readonly class ExtraTargetValues +{ + /** + * @param array> $arguments + */ + public function __construct( + private array $arguments = [], + ) {} + + /** + * @param list $classes The class and its parent classes and interfaces. + * @return array + */ + public function getArgumentsForClass(array $classes): array + { + $arguments = []; + + foreach ($classes as $class) { + if (isset($this->arguments[$class])) { + $arguments = array_merge($arguments, $this->arguments[$class]); + } + } + + return $arguments; + } +} diff --git a/src/Transformer/Implementation/ObjectToObjectTransformer.php b/src/Transformer/Implementation/ObjectToObjectTransformer.php index 5c1b6723..c48a5248 100644 --- a/src/Transformer/Implementation/ObjectToObjectTransformer.php +++ b/src/Transformer/Implementation/ObjectToObjectTransformer.php @@ -18,6 +18,7 @@ use Rekalogika\Mapper\CacheWarmer\WarmableObjectToObjectMetadataFactoryInterface; use Rekalogika\Mapper\CacheWarmer\WarmableTransformerInterface; use Rekalogika\Mapper\Context\Context; +use Rekalogika\Mapper\Context\ExtraTargetValues; use Rekalogika\Mapper\Context\MapperOptions; use Rekalogika\Mapper\Exception\InvalidArgumentException; use Rekalogika\Mapper\ObjectCache\ObjectCache; @@ -42,6 +43,7 @@ use Rekalogika\Mapper\Transformer\TransformerInterface; use Rekalogika\Mapper\Transformer\TypeMapping; use Rekalogika\Mapper\Transformer\Util\ReaderWriter; +use Rekalogika\Mapper\Util\ClassUtil; use Rekalogika\Mapper\Util\TypeFactory; use Rekalogika\Mapper\Util\TypeGuesser; use Symfony\Component\PropertyAccess\PropertyAccessorInterface; @@ -379,6 +381,16 @@ private function generateConstructorArguments( } } + if (($extraTargetValues = $context(ExtraTargetValues::class)) !== null) { + $targetValues = $extraTargetValues + ->getArgumentsForClass(ClassUtil::getAllClassesFromObject($objectToObjectMetadata->getTargetClass())); + + /** @var mixed $value */ + foreach ($targetValues as $property => $value) { + $constructorArguments->addArgument($property, $value); + } + } + return $constructorArguments; } diff --git a/src/Util/ClassUtil.php b/src/Util/ClassUtil.php index 3df7380b..367a42cb 100644 --- a/src/Util/ClassUtil.php +++ b/src/Util/ClassUtil.php @@ -172,7 +172,7 @@ public static function getSkippedProperties( /** * @param object|class-string $objectOrClass - * @return array + * @return list */ public static function getAllClassesFromObject( object|string $objectOrClass, diff --git a/tests/config/rekalogika-mapper/generated-mappings.php b/tests/config/rekalogika-mapper/generated-mappings.php index b208898f..5c462da5 100644 --- a/tests/config/rekalogika-mapper/generated-mappings.php +++ b/tests/config/rekalogika-mapper/generated-mappings.php @@ -387,6 +387,12 @@ target: \Rekalogika\Mapper\Tests\Fixtures\EnumAndStringableDto\ObjectWithStringPropertyDto::class ); + $mappingCollection->addObjectMapping( + // tests/src/IntegrationTest/ExtraTargetValuesTest.php on line 26 + source: \Rekalogika\Mapper\Tests\Fixtures\ExtraTargetValues\SomeObject::class, + target: \Rekalogika\Mapper\Tests\Fixtures\ExtraTargetValues\SomeObjectWithConstructorDto::class + ); + $mappingCollection->addObjectMapping( // tests/src/IntegrationTest/InheritanceReversedTest.php on line 29 source: \Rekalogika\Mapper\Tests\Fixtures\InheritanceDto\ConcreteClassADto::class, diff --git a/tests/src/Fixtures/ExtraTargetValues/SomeObject.php b/tests/src/Fixtures/ExtraTargetValues/SomeObject.php new file mode 100644 index 00000000..dbe2eae7 --- /dev/null +++ b/tests/src/Fixtures/ExtraTargetValues/SomeObject.php @@ -0,0 +1,19 @@ + + * + * For the full copyright and license information, please view the LICENSE file + * that was distributed with this source code. + */ + +namespace Rekalogika\Mapper\Tests\Fixtures\ExtraTargetValues; + +class SomeObject +{ + public string $property = 'someProperty'; +} diff --git a/tests/src/Fixtures/ExtraTargetValues/SomeObjectWithConstructorDto.php b/tests/src/Fixtures/ExtraTargetValues/SomeObjectWithConstructorDto.php new file mode 100644 index 00000000..1a534440 --- /dev/null +++ b/tests/src/Fixtures/ExtraTargetValues/SomeObjectWithConstructorDto.php @@ -0,0 +1,22 @@ + + * + * For the full copyright and license information, please view the LICENSE file + * that was distributed with this source code. + */ + +namespace Rekalogika\Mapper\Tests\Fixtures\ExtraTargetValues; + +final readonly class SomeObjectWithConstructorDto +{ + public function __construct( + public string $property, + public ?\DateTimeInterface $date, + ) {} +} diff --git a/tests/src/IntegrationTest/ExtraTargetValuesTest.php b/tests/src/IntegrationTest/ExtraTargetValuesTest.php new file mode 100644 index 00000000..7d716c70 --- /dev/null +++ b/tests/src/IntegrationTest/ExtraTargetValuesTest.php @@ -0,0 +1,42 @@ + + * + * For the full copyright and license information, please view the LICENSE file + * that was distributed with this source code. + */ + +namespace Rekalogika\Mapper\Tests\IntegrationTest; + +use Rekalogika\Mapper\Context\Context; +use Rekalogika\Mapper\Context\ExtraTargetValues; +use Rekalogika\Mapper\Tests\Common\FrameworkTestCase; +use Rekalogika\Mapper\Tests\Fixtures\ExtraTargetValues\SomeObject; +use Rekalogika\Mapper\Tests\Fixtures\ExtraTargetValues\SomeObjectWithConstructorDto; + +class ExtraTargetValuesTest extends FrameworkTestCase +{ + public function testExtraArgumentsOnTargetConstructor(): void + { + $target = $this->mapper->map( + source: new SomeObject(), + target: SomeObjectWithConstructorDto::class, + context: Context::create( + new ExtraTargetValues([ + SomeObjectWithConstructorDto::class => [ + 'date' => new \DateTimeImmutable('2021-01-01'), + ], + ]), + ), + ); + + $this->assertSame('someProperty', $target->property); + $this->assertInstanceOf(\DateTimeImmutable::class, $target->date); + $this->assertSame('2021-01-01', $target->date->format('Y-m-d')); + } +} From ea6fad2a2fb7e35e0b2e426eb5dcb132a9e1ec05 Mon Sep 17 00:00:00 2001 From: Priyadi Iman Nurcahyo <1102197+priyadi@users.noreply.github.com> Date: Sun, 13 Oct 2024 00:49:14 +0700 Subject: [PATCH 2/6] optimization --- .../Implementation/ObjectToObjectTransformer.php | 3 +-- .../Implementation/ObjectToObjectMetadataFactory.php | 1 + .../ObjectToObjectMetadata.php | 12 ++++++++++++ 3 files changed, 14 insertions(+), 2 deletions(-) diff --git a/src/Transformer/Implementation/ObjectToObjectTransformer.php b/src/Transformer/Implementation/ObjectToObjectTransformer.php index c48a5248..b53a0909 100644 --- a/src/Transformer/Implementation/ObjectToObjectTransformer.php +++ b/src/Transformer/Implementation/ObjectToObjectTransformer.php @@ -43,7 +43,6 @@ use Rekalogika\Mapper\Transformer\TransformerInterface; use Rekalogika\Mapper\Transformer\TypeMapping; use Rekalogika\Mapper\Transformer\Util\ReaderWriter; -use Rekalogika\Mapper\Util\ClassUtil; use Rekalogika\Mapper\Util\TypeFactory; use Rekalogika\Mapper\Util\TypeGuesser; use Symfony\Component\PropertyAccess\PropertyAccessorInterface; @@ -383,7 +382,7 @@ private function generateConstructorArguments( if (($extraTargetValues = $context(ExtraTargetValues::class)) !== null) { $targetValues = $extraTargetValues - ->getArgumentsForClass(ClassUtil::getAllClassesFromObject($objectToObjectMetadata->getTargetClass())); + ->getArgumentsForClass($objectToObjectMetadata->getAllTargetClasses()); /** @var mixed $value */ foreach ($targetValues as $property => $value) { diff --git a/src/Transformer/ObjectToObjectMetadata/Implementation/ObjectToObjectMetadataFactory.php b/src/Transformer/ObjectToObjectMetadata/Implementation/ObjectToObjectMetadataFactory.php index 871f84ed..4e125794 100644 --- a/src/Transformer/ObjectToObjectMetadata/Implementation/ObjectToObjectMetadataFactory.php +++ b/src/Transformer/ObjectToObjectMetadata/Implementation/ObjectToObjectMetadataFactory.php @@ -162,6 +162,7 @@ class: $targetClass, sourceClass: $sourceClass, targetClass: $targetClass, providedTargetClass: $providedTargetClass, + allTargetClasses: ClassUtil::getAllClassesFromObject($targetClass), sourceAllowsDynamicProperties: $sourceClassMetadata->hasReadableDynamicProperties(), targetAllowsDynamicProperties: $targetClassMetadata->hasWritableDynamicProperties(), sourceProperties: $effectivePropertiesToMap, diff --git a/src/Transformer/ObjectToObjectMetadata/ObjectToObjectMetadata.php b/src/Transformer/ObjectToObjectMetadata/ObjectToObjectMetadata.php index a668fea3..69fd19fd 100644 --- a/src/Transformer/ObjectToObjectMetadata/ObjectToObjectMetadata.php +++ b/src/Transformer/ObjectToObjectMetadata/ObjectToObjectMetadata.php @@ -52,6 +52,7 @@ * @param class-string $sourceClass * @param class-string $targetClass Effective target class after resolving inheritance map * @param class-string $providedTargetClass + * @param list $allTargetClasses * @param list $allPropertyMappings * @param array $targetProxySkippedProperties * @param list $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. @@ -60,6 +61,7 @@ public function __construct( private string $sourceClass, private string $targetClass, private string $providedTargetClass, + private array $allTargetClasses, private bool $sourceAllowsDynamicProperties, private bool $targetAllowsDynamicProperties, private array $sourceProperties, @@ -121,6 +123,7 @@ public function withTargetProxy( sourceClass: $this->sourceClass, targetClass: $this->targetClass, providedTargetClass: $this->providedTargetClass, + allTargetClasses: $this->allTargetClasses, sourceAllowsDynamicProperties: $this->sourceAllowsDynamicProperties, targetAllowsDynamicProperties: $this->targetAllowsDynamicProperties, sourceProperties: $this->sourceProperties, @@ -146,6 +149,7 @@ public function withReasonCannotUseProxy( sourceClass: $this->sourceClass, targetClass: $this->targetClass, providedTargetClass: $this->providedTargetClass, + allTargetClasses: $this->allTargetClasses, sourceAllowsDynamicProperties: $this->sourceAllowsDynamicProperties, targetAllowsDynamicProperties: $this->targetAllowsDynamicProperties, sourceProperties: $this->sourceProperties, @@ -188,6 +192,14 @@ public function getProvidedTargetClass(): string return $this->providedTargetClass; } + /** + * @return list + */ + public function getAllTargetClasses(): array + { + return $this->allTargetClasses; + } + public function isInstantiable(): bool { return $this->instantiable; From 9096ea8f853ffe6dcc87274cb723b0e529d88308 Mon Sep 17 00:00:00 2001 From: Priyadi Iman Nurcahyo <1102197+priyadi@users.noreply.github.com> Date: Sun, 13 Oct 2024 11:21:47 +0700 Subject: [PATCH 3/6] process properties --- .../ObjectToObjectTransformer.php | 87 +++++++++++++++++-- .../ObjectToObjectMetadataFactory.php | 2 +- .../ObjectToObjectMetadata.php | 37 ++++---- .../rekalogika-mapper/generated-mappings.php | 8 +- .../SomeObjectWithConstructorDto.php | 2 +- .../SomeObjectWithPropertyDto.php | 20 +++++ .../IntegrationTest/ExtraTargetValuesTest.php | 22 ++++- 7 files changed, 148 insertions(+), 30 deletions(-) create mode 100644 tests/src/Fixtures/ExtraTargetValues/SomeObjectWithPropertyDto.php diff --git a/src/Transformer/Implementation/ObjectToObjectTransformer.php b/src/Transformer/Implementation/ObjectToObjectTransformer.php index b53a0909..cb73ab93 100644 --- a/src/Transformer/Implementation/ObjectToObjectTransformer.php +++ b/src/Transformer/Implementation/ObjectToObjectTransformer.php @@ -124,6 +124,13 @@ public function transform( $target = null; } + // get extra target values + + $extraTargetValues = $this->getExtraTargetValues( + objectToObjectMetadata: $objectToObjectMetadata, + context: $context, + ); + // initialize target if target is null if (null === $target) { @@ -134,12 +141,14 @@ public function transform( $target = $this->instantiateTargetProxy( source: $source, objectToObjectMetadata: $objectToObjectMetadata, + extraTargetValues: $extraTargetValues, context: $context, ); } else { $target = $this->instantiateRealTarget( source: $source, objectToObjectMetadata: $objectToObjectMetadata, + extraTargetValues: $extraTargetValues, context: $context, ); } @@ -181,6 +190,7 @@ public function transform( source: $source, target: $target, propertyMappings: $objectToObjectMetadata->getPropertyMappings(), + extraTargetValues: $extraTargetValues, context: $context, ); } @@ -188,9 +198,25 @@ public function transform( return $target; } + /** + * @return array + */ + private function getExtraTargetValues( + ObjectToObjectMetadata $objectToObjectMetadata, + Context $context, + ): array { + return $context(ExtraTargetValues::class) + ?->getArgumentsForClass($objectToObjectMetadata->getAllTargetClasses()) + ?? []; + } + + /** + * @param array $extraTargetValues + */ private function instantiateRealTarget( object $source, ObjectToObjectMetadata $objectToObjectMetadata, + array $extraTargetValues, Context $context, ): object { $targetClass = $objectToObjectMetadata->getTargetClass(); @@ -204,6 +230,7 @@ private function instantiateRealTarget( $constructorArguments = $this->generateConstructorArguments( source: $source, objectToObjectMetadata: $objectToObjectMetadata, + extraTargetValues: $extraTargetValues, context: $context, ); @@ -224,9 +251,13 @@ private function instantiateRealTarget( } } + /** + * @param array $extraTargetValues + */ private function instantiateTargetProxy( object $source, ObjectToObjectMetadata $objectToObjectMetadata, + array $extraTargetValues, Context $context, ): object { $targetClass = $objectToObjectMetadata->getTargetClass(); @@ -244,6 +275,7 @@ private function instantiateTargetProxy( $source, $objectToObjectMetadata, $context, + $extraTargetValues, ): void { // if the constructor is lazy, run it here @@ -252,6 +284,7 @@ private function instantiateTargetProxy( source: $source, target: $target, objectToObjectMetadata: $objectToObjectMetadata, + extraTargetValues: $extraTargetValues, context: $context, ); } @@ -262,6 +295,7 @@ private function instantiateTargetProxy( source: $source, target: $target, propertyMappings: $objectToObjectMetadata->getLazyPropertyMappings(), + extraTargetValues: $extraTargetValues, context: $context, ); }; @@ -281,6 +315,7 @@ private function instantiateTargetProxy( source: $source, target: $target, objectToObjectMetadata: $objectToObjectMetadata, + extraTargetValues: $extraTargetValues, context: $context, ); } @@ -291,16 +326,21 @@ private function instantiateTargetProxy( source: $source, target: $target, propertyMappings: $objectToObjectMetadata->getEagerPropertyMappings(), + extraTargetValues: $extraTargetValues, context: $context, ); return $target; } + /** + * @param array $extraTargetValues + */ private function runConstructorManually( object $source, object $target, ObjectToObjectMetadata $objectToObjectMetadata, + array $extraTargetValues, Context $context, ): object { if (!method_exists($target, '__construct')) { @@ -310,6 +350,7 @@ private function runConstructorManually( $constructorArguments = $this->generateConstructorArguments( source: $source, objectToObjectMetadata: $objectToObjectMetadata, + extraTargetValues: $extraTargetValues, context: $context, ); @@ -335,16 +376,22 @@ private function runConstructorManually( return $target; } + /** + * @param array $extraTargetValues + */ private function generateConstructorArguments( object $source, ObjectToObjectMetadata $objectToObjectMetadata, + array $extraTargetValues, Context $context, ): ConstructorArguments { - $propertyMappings = $objectToObjectMetadata->getConstructorPropertyMappings(); + $constructorPropertyMappings = $objectToObjectMetadata->getConstructorPropertyMappings(); $constructorArguments = new ConstructorArguments(); - foreach ($propertyMappings as $propertyMapping) { + // add arguments from property mappings + + foreach ($constructorPropertyMappings as $propertyMapping) { try { /** @var mixed $targetPropertyValue */ [$targetPropertyValue,] = $this->transformValue( @@ -380,26 +427,30 @@ private function generateConstructorArguments( } } - if (($extraTargetValues = $context(ExtraTargetValues::class)) !== null) { - $targetValues = $extraTargetValues - ->getArgumentsForClass($objectToObjectMetadata->getAllTargetClasses()); + // add arguments from extra target values - /** @var mixed $value */ - foreach ($targetValues as $property => $value) { - $constructorArguments->addArgument($property, $value); + /** @var mixed $value */ + foreach ($extraTargetValues as $property => $value) { + // skip if there is no constructor property mapping for this + if (!isset($constructorPropertyMappings[$property])) { + continue; } + + $constructorArguments->addArgument($property, $value); } return $constructorArguments; } /** - * @param array $propertyMappings + * @param array $propertyMappings + * @param array $extraTargetValues */ private function readSourceAndWriteTarget( object $source, object $target, array $propertyMappings, + array $extraTargetValues, Context $context, ): object { foreach ($propertyMappings as $propertyMapping) { @@ -411,6 +462,24 @@ private function readSourceAndWriteTarget( ); } + // process extra target values + + /** @var mixed $value */ + foreach ($extraTargetValues as $property => $value) { + if (!isset($propertyMappings[$property])) { + continue; + } + + $propertyMapping = $propertyMappings[$property]; + + $target = $this->readerWriter->writeTargetProperty( + target: $target, + propertyMapping: $propertyMapping, + value: $value, + context: $context, + ); + } + return $target; } diff --git a/src/Transformer/ObjectToObjectMetadata/Implementation/ObjectToObjectMetadataFactory.php b/src/Transformer/ObjectToObjectMetadata/Implementation/ObjectToObjectMetadataFactory.php index 4e125794..5537998b 100644 --- a/src/Transformer/ObjectToObjectMetadata/Implementation/ObjectToObjectMetadataFactory.php +++ b/src/Transformer/ObjectToObjectMetadata/Implementation/ObjectToObjectMetadataFactory.php @@ -212,7 +212,7 @@ class: $targetClass, /** * @param class-string $targetClass * @param list $eagerProperties - * @param list $constructorPropertyMappings + * @param array $constructorPropertyMappings * @return array{array,bool} */ private function getProxyParameters( diff --git a/src/Transformer/ObjectToObjectMetadata/ObjectToObjectMetadata.php b/src/Transformer/ObjectToObjectMetadata/ObjectToObjectMetadata.php index 69fd19fd..84f4a252 100644 --- a/src/Transformer/ObjectToObjectMetadata/ObjectToObjectMetadata.php +++ b/src/Transformer/ObjectToObjectMetadata/ObjectToObjectMetadata.php @@ -24,27 +24,27 @@ final readonly class ObjectToObjectMetadata { /** - * @var list + * @var array */ private array $allPropertyMappings; /** - * @var list + * @var array */ private array $propertyMappings; /** - * @var list + * @var array */ private array $constructorPropertyMappings; /** - * @var list + * @var array */ private array $lazyPropertyMappings; /** - * @var list + * @var array */ private array $eagerPropertyMappings; @@ -82,10 +82,13 @@ public function __construct( $lazyPropertyMappings = []; $eagerPropertyMappings = []; $propertyPropertyMappings = []; + $processedAllPropertyMappings = []; foreach ($allPropertyMappings as $propertyMapping) { + $processedAllPropertyMappings[$propertyMapping->getTargetProperty()] = $propertyMapping; + if ($propertyMapping->getTargetConstructorWriteMode() === WriteMode::Constructor) { - $constructorPropertyMappings[] = $propertyMapping; + $constructorPropertyMappings[$propertyMapping->getTargetProperty()] = $propertyMapping; } if ( @@ -96,12 +99,12 @@ public function __construct( continue; } - $propertyPropertyMappings[] = $propertyMapping; + $propertyPropertyMappings[$propertyMapping->getTargetProperty()] = $propertyMapping; if ($propertyMapping->isSourceLazy()) { - $lazyPropertyMappings[] = $propertyMapping; + $lazyPropertyMappings[$propertyMapping->getTargetProperty()] = $propertyMapping; } else { - $eagerPropertyMappings[] = $propertyMapping; + $eagerPropertyMappings[$propertyMapping->getTargetProperty()] = $propertyMapping; } } @@ -109,7 +112,7 @@ public function __construct( $this->lazyPropertyMappings = $lazyPropertyMappings; $this->eagerPropertyMappings = $eagerPropertyMappings; $this->propertyMappings = $propertyPropertyMappings; - $this->allPropertyMappings = $allPropertyMappings; + $this->allPropertyMappings = $processedAllPropertyMappings; } /** @@ -127,7 +130,7 @@ public function withTargetProxy( sourceAllowsDynamicProperties: $this->sourceAllowsDynamicProperties, targetAllowsDynamicProperties: $this->targetAllowsDynamicProperties, sourceProperties: $this->sourceProperties, - allPropertyMappings: $this->allPropertyMappings, + allPropertyMappings: array_values($this->allPropertyMappings), instantiable: $this->instantiable, cloneable: $this->cloneable, targetUnalterable: $this->targetUnalterable, @@ -153,7 +156,7 @@ public function withReasonCannotUseProxy( sourceAllowsDynamicProperties: $this->sourceAllowsDynamicProperties, targetAllowsDynamicProperties: $this->targetAllowsDynamicProperties, sourceProperties: $this->sourceProperties, - allPropertyMappings: $this->allPropertyMappings, + allPropertyMappings: array_values($this->allPropertyMappings), instantiable: $this->instantiable, cloneable: $this->cloneable, targetUnalterable: $this->targetUnalterable, @@ -216,7 +219,7 @@ public function isTargetUnalterable(): bool } /** - * @return list + * @return array */ public function getPropertyMappings(): array { @@ -224,7 +227,7 @@ public function getPropertyMappings(): array } /** - * @return list + * @return array */ public function getLazyPropertyMappings(): array { @@ -232,7 +235,7 @@ public function getLazyPropertyMappings(): array } /** - * @return list + * @return array */ public function getEagerPropertyMappings(): array { @@ -240,7 +243,7 @@ public function getEagerPropertyMappings(): array } /** - * @return list + * @return array */ public function getConstructorPropertyMappings(): array { @@ -248,7 +251,7 @@ public function getConstructorPropertyMappings(): array } /** - * @return list + * @return array */ public function getAllPropertyMappings(): array { diff --git a/tests/config/rekalogika-mapper/generated-mappings.php b/tests/config/rekalogika-mapper/generated-mappings.php index 5c462da5..252d6e55 100644 --- a/tests/config/rekalogika-mapper/generated-mappings.php +++ b/tests/config/rekalogika-mapper/generated-mappings.php @@ -388,11 +388,17 @@ ); $mappingCollection->addObjectMapping( - // tests/src/IntegrationTest/ExtraTargetValuesTest.php on line 26 + // tests/src/IntegrationTest/ExtraTargetValuesTest.php on line 27 source: \Rekalogika\Mapper\Tests\Fixtures\ExtraTargetValues\SomeObject::class, target: \Rekalogika\Mapper\Tests\Fixtures\ExtraTargetValues\SomeObjectWithConstructorDto::class ); + $mappingCollection->addObjectMapping( + // tests/src/IntegrationTest/ExtraTargetValuesTest.php on line 46 + source: \Rekalogika\Mapper\Tests\Fixtures\ExtraTargetValues\SomeObject::class, + target: \Rekalogika\Mapper\Tests\Fixtures\ExtraTargetValues\SomeObjectWithPropertyDto::class + ); + $mappingCollection->addObjectMapping( // tests/src/IntegrationTest/InheritanceReversedTest.php on line 29 source: \Rekalogika\Mapper\Tests\Fixtures\InheritanceDto\ConcreteClassADto::class, diff --git a/tests/src/Fixtures/ExtraTargetValues/SomeObjectWithConstructorDto.php b/tests/src/Fixtures/ExtraTargetValues/SomeObjectWithConstructorDto.php index 1a534440..5f23eea3 100644 --- a/tests/src/Fixtures/ExtraTargetValues/SomeObjectWithConstructorDto.php +++ b/tests/src/Fixtures/ExtraTargetValues/SomeObjectWithConstructorDto.php @@ -16,7 +16,7 @@ final readonly class SomeObjectWithConstructorDto { public function __construct( - public string $property, public ?\DateTimeInterface $date, + public ?string $property, ) {} } diff --git a/tests/src/Fixtures/ExtraTargetValues/SomeObjectWithPropertyDto.php b/tests/src/Fixtures/ExtraTargetValues/SomeObjectWithPropertyDto.php new file mode 100644 index 00000000..9669f347 --- /dev/null +++ b/tests/src/Fixtures/ExtraTargetValues/SomeObjectWithPropertyDto.php @@ -0,0 +1,20 @@ + + * + * For the full copyright and license information, please view the LICENSE file + * that was distributed with this source code. + */ + +namespace Rekalogika\Mapper\Tests\Fixtures\ExtraTargetValues; + +final class SomeObjectWithPropertyDto +{ + public ?string $property = null; + public ?\DateTimeInterface $date = null; +} diff --git a/tests/src/IntegrationTest/ExtraTargetValuesTest.php b/tests/src/IntegrationTest/ExtraTargetValuesTest.php index 7d716c70..2ba5aaac 100644 --- a/tests/src/IntegrationTest/ExtraTargetValuesTest.php +++ b/tests/src/IntegrationTest/ExtraTargetValuesTest.php @@ -18,10 +18,11 @@ use Rekalogika\Mapper\Tests\Common\FrameworkTestCase; use Rekalogika\Mapper\Tests\Fixtures\ExtraTargetValues\SomeObject; use Rekalogika\Mapper\Tests\Fixtures\ExtraTargetValues\SomeObjectWithConstructorDto; +use Rekalogika\Mapper\Tests\Fixtures\ExtraTargetValues\SomeObjectWithPropertyDto; class ExtraTargetValuesTest extends FrameworkTestCase { - public function testExtraArgumentsOnTargetConstructor(): void + public function testExtraTargetValuesOnConstructor(): void { $target = $this->mapper->map( source: new SomeObject(), @@ -39,4 +40,23 @@ public function testExtraArgumentsOnTargetConstructor(): void $this->assertInstanceOf(\DateTimeImmutable::class, $target->date); $this->assertSame('2021-01-01', $target->date->format('Y-m-d')); } + + public function testExtraTargetValuesOnProperty(): void + { + $target = $this->mapper->map( + source: new SomeObject(), + target: SomeObjectWithPropertyDto::class, + context: Context::create( + new ExtraTargetValues([ + SomeObjectWithPropertyDto::class => [ + 'date' => new \DateTimeImmutable('2021-01-01'), + ], + ]), + ), + ); + + $this->assertSame('someProperty', $target->property); + $this->assertInstanceOf(\DateTimeImmutable::class, $target->date); + $this->assertSame('2021-01-01', $target->date->format('Y-m-d')); + } } From 4cd887bb58dc796fb87317491feb645fd5796c0a Mon Sep 17 00:00:00 2001 From: Priyadi Iman Nurcahyo <1102197+priyadi@users.noreply.github.com> Date: Sun, 13 Oct 2024 12:09:44 +0700 Subject: [PATCH 4/6] fix error --- .../Implementation/ObjectToObjectTransformer.php | 10 +++++++--- src/Transformer/Util/ReaderWriter.php | 8 +++++++- 2 files changed, 14 insertions(+), 4 deletions(-) diff --git a/src/Transformer/Implementation/ObjectToObjectTransformer.php b/src/Transformer/Implementation/ObjectToObjectTransformer.php index cb73ab93..1ebcc2e9 100644 --- a/src/Transformer/Implementation/ObjectToObjectTransformer.php +++ b/src/Transformer/Implementation/ObjectToObjectTransformer.php @@ -477,6 +477,7 @@ private function readSourceAndWriteTarget( propertyMapping: $propertyMapping, value: $value, context: $context, + silentOnError: true, ); } @@ -515,7 +516,10 @@ private function readSourcePropertyAndWriteTargetProperty( // write - if ($isChanged) { + if ( + $isChanged + || $propertyMapping->getTargetSetterWriteMode() === WriteMode::DynamicProperty + ) { if ($targetPropertyValue instanceof AdderRemoverProxy) { $target = $targetPropertyValue->getHostObject(); } @@ -525,6 +529,7 @@ private function readSourcePropertyAndWriteTargetProperty( propertyMapping: $propertyMapping, value: $targetPropertyValue, context: $context, + silentOnError: false, ); } @@ -691,8 +696,7 @@ private function transformValue( return [ $targetPropertyValue, - $targetPropertyValue !== $originalTargetPropertyValue - || $propertyMapping->getTargetSetterWriteMode() === WriteMode::DynamicProperty, + $targetPropertyValue !== $originalTargetPropertyValue, ]; } diff --git a/src/Transformer/Util/ReaderWriter.php b/src/Transformer/Util/ReaderWriter.php index f1018582..16bdb78e 100644 --- a/src/Transformer/Util/ReaderWriter.php +++ b/src/Transformer/Util/ReaderWriter.php @@ -200,14 +200,20 @@ public function writeTargetProperty( PropertyMapping $propertyMapping, mixed $value, Context $context, + bool $silentOnError, ): object { $accessorName = $propertyMapping->getTargetSetterWriteName(); $writeMode = $propertyMapping->getTargetSetterWriteMode(); $visibility = $propertyMapping->getTargetSetterWriteVisibility(); if ( - $visibility !== Visibility::Public || $writeMode === WriteMode::None + $visibility !== Visibility::Public + || $writeMode === WriteMode::None ) { + if ($silentOnError) { + return $target; + } + throw new NewInstanceReturnedButCannotBeSetOnTargetException( $target, $propertyMapping->getTargetProperty(), From 1158d2a901cc380b2671eaefeaad474271c22911 Mon Sep 17 00:00:00 2001 From: Priyadi Iman Nurcahyo <1102197+priyadi@users.noreply.github.com> Date: Sun, 13 Oct 2024 12:19:01 +0700 Subject: [PATCH 5/6] changelog --- CHANGELOG.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 019ad9c0..e3b18c4c 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,6 +1,6 @@ # CHANGELOG -## 1.10.1 +## 1.11.0 * refactor: spin off `resolveTargetClass()` to separate class * perf: proxy warming From 3b15b2327c1005cd5dd559afcb5fba7372c3a7a1 Mon Sep 17 00:00:00 2001 From: Priyadi Iman Nurcahyo <1102197+priyadi@users.noreply.github.com> Date: Sun, 13 Oct 2024 12:51:12 +0700 Subject: [PATCH 6/6] handle invalid extra target properties --- .../ExtraTargetPropertyNotFoundException.php | 40 +++++++++++++++++++ .../ObjectToObjectTransformer.php | 17 +++++++- .../rekalogika-mapper/generated-mappings.php | 4 +- .../IntegrationTest/ExtraTargetValuesTest.php | 18 +++++++++ 4 files changed, 76 insertions(+), 3 deletions(-) create mode 100644 src/Transformer/Exception/ExtraTargetPropertyNotFoundException.php diff --git a/src/Transformer/Exception/ExtraTargetPropertyNotFoundException.php b/src/Transformer/Exception/ExtraTargetPropertyNotFoundException.php new file mode 100644 index 00000000..fcdc44fd --- /dev/null +++ b/src/Transformer/Exception/ExtraTargetPropertyNotFoundException.php @@ -0,0 +1,40 @@ + + * + * For the full copyright and license information, please view the LICENSE file + * that was distributed with this source code. + */ + +namespace Rekalogika\Mapper\Transformer\Exception; + +use Rekalogika\Mapper\Context\Context; +use Rekalogika\Mapper\Exception\LogicException; + +/** + * @internal + */ +class ExtraTargetPropertyNotFoundException extends LogicException +{ + /** + * @param class-string $class + */ + public function __construct( + string $class, + string $property, + Context $context, + ) { + $message = \sprintf( + 'Mapper is called with "ExtraTargetValues", but cannot find the target property "%s" in class "%s"', + $property, + $class, + ); + + parent::__construct($message, context: $context); + } +} diff --git a/src/Transformer/Implementation/ObjectToObjectTransformer.php b/src/Transformer/Implementation/ObjectToObjectTransformer.php index 1ebcc2e9..2dc8917f 100644 --- a/src/Transformer/Implementation/ObjectToObjectTransformer.php +++ b/src/Transformer/Implementation/ObjectToObjectTransformer.php @@ -27,6 +27,7 @@ use Rekalogika\Mapper\ServiceMethod\ServiceMethodSpecification; use Rekalogika\Mapper\SubMapper\SubMapperFactoryInterface; use Rekalogika\Mapper\Transformer\Exception\ClassNotInstantiableException; +use Rekalogika\Mapper\Transformer\Exception\ExtraTargetPropertyNotFoundException; use Rekalogika\Mapper\Transformer\Exception\InstantiationFailureException; use Rekalogika\Mapper\Transformer\Exception\NotAClassException; use Rekalogika\Mapper\Transformer\Exception\UninitializedSourcePropertyException; @@ -205,9 +206,23 @@ private function getExtraTargetValues( ObjectToObjectMetadata $objectToObjectMetadata, Context $context, ): array { - return $context(ExtraTargetValues::class) + $extraTargetValues = $context(ExtraTargetValues::class) ?->getArgumentsForClass($objectToObjectMetadata->getAllTargetClasses()) ?? []; + + $allPropertyMappings = $objectToObjectMetadata->getPropertyMappings(); + + foreach (array_keys($extraTargetValues) as $property) { + if (!isset($allPropertyMappings[$property])) { + throw new ExtraTargetPropertyNotFoundException( + class: $objectToObjectMetadata->getTargetClass(), + property: $property, + context: $context, + ); + } + } + + return $extraTargetValues; } /** diff --git a/tests/config/rekalogika-mapper/generated-mappings.php b/tests/config/rekalogika-mapper/generated-mappings.php index 252d6e55..ab866e22 100644 --- a/tests/config/rekalogika-mapper/generated-mappings.php +++ b/tests/config/rekalogika-mapper/generated-mappings.php @@ -388,13 +388,13 @@ ); $mappingCollection->addObjectMapping( - // tests/src/IntegrationTest/ExtraTargetValuesTest.php on line 27 + // tests/src/IntegrationTest/ExtraTargetValuesTest.php on lines 28, 68 source: \Rekalogika\Mapper\Tests\Fixtures\ExtraTargetValues\SomeObject::class, target: \Rekalogika\Mapper\Tests\Fixtures\ExtraTargetValues\SomeObjectWithConstructorDto::class ); $mappingCollection->addObjectMapping( - // tests/src/IntegrationTest/ExtraTargetValuesTest.php on line 46 + // tests/src/IntegrationTest/ExtraTargetValuesTest.php on line 47 source: \Rekalogika\Mapper\Tests\Fixtures\ExtraTargetValues\SomeObject::class, target: \Rekalogika\Mapper\Tests\Fixtures\ExtraTargetValues\SomeObjectWithPropertyDto::class ); diff --git a/tests/src/IntegrationTest/ExtraTargetValuesTest.php b/tests/src/IntegrationTest/ExtraTargetValuesTest.php index 2ba5aaac..70b83539 100644 --- a/tests/src/IntegrationTest/ExtraTargetValuesTest.php +++ b/tests/src/IntegrationTest/ExtraTargetValuesTest.php @@ -19,6 +19,7 @@ use Rekalogika\Mapper\Tests\Fixtures\ExtraTargetValues\SomeObject; use Rekalogika\Mapper\Tests\Fixtures\ExtraTargetValues\SomeObjectWithConstructorDto; use Rekalogika\Mapper\Tests\Fixtures\ExtraTargetValues\SomeObjectWithPropertyDto; +use Rekalogika\Mapper\Transformer\Exception\ExtraTargetPropertyNotFoundException; class ExtraTargetValuesTest extends FrameworkTestCase { @@ -59,4 +60,21 @@ public function testExtraTargetValuesOnProperty(): void $this->assertInstanceOf(\DateTimeImmutable::class, $target->date); $this->assertSame('2021-01-01', $target->date->format('Y-m-d')); } + + public function testInvalidExtraTargetValues(): void + { + $this->expectException(ExtraTargetPropertyNotFoundException::class); + + $this->mapper->map( + source: new SomeObject(), + target: SomeObjectWithConstructorDto::class, + context: Context::create( + new ExtraTargetValues([ + SomeObjectWithConstructorDto::class => [ + 'foo' => new \DateTimeImmutable('2021-01-01'), + ], + ]), + ), + ); + } }