Skip to content

Commit 511fa75

Browse files
authored
Map null for nullable object with empty array data (#22)
1 parent e232b36 commit 511fa75

File tree

4 files changed

+57
-7
lines changed

4 files changed

+57
-7
lines changed

src/MapperConfig.php

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -41,6 +41,11 @@ class MapperConfig
4141
*/
4242
public bool $enumTryFrom = false;
4343

44+
/**
45+
* If true, an empty array will be mapped as null to a nullable object type.
46+
*/
47+
public bool $nullObjectFromEmptyArray = false;
48+
4449
/**
4550
* If true, mapping a null value to a non-nullable field will throw an UnexpectedNullValueException.
4651
*/

src/Objects/ObjectMapper.php

Lines changed: 23 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -46,12 +46,12 @@ public function map(DataType|string $type, array|string $data): ?object
4646
return $class::from($data);
4747
}
4848

49-
$functionName = self::MAPPER_FUNCTION_PREFIX . \md5($class);
49+
$functionName = self::MAPPER_FUNCTION_PREFIX . \md5($class . ($type instanceof DataType && $type->isNullable ? '1' : '0'));
5050
if ($this->mapper->config->classCacheKeySource === 'md5' || $this->mapper->config->classCacheKeySource === 'modified') {
5151
$reflection = new ReflectionClass($class);
5252
$functionName = match ($this->mapper->config->classCacheKeySource) {
53-
'md5' => self::MAPPER_FUNCTION_PREFIX . \md5_file($reflection->getFileName()),
54-
'modified' => self::MAPPER_FUNCTION_PREFIX . \md5(\filemtime($reflection->getFileName())),
53+
'md5' => self::MAPPER_FUNCTION_PREFIX . \md5(\md5_file($reflection->getFileName()) . $functionName),
54+
'modified' => self::MAPPER_FUNCTION_PREFIX . \md5(\filemtime($reflection->getFileName()) . $functionName),
5555
};
5656
}
5757

@@ -62,6 +62,7 @@ public function map(DataType|string $type, array|string $data): ?object
6262
$this->createObjectMappingFunction(
6363
$this->classBluePrinter->print($class),
6464
$functionName,
65+
$type instanceof DataType && $type->isNullable,
6566
),
6667
);
6768
}
@@ -88,14 +89,24 @@ public function mapperDirectory(): string
8889
return \rtrim($dir, \DIRECTORY_SEPARATOR);
8990
}
9091

91-
private function createObjectMappingFunction(ClassBluePrint $blueprint, string $mapFunctionName): string
92+
private function createObjectMappingFunction(ClassBluePrint $blueprint, string $mapFunctionName, bool $isNullable): string
9293
{
9394
$tab = ' ';
95+
$content = '';
96+
97+
if ($isNullable) {
98+
$content .= $tab . $tab . 'if ($data === [] && $mapper->config->nullObjectFromEmptyArray) {' . \PHP_EOL;
99+
$content .= $tab . $tab . $tab . 'return null;' . \PHP_EOL;
100+
$content .= $tab . $tab . '}' . \PHP_EOL . \PHP_EOL;
101+
}
94102

95103
// Instantiate a new object
96104
$args = [];
97105
foreach ($blueprint->constructorArguments as $name => $argument) {
98106
$arg = "\$data['{$name}']";
107+
if ($argument['type']->isNullable()) {
108+
$arg = "({$arg} ?? null)";
109+
}
99110

100111
if ($argument['type'] !== null) {
101112
$arg = $this->castInMapperFunction($arg, $argument['type'], $blueprint);
@@ -107,7 +118,7 @@ private function createObjectMappingFunction(ClassBluePrint $blueprint, string $
107118

108119
$args[] = $arg;
109120
}
110-
$content = '$x = new ' . $blueprint->namespacedClassName . '(' . \implode(', ', $args) . ');';
121+
$content .= $tab . $tab . '$x = new ' . $blueprint->namespacedClassName . '(' . \implode(', ', $args) . ');';
111122

112123
// Map properties
113124
foreach ($blueprint->properties as $name => $property) {
@@ -118,7 +129,12 @@ private function createObjectMappingFunction(ClassBluePrint $blueprint, string $
118129
continue;
119130
}
120131

121-
$propertyMap = $this->castInMapperFunction("\$data['{$name}']", $property['type'], $blueprint);
132+
$propertyName = "\$data['{$name}']";
133+
if ($property['type']->isNullable()) {
134+
$propertyName = "({$propertyName} ?? null)";
135+
}
136+
137+
$propertyMap = $this->castInMapperFunction($propertyName, $property['type'], $blueprint);
122138
if (\array_key_exists('default', $property)) {
123139
$propertyMap = $this->wrapDefault($propertyMap, $name, $property['default']);
124140
}
@@ -151,7 +167,7 @@ private function createObjectMappingFunction(ClassBluePrint $blueprint, string $
151167
if (! \\function_exists('{$mapFunctionName}')) {
152168
function {$mapFunctionName}({$mapperClass} \$mapper, array \$data)
153169
{
154-
{$content}
170+
{$content}
155171
156172
return \$x;
157173
}

tests/MapperTest.php

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@
88
use Jerodev\DataMapper\MapperConfig;
99
use Jerodev\DataMapper\Tests\_Mocks\Aliases;
1010
use Jerodev\DataMapper\Tests\_Mocks\Constructor;
11+
use Jerodev\DataMapper\Tests\_Mocks\Nullable;
1112
use Jerodev\DataMapper\Tests\_Mocks\SelfMapped;
1213
use Jerodev\DataMapper\Tests\_Mocks\SuitEnum;
1314
use Jerodev\DataMapper\Tests\_Mocks\SuperUserDto;
@@ -24,6 +25,21 @@ public function it_should_map_native_values(string $type, mixed $value, mixed $e
2425
{
2526
$this->assertSame($expectation, (new Mapper())->map($type, $value));
2627
}
28+
29+
/** @test */
30+
public function it_should_map_nullable_objects_from_empty_array(): void
31+
{
32+
$options = new MapperConfig();
33+
$options->nullObjectFromEmptyArray = true;
34+
$mapper = new Mapper($options);
35+
36+
// Return null for nullable object
37+
$this->assertNull($mapper->map('?' . Nullable::class, []));
38+
39+
// Don't return null if not nullable
40+
$this->assertInstanceOf(Nullable::class, $mapper->map(Nullable::class, []));
41+
}
42+
2743
/**
2844
* @test
2945
* @dataProvider objectValuesDataProvider

tests/_Mocks/Nullable.php

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
<?php
2+
3+
namespace Jerodev\DataMapper\Tests\_Mocks;
4+
5+
class Nullable
6+
{
7+
public ?string $name;
8+
9+
public function __construct(
10+
public ?int $id,
11+
) {
12+
}
13+
}

0 commit comments

Comments
 (0)