diff --git a/CHANGELOG.md b/CHANGELOG.md index 106bde6d..1888e903 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,6 +4,7 @@ * fix: rethrow our exceptions in `writeTargetProperty`, fix confusing error message +* fix: error when mapping null to existing target ## 1.13.2 diff --git a/src/MainTransformer/Exception/TransformerReturnsUnexpectedValueException.php b/src/MainTransformer/Exception/TransformerReturnsUnexpectedValueException.php index 4502ade2..b5b16341 100644 --- a/src/MainTransformer/Exception/TransformerReturnsUnexpectedValueException.php +++ b/src/MainTransformer/Exception/TransformerReturnsUnexpectedValueException.php @@ -14,6 +14,7 @@ namespace Rekalogika\Mapper\MainTransformer\Exception; use Rekalogika\Mapper\Context\Context; +use Rekalogika\Mapper\Debug\TraceableTransformer; use Rekalogika\Mapper\Exception\UnexpectedValueException; use Rekalogika\Mapper\Transformer\MixedType; use Rekalogika\Mapper\Transformer\TransformerInterface; @@ -29,6 +30,10 @@ public function __construct( TransformerInterface $transformer, Context $context, ) { + if ($transformer instanceof TraceableTransformer) { + $transformer = $transformer->getDecorated(); + } + $message = \sprintf( 'Trying to map source type "%s" to target type "%s", but the assigned transformer "%s" returns an unexpected type "%s".', get_debug_type($source), diff --git a/src/Transformer/Exception/NullSourceButMandatoryTargetException.php b/src/Transformer/Exception/NullSourceButMandatoryTargetException.php new file mode 100644 index 00000000..3d8ec265 --- /dev/null +++ b/src/Transformer/Exception/NullSourceButMandatoryTargetException.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\Exception; + +use Rekalogika\Mapper\Context\Context; +use Rekalogika\Mapper\Exception\RuntimeException; +use Rekalogika\Mapper\Util\TypeFactory; +use Rekalogika\Mapper\Util\TypeUtil; +use Symfony\Component\PropertyInfo\Type; + +class NullSourceButMandatoryTargetException extends RuntimeException +{ + public function __construct( + ?Type $targetType, + ?\Throwable $previous = null, + ?Context $context = null, + ) { + parent::__construct( + message: \sprintf( + 'The source is null, the target is mandatory & expected to be of type "%s". But no transformer is able to handle this case.', + TypeUtil::getTypeString($targetType ?? TypeFactory::mixed()), + ), + previous: $previous, + context: $context, + ); + } +} diff --git a/src/Transformer/Implementation/NullTransformer.php b/src/Transformer/Implementation/NullTransformer.php index 509ad196..1dfcedac 100644 --- a/src/Transformer/Implementation/NullTransformer.php +++ b/src/Transformer/Implementation/NullTransformer.php @@ -15,6 +15,7 @@ use Rekalogika\Mapper\Context\Context; use Rekalogika\Mapper\Exception\InvalidArgumentException; +use Rekalogika\Mapper\Transformer\Exception\NullSourceButMandatoryTargetException; use Rekalogika\Mapper\Transformer\TransformerInterface; use Rekalogika\Mapper\Transformer\TypeMapping; use Rekalogika\Mapper\Util\TypeCheck; @@ -31,6 +32,21 @@ public function transform( ?Type $targetType, Context $context, ): mixed { + // if the source is null & the target is an object, ignore it by + // returning the existing target. if the existing target is not already + // an object, throw an exception + + if (TypeCheck::isObject($targetType)) { + if ($target === null) { + throw new NullSourceButMandatoryTargetException( + targetType: $targetType, + context: $context, + ); + } + + return $target; + } + if ($target !== null) { throw new InvalidArgumentException('Target must be null'); } @@ -70,6 +86,7 @@ public function getSupportedTransformation(): iterable yield new TypeMapping(TypeFactory::null(), TypeFactory::float()); yield new TypeMapping(TypeFactory::null(), TypeFactory::bool()); yield new TypeMapping(TypeFactory::null(), TypeFactory::array()); + yield new TypeMapping(TypeFactory::null(), TypeFactory::object(), true); yield new TypeMapping(TypeFactory::mixed(), TypeFactory::null()); } } diff --git a/src/Util/TypeUtil.php b/src/Util/TypeUtil.php index c0ab1efa..7478eb13 100644 --- a/src/Util/TypeUtil.php +++ b/src/Util/TypeUtil.php @@ -21,6 +21,7 @@ use Rekalogika\Mapper\Tests\IntegrationTest\MapPropertyPathTest; use Rekalogika\Mapper\Tests\UnitTest\Util\TypeUtil2Test; use Rekalogika\Mapper\Tests\UnitTest\Util\TypeUtilTest; +use Rekalogika\Mapper\Transformer\Exception\NullSourceButMandatoryTargetException; use Rekalogika\Mapper\Transformer\MixedType; use Rekalogika\Mapper\TypeResolver\Implementation\TypeResolver; use Symfony\Component\PropertyInfo\Type; @@ -257,6 +258,7 @@ public static function getDebugType(null|Type|MixedType|array $type): string TypeUtilTest::class, TraceData::class, MapPropertyPathTest::class, + NullSourceButMandatoryTargetException::class, )] public static function getTypeString(Type|MixedType $type): string { diff --git a/tests/config/rekalogika-mapper/generated-mappings.php b/tests/config/rekalogika-mapper/generated-mappings.php index fce91e61..4da7fece 100644 --- a/tests/config/rekalogika-mapper/generated-mappings.php +++ b/tests/config/rekalogika-mapper/generated-mappings.php @@ -693,6 +693,18 @@ target: \Rekalogika\Mapper\Tests\Fixtures\Money\ObjectWithIntegerBackedMoneyProperty::class ); + $mappingCollection->addObjectMapping( + // tests/src/IntegrationTest/NullSourceTest.php on line 36 + source: \Rekalogika\Mapper\Tests\Fixtures\NullSource\Source::class, + target: \Rekalogika\Mapper\Tests\Fixtures\NullSource\TargetString::class + ); + + $mappingCollection->addObjectMapping( + // tests/src/IntegrationTest/NullSourceTest.php on line 27 + source: \Rekalogika\Mapper\Tests\Fixtures\NullSource\Source::class, + target: \Rekalogika\Mapper\Tests\Fixtures\NullSource\TargetUuid::class + ); + $mappingCollection->addObjectMapping( // tests/src/IntegrationTest/ObjectKeysTest.php on line 27 source: \Rekalogika\Mapper\Tests\Fixtures\ObjectKeys\RelationshipMap::class, diff --git a/tests/src/Fixtures/NullSource/Source.php b/tests/src/Fixtures/NullSource/Source.php new file mode 100644 index 00000000..cb3215de --- /dev/null +++ b/tests/src/Fixtures/NullSource/Source.php @@ -0,0 +1,23 @@ + + * + * For the full copyright and license information, please view the LICENSE file + * that was distributed with this source code. + */ + +namespace Rekalogika\Mapper\Tests\Fixtures\NullSource; + +final readonly class Source +{ + // @phpstan-ignore return.unusedType + public function getProperty(): ?string + { + return null; + } +} diff --git a/tests/src/Fixtures/NullSource/TargetString.php b/tests/src/Fixtures/NullSource/TargetString.php new file mode 100644 index 00000000..b7588bb0 --- /dev/null +++ b/tests/src/Fixtures/NullSource/TargetString.php @@ -0,0 +1,31 @@ + + * + * For the full copyright and license information, please view the LICENSE file + * that was distributed with this source code. + */ + +namespace Rekalogika\Mapper\Tests\Fixtures\NullSource; + +use Symfony\Component\Uid\Uuid; + +class TargetString +{ + private string $property; + + public function __construct() + { + $this->property = (string) Uuid::v7(); + } + + public function getProperty(): string + { + return $this->property; + } +} diff --git a/tests/src/Fixtures/NullSource/TargetUuid.php b/tests/src/Fixtures/NullSource/TargetUuid.php new file mode 100644 index 00000000..a5c0842b --- /dev/null +++ b/tests/src/Fixtures/NullSource/TargetUuid.php @@ -0,0 +1,31 @@ + + * + * For the full copyright and license information, please view the LICENSE file + * that was distributed with this source code. + */ + +namespace Rekalogika\Mapper\Tests\Fixtures\NullSource; + +use Symfony\Component\Uid\Uuid; + +class TargetUuid +{ + private Uuid $property; + + public function __construct() + { + $this->property = Uuid::v7(); + } + + public function getProperty(): Uuid + { + return $this->property; + } +} diff --git a/tests/src/IntegrationTest/ConstructorTest.php b/tests/src/IntegrationTest/ConstructorTest.php index 319804b8..5f127e85 100644 --- a/tests/src/IntegrationTest/ConstructorTest.php +++ b/tests/src/IntegrationTest/ConstructorTest.php @@ -13,7 +13,6 @@ namespace Rekalogika\Mapper\Tests\IntegrationTest; -use Rekalogika\Mapper\MainTransformer\Exception\CannotFindTransformerException; use Rekalogika\Mapper\Tests\Common\FrameworkTestCase; use Rekalogika\Mapper\Tests\Fixtures\Constructor\ObjectWithConstructorAndExtraMandatoryArgumentDto; use Rekalogika\Mapper\Tests\Fixtures\Constructor\ObjectWithConstructorAndPropertiesDto; @@ -25,6 +24,7 @@ use Rekalogika\Mapper\Tests\Fixtures\Scalar\ObjectWithScalarPropertiesAndAdditionalNullProperty; use Rekalogika\Mapper\Transformer\Exception\ClassNotInstantiableException; use Rekalogika\Mapper\Transformer\Exception\InstantiationFailureException; +use Rekalogika\Mapper\Transformer\Exception\NullSourceButMandatoryTargetException; class ConstructorTest extends FrameworkTestCase { @@ -91,7 +91,7 @@ public function testFromEmptyStdClassToMandatoryArguments(): void public function testFromEmptyStdClassToMandatoryArgumentsThatCannotBeCastFromNull(): void { - $this->expectException(CannotFindTransformerException::class); + $this->expectException(NullSourceButMandatoryTargetException::class); $source = new \stdClass(); $this->mapper->map($source, ObjectWithMandatoryConstructorThatCannotBeCastFromNullDto::class); } diff --git a/tests/src/IntegrationTest/NullSourceTest.php b/tests/src/IntegrationTest/NullSourceTest.php new file mode 100644 index 00000000..ebd5d8ec --- /dev/null +++ b/tests/src/IntegrationTest/NullSourceTest.php @@ -0,0 +1,41 @@ + + * + * For the full copyright and license information, please view the LICENSE file + * that was distributed with this source code. + */ + +namespace Rekalogika\Mapper\Tests\IntegrationTest; + +use Rekalogika\Mapper\Tests\Common\FrameworkTestCase; +use Rekalogika\Mapper\Tests\Fixtures\NullSource\Source; +use Rekalogika\Mapper\Tests\Fixtures\NullSource\TargetString; +use Rekalogika\Mapper\Tests\Fixtures\NullSource\TargetUuid; + +class NullSourceTest extends FrameworkTestCase +{ + public function testNullSourceToUuid(): void + { + $source = new Source(); + $target = new TargetUuid(); + $newTarget = $this->mapper->map($source, $target); + + $this->assertSame($target, $newTarget); + } + + public function testNullSourceToString(): void + { + $source = new Source(); + $target = new TargetString(); + $newTarget = $this->mapper->map($source, $target); + + $this->assertSame($target, $newTarget); + } + +}