Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions src/MapperConfig.php
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,11 @@ class MapperConfig
*/
public bool $enumTryFrom = false;

/**
* If true, an empty array will be mapped as null to a nullable object type.
*/
public bool $nullObjectFromEmptyArray = false;

/**
* If true, mapping a null value to a non-nullable field will throw an UnexpectedNullValueException.
*/
Expand Down
30 changes: 23 additions & 7 deletions src/Objects/ObjectMapper.php
Original file line number Diff line number Diff line change
Expand Up @@ -46,12 +46,12 @@ public function map(DataType|string $type, array|string $data): ?object
return $class::from($data);
}

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

Expand All @@ -62,6 +62,7 @@ public function map(DataType|string $type, array|string $data): ?object
$this->createObjectMappingFunction(
$this->classBluePrinter->print($class),
$functionName,
$type instanceof DataType && $type->isNullable,
),
);
}
Expand All @@ -88,14 +89,24 @@ public function mapperDirectory(): string
return \rtrim($dir, \DIRECTORY_SEPARATOR);
}

private function createObjectMappingFunction(ClassBluePrint $blueprint, string $mapFunctionName): string
private function createObjectMappingFunction(ClassBluePrint $blueprint, string $mapFunctionName, bool $isNullable): string
{
$tab = ' ';
$content = '';

if ($isNullable) {
$content .= $tab . $tab . 'if ($data === [] && $mapper->config->nullObjectFromEmptyArray) {' . \PHP_EOL;
$content .= $tab . $tab . $tab . 'return null;' . \PHP_EOL;
$content .= $tab . $tab . '}' . \PHP_EOL . \PHP_EOL;
}

// Instantiate a new object
$args = [];
foreach ($blueprint->constructorArguments as $name => $argument) {
$arg = "\$data['{$name}']";
if ($argument['type']->isNullable()) {
$arg = "({$arg} ?? null)";
}

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

$args[] = $arg;
}
$content = '$x = new ' . $blueprint->namespacedClassName . '(' . \implode(', ', $args) . ');';
$content .= $tab . $tab . '$x = new ' . $blueprint->namespacedClassName . '(' . \implode(', ', $args) . ');';

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

$propertyMap = $this->castInMapperFunction("\$data['{$name}']", $property['type'], $blueprint);
$propertyName = "\$data['{$name}']";
if ($property['type']->isNullable()) {
$propertyName = "({$propertyName} ?? null)";
}

$propertyMap = $this->castInMapperFunction($propertyName, $property['type'], $blueprint);
if (\array_key_exists('default', $property)) {
$propertyMap = $this->wrapDefault($propertyMap, $name, $property['default']);
}
Expand Down Expand Up @@ -151,7 +167,7 @@ private function createObjectMappingFunction(ClassBluePrint $blueprint, string $
if (! \\function_exists('{$mapFunctionName}')) {
function {$mapFunctionName}({$mapperClass} \$mapper, array \$data)
{
{$content}
{$content}

return \$x;
}
Expand Down
16 changes: 16 additions & 0 deletions tests/MapperTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@
use Jerodev\DataMapper\MapperConfig;
use Jerodev\DataMapper\Tests\_Mocks\Aliases;
use Jerodev\DataMapper\Tests\_Mocks\Constructor;
use Jerodev\DataMapper\Tests\_Mocks\Nullable;
use Jerodev\DataMapper\Tests\_Mocks\SelfMapped;
use Jerodev\DataMapper\Tests\_Mocks\SuitEnum;
use Jerodev\DataMapper\Tests\_Mocks\SuperUserDto;
Expand All @@ -24,6 +25,21 @@ public function it_should_map_native_values(string $type, mixed $value, mixed $e
{
$this->assertSame($expectation, (new Mapper())->map($type, $value));
}

/** @test */
public function it_should_map_nullable_objects_from_empty_array(): void
{
$options = new MapperConfig();
$options->nullObjectFromEmptyArray = true;
$mapper = new Mapper($options);

// Return null for nullable object
$this->assertNull($mapper->map('?' . Nullable::class, []));

// Don't return null if not nullable
$this->assertInstanceOf(Nullable::class, $mapper->map(Nullable::class, []));
}

/**
* @test
* @dataProvider objectValuesDataProvider
Expand Down
13 changes: 13 additions & 0 deletions tests/_Mocks/Nullable.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
<?php

namespace Jerodev\DataMapper\Tests\_Mocks;

class Nullable
{
public ?string $name;

public function __construct(
public ?int $id,
) {
}
}