Skip to content

Commit cecf816

Browse files
committed
[ObjectMapper] Auto backed Enum conversion
1 parent 96b8d34 commit cecf816

File tree

10 files changed

+206
-3
lines changed

10 files changed

+206
-3
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))
@@ -265,8 +266,13 @@ private function getSourceValue(object $source, object $target, mixed $value, \W
265266
* @param array<string, mixed> $mapToProperties
266267
* @param array<string, mixed> $ctorArguments
267268
*/
268-
private function storeValue(string $propertyName, array &$mapToProperties, array &$ctorArguments, mixed $value): void
269+
private function storeValue(string $propertyName, array &$mapToProperties, array &$ctorArguments, mixed $value, \ReflectionClass $targetRefl, ?string $sourcePropertyName = null): void
269270
{
271+
if ($this->needsEnumConversion($value, $targetRefl, $propertyName)) {
272+
$property = $targetRefl->getProperty($propertyName);
273+
$value = $this->convertEnumValue($value, $property);
274+
}
275+
270276
if (\array_key_exists($propertyName, $ctorArguments)) {
271277
$ctorArguments[$propertyName] = $value;
272278

@@ -385,4 +391,108 @@ public function withObjectMapper(ObjectMapperInterface $objectMapper): static
385391

386392
return $clone;
387393
}
394+
395+
private function needsEnumConversion(mixed $value, \ReflectionClass $targetRefl, string $propertyName): bool
396+
{
397+
if ($value instanceof \UnitEnum && !$value instanceof \BackedEnum) {
398+
$property = $targetRefl->getProperty($propertyName);
399+
$targetType = $property->getType();
400+
401+
if ($targetType instanceof \ReflectionNamedType && \in_array($targetType->getName(), ['string', 'int'], true)) {
402+
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));
403+
}
404+
405+
return false;
406+
}
407+
408+
if (!($value instanceof \BackedEnum || \is_int($value) || \is_string($value))) {
409+
return false;
410+
}
411+
412+
$property = $targetRefl->getProperty($propertyName);
413+
$targetType = $property->getType();
414+
415+
if (!$targetType instanceof \ReflectionNamedType) {
416+
return false;
417+
}
418+
419+
$targetTypeName = $targetType->getName();
420+
421+
if (enum_exists($targetTypeName)) {
422+
return true;
423+
}
424+
425+
if ($value instanceof \BackedEnum && \in_array($targetTypeName, ['string', 'int'], true)) {
426+
return true;
427+
}
428+
429+
return false;
430+
}
431+
432+
/**
433+
* @return int|string|\BackedEnum the target-expected value
434+
*/
435+
private function convertEnumValue(mixed $sourceValue, \ReflectionProperty $targetProperty): int|string|\BackedEnum
436+
{
437+
$targetType = $targetProperty->getType();
438+
$targetEnumTypeName = $targetType instanceof \ReflectionNamedType ? $targetType->getName() : (string) $targetType;
439+
440+
if ($sourceValue instanceof \BackedEnum && \in_array($targetEnumTypeName, ['string', 'int'], true)) {
441+
return $this->convertFromBackedEnum($sourceValue, $targetEnumTypeName);
442+
}
443+
444+
if (is_a($targetEnumTypeName, \BackedEnum::class, true) && !\is_object($sourceValue)) {
445+
return $this->convertToBackedEnum($sourceValue, $targetEnumTypeName);
446+
}
447+
448+
if (is_a($targetEnumTypeName, \UnitEnum::class, true) && !is_a($targetEnumTypeName, \BackedEnum::class, true) && !\is_object($sourceValue)) {
449+
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()));
450+
}
451+
452+
return $sourceValue;
453+
}
454+
455+
/**
456+
* @template T of \BackedEnum
457+
*
458+
* @param class-string<T> $targetEnumClass
459+
*
460+
* @return T
461+
*/
462+
private function convertToBackedEnum(mixed $value, string $targetEnumClass): \BackedEnum
463+
{
464+
if ($value instanceof $targetEnumClass) {
465+
return $value;
466+
}
467+
468+
$targetBackingType = (new \ReflectionEnum($targetEnumClass))->getBackingType();
469+
470+
/** @var \ReflectionNamedType $targetBackingType */
471+
$expectedType = $targetBackingType->getName();
472+
$actualType = get_debug_type($value);
473+
474+
if ($expectedType !== $actualType) {
475+
throw new MappingTransformException(\sprintf('Cannot convert value of type "%s" to "%s"-backed enum "%s".', $actualType, $expectedType, $targetEnumClass));
476+
}
477+
478+
try {
479+
return $targetEnumClass::from($value);
480+
} catch (\ValueError $e) {
481+
throw new MappingTransformException(\sprintf('Invalid value "%s" for enum "%s": "%s"', $value, $targetEnumClass, $e->getMessage()), 0, $e);
482+
}
483+
}
484+
485+
/**
486+
* @param 'int'|'string' $targetType
487+
*/
488+
private function convertFromBackedEnum(\BackedEnum $enum, string $targetType): int|string
489+
{
490+
$backingType = get_debug_type($enum->value);
491+
492+
if ($backingType !== $targetType) {
493+
throw new MappingTransformException(\sprintf('Cannot convert "%s" backed enum "%s" to "%s".', $backingType, $enum::class, $targetType));
494+
}
495+
496+
return $enum->value;
497+
}
388498
}
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)