diff --git a/CHANGELOG.md b/CHANGELOG.md index 586ed68..70b69e3 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,10 @@ # CHANGELOG +## 1.5.0 + +* feat: utilize `InheritanceMap` on the source side to determine the target + class + ## 1.4.0 * feat: `ramsey/uuid` support diff --git a/src/Attribute/InheritanceMap.php b/src/Attribute/InheritanceMap.php index 1f9cd08..c6f8622 100644 --- a/src/Attribute/InheritanceMap.php +++ b/src/Attribute/InheritanceMap.php @@ -40,4 +40,13 @@ public function getTargetClassFromSourceClass(string $sourceClass): ?string { return $this->map[$sourceClass] ?? null; } + + /** + * @param class-string $targetClass + * @return class-string|null + */ + public function getSourceClassFromTargetClass(string $targetClass): ?string + { + return array_search($targetClass, $this->map, true) ?: null; + } } diff --git a/src/Transformer/ObjectToObjectMetadata/Implementation/ObjectToObjectMetadataFactory.php b/src/Transformer/ObjectToObjectMetadata/Implementation/ObjectToObjectMetadataFactory.php index 7b0a99b..cf1044a 100644 --- a/src/Transformer/ObjectToObjectMetadata/Implementation/ObjectToObjectMetadataFactory.php +++ b/src/Transformer/ObjectToObjectMetadata/Implementation/ObjectToObjectMetadataFactory.php @@ -57,31 +57,87 @@ public function __construct( ) { } - public function createObjectToObjectMetadata( + /** + * @param class-string $sourceClass + * @param class-string $targetClass + * @return class-string + */ + private function resolveTargetClass( string $sourceClass, - string $targetClass, - ): ObjectToObjectMetadata { - $providedTargetClass = $targetClass; - + string $targetClass + ): string { $sourceReflection = new \ReflectionClass($sourceClass); - $providedTargetReflection = new \ReflectionClass($providedTargetClass); + $targetReflection = new \ReflectionClass($targetClass); + + $targetAttributes = $targetReflection->getAttributes(InheritanceMap::class); + + if (count($targetAttributes) > 0) { + // if the target has an InheritanceMap, we try to resolve the target + // class using the InheritanceMap + + $inheritanceMap = $targetAttributes[0]->newInstance(); + + $resolvedTargetClass = $inheritanceMap->getTargetClassFromSourceClass($sourceClass); + + if ($resolvedTargetClass === null) { + throw new SourceClassNotInInheritanceMapException($sourceClass, $targetClass); + } + + return $resolvedTargetClass; + } elseif ($targetReflection->isAbstract() || $targetReflection->isInterface()) { + // if target doesn't have an inheritance map, but is also abstract + // or an interface, we try to find the InheritanceMap from the + // source + + $parents = class_parents($sourceClass, true); + if ($parents === false) { + $parents = []; + } + + $interfaces = class_implements($sourceClass, true); + if ($interfaces === false) { + $interfaces = []; + } + + $sourceClasses = [ + $sourceClass, + ...$parents, + ...$interfaces, + ]; - // check inheritance map + foreach ($sourceClasses as $currentSourceClass) { + $sourceReflection = new \ReflectionClass($currentSourceClass); + $sourceAttributes = $sourceReflection->getAttributes(InheritanceMap::class); - $attributes = $providedTargetReflection->getAttributes(InheritanceMap::class); + if (count($sourceAttributes) > 0) { + $inheritanceMap = $sourceAttributes[0]->newInstance(); - if (count($attributes) > 0) { - $inheritanceMap = $attributes[0]->newInstance(); + $resolvedTargetClass = $inheritanceMap->getSourceClassFromTargetClass($sourceClass); - $targetClass = $inheritanceMap->getTargetClassFromSourceClass($sourceClass); + if ($resolvedTargetClass === null) { + throw new SourceClassNotInInheritanceMapException($currentSourceClass, $targetClass); + } - if ($targetClass === null) { - throw new SourceClassNotInInheritanceMapException($sourceClass, $providedTargetClass); + return $resolvedTargetClass; + } } } + return $targetClass; + } + + public function createObjectToObjectMetadata( + string $sourceClass, + string $targetClass, + ): ObjectToObjectMetadata { + $providedTargetClass = $targetClass; + $sourceReflection = new \ReflectionClass($sourceClass); + + $targetClass = $this->resolveTargetClass($sourceClass, $providedTargetClass); $targetReflection = new \ReflectionClass($targetClass); + // dynamic properties + $sourceAllowsDynamicProperties = $this->allowsDynamicProperties($sourceReflection); $targetAllowsDynamicProperties = $this->allowsDynamicProperties($targetReflection); diff --git a/tests/IntegrationTest/InheritanceReversedTest.php b/tests/IntegrationTest/InheritanceReversedTest.php new file mode 100644 index 0000000..3b13307 --- /dev/null +++ b/tests/IntegrationTest/InheritanceReversedTest.php @@ -0,0 +1,37 @@ + + * + * 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\Tests\Common\FrameworkTestCase; +use Rekalogika\Mapper\Tests\Fixtures\Inheritance\AbstractClass; +use Rekalogika\Mapper\Tests\Fixtures\Inheritance\ConcreteClassA; +use Rekalogika\Mapper\Tests\Fixtures\InheritanceDto\ConcreteClassADto; + +class InheritanceReversedTest extends FrameworkTestCase +{ + public function testMapDtoToAbstractClass(): void + { + $concreteClassADto = new ConcreteClassADto(); + $concreteClassADto->propertyInA = 'xxpropertyInA'; + $concreteClassADto->propertyInParent = 'xxpropertyInParent'; + + $result = $this->mapper->map($concreteClassADto, AbstractClass::class); + + /** @var ConcreteClassA $result */ + + $this->assertInstanceOf(ConcreteClassA::class, $result); + $this->assertSame('xxpropertyInA', $result->propertyInA); + $this->assertSame('xxpropertyInParent', $result->propertyInParent); + } +}