From 6a2be9799db406100d7bce6bf02303d7b00536f5 Mon Sep 17 00:00:00 2001 From: Priyadi Iman Nurcahyo <1102197+priyadi@users.noreply.github.com> Date: Fri, 12 Jan 2024 23:05:42 +0700 Subject: [PATCH] feat: Method mapper --- CHANGELOG.md | 1 + config/services.php | 22 ++- src/MapperFactory/MapperFactory.php | 30 +++++ src/MethodMapper/ClassMethodTransformer.php | 119 +++++++++++++++++ src/MethodMapper/MapFromObjectInterface.php | 26 ++++ src/MethodMapper/MapToObjectInterface.php | 26 ++++ src/MethodMapper/SubMapper.php | 92 +++++++++++++ src/MethodMapper/SubMapperInterface.php | 44 ++++++ tests/Fixtures/MethodMapper/MoneyDto.php | 61 +++++++++ .../ObjectWithArrayPropertyDto.php | 50 +++++++ .../ObjectWithCollectionProperty.php | 34 +++++ .../ObjectWithObjectWithScalarProperties.php | 26 ++++ ...bjectWithObjectWithScalarPropertiesDto.php | 67 ++++++++++ tests/IntegrationTest/MethodMapperTest.php | 125 ++++++++++++++++++ 14 files changed, 721 insertions(+), 2 deletions(-) create mode 100644 src/MethodMapper/ClassMethodTransformer.php create mode 100644 src/MethodMapper/MapFromObjectInterface.php create mode 100644 src/MethodMapper/MapToObjectInterface.php create mode 100644 src/MethodMapper/SubMapper.php create mode 100644 src/MethodMapper/SubMapperInterface.php create mode 100644 tests/Fixtures/MethodMapper/MoneyDto.php create mode 100644 tests/Fixtures/MethodMapper/ObjectWithArrayPropertyDto.php create mode 100644 tests/Fixtures/MethodMapper/ObjectWithCollectionProperty.php create mode 100644 tests/Fixtures/MethodMapper/ObjectWithObjectWithScalarProperties.php create mode 100644 tests/Fixtures/MethodMapper/ObjectWithObjectWithScalarPropertiesDto.php create mode 100644 tests/IntegrationTest/MethodMapperTest.php diff --git a/CHANGELOG.md b/CHANGELOG.md index 4a9051a..8ee5374 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -23,6 +23,7 @@ `Countable` result. * docs: Improve documentation * fix: Change `ObjectCache` to use `WeakMap`. Should improve memory usage. +* feat: Method mapper ## 0.5.3 diff --git a/config/services.php b/config/services.php index 3395d4b..e5d4c60 100644 --- a/config/services.php +++ b/config/services.php @@ -19,6 +19,8 @@ use Rekalogika\Mapper\Mapping\CachingMappingFactory; use Rekalogika\Mapper\Mapping\MappingFactory; use Rekalogika\Mapper\Mapping\MappingFactoryInterface; +use Rekalogika\Mapper\MethodMapper\ClassMethodTransformer; +use Rekalogika\Mapper\MethodMapper\SubMapper; use Rekalogika\Mapper\ObjectCache\ObjectCacheFactory; use Rekalogika\Mapper\Transformer\ArrayToObjectTransformer; use Rekalogika\Mapper\Transformer\CopyTransformer; @@ -84,14 +86,22 @@ $services ->set('rekalogika.mapper.transformer.scalar_to_scalar', ScalarToScalarTransformer::class) - ->tag('rekalogika.mapper.transformer', ['priority' => -500]); + ->tag('rekalogika.mapper.transformer', ['priority' => -450]); $services ->set('rekalogika.mapper.transformer.datetime', DateTimeTransformer::class) - ->tag('rekalogika.mapper.transformer', ['priority' => -550]); + ->tag('rekalogika.mapper.transformer', ['priority' => -500]); $services ->set('rekalogika.mapper.transformer.string_to_backed_enum', StringToBackedEnumTransformer::class) + ->tag('rekalogika.mapper.transformer', ['priority' => -550]); + + $services + ->set('rekalogika.mapper.method_mapper.transformer', ClassMethodTransformer::class) + ->args([ + service('rekalogika.mapper.method_mapper.sub_mapper'), + service('rekalogika.mapper.object_cache_factory'), + ]) ->tag('rekalogika.mapper.transformer', ['priority' => -600]); $services @@ -176,6 +186,14 @@ service('rekalogika.mapper.type_resolver.caching.inner'), ]); + # method mapper + + $services + ->set('rekalogika.mapper.method_mapper.sub_mapper', SubMapper::class) + ->args([ + service('rekalogika.mapper.property_info'), + ]); + # other services $services diff --git a/src/MapperFactory/MapperFactory.php b/src/MapperFactory/MapperFactory.php index 3f76a27..df8a71a 100644 --- a/src/MapperFactory/MapperFactory.php +++ b/src/MapperFactory/MapperFactory.php @@ -23,6 +23,9 @@ use Rekalogika\Mapper\MapperInterface; use Rekalogika\Mapper\Mapping\MappingFactory; use Rekalogika\Mapper\Mapping\MappingFactoryInterface; +use Rekalogika\Mapper\MethodMapper\ClassMethodTransformer; +use Rekalogika\Mapper\MethodMapper\SubMapper; +use Rekalogika\Mapper\MethodMapper\SubMapperInterface; use Rekalogika\Mapper\ObjectCache\ObjectCacheFactory; use Rekalogika\Mapper\ObjectCache\ObjectCacheFactoryInterface; use Rekalogika\Mapper\Transformer\ArrayToObjectTransformer; @@ -82,6 +85,7 @@ class MapperFactory private ?TraversableToArrayAccessTransformer $traversableToArrayAccessTransformer = null; private ?TraversableToTraversableTransformer $traversableToTraversableTransformer = null; private ?CopyTransformer $copyTransformer = null; + private ?ClassMethodTransformer $classMethodTransformer = null; private CacheItemPoolInterface $propertyInfoExtractorCache; private null|(PropertyInfoExtractorInterface&PropertyInitializableExtractorInterface) $propertyInfoExtractor = null; @@ -90,6 +94,7 @@ class MapperFactory private ?MapperInterface $mapper = null; private ?MappingFactoryInterface $mappingFactory = null; private ?ObjectCacheFactoryInterface $objectCacheFactory = null; + private ?SubMapper $subMapper = null; private ?MappingCommand $mappingCommand = null; private ?TryCommand $tryCommand = null; @@ -365,6 +370,18 @@ protected function getCopyTransformer(): TransformerInterface return $this->copyTransformer; } + protected function getClassMethodTransformer(): ClassMethodTransformer + { + if (null === $this->classMethodTransformer) { + $this->classMethodTransformer = new ClassMethodTransformer( + $this->getSubMapper(), + $this->getObjectCacheFactory(), + ); + } + + return $this->classMethodTransformer; + } + // // other services // @@ -390,6 +407,8 @@ protected function getTransformersIterator(): iterable => $this->getDateTimeTransformer(); yield 'StringToBackedEnumTransformer' => $this->getStringToBackedEnumTransformer(); + yield 'ClassMethodTransformer' + => $this->getClassMethodTransformer(); yield 'ObjectToStringTransformer' => $this->getObjectToStringTransformer(); yield 'TraversableToArrayAccessTransformer' @@ -449,6 +468,17 @@ protected function getObjectCacheFactory(): ObjectCacheFactoryInterface return $this->objectCacheFactory; } + protected function getSubMapper(): SubMapper + { + if (null === $this->subMapper) { + $this->subMapper = new SubMapper( + $this->getPropertyInfoExtractor(), + ); + } + + return $this->subMapper; + } + // // command // diff --git a/src/MethodMapper/ClassMethodTransformer.php b/src/MethodMapper/ClassMethodTransformer.php new file mode 100644 index 0000000..eee3d1f --- /dev/null +++ b/src/MethodMapper/ClassMethodTransformer.php @@ -0,0 +1,119 @@ + + * + * For the full copyright and license information, please view the LICENSE file + * that was distributed with this source code. + */ + +namespace Rekalogika\Mapper\MethodMapper; + +use Rekalogika\Mapper\Contracts\MainTransformerAwareInterface; +use Rekalogika\Mapper\Contracts\MainTransformerAwareTrait; +use Rekalogika\Mapper\Contracts\TransformerInterface; +use Rekalogika\Mapper\Contracts\TypeMapping; +use Rekalogika\Mapper\Exception\InvalidArgumentException; +use Rekalogika\Mapper\MainTransformer; +use Rekalogika\Mapper\ObjectCache\ObjectCache; +use Rekalogika\Mapper\ObjectCache\ObjectCacheFactoryInterface; +use Rekalogika\Mapper\Util\TypeFactory; +use Symfony\Component\PropertyInfo\Type; + +final class ClassMethodTransformer implements + TransformerInterface, + MainTransformerAwareInterface +{ + use MainTransformerAwareTrait; + + public function __construct( + private SubMapper $subMapper, + private ObjectCacheFactoryInterface $objectCacheFactory, + ) { + } + + public function transform( + mixed $source, + mixed $target, + Type $sourceType, + ?Type $targetType, + array $context + ): mixed { + // target type must not be null + + if ($targetType === null) { + throw new InvalidArgumentException('Target type must not be null.'); + } + + // prepare subMapper + + $subMapper = $this->subMapper->withMainTransformer($this->getMainTransformer()); + + // target class must be valid + + $targetClass = $targetType->getClassName(); + + if ( + !is_string($targetClass) + || !\class_exists($targetClass) + ) { + throw new InvalidArgumentException(sprintf('Target class "%s" is not a valid class.', (string) $targetClass)); + } + + + if (is_a($targetClass, MapFromObjectInterface::class, true)) { + + // map from object to self path + + if (!is_object($source)) { + throw new InvalidArgumentException(sprintf('Source must be object, "%s" given', get_debug_type($source))); + } + + $result = $targetClass::mapFromObject($source, $subMapper, $context); + } elseif ($source instanceof MapToObjectInterface) { + + // map self to object path + + if (!is_object($target)) { + $target = $targetClass; + } + + $result = $source->mapToObject($target, $subMapper, $context); + } else { + throw new \LogicException('Should not reach here'); + } + + // get object cache + + if (!isset($context[MainTransformer::OBJECT_CACHE])) { + $objectCache = $this->objectCacheFactory->createObjectCache(); + $context[MainTransformer::OBJECT_CACHE] = $objectCache; + } else { + /** @var ObjectCache */ + $objectCache = $context[MainTransformer::OBJECT_CACHE]; + } + + // save to object cache + + $objectCache->saveTarget($source, $targetType, $target); + + return $result; + } + + public function getSupportedTransformation(): iterable + { + yield new TypeMapping( + TypeFactory::objectOfClass(MapToObjectInterface::class), + TypeFactory::object(), + ); + + yield new TypeMapping( + TypeFactory::object(), + TypeFactory::objectOfClass(MapFromObjectInterface::class), + ); + } +} diff --git a/src/MethodMapper/MapFromObjectInterface.php b/src/MethodMapper/MapFromObjectInterface.php new file mode 100644 index 0000000..97b0589 --- /dev/null +++ b/src/MethodMapper/MapFromObjectInterface.php @@ -0,0 +1,26 @@ + + * + * For the full copyright and license information, please view the LICENSE file + * that was distributed with this source code. + */ + +namespace Rekalogika\Mapper\MethodMapper; + +interface MapFromObjectInterface +{ + /** + * @param array $context + */ + public static function mapFromObject( + object $source, + SubMapperInterface $mapper, + array $context = [] + ): static; +} diff --git a/src/MethodMapper/MapToObjectInterface.php b/src/MethodMapper/MapToObjectInterface.php new file mode 100644 index 0000000..1a068f4 --- /dev/null +++ b/src/MethodMapper/MapToObjectInterface.php @@ -0,0 +1,26 @@ + + * + * For the full copyright and license information, please view the LICENSE file + * that was distributed with this source code. + */ + +namespace Rekalogika\Mapper\MethodMapper; + +interface MapToObjectInterface +{ + /** + * @param array $context + */ + public function mapToObject( + object|string $target, + SubMapperInterface $mapper, + array $context = [] + ): object; +} diff --git a/src/MethodMapper/SubMapper.php b/src/MethodMapper/SubMapper.php new file mode 100644 index 0000000..9463ec4 --- /dev/null +++ b/src/MethodMapper/SubMapper.php @@ -0,0 +1,92 @@ + + * + * For the full copyright and license information, please view the LICENSE file + * that was distributed with this source code. + */ + +namespace Rekalogika\Mapper\MethodMapper; + +use Rekalogika\Mapper\Contracts\MainTransformerAwareInterface; +use Rekalogika\Mapper\Contracts\MainTransformerAwareTrait; +use Rekalogika\Mapper\Exception\UnexpectedValueException; +use Rekalogika\Mapper\Util\TypeFactory; +use Symfony\Component\PropertyInfo\PropertyTypeExtractorInterface; +use Symfony\Component\PropertyInfo\Type; + +/** + * Specialized mapper used in MethodMapper. + */ +class SubMapper implements SubMapperInterface, MainTransformerAwareInterface +{ + use MainTransformerAwareTrait; + + public function __construct( + private PropertyTypeExtractorInterface $propertyTypeExtractor, + ) { + } + + public function map( + object $source, + object|string $target, + array $context = [] + ): object { + if (is_object($target)) { + $targetClass = $target::class; + $targetObject = $target; + } else { + $targetClass = $target; + $targetObject = null; + } + + /** @var mixed */ + $result = $this->getMainTransformer()->transform( + $source, + $targetObject, + [TypeFactory::objectOfClass($targetClass)], + $context + ); + + if (is_object($target)) { + $targetClass = $target::class; + } else { + $targetClass = $target; + } + + if ($result instanceof $targetClass) { + return $result; + } + + throw new UnexpectedValueException(sprintf('The mapper did not return the variable of expected class, expecting "%s", returned "%s".', $targetClass, get_debug_type($target))); + } + + public function mapForProperty( + object $source, + string $class, + string $property, + array $context = [] + ): mixed { + /** @var array|null */ + $targetPropertyTypes = $this->propertyTypeExtractor->getTypes( + $class, + $property, + $context + ); + + /** @var mixed */ + $result = $this->getMainTransformer()->transform( + $source, + null, + $targetPropertyTypes ?? [], + $context + ); + + return $result; + } +} diff --git a/src/MethodMapper/SubMapperInterface.php b/src/MethodMapper/SubMapperInterface.php new file mode 100644 index 0000000..5589318 --- /dev/null +++ b/src/MethodMapper/SubMapperInterface.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\MethodMapper; + +interface SubMapperInterface +{ + /** + * Maps a source to the specified target. + * + * @template T of object + * @param class-string|T $target + * @param array $context + * @return T + */ + public function map( + object $source, + object|string $target, + array $context = [] + ): object; + + /** + * Maps a source to the type of the specified class & property + * + * @param class-string $class + * @param array $context + */ + public function mapForProperty( + object $source, + string $class, + string $property, + array $context = [] + ): mixed; +} diff --git a/tests/Fixtures/MethodMapper/MoneyDto.php b/tests/Fixtures/MethodMapper/MoneyDto.php new file mode 100644 index 0000000..8bde6ec --- /dev/null +++ b/tests/Fixtures/MethodMapper/MoneyDto.php @@ -0,0 +1,61 @@ + + * + * For the full copyright and license information, please view the LICENSE file + * that was distributed with this source code. + */ + +namespace Rekalogika\Mapper\Tests\Fixtures\MethodMapper; + +use Brick\Money\Money; +use Rekalogika\Mapper\MethodMapper\MapFromObjectInterface; +use Rekalogika\Mapper\MethodMapper\MapToObjectInterface; +use Rekalogika\Mapper\MethodMapper\SubMapperInterface; + +final class MoneyDto implements MapToObjectInterface, MapFromObjectInterface +{ + public function __construct( + private string $amount, + private string $currency, + ) { + } + + public function getAmount(): string + { + return $this->amount; + } + + public function getCurrency(): string + { + return $this->currency; + } + + public function mapToObject( + object|string $target, + SubMapperInterface $mapper, + array $context = [] + ): object { + return Money::of($this->amount, $this->currency); + } + + public static function mapFromObject( + object $source, + SubMapperInterface $mapper, + array $context = [] + ): static { + if (!$source instanceof Money) { + throw new \InvalidArgumentException('Source must be instance of ' . Money::class); + } + + return new static( + $source->getAmount()->__toString(), + $source->getCurrency()->getCurrencyCode(), + ); + } +} diff --git a/tests/Fixtures/MethodMapper/ObjectWithArrayPropertyDto.php b/tests/Fixtures/MethodMapper/ObjectWithArrayPropertyDto.php new file mode 100644 index 0000000..f25dd40 --- /dev/null +++ b/tests/Fixtures/MethodMapper/ObjectWithArrayPropertyDto.php @@ -0,0 +1,50 @@ + + * + * For the full copyright and license information, please view the LICENSE file + * that was distributed with this source code. + */ + +namespace Rekalogika\Mapper\Tests\Fixtures\MethodMapper; + +use Rekalogika\Mapper\MethodMapper\MapFromObjectInterface; +use Rekalogika\Mapper\MethodMapper\SubMapperInterface; +use Rekalogika\Mapper\Tests\Fixtures\Scalar\ObjectWithScalarPropertiesDto; + +final class ObjectWithArrayPropertyDto implements MapFromObjectInterface +{ + /** + * @var ?array + */ + public ?array $property = null; + + public static function mapFromObject( + object $source, + SubMapperInterface $mapper, + array $context = [] + ): static { + assert($source instanceof ObjectWithCollectionProperty); + + $result = new self(); + + $property = $mapper->mapForProperty( + $source->property, + ObjectWithArrayPropertyDto::class, + 'property', + $context + ); + + assert(is_array($property)); + + /** @psalm-suppress MixedPropertyTypeCoercion */ + $result->property = $property; + + return $result; + } +} diff --git a/tests/Fixtures/MethodMapper/ObjectWithCollectionProperty.php b/tests/Fixtures/MethodMapper/ObjectWithCollectionProperty.php new file mode 100644 index 0000000..02ac246 --- /dev/null +++ b/tests/Fixtures/MethodMapper/ObjectWithCollectionProperty.php @@ -0,0 +1,34 @@ + + * + * For the full copyright and license information, please view the LICENSE file + * that was distributed with this source code. + */ + +namespace Rekalogika\Mapper\Tests\Fixtures\MethodMapper; + +use Doctrine\Common\Collections\ArrayCollection; +use Doctrine\Common\Collections\Collection; +use Rekalogika\Mapper\Tests\Fixtures\Scalar\ObjectWithScalarProperties; + +class ObjectWithCollectionProperty +{ + /** + * @var Collection + */ + public Collection $property; + + public function __construct() + { + $this->property = new ArrayCollection(); + $this->property->add(new ObjectWithScalarProperties()); + $this->property->add(new ObjectWithScalarProperties()); + $this->property->add(new ObjectWithScalarProperties()); + } +} diff --git a/tests/Fixtures/MethodMapper/ObjectWithObjectWithScalarProperties.php b/tests/Fixtures/MethodMapper/ObjectWithObjectWithScalarProperties.php new file mode 100644 index 0000000..3fded3a --- /dev/null +++ b/tests/Fixtures/MethodMapper/ObjectWithObjectWithScalarProperties.php @@ -0,0 +1,26 @@ + + * + * For the full copyright and license information, please view the LICENSE file + * that was distributed with this source code. + */ + +namespace Rekalogika\Mapper\Tests\Fixtures\MethodMapper; + +use Rekalogika\Mapper\Tests\Fixtures\Scalar\ObjectWithScalarProperties; + +final class ObjectWithObjectWithScalarProperties +{ + public ObjectWithScalarProperties $objectWithScalarProperties; + + public function __construct() + { + $this->objectWithScalarProperties = new ObjectWithScalarProperties(); + } +} diff --git a/tests/Fixtures/MethodMapper/ObjectWithObjectWithScalarPropertiesDto.php b/tests/Fixtures/MethodMapper/ObjectWithObjectWithScalarPropertiesDto.php new file mode 100644 index 0000000..d80854e --- /dev/null +++ b/tests/Fixtures/MethodMapper/ObjectWithObjectWithScalarPropertiesDto.php @@ -0,0 +1,67 @@ + + * + * For the full copyright and license information, please view the LICENSE file + * that was distributed with this source code. + */ + +namespace Rekalogika\Mapper\Tests\Fixtures\MethodMapper; + +use Rekalogika\Mapper\MethodMapper\MapFromObjectInterface; +use Rekalogika\Mapper\MethodMapper\MapToObjectInterface; +use Rekalogika\Mapper\MethodMapper\SubMapperInterface; +use Rekalogika\Mapper\Tests\Fixtures\Scalar\ObjectWithScalarProperties; +use Rekalogika\Mapper\Tests\Fixtures\Scalar\ObjectWithScalarPropertiesDto; + +final class ObjectWithObjectWithScalarPropertiesDto implements + MapFromObjectInterface, + MapToObjectInterface +{ + public ?ObjectWithScalarPropertiesDto $objectWithScalarProperties = null; + + + + public static function mapFromObject( + object $source, + SubMapperInterface $mapper, + array $context = [] + ): static { + assert($source instanceof ObjectWithObjectWithScalarProperties); + + $self = new static(); + + $self->objectWithScalarProperties = $mapper->map( + $source->objectWithScalarProperties, + ObjectWithScalarPropertiesDto::class, + $context + ); + + return $self; + } + + public function mapToObject( + object|string $target, + SubMapperInterface $mapper, + array $context = [] + ): object { + if ($target === ObjectWithObjectWithScalarProperties::class) { + $target = new $target(); + } + assert($target instanceof ObjectWithObjectWithScalarProperties); + assert($this->objectWithScalarProperties instanceof ObjectWithScalarPropertiesDto); + + $target->objectWithScalarProperties = $mapper->map( + $this->objectWithScalarProperties, + ObjectWithScalarProperties::class, + $context + ); + + return $target; + } +} diff --git a/tests/IntegrationTest/MethodMapperTest.php b/tests/IntegrationTest/MethodMapperTest.php new file mode 100644 index 0000000..0078c5a --- /dev/null +++ b/tests/IntegrationTest/MethodMapperTest.php @@ -0,0 +1,125 @@ + + * + * 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 Brick\Money\Money; +use Rekalogika\Mapper\Tests\Common\AbstractIntegrationTest; +use Rekalogika\Mapper\Tests\Fixtures\MethodMapper\MoneyDto; +use Rekalogika\Mapper\Tests\Fixtures\MethodMapper\ObjectWithArrayPropertyDto; +use Rekalogika\Mapper\Tests\Fixtures\MethodMapper\ObjectWithCollectionProperty; +use Rekalogika\Mapper\Tests\Fixtures\MethodMapper\ObjectWithObjectWithScalarProperties; +use Rekalogika\Mapper\Tests\Fixtures\MethodMapper\ObjectWithObjectWithScalarPropertiesDto; +use Rekalogika\Mapper\Tests\Fixtures\Scalar\ObjectWithScalarPropertiesDto; + +class MethodMapperTest extends AbstractIntegrationTest +{ + public function testMoneyToMoneyDto(): void + { + $money = Money::of('100.00', 'USD'); + $result = $this->mapper->map($money, MoneyDto::class); + + $this->assertInstanceOf(MoneyDto::class, $result); + $this->assertSame('100.00', $result->getAmount()); + $this->assertSame('USD', $result->getCurrency()); + } + + public function testMoneyDtoToMoney(): void + { + $moneyDto = new MoneyDto('100.00', 'USD'); + $result = $this->mapper->map($moneyDto, Money::class); + + $this->assertInstanceOf(Money::class, $result); + $this->assertSame('100.00', $result->getAmount()->__toString()); + $this->assertSame('USD', $result->getCurrency()->getCurrencyCode()); + } + + public function testSubMapperToDto(): void + { + $objectWithObjectWithScalarProperties = new ObjectWithObjectWithScalarProperties(); + + $result = $this->mapper->map( + $objectWithObjectWithScalarProperties, + ObjectWithObjectWithScalarPropertiesDto::class + ); + + $this->assertInstanceOf(ObjectWithObjectWithScalarPropertiesDto::class, $result); + $this->assertEquals( + $objectWithObjectWithScalarProperties->objectWithScalarProperties->a, + $result->objectWithScalarProperties?->a + ); + $this->assertEquals( + $objectWithObjectWithScalarProperties->objectWithScalarProperties->b, + $result->objectWithScalarProperties?->b + ); + $this->assertEquals( + $objectWithObjectWithScalarProperties->objectWithScalarProperties->c, + $result->objectWithScalarProperties?->c + ); + $this->assertEquals( + $objectWithObjectWithScalarProperties->objectWithScalarProperties->d, + $result->objectWithScalarProperties?->d + ); + } + + public function testSubMapperFromDto(): void + { + $objectWithObjectWithScalarPropertiesDto = new ObjectWithObjectWithScalarPropertiesDto(); + $objectWithScalarPropertiesDto = new ObjectWithScalarPropertiesDto(); + $objectWithScalarPropertiesDto->a = 123; + $objectWithScalarPropertiesDto->b = 'foo'; + $objectWithScalarPropertiesDto->c = false; + $objectWithScalarPropertiesDto->d = 123.45; + + $objectWithObjectWithScalarPropertiesDto->objectWithScalarProperties = $objectWithScalarPropertiesDto; + + $target = new ObjectWithObjectWithScalarProperties(); + + $result = $this->mapper->map( + $objectWithObjectWithScalarPropertiesDto, + $target, + ); + + $this->assertInstanceOf(ObjectWithObjectWithScalarProperties::class, $result); + $this->assertEquals( + $objectWithObjectWithScalarPropertiesDto->objectWithScalarProperties->a, + $result->objectWithScalarProperties->a + ); + $this->assertEquals( + $objectWithObjectWithScalarPropertiesDto->objectWithScalarProperties->b, + $result->objectWithScalarProperties->b + ); + $this->assertEquals( + $objectWithObjectWithScalarPropertiesDto->objectWithScalarProperties->c, + $result->objectWithScalarProperties->c + ); + $this->assertEquals( + $objectWithObjectWithScalarPropertiesDto->objectWithScalarProperties->d, + $result->objectWithScalarProperties->d + ); + } + + public function testMapForProperty(): void + { + $source = new ObjectWithCollectionProperty(); + $result = $this->mapper->map( + $source, + ObjectWithArrayPropertyDto::class, + ); + + $this->assertInstanceOf(ObjectWithArrayPropertyDto::class, $result); + $this->assertIsArray($result->property); + $this->assertCount(3, $result->property); + } + +}