Skip to content

Commit

Permalink
Improving denormalization
Browse files Browse the repository at this point in the history
  • Loading branch information
nyamsprod committed Sep 20, 2024
1 parent 02dae2d commit bbfefd8
Show file tree
Hide file tree
Showing 4 changed files with 141 additions and 30 deletions.
43 changes: 43 additions & 0 deletions src/Serializer/AfterMapping.php
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,10 @@
namespace League\Csv\Serializer;

use Attribute;
use ReflectionAttribute;
use ReflectionClass;
use ReflectionException;
use ReflectionMethod;

#[Attribute(Attribute::TARGET_CLASS)]
final class AfterMapping
Expand All @@ -25,4 +29,43 @@ public function __construct(string ...$methods)
{
$this->methods = $methods;
}

/**
*
* @return array<ReflectionMethod>
*/
public function afterMappingMethods(ReflectionClass $class): array
{
$methods = [];
foreach ($this->methods as $method) {
try {
$accessor = $class->getMethod($method);
} catch (ReflectionException $exception) {
throw new MappingFailed('The method `'.$method.'` is not defined on the `'.$class->getName().'` class.', 0, $exception);
}

if (0 !== $accessor->getNumberOfRequiredParameters()) {
throw new MappingFailed('The method `'.$class->getName().'::'.$accessor->getName().'` has too many required parameters.');
}

$methods[] = $accessor;
}

return $methods;
}

public static function from(ReflectionClass $class): ?self
{
$attributes = $class->getAttributes(AfterMapping::class, ReflectionAttribute::IS_INSTANCEOF);
$nbAttributes = count($attributes);
if (0 === $nbAttributes) {
return null;
}

if (1 < $nbAttributes) {
throw new MappingFailed('Using more than one `'.AfterMapping::class.'` attribute on a class property or method is not supported.');
}

return $attributes[0]->newInstance();
}
}
55 changes: 28 additions & 27 deletions src/Serializer/Denormalizer.php
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,7 @@ final class Denormalizer
private readonly array $propertySetters;
/** @var array<ReflectionMethod> */
private readonly array $postMapCalls;
private readonly ?MapRecord $mapRecord;

/**
* @param class-string $className
Expand All @@ -50,6 +51,7 @@ final class Denormalizer
public function __construct(string $className, array $propertyNames = [])
{
$this->class = $this->setClass($className);
$this->mapRecord = $this->getMapRecord();
$this->properties = $this->class->getProperties();
$this->propertySetters = $this->setPropertySetters($propertyNames);
$this->postMapCalls = $this->setPostMapCalls();
Expand Down Expand Up @@ -255,6 +257,7 @@ private function setPropertySetters(array $propertyNames): array
{
$propertySetters = [];
$methodNames = array_map(fn (string $propertyName) => 'set'.ucfirst($propertyName), $propertyNames);

foreach ([...$this->properties, ...$this->class->getMethods()] as $accessor) {
$attributes = $accessor->getAttributes(MapCell::class, ReflectionAttribute::IS_INSTANCEOF);
$propertySetter = match (count($attributes)) {
Expand All @@ -273,36 +276,28 @@ private function setPropertySetters(array $propertyNames): array
};
}

private function setPostMapCalls(): array
private function getMapRecord(): ?MapRecord
{
$methods = [];
$attributes = $this->class->getAttributes(AfterMapping::class, ReflectionAttribute::IS_INSTANCEOF);
$attributes = $this->class->getAttributes(MapRecord::class, ReflectionAttribute::IS_INSTANCEOF);
$nbAttributes = count($attributes);
if (0 === $nbAttributes) {
return $methods;
}

if (1 < $nbAttributes) {
throw new MappingFailed('Using more than one `'.AfterMapping::class.'` attribute on a class property or method is not supported.');
}

/** @var AfterMapping $postMap */
$postMap = $attributes[0]->newInstance();
foreach ($postMap->methods as $method) {
try {
$accessor = $this->class->getMethod($method);
} catch (ReflectionException $exception) {
throw new MappingFailed('The method `'.$method.'` is not defined on the `'.$this->class->getName().'` class.', 0, $exception);
}

if (0 !== $accessor->getNumberOfRequiredParameters()) {
throw new MappingFailed('The method `'.$this->class->getName().'::'.$accessor->getName().'` has too many required parameters.');
}
return match ($nbAttributes) {
0 => null,
1 => $attributes[0]->newInstance(),
default => throw new MappingFailed('Using more than one `'.MapRecord::class.'` attribute on a class property or method is not supported.'),
};
}

$methods[] = $accessor;
/**
* @return array<ReflectionMethod>
*/
private function setPostMapCalls(): array
{
if ($this->mapRecord instanceof MapRecord) {
return $this->mapRecord->afterMappingMethods($this->class);
}

return $methods;
return AfterMapping::from($this->class)?->afterMappingMethods($this->class) ?? [];
}

/**
Expand Down Expand Up @@ -344,7 +339,10 @@ private function autoDiscoverPropertySetter(ReflectionMethod|ReflectionProperty
default => new PropertySetter(
$accessor,
$offset,
$this->resolveTypeCasting($reflectionProperty)
$this->resolveTypeCasting(
$reflectionProperty,
$this->mapRecord?->resolveOptions() ?? [],
)
),
};
}
Expand Down Expand Up @@ -386,11 +384,14 @@ private function findPropertySetter(MapCell $cell, ReflectionMethod|ReflectionPr
$accessor instanceof ReflectionProperty => $accessor,
};


$options = $this->mapRecord?->resolveOptions($cell) ?? $cell->options;

return match (true) {
0 > $offset => throw new MappingFailed('offset integer position can only be positive or equals to 0; received `'.$offset.'`'),
[] !== $propertyNames && $offset > count($propertyNames) - 1 => throw new MappingFailed('offset integer position can not exceed property names count.'),
null === $typeCaster => new PropertySetter($accessor, $offset, $this->resolveTypeCasting($reflectionProperty, $cell->options)),
default => new PropertySetter($accessor, $offset, $this->getTypeCasting($reflectionProperty, $typeCaster, $cell->options)),
null === $typeCaster => new PropertySetter($accessor, $offset, $this->resolveTypeCasting($reflectionProperty, $options)),
default => new PropertySetter($accessor, $offset, $this->getTypeCasting($reflectionProperty, $typeCaster, $options)),
};
}

Expand Down
6 changes: 3 additions & 3 deletions src/Serializer/DenormalizerTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -672,7 +672,7 @@ enum Place: string
case Abidjan = 'Abidjan';
}

#[AfterMapping('addOne')]
#[MapRecord(afterMapping: ['addOne'])]
class UsingAfterMapping
{
public function __construct(public int $addition)
Expand All @@ -686,7 +686,7 @@ private function addOne(): void
}
}

#[AfterMapping('addOne', 'addTow')]
#[MapRecord(afterMapping: ['addOne', 'addTow'])]
class MissingMethodAfterMapping
{
public function __construct(public int $addition)
Expand All @@ -700,7 +700,7 @@ private function addOne(): void
}
}

#[AfterMapping('addOne')]
#[MapRecord(afterMapping: ['addOne'])]
class RequiresArgumentAfterMapping
{
public function __construct(public int $addition)
Expand Down
67 changes: 67 additions & 0 deletions src/Serializer/MapRecord.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,67 @@
<?php

/**
* League.Csv (https://csv.thephpleague.com)
*
* (c) Ignace Nyamagana Butera <[email protected]>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/

declare(strict_types=1);

namespace League\Csv\Serializer;

use Attribute;
use ReflectionClass;
use ReflectionException;
use ReflectionMethod;
use ValueError;

#[Attribute(Attribute::TARGET_CLASS)]
final class MapRecord
{
public function __construct(
/** @var array<string> $afterMapping */
public readonly array $afterMapping,
public readonly bool $allowEmptyStringAsNull = false,
) {
foreach ($this->afterMapping as $method) {
if (!is_string($method)) {
throw new ValueError('The method names must be strings.');
}
}
}

/**
* @return array<ReflectionMethod>
*/
public function afterMappingMethods(ReflectionClass $class): array
{
$methods = [];
foreach ($this->afterMapping as $method) {
try {
$accessor = $class->getMethod($method);
} catch (ReflectionException $exception) {
throw new MappingFailed('The method `'.$method.'` is not defined on the `'.$class->getName().'` class.', 0, $exception);
}

if (0 !== $accessor->getNumberOfRequiredParameters()) {
throw new MappingFailed('The method `'.$class->getName().'::'.$accessor->getName().'` has too many required parameters.');
}

$methods[] = $accessor;
}

return $methods;
}

public function resolveOptions(?MapCell $mapCell = null): array
{
return [
...['allowEmptyStringAsNull' => $this->allowEmptyStringAsNull],
...$mapCell?->options ?? [],
];
}
}

0 comments on commit bbfefd8

Please sign in to comment.