From 6dfe8b761fffd07c26e316116b4c26bb9c60da1e Mon Sep 17 00:00:00 2001 From: soyuka Date: Sun, 16 Nov 2025 21:42:37 +0100 Subject: [PATCH] [ObjectMapper] bypass lazy ghost with class transform --- .../Component/ObjectMapper/ObjectMapper.php | 8 +++++ .../ServiceLoadedValue/LoadedValue.php | 19 +++++++++++ .../ServiceLoadedValue/LoadedValueService.php | 29 ++++++++++++++++ .../ServiceLoadedValue/LoadedValueTarget.php | 19 +++++++++++ .../ServiceLoadedValueTransformer.php | 33 +++++++++++++++++++ .../ServiceLoadedValue/ValueToMap.php | 11 +++++++ .../ServiceLoadedValue/ValueToMapRelation.php | 13 ++++++++ .../ObjectMapper/Tests/ObjectMapperTest.php | 30 ++++++++++++++++- 8 files changed, 161 insertions(+), 1 deletion(-) create mode 100644 src/Symfony/Component/ObjectMapper/Tests/Fixtures/ServiceLoadedValue/LoadedValue.php create mode 100644 src/Symfony/Component/ObjectMapper/Tests/Fixtures/ServiceLoadedValue/LoadedValueService.php create mode 100644 src/Symfony/Component/ObjectMapper/Tests/Fixtures/ServiceLoadedValue/LoadedValueTarget.php create mode 100644 src/Symfony/Component/ObjectMapper/Tests/Fixtures/ServiceLoadedValue/ServiceLoadedValueTransformer.php create mode 100644 src/Symfony/Component/ObjectMapper/Tests/Fixtures/ServiceLoadedValue/ValueToMap.php create mode 100644 src/Symfony/Component/ObjectMapper/Tests/Fixtures/ServiceLoadedValue/ValueToMapRelation.php diff --git a/src/Symfony/Component/ObjectMapper/ObjectMapper.php b/src/Symfony/Component/ObjectMapper/ObjectMapper.php index bb14c9e7191b1..d7e36428f80d9 100644 --- a/src/Symfony/Component/ObjectMapper/ObjectMapper.php +++ b/src/Symfony/Component/ObjectMapper/ObjectMapper.php @@ -241,8 +241,16 @@ private function getSourceValue(object $source, object $target, mixed $value, \W } elseif ($objectMap->offsetExists($value)) { $value = $objectMap[$value]; } elseif (\PHP_VERSION_ID < 80400) { + if ($mapTo->transform) { + return $value; + } + return ($this->objectMapper ?? $this)->map($value, $mapTo->target); } else { + if ($mapTo->transform) { + return $value; + } + $refl = new \ReflectionClass($mapTo->target); $mapper = $this->objectMapper ?? $this; diff --git a/src/Symfony/Component/ObjectMapper/Tests/Fixtures/ServiceLoadedValue/LoadedValue.php b/src/Symfony/Component/ObjectMapper/Tests/Fixtures/ServiceLoadedValue/LoadedValue.php new file mode 100644 index 0000000000000..7221f740e6c49 --- /dev/null +++ b/src/Symfony/Component/ObjectMapper/Tests/Fixtures/ServiceLoadedValue/LoadedValue.php @@ -0,0 +1,19 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\ObjectMapper\Tests\Fixtures\ServiceLoadedValue; + +class LoadedValue +{ + public function __construct(public string $name) + { + } +} diff --git a/src/Symfony/Component/ObjectMapper/Tests/Fixtures/ServiceLoadedValue/LoadedValueService.php b/src/Symfony/Component/ObjectMapper/Tests/Fixtures/ServiceLoadedValue/LoadedValueService.php new file mode 100644 index 0000000000000..5dc8be947fdde --- /dev/null +++ b/src/Symfony/Component/ObjectMapper/Tests/Fixtures/ServiceLoadedValue/LoadedValueService.php @@ -0,0 +1,29 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\ObjectMapper\Tests\Fixtures\ServiceLoadedValue; + +class LoadedValueService +{ + public function __construct(private ?LoadedValue $value = null) + { + } + + public function load(): void + { + $this->value = new LoadedValue(name: 'loaded'); + } + + public function get(): LoadedValue + { + return $this->value; + } +} diff --git a/src/Symfony/Component/ObjectMapper/Tests/Fixtures/ServiceLoadedValue/LoadedValueTarget.php b/src/Symfony/Component/ObjectMapper/Tests/Fixtures/ServiceLoadedValue/LoadedValueTarget.php new file mode 100644 index 0000000000000..87d0f27567d34 --- /dev/null +++ b/src/Symfony/Component/ObjectMapper/Tests/Fixtures/ServiceLoadedValue/LoadedValueTarget.php @@ -0,0 +1,19 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\ObjectMapper\Tests\Fixtures\ServiceLoadedValue; + +class LoadedValueTarget +{ + public function __construct(public ?LoadedValue $relation = null) + { + } +} diff --git a/src/Symfony/Component/ObjectMapper/Tests/Fixtures/ServiceLoadedValue/ServiceLoadedValueTransformer.php b/src/Symfony/Component/ObjectMapper/Tests/Fixtures/ServiceLoadedValue/ServiceLoadedValueTransformer.php new file mode 100644 index 0000000000000..ed1923e1aaf30 --- /dev/null +++ b/src/Symfony/Component/ObjectMapper/Tests/Fixtures/ServiceLoadedValue/ServiceLoadedValueTransformer.php @@ -0,0 +1,33 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\ObjectMapper\Tests\Fixtures\ServiceLoadedValue; + +use Symfony\Component\ObjectMapper\Metadata\ObjectMapperMetadataFactoryInterface; +use Symfony\Component\ObjectMapper\TransformCallableInterface; + +/** + * @implements TransformCallableInterface + */ +class ServiceLoadedValueTransformer implements TransformCallableInterface +{ + public function __construct(private readonly LoadedValueService $serviceLoadedValue, private readonly ObjectMapperMetadataFactoryInterface $metadata) + { + } + + public function __invoke(mixed $value, object $source, ?object $target): mixed + { + $metadata = $this->metadata->create($value); + \assert(count($metadata) === 1); + \assert($metadata[0]->target === LoadedValue::class); + return $this->serviceLoadedValue->get(); + } +} diff --git a/src/Symfony/Component/ObjectMapper/Tests/Fixtures/ServiceLoadedValue/ValueToMap.php b/src/Symfony/Component/ObjectMapper/Tests/Fixtures/ServiceLoadedValue/ValueToMap.php new file mode 100644 index 0000000000000..6acf6d2d4c9db --- /dev/null +++ b/src/Symfony/Component/ObjectMapper/Tests/Fixtures/ServiceLoadedValue/ValueToMap.php @@ -0,0 +1,11 @@ +map(...$args); - if (\PHP_VERSION_ID >= 80400 && isset($mapped->relation) && $mapped->relation instanceof D ) { + if (\PHP_VERSION_ID >= 80400 && isset($mapped->relation) && $mapped->relation instanceof D) { $mapped->relation->baz; } @@ -583,4 +587,28 @@ public function testEmbedsAreLazyLoadedByDefault() $this->assertSame('Test User', $target->user->name); $this->assertFalse($refl->isUninitializedLazyObject($target->user)); } + + public function testSkipLazyGhostWithClassTransform() + { + $service = new LoadedValueService(); + $service->load(); + + $metadataFactory = new ReflectionObjectMapperMetadataFactory(); + $mapper = new ObjectMapper( + metadataFactory: $metadataFactory, + transformCallableLocator: $this->getServiceLocator([ServiceLoadedValueTransformer::class => new ServiceLoadedValueTransformer($service, $metadataFactory)]) + ); + + $value = new ValueToMap(); + $value->relation = new ValueToMapRelation('test'); + + $result = $mapper->map($value); + if (\PHP_VERSION_ID >= 80400) { + $refl = new \ReflectionClass($result->relation); + $this->assertFalse($refl->isUninitializedLazyObject($result->relation)); + } + + $this->assertSame($result->relation, $service->get()); + $this->assertSame($result->relation->name, 'loaded'); + } }