Skip to content

Commit

Permalink
Add support for public setter method autodiscovery
Browse files Browse the repository at this point in the history
  • Loading branch information
nyamsprod committed Nov 19, 2023
1 parent 9127c5e commit 8813656
Show file tree
Hide file tree
Showing 3 changed files with 108 additions and 15 deletions.
23 changes: 15 additions & 8 deletions docs/9.0/reader/record-mapping.md
Original file line number Diff line number Diff line change
Expand Up @@ -56,14 +56,19 @@ We can define a PHP DTO using the following properties.
```php
<?php

final readonly class Weather
final class Weather
{
public function __construct(
public ?float $temperature,
public Place $place,
public DateTimeImmutable $date,
public readonly ?float $temperature,
public readonly Place $place,
private DateTimeImmutable $date,
) {
}

public function setDate(string $date): void
{
$this->date = new DateTimeImmutable($date, new DateTimeZone('Africa/Abidjan'));
}
}

enum Place
Expand All @@ -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`
Expand Down
57 changes: 50 additions & 7 deletions src/Serializer/Denormalizer.php
Original file line number Diff line number Diff line change
Expand Up @@ -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;
}
Expand All @@ -210,23 +211,51 @@ private function setPropertySetters(array $propertyNames): array

/**
* @param array<string> $propertyNames
* @param array<string|null> $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,
})
),
};
}

Expand All @@ -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(),
};

Expand All @@ -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
*/
Expand Down
43 changes: 43 additions & 0 deletions src/Serializer/DenormalizerTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@
use DateTime;
use DateTimeImmutable;
use DateTimeInterface;
use DateTimeZone;
use PHPUnit\Framework\TestCase;
use SplFileObject;
use stdClass;
Expand Down Expand Up @@ -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
Expand Down

0 comments on commit 8813656

Please sign in to comment.