diff --git a/CHANGELOG.md b/CHANGELOG.md index 3d9a8419..ae787085 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,6 +1,10 @@ # CHANGELOG -## 1.11.1 +## 1.13.0 + +* feat: `Map(property: null)` ignores the mapping + +## 1.12.0 * refactor: refactor `ObjectToObjectTransformer` for future optimization * fix: remove unalterable check in `ObjectProcessor` to allow transformers to diff --git a/src/Attribute/Map.php b/src/Attribute/Map.php index 03b8f0d0..f785c99e 100644 --- a/src/Attribute/Map.php +++ b/src/Attribute/Map.php @@ -23,7 +23,7 @@ * @param class-string|null $class */ public function __construct( - public string $property, + public ?string $property = null, public ?string $class = null, ) {} } diff --git a/src/Transformer/MetadataUtil/PropertyMappingResolver/PropertyMappingResolver.php b/src/Transformer/MetadataUtil/PropertyMappingResolver/PropertyMappingResolver.php index 37847a93..15252559 100644 --- a/src/Transformer/MetadataUtil/PropertyMappingResolver/PropertyMappingResolver.php +++ b/src/Transformer/MetadataUtil/PropertyMappingResolver/PropertyMappingResolver.php @@ -38,6 +38,7 @@ public function getPropertiesToMap( $targetProperties = $this->listProperties($targetClass); $targetPropertyToSourceProperty = []; + $skippedTargetProperties = []; foreach ($targetProperties as $targetProperty) { $sourceProperty = $this->determinePairedProperty( @@ -47,6 +48,11 @@ class: $targetClass, pairedClassProperties: $sourceProperties, ); + if ($sourceProperty === null) { + $skippedTargetProperties[$targetProperty] = true; + continue; + } + $targetPropertyToSourceProperty[$targetProperty] = $sourceProperty; } @@ -58,6 +64,18 @@ class: $sourceClass, pairedClassProperties: $targetProperties, ); + if (isset($skippedTargetProperties[$targetProperty])) { + continue; + } + + if ($targetProperty === null) { + if (isset($targetPropertyToSourceProperty[$sourceProperty])) { + unset($targetPropertyToSourceProperty[$sourceProperty]); + } + + continue; + } + $targetPropertyToSourceProperty[$targetProperty] = $sourceProperty; } @@ -93,7 +111,7 @@ private function determinePairedProperty( string $property, string $pairedClass, array $pairedClassProperties, - ): string { + ): ?string { $attributes = ClassUtil::getPropertyAttributes( class: $class, property: $property, @@ -112,6 +130,10 @@ class: $class, if (\count($attributesWithClass) >= 1) { $pairedProperty = $attributesWithClass[0]->property; + if ($pairedProperty === null) { + return null; + } + if ( !$this->isPropertyPath($pairedProperty) && !\in_array($pairedProperty, $pairedClassProperties, true) @@ -135,7 +157,25 @@ class: $class, ); if (\count($attributesWithoutClass) >= 1) { - return $attributesWithoutClass[0]->property; + $pairedProperty = $attributesWithoutClass[0]->property; + + if ($pairedProperty === null) { + return null; + } + + if ( + !$this->isPropertyPath($pairedProperty) + && !\in_array($pairedProperty, $pairedClassProperties, true) + ) { + throw new PairedPropertyNotFoundException( + class: $class, + property: $property, + pairedClass: $pairedClass, + pairedProperty: $pairedProperty, + ); + } + + return $pairedProperty; } // if not found diff --git a/src/Transformer/MetadataUtil/PropertyMappingResolverInterface.php b/src/Transformer/MetadataUtil/PropertyMappingResolverInterface.php index 50385d62..aa59257e 100644 --- a/src/Transformer/MetadataUtil/PropertyMappingResolverInterface.php +++ b/src/Transformer/MetadataUtil/PropertyMappingResolverInterface.php @@ -21,7 +21,7 @@ interface PropertyMappingResolverInterface /** * @param class-string $sourceClass * @param class-string $targetClass - * @return list + * @return list */ public function getPropertiesToMap( string $sourceClass, diff --git a/src/Transformer/ObjectToObjectMetadata/Implementation/ObjectToObjectMetadataFactory.php b/src/Transformer/ObjectToObjectMetadata/Implementation/ObjectToObjectMetadataFactory.php index 5ae8e99e..b73585d7 100644 --- a/src/Transformer/ObjectToObjectMetadata/Implementation/ObjectToObjectMetadataFactory.php +++ b/src/Transformer/ObjectToObjectMetadata/Implementation/ObjectToObjectMetadataFactory.php @@ -97,6 +97,12 @@ public function createObjectToObjectMetadata( $serviceMethodSpecification = $this->propertyMapperResolver ->getPropertyMapper($sourceClass, $targetClass, $targetProperty); + // if source property is null, then skip + + if ($sourceProperty === null) { + continue; + } + // generate source & target property metadata $sourcePropertyMetadata = $this->propertyMetadataFactory diff --git a/tests/config/rekalogika-mapper/generated-mappings.php b/tests/config/rekalogika-mapper/generated-mappings.php index 3916e3be..3e9251d1 100644 --- a/tests/config/rekalogika-mapper/generated-mappings.php +++ b/tests/config/rekalogika-mapper/generated-mappings.php @@ -538,59 +538,77 @@ ); $mappingCollection->addObjectMapping( - // tests/src/IntegrationTest/MapAttributeTest.php on lines 92, 112 + // tests/src/IntegrationTest/MapAttributeTest.php on lines 95, 115 source: \Rekalogika\Mapper\Tests\Fixtures\MapAttribute\OtherObject::class, target: \Rekalogika\Mapper\Tests\Fixtures\MapAttribute\SomeObjectDto::class ); $mappingCollection->addObjectMapping( - // tests/src/IntegrationTest/MapAttributeTest.php on line 52 + // tests/src/IntegrationTest/MapAttributeTest.php on line 55 source: \Rekalogika\Mapper\Tests\Fixtures\MapAttribute\SomeObject::class, target: \Rekalogika\Mapper\Tests\Fixtures\MapAttribute\ObjectExtendingSomeObjectDto::class ); $mappingCollection->addObjectMapping( - // tests/src/IntegrationTest/MapAttributeTest.php on line 72 + // tests/src/IntegrationTest/MapAttributeTest.php on line 75 source: \Rekalogika\Mapper\Tests\Fixtures\MapAttribute\SomeObject::class, target: \Rekalogika\Mapper\Tests\Fixtures\MapAttribute\ObjectOverridingSomeObjectDto::class ); $mappingCollection->addObjectMapping( - // tests/src/IntegrationTest/MapAttributeTest.php on line 32 + // tests/src/IntegrationTest/MapAttributeTest.php on line 35 source: \Rekalogika\Mapper\Tests\Fixtures\MapAttribute\SomeObject::class, target: \Rekalogika\Mapper\Tests\Fixtures\MapAttribute\SomeObjectDto::class ); $mappingCollection->addObjectMapping( - // tests/src/IntegrationTest/MapAttributeTest.php on line 141 + // tests/src/IntegrationTest/MapAttributeTest.php on line 161 + source: \Rekalogika\Mapper\Tests\Fixtures\MapAttribute\SomeObject::class, + target: \Rekalogika\Mapper\Tests\Fixtures\MapAttribute\SomeObjectSkippingMappingDto::class + ); + + $mappingCollection->addObjectMapping( + // tests/src/IntegrationTest/MapAttributeTest.php on line 144 source: \Rekalogika\Mapper\Tests\Fixtures\MapAttribute\SomeObject::class, target: \Rekalogika\Mapper\Tests\Fixtures\MapAttribute\SomeObjectWithInvalidTargetDto::class ); $mappingCollection->addObjectMapping( - // tests/src/IntegrationTest/MapAttributeTest.php on line 132 + // tests/src/IntegrationTest/MapAttributeTest.php on line 151 + source: \Rekalogika\Mapper\Tests\Fixtures\MapAttribute\SomeObject::class, + target: \Rekalogika\Mapper\Tests\Fixtures\MapAttribute\SomeObjectWithSamePropertyNameDto::class + ); + + $mappingCollection->addObjectMapping( + // tests/src/IntegrationTest/MapAttributeTest.php on line 135 source: \Rekalogika\Mapper\Tests\Fixtures\MapAttribute\SomeObject::class, target: \Rekalogika\Mapper\Tests\Fixtures\MapAttribute\SomeObjectWithUnpromotedConstructorDto::class ); $mappingCollection->addObjectMapping( - // tests/src/IntegrationTest/MapAttributeTest.php on line 122 + // tests/src/IntegrationTest/MapAttributeTest.php on line 125 source: \Rekalogika\Mapper\Tests\Fixtures\MapAttribute\SomeObjectDto::class, target: \Rekalogika\Mapper\Tests\Fixtures\MapAttribute\ObjectExtendingOtherObject::class ); $mappingCollection->addObjectMapping( - // tests/src/IntegrationTest/MapAttributeTest.php on line 102 + // tests/src/IntegrationTest/MapAttributeTest.php on line 105 source: \Rekalogika\Mapper\Tests\Fixtures\MapAttribute\SomeObjectDto::class, target: \Rekalogika\Mapper\Tests\Fixtures\MapAttribute\OtherObject::class ); $mappingCollection->addObjectMapping( - // tests/src/IntegrationTest/MapAttributeTest.php on lines 42, 62, 82 + // tests/src/IntegrationTest/MapAttributeTest.php on lines 45, 65, 85 source: \Rekalogika\Mapper\Tests\Fixtures\MapAttribute\SomeObjectDto::class, target: \Rekalogika\Mapper\Tests\Fixtures\MapAttribute\SomeObject::class ); + $mappingCollection->addObjectMapping( + // tests/src/IntegrationTest/MapAttributeTest.php on line 171 + source: \Rekalogika\Mapper\Tests\Fixtures\MapAttribute\SomeObjectSkippingMapping::class, + target: \Rekalogika\Mapper\Tests\Fixtures\MapAttribute\SomeObjectWithSamePropertyNameDto::class + ); + $mappingCollection->addObjectMapping( // tests/src/IntegrationTest/MapPropertyPathTest.php on line 297 source: \Rekalogika\Mapper\Tests\Fixtures\MapPropertyPathDto\BookDto::class, diff --git a/tests/src/Fixtures/MapAttribute/SomeObjectSkippingMapping.php b/tests/src/Fixtures/MapAttribute/SomeObjectSkippingMapping.php new file mode 100644 index 00000000..af38dfe1 --- /dev/null +++ b/tests/src/Fixtures/MapAttribute/SomeObjectSkippingMapping.php @@ -0,0 +1,38 @@ + + * + * For the full copyright and license information, please view the LICENSE file + * that was distributed with this source code. + */ + +namespace Rekalogika\Mapper\Tests\Fixtures\MapAttribute; + +use Rekalogika\Mapper\Attribute\Map; + +class SomeObjectSkippingMapping +{ + #[Map(property: null)] + public ?string $sourcePropertyA = null; + + #[Map(property: null)] + public ?string $sourcePropertyB = null; + + #[Map(property: null)] + public ?string $sourcePropertyC = null; + + public static function preinitialized(): self + { + $object = new self(); + $object->sourcePropertyA = 'sourcePropertyA'; + $object->sourcePropertyB = 'sourcePropertyB'; + $object->sourcePropertyC = 'sourcePropertyC'; + + return $object; + } +} diff --git a/tests/src/Fixtures/MapAttribute/SomeObjectSkippingMappingDto.php b/tests/src/Fixtures/MapAttribute/SomeObjectSkippingMappingDto.php new file mode 100644 index 00000000..12894733 --- /dev/null +++ b/tests/src/Fixtures/MapAttribute/SomeObjectSkippingMappingDto.php @@ -0,0 +1,29 @@ + + * + * For the full copyright and license information, please view the LICENSE file + * that was distributed with this source code. + */ + +namespace Rekalogika\Mapper\Tests\Fixtures\MapAttribute; + +use Rekalogika\Mapper\Attribute\Map; + +class SomeObjectSkippingMappingDto +{ + #[Map(property: null)] + public ?string $sourcePropertyA = null; + + #[Map(property: null)] + public ?string $sourcePropertyB = null; + + #[Map(property: null)] + public ?string $sourcePropertyC = null; + +} diff --git a/tests/src/Fixtures/MapAttribute/SomeObjectWithSamePropertyNameDto.php b/tests/src/Fixtures/MapAttribute/SomeObjectWithSamePropertyNameDto.php new file mode 100644 index 00000000..df61837b --- /dev/null +++ b/tests/src/Fixtures/MapAttribute/SomeObjectWithSamePropertyNameDto.php @@ -0,0 +1,24 @@ + + * + * For the full copyright and license information, please view the LICENSE file + * that was distributed with this source code. + */ + +namespace Rekalogika\Mapper\Tests\Fixtures\MapAttribute; + +class SomeObjectWithSamePropertyNameDto +{ + public ?string $sourcePropertyA = null; + + public ?string $sourcePropertyB = null; + + public ?string $sourcePropertyC = null; + +} diff --git a/tests/src/IntegrationTest/MapAttributeTest.php b/tests/src/IntegrationTest/MapAttributeTest.php index 7a2a4453..17d13d51 100644 --- a/tests/src/IntegrationTest/MapAttributeTest.php +++ b/tests/src/IntegrationTest/MapAttributeTest.php @@ -20,7 +20,10 @@ use Rekalogika\Mapper\Tests\Fixtures\MapAttribute\OtherObject; use Rekalogika\Mapper\Tests\Fixtures\MapAttribute\SomeObject; use Rekalogika\Mapper\Tests\Fixtures\MapAttribute\SomeObjectDto; +use Rekalogika\Mapper\Tests\Fixtures\MapAttribute\SomeObjectSkippingMapping; +use Rekalogika\Mapper\Tests\Fixtures\MapAttribute\SomeObjectSkippingMappingDto; use Rekalogika\Mapper\Tests\Fixtures\MapAttribute\SomeObjectWithInvalidTargetDto; +use Rekalogika\Mapper\Tests\Fixtures\MapAttribute\SomeObjectWithSamePropertyNameDto; use Rekalogika\Mapper\Tests\Fixtures\MapAttribute\SomeObjectWithUnpromotedConstructorDto; use Rekalogika\Mapper\Transformer\Exception\PairedPropertyNotFoundException; @@ -141,4 +144,34 @@ public function testMapAttributeWithInvalidProperty(): void $target = $this->mapper->map($source, SomeObjectWithInvalidTargetDto::class); } + + public function testToSameProperty(): void + { + $source = SomeObject::preinitialized(); + $target = $this->mapper->map($source, SomeObjectWithSamePropertyNameDto::class); + + $this->assertEquals('sourcePropertyA', $target->sourcePropertyA); + $this->assertEquals('sourcePropertyB', $target->sourcePropertyB); + $this->assertEquals('sourcePropertyC', $target->sourcePropertyC); + } + + public function testToSamePropertyButUnmapped(): void + { + $source = SomeObject::preinitialized(); + $target = $this->mapper->map($source, SomeObjectSkippingMappingDto::class); + + $this->assertNull($target->sourcePropertyA); + $this->assertNull($target->sourcePropertyB); + $this->assertNull($target->sourcePropertyC); + } + + public function testIgnoringFromSourceSide(): void + { + $source = SomeObjectSkippingMapping::preinitialized(); + $target = $this->mapper->map($source, SomeObjectWithSamePropertyNameDto::class); + + $this->assertNull($target->sourcePropertyA); + $this->assertNull($target->sourcePropertyB); + $this->assertNull($target->sourcePropertyC); + } }