Skip to content

Commit

Permalink
Adding TypeCasting::setOptions method
Browse files Browse the repository at this point in the history
  • Loading branch information
nyamsprod committed Nov 24, 2023
1 parent 4116eda commit 37d5183
Show file tree
Hide file tree
Showing 20 changed files with 245 additions and 139 deletions.
19 changes: 12 additions & 7 deletions docs/9.0/reader/record-mapping.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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 {
Expand Down
48 changes: 34 additions & 14 deletions src/Serializer/CastToArray.php
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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.'),
Expand Down
18 changes: 11 additions & 7 deletions src/Serializer/CastToArrayTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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')]
Expand Down
12 changes: 8 additions & 4 deletions src/Serializer/CastToBool.php
Original file line number Diff line number Diff line change
Expand Up @@ -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
*/
Expand Down
5 changes: 4 additions & 1 deletion src/Serializer/CastToBoolTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
36 changes: 24 additions & 12 deletions src/Serializer/CastToDate.php
Original file line number Diff line number Diff line change
Expand Up @@ -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) {
Expand Down
25 changes: 12 additions & 13 deletions src/Serializer/CastToDateTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand All @@ -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);
Expand All @@ -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');
}

Expand All @@ -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
Expand All @@ -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);
Expand All @@ -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));
}
Expand Down
35 changes: 20 additions & 15 deletions src/Serializer/CastToEnum.php
Original file line number Diff line number Diff line change
Expand Up @@ -25,32 +25,37 @@
*/
class CastToEnum implements TypeCasting
{
/** @var class-string<UnitEnum|BackedEnum> */
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<UnitEnum|BackedEnum> */
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<UnitEnum|BackedEnum> $className
* @param ?class-string<UnitEnum|BackedEnum> $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) {
Expand Down
6 changes: 4 additions & 2 deletions src/Serializer/CastToEnumTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -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));
}
Expand All @@ -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'));
}
Expand Down
Loading

0 comments on commit 37d5183

Please sign in to comment.