From d7813f4c9781fe0f188fbe0ebdec2f311dabfff2 Mon Sep 17 00:00:00 2001 From: Priyadi Iman Nurcahyo <1102197+priyadi@users.noreply.github.com> Date: Fri, 12 Jan 2024 18:02:00 +0700 Subject: [PATCH] Various fixes * refactor: `MainTransformerInterface` now only accept array `$targetType` * refactor: `TransformerInterface` now accepts null `$targetType` to signify mixed type. * refactor: Remove deprecated facade. * feat: Add `CopyTransformer` to handle mixed to mixed mapping. --- CHANGELOG.md | 5 + config/services.php | 23 +-- src/Contracts/MainTransformerInterface.php | 4 +- src/Contracts/TransformerInterface.php | 2 +- .../InvalidTypeInArgumentException.php | 2 +- ...ableToFindSuitableTransformerException.php | 7 +- src/Facade/AllPurposeMapper.php | 134 ------------------ src/Facade/AllPurposeMapperInterface.php | 34 ----- src/MainTransformer.php | 18 +-- src/Mapper.php | 2 +- src/MapperFactory/MapperFactory.php | 13 ++ src/Transformer/ArrayToObjectTransformer.php | 4 +- src/Transformer/CopyTransformer.php | 46 ++++++ src/Transformer/DateTimeTransformer.php | 2 +- src/Transformer/NullTransformer.php | 2 +- src/Transformer/ObjectToArrayTransformer.php | 2 +- src/Transformer/ObjectToObjectTransformer.php | 6 +- src/Transformer/ObjectToStringTransformer.php | 2 +- src/Transformer/ScalarToScalarTransformer.php | 2 +- .../StringToBackedEnumTransformer.php | 4 +- .../TraversableToArrayAccessTransformer.php | 17 +-- .../TraversableToTraversableTransformer.php | 5 +- src/TypeResolver/CachingTypeResolver.php | 9 +- src/TypeResolver/TypeResolver.php | 12 +- src/TypeResolver/TypeResolverInterface.php | 6 +- src/Util/TypeCheck.php | 51 ++++--- src/Util/TypeUtil.php | 6 +- .../MoneyToMoneyDtoTransformer.php | 2 +- .../TraversableToArrayAccessMappingTest.php | 39 +++-- 29 files changed, 189 insertions(+), 272 deletions(-) delete mode 100644 src/Facade/AllPurposeMapper.php delete mode 100644 src/Facade/AllPurposeMapperInterface.php create mode 100644 src/Transformer/CopyTransformer.php diff --git a/CHANGELOG.md b/CHANGELOG.md index f5d6940..995369d 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -12,6 +12,11 @@ * refactor: Simplify `MapperInterface` * test: Fix tests due to refactor * refactor: Move deprecated facade to Facade namespace +* refactor: `MainTransformerInterface` now only accept array `$targetType` +* refactor: `TransformerInterface` now accepts null `$targetType` to signify + mixed type. +* refactor: Remove deprecated facade. +* feat: Add `CopyTransformer` to handle mixed to mixed mapping. ## 0.5.3 diff --git a/config/services.php b/config/services.php index 3a50e3a..3395d4b 100644 --- a/config/services.php +++ b/config/services.php @@ -21,6 +21,7 @@ use Rekalogika\Mapper\Mapping\MappingFactoryInterface; use Rekalogika\Mapper\ObjectCache\ObjectCacheFactory; use Rekalogika\Mapper\Transformer\ArrayToObjectTransformer; +use Rekalogika\Mapper\Transformer\CopyTransformer; use Rekalogika\Mapper\Transformer\DateTimeTransformer; use Rekalogika\Mapper\Transformer\NullTransformer; use Rekalogika\Mapper\Transformer\ObjectToArrayTransformer; @@ -83,43 +84,43 @@ $services ->set('rekalogika.mapper.transformer.scalar_to_scalar', ScalarToScalarTransformer::class) - ->tag('rekalogika.mapper.transformer', ['priority' => -550]); + ->tag('rekalogika.mapper.transformer', ['priority' => -500]); $services ->set('rekalogika.mapper.transformer.datetime', DateTimeTransformer::class) - ->tag('rekalogika.mapper.transformer', ['priority' => -600]); + ->tag('rekalogika.mapper.transformer', ['priority' => -550]); $services ->set('rekalogika.mapper.transformer.string_to_backed_enum', StringToBackedEnumTransformer::class) - ->tag('rekalogika.mapper.transformer', ['priority' => -650]); + ->tag('rekalogika.mapper.transformer', ['priority' => -600]); $services ->set('rekalogika.mapper.transformer.object_to_string', ObjectToStringTransformer::class) - ->tag('rekalogika.mapper.transformer', ['priority' => -700]); + ->tag('rekalogika.mapper.transformer', ['priority' => -650]); $services ->set('rekalogika.mapper.transformer.traversable_to_arrayaccess', TraversableToArrayAccessTransformer::class) ->args([ '$objectCacheFactory' => service('rekalogika.mapper.object_cache_factory'), ]) - ->tag('rekalogika.mapper.transformer', ['priority' => -750]); + ->tag('rekalogika.mapper.transformer', ['priority' => -700]); $services ->set('rekalogika.mapper.transformer.traversable_to_traversable', TraversableToTraversableTransformer::class) ->args([ '$objectCacheFactory' => service('rekalogika.mapper.object_cache_factory'), ]) - ->tag('rekalogika.mapper.transformer', ['priority' => -800]); + ->tag('rekalogika.mapper.transformer', ['priority' => -750]); $services ->set('rekalogika.mapper.transformer.object_to_array', ObjectToArrayTransformer::class) ->args([service(NormalizerInterface::class)]) - ->tag('rekalogika.mapper.transformer', ['priority' => -850]); + ->tag('rekalogika.mapper.transformer', ['priority' => -800]); $services ->set('rekalogika.mapper.transformer.array_to_object', ArrayToObjectTransformer::class) ->args([service(DenormalizerInterface::class)]) - ->tag('rekalogika.mapper.transformer', ['priority' => -900]); + ->tag('rekalogika.mapper.transformer', ['priority' => -850]); $services ->set('rekalogika.mapper.transformer.object_to_object', ObjectToObjectTransformer::class) @@ -132,10 +133,14 @@ '$typeResolver' => service('rekalogika.mapper.type_resolver'), '$objectCacheFactory' => service('rekalogika.mapper.object_cache_factory'), ]) - ->tag('rekalogika.mapper.transformer', ['priority' => -950]); + ->tag('rekalogika.mapper.transformer', ['priority' => -900]); $services ->set('rekalogika.mapper.transformer.null', NullTransformer::class) + ->tag('rekalogika.mapper.transformer', ['priority' => -950]); + + $services + ->set('rekalogika.mapper.transformer.null', CopyTransformer::class) ->tag('rekalogika.mapper.transformer', ['priority' => -1000]); # mappingfactory diff --git a/src/Contracts/MainTransformerInterface.php b/src/Contracts/MainTransformerInterface.php index ad64826..ca37a5b 100644 --- a/src/Contracts/MainTransformerInterface.php +++ b/src/Contracts/MainTransformerInterface.php @@ -18,13 +18,13 @@ interface MainTransformerInterface { /** - * @param null|Type|array $targetType If provided, it will be used instead of guessing the type + * @param array $targetType If provided, it will be used instead of guessing the type * @param array $context */ public function transform( mixed $source, mixed $target, - null|Type|array $targetType, + array $targetType, array $context ): mixed; } diff --git a/src/Contracts/TransformerInterface.php b/src/Contracts/TransformerInterface.php index 90d993f..ba73109 100644 --- a/src/Contracts/TransformerInterface.php +++ b/src/Contracts/TransformerInterface.php @@ -35,7 +35,7 @@ public function transform( mixed $source, mixed $target, Type $sourceType, - Type $targetType, + ?Type $targetType, array $context ): mixed; diff --git a/src/Exception/InvalidTypeInArgumentException.php b/src/Exception/InvalidTypeInArgumentException.php index 2525c09..a8778d6 100644 --- a/src/Exception/InvalidTypeInArgumentException.php +++ b/src/Exception/InvalidTypeInArgumentException.php @@ -18,7 +18,7 @@ class InvalidTypeInArgumentException extends InvalidArgumentException { - public function __construct(string $printfMessage, Type $expectedType) + public function __construct(string $printfMessage, ?Type $expectedType) { parent::__construct(sprintf($printfMessage, TypeUtil::getDebugType($expectedType))); } diff --git a/src/Exception/UnableToFindSuitableTransformerException.php b/src/Exception/UnableToFindSuitableTransformerException.php index 88f35ff..75bef34 100644 --- a/src/Exception/UnableToFindSuitableTransformerException.php +++ b/src/Exception/UnableToFindSuitableTransformerException.php @@ -13,6 +13,7 @@ namespace Rekalogika\Mapper\Exception; +use Rekalogika\Mapper\Contracts\MixedType; use Rekalogika\Mapper\Util\TypeUtil; use Symfony\Component\PropertyInfo\Type; @@ -20,12 +21,12 @@ class UnableToFindSuitableTransformerException extends NotMappableValueException { /** * @param Type $sourceType - * @param Type|array $targetType + * @param Type|array $targetType */ - public function __construct(Type $sourceType, Type|array $targetType) + public function __construct(Type $sourceType, Type|MixedType|array $targetType) { if (is_array($targetType)) { - $targetType = implode(', ', array_map(fn (Type $type) => TypeUtil::getDebugType($type), $targetType)); + $targetType = implode(', ', array_map(fn (Type|MixedType $type) => TypeUtil::getDebugType($type), $targetType)); } else { $targetType = TypeUtil::getDebugType($targetType); } diff --git a/src/Facade/AllPurposeMapper.php b/src/Facade/AllPurposeMapper.php deleted file mode 100644 index d01b324..0000000 --- a/src/Facade/AllPurposeMapper.php +++ /dev/null @@ -1,134 +0,0 @@ - - * - * For the full copyright and license information, please view the LICENSE file - * that was distributed with this source code. - */ - -namespace Rekalogika\Mapper\Facade; - -use Rekalogika\Mapper\Contracts\MainTransformerInterface; -use Rekalogika\Mapper\Contracts\MixedType; -use Rekalogika\Mapper\Exception\MapperReturnsUnexpectedValueException; -use Rekalogika\Mapper\Exception\UnexpectedValueException; -use Rekalogika\Mapper\Util\TypeFactory; -use Symfony\Component\PropertyInfo\Type; - -final class AllPurposeMapper implements AllPurposeMapperInterface -{ - /** - * Informs the key and value type of the member of the target collection. - */ - public const TARGET_KEY_TYPE = 'target_key_type'; - public const TARGET_VALUE_TYPE = 'target_value_type'; - - public function __construct( - private MainTransformerInterface $transformer, - ) { - } - - public function map(mixed $source, mixed $target, array $context = []): mixed - { - $originalTarget = $target; - - if ( - is_string($target) - && ( - class_exists($target) - || interface_exists($target) - || enum_exists($target) - ) - ) { - /** @var class-string $target */ - $targetClass = $target; - $targetType = TypeFactory::objectOfClass($targetClass); - $target = null; - } elseif (is_object($target)) { - /** @var object $target */ - $targetClass = $target::class; - $targetType = TypeFactory::objectOfClass($targetClass); - } else { - $targetClass = null; - $targetType = TypeFactory::fromBuiltIn($target); - $target = null; - } - - /** @var ?string */ - $contextTargetKeyType = $context[self::TARGET_KEY_TYPE] ?? null; - /** @var ?string */ - $contextTargetValueType = $context[self::TARGET_VALUE_TYPE] ?? null; - unset($context[self::TARGET_KEY_TYPE]); - unset($context[self::TARGET_VALUE_TYPE]); - - $targetKeyType = null; - $targetValueType = null; - - if ($contextTargetKeyType) { - $targetKeyType = TypeFactory::fromString($contextTargetKeyType); - if ($targetKeyType instanceof MixedType) { - $targetKeyType = null; - } - } - - if ($contextTargetValueType) { - $targetValueType = TypeFactory::fromString($contextTargetValueType); - if ($targetValueType instanceof MixedType) { - $targetValueType = null; - } - } - - if ($targetKeyType !== null || $targetValueType !== null) { - $targetType = new Type( - builtinType: $targetType->getBuiltinType(), - nullable: $targetType->isNullable(), - class: $targetType->getClassName(), - collection: true, - collectionKeyType: $targetKeyType, - collectionValueType: $targetValueType, - ); - } - - /** @var mixed */ - $target = $this->transformer->transform( - source: $source, - target: $target, - targetType: $targetType, - context: $context - ); - - if (is_object($target) && is_string($targetClass)) { - if (!is_a($target, $targetClass)) { - throw new UnexpectedValueException(sprintf('The transformer did not return the variable of expected class, expecting "%s", returned "%s".', $targetClass, get_debug_type($target))); - } - return $target; - } - - if ($originalTarget === 'string' && is_string($target)) { - return $target; - } - - if ($originalTarget === 'int' && is_int($target)) { - return $target; - } - - if ($originalTarget === 'float' && is_float($target)) { - return $target; - } - - if ($originalTarget === 'bool' && is_bool($target)) { - return $target; - } - - if ($originalTarget === 'array' && is_array($target)) { - return $target; - } - - throw new MapperReturnsUnexpectedValueException($targetType, $target); - } -} diff --git a/src/Facade/AllPurposeMapperInterface.php b/src/Facade/AllPurposeMapperInterface.php deleted file mode 100644 index 3b73bf9..0000000 --- a/src/Facade/AllPurposeMapperInterface.php +++ /dev/null @@ -1,34 +0,0 @@ - - * - * For the full copyright and license information, please view the LICENSE file - * that was distributed with this source code. - */ - -namespace Rekalogika\Mapper\Facade; - -use Rekalogika\Mapper\Exception\CircularReferenceException; -use Rekalogika\Mapper\Exception\ExceptionInterface; -use Rekalogika\Mapper\Exception\InvalidArgumentException; -use Rekalogika\Mapper\Exception\LogicException; - -interface AllPurposeMapperInterface -{ - /** - * @template T of object - * @param class-string|T|"int"|"string"|"float"|"bool"|"array" $target - * @param array $context - * @return ($target is class-string|T ? T : ($target is "int" ? int : ($target is "string" ? string : ($target is "float" ? float : ($target is "bool" ? bool : ($target is "array" ? array : mixed )))))) - * @throws InvalidArgumentException - * @throws CircularReferenceException - * @throws LogicException - * @throws ExceptionInterface - */ - public function map(mixed $source, mixed $target, array $context = []): mixed; -} diff --git a/src/MainTransformer.php b/src/MainTransformer.php index 75e32eb..c941f03 100644 --- a/src/MainTransformer.php +++ b/src/MainTransformer.php @@ -55,23 +55,23 @@ private function getTransformer(string $id): TransformerInterface return $transformer->withMainTransformer($this); } return $transformer; - } public function transform( mixed $source, mixed $target, - null|Type|array $targetType, + array $targetType, array $context ): mixed { // if targettype is not provided, guess it from target - // if the target is also missing then throw exception + // if the target is also missing then the target is mixed - if ($targetType === null) { + if (count($targetType) === 0) { if ($target === null) { - throw new LogicException('Either $target or $targetType must be provided'); + $targetType = [MixedType::instance()]; + } else { + $targetType = [$this->typeResolver->guessTypeFromVariable($target)]; } - $targetType = $this->typeResolver->guessTypeFromVariable($target); } // get object cache @@ -86,10 +86,6 @@ public function transform( // gets simple target types from the provided target type - if ($targetType instanceof Type) { - $targetType = [$targetType]; - } - $simpleTargetTypes = []; foreach ($targetType as $singleTargetType) { @@ -113,7 +109,7 @@ public function transform( source: $source, target: $target, sourceType: $sourceType, - targetType: $singleTargetType, + targetType: $singleTargetType instanceof MixedType ? null : $singleTargetType, context: $context ); diff --git a/src/Mapper.php b/src/Mapper.php index 6438ad0..ae5437b 100644 --- a/src/Mapper.php +++ b/src/Mapper.php @@ -52,7 +52,7 @@ public function map(mixed $source, object|string $target, array $context = []): $target = $this->transformer->transform( source: $source, target: $target, - targetType: $targetType, + targetType: [$targetType], context: $context ); diff --git a/src/MapperFactory/MapperFactory.php b/src/MapperFactory/MapperFactory.php index ad2c9f6..3f76a27 100644 --- a/src/MapperFactory/MapperFactory.php +++ b/src/MapperFactory/MapperFactory.php @@ -26,6 +26,7 @@ use Rekalogika\Mapper\ObjectCache\ObjectCacheFactory; use Rekalogika\Mapper\ObjectCache\ObjectCacheFactoryInterface; use Rekalogika\Mapper\Transformer\ArrayToObjectTransformer; +use Rekalogika\Mapper\Transformer\CopyTransformer; use Rekalogika\Mapper\Transformer\DateTimeTransformer; use Rekalogika\Mapper\Transformer\NullTransformer; use Rekalogika\Mapper\Transformer\ObjectToArrayTransformer; @@ -80,6 +81,7 @@ class MapperFactory private ?DateTimeTransformer $dateTimeTransformer = null; private ?TraversableToArrayAccessTransformer $traversableToArrayAccessTransformer = null; private ?TraversableToTraversableTransformer $traversableToTraversableTransformer = null; + private ?CopyTransformer $copyTransformer = null; private CacheItemPoolInterface $propertyInfoExtractorCache; private null|(PropertyInfoExtractorInterface&PropertyInitializableExtractorInterface) $propertyInfoExtractor = null; @@ -354,6 +356,15 @@ protected function getTraversableToTraversableTransformer(): TransformerInterfac return $this->traversableToTraversableTransformer; } + protected function getCopyTransformer(): TransformerInterface + { + if (null === $this->copyTransformer) { + $this->copyTransformer = new CopyTransformer(); + } + + return $this->copyTransformer; + } + // // other services // @@ -393,6 +404,8 @@ protected function getTransformersIterator(): iterable => $this->getObjectToObjectTransformer(); yield 'NullTransformer' => $this->getNullTransformer(); + yield 'CopyTransformer' + => $this->getCopyTransformer(); } protected function getTransformersLocator(): ContainerInterface diff --git a/src/Transformer/ArrayToObjectTransformer.php b/src/Transformer/ArrayToObjectTransformer.php index 606c499..1de4327 100644 --- a/src/Transformer/ArrayToObjectTransformer.php +++ b/src/Transformer/ArrayToObjectTransformer.php @@ -39,7 +39,7 @@ public function transform( mixed $source, mixed $target, Type $sourceType, - Type $targetType, + ?Type $targetType, array $context ): mixed { if (!is_array($source)) { @@ -58,7 +58,7 @@ public function transform( $targetClass = $target::class; $context[AbstractNormalizer::OBJECT_TO_POPULATE] = $target; } else { - $targetClass = $targetType->getClassName() ?? \stdClass::class; + $targetClass = $targetType?->getClassName() ?? \stdClass::class; } return $this->denormalizer->denormalize( diff --git a/src/Transformer/CopyTransformer.php b/src/Transformer/CopyTransformer.php new file mode 100644 index 0000000..09950d4 --- /dev/null +++ b/src/Transformer/CopyTransformer.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; + +use Rekalogika\Mapper\Contracts\MixedType; +use Rekalogika\Mapper\Contracts\TransformerInterface; +use Rekalogika\Mapper\Contracts\TypeMapping; +use Symfony\Component\PropertyInfo\Type; + +final class CopyTransformer implements TransformerInterface +{ + public function transform( + mixed $source, + mixed $target, + Type $sourceType, + ?Type $targetType, + array $context + ): mixed { + if (!is_object($source)) { + return $source; + } + + $clonable = (new \ReflectionClass($source))->isCloneable(); + + if (!$clonable) { + return $source; + } + return clone $source; + } + + public function getSupportedTransformation(): iterable + { + yield new TypeMapping(MixedType::instance(), MixedType::instance()); + } +} diff --git a/src/Transformer/DateTimeTransformer.php b/src/Transformer/DateTimeTransformer.php index a35e173..14091a1 100644 --- a/src/Transformer/DateTimeTransformer.php +++ b/src/Transformer/DateTimeTransformer.php @@ -30,7 +30,7 @@ public function transform( mixed $source, mixed $target, Type $sourceType, - Type $targetType, + ?Type $targetType, array $context ): mixed { if (is_string($source)) { diff --git a/src/Transformer/NullTransformer.php b/src/Transformer/NullTransformer.php index e23b49c..fb94f9b 100644 --- a/src/Transformer/NullTransformer.php +++ b/src/Transformer/NullTransformer.php @@ -26,7 +26,7 @@ public function transform( mixed $source, mixed $target, Type $sourceType, - Type $targetType, + ?Type $targetType, array $context ): mixed { if ($target !== null) { diff --git a/src/Transformer/ObjectToArrayTransformer.php b/src/Transformer/ObjectToArrayTransformer.php index 088684d..6d552fd 100644 --- a/src/Transformer/ObjectToArrayTransformer.php +++ b/src/Transformer/ObjectToArrayTransformer.php @@ -36,7 +36,7 @@ public function transform( mixed $source, mixed $target, Type $sourceType, - Type $targetType, + ?Type $targetType, array $context ): mixed { if (!is_object($source)) { diff --git a/src/Transformer/ObjectToObjectTransformer.php b/src/Transformer/ObjectToObjectTransformer.php index e988a00..c95006f 100644 --- a/src/Transformer/ObjectToObjectTransformer.php +++ b/src/Transformer/ObjectToObjectTransformer.php @@ -56,9 +56,13 @@ public function transform( mixed $source, mixed $target, Type $sourceType, - Type $targetType, + ?Type $targetType, array $context ): mixed { + if ($targetType === null) { + throw new InvalidArgumentException('Target type must not be null.'); + } + // get object cache if (!isset($context[MainTransformer::OBJECT_CACHE])) { diff --git a/src/Transformer/ObjectToStringTransformer.php b/src/Transformer/ObjectToStringTransformer.php index 33fd3fb..72161d8 100644 --- a/src/Transformer/ObjectToStringTransformer.php +++ b/src/Transformer/ObjectToStringTransformer.php @@ -25,7 +25,7 @@ public function transform( mixed $source, mixed $target, Type $sourceType, - Type $targetType, + ?Type $targetType, array $context ): mixed { if ($source instanceof \Stringable) { diff --git a/src/Transformer/ScalarToScalarTransformer.php b/src/Transformer/ScalarToScalarTransformer.php index 974314f..24ebae7 100644 --- a/src/Transformer/ScalarToScalarTransformer.php +++ b/src/Transformer/ScalarToScalarTransformer.php @@ -26,7 +26,7 @@ public function transform( mixed $source, mixed $target, Type $sourceType, - Type $targetType, + ?Type $targetType, array $context ): mixed { if (!is_scalar($source)) { diff --git a/src/Transformer/StringToBackedEnumTransformer.php b/src/Transformer/StringToBackedEnumTransformer.php index 9ac2536..d48dd67 100644 --- a/src/Transformer/StringToBackedEnumTransformer.php +++ b/src/Transformer/StringToBackedEnumTransformer.php @@ -25,14 +25,14 @@ public function transform( mixed $source, mixed $target, Type $sourceType, - Type $targetType, + ?Type $targetType, array $context ): mixed { if (!is_string($source)) { throw new InvalidArgumentException(sprintf('Source must be string, "%s" given', get_debug_type($source))); } - $class = $targetType->getClassName(); + $class = $targetType?->getClassName(); if ($class === null || !\enum_exists($class)) { throw new InvalidArgumentException(sprintf('Target must be an enum class-string, "%s" given', get_debug_type($class))); diff --git a/src/Transformer/TraversableToArrayAccessTransformer.php b/src/Transformer/TraversableToArrayAccessTransformer.php index d38486d..845d9a1 100644 --- a/src/Transformer/TraversableToArrayAccessTransformer.php +++ b/src/Transformer/TraversableToArrayAccessTransformer.php @@ -23,7 +23,6 @@ use Rekalogika\Mapper\Exception\InvalidArgumentException; use Rekalogika\Mapper\Exception\InvalidTypeInArgumentException; use Rekalogika\Mapper\Exception\MissingMemberKeyTypeException; -use Rekalogika\Mapper\Exception\MissingMemberValueTypeException; use Rekalogika\Mapper\MainTransformer; use Rekalogika\Mapper\ObjectCache\ObjectCache; use Rekalogika\Mapper\ObjectCache\ObjectCacheFactoryInterface; @@ -44,9 +43,13 @@ public function transform( mixed $source, mixed $target, Type $sourceType, - Type $targetType, + ?Type $targetType, array $context ): mixed { + if ($targetType === null) { + throw new InvalidArgumentException('Target type must not be null.'); + } + // get object cache if (!isset($context[MainTransformer::OBJECT_CACHE])) { @@ -85,21 +88,13 @@ public function transform( $objectCache->saveTarget($source, $targetType, $target); - // We can't work if the target type doesn't contain the information - // about the type of its member objects - - $targetMemberValueType = $targetType->getCollectionValueTypes(); - - if (count($targetMemberValueType) === 0) { - throw new MissingMemberValueTypeException($sourceType, $targetType); - } - // Prepare variables for the output loop $targetMemberKeyType = $targetType->getCollectionKeyTypes(); $targetMemberKeyTypeIsMissing = count($targetMemberKeyType) === 0; $targetMemberKeyTypeIsInt = count($targetMemberKeyType) === 1 && TypeCheck::isInt($targetMemberKeyType[0]); + $targetMemberValueType = $targetType->getCollectionValueTypes(); /** @var mixed $sourceMemberValue */ foreach ($source as $sourceMemberKey => $sourceMemberValue) { diff --git a/src/Transformer/TraversableToTraversableTransformer.php b/src/Transformer/TraversableToTraversableTransformer.php index 4b590a3..163aec7 100644 --- a/src/Transformer/TraversableToTraversableTransformer.php +++ b/src/Transformer/TraversableToTraversableTransformer.php @@ -41,9 +41,12 @@ public function transform( mixed $source, mixed $target, Type $sourceType, - Type $targetType, + ?Type $targetType, array $context ): mixed { + if ($targetType === null) { + throw new InvalidArgumentException('Target type must not be null.'); + } // get object cache if (!isset($context[MainTransformer::OBJECT_CACHE])) { diff --git a/src/TypeResolver/CachingTypeResolver.php b/src/TypeResolver/CachingTypeResolver.php index b462d6d..24b7c91 100644 --- a/src/TypeResolver/CachingTypeResolver.php +++ b/src/TypeResolver/CachingTypeResolver.php @@ -60,17 +60,22 @@ public function getTypeString(Type|MixedType $type): string // can be expensive in a loop. we cache using a weakmap /** - * @var \WeakMap> + * @var \WeakMap> */ private \WeakMap $simpleTypesCache; - public function getSimpleTypes(Type $type): array + public function getSimpleTypes(Type|MixedType $type): array { + if ($type instanceof MixedType) { + return [$type]; + } + if ($result = $this->simpleTypesCache[$type] ?? null) { return $result; } $simpleTypes = $this->decorated->getSimpleTypes($type); + $this->simpleTypesCache->offsetSet($type, $simpleTypes); return $simpleTypes; diff --git a/src/TypeResolver/TypeResolver.php b/src/TypeResolver/TypeResolver.php index 52eabd4..0739fa3 100644 --- a/src/TypeResolver/TypeResolver.php +++ b/src/TypeResolver/TypeResolver.php @@ -71,14 +71,12 @@ public function isSimpleType(Type $type): bool return TypeUtil::isSimpleType($type); } - /** - * Gets all the possible simple types from a Type - * - * @param Type $type - * @return array - */ - public function getSimpleTypes(Type $type): array + public function getSimpleTypes(Type|MixedType $type): array { + if ($type instanceof MixedType) { + return [$type]; + } + return TypeUtil::getSimpleTypes($type); } diff --git a/src/TypeResolver/TypeResolverInterface.php b/src/TypeResolver/TypeResolverInterface.php index 89697eb..396aeaa 100644 --- a/src/TypeResolver/TypeResolverInterface.php +++ b/src/TypeResolver/TypeResolverInterface.php @@ -34,10 +34,10 @@ public function getTypeString(Type|MixedType $type): string; /** * Gets all the possible simple types from a Type * - * @param Type $type - * @return array + * @param Type|MixedType $type + * @return array */ - public function getSimpleTypes(Type $type): array; + public function getSimpleTypes(Type|MixedType $type): array; /** * Simple Type is a type that is not nullable, and does not have more diff --git a/src/Util/TypeCheck.php b/src/Util/TypeCheck.php index 2210745..92e32fe 100644 --- a/src/Util/TypeCheck.php +++ b/src/Util/TypeCheck.php @@ -35,33 +35,37 @@ public static function nameExists(string $class): bool || enum_exists($class); } - public static function isInt(Type $type): bool + public static function isInt(?Type $type): bool { - return $type->getBuiltinType() === Type::BUILTIN_TYPE_INT; + return $type?->getBuiltinType() === Type::BUILTIN_TYPE_INT; } - public static function isFloat(Type $type): bool + public static function isFloat(?Type $type): bool { - return $type->getBuiltinType() === Type::BUILTIN_TYPE_FLOAT; + return $type?->getBuiltinType() === Type::BUILTIN_TYPE_FLOAT; } - public static function isString(Type $type): bool + public static function isString(?Type $type): bool { - return $type->getBuiltinType() === Type::BUILTIN_TYPE_STRING; + return $type?->getBuiltinType() === Type::BUILTIN_TYPE_STRING; } - public static function isBool(Type $type): bool + public static function isBool(?Type $type): bool { - return $type->getBuiltinType() === Type::BUILTIN_TYPE_BOOL; + return $type?->getBuiltinType() === Type::BUILTIN_TYPE_BOOL; } - public static function isArray(Type $type): bool + public static function isArray(?Type $type): bool { - return $type->getBuiltinType() === Type::BUILTIN_TYPE_ARRAY; + return $type?->getBuiltinType() === Type::BUILTIN_TYPE_ARRAY; } - public static function isObject(Type $type): bool + public static function isObject(?Type $type): bool { + if ($type === null) { + return false; + } + if ($type->getBuiltinType() !== Type::BUILTIN_TYPE_OBJECT) { return false; } @@ -78,8 +82,12 @@ public static function isObject(Type $type): bool /** * @param class-string $classes */ - public static function isObjectOfType(Type $type, string ...$classes): bool + public static function isObjectOfType(?Type $type, string ...$classes): bool { + if ($type === null) { + return false; + } + if (!self::isObject($type)) { return false; } @@ -99,24 +107,27 @@ public static function isObjectOfType(Type $type, string ...$classes): bool return false; } - public static function isEnum(Type $type): bool + public static function isEnum(?Type $type): bool { + if ($type === null) { + return false; + } return $type->getBuiltinType() === Type::BUILTIN_TYPE_OBJECT && $type->getClassName() !== null && enum_exists($type->getClassName()); } - public static function isResource(Type $type): bool + public static function isResource(?Type $type): bool { - return $type->getBuiltinType() === Type::BUILTIN_TYPE_RESOURCE; + return $type?->getBuiltinType() === Type::BUILTIN_TYPE_RESOURCE; } - public static function isNull(Type $type): bool + public static function isNull(?Type $type): bool { - return $type->getBuiltinType() === Type::BUILTIN_TYPE_NULL; + return $type?->getBuiltinType() === Type::BUILTIN_TYPE_NULL; } - public static function isScalar(Type $type): bool + public static function isScalar(?Type $type): bool { return self::isInt($type) || self::isFloat($type) @@ -124,9 +135,9 @@ public static function isScalar(Type $type): bool || self::isBool($type); } - public static function isIterable(Type $type): bool + public static function isIterable(?Type $type): bool { - return $type->getBuiltinType() === Type::BUILTIN_TYPE_ITERABLE; + return $type?->getBuiltinType() === Type::BUILTIN_TYPE_ITERABLE; } /** diff --git a/src/Util/TypeUtil.php b/src/Util/TypeUtil.php index 1c5f8c9..40322f6 100644 --- a/src/Util/TypeUtil.php +++ b/src/Util/TypeUtil.php @@ -228,8 +228,12 @@ class: $type->getClassName(), } #[NamespaceVisibility(namespace: 'Rekalogika\Mapper\Exception')] - public static function getDebugType(Type $type): string + public static function getDebugType(null|Type|MixedType $type): string { + if ($type === null) { + return 'null'; + } + return TypeUtil::getTypeString($type); } diff --git a/tests/Fixtures/Transformer/MoneyToMoneyDtoTransformer.php b/tests/Fixtures/Transformer/MoneyToMoneyDtoTransformer.php index c0065be..25199c4 100644 --- a/tests/Fixtures/Transformer/MoneyToMoneyDtoTransformer.php +++ b/tests/Fixtures/Transformer/MoneyToMoneyDtoTransformer.php @@ -61,7 +61,7 @@ public function transform( mixed $source, mixed $target, Type $sourceType, - Type $targetType, + ?Type $targetType, array $context ): mixed { if ( diff --git a/tests/IntegrationTest/TraversableToArrayAccessMappingTest.php b/tests/IntegrationTest/TraversableToArrayAccessMappingTest.php index a4389b2..fcba2ac 100644 --- a/tests/IntegrationTest/TraversableToArrayAccessMappingTest.php +++ b/tests/IntegrationTest/TraversableToArrayAccessMappingTest.php @@ -15,7 +15,6 @@ use Doctrine\Common\Collections\ArrayCollection; use Doctrine\Common\Collections\Collection; -use Rekalogika\Mapper\Exception\MissingMemberValueTypeException; use Rekalogika\Mapper\Tests\Common\AbstractIntegrationTest; use Rekalogika\Mapper\Tests\Fixtures\ArrayLike\ObjectWithArrayProperty; use Rekalogika\Mapper\Tests\Fixtures\ArrayLike\ObjectWithArrayPropertyWithStringKey; @@ -25,36 +24,39 @@ use Rekalogika\Mapper\Tests\Fixtures\ArrayLikeDto\ObjectWithArrayPropertyDtoWithIntKey; use Rekalogika\Mapper\Tests\Fixtures\ArrayLikeDto\ObjectWithArrayPropertyWithoutTypeHintDto; use Rekalogika\Mapper\Tests\Fixtures\ArrayLikeDto\ObjectWithCollectionPropertyDto; +use Rekalogika\Mapper\Tests\Fixtures\Scalar\ObjectWithScalarProperties; use Rekalogika\Mapper\Tests\Fixtures\Scalar\ObjectWithScalarPropertiesDto; class TraversableToArrayAccessMappingTest extends AbstractIntegrationTest { - /** - * @todo array with mixed target type means we should just copy the source - */ public function testFailedArrayToArray(): void { $source = new ObjectWithArrayProperty(); - - // target array does not have information about the type of its - // elements - - $this->expectException(MissingMemberValueTypeException::class); $result = $this->mapper->map($source, ObjectWithArrayPropertyWithoutTypeHintDto::class); + + $this->assertInstanceOf(ObjectWithArrayPropertyWithoutTypeHintDto::class, $result); + $this->assertCount(3, $result->property); + $this->assertArrayHasKey(0, $result->property); + $this->assertArrayHasKey(1, $result->property); + $this->assertArrayHasKey(2, $result->property); + $this->assertInstanceOf(ObjectWithScalarProperties::class, $result->property[0]); + $this->assertInstanceOf(ObjectWithScalarProperties::class, $result->property[1]); + $this->assertInstanceOf(ObjectWithScalarProperties::class, $result->property[2]); } - /** - * @todo array with mixed target type means we should just copy the source - */ public function testFailedTraversableToArrayAccess(): void { $source = new ObjectWithTraversableProperties(); + $result = $this->mapper->map($source, ObjectWithArrayPropertyWithoutTypeHintDto::class); - // cannot do a direct mapping from array to \ArrayAccess because - // it does not have information about the type of its elements - - $this->expectException(MissingMemberValueTypeException::class); - $this->mapper->map($source, ObjectWithArrayPropertyWithoutTypeHintDto::class); + $this->assertInstanceOf(ObjectWithArrayPropertyWithoutTypeHintDto::class, $result); + $this->assertCount(3, $result->property); + $this->assertArrayHasKey(0, $result->property); + $this->assertArrayHasKey(1, $result->property); + $this->assertArrayHasKey(2, $result->property); + $this->assertInstanceOf(ObjectWithScalarProperties::class, $result->property[0]); + $this->assertInstanceOf(ObjectWithScalarProperties::class, $result->property[1]); + $this->assertInstanceOf(ObjectWithScalarProperties::class, $result->property[2]); } // @@ -242,7 +244,4 @@ public function testSourceStringKeyToTargetIntKey(): void $this->assertArrayHasKey(1, $result->property); $this->assertArrayHasKey(2, $result->property); } - - - }