Skip to content

Commit 1895c59

Browse files
committed
[ObjectMapper] Auto backed Enum conversion
1 parent 68c6f2d commit 1895c59

File tree

11 files changed

+359
-4
lines changed

11 files changed

+359
-4
lines changed

src/Symfony/Component/ObjectMapper/CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ CHANGELOG
77
* The component is not marked as `@experimental` anymore
88
* Add `ObjectMapperAwareInterface` to set the owning object mapper instance
99
* Add a `MapCollection` transform that calls the Mapper over iterable properties
10+
* Automatic backed Enum to scalar conversion
1011

1112
7.3
1213
---

src/Symfony/Component/ObjectMapper/ObjectMapper.php

Lines changed: 113 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -155,7 +155,7 @@ private function doMap(object $source, object|string|null $target, \WeakMap $obj
155155
}
156156

157157
$value = $this->getSourceValue($source, $mappedTarget, $value, $objectMap, $mapping);
158-
$this->storeValue($targetPropertyName, $mapToProperties, $ctorArguments, $value);
158+
$this->storeValue($targetPropertyName, $mapToProperties, $ctorArguments, $value, $targetRefl, $sourcePropertyName);
159159
}
160160

161161
if (!$mappings && $targetRefl->hasProperty($propertyName)) {
@@ -165,7 +165,7 @@ private function doMap(object $source, object|string|null $target, \WeakMap $obj
165165
}
166166

167167
$value = $this->getSourceValue($source, $mappedTarget, $this->getRawValue($source, $propertyName), $objectMap);
168-
$this->storeValue($propertyName, $mapToProperties, $ctorArguments, $value);
168+
$this->storeValue($propertyName, $mapToProperties, $ctorArguments, $value, $targetRefl, $propertyName);
169169
}
170170
}
171171

@@ -230,6 +230,7 @@ private function getSourceValue(object $source, object $target, mixed $value, \W
230230

231231
if (
232232
\is_object($value)
233+
&& !($value instanceof \UnitEnum)
233234
&& ($innerMetadata = $this->metadataFactory->create($value))
234235
&& ($mapTo = $this->getMapTarget($innerMetadata, $value, $source, $target))
235236
&& (\is_string($mapTo->target) && class_exists($mapTo->target))
@@ -267,8 +268,13 @@ private function getSourceValue(object $source, object $target, mixed $value, \W
267268
* @param array<string, mixed> $mapToProperties
268269
* @param array<string, mixed> $ctorArguments
269270
*/
270-
private function storeValue(string $propertyName, array &$mapToProperties, array &$ctorArguments, mixed $value): void
271+
private function storeValue(string $propertyName, array &$mapToProperties, array &$ctorArguments, mixed $value, \ReflectionClass $targetRefl, ?string $sourcePropertyName = null): void
271272
{
273+
if ($this->needsEnumConversion($value, $targetRefl, $propertyName)) {
274+
$property = $targetRefl->getProperty($propertyName);
275+
$value = $this->convertEnumValue($value, $property);
276+
}
277+
272278
if (\array_key_exists($propertyName, $ctorArguments)) {
273279
$ctorArguments[$propertyName] = $value;
274280

@@ -387,4 +393,108 @@ public function withObjectMapper(ObjectMapperInterface $objectMapper): static
387393

388394
return $clone;
389395
}
396+
397+
private function needsEnumConversion(mixed $value, \ReflectionClass $targetRefl, string $propertyName): bool
398+
{
399+
if ($value instanceof \UnitEnum && !$value instanceof \BackedEnum) {
400+
$property = $targetRefl->getProperty($propertyName);
401+
$targetType = $property->getType();
402+
403+
if ($targetType instanceof \ReflectionNamedType && \in_array($targetType->getName(), ['string', 'int'], true)) {
404+
throw new MappingTransformException(\sprintf('Cannot map pure enum "%s" to scalar type "%s" on property "%s". Only BackedEnum can be converted to scalar values.', $value::class, $targetType->getName(), $propertyName));
405+
}
406+
407+
return false;
408+
}
409+
410+
if (!($value instanceof \BackedEnum || \is_int($value) || \is_string($value))) {
411+
return false;
412+
}
413+
414+
$property = $targetRefl->getProperty($propertyName);
415+
$targetType = $property->getType();
416+
417+
if (!$targetType instanceof \ReflectionNamedType) {
418+
return false;
419+
}
420+
421+
$targetTypeName = $targetType->getName();
422+
423+
if (enum_exists($targetTypeName)) {
424+
return true;
425+
}
426+
427+
if ($value instanceof \BackedEnum && \in_array($targetTypeName, ['string', 'int'], true)) {
428+
return true;
429+
}
430+
431+
return false;
432+
}
433+
434+
/**
435+
* @return int|string|\BackedEnum the target-expected value
436+
*/
437+
private function convertEnumValue(mixed $sourceValue, \ReflectionProperty $targetProperty): int|string|\BackedEnum
438+
{
439+
$targetType = $targetProperty->getType();
440+
$targetEnumTypeName = $targetType instanceof \ReflectionNamedType ? $targetType->getName() : (string) $targetType;
441+
442+
if ($sourceValue instanceof \BackedEnum && \in_array($targetEnumTypeName, ['string', 'int'], true)) {
443+
return $this->convertFromBackedEnum($sourceValue, $targetEnumTypeName);
444+
}
445+
446+
if (is_a($targetEnumTypeName, \BackedEnum::class, true) && !\is_object($sourceValue)) {
447+
return $this->convertToBackedEnum($sourceValue, $targetEnumTypeName);
448+
}
449+
450+
if (is_a($targetEnumTypeName, \UnitEnum::class, true) && !is_a($targetEnumTypeName, \BackedEnum::class, true) && !\is_object($sourceValue)) {
451+
throw new MappingTransformException(\sprintf('Cannot map "%s" to "%s" on property "%s". Pure enums cannot be mapped from scalar values.', get_debug_type($sourceValue), $targetEnumTypeName, $targetProperty->getName()));
452+
}
453+
454+
return $sourceValue;
455+
}
456+
457+
/**
458+
* @template T of \BackedEnum
459+
*
460+
* @param class-string<T> $targetEnumClass
461+
*
462+
* @return T
463+
*/
464+
private function convertToBackedEnum(mixed $value, string $targetEnumClass): \BackedEnum
465+
{
466+
if ($value instanceof $targetEnumClass) {
467+
return $value;
468+
}
469+
470+
$targetBackingType = (new \ReflectionEnum($targetEnumClass))->getBackingType();
471+
472+
/** @var \ReflectionNamedType $targetBackingType */
473+
$expectedType = $targetBackingType->getName();
474+
$actualType = get_debug_type($value);
475+
476+
if ($expectedType !== $actualType) {
477+
throw new MappingTransformException(\sprintf('Cannot convert value of type "%s" to "%s"-backed enum "%s".', $actualType, $expectedType, $targetEnumClass));
478+
}
479+
480+
try {
481+
return $targetEnumClass::from($value);
482+
} catch (\ValueError $e) {
483+
throw new MappingTransformException(\sprintf('Invalid value "%s" for enum "%s": "%s"', $value, $targetEnumClass, $e->getMessage()), 0, $e);
484+
}
485+
}
486+
487+
/**
488+
* @param 'int'|'string' $targetType
489+
*/
490+
private function convertFromBackedEnum(\BackedEnum $enum, string $targetType): int|string
491+
{
492+
$backingType = get_debug_type($enum->value);
493+
494+
if ($backingType !== $targetType) {
495+
throw new MappingTransformException(\sprintf('Cannot convert "%s" backed enum "%s" to "%s".', $backingType, $enum::class, $targetType));
496+
}
497+
498+
return $enum->value;
499+
}
390500
}
Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
<?php
2+
3+
4+
namespace Symfony\Component\ObjectMapper\Tests\Fixtures\EnumMapping;
5+
6+
class DtoWithIntBackedEnum
7+
{
8+
public function __construct(
9+
public Priority $property,
10+
) {
11+
}
12+
}
Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
<?php
2+
3+
4+
namespace Symfony\Component\ObjectMapper\Tests\Fixtures\EnumMapping;
5+
class DtoWithIntProperty
6+
{
7+
public function __construct(
8+
public int $property,
9+
) {
10+
}
11+
}
Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
<?php
2+
3+
4+
namespace Symfony\Component\ObjectMapper\Tests\Fixtures\EnumMapping;
5+
6+
class DtoWithPureEnum
7+
{
8+
public function __construct(
9+
public Role $property,
10+
) {
11+
}
12+
}
Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
<?php
2+
3+
4+
namespace Symfony\Component\ObjectMapper\Tests\Fixtures\EnumMapping;
5+
6+
class DtoWithStringBackedEnum
7+
{
8+
public function __construct(
9+
public Status $property,
10+
) {
11+
}
12+
}
Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
<?php
2+
3+
4+
namespace Symfony\Component\ObjectMapper\Tests\Fixtures\EnumMapping;
5+
6+
class DtoWithStringProperty
7+
{
8+
public function __construct(
9+
public string $property,
10+
) {
11+
}
12+
}
Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
<?php
2+
3+
4+
namespace Symfony\Component\ObjectMapper\Tests\Fixtures\EnumMapping;
5+
6+
enum Priority: int
7+
{
8+
case Low = 1;
9+
case Medium = 2;
10+
case High = 3;
11+
}
Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
<?php
2+
3+
4+
namespace Symfony\Component\ObjectMapper\Tests\Fixtures\EnumMapping;
5+
6+
enum Role
7+
{
8+
case Admin;
9+
case User;
10+
case Guest;
11+
}
Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
<?php
2+
3+
4+
namespace Symfony\Component\ObjectMapper\Tests\Fixtures\EnumMapping;
5+
6+
enum Status: string
7+
{
8+
case IsActive = 'active';
9+
case IsInactive = 'inactive';
10+
case IsPending = 'pending';
11+
}

0 commit comments

Comments
 (0)