Skip to content

Commit

Permalink
Fix mappings involving DateTimeInterface type (#26)
Browse files Browse the repository at this point in the history
* create unit tests for all of the possible DateTime mappings
* add tests for nullable DateTime fields
* Expect TypeError instead of Throwable
* Use createFromInterface for more reliable DateTime mapping
* php-cs-fixer run
* Added change log entry
  • Loading branch information
priyadi authored Dec 20, 2023
1 parent 901a867 commit d12d47d
Show file tree
Hide file tree
Showing 16 changed files with 235 additions and 38 deletions.
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
## [Unreleased]
### Added
- [GH#10](https://github.com/jolicode/automapper/pull/10) Introduce custom transformers
- [GH#26](https://github.com/jolicode/automapper/pull/26) Fix mappings involving DateTimeInterface type

## [8.1.0] - 2023-12-14
### Added
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -9,28 +9,24 @@
use PhpParser\Node\Arg;
use PhpParser\Node\Expr;
use PhpParser\Node\Name;
use PhpParser\Node\Scalar\String_;

/**
* Transform DateTimeImmutable to DateTime.
* Transform DateTimeInterface to DateTimeImmutable.
*
* @author Joel Wurtz <[email protected]>
*/
final class DateTimeImmutableToMutableTransformer implements TransformerInterface
final class DateTimeInterfaceToImmutableTransformer implements TransformerInterface
{
public function transform(Expr $input, Expr $target, PropertyMapping $propertyMapping, UniqueVariableScope $uniqueVariableScope): array
{
/*
* In case of immutable source we clone the value by using format into a new mutable DateTime.
* Handles all DateTime instance types using createFromInterface.
*
* \DateTime::createFromFormat(\DateTime::RFC3339, $input->format(\DateTime::RFC3339));
* \DateTimeImmutable::createFromInterface($input);
*/
return [
new Expr\StaticCall(new Name\FullyQualified(\DateTime::class), 'createFromFormat', [
new Arg(new String_(\DateTime::RFC3339)),
new Arg(new Expr\MethodCall($input, 'format', [
new Arg(new String_(\DateTime::RFC3339)),
])),
new Expr\StaticCall(new Name\FullyQualified(\DateTimeImmutable::class), 'createFromInterface', [
new Arg($input),
]),
[],
];
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -11,21 +11,21 @@
use PhpParser\Node\Name;

/**
* Transform DateTime to DateTimeImmutable.
* Transform DateTimeInterface to DateTime.
*
* @author Joel Wurtz <[email protected]>
*/
final class DateTimeMutableToImmutableTransformer implements TransformerInterface
final class DateTimeInterfaceToMutableTransformer implements TransformerInterface
{
public function transform(Expr $input, Expr $target, PropertyMapping $propertyMapping, UniqueVariableScope $uniqueVariableScope): array
{
/*
* In case of mutable source we create the immutable value by using createFromMutable.
* Handles all DateTime instance types using createFromInterface.
*
* \DateTimeImmutable::createFromMutable($input);
* \DateTimeImmutable::createFromInterface($input);
*/
return [
new Expr\StaticCall(new Name\FullyQualified(\DateTimeImmutable::class), 'createFromMutable', [
new Expr\StaticCall(new Name\FullyQualified(\DateTime::class), 'createFromInterface', [
new Arg($input),
]),
[],
Expand Down
15 changes: 5 additions & 10 deletions src/Transformer/DateTimeTransformerFactory.php
Original file line number Diff line number Diff line change
Expand Up @@ -34,18 +34,13 @@ protected function createTransformer(Type $sourceType, Type $targetType, MapperM

protected function createTransformerForSourceAndTarget(Type $sourceType, Type $targetType): ?TransformerInterface
{
$isSourceMutable = $this->isDateTimeMutable($sourceType);
$isTargetMutable = $this->isDateTimeMutable($targetType);

if ($isSourceMutable === $isTargetMutable) {
return new CopyTransformer();
}

if ($isSourceMutable) {
return new DateTimeMutableToImmutableTransformer();
// if target is mutable
if ($this->isDateTimeMutable($targetType)) {
return new DateTimeInterfaceToMutableTransformer();
}

return new DateTimeImmutableToMutableTransformer();
// if target is immutable or a generic DateTimeInterface
return new DateTimeInterfaceToImmutableTransformer();
}

protected function createTransformerForSource(Type $targetType, MapperMetadataInterface $mapperMetadata): ?TransformerInterface
Expand Down
54 changes: 54 additions & 0 deletions tests/AutoMapperTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,13 @@
use AutoMapper\Tests\Fixtures\ClassWithNullablePropertyInConstructor;
use AutoMapper\Tests\Fixtures\ClassWithPrivateProperty;
use AutoMapper\Tests\Fixtures\Fish;
use AutoMapper\Tests\Fixtures\HasDateTime;
use AutoMapper\Tests\Fixtures\HasDateTimeImmutable;
use AutoMapper\Tests\Fixtures\HasDateTimeImmutableWithNullValue;
use AutoMapper\Tests\Fixtures\HasDateTimeInterfaceWithImmutableInstance;
use AutoMapper\Tests\Fixtures\HasDateTimeInterfaceWithMutableInstance;
use AutoMapper\Tests\Fixtures\HasDateTimeInterfaceWithNullValue;
use AutoMapper\Tests\Fixtures\HasDateTimeWithNullValue;
use AutoMapper\Tests\Fixtures\ObjectWithDateTime;
use AutoMapper\Tests\Fixtures\Order;
use AutoMapper\Tests\Fixtures\PetOwner;
Expand Down Expand Up @@ -1161,6 +1168,53 @@ public function testDateTimeFormatCanBeConfiguredFromContext(): void
);
}

/**
* @param class-string<HasDateTime|HasDateTimeWithNullValue|HasDateTimeImmutable|HasDateTimeImmutableWithNullValue|HasDateTimeInterfaceWithImmutableInstance|HasDateTimeInterfaceWithNullValue> $from
* @param class-string<HasDateTime|HasDateTimeWithNullValue|HasDateTimeImmutable|HasDateTimeImmutableWithNullValue|HasDateTimeInterfaceWithImmutableInstance|HasDateTimeInterfaceWithNullValue> $to
*
* @dataProvider dateTimeMappingProvider
*/
public function testDateTimeMapping(
string $from,
string $to,
bool $isError,
): void {
if ($isError) {
$this->expectException(\TypeError::class);
}

$fromObject = $from::create();
$toObject = $this->autoMapper->map($fromObject, $to);

self::assertInstanceOf($to, $toObject);
self::assertEquals($fromObject->getString(), $toObject->getString());
}

/**
* @return iterable<array{0:HasDateTime|HasDateTimeWithNullValue|HasDateTimeImmutable|HasDateTimeImmutableWithNullValue|HasDateTimeInterfaceWithImmutableInstance|HasDateTimeInterfaceWithNullValue,1:HasDateTime|HasDateTimeWithNullValue|HasDateTimeImmutable|HasDateTimeImmutableWithNullValue|HasDateTimeInterfaceWithImmutableInstance|HasDateTimeInterfaceWithNullValue,2:bool}>
*/
public function dateTimeMappingProvider(): iterable
{
$classes = [
HasDateTime::class,
HasDateTimeWithNullValue::class,
HasDateTimeImmutable::class,
HasDateTimeImmutableWithNullValue::class,
HasDateTimeInterfaceWithImmutableInstance::class,
HasDateTimeInterfaceWithMutableInstance::class,
HasDateTimeInterfaceWithNullValue::class,
];

foreach ($classes as $from) {
foreach ($classes as $to) {
$fromIsNullable = str_contains($from, 'NullValue');
$toIsNullable = str_contains($to, 'NullValue');
$isError = $fromIsNullable && !$toIsNullable;
yield "$from to $to" => [$from, $to, $isError];
}
}
}

public function testMapToContextAttribute(): void
{
self::assertSame(
Expand Down
23 changes: 23 additions & 0 deletions tests/Fixtures/HasDateTime.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
<?php

declare(strict_types=1);

namespace AutoMapper\Tests\Fixtures;

final class HasDateTime
{
public \DateTime $dateTime;

public static function create(): self
{
$self = new self();
$self->dateTime = new \DateTime('2024-01-01 00:00:00');

return $self;
}

public function getString(): ?string
{
return $this->dateTime->format(\DateTime::ATOM);
}
}
23 changes: 23 additions & 0 deletions tests/Fixtures/HasDateTimeImmutable.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
<?php

declare(strict_types=1);

namespace AutoMapper\Tests\Fixtures;

final class HasDateTimeImmutable
{
public \DateTimeImmutable $dateTime;

public static function create(): self
{
$self = new self();
$self->dateTime = new \DateTimeImmutable('2024-01-01 00:00:00');

return $self;
}

public function getString(): ?string
{
return $this->dateTime->format(\DateTime::ATOM);
}
}
20 changes: 20 additions & 0 deletions tests/Fixtures/HasDateTimeImmutableWithNullValue.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
<?php

declare(strict_types=1);

namespace AutoMapper\Tests\Fixtures;

final class HasDateTimeImmutableWithNullValue
{
public ?\DateTimeImmutable $dateTime = null;

public static function create(): self
{
return new self();
}

public function getString(): ?string
{
return $this->dateTime?->format(\DateTime::ATOM);
}
}
23 changes: 23 additions & 0 deletions tests/Fixtures/HasDateTimeInterfaceWithImmutableInstance.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
<?php

declare(strict_types=1);

namespace AutoMapper\Tests\Fixtures;

final class HasDateTimeInterfaceWithImmutableInstance
{
public \DateTimeInterface $dateTime;

public static function create(): self
{
$self = new self();
$self->dateTime = new \DateTimeImmutable('2024-01-01 00:00:00');

return $self;
}

public function getString(): ?string
{
return $this->dateTime->format(\DateTime::ATOM);
}
}
23 changes: 23 additions & 0 deletions tests/Fixtures/HasDateTimeInterfaceWithMutableInstance.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
<?php

declare(strict_types=1);

namespace AutoMapper\Tests\Fixtures;

final class HasDateTimeInterfaceWithMutableInstance
{
public \DateTimeInterface $dateTime;

public static function create(): self
{
$self = new self();
$self->dateTime = new \DateTime('2024-01-01 00:00:00');

return $self;
}

public function getString(): ?string
{
return $this->dateTime->format(\DateTime::ATOM);
}
}
20 changes: 20 additions & 0 deletions tests/Fixtures/HasDateTimeInterfaceWithNullValue.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
<?php

declare(strict_types=1);

namespace AutoMapper\Tests\Fixtures;

final class HasDateTimeInterfaceWithNullValue
{
public ?\DateTimeInterface $dateTime = null;

public static function create(): self
{
return new self();
}

public function getString(): ?string
{
return $this->dateTime?->format(\DateTime::ATOM);
}
}
20 changes: 20 additions & 0 deletions tests/Fixtures/HasDateTimeWithNullValue.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
<?php

declare(strict_types=1);

namespace AutoMapper\Tests\Fixtures;

final class HasDateTimeWithNullValue
{
public ?\DateTime $dateTime = null;

public static function create(): self
{
return new self();
}

public function getString(): ?string
{
return $this->dateTime?->format(\DateTime::ATOM);
}
}
2 changes: 1 addition & 1 deletion tests/Fixtures/User.php
Original file line number Diff line number Diff line change
Expand Up @@ -37,7 +37,7 @@ class User
public $addresses = [];

/**
* @var \DateTime
* @var \DateTime|null
*/
public $createdAt;

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,16 +4,16 @@

namespace AutoMapper\Tests\Transformer;

use AutoMapper\Transformer\DateTimeMutableToImmutableTransformer;
use AutoMapper\Transformer\DateTimeInterfaceToImmutableTransformer;
use PHPUnit\Framework\TestCase;

class DateTimeMutableToImmutableTransformerTest extends TestCase
class DateTimeInterfaceToImmutableTransformerTest extends TestCase
{
use EvalTransformerTrait;

public function testDateTimeImmutableTransformer(): void
{
$transformer = new DateTimeMutableToImmutableTransformer();
$transformer = new DateTimeInterfaceToImmutableTransformer();

$date = new \DateTime();
$output = $this->evalTransformer($transformer, $date);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,16 +4,16 @@

namespace AutoMapper\Tests\Transformer;

use AutoMapper\Transformer\DateTimeImmutableToMutableTransformer;
use AutoMapper\Transformer\DateTimeInterfaceToMutableTransformer;
use PHPUnit\Framework\TestCase;

class DateTimeImmutableToMutableTransformerTest extends TestCase
class DateTimeInterfaceToMutableTransformerTest extends TestCase
{
use EvalTransformerTrait;

public function testDateTimeImmutableTransformer(): void
{
$transformer = new DateTimeImmutableToMutableTransformer();
$transformer = new DateTimeInterfaceToMutableTransformer();

$date = new \DateTimeImmutable();
$output = $this->evalTransformer($transformer, $date);
Expand Down
Loading

0 comments on commit d12d47d

Please sign in to comment.