From 43ed976e3ff6b1ded30199844c7cff7e12083cd0 Mon Sep 17 00:00:00 2001 From: Priyadi Iman Nurcahyo <1102197+priyadi@users.noreply.github.com> Date: Sat, 13 Jan 2024 21:35:01 +0700 Subject: [PATCH] feat: Inheritance support. --- CHANGELOG.md | 1 + README.md | 3 +- config/services.php | 14 +- src/Attribute/InheritanceMap.php | 43 ++++++ src/Context/Context.php | 2 +- src/MapperFactory/MapperFactory.php | 13 ++ .../Exception/NotAClassException.php | 46 +++++++ ...ourceClassNotInInheritanceMapException.php | 33 +++++ src/Transformer/InheritanceMapTransformer.php | 130 ++++++++++++++++++ src/Transformer/ObjectToObjectTransformer.php | 9 +- tests/Fixtures/Inheritance/AbstractClass.php | 19 +++ tests/Fixtures/Inheritance/ConcreteClassA.php | 19 +++ tests/Fixtures/Inheritance/ConcreteClassB.php | 19 +++ tests/Fixtures/Inheritance/ConcreteClassC.php | 19 +++ .../InheritanceDto/AbstractClassDto.php | 28 ++++ .../AbstractClassWithoutMapDto.php | 18 +++ .../InheritanceDto/ConcreteClassADto.php | 19 +++ .../InheritanceDto/ConcreteClassBDto.php | 19 +++ .../InheritanceDto/ConcreteClassCDto.php | 19 +++ .../InheritanceDto/ImplementationADto.php | 19 +++ .../InheritanceDto/ImplementationBDto.php | 19 +++ .../InheritanceDto/ImplementationCDto.php | 19 +++ .../Fixtures/InheritanceDto/InterfaceDto.php | 27 ++++ .../InheritanceDto/InterfaceWithoutMapDto.php | 18 +++ tests/IntegrationTest/InheritanceTest.php | 81 +++++++++++ 25 files changed, 648 insertions(+), 8 deletions(-) create mode 100644 src/Attribute/InheritanceMap.php create mode 100644 src/Transformer/Exception/NotAClassException.php create mode 100644 src/Transformer/Exception/SourceClassNotInInheritanceMapException.php create mode 100644 src/Transformer/InheritanceMapTransformer.php create mode 100644 tests/Fixtures/Inheritance/AbstractClass.php create mode 100644 tests/Fixtures/Inheritance/ConcreteClassA.php create mode 100644 tests/Fixtures/Inheritance/ConcreteClassB.php create mode 100644 tests/Fixtures/Inheritance/ConcreteClassC.php create mode 100644 tests/Fixtures/InheritanceDto/AbstractClassDto.php create mode 100644 tests/Fixtures/InheritanceDto/AbstractClassWithoutMapDto.php create mode 100644 tests/Fixtures/InheritanceDto/ConcreteClassADto.php create mode 100644 tests/Fixtures/InheritanceDto/ConcreteClassBDto.php create mode 100644 tests/Fixtures/InheritanceDto/ConcreteClassCDto.php create mode 100644 tests/Fixtures/InheritanceDto/ImplementationADto.php create mode 100644 tests/Fixtures/InheritanceDto/ImplementationBDto.php create mode 100644 tests/Fixtures/InheritanceDto/ImplementationCDto.php create mode 100644 tests/Fixtures/InheritanceDto/InterfaceDto.php create mode 100644 tests/Fixtures/InheritanceDto/InterfaceWithoutMapDto.php create mode 100644 tests/IntegrationTest/InheritanceTest.php diff --git a/CHANGELOG.md b/CHANGELOG.md index 37a797bd..0274fa40 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -16,6 +16,7 @@ * refactor: Move `Context` to its own namespace. * style(Context): Rename `set` to `with` and `remove` to `without`. * refactor: Reintroduce `Context` to `MapperInterface`. +* feat: Inheritance support. ## 0.5.4 diff --git a/README.md b/README.md index 1def6453..5286d873 100644 --- a/README.md +++ b/README.md @@ -16,6 +16,8 @@ Full documentation is available at [rekalogika.dev/mapper](https://rekalogika.de * Constructor initialization. * Handles nested objects. * Handles recursion and circular references. +* Inheritance support. Maps to abstract classes and interfaces using an + inheritance map attribute. * Reads the type from PHP type declaration and PHPDoc annotations, including the type of the nested objects. * Handles `array`, `ArrayAccess` and `Traversable` objects, and the mapping @@ -34,7 +36,6 @@ Full documentation is available at [rekalogika.dev/mapper](https://rekalogika.de ## Future Features -* Mapping to interfaces and abstract classes. * Option to map to or from different property name? (seems to be a popular feature, but I prefer the native OOP way of doing it) * Option to read & write to private properties? diff --git a/config/services.php b/config/services.php index 8aea7213..9b14a3b8 100644 --- a/config/services.php +++ b/config/services.php @@ -25,6 +25,7 @@ use Rekalogika\Mapper\Transformer\ArrayToObjectTransformer; use Rekalogika\Mapper\Transformer\CopyTransformer; use Rekalogika\Mapper\Transformer\DateTimeTransformer; +use Rekalogika\Mapper\Transformer\InheritanceMapTransformer; use Rekalogika\Mapper\Transformer\NullTransformer; use Rekalogika\Mapper\Transformer\ObjectToArrayTransformer; use Rekalogika\Mapper\Transformer\ObjectToObjectTransformer; @@ -87,25 +88,29 @@ $services ->set('rekalogika.mapper.transformer.scalar_to_scalar', ScalarToScalarTransformer::class) - ->tag('rekalogika.mapper.transformer', ['priority' => -450]); + ->tag('rekalogika.mapper.transformer', ['priority' => -400]); $services ->set('rekalogika.mapper.transformer.datetime', DateTimeTransformer::class) - ->tag('rekalogika.mapper.transformer', ['priority' => -500]); + ->tag('rekalogika.mapper.transformer', ['priority' => -450]); $services ->set('rekalogika.mapper.transformer.string_to_backed_enum', StringToBackedEnumTransformer::class) - ->tag('rekalogika.mapper.transformer', ['priority' => -550]); + ->tag('rekalogika.mapper.transformer', ['priority' => -500]); $services ->set('rekalogika.mapper.method_mapper.transformer', ClassMethodTransformer::class) ->args([ service('rekalogika.mapper.method_mapper.sub_mapper'), ]) - ->tag('rekalogika.mapper.transformer', ['priority' => -600]); + ->tag('rekalogika.mapper.transformer', ['priority' => -550]); $services ->set('rekalogika.mapper.transformer.object_to_string', ObjectToStringTransformer::class) + ->tag('rekalogika.mapper.transformer', ['priority' => -600]); + + $services + ->set('rekalogika.mapper.transformer.inheritance_map', InheritanceMapTransformer::class) ->tag('rekalogika.mapper.transformer', ['priority' => -650]); $services @@ -146,6 +151,7 @@ ->set('rekalogika.mapper.transformer.null', CopyTransformer::class) ->tag('rekalogika.mapper.transformer', ['priority' => -1000]); + # mappingfactory $services diff --git a/src/Attribute/InheritanceMap.php b/src/Attribute/InheritanceMap.php new file mode 100644 index 00000000..d7c5573c --- /dev/null +++ b/src/Attribute/InheritanceMap.php @@ -0,0 +1,43 @@ + + * + * For the full copyright and license information, please view the LICENSE file + * that was distributed with this source code. + */ + +namespace Rekalogika\Mapper\Attribute; + +#[\Attribute(\Attribute::TARGET_CLASS)] +final readonly class InheritanceMap implements MapperAttributeInterface +{ + /** + * @param array $map + */ + public function __construct( + private array $map = [] + ) { + } + + /** + * @return array + */ + public function getMap(): array + { + return $this->map; + } + + /** + * @param class-string $sourceClass + * @return class-string|null + */ + public function getTargetClassFromSourceClass(string $sourceClass): ?string + { + return $this->map[$sourceClass] ?? null; + } +} diff --git a/src/Context/Context.php b/src/Context/Context.php index eb82f4bf..6b73606b 100644 --- a/src/Context/Context.php +++ b/src/Context/Context.php @@ -18,7 +18,7 @@ /** * @immutable */ -class Context +final readonly class Context { /** * @param array $context diff --git a/src/MapperFactory/MapperFactory.php b/src/MapperFactory/MapperFactory.php index 26d0b2cd..681b1a99 100644 --- a/src/MapperFactory/MapperFactory.php +++ b/src/MapperFactory/MapperFactory.php @@ -30,6 +30,7 @@ use Rekalogika\Mapper\Transformer\Contracts\TransformerInterface; use Rekalogika\Mapper\Transformer\CopyTransformer; use Rekalogika\Mapper\Transformer\DateTimeTransformer; +use Rekalogika\Mapper\Transformer\InheritanceMapTransformer; use Rekalogika\Mapper\Transformer\NullTransformer; use Rekalogika\Mapper\Transformer\ObjectToArrayTransformer; use Rekalogika\Mapper\Transformer\ObjectToObjectTransformer; @@ -87,6 +88,7 @@ class MapperFactory private ?TraversableToTraversableTransformer $traversableToTraversableTransformer = null; private ?CopyTransformer $copyTransformer = null; private ?ClassMethodTransformer $classMethodTransformer = null; + private ?InheritanceMapTransformer $inheritanceMapTransformer = null; private CacheItemPoolInterface $propertyInfoExtractorCache; private null|(PropertyInfoExtractorInterface&PropertyInitializableExtractorInterface) $propertyInfoExtractor = null; @@ -378,6 +380,15 @@ protected function getClassMethodTransformer(): ClassMethodTransformer return $this->classMethodTransformer; } + protected function getInheritanceMapTransformer(): InheritanceMapTransformer + { + if (null === $this->inheritanceMapTransformer) { + $this->inheritanceMapTransformer = new InheritanceMapTransformer(); + } + + return $this->inheritanceMapTransformer; + } + // // other services // @@ -407,6 +418,8 @@ protected function getTransformersIterator(): iterable => $this->getClassMethodTransformer(); yield 'ObjectToStringTransformer' => $this->getObjectToStringTransformer(); + yield 'InheritanceMapTransformer' + => $this->getInheritanceMapTransformer(); yield 'TraversableToArrayAccessTransformer' => $this->getTraversableToArrayAccessTransformer(); yield 'TraversableToTraversableTransformer' diff --git a/src/Transformer/Exception/NotAClassException.php b/src/Transformer/Exception/NotAClassException.php new file mode 100644 index 00000000..abae9ab6 --- /dev/null +++ b/src/Transformer/Exception/NotAClassException.php @@ -0,0 +1,46 @@ + + * + * 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\Attribute\InheritanceMap; + +class NotAClassException extends NotMappableValueException +{ + public function __construct(string $class) + { + /** @var class-string $class */ + + try { + $reflectionClass = new \ReflectionClass($class); + + if ($reflectionClass->isInterface()) { + parent::__construct(sprintf( + 'Trying to map to "%s", but it is an interface, not a class. If you want to map to an interface, you need to add the attribute "%s" to the interface."', + $class, + InheritanceMap::class + )); + } else { + parent::__construct(sprintf( + 'Trying to map to "%s", but it is not a class.', + $class, + )); + } + } catch (\ReflectionException) { + parent::__construct(sprintf( + 'The name "%s" is not a valid class.', + $class + )); + } + } +} diff --git a/src/Transformer/Exception/SourceClassNotInInheritanceMapException.php b/src/Transformer/Exception/SourceClassNotInInheritanceMapException.php new file mode 100644 index 00000000..86c23883 --- /dev/null +++ b/src/Transformer/Exception/SourceClassNotInInheritanceMapException.php @@ -0,0 +1,33 @@ + + * + * 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\Attribute\InheritanceMap; + +class SourceClassNotInInheritanceMapException extends NotMappableValueException +{ + /** + * @param class-string $sourceClass + * @param class-string $targetClass + */ + public function __construct(string $sourceClass, string $targetClass) + { + parent::__construct(sprintf( + 'Trying to map to a class with an inheritance map, but source class "%s" is not found in the "%s" attribute of the target class "%s"', + $sourceClass, + InheritanceMap::class, + $targetClass + )); + } +} diff --git a/src/Transformer/InheritanceMapTransformer.php b/src/Transformer/InheritanceMapTransformer.php new file mode 100644 index 00000000..15ee1ebb --- /dev/null +++ b/src/Transformer/InheritanceMapTransformer.php @@ -0,0 +1,130 @@ + + * + * For the full copyright and license information, please view the LICENSE file + * that was distributed with this source code. + */ + +namespace Rekalogika\Mapper\Transformer; + +use Rekalogika\Mapper\Attribute\InheritanceMap; +use Rekalogika\Mapper\Context\Context; +use Rekalogika\Mapper\Exception\InvalidArgumentException; +use Rekalogika\Mapper\Exception\UnexpectedValueException; +use Rekalogika\Mapper\ObjectCache\ObjectCache; +use Rekalogika\Mapper\Transformer\Contracts\MainTransformerAwareInterface; +use Rekalogika\Mapper\Transformer\Contracts\MainTransformerAwareTrait; +use Rekalogika\Mapper\Transformer\Contracts\TransformerInterface; +use Rekalogika\Mapper\Transformer\Contracts\TypeMapping; +use Rekalogika\Mapper\Transformer\Exception\SourceClassNotInInheritanceMapException; +use Rekalogika\Mapper\Util\TypeFactory; +use Symfony\Component\PropertyInfo\Type; + +final class InheritanceMapTransformer implements + TransformerInterface, + MainTransformerAwareInterface +{ + use MainTransformerAwareTrait; + + public function transform( + mixed $source, + mixed $target, + ?Type $sourceType, + ?Type $targetType, + Context $context + ): mixed { + + # source must be an object + + if (!is_object($source)) { + throw new InvalidArgumentException( + \sprintf('Source must be an object, "%s" given.', \get_debug_type($source)) + ); + } + + # target type must exist + + if ($targetType === null) { + throw new InvalidArgumentException('Target type must be specified.'); + } + + # target must be an interface or class + + $targetClass = $targetType->getClassName(); + + if ( + $targetClass === null + || ( + !\class_exists($targetClass) + && !\interface_exists($targetClass) + ) + ) { + throw new InvalidArgumentException( + \sprintf('Target class "%s" does not exist.', $targetClass ?? 'null') + ); + } + + # gets the inheritance map + + $attributes = (new \ReflectionClass($targetClass)) + ->getAttributes(InheritanceMap::class); + + if (\count($attributes) === 0) { + throw new InvalidArgumentException( + \sprintf('Target class "%s" must have inheritance map.', $targetClass) + ); + } + + $inheritanceMap = $attributes[0]->newInstance(); + + # gets the target class from the inheritance map + + $sourceClass = \get_class($source); + $targetClassInMap = $inheritanceMap->getTargetClassFromSourceClass($sourceClass); + + if ($targetClassInMap === null) { + throw new SourceClassNotInInheritanceMapException($sourceClass, $targetClass); + } + + # pass the transformation back to the main transformer + + $concreteTargetType = TypeFactory::objectOfClass($targetClassInMap); + + $result = $this->getMainTransformer()->transform( + source: $source, + target: null, + targetTypes: [$concreteTargetType], + context: $context + ); + + # make sure $result is the correct type + + if (!is_object($result) || !is_a($result, $targetClassInMap)) { + throw new UnexpectedValueException( + \sprintf('Expecting an instance of "%s", "%s" given.', $targetClassInMap, \get_debug_type($result)) + ); + } + + # cache the result. we cache the abstract class/interface as the key. + # the concrete should be cached by whatever transformer that handles it. + + $context->get(ObjectCache::class) + ->saveTarget($source, $targetType, $result); + + return $result; + } + + public function getSupportedTransformation(): iterable + { + yield new TypeMapping( + TypeFactory::object(), + TypeFactory::objectOfClass(InheritanceMap::class) + ); + } +} diff --git a/src/Transformer/ObjectToObjectTransformer.php b/src/Transformer/ObjectToObjectTransformer.php index 4914c115..c82f28e0 100644 --- a/src/Transformer/ObjectToObjectTransformer.php +++ b/src/Transformer/ObjectToObjectTransformer.php @@ -24,6 +24,7 @@ use Rekalogika\Mapper\Transformer\Exception\IncompleteConstructorArgument; use Rekalogika\Mapper\Transformer\Exception\InstantiationFailureException; use Rekalogika\Mapper\Transformer\Exception\InvalidClassException; +use Rekalogika\Mapper\Transformer\Exception\NotAClassException; use Rekalogika\Mapper\TypeResolver\TypeResolverInterface; use Rekalogika\Mapper\Util\TypeCheck; use Rekalogika\Mapper\Util\TypeFactory; @@ -70,8 +71,12 @@ public function transform( $targetClass = $targetType->getClassName(); - if (null === $targetClass || !\class_exists($targetClass)) { - throw new InvalidArgumentException('Cannot get class name from target type.'); + if (null === $targetClass) { + throw new InvalidArgumentException("Cannot get the class name for the target type."); + } + + if (!\class_exists($targetClass)) { + throw new NotAClassException($targetClass); } // if sourceType and targetType are the same, just return the source diff --git a/tests/Fixtures/Inheritance/AbstractClass.php b/tests/Fixtures/Inheritance/AbstractClass.php new file mode 100644 index 00000000..7c1c92f5 --- /dev/null +++ b/tests/Fixtures/Inheritance/AbstractClass.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\Inheritance; + +abstract class AbstractClass +{ + public string $propertyInParent = 'propertyInParent'; +} diff --git a/tests/Fixtures/Inheritance/ConcreteClassA.php b/tests/Fixtures/Inheritance/ConcreteClassA.php new file mode 100644 index 00000000..b1fd8390 --- /dev/null +++ b/tests/Fixtures/Inheritance/ConcreteClassA.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\Inheritance; + +class ConcreteClassA extends AbstractClass +{ + public string $propertyInA = 'propertyInA'; +} diff --git a/tests/Fixtures/Inheritance/ConcreteClassB.php b/tests/Fixtures/Inheritance/ConcreteClassB.php new file mode 100644 index 00000000..be4c525d --- /dev/null +++ b/tests/Fixtures/Inheritance/ConcreteClassB.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\Inheritance; + +class ConcreteClassB extends AbstractClass +{ + public string $propertyInB = 'propertyInB'; +} diff --git a/tests/Fixtures/Inheritance/ConcreteClassC.php b/tests/Fixtures/Inheritance/ConcreteClassC.php new file mode 100644 index 00000000..f9d0aeff --- /dev/null +++ b/tests/Fixtures/Inheritance/ConcreteClassC.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\Inheritance; + +class ConcreteClassC extends AbstractClass +{ + public string $propertyInC = 'propertyInC'; +} diff --git a/tests/Fixtures/InheritanceDto/AbstractClassDto.php b/tests/Fixtures/InheritanceDto/AbstractClassDto.php new file mode 100644 index 00000000..409fb89b --- /dev/null +++ b/tests/Fixtures/InheritanceDto/AbstractClassDto.php @@ -0,0 +1,28 @@ + + * + * For the full copyright and license information, please view the LICENSE file + * that was distributed with this source code. + */ + +namespace Rekalogika\Mapper\Tests\Fixtures\InheritanceDto; + +use Rekalogika\Mapper\Attribute\InheritanceMap; +use Rekalogika\Mapper\Tests\Fixtures\Inheritance\ConcreteClassA; +use Rekalogika\Mapper\Tests\Fixtures\Inheritance\ConcreteClassB; + +#[InheritanceMap([ + ConcreteClassA::class => ConcreteClassADto::class, + ConcreteClassB::class => ConcreteClassBDto::class, + // C is deliberately omitted +])] +abstract class AbstractClassDto +{ + public ?string $propertyInParent = null; +} diff --git a/tests/Fixtures/InheritanceDto/AbstractClassWithoutMapDto.php b/tests/Fixtures/InheritanceDto/AbstractClassWithoutMapDto.php new file mode 100644 index 00000000..a1d0458f --- /dev/null +++ b/tests/Fixtures/InheritanceDto/AbstractClassWithoutMapDto.php @@ -0,0 +1,18 @@ + + * + * For the full copyright and license information, please view the LICENSE file + * that was distributed with this source code. + */ + +namespace Rekalogika\Mapper\Tests\Fixtures\InheritanceDto; + +abstract class AbstractClassWithoutMapDto +{ +} diff --git a/tests/Fixtures/InheritanceDto/ConcreteClassADto.php b/tests/Fixtures/InheritanceDto/ConcreteClassADto.php new file mode 100644 index 00000000..8a940c92 --- /dev/null +++ b/tests/Fixtures/InheritanceDto/ConcreteClassADto.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\InheritanceDto; + +class ConcreteClassADto extends AbstractClassDto +{ + public ?string $propertyInA = null; +} diff --git a/tests/Fixtures/InheritanceDto/ConcreteClassBDto.php b/tests/Fixtures/InheritanceDto/ConcreteClassBDto.php new file mode 100644 index 00000000..e9b98a73 --- /dev/null +++ b/tests/Fixtures/InheritanceDto/ConcreteClassBDto.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\InheritanceDto; + +class ConcreteClassBDto extends AbstractClassDto +{ + public ?string $propertyInB = null; +} diff --git a/tests/Fixtures/InheritanceDto/ConcreteClassCDto.php b/tests/Fixtures/InheritanceDto/ConcreteClassCDto.php new file mode 100644 index 00000000..81f85bc7 --- /dev/null +++ b/tests/Fixtures/InheritanceDto/ConcreteClassCDto.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\InheritanceDto; + +class ConcreteClassCDto extends AbstractClassDto +{ + public ?string $propertyInC = null; +} diff --git a/tests/Fixtures/InheritanceDto/ImplementationADto.php b/tests/Fixtures/InheritanceDto/ImplementationADto.php new file mode 100644 index 00000000..624d9706 --- /dev/null +++ b/tests/Fixtures/InheritanceDto/ImplementationADto.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\InheritanceDto; + +class ImplementationADto implements InterfaceDto +{ + public ?string $propertyInA = null; +} diff --git a/tests/Fixtures/InheritanceDto/ImplementationBDto.php b/tests/Fixtures/InheritanceDto/ImplementationBDto.php new file mode 100644 index 00000000..f18d69d9 --- /dev/null +++ b/tests/Fixtures/InheritanceDto/ImplementationBDto.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\InheritanceDto; + +class ImplementationBDto implements InterfaceDto +{ + public ?string $propertyInB = null; +} diff --git a/tests/Fixtures/InheritanceDto/ImplementationCDto.php b/tests/Fixtures/InheritanceDto/ImplementationCDto.php new file mode 100644 index 00000000..ecb25e35 --- /dev/null +++ b/tests/Fixtures/InheritanceDto/ImplementationCDto.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\InheritanceDto; + +class ImplementationCDto implements InterfaceDto +{ + public ?string $propertyInC = null; +} diff --git a/tests/Fixtures/InheritanceDto/InterfaceDto.php b/tests/Fixtures/InheritanceDto/InterfaceDto.php new file mode 100644 index 00000000..4118cdc8 --- /dev/null +++ b/tests/Fixtures/InheritanceDto/InterfaceDto.php @@ -0,0 +1,27 @@ + + * + * For the full copyright and license information, please view the LICENSE file + * that was distributed with this source code. + */ + +namespace Rekalogika\Mapper\Tests\Fixtures\InheritanceDto; + +use Rekalogika\Mapper\Attribute\InheritanceMap; +use Rekalogika\Mapper\Tests\Fixtures\Inheritance\ConcreteClassA; +use Rekalogika\Mapper\Tests\Fixtures\Inheritance\ConcreteClassB; + +#[InheritanceMap([ + ConcreteClassA::class => ImplementationADto::class, + ConcreteClassB::class => ImplementationBDto::class, + // C is deliberately omitted +])] +interface InterfaceDto +{ +} diff --git a/tests/Fixtures/InheritanceDto/InterfaceWithoutMapDto.php b/tests/Fixtures/InheritanceDto/InterfaceWithoutMapDto.php new file mode 100644 index 00000000..fdef605c --- /dev/null +++ b/tests/Fixtures/InheritanceDto/InterfaceWithoutMapDto.php @@ -0,0 +1,18 @@ + + * + * For the full copyright and license information, please view the LICENSE file + * that was distributed with this source code. + */ + +namespace Rekalogika\Mapper\Tests\Fixtures\InheritanceDto; + +interface InterfaceWithoutMapDto +{ +} diff --git a/tests/IntegrationTest/InheritanceTest.php b/tests/IntegrationTest/InheritanceTest.php new file mode 100644 index 00000000..60ccf476 --- /dev/null +++ b/tests/IntegrationTest/InheritanceTest.php @@ -0,0 +1,81 @@ + + * + * 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\AbstractIntegrationTest; +use Rekalogika\Mapper\Tests\Fixtures\Inheritance\ConcreteClassA; +use Rekalogika\Mapper\Tests\Fixtures\Inheritance\ConcreteClassC; +use Rekalogika\Mapper\Tests\Fixtures\InheritanceDto\AbstractClassDto; +use Rekalogika\Mapper\Tests\Fixtures\InheritanceDto\AbstractClassWithoutMapDto; +use Rekalogika\Mapper\Tests\Fixtures\InheritanceDto\ConcreteClassADto; +use Rekalogika\Mapper\Tests\Fixtures\InheritanceDto\ImplementationADto; +use Rekalogika\Mapper\Tests\Fixtures\InheritanceDto\InterfaceDto; +use Rekalogika\Mapper\Tests\Fixtures\InheritanceDto\InterfaceWithoutMapDto; +use Rekalogika\Mapper\Transformer\Exception\ClassNotInstantiableException; +use Rekalogika\Mapper\Transformer\Exception\NotAClassException; +use Rekalogika\Mapper\Transformer\Exception\SourceClassNotInInheritanceMapException; + +class InheritanceTest extends AbstractIntegrationTest +{ + public function testMapToAbstractClass(): void + { + $concreteClassA = new ConcreteClassA(); + $result = $this->mapper->map($concreteClassA, AbstractClassDto::class); + + /** @var ConcreteClassADto $result */ + + $this->assertInstanceOf(ConcreteClassADto::class, $result); + $this->assertSame('propertyInA', $result->propertyInA); + $this->assertSame('propertyInParent', $result->propertyInParent); + } + + public function testMapToAbstractClassWithoutMap(): void + { + $concreteClassA = new ConcreteClassA(); + $this->expectException(ClassNotInstantiableException::class); + $result = $this->mapper->map($concreteClassA, AbstractClassWithoutMapDto::class); + } + + public function testMapToAbstractClassWithMissingSourceClassInMap(): void + { + $concreteClassC = new ConcreteClassC(); + $this->expectException(SourceClassNotInInheritanceMapException::class); + $result = $this->mapper->map($concreteClassC, AbstractClassDto::class); + } + + public function testMapToInterface(): void + { + $concreteClassA = new ConcreteClassA(); + $result = $this->mapper->map($concreteClassA, InterfaceDto::class); + + /** @var ImplementationADto $result */ + + $this->assertInstanceOf(ImplementationADto::class, $result); + $this->assertSame('propertyInA', $result->propertyInA); + } + + public function testMapToInterfaceWithoutMap(): void + { + $concreteClassA = new ConcreteClassA(); + $this->expectException(NotAClassException::class); + $result = $this->mapper->map($concreteClassA, InterfaceWithoutMapDto::class); + } + + public function testMapToInterfaceWithMissingSourceClassInMap(): void + { + $concreteClassC = new ConcreteClassC(); + $this->expectException(SourceClassNotInInheritanceMapException::class); + $result = $this->mapper->map($concreteClassC, InterfaceDto::class); + } +}