Skip to content

Commit 03de92a

Browse files
bug #62214 [ObjectMapper] lazy loading (soyuka)
This PR was squashed before being merged into the 7.4 branch. Discussion ---------- [ObjectMapper] lazy loading | Q | A | ------------- | --- | Branch? | 7.4 | Bug fix? | yes | New feature? | no | Deprecations? | no | Issues | #61357 | License | MIT Introduces lazy loading over the recursive mapping behavior of the ObjectMapper. Commits ------- 83f086d0b57 [ObjectMapper] lazy loading
2 parents 640f739 + 1e22b51 commit 03de92a

File tree

6 files changed

+110
-29
lines changed

6 files changed

+110
-29
lines changed

ObjectMapper.php

Lines changed: 32 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -32,7 +32,7 @@ final class ObjectMapper implements ObjectMapperInterface, ObjectMapperAwareInte
3232
/**
3333
* Tracks recursive references.
3434
*/
35-
private ?\SplObjectStorage $objectMap = null;
35+
private ?\WeakMap $objectMap = null;
3636

3737
public function __construct(
3838
private readonly ObjectMapperMetadataFactoryInterface $metadataFactory = new ReflectionObjectMapperMetadataFactory(),
@@ -45,12 +45,20 @@ public function __construct(
4545

4646
public function map(object $source, object|string|null $target = null): object
4747
{
48-
$objectMapInitialized = false;
49-
if (null === $this->objectMap) {
50-
$this->objectMap = new \SplObjectStorage();
51-
$objectMapInitialized = true;
48+
if ($this->objectMap) {
49+
return $this->doMap($source, $target, $this->objectMap, false);
5250
}
5351

52+
$this->objectMap = new \WeakMap();
53+
try {
54+
return $this->doMap($source, $target, $this->objectMap, true);
55+
} finally {
56+
$this->objectMap = null;
57+
}
58+
}
59+
60+
private function doMap(object $source, object|string|null $target, \WeakMap $objectMap, bool $rootCall): object
61+
{
5462
$metadata = $this->metadataFactory->create($source);
5563
$map = $this->getMapTarget($metadata, null, $source, null);
5664
$target ??= $map?->target;
@@ -89,7 +97,7 @@ public function map(object $source, object|string|null $target = null): object
8997
throw new MappingException(\sprintf('Expected the mapped object to be an instance of "%s" but got "%s".', $targetRefl->getName(), get_debug_type($mappedTarget)));
9098
}
9199

92-
$this->objectMap[$source] = $mappedTarget;
100+
$objectMap[$source] = $mappedTarget;
93101
$ctorArguments = [];
94102
$targetConstructor = $targetRefl->getConstructor();
95103
foreach ($targetConstructor?->getParameters() ?? [] as $parameter) {
@@ -146,7 +154,7 @@ public function map(object $source, object|string|null $target = null): object
146154
continue;
147155
}
148156

149-
$value = $this->getSourceValue($source, $mappedTarget, $value, $this->objectMap, $mapping);
157+
$value = $this->getSourceValue($source, $mappedTarget, $value, $objectMap, $mapping);
150158
$this->storeValue($targetPropertyName, $mapToProperties, $ctorArguments, $value);
151159
}
152160

@@ -156,12 +164,12 @@ public function map(object $source, object|string|null $target = null): object
156164
continue;
157165
}
158166

159-
$value = $this->getSourceValue($source, $mappedTarget, $this->getRawValue($source, $propertyName), $this->objectMap);
167+
$value = $this->getSourceValue($source, $mappedTarget, $this->getRawValue($source, $propertyName), $objectMap);
160168
$this->storeValue($propertyName, $mapToProperties, $ctorArguments, $value);
161169
}
162170
}
163171

164-
if (!$mappingToObject && !$map?->transform && $targetConstructor) {
172+
if ((!$mappingToObject || !$rootCall) && !$map?->transform && $targetConstructor) {
165173
try {
166174
$mappedTarget->__construct(...$ctorArguments);
167175
} catch (\ReflectionException $e) {
@@ -181,10 +189,6 @@ public function map(object $source, object|string|null $target = null): object
181189
$this->propertyAccessor ? $this->propertyAccessor->setValue($mappedTarget, $property, $value) : ($mappedTarget->{$property} = $value);
182190
}
183191

184-
if ($objectMapInitialized) {
185-
$this->objectMap = null;
186-
}
187-
188192
return $mappedTarget;
189193
}
190194

@@ -218,7 +222,7 @@ private function getRawValue(object $source, string $propertyName): mixed
218222
return $source->{$propertyName};
219223
}
220224

221-
private function getSourceValue(object $source, object $target, mixed $value, \SplObjectStorage $objectMap, ?Mapping $mapping = null): mixed
225+
private function getSourceValue(object $source, object $target, mixed $value, \WeakMap $objectMap, ?Mapping $mapping = null): mixed
222226
{
223227
if ($mapping?->transform) {
224228
$value = $this->applyTransforms($mapping, $value, $source, $target);
@@ -236,8 +240,21 @@ private function getSourceValue(object $source, object $target, mixed $value, \S
236240
$value = $target;
237241
} elseif ($objectMap->offsetExists($value)) {
238242
$value = $objectMap[$value];
243+
} elseif (\PHP_VERSION_ID < 80400) {
244+
return ($this->objectMapper ?? $this)->map($value, $mapTo->target);
239245
} else {
240-
$value = ($this->objectMapper ?? $this)->map($value, $mapTo->target);
246+
$refl = new \ReflectionClass($mapTo->target);
247+
$mapper = $this->objectMapper ?? $this;
248+
249+
return $refl->newLazyGhost(function ($target) use ($mapper, $value, $objectMap) {
250+
$previousMap = $this->objectMap;
251+
$this->objectMap = $objectMap;
252+
try {
253+
$objectMap[$value] = $mapper->map($value, $target);
254+
} finally {
255+
$this->objectMap = $previousMap;
256+
}
257+
});
241258
}
242259
}
243260

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
<?php
2+
3+
namespace Symfony\Component\ObjectMapper\Tests\Fixtures\DefaultLazy;
4+
5+
use Symfony\Component\ObjectMapper\Attribute\Map;
6+
7+
#[Map(target: OrderTarget::class)]
8+
class OrderSource
9+
{
10+
public ?int $id = null;
11+
public ?UserSource $user = null;
12+
}
Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
<?php
2+
3+
namespace Symfony\Component\ObjectMapper\Tests\Fixtures\DefaultLazy;
4+
5+
class OrderTarget
6+
{
7+
public ?int $id = null;
8+
public ?UserTarget $user = null;
9+
}
Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
<?php
2+
3+
namespace Symfony\Component\ObjectMapper\Tests\Fixtures\DefaultLazy;
4+
5+
use Symfony\Component\ObjectMapper\Attribute\Map;
6+
7+
#[Map(target: UserTarget::class)]
8+
class UserSource
9+
{
10+
public ?string $name = null;
11+
}
Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
<?php
2+
3+
namespace Symfony\Component\ObjectMapper\Tests\Fixtures\DefaultLazy;
4+
5+
class UserTarget
6+
{
7+
public ?string $name = null;
8+
}

Tests/ObjectMapperTest.php

Lines changed: 38 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,10 @@
3232
use Symfony\Component\ObjectMapper\Tests\Fixtures\DeeperRecursion\RecursiveDto;
3333
use Symfony\Component\ObjectMapper\Tests\Fixtures\DeeperRecursion\Relation;
3434
use Symfony\Component\ObjectMapper\Tests\Fixtures\DeeperRecursion\RelationDto;
35+
use Symfony\Component\ObjectMapper\Tests\Fixtures\DefaultLazy\OrderSource;
36+
use Symfony\Component\ObjectMapper\Tests\Fixtures\DefaultLazy\OrderTarget;
37+
use Symfony\Component\ObjectMapper\Tests\Fixtures\DefaultLazy\UserSource;
38+
use Symfony\Component\ObjectMapper\Tests\Fixtures\DefaultLazy\UserTarget;
3539
use Symfony\Component\ObjectMapper\Tests\Fixtures\DefaultValueStdClass\TargetDto;
3640
use Symfony\Component\ObjectMapper\Tests\Fixtures\Flatten\TargetUser;
3741
use Symfony\Component\ObjectMapper\Tests\Fixtures\Flatten\User;
@@ -84,7 +88,13 @@ final class ObjectMapperTest extends TestCase
8488
public function testMap($expect, $args, array $deps = [])
8589
{
8690
$mapper = new ObjectMapper(...$deps);
87-
$this->assertEquals($expect, $mapper->map(...$args));
91+
$mapped = $mapper->map(...$args);
92+
93+
if (\PHP_VERSION_ID >= 80400 && isset($mapped->relation) && $mapped->relation instanceof D ) {
94+
$mapped->relation->baz;
95+
}
96+
97+
$this->assertEquals($expect, $mapped);
8898
}
8999

90100
/**
@@ -457,31 +467,27 @@ public function testDecorateObjectMapper()
457467
{
458468
$mapper = new ObjectMapper();
459469
$myMapper = new class($mapper) implements ObjectMapperInterface {
460-
private ?\SplObjectStorage $embededMap = null;
461-
462-
public function __construct(private readonly ObjectMapperInterface $mapper)
470+
public function __construct(private ObjectMapperInterface $mapper)
463471
{
464-
$this->embededMap = new \SplObjectStorage();
472+
$this->mapper = $mapper->withObjectMapper($this);
465473
}
466474

467475
public function map(object $source, object|string|null $target = null): object
468476
{
469-
if (isset($this->embededMap[$source])) {
470-
$target = $this->embededMap[$source];
471-
}
472-
473477
$mapped = $this->mapper->map($source, $target);
474-
$this->embededMap[$source] = $mapped;
478+
479+
if ($source instanceof C) {
480+
$mapped->baz = 'got decorated';
481+
}
475482

476483
return $mapped;
477484
}
478485
};
479486

480-
$mapper = $mapper->withObjectMapper($myMapper);
481-
482487
$d = new D(baz: 'foo', bat: 'bar');
483488
$c = new C(foo: 'foo', bar: 'bar');
484489
$myNewD = $myMapper->map($c);
490+
$this->assertSame('got decorated', $myNewD->baz);
485491

486492
$a = new A();
487493
$a->foo = 'test';
@@ -491,8 +497,8 @@ public function map(object $source, object|string|null $target = null): object
491497
$a->relation = $c;
492498
$a->relationNotMapped = $d;
493499

494-
$b = $mapper->map($a);
495-
$this->assertSame($myNewD, $b->relation);
500+
$b = $myMapper->map($a);
501+
$this->assertSame('got decorated', $b->relation->baz);
496502
}
497503

498504
#[DataProvider('validPartialInputProvider')]
@@ -559,4 +565,22 @@ public function testTransformCollection()
559565

560566
$this->assertEquals([new TransformCollectionD('a'), new TransformCollectionD('b')], $transformed->foo);
561567
}
568+
569+
#[RequiresPhp('>=8.4')]
570+
public function testEmbedsAreLazyLoadedByDefault()
571+
{
572+
$mapper = new ObjectMapper();
573+
$source = new OrderSource();
574+
$source->id = 123;
575+
$source->user = new UserSource();
576+
$source->user->name = 'Test User';
577+
$target = $mapper->map($source, OrderTarget::class);
578+
$this->assertInstanceOf(OrderTarget::class, $target);
579+
$this->assertSame(123, $target->id);
580+
$this->assertInstanceOf(UserTarget::class, $target->user);
581+
$refl = new \ReflectionClass(UserTarget::class);
582+
$this->assertTrue($refl->isUninitializedLazyObject($target->user));
583+
$this->assertSame('Test User', $target->user->name);
584+
$this->assertFalse($refl->isUninitializedLazyObject($target->user));
585+
}
562586
}

0 commit comments

Comments
 (0)