diff --git a/docs/9.0/reader/record-mapping.md b/docs/9.0/reader/record-mapping.md index 6c5e7315..168d92a0 100644 --- a/docs/9.0/reader/record-mapping.md +++ b/docs/9.0/reader/record-mapping.md @@ -61,9 +61,14 @@ final readonly class Weather public function __construct( public ?float $temperature, public Place $place, - public DateTimeImmutable $date, + private DateTimeImmutable $date, ) { } + + public function setDate(string $date): void + { + $this->date = new DateTimeImmutable($date, new DateTimeZone('Africa/Abidjan')); + } } enum Place @@ -89,10 +94,12 @@ foreach ($csv->getObjects(Weather::class) { ## Defining the mapping rules By default, the denormalization engine will automatically convert public properties using their name. -In other words, if there is a public class property, which name is the same as a record key, -the record value will be assigned to that property. While the record value **MUST BE** a -`string` or `null`, the autodiscovery feature only works with public properties typed with one of -the following type: +In other words, if there is: + +- a public class property, which name is the same as a record key, the record value will be assigned to that property. +- a public class method, whose name starts with `set` and ends with the record key with the first character upper-cased, the record value will be assigned to the method first argument. + +While the record value **MUST BE** a `string` or `null`, the autodiscovery feature only works with public properties typed with one of the following type: - a scalar type (`string`, `int`, `float`, `bool`) - `null` diff --git a/src/Serializer/Denormalizer.php b/src/Serializer/Denormalizer.php index 214bcd69..2575fd62 100644 --- a/src/Serializer/Denormalizer.php +++ b/src/Serializer/Denormalizer.php @@ -216,17 +216,33 @@ private function setPropertySetters(array $propertyNames): array private function findPropertySetter(ReflectionProperty|ReflectionMethod $accessor, array $propertyNames): ?PropertySetter { $attributes = $accessor->getAttributes(Cell::class, ReflectionAttribute::IS_INSTANCEOF); + $methodNames = array_map(fn (string|int $propertyName) => 'set'.ucfirst((string) $propertyName), $propertyNames); + if ([] === $attributes) { - if (!$accessor instanceof ReflectionProperty || $accessor->isStatic() || !$accessor->isPublic()) { + if ($accessor->isStatic() || !$accessor->isPublic()) { + return null; + } + + if ($accessor instanceof ReflectionMethod && $accessor->isConstructor()) { return null; } /** @var int|false $offset */ - $offset = array_search($accessor->getName(), $propertyNames, true); + $offset = match (true) { + $accessor instanceof ReflectionMethod => array_search($accessor->getName(), $methodNames, true), + default => array_search($accessor->getName(), $propertyNames, true), + }; return match (false) { $offset => null, - default => new PropertySetter($accessor, $offset, $this->resolveTypeCasting($accessor)), + default => new PropertySetter( + $accessor, + $offset, + $this->resolveTypeCasting(match (true) { + $accessor instanceof ReflectionMethod => $this->getMethodFirstArgument($accessor), + $accessor instanceof ReflectionProperty => $accessor, + }) + ), }; } @@ -237,7 +253,7 @@ private function findPropertySetter(ReflectionProperty|ReflectionMethod $accesso /** @var Cell $cell */ $cell = $attributes[0]->newInstance(); $offset = $cell->offset ?? match (true) { - $accessor instanceof ReflectionMethod => $accessor->getParameters()[0]->getName(), + $accessor instanceof ReflectionMethod => $this->getMethodFirstArgument($accessor)->getName(), default => $accessor->getName(), }; @@ -263,6 +279,19 @@ private function findPropertySetter(ReflectionProperty|ReflectionMethod $accesso }; } + /** + * @throws MappingFailed + */ + private function getMethodFirstArgument(ReflectionMethod $reflectionMethod): ReflectionParameter + { + $arguments = $reflectionMethod->getParameters(); + + return match (true) { + [] === $arguments => throw new MappingFailed('The method '.$reflectionMethod->getName().' does not have parameters defined.'), + default => $arguments[0] + }; + } + /** * @throws MappingFailed */ diff --git a/src/Serializer/DenormalizerTest.php b/src/Serializer/DenormalizerTest.php index 575346cc..20d3d0f6 100644 --- a/src/Serializer/DenormalizerTest.php +++ b/src/Serializer/DenormalizerTest.php @@ -17,6 +17,7 @@ use DateTime; use DateTimeImmutable; use DateTimeInterface; +use DateTimeZone; use PHPUnit\Framework\TestCase; use SplFileObject; use stdClass; @@ -257,6 +258,28 @@ public function setFoobar($foobar): void Denormalizer::assign($class::class, ['foobar' => 'barbaz']); } + + public function testItWillAutoDiscoverThePublicMethod(): void + { + $class = new class () { + private DateTimeInterface $foo; + + public function setDate(string $toto): void + { + $this->foo = new DateTimeImmutable($toto, new DateTimeZone('Africa/Abidjan')); + } + + public function getDate(): DateTimeInterface + { + return $this->foo; + } + }; + + $object = Denormalizer::assign($class::class, ['date' => 'tomorrow']); + self::assertInstanceOf($class::class, $object); + self::assertEquals(new DateTimeZone('Africa/Abidjan'), $object->getDate()->getTimezone()); + + } } enum Place: string