Skip to content

Commit e265ca3

Browse files
committed
Adding support for Closure in constant expressions
1 parent 7405032 commit e265ca3

4 files changed

Lines changed: 56 additions & 10 deletions

File tree

docs/9.0/reader/record-mapping.md

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -713,6 +713,35 @@ Serializer\Denormalizer::unregisterAllAliases();
713713

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

716+
#### Register a callback directly
717+
718+
<p class="message-info">new in version <code>9.29</code></p>
719+
720+
You can directly register a callback if you are using `PHP8.5+` prior to this version the code will throw an error since using
721+
a static closure on attribute is not yet valid.
722+
723+
Once your code is migrated to the correct PHP version you can write the following:
724+
725+
```php
726+
use App\Domain\Money
727+
use League\Csv\Serializer;
728+
729+
#[Serializer\MapCell(
730+
column: 'amount',
731+
cast: static function (mixed $value): int {
732+
return 42;
733+
}
734+
)]
735+
private ?int $amount;
736+
```
737+
738+
This will result in the same mapping as the previous example using the alias. But you no longer have to register the `alias` yourself as
739+
this is taking care of by the engine itself.
740+
741+
<p class="message-notice">Of note, if you want to re-use multiple time registering the callback casting using an
742+
explicit <code>alias</code> will always be the recommended way.</p>
743+
744+
716745
### Implementing a TypeCasting class
717746

718747
If you need to support `Intersection` type you need to provide your own class to typecast the value according

src/Serializer/CallbackCasting.php

Lines changed: 9 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -49,7 +49,7 @@ public function __construct(
4949
ReflectionProperty|ReflectionParameter $reflectionProperty,
5050
private readonly ?string $alias = null
5151
) {
52-
[$this->type, $this->isNullable] = self::resolve($reflectionProperty);
52+
[$this->type, $this->isNullable] = self::resolve($reflectionProperty, $alias);
5353

5454
$this->message = match (true) {
5555
$reflectionProperty instanceof ReflectionParameter => 'The method `'.$reflectionProperty->getDeclaringClass()?->getName().'::'.$reflectionProperty->getDeclaringFunction()->getName().'` argument `'.$reflectionProperty->getName().'` must be typed with a supported type.',
@@ -251,7 +251,11 @@ public static function supports(ReflectionParameter|ReflectionProperty $reflecti
251251
continue;
252252
}
253253

254-
if (self::aliasSupportsType($type) || (Type::Mixed->value === $type && self::supportsAlias($alias))) {
254+
if (
255+
self::aliasSupportsType($type) ||
256+
(Type::Mixed->value === $type && self::supportsAlias($alias))
257+
|| isset(self::$aliases[Type::Mixed->value][$alias])
258+
) {
255259
return true;
256260
}
257261
}
@@ -324,7 +328,7 @@ private static function resolveAliasCallback(string $type, string $alias): Closu
324328
*
325329
* @return array{0:string, 1:bool}
326330
*/
327-
private static function resolve(ReflectionParameter|ReflectionProperty $reflectionProperty): array
331+
private static function resolve(ReflectionParameter|ReflectionProperty $reflectionProperty, ?string $alias = null): array
328332
{
329333
if (null === $reflectionProperty->getType()) {
330334
return [Type::Mixed->value, true];
@@ -334,7 +338,8 @@ private static function resolve(ReflectionParameter|ReflectionProperty $reflecti
334338

335339
$type = null;
336340
$isNullable = false;
337-
$hasMixed = false;
341+
$hasMixed = null !== $alias && isset(self::$aliases[Type::Mixed->value][$alias]);
342+
338343
foreach ($types as $foundType) {
339344
if (!$isNullable && $foundType->allowsNull()) {
340345
$isNullable = true;

src/Serializer/Denormalizer.php

Lines changed: 15 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,7 @@
2929
use function array_values;
3030
use function count;
3131
use function is_int;
32+
use function str_replace;
3233

3334
final class Denormalizer
3435
{
@@ -434,12 +435,24 @@ private function resolveTypeCasting(ReflectionProperty|ReflectionParameter $refl
434435

435436
public function resolveTypeCaster(MapCell $mapCell, ReflectionMethod|ReflectionProperty $accessor): ?string
436437
{
437-
/** @var ?class-string<TypeCasting> $typeCaster */
438+
/** @var class-string<TypeCasting>|Closure|null $typeCaster */
438439
$typeCaster = $mapCell->cast;
439440
if (null === $typeCaster) {
440441
return null;
441442
}
442443

444+
if ($typeCaster instanceof Closure) {
445+
$name = $accessor->getName();
446+
if ($accessor instanceof ReflectionMethod) {
447+
$name .= '_'.$accessor->getParameters()[0]->getName();
448+
}
449+
450+
$alias = '@_league_csv_generated_'.str_replace('\\', '_', $this->class->getName()).'__'.$name;
451+
Denormalizer::registerAlias($alias, Type::Mixed->value, $typeCaster);
452+
453+
return CallbackCasting::class.$alias;
454+
}
455+
443456
if (class_exists($typeCaster)) {
444457
if (!(new ReflectionClass($typeCaster))->implementsInterface(TypeCasting::class)) {
445458
throw MappingFailed::dueToInvalidTypeCastingClass($typeCaster);
@@ -452,9 +465,7 @@ public function resolveTypeCaster(MapCell $mapCell, ReflectionMethod|ReflectionP
452465
$accessor = $accessor->getParameters()[0];
453466
}
454467

455-
if (!CallbackCasting::supports($accessor, $typeCaster)) {
456-
throw MappingFailed::dueToInvalidTypeCastingClass($typeCaster);
457-
}
468+
CallbackCasting::supports($accessor, $typeCaster) || throw MappingFailed::dueToInvalidTypeCastingClass($typeCaster);
458469

459470
return CallbackCasting::class.$typeCaster;
460471
}

src/Serializer/MapCell.php

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -14,16 +14,17 @@
1414
namespace League\Csv\Serializer;
1515

1616
use Attribute;
17+
use Closure;
1718

1819
#[Attribute(Attribute::TARGET_METHOD | Attribute::TARGET_PROPERTY)]
1920
final readonly class MapCell
2021
{
2122
/**
22-
* @param class-string|string|null $cast
23+
* @param Closure|class-string|string|null $cast
2324
*/
2425
public function __construct(
2526
public string|int|null $column = null,
26-
public ?string $cast = null,
27+
public Closure|string|null $cast = null,
2728
public array $options = [],
2829
public bool $ignore = false,
2930
public ?bool $convertEmptyStringToNull = null,

0 commit comments

Comments
 (0)