Skip to content

Commit

Permalink
Improve alias usage with dernormalization
Browse files Browse the repository at this point in the history
  • Loading branch information
nyamsprod committed Dec 6, 2023
1 parent b2d9063 commit a8fa559
Show file tree
Hide file tree
Showing 5 changed files with 130 additions and 51 deletions.
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ All Notable changes to `Csv` will be documented in this file

- `SwapDelimiter` stream filter to allow working with multibyte CSV delimiter
- `League\Csv\Serializer\AfterMapping` to work around the limitation aroud constructor usage.
- `Denormalizer` can register type alias to simplify callback usage.

### Deprecated

Expand Down
58 changes: 50 additions & 8 deletions docs/9.0/reader/record-mapping.md
Original file line number Diff line number Diff line change
Expand Up @@ -380,13 +380,14 @@ The `type` option only supports scalar type (`string`, `int`, `float` and `bool`

## Extending Type Casting capabilities

Two (2) mechanisms to extend typecasting are provided. You can register a callback via the `Denormalizer` class
Three (3) mechanisms to extend typecasting are provided. You can register a callback via the `Denormalizer` class
or create a `League\Csv\Serializer\TypeCasting` implementing class. Of course, the choice will depend on your use case.

### Registering a callback
### Registering a type using a callback

You can register a callback using the `Denormalizer` class to convert a specific type. The type can be
any built-in type or a specific class.
any built-in type or a specific class. Once registered, the type will be automatically resolved using your
callback even during autodiscovery.

```php
use App\Domain\Money\Naira;
Expand Down Expand Up @@ -461,10 +462,52 @@ The three (3) methods are static.

<p class="message-notice">the callback mechanism does not support <code>IntersectionType</code></p>

### Registering a type alias using a callback

<p class="message-info">new in version <code>9.13.0</code></p>

If you want to provide alternative way to convert your string into a specific type you can instead register an alias.
Contrary to registering a type an alias :

- is not available during autodiscovery and needs to be specified using the `MapCell` attribute `cast` argument.
- does not take precedence over a type definition.

Registering an alias is similar to registering a type via callback:

```php
use League\Csv\Serializer;

Serializer\Denormalizer::registerAlias('@forty-two', 'int', fn (?string $value): int => 42);
```

The excepted callback argument follow the same signature and will be called exactly the same as with a type callback.

<p class="message-notice">The alias must start with an <code>@</code> character and contain alphanumeric (letters, numbers, regardless of case) plus underscore (_).</p>

Once generated you can use it as shown below:

```php
use App\Domain\Money
use League\Csv\Serializer;

#[Serializer\MapCell(column: 'amount', cast: '@forty-two')]
private ?int $amount;
```

It is possible to unregister aliases using the following static methods:

```php
use League\Csv\Serializer;

Serializer\Denormalizer::unregisterAlias('@forty-two');
Serializer\Denormalizer::unregisterAllAliases();
```

<p class="message-info">If needed, can use the <code>Denormalizer::unregisterAll</code> to unregister all callbacks (alias and types)</p>

### Implementing a TypeCasting class

If you need to support `Intersection` type or properties/argument without type, or you want to be
able to fine tune the typecasting you can provide your own class to typecast the value according
If you need to support `Intersection` type you need to 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 `MapCell` attribute `cast` argument
Expand All @@ -484,7 +527,6 @@ private ?Money $naira;

The `CastToNaira` will convert the cell value into a `Narai` object and if the value is `null`, `20_00` will be used.
To allow your object to cast the cell value to your liking it needs to implement the `TypeCasting` interface.
To do so, you must define a `toVariable` method that will return the correct value once converted.

```php
<?php
Expand Down Expand Up @@ -531,13 +573,13 @@ final class CastToNaira implements TypeCasting
implementing class can support them via inspection of the <code>$reflectionProperty</code> argument.</p>

<p class="message-notice">Don't hesitate to check the repository code source to see how each default
<code>TypeCasting</code> classes are implemented.</p>
<code>TypeCasting</code> classes are implemented for reference.</p>

## Using the feature without a TabularDataReader

The feature can be used outside the package default usage via the `Denormalizer` class.

The class exposes four (4) methods to ease `array` to `object` conversion:
The class exposes four (4) methods to ease `array` to `object` denormalization:

- `Denormalizer::denormalizeAll` and `Denormalizer::assignAll` to convert a collection of records into a collection of instances of a specified class.
- `Denormalizer::denormalize` and `Denormalizer::assign` to convert a single record into a new instance of the specified class.
Expand Down
80 changes: 49 additions & 31 deletions src/Serializer/CallbackCasting.php
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,7 @@
final class CallbackCasting implements TypeCasting
{
/** @var array<string, Closure(?string, bool, mixed...): mixed> */
private static array $casters = [];
private static array $types = [];

/** @var array<string, array<string, Closure(?string, bool, mixed...): mixed>> */
private static array $aliases = [];
Expand Down Expand Up @@ -66,8 +66,8 @@ public function setOptions(string $type = null, mixed ...$options): void
$this->type = $type;
}

if (array_key_exists($this->type, self::$casters)) {
$this->callback = self::$casters[$this->type];
if (array_key_exists($this->type, self::$types)) {
$this->callback = self::$types[$this->type];
$this->options = $options;

return;
Expand Down Expand Up @@ -115,7 +115,7 @@ public function toVariable(?string $value): mixed
public static function register(string $type, Closure $callback, string $alias = null): void
{
if (null === $alias) {
self::$casters[$type] = match (true) {
self::$types[$type] = match (true) {
class_exists($type),
interface_exists($type),
Type::tryFrom($type) instanceof Type => $callback,
Expand Down Expand Up @@ -145,48 +145,49 @@ interface_exists($type),
};
}

public static function unregister(string $type, string $alias = null): bool
public static function unregisterType(string $type): bool
{
if (null !== $alias) {
$callback = self::$aliases[$type][$alias] ?? null;
if (null === $callback) {
return false;
}

unset(self::$aliases[$type][$alias]);

return true;
}

if (!array_key_exists($type, self::$casters)) {
if (!array_key_exists($type, self::$types)) {
return false;
}

unset(self::$casters[$type]);
unset(self::$types[$type]);

return true;
}

public static function unregisterAliases(string $type): bool
public static function unregisterTypes(): void
{
self::$types = [];
}

public static function unregisterAlias(string $alias): bool
{
if (!array_key_exists($type, self::$aliases)) {
if (1 !== preg_match('/^@\w+$/', $alias)) {
return false;
}

unset(self::$aliases[$type]);
foreach (self::$aliases as $type => $aliases) {
foreach ($aliases as $registeredAlias => $__) {
if ($registeredAlias === $alias) {
unset(self::$aliases[$type][$registeredAlias]);

return true;
return true;
}
}
}

return false;
}

public static function unregisterAll(string $type = null): void
public static function unregisterAliases(): void
{
if (null !== $type) {
unset(self::$aliases[$type], self::$casters[$type]);

return;
}
self::$aliases = [];
}

self::$casters = [];
public static function unregisterAll(): void
{
self::$types = [];
self::$aliases = [];
}

Expand All @@ -195,6 +196,11 @@ public static function supportsAlias(string $alias): bool
return array_key_exists($alias, self::aliases());
}

public static function supportsType(string $type): bool
{
return array_key_exists($type, self::$types);
}

/**
* @return array<string, string>
*/
Expand All @@ -215,7 +221,7 @@ public static function supports(ReflectionParameter|ReflectionProperty $reflecti
foreach (self::getTypes($reflectionProperty->getType()) as $propertyType) {
$type = $propertyType->getName();
if (null === $alias) {
if (array_key_exists($type, self::$casters)) {
if (array_key_exists($type, self::$types)) {
return true;
}

Expand Down Expand Up @@ -249,7 +255,7 @@ private static function resolve(ReflectionParameter|ReflectionProperty $reflecti

if (null === $type) {
if (
array_key_exists($foundType->getName(), self::$casters)
array_key_exists($foundType->getName(), self::$types)
|| array_key_exists($foundType->getName(), self::$aliases)
) {
$type = $foundType;
Expand Down Expand Up @@ -285,4 +291,16 @@ private static function getTypes(?ReflectionType $type): array
default => [],
};
}

/**
* DEPRECATION WARNING! This method will be removed in the next major point release.
*
* @deprecated since version 9.13.0
* @see CallbackCasting::unregisterType()
* @codeCoverageIgnore
*/
public static function unregister(string $type): bool
{
return self::unregisterType($type);
}
}
30 changes: 24 additions & 6 deletions src/Serializer/Denormalizer.php
Original file line number Diff line number Diff line change
Expand Up @@ -68,22 +68,40 @@ public static function disallowEmptyStringAsNull(): void
/**
* @throws MappingFailed
*/
public static function registerType(string $type, Closure $callback, string $alias = null): void
public static function registerType(string $type, Closure $callback): void
{
CallbackCasting::register($type, $callback);
}

public static function unregisterType(string $type): bool
{
return CallbackCasting::unregisterType($type);
}

public static function unregisterAllTypes(): void
{
CallbackCasting::unregisterTypes();
}

/**
* @throws MappingFailed
*/
public static function registerAlias(string $alias, string $type, Closure $callback): void
{
CallbackCasting::register($type, $callback, $alias);
}

public static function unregisterType(string $type, string $alias = null): bool
public static function unregisterAlias(string $alias): bool
{
return CallbackCasting::unregister($type, $alias);
return CallbackCasting::unregisterAlias($alias);
}

public static function unregisterTypeAliases(string $type): void
public static function unregisterAllAliases(): void
{
CallbackCasting::unregisterAliases($type);
CallbackCasting::unregisterAliases();
}

public static function unregisterAllTypes(): void
public static function unregisterAll(): void
{
CallbackCasting::unregisterAll();
}
Expand Down
12 changes: 6 additions & 6 deletions src/Serializer/DenormalizerTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -559,7 +559,7 @@ public function it_can_use_aliases(): void
self::assertSame([], Denormalizer::aliases());
self::assertFalse(Denormalizer::supportsAlias('@strtoupper'));

Denormalizer::registerType('string', fn (?string $str) => null === $str ? '' : strtoupper($str), '@strtoupper');
Denormalizer::registerAlias('@strtoupper', 'string', fn (?string $str) => null === $str ? '' : strtoupper($str));

self::assertSame(['@strtoupper' => 'string'], Denormalizer::aliases());
self::assertTrue(Denormalizer::supportsAlias('@strtoupper'));
Expand All @@ -577,8 +577,8 @@ public function __construct(
self::assertInstanceOf($class::class, $instance);
self::assertSame('KINSHASA', $instance->str);

self::assertTrue(Denormalizer::unregisterType('string', '@strtoupper'));
self::assertFalse(Denormalizer::unregisterType('string', '@strtoupper'));
self::assertTrue(Denormalizer::unregisterAlias('@strtoupper'));
self::assertFalse(Denormalizer::unregisterAlias('@strtoupper'));

$this->expectException(MappingFailed::class);
$this->expectExceptionMessage('`@strtoupper` must be an resolvable class implementing the `'.TypeCasting::class.'` interface or a supported alias.');
Expand All @@ -593,7 +593,7 @@ public function it_will_fail_to_registered_an_invalid_alias_name(): void
$this->expectException(MappingFailed::class);
$this->expectExceptionMessage("The alias `$invalidAlias` is invalid. It must start with an `@` character and contain alphanumeric (letters, numbers, regardless of case) plus underscore (_).");

Denormalizer::registerType('string', fn (?string $str) => null === $str ? '' : strtoupper($str), $invalidAlias);
Denormalizer::registerAlias($invalidAlias, 'string', fn (?string $str) => null === $str ? '' : strtoupper($str));
}

#[Test]
Expand All @@ -604,8 +604,8 @@ public function it_will_fail_to_registered_twice_the_same_alias(): void
$this->expectException(MappingFailed::class);
$this->expectExceptionMessage('The alias `'.$validAlias.'` is already registered. Please choose another name.');

Denormalizer::registerType('string', fn (?string $str) => null === $str ? '' : strtoupper($str), $validAlias);
Denormalizer::registerType('int', fn (?string $str) => null === $str ? '' : strtoupper($str), $validAlias);
Denormalizer::registerAlias($validAlias, 'string', fn (?string $str) => null === $str ? '' : strtoupper($str));
Denormalizer::registerAlias($validAlias, 'int', fn (?string $str) => null === $str ? '' : strtoupper($str));
}
}

Expand Down

0 comments on commit a8fa559

Please sign in to comment.