From 8813be7725c606697022f56dc6353699c07f12e3 Mon Sep 17 00:00:00 2001 From: Baptiste Date: Fri, 24 May 2024 13:13:46 +0200 Subject: [PATCH] Handle DateTimeFormat in MapTo/MapFrom/Mapper attributes --- CHANGELOG.md | 3 +++ docs/_nav.md | 1 + docs/bundle/configuration.md | 4 +++- docs/mapping/date-time.md | 14 ++++++++++++++ docs/mapping/index.md | 1 + src/Attribute/MapFrom.php | 16 +++++++++------- src/Attribute/MapTo.php | 16 +++++++++------- src/Attribute/Mapper.php | 6 ++++-- src/Event/PropertyMetadataEvent.php | 1 + src/EventListener/MapFromListener.php | 1 + src/EventListener/MapToListener.php | 1 + src/EventListener/MapperListener.php | 1 + src/Extractor/MappingExtractor.php | 11 ++++++++++- src/Extractor/MappingExtractorInterface.php | 3 ++- src/Metadata/MapperMetadata.php | 1 + src/Metadata/MetadataFactory.php | 4 ++-- tests/AutoMapperMapToTest.php | 18 ++++++++++++++++++ tests/Fixtures/MapTo/DateTimeFormatMapTo.php | 20 ++++++++++++++++++++ 18 files changed, 101 insertions(+), 21 deletions(-) create mode 100644 docs/mapping/date-time.md create mode 100644 tests/Fixtures/MapTo/DateTimeFormatMapTo.php diff --git a/CHANGELOG.md b/CHANGELOG.md index 7632f2a3..fd9b5f51 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -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 diff --git a/docs/_nav.md b/docs/_nav.md index 3f52140d..f8cacbe6 100644 --- a/docs/_nav.md +++ b/docs/_nav.md @@ -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) diff --git a/docs/bundle/configuration.md b/docs/bundle/configuration.md index 2fe23d79..c818bfe7 100644 --- a/docs/bundle/configuration.md +++ b/docs/bundle/configuration.md @@ -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 diff --git a/docs/mapping/date-time.md b/docs/mapping/date-time.md new file mode 100644 index 00000000..0affaf1e --- /dev/null +++ b/docs/mapping/date-time.md @@ -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. diff --git a/docs/mapping/index.md b/docs/mapping/index.md index aa45d9b3..8cbf4dfa 100644 --- a/docs/mapping/index.md +++ b/docs/mapping/index.md @@ -11,3 +11,4 @@ a `source` and a `target`. - [Transformer](transformer.md) - [Provider](provider.md) - [Mapping inheritance](inheritance.md) +- [DateTime format](date-time.md) diff --git a/src/Attribute/MapFrom.php b/src/Attribute/MapFrom.php index 297aee12..2bc9d32a 100644 --- a/src/Attribute/MapFrom.php +++ b/src/Attribute/MapFrom.php @@ -11,13 +11,14 @@ final readonly class MapFrom { /** - * @param class-string|'array'|array|'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|'array'|array|'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, @@ -28,6 +29,7 @@ public function __construct( public ?string $if = null, public ?array $groups = null, public int $priority = 0, + public ?string $dateTimeFormat = null, ) { } } diff --git a/src/Attribute/MapTo.php b/src/Attribute/MapTo.php index 6b6e9838..acac9681 100644 --- a/src/Attribute/MapTo.php +++ b/src/Attribute/MapTo.php @@ -11,13 +11,14 @@ final readonly class MapTo { /** - * @param class-string|'array'|array|'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|'array'|array|'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, @@ -28,6 +29,7 @@ public function __construct( public ?string $if = null, public ?array $groups = null, public int $priority = 0, + public ?string $dateTimeFormat = null, ) { } } diff --git a/src/Attribute/Mapper.php b/src/Attribute/Mapper.php index 516b2c00..6899668a 100644 --- a/src/Attribute/Mapper.php +++ b/src/Attribute/Mapper.php @@ -13,8 +13,9 @@ final readonly class Mapper { /** - * @param class-string|'array'|array|'array'>|null $source the source class or classes - * @param class-string|'array'|array|'array'>|null $target the target class or classes + * @param class-string|'array'|array|'array'>|null $source The source class or classes + * @param class-string|'array'|array|'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, @@ -23,6 +24,7 @@ public function __construct( public ?ConstructorStrategy $constructorStrategy = null, public ?bool $allowReadOnlyTargetToPopulate = null, public int $priority = 0, + public ?string $dateTimeFormat = null, ) { } } diff --git a/src/Event/PropertyMetadataEvent.php b/src/Event/PropertyMetadataEvent.php index 0e11d310..d86f0f87 100644 --- a/src/Event/PropertyMetadataEvent.php +++ b/src/Event/PropertyMetadataEvent.php @@ -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, diff --git a/src/EventListener/MapFromListener.php b/src/EventListener/MapFromListener.php index 59af1ac8..0ecf9f47 100644 --- a/src/EventListener/MapFromListener.php +++ b/src/EventListener/MapFromListener.php @@ -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, diff --git a/src/EventListener/MapToListener.php b/src/EventListener/MapToListener.php index 6e98f28b..cb195e5b 100644 --- a/src/EventListener/MapToListener.php +++ b/src/EventListener/MapToListener.php @@ -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, diff --git a/src/EventListener/MapperListener.php b/src/EventListener/MapperListener.php index d1bf2216..84fed6cd 100644 --- a/src/EventListener/MapperListener.php +++ b/src/EventListener/MapperListener.php @@ -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; } } diff --git a/src/Extractor/MappingExtractor.php b/src/Extractor/MappingExtractor.php index c7d39a37..9db0e46a 100644 --- a/src/Extractor/MappingExtractor.php +++ b/src/Extractor/MappingExtractor.php @@ -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; @@ -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; } } diff --git a/src/Extractor/MappingExtractorInterface.php b/src/Extractor/MappingExtractorInterface.php index 8a682b5e..59f43158 100644 --- a/src/Extractor/MappingExtractorInterface.php +++ b/src/Extractor/MappingExtractorInterface.php @@ -4,6 +4,7 @@ namespace AutoMapper\Extractor; +use AutoMapper\Event\PropertyMetadataEvent; use AutoMapper\Metadata\SourcePropertyMetadata; use AutoMapper\Metadata\TargetPropertyMetadata; use AutoMapper\Metadata\TypesMatching; @@ -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|null diff --git a/src/Metadata/MapperMetadata.php b/src/Metadata/MapperMetadata.php index dae70e62..42381c18 100644 --- a/src/Metadata/MapperMetadata.php +++ b/src/Metadata/MapperMetadata.php @@ -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); diff --git a/src/Metadata/MetadataFactory.php b/src/Metadata/MetadataFactory.php index e8beed18..d7bffffe 100644 --- a/src/Metadata/MetadataFactory.php +++ b/src/Metadata/MetadataFactory.php @@ -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 @@ -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); diff --git a/tests/AutoMapperMapToTest.php b/tests/AutoMapperMapToTest.php index 050471cd..89ed8379 100644 --- a/tests/AutoMapperMapToTest.php +++ b/tests/AutoMapperMapToTest.php @@ -9,6 +9,7 @@ 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\PriorityMapTo; use AutoMapper\Tests\Fixtures\Transformer\CustomTransformer\FooDependency; @@ -131,4 +132,21 @@ 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']); + } } diff --git a/tests/Fixtures/MapTo/DateTimeFormatMapTo.php b/tests/Fixtures/MapTo/DateTimeFormatMapTo.php new file mode 100644 index 00000000..9f5a2dda --- /dev/null +++ b/tests/Fixtures/MapTo/DateTimeFormatMapTo.php @@ -0,0 +1,20 @@ +