diff --git a/CHANGELOG.md b/CHANGELOG.md index d9f7f8d1..f7e8964a 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,9 @@ # CHANGELOG +## 1.10.1 + +* refactor: spin off `resolveTargetClass()` to separate class + ## 1.10.0 * perf: warmable cache diff --git a/src/Transformer/MetadataUtil/MetadataUtilLocator.php b/src/Transformer/MetadataUtil/MetadataUtilLocator.php index d4853cc7..e17d3c0d 100644 --- a/src/Transformer/MetadataUtil/MetadataUtilLocator.php +++ b/src/Transformer/MetadataUtil/MetadataUtilLocator.php @@ -25,6 +25,8 @@ use Rekalogika\Mapper\Transformer\MetadataUtil\PropertyAccessInfoExtractor\PropertyAccessInfoExtractor; use Rekalogika\Mapper\Transformer\MetadataUtil\PropertyMappingResolver\PropertyMappingResolver; use Rekalogika\Mapper\Transformer\MetadataUtil\PropertyMetadataFactory\PropertyMetadataFactory; +use Rekalogika\Mapper\Transformer\MetadataUtil\TargetClassResolver\CachingTargetClassResolver; +use Rekalogika\Mapper\Transformer\MetadataUtil\TargetClassResolver\TargetClassResolver; use Rekalogika\Mapper\Transformer\MetadataUtil\UnalterableDeterminer\UnalterableDeterminer; use Rekalogika\Mapper\Transformer\ObjectToObjectMetadata\Implementation\ObjectToObjectMetadataFactory; use Rekalogika\Mapper\Transformer\ObjectToObjectMetadata\ObjectToObjectMetadataFactoryInterface; @@ -55,6 +57,8 @@ final class MetadataUtilLocator private ?ObjectToObjectMetadataFactoryInterface $objectToObjectMetadataFactory = null; + private ?TargetClassResolverInterface $targetClassResolver = null; + public function __construct( private readonly PropertyListExtractorInterface $propertyListExtractor, private readonly PropertyTypeExtractorInterface $propertyTypeExtractor, @@ -133,6 +137,13 @@ private function getPropertyMappingResolver(): PropertyMappingResolverInterface ); } + private function getTargetClassResolver(): TargetClassResolverInterface + { + return $this->targetClassResolver ??= new CachingTargetClassResolver( + new TargetClassResolver(), + ); + } + public function getObjectToObjectMetadataFactory(): ObjectToObjectMetadataFactoryInterface { return $this->objectToObjectMetadataFactory @@ -142,6 +153,7 @@ public function getObjectToObjectMetadataFactory(): ObjectToObjectMetadataFactor propertyMetadataFactory: $this->getPropertyMetadataFactory(), classMetadataFactory: $this->getClassMetadataFactory(), propertyMappingResolver: $this->getPropertyMappingResolver(), + targetClassResolver: $this->getTargetClassResolver(), ); } } diff --git a/src/Transformer/MetadataUtil/TargetClassResolver/CachingTargetClassResolver.php b/src/Transformer/MetadataUtil/TargetClassResolver/CachingTargetClassResolver.php new file mode 100644 index 00000000..30198510 --- /dev/null +++ b/src/Transformer/MetadataUtil/TargetClassResolver/CachingTargetClassResolver.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\Transformer\MetadataUtil\TargetClassResolver; + +use Rekalogika\Mapper\Transformer\MetadataUtil\TargetClassResolverInterface; + +/** + * @internal + */ +final class CachingTargetClassResolver implements TargetClassResolverInterface +{ + /** + * @var array> + */ + private array $cache = []; + + public function __construct( + private readonly TargetClassResolverInterface $decorated, + ) {} + + #[\Override] + public function resolveTargetClass(string $sourceClass, string $targetClass): string + { + return $this->cache[$sourceClass][$targetClass] + ??= $this->decorated->resolveTargetClass($sourceClass, $targetClass); + } +} diff --git a/src/Transformer/MetadataUtil/TargetClassResolver/TargetClassResolver.php b/src/Transformer/MetadataUtil/TargetClassResolver/TargetClassResolver.php new file mode 100644 index 00000000..b263733a --- /dev/null +++ b/src/Transformer/MetadataUtil/TargetClassResolver/TargetClassResolver.php @@ -0,0 +1,75 @@ + + * + * For the full copyright and license information, please view the LICENSE file + * that was distributed with this source code. + */ + +namespace Rekalogika\Mapper\Transformer\MetadataUtil\TargetClassResolver; + +use Rekalogika\Mapper\Attribute\InheritanceMap; +use Rekalogika\Mapper\Transformer\Exception\SourceClassNotInInheritanceMapException; +use Rekalogika\Mapper\Transformer\MetadataUtil\TargetClassResolverInterface; +use Rekalogika\Mapper\Util\ClassUtil; + +/** + * @internal + */ +final readonly class TargetClassResolver implements TargetClassResolverInterface +{ + public function resolveTargetClass( + string $sourceClass, + string $targetClass, + ): string { + $sourceReflection = new \ReflectionClass($sourceClass); + $targetReflection = new \ReflectionClass($targetClass); + + $targetAttributes = $targetReflection->getAttributes(InheritanceMap::class); + + if ($targetAttributes !== []) { + // 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 + + $sourceClasses = ClassUtil::getAllClassesFromObject($sourceClass); + + foreach ($sourceClasses as $currentSourceClass) { + $sourceReflection = new \ReflectionClass($currentSourceClass); + $sourceAttributes = $sourceReflection->getAttributes(InheritanceMap::class); + + if ($sourceAttributes !== []) { + $inheritanceMap = $sourceAttributes[0]->newInstance(); + + $resolvedTargetClass = $inheritanceMap->getSourceClassFromTargetClass($sourceClass); + + if ($resolvedTargetClass === null) { + throw new SourceClassNotInInheritanceMapException($currentSourceClass, $targetClass); + } + + return $resolvedTargetClass; + } + } + } + + return $targetClass; + } +} diff --git a/src/Transformer/MetadataUtil/TargetClassResolverInterface.php b/src/Transformer/MetadataUtil/TargetClassResolverInterface.php new file mode 100644 index 00000000..ad6b8570 --- /dev/null +++ b/src/Transformer/MetadataUtil/TargetClassResolverInterface.php @@ -0,0 +1,36 @@ + + * + * For the full copyright and license information, please view the LICENSE file + * that was distributed with this source code. + */ + +namespace Rekalogika\Mapper\Transformer\MetadataUtil; + +/** + * Resolves the target type hint to the actual class name. Especially useful + * for resolving abstract classes or interfaces to concrete classes. + * + * @internal + */ +interface TargetClassResolverInterface +{ + /** + * Resolves the target type hint to the actual class name. Especially useful + * for resolving abstract classes or interfaces to concrete classes. + * + * @param class-string $sourceClass + * @param class-string $targetClass + * @return class-string + */ + public function resolveTargetClass( + string $sourceClass, + string $targetClass, + ): string; +} diff --git a/src/Transformer/ObjectToObjectMetadata/Implementation/ObjectToObjectMetadataFactory.php b/src/Transformer/ObjectToObjectMetadata/Implementation/ObjectToObjectMetadataFactory.php index a6547f1e..871f84ed 100644 --- a/src/Transformer/ObjectToObjectMetadata/Implementation/ObjectToObjectMetadataFactory.php +++ b/src/Transformer/ObjectToObjectMetadata/Implementation/ObjectToObjectMetadataFactory.php @@ -14,7 +14,6 @@ namespace Rekalogika\Mapper\Transformer\ObjectToObjectMetadata\Implementation; use Rekalogika\Mapper\Attribute\Eager; -use Rekalogika\Mapper\Attribute\InheritanceMap; use Rekalogika\Mapper\CustomMapper\PropertyMapperResolverInterface; use Rekalogika\Mapper\Proxy\Exception\ProxyNotSupportedException; use Rekalogika\Mapper\Proxy\ProxyFactoryInterface; @@ -23,10 +22,10 @@ use Rekalogika\Mapper\Transformer\Context\TargetClassAttributes; use Rekalogika\Mapper\Transformer\Context\TargetPropertyAttributes; use Rekalogika\Mapper\Transformer\Exception\InternalClassUnsupportedException; -use Rekalogika\Mapper\Transformer\Exception\SourceClassNotInInheritanceMapException; use Rekalogika\Mapper\Transformer\MetadataUtil\ClassMetadataFactoryInterface; use Rekalogika\Mapper\Transformer\MetadataUtil\PropertyMappingResolverInterface; use Rekalogika\Mapper\Transformer\MetadataUtil\PropertyMetadataFactoryInterface; +use Rekalogika\Mapper\Transformer\MetadataUtil\TargetClassResolverInterface; use Rekalogika\Mapper\Transformer\ObjectToObjectMetadata\ObjectToObjectMetadata; use Rekalogika\Mapper\Transformer\ObjectToObjectMetadata\ObjectToObjectMetadataFactoryInterface; use Rekalogika\Mapper\Transformer\ObjectToObjectMetadata\PropertyMapping; @@ -44,70 +43,18 @@ public function __construct( private PropertyMetadataFactoryInterface $propertyMetadataFactory, private ClassMetadataFactoryInterface $classMetadataFactory, private PropertyMappingResolverInterface $propertyMappingResolver, + private TargetClassResolverInterface $targetClassResolver, ) {} - /** - * @param class-string $sourceClass - * @param class-string $targetClass - * @return class-string - */ - private function resolveTargetClass( - string $sourceClass, - string $targetClass, - ): string { - $sourceReflection = new \ReflectionClass($sourceClass); - $targetReflection = new \ReflectionClass($targetClass); - - $targetAttributes = $targetReflection->getAttributes(InheritanceMap::class); - - if ($targetAttributes !== []) { - // 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 - - $sourceClasses = ClassUtil::getAllClassesFromObject($sourceClass); - - foreach ($sourceClasses as $currentSourceClass) { - $sourceReflection = new \ReflectionClass($currentSourceClass); - $sourceAttributes = $sourceReflection->getAttributes(InheritanceMap::class); - - if ($sourceAttributes !== []) { - $inheritanceMap = $sourceAttributes[0]->newInstance(); - - $resolvedTargetClass = $inheritanceMap->getSourceClassFromTargetClass($sourceClass); - - if ($resolvedTargetClass === null) { - throw new SourceClassNotInInheritanceMapException($currentSourceClass, $targetClass); - } - - return $resolvedTargetClass; - } - } - } - - return $targetClass; - } - #[\Override] public function createObjectToObjectMetadata( string $sourceClass, string $targetClass, ): ObjectToObjectMetadata { $providedTargetClass = $targetClass; - $targetClass = $this->resolveTargetClass($sourceClass, $providedTargetClass); + + $targetClass = $this->targetClassResolver + ->resolveTargetClass($sourceClass, $providedTargetClass); $sourceClassMetadata = $this->classMetadataFactory->createClassMetadata($sourceClass); $targetClassMetadata = $this->classMetadataFactory->createClassMetadata($targetClass);