Skip to content

Commit 8ddacb3

Browse files
committed
[ObjectMapper] lazy map relations
1 parent 9b32764 commit 8ddacb3

File tree

7 files changed

+124
-13
lines changed

7 files changed

+124
-13
lines changed
Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,32 @@
1+
<?php
2+
3+
/*
4+
* This file is part of the Symfony package.
5+
*
6+
* (c) Fabien Potencier <fabien@symfony.com>
7+
*
8+
* For the full copyright and license information, please view the LICENSE
9+
* file that was distributed with this source code.
10+
*/
11+
12+
namespace Symfony\Component\ObjectMapper;
13+
14+
/*
15+
* This file is part of the Symfony package.
16+
*
17+
* (c) Fabien Potencier <fabien@symfony.com>
18+
*
19+
* For the full copyright and license information, please view the LICENSE
20+
* file that was distributed with this source code.
21+
*/
22+
23+
/**
24+
* @internal
25+
*/
26+
interface ClearObjectMapInterface
27+
{
28+
/**
29+
* Clear object map to free memory.
30+
*/
31+
public function clearObjectMap(): void;
32+
}

src/Symfony/Component/ObjectMapper/ObjectMapper.php

Lines changed: 16 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -27,7 +27,7 @@
2727
*
2828
* @author Antoine Bluchet <soyuka@gmail.com>
2929
*/
30-
final class ObjectMapper implements ObjectMapperInterface, ObjectMapperAwareInterface
30+
final class ObjectMapper implements ObjectMapperInterface, ObjectMapperAwareInterface, ClearObjectMapInterface
3131
{
3232
/**
3333
* Tracks recursive references.
@@ -45,10 +45,8 @@ public function __construct(
4545

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

5452
$metadata = $this->metadataFactory->create($source);
@@ -181,10 +179,6 @@ public function map(object $source, object|string|null $target = null): object
181179
$this->propertyAccessor ? $this->propertyAccessor->setValue($mappedTarget, $property, $value) : ($mappedTarget->{$property} = $value);
182180
}
183181

184-
if ($objectMapInitialized) {
185-
$this->objectMap = null;
186-
}
187-
188182
return $mappedTarget;
189183
}
190184

@@ -237,7 +231,16 @@ private function getSourceValue(object $source, object $target, mixed $value, \S
237231
} elseif ($objectMap->offsetExists($value)) {
238232
$value = $objectMap[$value];
239233
} else {
240-
$value = ($this->objectMapper ?? $this)->map($value, $mapTo->target);
234+
if (\PHP_VERSION_ID >= 80400) {
235+
$refl = new \ReflectionClass($mapTo->target);
236+
$mapper = $this->objectMapper ?? $this;
237+
238+
return $refl->newLazyProxy(static function () use ($mapper, $value, $objectMap, $mapTo) {
239+
return $objectMap[$value] = $mapper->map($value, $mapTo->target);
240+
});
241+
}
242+
243+
return ($this->objectMapper ?? $this)->map($value, $mapTo->target);
241244
}
242245
}
243246

@@ -370,4 +373,9 @@ public function withObjectMapper(ObjectMapperInterface $objectMapper): static
370373

371374
return $clone;
372375
}
376+
377+
public function clearObjectMap(): void
378+
{
379+
$this->objectMap = null;
380+
}
373381
}
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+
}

src/Symfony/Component/ObjectMapper/Tests/ObjectMapperTest.php

Lines changed: 36 additions & 5 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,17 @@ 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 (
94+
\PHP_VERSION_ID >= 80400
95+
&& isset($mapped->relation)
96+
&& $mapped->relation instanceof D
97+
) {
98+
$mapped->relation->baz;
99+
}
100+
101+
$this->assertEquals($expect, $mapped);
88102
}
89103

90104
/**
@@ -459,9 +473,10 @@ public function testDecorateObjectMapper()
459473
$myMapper = new class($mapper) implements ObjectMapperInterface {
460474
private ?\SplObjectStorage $embededMap = null;
461475

462-
public function __construct(private readonly ObjectMapperInterface $mapper)
476+
public function __construct(private ObjectMapperInterface $mapper)
463477
{
464478
$this->embededMap = new \SplObjectStorage();
479+
$this->mapper = $mapper->withObjectMapper($this);
465480
}
466481

467482
public function map(object $source, object|string|null $target = null): object
@@ -477,8 +492,6 @@ public function map(object $source, object|string|null $target = null): object
477492
}
478493
};
479494

480-
$mapper = $mapper->withObjectMapper($myMapper);
481-
482495
$d = new D(baz: 'foo', bat: 'bar');
483496
$c = new C(foo: 'foo', bar: 'bar');
484497
$myNewD = $myMapper->map($c);
@@ -491,7 +504,7 @@ public function map(object $source, object|string|null $target = null): object
491504
$a->relation = $c;
492505
$a->relationNotMapped = $d;
493506

494-
$b = $mapper->map($a);
507+
$b = $myMapper->map($a);
495508
$this->assertSame($myNewD, $b->relation);
496509
}
497510

@@ -559,4 +572,22 @@ public function testTransformCollection()
559572

560573
$this->assertEquals([new TransformCollectionD('a'), new TransformCollectionD('b')], $transformed->foo);
561574
}
575+
576+
#[RequiresPhp('>=8.4')]
577+
public function testEmbedsAreLazyLoadedByDefault()
578+
{
579+
$mapper = new ObjectMapper();
580+
$source = new OrderSource();
581+
$source->id = 123;
582+
$source->user = new UserSource();
583+
$source->user->name = 'Test User';
584+
$target = $mapper->map($source, OrderTarget::class);
585+
$this->assertInstanceOf(OrderTarget::class, $target);
586+
$this->assertSame(123, $target->id);
587+
$this->assertInstanceOf(UserTarget::class, $target->user);
588+
$refl = new \ReflectionClass(UserTarget::class);
589+
$this->assertTrue($refl->isUninitializedLazyObject($target->user));
590+
$this->assertSame('Test User', $target->user->name);
591+
$this->assertFalse($refl->isUninitializedLazyObject($target->user));
592+
}
562593
}

0 commit comments

Comments
 (0)