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 776acaa
Show file tree
Hide file tree
Showing 3 changed files with 68 additions and 9 deletions.
17 changes: 12 additions & 5 deletions docs/9.0/reader/record-mapping.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
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
37 changes: 33 additions & 4 deletions src/Serializer/Denormalizer.php
Original file line number Diff line number Diff line change
Expand Up @@ -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,
})
),
};
}

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

Expand All @@ -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
*/
Expand Down
23 changes: 23 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,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
Expand Down

0 comments on commit 776acaa

Please sign in to comment.