diff --git a/CHANGELOG.md b/CHANGELOG.md index b6a10931..e3b18c4c 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,6 +1,6 @@ # CHANGELOG -## 1.10.1 +## 1.11.0 * refactor: spin off `resolveTargetClass()` to separate class * perf: proxy warming @@ -11,6 +11,7 @@ * perf: `ObjectToObjectTransformer` optimization * perf: move `TypeResolver` outside of the hot path * perf(`ObjectCache`): use sentinel value instead of exception +* feat: extra target values ## 1.10.0 diff --git a/src/Context/ExtraTargetValues.php b/src/Context/ExtraTargetValues.php new file mode 100644 index 00000000..73fece01 --- /dev/null +++ b/src/Context/ExtraTargetValues.php @@ -0,0 +1,44 @@ + + * + * For the full copyright and license information, please view the LICENSE file + * that was distributed with this source code. + */ + +namespace Rekalogika\Mapper\Context; + +/** + * Adds additional values to the target object + */ +final readonly class ExtraTargetValues +{ + /** + * @param array> $arguments + */ + public function __construct( + private array $arguments = [], + ) {} + + /** + * @param list $classes The class and its parent classes and interfaces. + * @return array + */ + public function getArgumentsForClass(array $classes): array + { + $arguments = []; + + foreach ($classes as $class) { + if (isset($this->arguments[$class])) { + $arguments = array_merge($arguments, $this->arguments[$class]); + } + } + + return $arguments; + } +} diff --git a/src/Transformer/Exception/ExtraTargetPropertyNotFoundException.php b/src/Transformer/Exception/ExtraTargetPropertyNotFoundException.php new file mode 100644 index 00000000..fcdc44fd --- /dev/null +++ b/src/Transformer/Exception/ExtraTargetPropertyNotFoundException.php @@ -0,0 +1,40 @@ + + * + * For the full copyright and license information, please view the LICENSE file + * that was distributed with this source code. + */ + +namespace Rekalogika\Mapper\Transformer\Exception; + +use Rekalogika\Mapper\Context\Context; +use Rekalogika\Mapper\Exception\LogicException; + +/** + * @internal + */ +class ExtraTargetPropertyNotFoundException extends LogicException +{ + /** + * @param class-string $class + */ + public function __construct( + string $class, + string $property, + Context $context, + ) { + $message = \sprintf( + 'Mapper is called with "ExtraTargetValues", but cannot find the target property "%s" in class "%s"', + $property, + $class, + ); + + parent::__construct($message, context: $context); + } +} diff --git a/src/Transformer/Implementation/ObjectToObjectTransformer.php b/src/Transformer/Implementation/ObjectToObjectTransformer.php index 5c1b6723..2dc8917f 100644 --- a/src/Transformer/Implementation/ObjectToObjectTransformer.php +++ b/src/Transformer/Implementation/ObjectToObjectTransformer.php @@ -18,6 +18,7 @@ use Rekalogika\Mapper\CacheWarmer\WarmableObjectToObjectMetadataFactoryInterface; use Rekalogika\Mapper\CacheWarmer\WarmableTransformerInterface; use Rekalogika\Mapper\Context\Context; +use Rekalogika\Mapper\Context\ExtraTargetValues; use Rekalogika\Mapper\Context\MapperOptions; use Rekalogika\Mapper\Exception\InvalidArgumentException; use Rekalogika\Mapper\ObjectCache\ObjectCache; @@ -26,6 +27,7 @@ use Rekalogika\Mapper\ServiceMethod\ServiceMethodSpecification; use Rekalogika\Mapper\SubMapper\SubMapperFactoryInterface; use Rekalogika\Mapper\Transformer\Exception\ClassNotInstantiableException; +use Rekalogika\Mapper\Transformer\Exception\ExtraTargetPropertyNotFoundException; use Rekalogika\Mapper\Transformer\Exception\InstantiationFailureException; use Rekalogika\Mapper\Transformer\Exception\NotAClassException; use Rekalogika\Mapper\Transformer\Exception\UninitializedSourcePropertyException; @@ -123,6 +125,13 @@ public function transform( $target = null; } + // get extra target values + + $extraTargetValues = $this->getExtraTargetValues( + objectToObjectMetadata: $objectToObjectMetadata, + context: $context, + ); + // initialize target if target is null if (null === $target) { @@ -133,12 +142,14 @@ public function transform( $target = $this->instantiateTargetProxy( source: $source, objectToObjectMetadata: $objectToObjectMetadata, + extraTargetValues: $extraTargetValues, context: $context, ); } else { $target = $this->instantiateRealTarget( source: $source, objectToObjectMetadata: $objectToObjectMetadata, + extraTargetValues: $extraTargetValues, context: $context, ); } @@ -180,6 +191,7 @@ public function transform( source: $source, target: $target, propertyMappings: $objectToObjectMetadata->getPropertyMappings(), + extraTargetValues: $extraTargetValues, context: $context, ); } @@ -187,9 +199,39 @@ public function transform( return $target; } + /** + * @return array + */ + private function getExtraTargetValues( + ObjectToObjectMetadata $objectToObjectMetadata, + Context $context, + ): array { + $extraTargetValues = $context(ExtraTargetValues::class) + ?->getArgumentsForClass($objectToObjectMetadata->getAllTargetClasses()) + ?? []; + + $allPropertyMappings = $objectToObjectMetadata->getPropertyMappings(); + + foreach (array_keys($extraTargetValues) as $property) { + if (!isset($allPropertyMappings[$property])) { + throw new ExtraTargetPropertyNotFoundException( + class: $objectToObjectMetadata->getTargetClass(), + property: $property, + context: $context, + ); + } + } + + return $extraTargetValues; + } + + /** + * @param array $extraTargetValues + */ private function instantiateRealTarget( object $source, ObjectToObjectMetadata $objectToObjectMetadata, + array $extraTargetValues, Context $context, ): object { $targetClass = $objectToObjectMetadata->getTargetClass(); @@ -203,6 +245,7 @@ private function instantiateRealTarget( $constructorArguments = $this->generateConstructorArguments( source: $source, objectToObjectMetadata: $objectToObjectMetadata, + extraTargetValues: $extraTargetValues, context: $context, ); @@ -223,9 +266,13 @@ private function instantiateRealTarget( } } + /** + * @param array $extraTargetValues + */ private function instantiateTargetProxy( object $source, ObjectToObjectMetadata $objectToObjectMetadata, + array $extraTargetValues, Context $context, ): object { $targetClass = $objectToObjectMetadata->getTargetClass(); @@ -243,6 +290,7 @@ private function instantiateTargetProxy( $source, $objectToObjectMetadata, $context, + $extraTargetValues, ): void { // if the constructor is lazy, run it here @@ -251,6 +299,7 @@ private function instantiateTargetProxy( source: $source, target: $target, objectToObjectMetadata: $objectToObjectMetadata, + extraTargetValues: $extraTargetValues, context: $context, ); } @@ -261,6 +310,7 @@ private function instantiateTargetProxy( source: $source, target: $target, propertyMappings: $objectToObjectMetadata->getLazyPropertyMappings(), + extraTargetValues: $extraTargetValues, context: $context, ); }; @@ -280,6 +330,7 @@ private function instantiateTargetProxy( source: $source, target: $target, objectToObjectMetadata: $objectToObjectMetadata, + extraTargetValues: $extraTargetValues, context: $context, ); } @@ -290,16 +341,21 @@ private function instantiateTargetProxy( source: $source, target: $target, propertyMappings: $objectToObjectMetadata->getEagerPropertyMappings(), + extraTargetValues: $extraTargetValues, context: $context, ); return $target; } + /** + * @param array $extraTargetValues + */ private function runConstructorManually( object $source, object $target, ObjectToObjectMetadata $objectToObjectMetadata, + array $extraTargetValues, Context $context, ): object { if (!method_exists($target, '__construct')) { @@ -309,6 +365,7 @@ private function runConstructorManually( $constructorArguments = $this->generateConstructorArguments( source: $source, objectToObjectMetadata: $objectToObjectMetadata, + extraTargetValues: $extraTargetValues, context: $context, ); @@ -334,16 +391,22 @@ private function runConstructorManually( return $target; } + /** + * @param array $extraTargetValues + */ private function generateConstructorArguments( object $source, ObjectToObjectMetadata $objectToObjectMetadata, + array $extraTargetValues, Context $context, ): ConstructorArguments { - $propertyMappings = $objectToObjectMetadata->getConstructorPropertyMappings(); + $constructorPropertyMappings = $objectToObjectMetadata->getConstructorPropertyMappings(); $constructorArguments = new ConstructorArguments(); - foreach ($propertyMappings as $propertyMapping) { + // add arguments from property mappings + + foreach ($constructorPropertyMappings as $propertyMapping) { try { /** @var mixed $targetPropertyValue */ [$targetPropertyValue,] = $this->transformValue( @@ -379,16 +442,30 @@ private function generateConstructorArguments( } } + // add arguments from extra target values + + /** @var mixed $value */ + foreach ($extraTargetValues as $property => $value) { + // skip if there is no constructor property mapping for this + if (!isset($constructorPropertyMappings[$property])) { + continue; + } + + $constructorArguments->addArgument($property, $value); + } + return $constructorArguments; } /** - * @param array $propertyMappings + * @param array $propertyMappings + * @param array $extraTargetValues */ private function readSourceAndWriteTarget( object $source, object $target, array $propertyMappings, + array $extraTargetValues, Context $context, ): object { foreach ($propertyMappings as $propertyMapping) { @@ -400,6 +477,25 @@ private function readSourceAndWriteTarget( ); } + // process extra target values + + /** @var mixed $value */ + foreach ($extraTargetValues as $property => $value) { + if (!isset($propertyMappings[$property])) { + continue; + } + + $propertyMapping = $propertyMappings[$property]; + + $target = $this->readerWriter->writeTargetProperty( + target: $target, + propertyMapping: $propertyMapping, + value: $value, + context: $context, + silentOnError: true, + ); + } + return $target; } @@ -435,7 +531,10 @@ private function readSourcePropertyAndWriteTargetProperty( // write - if ($isChanged) { + if ( + $isChanged + || $propertyMapping->getTargetSetterWriteMode() === WriteMode::DynamicProperty + ) { if ($targetPropertyValue instanceof AdderRemoverProxy) { $target = $targetPropertyValue->getHostObject(); } @@ -445,6 +544,7 @@ private function readSourcePropertyAndWriteTargetProperty( propertyMapping: $propertyMapping, value: $targetPropertyValue, context: $context, + silentOnError: false, ); } @@ -611,8 +711,7 @@ private function transformValue( return [ $targetPropertyValue, - $targetPropertyValue !== $originalTargetPropertyValue - || $propertyMapping->getTargetSetterWriteMode() === WriteMode::DynamicProperty, + $targetPropertyValue !== $originalTargetPropertyValue, ]; } diff --git a/src/Transformer/ObjectToObjectMetadata/Implementation/ObjectToObjectMetadataFactory.php b/src/Transformer/ObjectToObjectMetadata/Implementation/ObjectToObjectMetadataFactory.php index 871f84ed..5537998b 100644 --- a/src/Transformer/ObjectToObjectMetadata/Implementation/ObjectToObjectMetadataFactory.php +++ b/src/Transformer/ObjectToObjectMetadata/Implementation/ObjectToObjectMetadataFactory.php @@ -162,6 +162,7 @@ class: $targetClass, sourceClass: $sourceClass, targetClass: $targetClass, providedTargetClass: $providedTargetClass, + allTargetClasses: ClassUtil::getAllClassesFromObject($targetClass), sourceAllowsDynamicProperties: $sourceClassMetadata->hasReadableDynamicProperties(), targetAllowsDynamicProperties: $targetClassMetadata->hasWritableDynamicProperties(), sourceProperties: $effectivePropertiesToMap, @@ -211,7 +212,7 @@ class: $targetClass, /** * @param class-string $targetClass * @param list $eagerProperties - * @param list $constructorPropertyMappings + * @param array $constructorPropertyMappings * @return array{array,bool} */ private function getProxyParameters( diff --git a/src/Transformer/ObjectToObjectMetadata/ObjectToObjectMetadata.php b/src/Transformer/ObjectToObjectMetadata/ObjectToObjectMetadata.php index a668fea3..84f4a252 100644 --- a/src/Transformer/ObjectToObjectMetadata/ObjectToObjectMetadata.php +++ b/src/Transformer/ObjectToObjectMetadata/ObjectToObjectMetadata.php @@ -24,27 +24,27 @@ final readonly class ObjectToObjectMetadata { /** - * @var list + * @var array */ private array $allPropertyMappings; /** - * @var list + * @var array */ private array $propertyMappings; /** - * @var list + * @var array */ private array $constructorPropertyMappings; /** - * @var list + * @var array */ private array $lazyPropertyMappings; /** - * @var list + * @var array */ private array $eagerPropertyMappings; @@ -52,6 +52,7 @@ * @param class-string $sourceClass * @param class-string $targetClass Effective target class after resolving inheritance map * @param class-string $providedTargetClass + * @param list $allTargetClasses * @param list $allPropertyMappings * @param array $targetProxySkippedProperties * @param list $sourceProperties List of the source properties. Used by `ObjectToObjectTransformer` to determine if a property is a dynamic property. A property not listed here is considered dynamic. @@ -60,6 +61,7 @@ public function __construct( private string $sourceClass, private string $targetClass, private string $providedTargetClass, + private array $allTargetClasses, private bool $sourceAllowsDynamicProperties, private bool $targetAllowsDynamicProperties, private array $sourceProperties, @@ -80,10 +82,13 @@ public function __construct( $lazyPropertyMappings = []; $eagerPropertyMappings = []; $propertyPropertyMappings = []; + $processedAllPropertyMappings = []; foreach ($allPropertyMappings as $propertyMapping) { + $processedAllPropertyMappings[$propertyMapping->getTargetProperty()] = $propertyMapping; + if ($propertyMapping->getTargetConstructorWriteMode() === WriteMode::Constructor) { - $constructorPropertyMappings[] = $propertyMapping; + $constructorPropertyMappings[$propertyMapping->getTargetProperty()] = $propertyMapping; } if ( @@ -94,12 +99,12 @@ public function __construct( continue; } - $propertyPropertyMappings[] = $propertyMapping; + $propertyPropertyMappings[$propertyMapping->getTargetProperty()] = $propertyMapping; if ($propertyMapping->isSourceLazy()) { - $lazyPropertyMappings[] = $propertyMapping; + $lazyPropertyMappings[$propertyMapping->getTargetProperty()] = $propertyMapping; } else { - $eagerPropertyMappings[] = $propertyMapping; + $eagerPropertyMappings[$propertyMapping->getTargetProperty()] = $propertyMapping; } } @@ -107,7 +112,7 @@ public function __construct( $this->lazyPropertyMappings = $lazyPropertyMappings; $this->eagerPropertyMappings = $eagerPropertyMappings; $this->propertyMappings = $propertyPropertyMappings; - $this->allPropertyMappings = $allPropertyMappings; + $this->allPropertyMappings = $processedAllPropertyMappings; } /** @@ -121,10 +126,11 @@ public function withTargetProxy( sourceClass: $this->sourceClass, targetClass: $this->targetClass, providedTargetClass: $this->providedTargetClass, + allTargetClasses: $this->allTargetClasses, sourceAllowsDynamicProperties: $this->sourceAllowsDynamicProperties, targetAllowsDynamicProperties: $this->targetAllowsDynamicProperties, sourceProperties: $this->sourceProperties, - allPropertyMappings: $this->allPropertyMappings, + allPropertyMappings: array_values($this->allPropertyMappings), instantiable: $this->instantiable, cloneable: $this->cloneable, targetUnalterable: $this->targetUnalterable, @@ -146,10 +152,11 @@ public function withReasonCannotUseProxy( sourceClass: $this->sourceClass, targetClass: $this->targetClass, providedTargetClass: $this->providedTargetClass, + allTargetClasses: $this->allTargetClasses, sourceAllowsDynamicProperties: $this->sourceAllowsDynamicProperties, targetAllowsDynamicProperties: $this->targetAllowsDynamicProperties, sourceProperties: $this->sourceProperties, - allPropertyMappings: $this->allPropertyMappings, + allPropertyMappings: array_values($this->allPropertyMappings), instantiable: $this->instantiable, cloneable: $this->cloneable, targetUnalterable: $this->targetUnalterable, @@ -188,6 +195,14 @@ public function getProvidedTargetClass(): string return $this->providedTargetClass; } + /** + * @return list + */ + public function getAllTargetClasses(): array + { + return $this->allTargetClasses; + } + public function isInstantiable(): bool { return $this->instantiable; @@ -204,7 +219,7 @@ public function isTargetUnalterable(): bool } /** - * @return list + * @return array */ public function getPropertyMappings(): array { @@ -212,7 +227,7 @@ public function getPropertyMappings(): array } /** - * @return list + * @return array */ public function getLazyPropertyMappings(): array { @@ -220,7 +235,7 @@ public function getLazyPropertyMappings(): array } /** - * @return list + * @return array */ public function getEagerPropertyMappings(): array { @@ -228,7 +243,7 @@ public function getEagerPropertyMappings(): array } /** - * @return list + * @return array */ public function getConstructorPropertyMappings(): array { @@ -236,7 +251,7 @@ public function getConstructorPropertyMappings(): array } /** - * @return list + * @return array */ public function getAllPropertyMappings(): array { diff --git a/src/Transformer/Util/ReaderWriter.php b/src/Transformer/Util/ReaderWriter.php index f1018582..16bdb78e 100644 --- a/src/Transformer/Util/ReaderWriter.php +++ b/src/Transformer/Util/ReaderWriter.php @@ -200,14 +200,20 @@ public function writeTargetProperty( PropertyMapping $propertyMapping, mixed $value, Context $context, + bool $silentOnError, ): object { $accessorName = $propertyMapping->getTargetSetterWriteName(); $writeMode = $propertyMapping->getTargetSetterWriteMode(); $visibility = $propertyMapping->getTargetSetterWriteVisibility(); if ( - $visibility !== Visibility::Public || $writeMode === WriteMode::None + $visibility !== Visibility::Public + || $writeMode === WriteMode::None ) { + if ($silentOnError) { + return $target; + } + throw new NewInstanceReturnedButCannotBeSetOnTargetException( $target, $propertyMapping->getTargetProperty(), diff --git a/src/Util/ClassUtil.php b/src/Util/ClassUtil.php index 3df7380b..367a42cb 100644 --- a/src/Util/ClassUtil.php +++ b/src/Util/ClassUtil.php @@ -172,7 +172,7 @@ public static function getSkippedProperties( /** * @param object|class-string $objectOrClass - * @return array + * @return list */ public static function getAllClassesFromObject( object|string $objectOrClass, diff --git a/tests/config/rekalogika-mapper/generated-mappings.php b/tests/config/rekalogika-mapper/generated-mappings.php index b208898f..ab866e22 100644 --- a/tests/config/rekalogika-mapper/generated-mappings.php +++ b/tests/config/rekalogika-mapper/generated-mappings.php @@ -387,6 +387,18 @@ target: \Rekalogika\Mapper\Tests\Fixtures\EnumAndStringableDto\ObjectWithStringPropertyDto::class ); + $mappingCollection->addObjectMapping( + // tests/src/IntegrationTest/ExtraTargetValuesTest.php on lines 28, 68 + source: \Rekalogika\Mapper\Tests\Fixtures\ExtraTargetValues\SomeObject::class, + target: \Rekalogika\Mapper\Tests\Fixtures\ExtraTargetValues\SomeObjectWithConstructorDto::class + ); + + $mappingCollection->addObjectMapping( + // tests/src/IntegrationTest/ExtraTargetValuesTest.php on line 47 + source: \Rekalogika\Mapper\Tests\Fixtures\ExtraTargetValues\SomeObject::class, + target: \Rekalogika\Mapper\Tests\Fixtures\ExtraTargetValues\SomeObjectWithPropertyDto::class + ); + $mappingCollection->addObjectMapping( // tests/src/IntegrationTest/InheritanceReversedTest.php on line 29 source: \Rekalogika\Mapper\Tests\Fixtures\InheritanceDto\ConcreteClassADto::class, diff --git a/tests/src/Fixtures/ExtraTargetValues/SomeObject.php b/tests/src/Fixtures/ExtraTargetValues/SomeObject.php new file mode 100644 index 00000000..dbe2eae7 --- /dev/null +++ b/tests/src/Fixtures/ExtraTargetValues/SomeObject.php @@ -0,0 +1,19 @@ + + * + * For the full copyright and license information, please view the LICENSE file + * that was distributed with this source code. + */ + +namespace Rekalogika\Mapper\Tests\Fixtures\ExtraTargetValues; + +class SomeObject +{ + public string $property = 'someProperty'; +} diff --git a/tests/src/Fixtures/ExtraTargetValues/SomeObjectWithConstructorDto.php b/tests/src/Fixtures/ExtraTargetValues/SomeObjectWithConstructorDto.php new file mode 100644 index 00000000..5f23eea3 --- /dev/null +++ b/tests/src/Fixtures/ExtraTargetValues/SomeObjectWithConstructorDto.php @@ -0,0 +1,22 @@ + + * + * For the full copyright and license information, please view the LICENSE file + * that was distributed with this source code. + */ + +namespace Rekalogika\Mapper\Tests\Fixtures\ExtraTargetValues; + +final readonly class SomeObjectWithConstructorDto +{ + public function __construct( + public ?\DateTimeInterface $date, + public ?string $property, + ) {} +} diff --git a/tests/src/Fixtures/ExtraTargetValues/SomeObjectWithPropertyDto.php b/tests/src/Fixtures/ExtraTargetValues/SomeObjectWithPropertyDto.php new file mode 100644 index 00000000..9669f347 --- /dev/null +++ b/tests/src/Fixtures/ExtraTargetValues/SomeObjectWithPropertyDto.php @@ -0,0 +1,20 @@ + + * + * For the full copyright and license information, please view the LICENSE file + * that was distributed with this source code. + */ + +namespace Rekalogika\Mapper\Tests\Fixtures\ExtraTargetValues; + +final class SomeObjectWithPropertyDto +{ + public ?string $property = null; + public ?\DateTimeInterface $date = null; +} diff --git a/tests/src/IntegrationTest/ExtraTargetValuesTest.php b/tests/src/IntegrationTest/ExtraTargetValuesTest.php new file mode 100644 index 00000000..70b83539 --- /dev/null +++ b/tests/src/IntegrationTest/ExtraTargetValuesTest.php @@ -0,0 +1,80 @@ + + * + * For the full copyright and license information, please view the LICENSE file + * that was distributed with this source code. + */ + +namespace Rekalogika\Mapper\Tests\IntegrationTest; + +use Rekalogika\Mapper\Context\Context; +use Rekalogika\Mapper\Context\ExtraTargetValues; +use Rekalogika\Mapper\Tests\Common\FrameworkTestCase; +use Rekalogika\Mapper\Tests\Fixtures\ExtraTargetValues\SomeObject; +use Rekalogika\Mapper\Tests\Fixtures\ExtraTargetValues\SomeObjectWithConstructorDto; +use Rekalogika\Mapper\Tests\Fixtures\ExtraTargetValues\SomeObjectWithPropertyDto; +use Rekalogika\Mapper\Transformer\Exception\ExtraTargetPropertyNotFoundException; + +class ExtraTargetValuesTest extends FrameworkTestCase +{ + public function testExtraTargetValuesOnConstructor(): void + { + $target = $this->mapper->map( + source: new SomeObject(), + target: SomeObjectWithConstructorDto::class, + context: Context::create( + new ExtraTargetValues([ + SomeObjectWithConstructorDto::class => [ + 'date' => new \DateTimeImmutable('2021-01-01'), + ], + ]), + ), + ); + + $this->assertSame('someProperty', $target->property); + $this->assertInstanceOf(\DateTimeImmutable::class, $target->date); + $this->assertSame('2021-01-01', $target->date->format('Y-m-d')); + } + + public function testExtraTargetValuesOnProperty(): void + { + $target = $this->mapper->map( + source: new SomeObject(), + target: SomeObjectWithPropertyDto::class, + context: Context::create( + new ExtraTargetValues([ + SomeObjectWithPropertyDto::class => [ + 'date' => new \DateTimeImmutable('2021-01-01'), + ], + ]), + ), + ); + + $this->assertSame('someProperty', $target->property); + $this->assertInstanceOf(\DateTimeImmutable::class, $target->date); + $this->assertSame('2021-01-01', $target->date->format('Y-m-d')); + } + + public function testInvalidExtraTargetValues(): void + { + $this->expectException(ExtraTargetPropertyNotFoundException::class); + + $this->mapper->map( + source: new SomeObject(), + target: SomeObjectWithConstructorDto::class, + context: Context::create( + new ExtraTargetValues([ + SomeObjectWithConstructorDto::class => [ + 'foo' => new \DateTimeImmutable('2021-01-01'), + ], + ]), + ), + ); + } +}