From 423010674da1247a70367b64259dbcba88b303f2 Mon Sep 17 00:00:00 2001 From: ignace nyamagana butera Date: Wed, 15 Nov 2023 22:38:25 +0100 Subject: [PATCH] Rename Serializer class to Denormalizer --- CHANGELOG.md | 2 +- docs/9.0/reader/record-mapping.md | 127 +++++++++++------- src/{Serializer.php => Denormalizer.php} | 126 ++++++++--------- ...erializerTest.php => DenormalizerTest.php} | 54 ++++---- src/Reader.php | 4 +- src/ResultSet.php | 4 +- src/Serializer/ClosureCasting.php | 13 +- src/Serializer/Type.php | 4 +- 8 files changed, 174 insertions(+), 160 deletions(-) rename src/{Serializer.php => Denormalizer.php} (74%) rename src/{SerializerTest.php => DenormalizerTest.php} (81%) diff --git a/CHANGELOG.md b/CHANGELOG.md index 8a447599..e03daca3 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -16,7 +16,7 @@ All Notable changes to `Csv` will be documented in this file - `ResultSet::fromRecords` - `Stream::setMaxLineLen` - `Stream::getMaxLineLen` -- `League\Csv\Serializer` to allow casting records to objects [#508](https://github.com/thephpleague/csv/issues/508) +- `League\Csv\Denormalizer` to allow casting records to objects [#508](https://github.com/thephpleague/csv/issues/508) ### Deprecated diff --git a/docs/9.0/reader/record-mapping.md b/docs/9.0/reader/record-mapping.md index 2e8dcb83..73cef8d0 100644 --- a/docs/9.0/reader/record-mapping.md +++ b/docs/9.0/reader/record-mapping.md @@ -1,6 +1,6 @@ --- layout: default -title: Deserializing a Tabular Data record into an object +title: Denormalize a Tabular Data record into an object --- # Record to object conversion @@ -9,16 +9,16 @@ title: Deserializing a Tabular Data record into an object ## Assign an array to an object -To work with objects instead of arrays the `Serializer` class is introduced to expose a -text based deserialization mechanism for tabular data. +To work with objects instead of arrays the `Denormalizer` class is introduced to expose a +text based denormalization mechanism for tabular data. The class exposes four (4) methods to ease `array` to `object` conversion: -- `Serializer::deserializeAll` and `Serializer::assignAll` which convert a collection of records into a collection of instances of a specified class. -- `Serializer::deserialize` and `Serializer::assign` which convert a single record into a new instance of the specified class. +- `Denormalizer::denormalizeAll` and `Denormalizer::assignAll` which convert a collection of records into a collection of instances of a specified class. +- `Denormalizer::denormalize` and `Denormalizer::assign` which convert a single record into a new instance of the specified class. ```php -use League\Csv\Serializer; +use League\Csv\Denormalizer; $record = [ 'date' => '2023-10-30', @@ -28,19 +28,19 @@ $record = [ //a complete collection of records as shown below $collection = [$record]; -//we first instantiate the serializer -$serializer = new Serializer(Weather::class, ['date', 'temperature', 'place']); +//we first instantiate the denormalizer +$denormalizer = new Denormalizer(Weather::class, ['date', 'temperature', 'place']); -$weather = $serializer->deserialize($record); //we convert 1 record into 1 instance -foreach ($serializer->deserializeAll($collection) as $weather) { +$weather = $denormalizer->denormalize($record); //we convert 1 record into 1 instance +foreach ($denormalizer->denormalizeAll($collection) as $weather) { // each $weather entry will be an instance of the Weather class; } // you can use the alternate syntactic sugar methods -// if you only need the deserializing mechanism once -$weather = Serializer::assign(Weather::class, $record); +// if you only need the denormalizing mechanism once +$weather = Denormalizer::assign(Weather::class, $record); -foreach (Serializer::assignAll(Weather::class, $collection, ['date', 'temperature', 'place']) as $weather) { +foreach (Denormalizer::assignAll(Weather::class, $collection, ['date', 'temperature', 'place']) as $weather) { // each $weather entry will be an instance of the Weather class; } ``` @@ -64,7 +64,7 @@ In the following sections we will explain the conversion and how it can be confi ## Prerequisite -The deserialization mechanism works mainly with DTO or objects +The denormalization mechanism works mainly with DTO or objects without complex logic in their constructors.

The mechanism relies on PHP's Reflection @@ -74,7 +74,7 @@ the mechanism may either fail or produced unexpected results.

To work as intended the mechanism expects the following: -- A target class where the array will be deserialized in; +- A target class where the array will be denormalized in; - information on how to convert cell values into object properties; As an example if we assume we have the following CSV document: @@ -113,29 +113,29 @@ enum Place } ``` -To get instances of your object, you now can call one of the `Serializer` method as show below: +To get instances of your object, you now can call one of the `Denormalizer` method as show below: ```php use League\Csv\Reader; -use League\Csv\Serializer +use League\Csv\Denormalizer $csv = Reader::createFromString($document); $csv->setHeaderOffset(0); -$serializer = new Serializer(Weather::class, $csv->header()); +$denormalizer = new Denormalizer(Weather::class, $csv->header()); foreach ($csv as $record) { - $weather = $serializer->deserialize($record); + $weather = $denormalizer->denormalize($record); } //or -foreach ($serializer->deserializeAll($csv) as $weather) { +foreach ($denormalizer->denormalizeAll($csv) as $weather) { // each $weather entry will be an instance of the Weather class; } //or -foreach (Serializer::assignAll(Weather::class, $csv, $csv->getHeader()) as $weather) { +foreach (Denormalizer::assignAll(Weather::class, $csv, $csv->getHeader()) as $weather) { // each $weather entry will be an instance of the Weather class; } ``` @@ -144,7 +144,7 @@ foreach (Serializer::assignAll(Weather::class, $csv, $csv->getHeader()) as $weat ## Defining the mapping rules -By default, the deserialization engine will automatically convert public properties using their name. +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. The record value **MUST BE** a `string` or `null` and the object public properties **MUST BE** typed with one of @@ -197,19 +197,19 @@ The above rule can be translated in plain english like this: ### Handling the empty string -Out of the box the `Serializer` makes no distinction between an empty string and the `null` value. +Out of the box the `Denormalizer` makes no distinction between an empty string and the `null` value. You can however change this behaviour using two (2) static methods: -- `Serializer::allowEmptyStringAsNull` -- `Serializer::disallowEmptyStringAsNull` +- `Denormalizer::allowEmptyStringAsNull` +- `Denormalizer::disallowEmptyStringAsNull` When called these methods will change the class behaviour when it comes to handling empty string. -`Serializer::allowEmptyStringAsNull` will trigger conversion of all empty string into the `null` value -before typecasting whereas `Serializer::disallowEmptyStringAsNull` will maintain the distinction. -Using these methods will affect the `Serializer` usage throughout your codebase. +`Denormalizer::allowEmptyStringAsNull` will trigger conversion of all empty string into the `null` value +before typecasting whereas `Denormalizer::disallowEmptyStringAsNull` will maintain the distinction. +Using these methods will affect the `Denormalizer` usage throughout your codebase. ```php -use League\Csv\Serializer; +use League\Csv\Denormalizer; $record = [ 'date' => '2023-10-30', @@ -217,11 +217,11 @@ $record = [ 'place' => 'Berkeley', ]; -$weather = Serializer::assign(Weather::class, $record); +$weather = Denormalizer::assign(Weather::class, $record); $weather->temperature; // returns null -Serializer::disallowEmptyStringAsNull(); -Serializer::assign(Weather::class, $record); +Denormalizer::disallowEmptyStringAsNull(); +Denormalizer::assign(Weather::class, $record); //a TypeCastingFailed exception is thrown because we //can not convert the empty string into a temperature property //which expects `null` or a non-empty string. @@ -330,7 +330,7 @@ use League\Csv\Serializer; private array $data; ``` -In the above example, the array has a JSON value associated with the key `data` and the `Serializer` will convert the +In the above example, the array has a JSON value associated with the key `data` and the `Denormalizer` will convert the JSON string into an `array` and use the `JSON_BIGINT_AS_STRING` option of the `json_decode` function. If you use the array shape `list` or `csv` you can also typecast the `array` content using the @@ -355,35 +355,61 @@ The `type` option only supports scalar type (`string`, `int`, `float` and `bool` ## Extending Type Casting capabilities -We provide two mechanisms to extends typecasting. You can register a closure via the `Serializer` class +We provide two mechanisms to extend typecasting. You can register a closure via the `Denormalizer` class or create a fully fledge `TypeCasting` class. Of course, the choice will depend on your use case. ### Registering a closure -You can register a closure using the `Serializer` class to convert a specific type. The type can be +You can register a closure using the `Denormalizer` class to convert a specific type. The type can be any built-in type or a specific class. ```php use App\Domain\Money; -use League\Csv\Serializer; +use League\Csv\Denormalizer; + +$typeCasting = function ( + ?string $value, + bool $isNullable, + ?int $default = 20_00 + ): ?Money { + if (null === $value && $isNullable) { + if (null !== $default) { + return Money::fromNaira($default); + } + + return null; + } + + return Money::fromNaira(filter_var($value, FILTER_VALIDATE_INT)); +} -Serializer::registerType(Money::class, fn (?string $value, bool $isNullable, ?int $default = null): Money => match (true) { - $isNullable && null === $value => Money::fromNaira($default ?? 20_00), - default => Money::fromNaira(filter_var($value, FILTER_VALIDATE_INT)), -}); +Denormalizer::registerType(Money::class, $typeCasting); ``` -The Serializer will automatically call the closure for any `App\Domain\Money` conversion. +The `Denormalizer` will automatically call the closure for any `App\Domain\Money` conversion. You can +also use the `Cell` attribute to further control the conversion + +To do so, first, specify your casting with the attribute: ```php +use App\Domain\Money use League\Csv\Serializer; -Serializer::registerType('int', fn (?string $value): int => 42); +#[Serializer\Cell(offset: 'amount', castArguments: ['default' => 20_00])] +private ?Money $naira; ``` +

No need to specify the cast argument as the closure is registered.

+ In the following example, the closure takes precedence over the `CastToInt` class to convert to the `int` type. If you still wish to use the `CastToInt` class you are require to -explicitly declare it using the `Cell` attribute `cast` argument. +explicitly declare it via the `Cell` attribute `cast` argument. + +```php +use League\Csv\Denormalizer; + +Denormalizer::registerType('int', fn (?string $value): int => 42); +``` The closure signature is the following: @@ -399,7 +425,7 @@ where: To complete the feature you can use: -- `Serializer::unregisterType` to remove the registered closure for a specific `type`; +- `Denormalizer::unregisterType` to remove the registered closure for a specific `type`; The two (2) methods are static. @@ -408,8 +434,8 @@ The two (2) methods are static. ### Implementing a TypeCasting class If you need to support `Intersection` type, or you want to be able to fine tune the typecasting -you can provide your own class to typecast the value according to your own rules. To do so, first, -specify your casting with the attribute: +you can provide your own class to typecast the value according to your own rules. Since the class +is not registered by default you must configure its usage via the `Cell` attribute `cast` argument. ```php use App\Domain\Money @@ -473,10 +499,11 @@ final class CastToNaira implements TypeCasting public function toVariable(?string $value): ?Money { try { - return match (true) { - $this->isNullable && null === $value => $this->default, - default => Money::fromNaira(filter_var($value, FILTER_VALIDATE_INT)), - }; + if (null === $value && $this->isNullable) { + return $this->default; + } + + return Money::fromNaira(filter_var($value, FILTER_VALIDATE_INT)); } catch (Throwable $exception) { throw new TypeCastingFailed('Unable to cast the given data `'.$value.'` to a `'.Money::class.'`.', 0, $exception); } diff --git a/src/Serializer.php b/src/Denormalizer.php similarity index 74% rename from src/Serializer.php rename to src/Denormalizer.php index e6c18511..4397b9af 100644 --- a/src/Serializer.php +++ b/src/Denormalizer.php @@ -31,21 +31,18 @@ use League\Csv\Serializer\TypeCastingFailed; use ReflectionAttribute; use ReflectionClass; -use ReflectionException; use ReflectionMethod; use ReflectionParameter; use ReflectionProperty; use Throwable; use function array_key_exists; -use function array_reduce; use function array_search; use function array_values; use function count; -use function in_array; use function is_int; -final class Serializer +final class Denormalizer { private static bool $emptyStringAsNull = true; @@ -60,13 +57,12 @@ final class Serializer * @param array $propertyNames * * @throws MappingFailed - * @throws ReflectionException */ public function __construct(string $className, array $propertyNames = []) { - $this->class = new ReflectionClass($className); + $this->class = $this->setClass($className); $this->properties = $this->class->getProperties(); - $this->propertySetters = $this->findPropertySetters($propertyNames); + $this->propertySetters = $this->setPropertySetters($propertyNames); } public static function allowEmptyStringAsNull(): void @@ -79,6 +75,9 @@ public static function disallowEmptyStringAsNull(): void self::$emptyStringAsNull = false; } + /** + * @throws MappingFailed + */ public static function registerType(string $type, Closure $closure): void { ClosureCasting::register($type, $closure); @@ -94,12 +93,11 @@ public static function unregisterType(string $type): void * @param array $record * * @throws MappingFailed - * @throws ReflectionException * @throws TypeCastingFailed */ public static function assign(string $className, array $record): object { - return (new self($className, array_keys($record)))->deserialize($record); + return (new self($className, array_keys($record)))->denormalize($record); } /** @@ -107,15 +105,14 @@ public static function assign(string $className, array $record): object * @param array $propertyNames * * @throws MappingFailed - * @throws ReflectionException * @throws TypeCastingFailed */ public static function assignAll(string $className, iterable $records, array $propertyNames = []): Iterator { - return (new self($className, $propertyNames))->deserializeAll($records); + return (new self($className, $propertyNames))->denormalizeAll($records); } - public function deserializeAll(iterable $records): Iterator + public function denormalizeAll(iterable $records): Iterator { $check = true; $assign = function (array $record) use (&$check) { @@ -134,10 +131,9 @@ public function deserializeAll(iterable $records): Iterator } /** - * @throws ReflectionException * @throws TypeCastingFailed */ - public function deserialize(array $record): object + public function denormalize(array $record): object { $object = $this->class->newInstanceWithoutConstructor(); @@ -178,65 +174,45 @@ private function assertObjectIsInValidState(object $object): void } /** - * @param array $propertyNames + * @param class-string $className * * @throws MappingFailed - * - * @return non-empty-array */ - private function findPropertySetters(array $propertyNames): array + private function setClass(string $className): ReflectionClass { - $propertySetters = []; - foreach ($this->class->getProperties(ReflectionProperty::IS_PUBLIC) as $property) { - if ($property->isStatic()) { - continue; - } - - $attribute = $property->getAttributes(Cell::class, ReflectionAttribute::IS_INSTANCEOF); - if ([] !== $attribute) { - continue; - } - - /** @var int|false $offset */ - $offset = array_search($property->getName(), $propertyNames, true); - if (false === $offset) { - continue; - } - - $propertySetters[] = new PropertySetter($property, $offset, $this->resolveTypeCasting($property)); + if (!class_exists($className)) { + throw new MappingFailed('The class `'.$className.'` does not exist or was not found.'); } - $propertySetters = [...$propertySetters, ...$this->findPropertySettersByAttribute($propertyNames)]; - if ([] === $propertySetters) { - throw new MappingFailed('No properties or method setters were found eligible on the class `'.$this->class->getName().'` to be used for type casting.'); + $class = new ReflectionClass($className); + if ($class->isInternal() && $class->isFinal()) { + throw new MappingFailed('The class `'.$className.'` can not be deserialize using `'.self::class.'`.'); } - return $propertySetters; + return $class; } /** * @param array $propertyNames * - * @return array + * @throws MappingFailed + * + * @return non-empty-array */ - private function findPropertySettersByAttribute(array $propertyNames): array + private function setPropertySetters(array $propertyNames): array { - $addPropertySetter = function (array $carry, ReflectionProperty|ReflectionMethod $accessor) use ($propertyNames) { + $propertySetters = []; + foreach ([...$this->properties, ...$this->class->getMethods(ReflectionMethod::IS_PUBLIC)] as $accessor) { $propertySetter = $this->findPropertySetter($accessor, $propertyNames); - if (null === $propertySetter) { - return $carry; + if (null !== $propertySetter) { + $propertySetters[] = $propertySetter; } + } - $carry[] = $propertySetter; - - return $carry; + return match ([]) { + $propertySetters => throw new MappingFailed('No property or method from `'.$this->class->getName().'` can be used for deserialization.'), + default => $propertySetters, }; - - return array_reduce( - [...$this->properties, ...$this->class->getMethods(ReflectionMethod::IS_PUBLIC)], - $addPropertySetter, - [] - ); } /** @@ -248,7 +224,21 @@ private function findPropertySetter(ReflectionProperty|ReflectionMethod $accesso { $attributes = $accessor->getAttributes(Cell::class, ReflectionAttribute::IS_INSTANCEOF); if ([] === $attributes) { - return null; + if (!$accessor instanceof ReflectionProperty) { + return null; + } + + if ($accessor->isStatic()) { + return null; + } + + /** @var int|false $offset */ + $offset = array_search($accessor->getName(), $propertyNames, true); + + return match (false) { + $offset => null, + default => new PropertySetter($accessor, $offset, $this->resolveTypeCasting($accessor)), + }; } if (1 < count($attributes)) { @@ -277,11 +267,11 @@ private function findPropertySetter(ReflectionProperty|ReflectionMethod $accesso /** @var int<0, max>|false $index */ $index = array_search($offset, $propertyNames, true); - if (false === $index) { - throw new MappingFailed('The offset `'.$offset.'` could not be found in the header; Pleaser verify your header data.'); - } - return new PropertySetter($accessor, $index, $cast); + return match (false) { + $index => throw new MappingFailed('The offset `'.$offset.'` could not be found in the header; Pleaser verify your header data.'), + default => new PropertySetter($accessor, $index, $cast), + }; } /** @@ -303,8 +293,8 @@ private function getTypeCasting(Cell $cell, ReflectionProperty|ReflectionMethod return $this->resolveTypeCasting($reflectionProperty, $cell->castArguments); } - if (!in_array(TypeCasting::class, class_implements($typeCaster), true)) { - throw new MappingFailed('The class `'.$typeCaster.'` does not implements the `'.TypeCasting::class.'` interface.'); + if (!class_exists($typeCaster) || !(new ReflectionClass($typeCaster))->implementsInterface(TypeCasting::class)) { + throw new MappingFailed('`'.$typeCaster.'` must be an resolvable class implementing the `'.TypeCasting::class.'` interface.'); } try { @@ -312,12 +302,10 @@ private function getTypeCasting(Cell $cell, ReflectionProperty|ReflectionMethod $cast = new $typeCaster(...$cell->castArguments, ...['reflectionProperty' => $reflectionProperty]); return $cast; + } catch (MappingFailed $exception) { + throw $exception; } catch (Throwable $exception) { - if ($exception instanceof MappingFailed) { - throw $exception; - } - - throw new MappingFailed(message:'Unable to instantiate a casting mechanism. Please verify your casting arguments', previous: $exception); + throw new MappingFailed('Unable to load the casting mechanism. Please verify your casting arguments', 0, $exception); } } @@ -345,10 +333,10 @@ private function resolveTypeCasting(ReflectionProperty|ReflectionParameter $refl Type::Enum => new CastToEnum(...$arguments), default => throw $exception, }; - } catch (MappingFailed $exception) { - throw $exception; + } catch (MappingFailed $mappingFailed) { + throw $mappingFailed; } catch (Throwable $exception) { - throw new MappingFailed(message:'Unable to load the casting mechanism. Please verify your casting arguments', previous: $exception); + throw new MappingFailed('Unable to load the casting mechanism. Please verify your casting arguments', 0, $exception); } } } diff --git a/src/SerializerTest.php b/src/DenormalizerTest.php similarity index 81% rename from src/SerializerTest.php rename to src/DenormalizerTest.php index 26c7ed31..1f6cd33e 100644 --- a/src/SerializerTest.php +++ b/src/DenormalizerTest.php @@ -26,7 +26,7 @@ use stdClass; use Traversable; -final class SerializerTest extends TestCase +final class DenormalizerTest extends TestCase { public function testItConvertsAnIterableListOfRecords(): void { @@ -43,7 +43,7 @@ public function testItConvertsAnIterableListOfRecords(): void ], ]; - $results = [...Serializer::assignAll(WeatherWithRecordAttribute::class, $records, ['date', 'temperature', 'place'])]; + $results = [...Denormalizer::assignAll(WeatherWithRecordAttribute::class, $records, ['date', 'temperature', 'place'])]; self::assertCount(2, $results); foreach ($results as $result) { self::assertInstanceOf(WeatherWithRecordAttribute::class, $result); @@ -58,7 +58,7 @@ public function testItConvertsARecordsToAnObjectUsingRecordAttribute(): void 'place' => 'Berkeley', ]; - $weather = Serializer::assign(WeatherWithRecordAttribute::class, $record); + $weather = Denormalizer::assign(WeatherWithRecordAttribute::class, $record); self::assertInstanceOf(WeatherWithRecordAttribute::class, $weather); self::assertInstanceOf(DateTimeImmutable::class, $weather->observedOn); @@ -74,7 +74,7 @@ public function testItConvertsARecordsToAnObjectUsingProperties(): void 'place' => 'Berkeley', ]; - $weather = Serializer::assign(WeatherProperty::class, $record); + $weather = Denormalizer::assign(WeatherProperty::class, $record); self::assertInstanceOf(WeatherProperty::class, $weather); self::assertInstanceOf(DateTimeImmutable::class, $weather->observedOn); @@ -90,7 +90,7 @@ public function testItConvertsARecordsToAnObjectUsingMethods(): void 'place' => 'Berkeley', ]; - $weather = Serializer::assign(WeatherSetterGetter::class, $record); + $weather = Denormalizer::assign(WeatherSetterGetter::class, $record); self::assertInstanceOf(WeatherSetterGetter::class, $weather); self::assertSame('2023-10-30', $weather->getObservedOn()->format('Y-m-d')); @@ -101,9 +101,9 @@ public function testItConvertsARecordsToAnObjectUsingMethods(): void public function testMappingFailBecauseTheRecordAttributeIsMissing(): void { $this->expectException(MappingFailed::class); - $this->expectExceptionMessage('No properties or method setters were found eligible on the class `stdClass` to be used for type casting.'); + $this->expectExceptionMessage('No property or method from `stdClass` can be used for deserialization.'); - Serializer::assign(stdClass::class, ['foo' => 'bar']); + Denormalizer::assign(stdClass::class, ['foo' => 'bar']); } public function testItWillThrowIfTheHeaderIsMissingAndTheColumnOffsetIsAString(): void @@ -111,8 +111,8 @@ public function testItWillThrowIfTheHeaderIsMissingAndTheColumnOffsetIsAString() $this->expectException(MappingFailed::class); $this->expectExceptionMessage('Column name as string are only supported if the tabular data has a non-empty header.'); - $serializer = new Serializer(WeatherSetterGetter::class); - $serializer->deserialize([ + $serializer = new Denormalizer(WeatherSetterGetter::class); + $serializer->denormalize([ 'date' => '2023-10-30', 'temperature' => '-1.5', 'place' => 'Berkeley', @@ -124,8 +124,8 @@ public function testItWillThrowIfTheHeaderContainsInvalidOffsetName(): void $this->expectException(MappingFailed::class); $this->expectExceptionMessage('The offset `temperature` could not be found in the header; Pleaser verify your header data.'); - $serializer = new Serializer(WeatherSetterGetter::class, ['date', 'toto', 'foobar']); - $serializer->deserialize([ + $serializer = new Denormalizer(WeatherSetterGetter::class, ['date', 'toto', 'foobar']); + $serializer->denormalize([ 'date' => '2023-10-30', 'temperature' => '-1.5', 'place' => 'Berkeley', @@ -137,15 +137,15 @@ public function testItWillThrowIfTheColumnAttributesIsUsedMultipleTimeForTheSame $this->expectException(MappingFailed::class); $this->expectExceptionMessage('Using more than one `League\Csv\Serializer\Cell` attribute on a class property or method is not supported.'); - new Serializer(InvalidWeatherAttributeUsage::class); + new Denormalizer(InvalidWeatherAttributeUsage::class); } public function testItWillThrowIfTheColumnAttributesCasterIsInvalid(): void { $this->expectException(MappingFailed::class); - $this->expectExceptionMessage('The class `stdClass` does not implements the `League\Csv\Serializer\TypeCasting` interface.'); + $this->expectExceptionMessage('`stdClass` must be an resolvable class implementing the `League\Csv\Serializer\TypeCasting` interface.'); - new Serializer(InvalidWeatherAttributeCasterNotSupported::class); + new Denormalizer(InvalidWeatherAttributeCasterNotSupported::class); } public function testItWillThrowBecauseTheObjectDoesNotHaveTypedProperties(): void @@ -153,7 +153,7 @@ public function testItWillThrowBecauseTheObjectDoesNotHaveTypedProperties(): voi $this->expectException(MappingFailed::class); $this->expectExceptionMessage('The property `temperature` must be typed with a supported type.'); - new Serializer(InvaliDWeatherWithRecordAttribute::class, ['temperature', 'foobar', 'observedOn']); + new Denormalizer(InvaliDWeatherWithRecordAttribute::class, ['temperature', 'foobar', 'observedOn']); } public function testItWillFailForLackOfTypeCasting(): void @@ -161,7 +161,7 @@ public function testItWillFailForLackOfTypeCasting(): void $this->expectException(MappingFailed::class); $this->expectExceptionMessage('The property `observedOn` must be typed with a supported type.'); - new Serializer(InvaliDWeatherWithRecordAttributeAndUnknownCasting::class, ['temperature', 'place', 'observedOn']); + new Denormalizer(InvaliDWeatherWithRecordAttributeAndUnknownCasting::class, ['temperature', 'place', 'observedOn']); } public function testItWillThrowIfTheClassContainsUninitializedProperties(): void @@ -169,7 +169,7 @@ public function testItWillThrowIfTheClassContainsUninitializedProperties(): void $this->expectException(MappingFailed::class); $this->expectExceptionMessage('The property `annee` must be typed with a supported type.'); - Serializer::assign( + Denormalizer::assign( InvalidObjectWithUninitializedProperty::class, ['prenoms' => 'John', 'nombre' => '42', 'sexe' => 'M', 'annee' => '2018'] ); @@ -184,7 +184,7 @@ public function testItCanNotAutodiscoverWithIntersectionType(): void public Countable&Traversable $traversable; }; - Serializer::assign($foobar::class, ['traversable' => '1']); + Denormalizer::assign($foobar::class, ['traversable' => '1']); } public function testItCanUseTheClosureRegisteringMechanism(): void @@ -194,13 +194,13 @@ public function testItCanUseTheClosureRegisteringMechanism(): void public string $foo; }; - Serializer::registerType('string', fn (?string $value) => 'yolo!'); + Denormalizer::registerType('string', fn (?string $value) => 'yolo!'); - self::assertSame('yolo!', Serializer::assign($foobar::class, $record)->foo); /* @phpstan-ignore-line */ + self::assertSame('yolo!', Denormalizer::assign($foobar::class, $record)->foo); /* @phpstan-ignore-line */ - Serializer::unregisterType('string'); + Denormalizer::unregisterType('string'); - self::assertSame('toto', Serializer::assign($foobar::class, $record)->foo); + self::assertSame('toto', Denormalizer::assign($foobar::class, $record)->foo); } public function testItFailsToRegisterUnknownType(): void @@ -209,7 +209,7 @@ public function testItFailsToRegisterUnknownType(): void $this->expectException(MappingFailed::class); $this->expectExceptionMessage('The `'.$type.'` could not be register.'); - Serializer::registerType($type, fn (?string $value) => 'yolo!'); + Denormalizer::registerType($type, fn (?string $value) => 'yolo!'); } public function testEmptyStringHandling(): void @@ -219,13 +219,13 @@ public function testEmptyStringHandling(): void public ?string $foo; }; - Serializer::disallowEmptyStringAsNull(); + Denormalizer::disallowEmptyStringAsNull(); - self::assertSame('', Serializer::assign($foobar::class, $record)->foo); /* @phpstan-ignore-line */ + self::assertSame('', Denormalizer::assign($foobar::class, $record)->foo); /* @phpstan-ignore-line */ - Serializer::allowEmptyStringAsNull(); + Denormalizer::allowEmptyStringAsNull(); - self::assertNull(Serializer::assign($foobar::class, $record)->foo); + self::assertNull(Denormalizer::assign($foobar::class, $record)->foo); } } diff --git a/src/Reader.php b/src/Reader.php index 16de14fb..638c0c8e 100644 --- a/src/Reader.php +++ b/src/Reader.php @@ -19,7 +19,6 @@ use JsonSerializable; use League\Csv\Serializer\MappingFailed; use League\Csv\Serializer\TypeCastingFailed; -use ReflectionException; use SplFileObject; use function array_filter; @@ -442,7 +441,6 @@ protected function prepareHeader($header = []): array * * @throws Exception * @throws MappingFailed - * @throws ReflectionException * @throws TypeCastingFailed */ public function getObjects(string $className, array $header = []): Iterator @@ -450,7 +448,7 @@ public function getObjects(string $className, array $header = []): Iterator /** @var array $header */ $header = $this->prepareHeader($header); - return Serializer::assignAll( + return Denormalizer::assignAll( $className, $this->combineHeader($this->prepareRecords(), $header), $header diff --git a/src/ResultSet.php b/src/ResultSet.php index 3e8a618e..f09d7f3e 100644 --- a/src/ResultSet.php +++ b/src/ResultSet.php @@ -22,7 +22,6 @@ use League\Csv\Serializer\MappingFailed; use League\Csv\Serializer\TypeCastingFailed; use LimitIterator; -use ReflectionException; use function array_filter; use function array_flip; @@ -247,14 +246,13 @@ public function getRecords(array $header = []): Iterator * * @throws Exception * @throws MappingFailed - * @throws ReflectionException * @throws TypeCastingFailed */ public function getObjects(string $className, array $header = []): Iterator { $header = $this->prepareHeader($header); - return Serializer::assignAll( + return Denormalizer::assignAll( $className, $this->combineHeader($header), $header diff --git a/src/Serializer/ClosureCasting.php b/src/Serializer/ClosureCasting.php index fe72a150..5ba0cd58 100644 --- a/src/Serializer/ClosureCasting.php +++ b/src/Serializer/ClosureCasting.php @@ -21,6 +21,9 @@ use ReflectionUnionType; use Throwable; +use function array_key_exists; +use function class_exists; + final class ClosureCasting implements TypeCasting { /** @var array */ @@ -60,11 +63,11 @@ public function toVariable(?string $value): mixed public static function register(string $type, Closure $closure): void { - if (!class_exists($type) && !(Type::tryFrom($type)?->isBuiltIn() ?? false)) { - throw new MappingFailed('The `'.$type.'` could not be register.'); - } - - self::$casters[$type] = $closure; + self::$casters[$type] = match (true) { + class_exists($type), + Type::tryFrom($type)?->isBuiltIn() ?? false => $closure, + default => throw new MappingFailed('The `'.$type.'` could not be register.'), + }; } public static function unregister(string $type): void diff --git a/src/Serializer/Type.php b/src/Serializer/Type.php index 4f5cee1f..a3a1e057 100644 --- a/src/Serializer/Type.php +++ b/src/Serializer/Type.php @@ -13,6 +13,7 @@ use DateTimeInterface; +use ReflectionClass; use ReflectionNamedType; use ReflectionParameter; use ReflectionProperty; @@ -20,7 +21,6 @@ use ReflectionUnionType; use function class_exists; -use function class_implements; use function enum_exists; use function in_array; use function interface_exists; @@ -168,7 +168,7 @@ private static function tryFromName(string $propertyType): ?self $type instanceof self => $type, enum_exists($propertyType) => self::Enum, interface_exists($propertyType) && DateTimeInterface::class === $propertyType, - class_exists($propertyType) && in_array(DateTimeInterface::class, class_implements($propertyType), true) => self::Date, + class_exists($propertyType) && (new ReflectionClass($propertyType))->implementsInterface(DateTimeInterface::class) => self::Date, default => null, }; }