Skip to content

Commit 9f905f6

Browse files
committed
fix: Null value to nullable target transformation (issue #4).
1 parent 35d6261 commit 9f905f6

11 files changed

+241
-31
lines changed

CHANGELOG.md

+2
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,8 @@
44

55
* fix(`MapperInterface`): Fix type-hint mismatch.
66
* test: Add hook to override the default `Context`.
7+
* test: Test with and without scalar short circuit.
8+
* fix: Null value to nullable target transformation (issue #4).
79

810
## 0.8.1
911

config/services.php

+5
Original file line numberDiff line numberDiff line change
@@ -42,6 +42,7 @@
4242
use Rekalogika\Mapper\Transformer\EagerPropertiesResolver\Implementation\ChainEagerPropertiesResolver;
4343
use Rekalogika\Mapper\Transformer\EagerPropertiesResolver\Implementation\DoctrineEagerPropertiesResolver;
4444
use Rekalogika\Mapper\Transformer\EagerPropertiesResolver\Implementation\HeuristicsEagerPropertiesResolver;
45+
use Rekalogika\Mapper\Transformer\NullToNullTransformer;
4546
use Rekalogika\Mapper\Transformer\NullTransformer;
4647
use Rekalogika\Mapper\Transformer\ObjectMapperTransformer;
4748
use Rekalogika\Mapper\Transformer\ObjectToArrayTransformer;
@@ -112,6 +113,10 @@
112113

113114
# transformers
114115

116+
$services
117+
->set(NullToNullTransformer::class)
118+
->tag('rekalogika.mapper.transformer', ['priority' => 1000]);
119+
115120
$services
116121
->set(ScalarToScalarTransformer::class)
117122
->tag('rekalogika.mapper.transformer', ['priority' => -350]);

src/Context/MapperOptions.php

+3-1
Original file line numberDiff line numberDiff line change
@@ -19,12 +19,14 @@
1919
final readonly class MapperOptions
2020
{
2121
/**
22-
* @param boolean $lazyLoading Enable or disable lazy loading.
22+
* @param boolean $lazyLoading Enable lazy loading.
2323
* @param boolean $readTargetValue If disabled, values on the target side will not be read, and assumed to be null.
24+
* @param boolean $objectToObjectScalarShortCircuit Performance optimization by doing scalar to scalar transformation within `ObjectToObjectTransformer` instead of delegating to the `MainTransformer`
2425
*/
2526
public function __construct(
2627
public bool $lazyLoading = true,
2728
public bool $readTargetValue = true,
29+
public bool $objectToObjectScalarShortCircuit = true,
2830
) {
2931
}
3032
}
+38
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,38 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
/*
6+
* This file is part of rekalogika/mapper package.
7+
*
8+
* (c) Priyadi Iman Nurcahyo <https://rekalogika.dev>
9+
*
10+
* For the full copyright and license information, please view the LICENSE file
11+
* that was distributed with this source code.
12+
*/
13+
14+
namespace Rekalogika\Mapper\Transformer;
15+
16+
use Rekalogika\Mapper\Context\Context;
17+
use Rekalogika\Mapper\Transformer\Contracts\TransformerInterface;
18+
use Rekalogika\Mapper\Transformer\Contracts\TypeMapping;
19+
use Rekalogika\Mapper\Util\TypeFactory;
20+
use Symfony\Component\PropertyInfo\Type;
21+
22+
final class NullToNullTransformer implements TransformerInterface
23+
{
24+
public function transform(
25+
mixed $source,
26+
mixed $target,
27+
?Type $sourceType,
28+
?Type $targetType,
29+
Context $context
30+
): mixed {
31+
return null;
32+
}
33+
34+
public function getSupportedTransformation(): iterable
35+
{
36+
yield new TypeMapping(TypeFactory::null(), TypeFactory::null());
37+
}
38+
}

src/Transformer/ObjectToObjectTransformer.php

+33-28
Original file line numberDiff line numberDiff line change
@@ -485,36 +485,41 @@ private function transformValue(
485485
$sourcePropertyValue = $this->readerWriter
486486
->readSourceProperty($source, $propertyMapping, $context);
487487

488-
// short circuit if the the source is null or scalar, and the target
489-
// is a scalar, so we don't have to delegate to the main transformer
490-
491-
$targetScalarType = $propertyMapping->getTargetScalarType();
492-
493-
if ($targetScalarType !== null) {
494-
if ($sourcePropertyValue === null) {
495-
return match ($targetScalarType) {
496-
'int' => 0,
497-
'float' => 0.0,
498-
'string' => '',
499-
'bool' => false,
500-
'null' => null,
501-
};
502-
} elseif (is_scalar($sourcePropertyValue)) {
503-
return match ($targetScalarType) {
504-
'int' => (int) $sourcePropertyValue,
505-
'float' => (float) $sourcePropertyValue,
506-
'string' => (string) $sourcePropertyValue,
507-
'bool' => (bool) $sourcePropertyValue,
508-
'null' => null,
509-
};
510-
}
511-
}
488+
// short circuit. optimization for transformation between scalar and
489+
// null, so that we don't have to go through the main transformer for
490+
// this common task.
512491

513-
// short circuit: if source is null & target accepts null, we set the
514-
// target to null
492+
if ($context(MapperOptions::class)?->objectToObjectScalarShortCircuit) {
493+
// if source is null & target accepts null, we set the
494+
// target to null
515495

516-
if ($propertyMapping->targetCanAcceptNull() && $sourcePropertyValue === null) {
517-
return null;
496+
if ($propertyMapping->targetCanAcceptNull() && $sourcePropertyValue === null) {
497+
return null;
498+
}
499+
500+
// if the the source is null or scalar, and the target is a scalar
501+
502+
$targetScalarType = $propertyMapping->getTargetScalarType();
503+
504+
if ($targetScalarType !== null) {
505+
if ($sourcePropertyValue === null) {
506+
return match ($targetScalarType) {
507+
'int' => 0,
508+
'float' => 0.0,
509+
'string' => '',
510+
'bool' => false,
511+
'null' => null,
512+
};
513+
} elseif (is_scalar($sourcePropertyValue)) {
514+
return match ($targetScalarType) {
515+
'int' => (int) $sourcePropertyValue,
516+
'float' => (float) $sourcePropertyValue,
517+
'string' => (string) $sourcePropertyValue,
518+
'bool' => (bool) $sourcePropertyValue,
519+
'null' => null,
520+
};
521+
}
522+
}
518523
}
519524

520525
// get the value of the target property if the target is an object and
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
/*
6+
* This file is part of rekalogika/mapper package.
7+
*
8+
* (c) Priyadi Iman Nurcahyo <https://rekalogika.dev>
9+
*
10+
* For the full copyright and license information, please view the LICENSE file
11+
* that was distributed with this source code.
12+
*/
13+
14+
namespace Rekalogika\Mapper\Tests\Fixtures\Scalar;
15+
16+
class ObjectWithScalarPropertiesWithNullContents
17+
{
18+
public ?int $a = null;
19+
public ?string $b = null;
20+
public ?bool $c = null;
21+
public ?float $d = null;
22+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
/*
6+
* This file is part of rekalogika/mapper package.
7+
*
8+
* (c) Priyadi Iman Nurcahyo <https://rekalogika.dev>
9+
*
10+
* For the full copyright and license information, please view the LICENSE file
11+
* that was distributed with this source code.
12+
*/
13+
14+
namespace Rekalogika\Mapper\Tests\Fixtures\ScalarDto;
15+
16+
class ObjectWithScalarConstructorArgumentDto
17+
{
18+
public function __construct(
19+
public ?int $a = null,
20+
public ?string $b = null,
21+
public ?bool $c = null,
22+
public ?float $d = null,
23+
) {
24+
}
25+
}

tests/IntegrationTest/ScalarPropertiesMappingTest.php tests/IntegrationTest/ScalarToScalar/AbstractScalarPropertiesMappingTest.php

+37-2
Original file line numberDiff line numberDiff line change
@@ -11,17 +11,19 @@
1111
* that was distributed with this source code.
1212
*/
1313

14-
namespace Rekalogika\Mapper\Tests\IntegrationTest;
14+
namespace Rekalogika\Mapper\Tests\IntegrationTest\ScalarToScalar;
1515

1616
use Rekalogika\Mapper\Tests\Common\AbstractFrameworkTest;
1717
use Rekalogika\Mapper\Tests\Fixtures\Scalar\ObjectWithScalarProperties;
18+
use Rekalogika\Mapper\Tests\Fixtures\Scalar\ObjectWithScalarPropertiesWithNullContents;
1819
use Rekalogika\Mapper\Tests\Fixtures\ScalarDto\ObjectWithBoolPropertiesDto;
1920
use Rekalogika\Mapper\Tests\Fixtures\ScalarDto\ObjectWithFloatPropertiesDto;
2021
use Rekalogika\Mapper\Tests\Fixtures\ScalarDto\ObjectWithIntPropertiesDto;
22+
use Rekalogika\Mapper\Tests\Fixtures\ScalarDto\ObjectWithScalarConstructorArgumentDto;
2123
use Rekalogika\Mapper\Tests\Fixtures\ScalarDto\ObjectWithScalarPropertiesDto;
2224
use Rekalogika\Mapper\Tests\Fixtures\ScalarDto\ObjectWithStringPropertiesDto;
2325

24-
class ScalarPropertiesMappingTest extends AbstractFrameworkTest
26+
abstract class AbstractScalarPropertiesMappingTest extends AbstractFrameworkTest
2527
{
2628
public function testScalarIdentity(): void
2729
{
@@ -34,6 +36,39 @@ public function testScalarIdentity(): void
3436
$this->assertEquals($class->d, $dto->d);
3537
}
3638

39+
public function testScalarIdentityWithTargetConstructorArgument(): void
40+
{
41+
$class = new ObjectWithScalarProperties();
42+
$dto = $this->mapper->map($class, ObjectWithScalarConstructorArgumentDto::class);
43+
44+
$this->assertEquals($class->a, $dto->a);
45+
$this->assertEquals($class->b, $dto->b);
46+
$this->assertEquals($class->c, $dto->c);
47+
$this->assertEquals($class->d, $dto->d);
48+
}
49+
50+
public function testNullSourcesToScalarNullableTargets(): void
51+
{
52+
$class = new ObjectWithScalarPropertiesWithNullContents();
53+
$dto = $this->mapper->map($class, ObjectWithScalarPropertiesDto::class);
54+
55+
$this->assertNull($dto->a);
56+
$this->assertNull($dto->b);
57+
$this->assertNull($dto->c);
58+
$this->assertNull($dto->d);
59+
}
60+
61+
public function testNullSourcesToScalarNullableConstructorArgumentsTargets(): void
62+
{
63+
$class = new ObjectWithScalarPropertiesWithNullContents();
64+
$dto = $this->mapper->map($class, ObjectWithScalarConstructorArgumentDto::class);
65+
66+
$this->assertEquals($class->a, $dto->a);
67+
$this->assertEquals($class->b, $dto->b);
68+
$this->assertEquals($class->c, $dto->c);
69+
$this->assertEquals($class->d, $dto->d);
70+
}
71+
3772
public function testScalarToInt(): void
3873
{
3974
$class = new ObjectWithScalarProperties();
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
/*
6+
* This file is part of rekalogika/mapper package.
7+
*
8+
* (c) Priyadi Iman Nurcahyo <https://rekalogika.dev>
9+
*
10+
* For the full copyright and license information, please view the LICENSE file
11+
* that was distributed with this source code.
12+
*/
13+
14+
namespace Rekalogika\Mapper\Tests\IntegrationTest\ScalarToScalar;
15+
16+
use Rekalogika\Mapper\Context\Context;
17+
use Rekalogika\Mapper\Context\MapperOptions;
18+
19+
class ScalarPropertiesMappingWithShortCircuitTest extends AbstractScalarPropertiesMappingTest
20+
{
21+
protected function getMapperContext(): Context
22+
{
23+
return Context::create(
24+
new MapperOptions(
25+
objectToObjectScalarShortCircuit: true
26+
)
27+
);
28+
}
29+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
/*
6+
* This file is part of rekalogika/mapper package.
7+
*
8+
* (c) Priyadi Iman Nurcahyo <https://rekalogika.dev>
9+
*
10+
* For the full copyright and license information, please view the LICENSE file
11+
* that was distributed with this source code.
12+
*/
13+
14+
namespace Rekalogika\Mapper\Tests\IntegrationTest\ScalarToScalar;
15+
16+
class ScalarPropertiesMappingWithoutOptionsTest extends AbstractScalarPropertiesMappingTest
17+
{
18+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
/*
6+
* This file is part of rekalogika/mapper package.
7+
*
8+
* (c) Priyadi Iman Nurcahyo <https://rekalogika.dev>
9+
*
10+
* For the full copyright and license information, please view the LICENSE file
11+
* that was distributed with this source code.
12+
*/
13+
14+
namespace Rekalogika\Mapper\Tests\IntegrationTest\ScalarToScalar;
15+
16+
use Rekalogika\Mapper\Context\Context;
17+
use Rekalogika\Mapper\Context\MapperOptions;
18+
19+
class ScalarPropertiesMappingWithoutShortCircuitTest extends AbstractScalarPropertiesMappingTest
20+
{
21+
protected function getMapperContext(): Context
22+
{
23+
return Context::create(
24+
new MapperOptions(
25+
objectToObjectScalarShortCircuit: false
26+
)
27+
);
28+
}
29+
}

0 commit comments

Comments
 (0)