From bc9fe6578a235e7729ff801e9fa81f800da869ca Mon Sep 17 00:00:00 2001 From: Priyadi Iman Nurcahyo <1102197+priyadi@users.noreply.github.com> Date: Sat, 12 Oct 2024 01:04:46 +0700 Subject: [PATCH] feat: throws exception if the paired property does not exist in the other class if the `Map` attribute has `class` set --- CHANGELOG.md | 2 + .../PairedPropertyNotFoundException.php | 44 +++++++++++++++++++ .../PropertyMappingResolver.php | 29 +++++++++++- .../rekalogika-mapper/generated-mappings.php | 22 ++++++---- .../SomeObjectWithInvalidTargetDto.php | 22 ++++++++++ .../src/IntegrationTest/MapAttributeTest.php | 10 +++++ 6 files changed, 119 insertions(+), 10 deletions(-) create mode 100644 src/Transformer/Exception/PairedPropertyNotFoundException.php create mode 100644 tests/src/Fixtures/MapAttribute/SomeObjectWithInvalidTargetDto.php diff --git a/CHANGELOG.md b/CHANGELOG.md index db60d8cf..08fda72c 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,6 +5,8 @@ * refactor: spin off `resolveTargetClass()` to separate class * perf: proxy warming * feat: process proxies of all classes in the inheritance chain +* feat: throws exception if the paired property does not exist in the other + class if the `Map` attribute has `class` set ## 1.10.0 diff --git a/src/Transformer/Exception/PairedPropertyNotFoundException.php b/src/Transformer/Exception/PairedPropertyNotFoundException.php new file mode 100644 index 00000000..b33550dc --- /dev/null +++ b/src/Transformer/Exception/PairedPropertyNotFoundException.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\Transformer\Exception; + +use Rekalogika\Mapper\Exception\ExceptionInterface; + +/** + * @internal + */ +class PairedPropertyNotFoundException extends \LogicException implements ExceptionInterface +{ + /** + * @param class-string $class + */ + public function __construct( + string $class, + string $property, + string $pairedClass, + string $pairedProperty, + ) { + $message = \sprintf( + 'Trying to map class "%s" property "%s" to class "%s" property "%s" according to the "Map" attribute, but the property "%s" is not found in class "%s"', + $class, + $property, + $pairedClass, + $pairedProperty, + $pairedProperty, + $pairedClass, + ); + + parent::__construct($message); + } +} diff --git a/src/Transformer/MetadataUtil/PropertyMappingResolver/PropertyMappingResolver.php b/src/Transformer/MetadataUtil/PropertyMappingResolver/PropertyMappingResolver.php index f1757d49..37847a93 100644 --- a/src/Transformer/MetadataUtil/PropertyMappingResolver/PropertyMappingResolver.php +++ b/src/Transformer/MetadataUtil/PropertyMappingResolver/PropertyMappingResolver.php @@ -14,6 +14,7 @@ namespace Rekalogika\Mapper\Transformer\MetadataUtil\PropertyMappingResolver; use Rekalogika\Mapper\Attribute\Map; +use Rekalogika\Mapper\Transformer\Exception\PairedPropertyNotFoundException; use Rekalogika\Mapper\Transformer\MetadataUtil\PropertyMappingResolverInterface; use Rekalogika\Mapper\Util\ClassUtil; use Symfony\Component\PropertyInfo\PropertyListExtractorInterface; @@ -43,6 +44,7 @@ public function getPropertiesToMap( class: $targetClass, property: $targetProperty, pairedClass: $sourceClass, + pairedClassProperties: $sourceProperties, ); $targetPropertyToSourceProperty[$targetProperty] = $sourceProperty; @@ -53,6 +55,7 @@ class: $targetClass, class: $sourceClass, property: $sourceProperty, pairedClass: $targetClass, + pairedClassProperties: $targetProperties, ); $targetPropertyToSourceProperty[$targetProperty] = $sourceProperty; @@ -64,6 +67,7 @@ class: $sourceClass, class: $targetClass, property: $targetProperty, pairedClass: $sourceClass, + pairedClassProperties: $sourceProperties, ); $targetPropertyToSourceProperty[$targetProperty] = $sourceProperty; @@ -82,11 +86,13 @@ class: $targetClass, /** * @param class-string $class * @param class-string $pairedClass + * @param list $pairedClassProperties */ private function determinePairedProperty( string $class, string $property, string $pairedClass, + array $pairedClassProperties, ): string { $attributes = ClassUtil::getPropertyAttributes( class: $class, @@ -104,7 +110,21 @@ class: $class, )); if (\count($attributesWithClass) >= 1) { - return $attributesWithClass[0]->property; + $pairedProperty = $attributesWithClass[0]->property; + + if ( + !$this->isPropertyPath($pairedProperty) + && !\in_array($pairedProperty, $pairedClassProperties, true) + ) { + throw new PairedPropertyNotFoundException( + class: $class, + property: $property, + pairedClass: $pairedClass, + pairedProperty: $pairedProperty, + ); + } + + return $pairedProperty; } // process attributes without pairedClass @@ -125,7 +145,7 @@ class: $class, /** * @param class-string $class - * @return array + * @return list */ private function listProperties( string $class, @@ -134,4 +154,9 @@ private function listProperties( return array_values($properties); } + + private function isPropertyPath(string $property): bool + { + return str_contains($property, '.') || str_contains($property, '['); + } } diff --git a/tests/config/rekalogika-mapper/generated-mappings.php b/tests/config/rekalogika-mapper/generated-mappings.php index 95c2839b..b208898f 100644 --- a/tests/config/rekalogika-mapper/generated-mappings.php +++ b/tests/config/rekalogika-mapper/generated-mappings.php @@ -484,49 +484,55 @@ ); $mappingCollection->addObjectMapping( - // tests/src/IntegrationTest/MapAttributeTest.php on lines 90, 110 + // tests/src/IntegrationTest/MapAttributeTest.php on lines 92, 112 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 50 + // tests/src/IntegrationTest/MapAttributeTest.php on line 52 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 70 + // tests/src/IntegrationTest/MapAttributeTest.php on line 72 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 30 + // tests/src/IntegrationTest/MapAttributeTest.php on line 32 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 130 + // tests/src/IntegrationTest/MapAttributeTest.php on line 141 + 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 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 120 + // tests/src/IntegrationTest/MapAttributeTest.php on line 122 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 100 + // tests/src/IntegrationTest/MapAttributeTest.php on line 102 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 40, 60, 80 + // tests/src/IntegrationTest/MapAttributeTest.php on lines 42, 62, 82 source: \Rekalogika\Mapper\Tests\Fixtures\MapAttribute\SomeObjectDto::class, target: \Rekalogika\Mapper\Tests\Fixtures\MapAttribute\SomeObject::class ); diff --git a/tests/src/Fixtures/MapAttribute/SomeObjectWithInvalidTargetDto.php b/tests/src/Fixtures/MapAttribute/SomeObjectWithInvalidTargetDto.php new file mode 100644 index 00000000..08426c16 --- /dev/null +++ b/tests/src/Fixtures/MapAttribute/SomeObjectWithInvalidTargetDto.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\MapAttribute; + +use Rekalogika\Mapper\Attribute\Map; + +class SomeObjectWithInvalidTargetDto +{ + #[Map(property: 'foo', class: SomeObject::class)] + public ?string $targetPropertyA = null; +} diff --git a/tests/src/IntegrationTest/MapAttributeTest.php b/tests/src/IntegrationTest/MapAttributeTest.php index 99ebf9bf..7a2a4453 100644 --- a/tests/src/IntegrationTest/MapAttributeTest.php +++ b/tests/src/IntegrationTest/MapAttributeTest.php @@ -20,7 +20,9 @@ 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\SomeObjectWithInvalidTargetDto; use Rekalogika\Mapper\Tests\Fixtures\MapAttribute\SomeObjectWithUnpromotedConstructorDto; +use Rekalogika\Mapper\Transformer\Exception\PairedPropertyNotFoundException; class MapAttributeTest extends FrameworkTestCase { @@ -131,4 +133,12 @@ public function testMapAttributeToUnpromotedConstructorParameter(): void $this->assertEquals('sourcePropertyA', $target->targetPropertyA); } + + public function testMapAttributeWithInvalidProperty(): void + { + $this->expectException(PairedPropertyNotFoundException::class); + $source = SomeObject::preinitialized(); + $target = $this->mapper->map($source, SomeObjectWithInvalidTargetDto::class); + + } }