Skip to content

Commit

Permalink
Handle DateTimeFormat in MapTo/MapFrom/Mapper attributes
Browse files Browse the repository at this point in the history
  • Loading branch information
Korbeil committed May 24, 2024
1 parent 132eb46 commit 06e85f0
Show file tree
Hide file tree
Showing 19 changed files with 141 additions and 21 deletions.
3 changes: 3 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,9 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0

## [Unreleased]

### Added
- [GH#153](https://github.com/jolicode/automapper/pull/153) Handle DateTime format in MapTo/MapFrom/Mapper attributes

## [9.0.2] - 2024-05-23
### Deprecated
- [GH#136](https://github.com/jolicode/automapper/pull/136) Deprecate the ability to inject AST transformer factories withing stand-alone AutoMapper
Expand Down
1 change: 1 addition & 0 deletions docs/_nav.md
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@
- [Transformer](mapping/transformer.md)
- [Provider](mapping/provider.md)
- [Mapping inheritance](mapping/inheritance.md)
- [DateTime format](mapping/date-time.md)
- [Symfony Bundle](bundle/index.md)
- [Installation](bundle/installation.md)
- [Configuration](bundle/configuration.md)
Expand Down
4 changes: 3 additions & 1 deletion docs/bundle/configuration.md
Original file line number Diff line number Diff line change
Expand Up @@ -45,7 +45,9 @@ automapper:
* When set to `always`, AutoMapper will always use the constructor, even if some mandatory properties are missing.
In this case you may need to [provide a default value for the missing properties using the context](../getting-started/context.md).

* `date_time_format` (default: `\DateTimeInterface::RFC3339`): The format to use to transform a date from/to a string;
* `date_time_format` (default: `\DateTimeInterface::RFC3339`): The format to use to transform a date from/to a string,
can be overwritten by the attributes thanks to `dateTimeFormat` property, see [DateTime format](../mapping/date-time.md)
for more details about it;
* `check_attributes` (default: `true`): Check if the field should be mapped at runtime, this allow you to have dynamic
partial mapping, if you don't use this feature set it to false as it will improve the performance;
* `auto_register` (default: `true`): If the bundle should auto register the mappers in the container when it does not
Expand Down
14 changes: 14 additions & 0 deletions docs/mapping/date-time.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
# DateTime format

In addition to the default DateTime format you can set in AutoMapper context or in Bundle configuration, you can also
use the `#[MapTo]` and `#[MapFrom]` attributes to define DateTime format of properties that should be mapped.

```php
class Source
{
#[MapTo(target: 'array', dateTimeFormat: \DateTimeInterface::ATOM)]
public \DateTimeImmutable $dateTime;
}
```

When doing so the property will be mapped using the given format.
1 change: 1 addition & 0 deletions docs/mapping/index.md
Original file line number Diff line number Diff line change
Expand Up @@ -11,3 +11,4 @@ a `source` and a `target`.
- [Transformer](transformer.md)
- [Provider](provider.md)
- [Mapping inheritance](inheritance.md)
- [DateTime format](date-time.md)
16 changes: 9 additions & 7 deletions src/Attribute/MapFrom.php
Original file line number Diff line number Diff line change
Expand Up @@ -11,13 +11,14 @@
final readonly class MapFrom
{
/**
* @param class-string<object>|'array'|array<class-string<object>|'array'>|null $source The specific source class name or array. If null this attribute will be used for all source classes.
* @param string|null $property The source property name. If null, the target property name will be used.
* @param int|null $maxDepth The maximum depth of the mapping. If null, the default max depth will be used.
* @param string|callable(mixed $value, object $object): mixed|null $transformer A transformer id or a callable that transform the value during mapping
* @param bool|null $ignore if true, the property will be ignored during mapping
* @param string|null $if The condition to map the property, using the expression language
* @param string[]|null $groups The groups to map the property
* @param class-string<object>|'array'|array<class-string<object>|'array'>|null $source The specific source class name or array. If null this attribute will be used for all source classes.
* @param string|null $property The source property name. If null, the target property name will be used.
* @param int|null $maxDepth The maximum depth of the mapping. If null, the default max depth will be used.
* @param string|callable(mixed $value, object $object): mixed|null $transformer A transformer id or a callable that transform the value during mapping
* @param bool|null $ignore If true, the property will be ignored during mapping
* @param string|null $if The condition to map the property, using the expression language
* @param string[]|null $groups The groups to map the property
* @param string|null $dateTimeFormat The date-time format to use when transforming this property
*/
public function __construct(
public string|array|null $source = null,
Expand All @@ -28,6 +29,7 @@ public function __construct(
public ?string $if = null,
public ?array $groups = null,
public int $priority = 0,
public ?string $dateTimeFormat = null,
) {
}
}
16 changes: 9 additions & 7 deletions src/Attribute/MapTo.php
Original file line number Diff line number Diff line change
Expand Up @@ -11,13 +11,14 @@
final readonly class MapTo
{
/**
* @param class-string<object>|'array'|array<class-string<object>|'array'>|null $target The specific target class name or array. If null this attribute will be used for all target classes.
* @param string|null $property The target property name. If null, the source property name will be used.
* @param int|null $maxDepth The maximum depth of the mapping. If null, the default max depth will be used.
* @param string|callable(mixed $value, object $object): mixed|null $transformer A transformer id or a callable that transform the value during mapping
* @param bool|null $ignore if true, the property will be ignored during mapping
* @param string|null $if The condition to map the property, using the expression language
* @param string[]|null $groups The groups to map the property
* @param class-string<object>|'array'|array<class-string<object>|'array'>|null $target The specific target class name or array. If null this attribute will be used for all target classes.
* @param string|null $property The target property name. If null, the source property name will be used.
* @param int|null $maxDepth The maximum depth of the mapping. If null, the default max depth will be used.
* @param string|callable(mixed $value, object $object): mixed|null $transformer A transformer id or a callable that transform the value during mapping
* @param bool|null $ignore If true, the property will be ignored during mapping
* @param string|null $if The condition to map the property, using the expression language
* @param string[]|null $groups The groups to map the property
* @param string|null $dateTimeFormat The date-time format to use when transforming this property
*/
public function __construct(
public string|array|null $target = null,
Expand All @@ -28,6 +29,7 @@ public function __construct(
public ?string $if = null,
public ?array $groups = null,
public int $priority = 0,
public ?string $dateTimeFormat = null,
) {
}
}
6 changes: 4 additions & 2 deletions src/Attribute/Mapper.php
Original file line number Diff line number Diff line change
Expand Up @@ -13,8 +13,9 @@
final readonly class Mapper
{
/**
* @param class-string<object>|'array'|array<class-string<object>|'array'>|null $source the source class or classes
* @param class-string<object>|'array'|array<class-string<object>|'array'>|null $target the target class or classes
* @param class-string<object>|'array'|array<class-string<object>|'array'>|null $source The source class or classes
* @param class-string<object>|'array'|array<class-string<object>|'array'>|null $target The target class or classes
* @param string|null $dateTimeFormat The date-time format to use when transforming this property
*/
public function __construct(
public string|array|null $source = null,
Expand All @@ -23,6 +24,7 @@ public function __construct(
public ?ConstructorStrategy $constructorStrategy = null,
public ?bool $allowReadOnlyTargetToPopulate = null,
public int $priority = 0,
public ?string $dateTimeFormat = null,
) {
}
}
1 change: 1 addition & 0 deletions src/Event/PropertyMetadataEvent.php
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ public function __construct(
public ?TypesMatching $types = null,
public ?int $maxDepth = null,
public ?TransformerInterface $transformer = null,
public ?string $dateTimeFormat = null,
public ?bool $ignored = null,
public ?string $ignoreReason = null,
public ?string $if = null,
Expand Down
1 change: 1 addition & 0 deletions src/EventListener/MapFromListener.php
Original file line number Diff line number Diff line change
Expand Up @@ -77,6 +77,7 @@ private function addPropertyFromTarget(GenerateMapperEvent $event, MapFrom $mapF
target: $targetProperty,
maxDepth: $mapFrom->maxDepth,
transformer: $this->getTransformerFromMapAttribute($event->mapperMetadata->target, $mapFrom, false),
dateTimeFormat: $mapFrom->dateTimeFormat,
ignored: $mapFrom->ignore,
ignoreReason: $mapFrom->ignore === true ? 'Property is ignored by MapFrom Attribute on Target' : null,
if: $mapFrom->if,
Expand Down
1 change: 1 addition & 0 deletions src/EventListener/MapToListener.php
Original file line number Diff line number Diff line change
Expand Up @@ -78,6 +78,7 @@ private function addPropertyFromSource(GenerateMapperEvent $event, MapTo $mapTo,
target: $targetProperty,
maxDepth: $mapTo->maxDepth,
transformer: $this->getTransformerFromMapAttribute($event->mapperMetadata->source, $mapTo),
dateTimeFormat: $mapTo->dateTimeFormat,
ignored: $mapTo->ignore,
ignoreReason: $mapTo->ignore === true ? 'Property is ignored by MapTo Attribute on Source' : null,
if: $mapTo->if,
Expand Down
1 change: 1 addition & 0 deletions src/EventListener/MapperListener.php
Original file line number Diff line number Diff line change
Expand Up @@ -72,5 +72,6 @@ public function __invoke(GenerateMapperEvent $event): void
$event->checkAttributes ??= $mapper->checkAttributes;
$event->constructorStrategy ??= $mapper->constructorStrategy;
$event->allowReadOnlyTargetToPopulate ??= $mapper->allowReadOnlyTargetToPopulate;
$event->mapperMetadata->dateTimeFormat = $mapper->dateTimeFormat;
}
}
11 changes: 10 additions & 1 deletion src/Extractor/MappingExtractor.php
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
namespace AutoMapper\Extractor;

use AutoMapper\Configuration;
use AutoMapper\Event\PropertyMetadataEvent;
use Symfony\Component\PropertyInfo\PropertyInfoExtractorInterface;
use Symfony\Component\PropertyInfo\PropertyReadInfo;
use Symfony\Component\PropertyInfo\PropertyReadInfoExtractorInterface;
Expand Down Expand Up @@ -119,8 +120,16 @@ public function getGroups(string $class, string $property): ?array
return null;
}

public function getDateTimeFormat(string $class, string $property): string
public function getDateTimeFormat(PropertyMetadataEvent $propertyMetadataEvent): string
{
if (null !== $propertyMetadataEvent->dateTimeFormat) {
return $propertyMetadataEvent->dateTimeFormat;
}

if (null !== $propertyMetadataEvent->mapperMetadata->dateTimeFormat) {
return $propertyMetadataEvent->mapperMetadata->dateTimeFormat;
}

return $this->configuration->dateTimeFormat;
}
}
3 changes: 2 additions & 1 deletion src/Extractor/MappingExtractorInterface.php
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@

namespace AutoMapper\Extractor;

use AutoMapper\Event\PropertyMetadataEvent;
use AutoMapper\Metadata\SourcePropertyMetadata;
use AutoMapper\Metadata\TargetPropertyMetadata;
use AutoMapper\Metadata\TypesMatching;
Expand All @@ -26,7 +27,7 @@ public function getProperties(string $class): iterable;

public function getTypes(string $source, SourcePropertyMetadata $sourceProperty, string $target, TargetPropertyMetadata $targetProperty): TypesMatching;

public function getDateTimeFormat(string $class, string $property): string;
public function getDateTimeFormat(PropertyMetadataEvent $propertyMetadataEvent): string;

/**
* @return list<string>|null
Expand Down
1 change: 1 addition & 0 deletions src/Metadata/MapperMetadata.php
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@ public function __construct(
public string $target,
public bool $registered,
private string $classPrefix = 'Mapper_',
public ?string $dateTimeFormat = null,
) {
if (class_exists($this->source) && $this->source !== \stdClass::class) {
$reflectionSource = new \ReflectionClass($this->source);
Expand Down
4 changes: 2 additions & 2 deletions src/Metadata/MetadataFactory.php
Original file line number Diff line number Diff line change
Expand Up @@ -232,7 +232,7 @@ private function createGeneratorMetadata(MapperMetadata $mapperMetadata): Genera
}

if ($propertyMappedEvent->source->dateTimeFormat === null) {
$propertyMappedEvent->source->dateTimeFormat = $extractor->getDateTimeFormat($mapperMetadata->source, $propertyMappedEvent->source->property);
$propertyMappedEvent->source->dateTimeFormat = $extractor->getDateTimeFormat($propertyMappedEvent);
}

// Create the target property metadata
Expand Down Expand Up @@ -261,7 +261,7 @@ private function createGeneratorMetadata(MapperMetadata $mapperMetadata): Genera
}

if ($propertyMappedEvent->target->dateTimeFormat === null) {
$propertyMappedEvent->target->dateTimeFormat = $extractor->getDateTimeFormat($mapperMetadata->target, $propertyMappedEvent->target->property);
$propertyMappedEvent->target->dateTimeFormat = $extractor->getDateTimeFormat($propertyMappedEvent);
}

$sourcePropertyMetadata = SourcePropertyMetadata::fromEvent($propertyMappedEvent->source);
Expand Down
38 changes: 38 additions & 0 deletions tests/AutoMapperMapToTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,9 @@
use AutoMapper\Symfony\ExpressionLanguageProvider;
use AutoMapper\Tests\Fixtures\MapTo\BadMapToTransformer;
use AutoMapper\Tests\Fixtures\MapTo\Bar;
use AutoMapper\Tests\Fixtures\MapTo\DateTimeFormatMapTo;
use AutoMapper\Tests\Fixtures\MapTo\FooMapTo;
use AutoMapper\Tests\Fixtures\MapTo\MapperDateTimeFormatMapTo;
use AutoMapper\Tests\Fixtures\MapTo\PriorityMapTo;
use AutoMapper\Tests\Fixtures\Transformer\CustomTransformer\FooDependency;
use AutoMapper\Tests\Fixtures\Transformer\CustomTransformer\TransformerWithDependency;
Expand Down Expand Up @@ -131,4 +133,40 @@ public function testBadDefinitionOnTransformer()
$this->expectException(BadMapDefinitionException::class);
$this->autoMapper->map($foo, 'array');
}

public function testDateTimeFormat(): void
{
$normal = new \DateTime();
$immutable = new \DateTimeImmutable();

$foo = new DateTimeFormatMapTo($normal, $immutable, $normal);
$result = $this->autoMapper->map($foo, 'array');

self::assertArrayHasKey('normal', $result);
self::assertSame($normal->format(\DateTimeInterface::ATOM), $result['normal']);
self::assertArrayHasKey('immutable', $result);
self::assertSame($immutable->format(\DateTimeInterface::RFC822), $result['immutable']);
self::assertArrayHasKey('interface', $result);
self::assertSame($normal->format(\DateTimeInterface::RFC7231), $result['interface']);

$bar = new MapperDateTimeFormatMapTo($normal, $immutable, $normal);
$result = $this->autoMapper->map($bar, 'array');

self::assertArrayHasKey('normal', $result);
self::assertSame($normal->format(\DateTimeInterface::ATOM), $result['normal']);
self::assertArrayHasKey('immutable', $result);
self::assertSame($immutable->format(\DateTimeInterface::ATOM), $result['immutable']);
self::assertArrayHasKey('interface', $result);
self::assertSame($normal->format(\DateTimeInterface::RFC7231), $result['interface']);

$baz = new MapperDateTimeFormatMapTo($normal, $immutable, $normal);
$result = $this->autoMapper->map($baz, 'array', [MapperContext::DATETIME_FORMAT => \DateTimeInterface::RFC822]);

self::assertArrayHasKey('normal', $result);
self::assertSame($normal->format(\DateTimeInterface::RFC822), $result['normal']);
self::assertArrayHasKey('immutable', $result);
self::assertSame($immutable->format(\DateTimeInterface::RFC822), $result['immutable']);
self::assertArrayHasKey('interface', $result);
self::assertSame($normal->format(\DateTimeInterface::RFC822), $result['interface']);
}
}
20 changes: 20 additions & 0 deletions tests/Fixtures/MapTo/DateTimeFormatMapTo.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
<?php

declare(strict_types=1);

namespace AutoMapper\Tests\Fixtures\MapTo;

use AutoMapper\Attribute\MapTo;

class DateTimeFormatMapTo
{
public function __construct(
#[MapTo('array', dateTimeFormat: \DateTimeInterface::ATOM)]
public \DateTime $normal,
#[MapTo('array', dateTimeFormat: \DateTimeInterface::RFC822)]
public \DateTimeImmutable $immutable,
#[MapTo('array', dateTimeFormat: \DateTimeInterface::RFC7231)]
public \DateTimeInterface $interface,
) {
}
}
20 changes: 20 additions & 0 deletions tests/Fixtures/MapTo/MapperDateTimeFormatMapTo.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
<?php

declare(strict_types=1);

namespace AutoMapper\Tests\Fixtures\MapTo;

use AutoMapper\Attribute\Mapper;
use AutoMapper\Attribute\MapTo;

#[Mapper(dateTimeFormat: \DateTimeInterface::ATOM)]
class MapperDateTimeFormatMapTo
{
public function __construct(
public \DateTime $normal,
public \DateTimeImmutable $immutable,
#[MapTo('array', dateTimeFormat: \DateTimeInterface::RFC7231)]
public \DateTimeInterface $interface,
) {
}
}

0 comments on commit 06e85f0

Please sign in to comment.