From f65a9959d4dfff1942279ca0add0d2cbc1f17d0d Mon Sep 17 00:00:00 2001
From: ignace nyamagana butera
Date: Mon, 6 Nov 2023 16:39:20 +0100
Subject: [PATCH] Improve CastToEnum implementation to work with mixed type
---
docs/9.0/reader/record-mapping.md | 45 +++++++++++++++----------------
src/Serializer.php | 12 ++++-----
src/Serializer/CastToEnum.php | 25 ++++++++++++-----
3 files changed, 46 insertions(+), 36 deletions(-)
diff --git a/docs/9.0/reader/record-mapping.md b/docs/9.0/reader/record-mapping.md
index 801d2017..9865d02c 100644
--- a/docs/9.0/reader/record-mapping.md
+++ b/docs/9.0/reader/record-mapping.md
@@ -9,14 +9,12 @@ title: Deserializing a Tabular Data record into an object
## Converting an array to an object
-If you prefer working with objects instead of typed arrays it is possible to map each record to
-a specified class. To do so a new `Serializer` class is introduced to expose a deserialization mechanism
+To work with objects instead of arrays the `Serializer` class is introduced to expose a deserialization mechanism.
-The class exposes three (3) methods to ease `array` to `object` conversion in the context of Tabular data:
+The class exposes two (2) methods to ease `array` to `object` conversion in the context of tabular data:
-- `Serializer::deserialize` which expect a single record as argument and returns on success an instance of the class.
-- `Serializer::deserializeAll` which expect a collection of records and returns a collection of class instances.
-- and the public static method `Serializer::map` which is a quick way to declare and convert a single record into an object.
+- `Serializer::deserialize` which converts a single record into an instance of the specified class.
+- `Serializer::deserializeAll` which converts a collection of records and returns a collection of the specified class instances.
```php
use League\Csv\Serializer;
@@ -27,20 +25,17 @@ $record = [
'place' => 'Berkeley',
];
-$serializer = new Serializer(Weather::class, array_keys($record));
+$serializer = new Serializer(Weather::class, ['date', 'temperature', 'place']);
$weather = $serializer->deserialize($record);
$collection = [$record];
foreach ($serializer->deserializeAll($collection) as $weather) {
// each $weather entry will be an instance of the Weather class;
}
-
-//this is equivalent to the first example
-$weather = Serializer::map(Weather::class, $record);
```
If you are working with a class which implements the `TabularDataReader` interface you can use this functionality
-directly by calling the `TabularDataReader::map` method.
+directly by calling the `TabularDataReader::getObjects` method.
Here's an example using the `Reader` class:
@@ -69,7 +64,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;
-- informations on how to convert cell value into object properties using dedicated attributes;
+- information on how to convert cell value into object properties using dedicated attributes;
As an example if we assume we have the following CSV document:
@@ -161,10 +156,7 @@ The above rule can be translated in plain english like this:
> using the date format `!Y-m-d` and the `Africa/Nairobi` timezone. Once created,
> inject the date instance into the `observedOn` property of the class.
-The `Cell` attribute can be used:
-
-- on class properties and methods (public, protected or private).
-
+The `Cell` attribute can be used on class properties as well as on the class methods regardless of their visibility.
The `Cell` attribute can take up to three (3) arguments which are all optional:
- The `offset` argument which tell the engine which cell to use via its numeric or name offset. If not present
@@ -179,24 +171,29 @@ In any cases, if type casting fails, an exception will be thrown.
## Type casting the record value
The library comes bundles with four (4) type casting classes which relies on the property type information. All the
-built-in methods support the `nullable` type. They will return `null` if the cell value is the empty string or `null`
-only if the type is considered to be `nullable` otherwise they will throw an exception.
+built-in methods support the `nullable` and the `mixed` types.
+
+- They will return `null` if the cell value is `null` and the type is `nullable`
+- If the value can not be cast they will throw an exception.
+
All classes are defined under the `League\Csv\Serializer` namespace.
### CastToBuiltInType
Converts the array value to a scalar type or `null` depending on the property type information. This class has no
-specific configuration but will work with all the scalar type, the `true`, `null` and `false` value type as well as
-with the `mixed` type. Type casting is done using the `filter_var` functionality of the `ext-filter` extension.
+specific configuration but will work with all the scalar type, `true`, `false` and `null` value type as well as
+with the `mixed` type. Type casting is done internally using the `filter_var` function.
### CastToEnum
-Convert the array value to a PHP `Enum` it supported both "real" and backed enumeration. No configuration is needed
-if the value is not recognized an exception will be thrown.
+Convert the array value to a PHP `Enum` it supported both "real" and backed enumeration.
### CastToDate
-Converts the cell value into a PHP `DateTimeInterface` implementing object. You can optionally specify the date format and its timezone if needed.
+Converts the cell value into a PHP `DateTimeInterface` implementing object. You can optionally specify:
+
+- the date format
+- the date timezone if needed.
### CastToArray
@@ -265,7 +262,7 @@ use League\Csv\Serializer\TypeCastingFailed;
readonly class IntegerRangeCasting implements TypeCasting
{
public function __construct(
- string $propertyType,
+ string $propertyType, //always required and given by the Serializer implementation
private int $min,
private int $max,
private int $default,
diff --git a/src/Serializer.php b/src/Serializer.php
index 44934de8..ccc3db18 100644
--- a/src/Serializer.php
+++ b/src/Serializer.php
@@ -169,7 +169,7 @@ private function findPropertySetters(array $propertyNames): array
$cast = $this->resolveTypeCasting($type->getName());
if (null === $cast) {
//the property can not be automatically cast
- //we can not throw yet as the caster may be set
+ //we can not throw yet as casting may be set
//using the Cell attribute
$check['property:'.$propertyName] = $propertyName;
@@ -264,13 +264,13 @@ private function findPropertySetter(ReflectionProperty|ReflectionMethod $accesso
/**
* @param array $arguments
*/
- private function resolveTypeCasting(string $type, array $arguments = []): ?TypeCasting
+ private function resolveTypeCasting(string $propertyType, array $arguments = []): ?TypeCasting
{
return match (true) {
- CastToDate::supports($type) => new CastToDate($type, ...$arguments), /* @phpstan-ignore-line */
- CastToArray::supports($type) => new CastToArray($type, ...$arguments), /* @phpstan-ignore-line */
- CastToEnum::supports($type) => new CastToEnum($type),
- CastToBuiltInType::supports($type) => new CastToBuiltInType($type),
+ CastToBuiltInType::supports($propertyType) => new CastToBuiltInType($propertyType),
+ CastToDate::supports($propertyType) => new CastToDate($propertyType, ...$arguments), /* @phpstan-ignore-line */
+ CastToArray::supports($propertyType) => new CastToArray($propertyType, ...$arguments), /* @phpstan-ignore-line */
+ CastToEnum::supports($propertyType) => new CastToEnum($propertyType),
default => null,
};
}
diff --git a/src/Serializer/CastToEnum.php b/src/Serializer/CastToEnum.php
index dfe35562..775f9120 100644
--- a/src/Serializer/CastToEnum.php
+++ b/src/Serializer/CastToEnum.php
@@ -29,6 +29,11 @@ class CastToEnum implements TypeCasting
public static function supports(string $type): bool
{
+ $enum = ltrim($type, '?');
+ if (BuiltInType::Mixed->value === $enum) {
+ return true;
+ }
+
try {
new ReflectionEnum(ltrim($type, '?'));
@@ -38,14 +43,23 @@ public static function supports(string $type): bool
}
}
- public function __construct(string $type)
+ public function __construct(string $propertyType, ?string $enum = null)
{
- if (!self::supports($type)) {
- throw new TypeCastingFailed('The property type `'.$type.'` is not a PHP Enumeration.');
+ if (!self::supports($propertyType)) {
+ throw new TypeCastingFailed('The property type `'.$propertyType.'` is not a PHP Enumeration.');
+ }
+
+ $enumClass = ltrim($propertyType, '?');
+ if (BuiltInType::Mixed->value === $enumClass) {
+ if (null === $enum || !self::supports($enum)) {
+ throw new TypeCastingFailed('The property type `'.$enum.'` is not a PHP Enumeration.');
+ }
+
+ $enumClass = $enum;
}
- $this->class = ltrim($type, '?');
- $this->isNullable = str_starts_with($type, '?');
+ $this->class = $enumClass;
+ $this->isNullable = str_starts_with($propertyType, '?');
}
/**
@@ -53,7 +67,6 @@ public function __construct(string $type)
*/
public function toVariable(?string $value): BackedEnum|UnitEnum|null
{
-
if (null === $value) {
return match (true) {
$this->isNullable, => null,