diff --git a/docs/9.0/reader/record-mapping.md b/docs/9.0/reader/record-mapping.md index 6c5e7315..5a8946c2 100644 --- a/docs/9.0/reader/record-mapping.md +++ b/docs/9.0/reader/record-mapping.md @@ -56,14 +56,19 @@ We can define a PHP DTO using the following properties. ```php 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..8081b7ef 100644 --- a/src/Serializer/Denormalizer.php +++ b/src/Serializer/Denormalizer.php @@ -195,8 +195,9 @@ private function setClass(string $className): ReflectionClass private function setPropertySetters(array $propertyNames): array { $propertySetters = []; + $methodNames = array_map(fn (string|int $propertyName) => is_int($propertyName) ? null : 'set'.ucfirst($propertyName), $propertyNames); foreach ([...$this->properties, ...$this->class->getMethods()] as $accessor) { - $propertySetter = $this->findPropertySetter($accessor, $propertyNames); + $propertySetter = $this->findPropertySetter($accessor, $propertyNames, $methodNames); if (null !== $propertySetter) { $propertySetters[] = $propertySetter; } @@ -210,23 +211,51 @@ private function setPropertySetters(array $propertyNames): array /** * @param array $propertyNames + * @param array $methodNames * * @throws MappingFailed */ - private function findPropertySetter(ReflectionProperty|ReflectionMethod $accessor, array $propertyNames): ?PropertySetter - { + private function findPropertySetter( + ReflectionProperty|ReflectionMethod $accessor, + array $propertyNames, + array $methodNames + ): ?PropertySetter { $attributes = $accessor->getAttributes(Cell::class, ReflectionAttribute::IS_INSTANCEOF); if ([] === $attributes) { - if (!$accessor instanceof ReflectionProperty || $accessor->isStatic() || !$accessor->isPublic()) { + if ($accessor->isStatic() || !$accessor->isPublic()) { return null; } + if ($accessor instanceof ReflectionMethod) { + if ($accessor->isConstructor()) { + return null; + } + + if ([] === $accessor->getParameters()) { + return null; + } + + if (1 !== $accessor->getNumberOfRequiredParameters()) { + 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 => $accessor->getParameters()[0], + $accessor instanceof ReflectionProperty => $accessor, + }) + ), }; } @@ -237,7 +266,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 +292,20 @@ 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.'), + 2 <= $reflectionMethod->getNumberOfRequiredParameters() => throw new MappingFailed('The method '.$reflectionMethod->getName().' has too many required parameters.'), + default => $arguments[0] + }; + } + /** * @throws MappingFailed */ diff --git a/src/Serializer/DenormalizerTest.php b/src/Serializer/DenormalizerTest.php index 575346cc..e60c86c2 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,48 @@ 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()); + } + public function testItFailToAutoDiscoverThePublicMethod(): void + { + $class = new class () { + private DateTimeInterface $foo; + + public function setDate(string $toto, string $timezone): void + { + $this->foo = new DateTimeImmutable($toto, new DateTimeZone($timezone)); + } + + public function getDate(): DateTimeInterface + { + return $this->foo; + } + }; + + $this->expectException(MappingFailed::class); + $this->expectExceptionMessage('No property or method from `'.$class::class.'` can be used for deserialization.'); + + $object = Denormalizer::assign($class::class, ['date' => 'tomorrow']); + } } enum Place: string