diff --git a/docs/9.0/reader/record-mapping.md b/docs/9.0/reader/record-mapping.md index c8bbe17d..e663c3e8 100644 --- a/docs/9.0/reader/record-mapping.md +++ b/docs/9.0/reader/record-mapping.md @@ -463,17 +463,11 @@ use League\Csv\Serializer\TypeCastingFailed; final class CastToNaira implements TypeCasting { private readonly bool $isNullable; - private readonly ?Naira $default; + private ?Naira $default; public function __construct( ReflectionProperty|ReflectionParameter $reflectionProperty, //always given by the Denormalizer - ?int $default = null //can be filled via the MapCell options array destructuring ) { - if (null !== $default) { - $default = Naira::fromKobos($default); - } - $this->default = $default; - // It is recommended to handle the $reflectionProperty argument. // The argument gives you access to property/argument information. // it allows validating that the argument does support your casting @@ -489,9 +483,20 @@ final class CastToNaira implements TypeCasting $reflectionProperty instanceof ReflectionProperty => 'The property '.$message, }); } + $this->isNullable = $reflectionType->allowsNull(); } + public function setOptions( + ?int $default = null //will be filled via the MapCell options array destructuring + ) : void{ + if (null !== $default) { + $default = Naira::fromKobos($default); + } + + $this->default = $default; + } + public function toVariable(?string $value): ?Naira { try { diff --git a/src/Serializer/CastToArray.php b/src/Serializer/CastToArray.php index 65234f96..09bd6d4a 100644 --- a/src/Serializer/CastToArray.php +++ b/src/Serializer/CastToArray.php @@ -31,10 +31,26 @@ */ final class CastToArray implements TypeCasting { + private ArrayShape $shape; private readonly Type $type; private readonly bool $isNullable; - private readonly int $filterFlag; - private readonly ArrayShape $shape; + private int $filterFlag; + /** @var non-empty-string */ + private string $separator = ','; + private string $delimiter = ''; + private string $enclosure = '"'; + /** @var int<1, max> $depth */ + private int $depth = 512; + private int $flags = 0; + private ?array $default = null; + + /** + * @throws MappingFailed + */ + public function __construct(ReflectionProperty|ReflectionParameter $reflectionProperty) + { + [$this->type, $this->isNullable] = $this->init($reflectionProperty); + } /** * @param non-empty-string $delimiter @@ -43,27 +59,31 @@ final class CastToArray implements TypeCasting * * @throws MappingFailed */ - public function __construct( - ReflectionProperty|ReflectionParameter $reflectionProperty, - private readonly ?array $default = null, + public function setOptions( + ?array $default = null, ArrayShape|string $shape = ArrayShape::List, - private readonly string $separator = ',', - private readonly string $delimiter = ',', - private readonly string $enclosure = '"', - private readonly int $depth = 512, - private readonly int $flags = 0, + string $separator = ',', + string $delimiter = ',', + string $enclosure = '"', + int $depth = 512, + int $flags = 0, Type|string $type = Type::String, - ) { - [$this->type, $this->isNullable] = $this->init($reflectionProperty); + ): void { if (!$shape instanceof ArrayShape) { - $shape = ArrayShape::tryFrom($shape) ?? throw new MappingFailed('Unable to resolve the array shape; Verify your cast arguments.'); + $shape = ArrayShape::tryFrom($shape) ?? throw new MappingFailed('Unable to resolve the array shape; Verify your options arguments.'); } if (!$type instanceof Type) { - $type = Type::tryFrom($type); + $type = Type::tryFrom($type) ?? throw new MappingFailed('Unable to resolve the array value type; Verify your options arguments.'); } $this->shape = $shape; + $this->depth = $depth; + $this->separator = $separator; + $this->delimiter = $delimiter; + $this->enclosure = $enclosure; + $this->flags = $flags; + $this->default = $default; $this->filterFlag = match (true) { 1 > $this->depth && $this->shape->equals(ArrayShape::Json) => throw new MappingFailed('the json depth can not be less than 1.'), 1 > strlen($this->separator) && $this->shape->equals(ArrayShape::List) => throw new MappingFailed('expects separator to be a non-empty string for list conversion; empty string given.'), diff --git a/src/Serializer/CastToArrayTest.php b/src/Serializer/CastToArrayTest.php index c0d4771c..9d8f5260 100644 --- a/src/Serializer/CastToArrayTest.php +++ b/src/Serializer/CastToArrayTest.php @@ -29,7 +29,10 @@ final class CastToArrayTest extends TestCase #[DataProvider('providesValidStringForArray')] public function testItCanConvertToArraygWithoutArguments(string $shape, string $type, string $input, array $expected): void { - self::assertSame($expected, (new CastToArray(reflectionProperty: new ReflectionProperty(ArrayClass::class, 'nullableIterable'), shape:$shape, type:$type))->toVariable($input)); + $cast = new CastToArray(new ReflectionProperty(ArrayClass::class, 'nullableIterable')); + $cast->setOptions(shape:$shape, type:$type); + + self::assertSame($expected, $cast->toVariable($input)); } public static function providesValidStringForArray(): iterable @@ -101,18 +104,19 @@ public function testItFailsToCastAnUnsupportedType(): void public function testItFailsToCastInvalidJson(): void { $this->expectException(TypeCastingFailed::class); - - (new CastToArray(new ReflectionProperty(ArrayClass::class, 'nullableIterable'), null, 'json'))->toVariable('{"json":toto}'); + $cast = new CastToArray(new ReflectionProperty(ArrayClass::class, 'nullableIterable')); + $cast->setOptions(shape: 'json'); + $cast->toVariable('{"json":toto}'); } public function testItCastNullableJsonUsingTheDefaultValue(): void { $defaultValue = ['toto']; - self::assertSame( - $defaultValue, - (new CastToArray(new ReflectionProperty(ArrayClass::class, 'nullableIterable'), $defaultValue, 'json'))->toVariable(null) - ); + $cast = new CastToArray(new ReflectionProperty(ArrayClass::class, 'nullableIterable')); + $cast->setOptions(default: $defaultValue, shape: 'json'); + + self::assertSame($defaultValue, $cast->toVariable(null)); } #[DataProvider('invalidPropertyName')] diff --git a/src/Serializer/CastToBool.php b/src/Serializer/CastToBool.php index 45606414..0746caed 100644 --- a/src/Serializer/CastToBool.php +++ b/src/Serializer/CastToBool.php @@ -25,14 +25,18 @@ final class CastToBool implements TypeCasting { private readonly bool $isNullable; private readonly Type $type; + private ?bool $default = null; - public function __construct( - ReflectionProperty|ReflectionParameter $reflectionProperty, - private readonly ?bool $default = null - ) { + public function __construct(ReflectionProperty|ReflectionParameter $reflectionProperty) + { [$this->type, $this->isNullable] = $this->init($reflectionProperty); } + public function setOptions(bool $default = null): void + { + $this->default = $default; + } + /** * @throws TypeCastingFailed */ diff --git a/src/Serializer/CastToBoolTest.php b/src/Serializer/CastToBoolTest.php index 255e1f44..2fed5ed7 100644 --- a/src/Serializer/CastToBoolTest.php +++ b/src/Serializer/CastToBoolTest.php @@ -36,7 +36,10 @@ public function testItCanConvertStringToBool( ?string $input, ?bool $expected ): void { - self::assertSame($expected, (new CastToBool($propertyType, $default))->toVariable($input)); + $cast = new CastToBool($propertyType); + $cast->setOptions($default); + + self::assertSame($expected, $cast->toVariable($input)); } public static function providesValidInputValue(): iterable diff --git a/src/Serializer/CastToDate.php b/src/Serializer/CastToDate.php index 8d84494e..3991006d 100644 --- a/src/Serializer/CastToDate.php +++ b/src/Serializer/CastToDate.php @@ -30,33 +30,45 @@ */ final class CastToDate implements TypeCasting { - private readonly ?DateTimeZone $timezone; /** @var class-string */ - private readonly string $class; + private string $class; private readonly bool $isNullable; - private readonly DateTimeImmutable|DateTime|null $default; + private DateTimeImmutable|DateTime|null $default = null; + private readonly Type $type; + private readonly string $propertyName; + private ?DateTimeZone $timezone = null; + private ?string $format = null; /** - * @param ?class-string $className - * * @throws MappingFailed */ public function __construct( ReflectionProperty|ReflectionParameter $reflectionProperty, + ) { + [$this->type, $this->class, $this->isNullable] = $this->init($reflectionProperty); + $this->propertyName = $reflectionProperty->getName(); + } + + /** + * @param ?class-string $className + * + * @throws MappingFailed + */ + public function setOptions( ?string $default = null, - private readonly ?string $format = null, + ?string $format = null, DateTimeZone|string|null $timezone = null, ?string $className = null - ) { - [$type, $class, $this->isNullable] = $this->init($reflectionProperty); + ): void { $this->class = match (true) { - !interface_exists($class) && !Type::Mixed->equals($type) => $class, - DateTimeInterface::class === $class && null === $className => DateTimeImmutable::class, - interface_exists($class) && null !== $className && class_exists($className) && (new ReflectionClass($className))->implementsInterface($class) => $className, - default => throw new MappingFailed('`'.$reflectionProperty->getName().'` type is `'.($class ?? 'mixed').'` but the specified class via the `$className` argument is invalid or could not be found.'), + !interface_exists($this->class) && !Type::Mixed->equals($this->type) => $this->class, + DateTimeInterface::class === $this->class && null === $className => DateTimeImmutable::class, + interface_exists($this->class) && null !== $className && class_exists($className) && (new ReflectionClass($className))->implementsInterface($this->class) => $className, + default => throw new MappingFailed('`'.$this->propertyName.'` type is `'.($this->class ?? 'mixed').'` but the specified class via the `$className` argument is invalid or could not be found.'), }; try { + $this->format = $format; $this->timezone = is_string($timezone) ? new DateTimeZone($timezone) : $timezone; $this->default = (null !== $default) ? $this->cast($default) : $default; } catch (Throwable $exception) { diff --git a/src/Serializer/CastToDateTest.php b/src/Serializer/CastToDateTest.php index da3fc62b..83658448 100644 --- a/src/Serializer/CastToDateTest.php +++ b/src/Serializer/CastToDateTest.php @@ -35,7 +35,8 @@ public function testItCanConvertADateWithoutArguments(): void public function testItCanConvertADateWithASpecificFormat(): void { - $cast = new CastToDate(new ReflectionProperty(DateClass::class, 'dateTimeInterface'), null, '!Y-m-d', 'Africa/Kinshasa'); + $cast = new CastToDate(new ReflectionProperty(DateClass::class, 'dateTimeInterface')); + $cast->setOptions(format:'!Y-m-d', timezone: 'Africa/Kinshasa'); $date = $cast->toVariable('2023-10-30'); self::assertInstanceOf(DateTimeImmutable::class, $date); @@ -54,10 +55,9 @@ public function testItCanConvertAnObjectImplementingTheDateTimeInterface(): void public function testItCanConvertAnObjectImplementingAnInterfaceThatExtendsDateTimeInterface(): void { - $cast = new CastToDate( - reflectionProperty: new ReflectionProperty(DateClass::class, 'myDateInterface'), - className: MyDate::class, - ); + $cast = new CastToDate(new ReflectionProperty(DateClass::class, 'myDateInterface')); + $cast->setOptions(className: MyDate::class); + $date = $cast->toVariable('2023-10-30'); self::assertInstanceOf(MyDate::class, $date); @@ -70,6 +70,7 @@ public function testItFailsConversionIfImplementationForTheCustomeInterfaceThatE $this->expectExceptionMessage('`myDateInterface` type is `'.MyDateInterface::class.'` but the specified class via the `$className` argument is invalid or could not be found.'); $cast = new CastToDate(new ReflectionProperty(DateClass::class, 'myDateInterface')); + $cast->setOptions(); $cast->toVariable('2023-10-30'); } @@ -84,12 +85,8 @@ public function testItCShouldThrowIfTheOptionsAreInvalid(): void { $this->expectException(MappingFailed::class); - new CastToDate( - new ReflectionProperty(DateClass::class, 'dateTimeInterface'), - '2023-11-11', - 'Y-m-d', - 'Europe\Blan' - ); + $cast = new CastToDate(new ReflectionProperty(DateClass::class, 'dateTimeInterface')); + $cast->setOptions('2023-11-11', 'Y-m-d', 'Europe\Blan'); } public function testItReturnsNullWhenTheVariableIsNullable(): void @@ -101,7 +98,8 @@ public function testItReturnsNullWhenTheVariableIsNullable(): void public function testItCanConvertADateWithADefaultValue(): void { - $cast = new CastToDate(new ReflectionProperty(DateClass::class, 'nullableDateTimeInterface'), '2023-01-01', '!Y-m-d', 'Africa/Kinshasa'); + $cast = new CastToDate(new ReflectionProperty(DateClass::class, 'nullableDateTimeInterface')); + $cast->setOptions('2023-01-01', '!Y-m-d', 'Africa/Kinshasa'); $date = $cast->toVariable(null); self::assertInstanceOf(DateTimeImmutable::class, $date); @@ -111,7 +109,8 @@ public function testItCanConvertADateWithADefaultValue(): void public function testItReturnsTheValueWithUnionType(): void { - $cast = new CastToDate(new ReflectionProperty(DateClass::class, 'unionType'), '2023-01-01'); + $cast = new CastToDate(new ReflectionProperty(DateClass::class, 'unionType')); + $cast->setOptions('2023-01-01'); self::assertEquals(new DateTimeImmutable('2023-01-01'), $cast->toVariable(null)); } diff --git a/src/Serializer/CastToEnum.php b/src/Serializer/CastToEnum.php index 381f19c6..4612145c 100644 --- a/src/Serializer/CastToEnum.php +++ b/src/Serializer/CastToEnum.php @@ -25,32 +25,37 @@ */ class CastToEnum implements TypeCasting { - /** @var class-string */ - private readonly string $class; private readonly bool $isNullable; - private readonly ?UnitEnum $default; + private readonly Type $type; + private ?UnitEnum $default = null; + private readonly string $propertyName; + /** @var class-string */ + private string $class; + + /** + * @throws MappingFailed + */ + public function __construct(ReflectionProperty|ReflectionParameter $reflectionProperty) + { + [$this->type, $this->class, $this->isNullable] = $this->init($reflectionProperty); + $this->propertyName = $reflectionProperty->getName(); + } /** - * @param ?class-string $className + * @param ?class-string $className * * * @throws MappingFailed */ - public function __construct( - ReflectionProperty|ReflectionParameter $reflectionProperty, - ?string $default = null, - ?string $className = null, - ) { - [$type, $class, $this->isNullable] = $this->init($reflectionProperty); - if (Type::Mixed->equals($type) || in_array($class, [BackedEnum::class , UnitEnum::class], true)) { + public function setOptions(?string $default = null, ?string $className = null): void + { + if (Type::Mixed->equals($this->type) || in_array($this->class, [BackedEnum::class , UnitEnum::class], true)) { if (null === $className || !enum_exists($className)) { - throw new MappingFailed('`'.$reflectionProperty->getName().'` type is `'.($class ?? 'mixed').'` but the specified class via the `$className` argument is invalid or could not be found.'); + throw new MappingFailed('`'.$this->propertyName.'` type is `'.($this->class ?? 'mixed').'` but the specified class via the `$className` argument is invalid or could not be found.'); } - $class = $className; + $this->class = $className; } - $this->class = $class; - try { $this->default = (null !== $default) ? $this->cast($default) : $default; } catch (TypeCastingFailed $exception) { diff --git a/src/Serializer/CastToEnumTest.php b/src/Serializer/CastToEnumTest.php index 55436669..95694278 100644 --- a/src/Serializer/CastToEnumTest.php +++ b/src/Serializer/CastToEnumTest.php @@ -60,7 +60,8 @@ public function testItReturnsNullWhenTheVariableIsNullable(): void public function testItReturnsTheDefaultValueWhenTheVariableIsNullable(): void { - $cast = new CastToEnum(new ReflectionProperty(EnumClass::class, 'nullableCurrency'), 'Naira'); + $cast = new CastToEnum(new ReflectionProperty(EnumClass::class, 'nullableCurrency')); + $cast->setOptions('Naira'); self::assertSame(Currency::Naira, $cast->toVariable(null)); } @@ -81,7 +82,8 @@ public function testThrowsIfTheValueIsNotRecognizedByTheEnum(): void public function testItReturnsTheDefaultValueWithUnionType(): void { - $cast = new CastToEnum(new ReflectionProperty(EnumClass::class, 'unionType'), 'orange'); + $cast = new CastToEnum(new ReflectionProperty(EnumClass::class, 'unionType')); + $cast->setOptions('orange'); self::assertSame(Colour::Violet, $cast->toVariable('violet')); } diff --git a/src/Serializer/CastToFloat.php b/src/Serializer/CastToFloat.php index d470e774..b34a090f 100644 --- a/src/Serializer/CastToFloat.php +++ b/src/Serializer/CastToFloat.php @@ -24,14 +24,18 @@ final class CastToFloat implements TypeCasting { private readonly bool $isNullable; + private ?float $default = null; - public function __construct( - ReflectionProperty|ReflectionParameter $reflectionProperty, - private readonly ?float $default = null, - ) { + public function __construct(ReflectionProperty|ReflectionParameter $reflectionProperty) + { $this->isNullable = $this->init($reflectionProperty); } + public function setOptions(int|float|null $default = null): void + { + $this->default = $default; + } + /** * @throws TypeCastingFailed */ diff --git a/src/Serializer/CastToFloatTest.php b/src/Serializer/CastToFloatTest.php index 799fac79..f3fd0b09 100644 --- a/src/Serializer/CastToFloatTest.php +++ b/src/Serializer/CastToFloatTest.php @@ -30,7 +30,10 @@ public function testItFailsToInstantiateWithAnUnSupportedType(): void #[DataProvider('providesValidStringForInt')] public function testItCanConvertToArraygWithoutArguments(ReflectionProperty $prototype, ?string $input, ?float $default, ?float $expected): void { - self::assertSame($expected, (new CastToFloat(reflectionProperty: $prototype, default:$default))->toVariable($input)); + $cast = new CastToFloat($prototype); + $cast->setOptions($default); + + self::assertSame($expected, $cast->toVariable($input)); } public static function providesValidStringForInt(): iterable diff --git a/src/Serializer/CastToInt.php b/src/Serializer/CastToInt.php index 310bf307..31e690d8 100644 --- a/src/Serializer/CastToInt.php +++ b/src/Serializer/CastToInt.php @@ -24,14 +24,18 @@ final class CastToInt implements TypeCasting { private readonly bool $isNullable; + private ?int $default = null; - public function __construct( - ReflectionProperty|ReflectionParameter $reflectionProperty, - private readonly ?int $default = null, - ) { + public function __construct(ReflectionProperty|ReflectionParameter $reflectionProperty) + { $this->isNullable = $this->init($reflectionProperty); } + public function setOptions(?int $default = null): void + { + $this->default = $default; + } + /** * @throws TypeCastingFailed */ diff --git a/src/Serializer/CastToIntTest.php b/src/Serializer/CastToIntTest.php index 70661bea..7caaf487 100644 --- a/src/Serializer/CastToIntTest.php +++ b/src/Serializer/CastToIntTest.php @@ -32,7 +32,10 @@ public function testItFailsToInstantiateWithAnUnSupportedType(): void #[DataProvider('providesValidStringForInt')] public function testItCanConvertToArraygWithoutArguments(ReflectionProperty $reflectionProperty, ?string $input, ?int $default, ?int $expected): void { - self::assertSame($expected, (new CastToInt(reflectionProperty: $reflectionProperty, default:$default))->toVariable($input)); + $cast = new CastToInt($reflectionProperty); + $cast->setOptions($default); + + self::assertSame($expected, $cast->toVariable($input)); } public static function providesValidStringForInt(): iterable @@ -105,7 +108,7 @@ public function testItFailsToConvertNonIntegerString(): void { $this->expectException(TypeCastingFailed::class); - (new CastToInt(reflectionProperty: new ReflectionProperty(IntClass::class, 'nullableInt')))->toVariable('00foobar'); + (new CastToInt(new ReflectionProperty(IntClass::class, 'nullableInt')))->toVariable('00foobar'); } #[DataProvider('invalidPropertyName')] @@ -113,9 +116,7 @@ public function testItWillThrowIfNotTypeAreSupported(string $propertyName): void { $this->expectException(MappingFailed::class); - $reflectionProperty = new ReflectionProperty(IntClass::class, $propertyName); - - new CastToInt($reflectionProperty); + new CastToInt(new ReflectionProperty(IntClass::class, $propertyName)); } public static function invalidPropertyName(): iterable diff --git a/src/Serializer/CastToString.php b/src/Serializer/CastToString.php index a396f7d7..f818541a 100644 --- a/src/Serializer/CastToString.php +++ b/src/Serializer/CastToString.php @@ -23,14 +23,18 @@ final class CastToString implements TypeCasting { private readonly bool $isNullable; private readonly Type $type; + private ?string $default = null; - public function __construct( - ReflectionProperty|ReflectionParameter $reflectionProperty, - private readonly ?string $default = null - ) { + public function __construct(ReflectionProperty|ReflectionParameter $reflectionProperty) + { [$this->type, $this->isNullable] = $this->init($reflectionProperty); } + public function setOptions(?string $default = null): void + { + $this->default = $default; + } + /** * @throws TypeCastingFailed */ diff --git a/src/Serializer/CastToStringTest.php b/src/Serializer/CastToStringTest.php index b9e1b938..d7d8602e 100644 --- a/src/Serializer/CastToStringTest.php +++ b/src/Serializer/CastToStringTest.php @@ -36,7 +36,10 @@ public function testItCanConvertStringToBool( ?string $input, ?string $expected ): void { - self::assertSame($expected, (new CastToString($reflectionProperty, $default))->toVariable($input)); + $cast = new CastToString($reflectionProperty); + $cast->setOptions($default); + + self::assertSame($expected, $cast->toVariable($input)); } public static function providesValidInputValue(): iterable @@ -89,9 +92,7 @@ public function testItWillThrowIfNotTypeAreSupported(string $propertyName): void { $this->expectException(MappingFailed::class); - $reflectionProperty = new ReflectionProperty(StringClass::class, $propertyName); - - new CastToString($reflectionProperty); + new CastToString(new ReflectionProperty(StringClass::class, $propertyName)); } public static function invalidPropertyName(): iterable diff --git a/src/Serializer/ClosureCasting.php b/src/Serializer/ClosureCasting.php index 8c687bbc..d4cc21ac 100644 --- a/src/Serializer/ClosureCasting.php +++ b/src/Serializer/ClosureCasting.php @@ -29,27 +29,43 @@ */ final class ClosureCasting implements TypeCasting { - /** @var array */ + /** @var array */ private static array $casters = []; - private readonly string $type; + private string $type; private readonly bool $isNullable; - /** @var Closure(?string, bool, mixed...): TValue */ - private readonly Closure $closure; - private readonly array $arguments; + /** @var Closure(?string, bool, mixed...): mixed */ + private Closure $closure; + private array $options; + private string $message; - public function __construct(ReflectionProperty|ReflectionParameter $reflectionProperty, string $type = null, mixed ...$arguments) + public function __construct(ReflectionProperty|ReflectionParameter $reflectionProperty) { - [$propertyType, $this->isNullable] = self::resolve($reflectionProperty); - $this->type = Type::Mixed->value !== $propertyType ? $propertyType : ($type ?? $propertyType); + [$this->type, $this->isNullable] = self::resolve($reflectionProperty); + + $this->message = match (true) { + $reflectionProperty instanceof ReflectionParameter => 'The method `'.$reflectionProperty->getDeclaringClass()?->getName().'::'.$reflectionProperty->getDeclaringFunction()->getName().'` argument `'.$reflectionProperty->getName().'` must be typed with a supported type.', + $reflectionProperty instanceof ReflectionProperty => 'The property `'.$reflectionProperty->getDeclaringClass()->getName().'::'.$reflectionProperty->getName().'` must be typed with a supported type.', + }; + + $this->closure = fn (?string $value, bool $isNullable, mixed ...$arguments): ?string => $value; + } + + /** + * @throws MappingFailed + */ + public function setOptions(string $type = null, mixed ...$options): void + { + if (Type::Mixed->value === $this->type && null !== $type) { + $this->type = $type; + } + if (!array_key_exists($this->type, self::$casters)) { - throw new MappingFailed(match (true) { - $reflectionProperty instanceof ReflectionParameter => 'The method `'.$reflectionProperty->getDeclaringClass()?->getName().'::'.$reflectionProperty->getDeclaringFunction()->getName().'` argument `'.$reflectionProperty->getName().'` must be typed with a supported type.', - $reflectionProperty instanceof ReflectionProperty => 'The property `'.$reflectionProperty->getDeclaringClass()->getName().'::'.$reflectionProperty->getName().'` must be typed with a supported type.', - }); + throw new MappingFailed($this->message); } + $this->closure = self::$casters[$this->type]; - $this->arguments = $arguments; + $this->options = $options; } /** @@ -58,7 +74,7 @@ public function __construct(ReflectionProperty|ReflectionParameter $reflectionPr public function toVariable(?string $value): mixed { try { - return ($this->closure)($value, $this->isNullable, ...$this->arguments); + return ($this->closure)($value, $this->isNullable, ...$this->options); } catch (Throwable $exception) { if ($exception instanceof TypeCastingFailed) { throw $exception; diff --git a/src/Serializer/Denormalizer.php b/src/Serializer/Denormalizer.php index c0205686..5e2f133c 100644 --- a/src/Serializer/Denormalizer.php +++ b/src/Serializer/Denormalizer.php @@ -24,7 +24,6 @@ use ReflectionProperty; use Throwable; -use function array_key_exists; use function array_search; use function array_values; use function count; @@ -253,7 +252,7 @@ private function autoDiscoverPropertySetter(ReflectionMethod|ReflectionProperty default => new PropertySetter( $accessor, $offset, - $this->resolveTypeCasting($reflectionProperty, ['reflectionProperty' => $reflectionProperty]) + $this->resolveTypeCasting($reflectionProperty) ), }; } @@ -265,10 +264,6 @@ private function autoDiscoverPropertySetter(ReflectionMethod|ReflectionProperty */ private function findPropertySetter(MapCell $cell, ReflectionMethod|ReflectionProperty $accessor, array $propertyNames): PropertySetter { - if (array_key_exists('reflectionProperty', $cell->options)) { - throw MappingFailed::dueToForbiddenOptionName(); - } - /** @var ?class-string $typeCaster */ $typeCaster = $cell->cast; if (null !== $typeCaster && (!class_exists($typeCaster) || !(new ReflectionClass($typeCaster))->implementsInterface(TypeCasting::class))) { @@ -294,16 +289,16 @@ private function findPropertySetter(MapCell $cell, ReflectionMethod|ReflectionPr $offset = $index; } - $arguments = [...$cell->options, ...['reflectionProperty' => $reflectionProperty = match (true) { + $reflectionProperty = match (true) { $accessor instanceof ReflectionMethod => $accessor->getParameters()[0], $accessor instanceof ReflectionProperty => $accessor, - }]]; + }; 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, $arguments)), - default => new PropertySetter($accessor, $offset, $this->getTypeCasting($typeCaster, $arguments)), + null === $typeCaster => new PropertySetter($accessor, $offset, $this->resolveTypeCasting($reflectionProperty, $cell->options)), + default => new PropertySetter($accessor, $offset, $this->getTypeCasting($reflectionProperty, $typeCaster, $cell->options)), }; } @@ -326,11 +321,17 @@ private function getMethodFirstArgument(ReflectionMethod $reflectionMethod): Ref * * @throws MappingFailed */ - private function getTypeCasting(string $typeCaster, array $arguments): TypeCasting - { + private function getTypeCasting( + ReflectionProperty|ReflectionParameter $reflectionProperty, + string $typeCaster, + array $options + ): TypeCasting { try { - return new $typeCaster(...$arguments); - } catch (Throwable $exception) { /* @phpstan-ignore-line */ + $cast = new $typeCaster($reflectionProperty); + $cast->setOptions(...$options); + + return $cast; + } catch (Throwable $exception) { throw $exception instanceof MappingFailed ? $exception : MappingFailed::dueToInvalidCastingArguments($exception); } } @@ -338,12 +339,19 @@ private function getTypeCasting(string $typeCaster, array $arguments): TypeCasti /** * @throws MappingFailed */ - private function resolveTypeCasting(ReflectionProperty|ReflectionParameter $reflectionProperty, array $arguments): TypeCasting + private function resolveTypeCasting(ReflectionProperty|ReflectionParameter $reflectionProperty, array $options = []): TypeCasting { + $castResolver = function (ReflectionProperty|ReflectionParameter $reflectionProperty, $options): TypeCasting { + $cast = new ClosureCasting($reflectionProperty); + $cast->setOptions(...$options); + + return $cast; + }; + try { return match (true) { - ClosureCasting::supports($reflectionProperty) => new ClosureCasting(...$arguments), - default => Type::resolve($reflectionProperty, $arguments), + ClosureCasting::supports($reflectionProperty) => $castResolver($reflectionProperty, $options), + default => Type::resolve($reflectionProperty, $options), }; } catch (MappingFailed $exception) { throw $exception; diff --git a/src/Serializer/DenormalizerTest.php b/src/Serializer/DenormalizerTest.php index af88e89f..1a4557f1 100644 --- a/src/Serializer/DenormalizerTest.php +++ b/src/Serializer/DenormalizerTest.php @@ -287,7 +287,7 @@ public function __construct( }; $this->expectException(MappingFailed::class); - $this->expectExceptionMessage('Using more than one `League\Csv\Serializer\Cell` attribute on a class property or method is not supported.'); + $this->expectExceptionMessage('Using more than one `'.MapCell::class.'` attribute on a class property or method is not supported.'); new Denormalizer($class::class); } diff --git a/src/Serializer/Type.php b/src/Serializer/Type.php index f6df3142..0b54b950 100644 --- a/src/Serializer/Type.php +++ b/src/Serializer/Type.php @@ -70,19 +70,21 @@ public function filterFlag(): int public static function resolve(ReflectionProperty|ReflectionParameter $reflectionProperty, array $arguments = []): TypeCasting { - $arguments['reflectionProperty'] = $reflectionProperty; - try { - return match (self::tryFromAccessor($reflectionProperty)) { - self::Mixed, self::Null, self::String => new CastToString(...$arguments), - self::Iterable, self::Array => new CastToArray(...$arguments), - self::False, self::True, self::Bool => new CastToBool(...$arguments), - self::Float => new CastToFloat(...$arguments), - self::Int => new CastToInt(...$arguments), - self::Date => new CastToDate(...$arguments), - self::Enum => new CastToEnum(...$arguments), + $cast = match (self::tryFromAccessor($reflectionProperty)) { + self::Mixed, self::Null, self::String => new CastToString($reflectionProperty), + self::Iterable, self::Array => new CastToArray($reflectionProperty), + self::False, self::True, self::Bool => new CastToBool($reflectionProperty), + self::Float => new CastToFloat($reflectionProperty), + self::Int => new CastToInt($reflectionProperty), + self::Date => new CastToDate($reflectionProperty), + self::Enum => new CastToEnum($reflectionProperty), null => throw MappingFailed::dueToUnsupportedType($reflectionProperty), }; + + $cast->setOptions(...$arguments); + + return $cast; } catch (MappingFailed $exception) { throw $exception; } catch (Throwable $exception) { diff --git a/src/Serializer/TypeCasting.php b/src/Serializer/TypeCasting.php index d5ad1e77..ec609d73 100644 --- a/src/Serializer/TypeCasting.php +++ b/src/Serializer/TypeCasting.php @@ -24,4 +24,13 @@ interface TypeCasting * @return TValue */ public function toVariable(?string $value): mixed; + + /** + * Accepts additional parameters to configure the class + * Parameters should be scalar value, null or array containing + * only scalar value and null. + * + * @throws MappingFailed + */ + public function setOptions(): void; }