Skip to content

Commit a4d279b

Browse files
committed
[ObjectMapper] skip proxy initialization at given depth
Fixes #61357
1 parent 9c413a3 commit a4d279b

File tree

12 files changed

+331
-4
lines changed

12 files changed

+331
-4
lines changed
Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,30 @@
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\Attribute;
13+
14+
/**
15+
* Applies a transformer to all properties of a class.
16+
*
17+
* @author Antoine Bluchet <soyuka@gmail.com>
18+
*/
19+
#[\Attribute(\Attribute::TARGET_CLASS | \Attribute::IS_REPEATABLE)]
20+
final class TransformAllProperties
21+
{
22+
/**
23+
* @param (string|callable(mixed, object): mixed)|(string|callable(mixed, object): mixed)[]|null $transform A service id or a callable that transforms the value during mapping
24+
*/
25+
public function __construct(
26+
public mixed $transform,
27+
) {
28+
}
29+
}
30+

src/Symfony/Component/ObjectMapper/CHANGELOG.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,11 @@ 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+
* Add a `DepthAwareInterface` to gather information about the mapping depth
11+
* Add a `TransformAllProperties` that applies a transform on all properties of
12+
a mapped object
13+
* Add a `UninitializeProxy` transform that skips proxy initialization for a
14+
given depth
1015

1116
7.3
1217
---
Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
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+
* Allows a callable or mapper to be aware of the current mapping depth.
16+
*
17+
* @author Antoine Bluchet <soyuka@gmail.com>
18+
*/
19+
interface DepthAwareInterface
20+
{
21+
public function setDepth(int $depth): void;
22+
}
23+

src/Symfony/Component/ObjectMapper/Metadata/ReflectionObjectMapperMetadataFactory.php

Lines changed: 48 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@
1212
namespace Symfony\Component\ObjectMapper\Metadata;
1313

1414
use Symfony\Component\ObjectMapper\Attribute\Map;
15+
use Symfony\Component\ObjectMapper\Attribute\TransformAllProperties;
1516
use Symfony\Component\ObjectMapper\Exception\MappingException;
1617

1718
/**
@@ -23,6 +24,8 @@ final class ReflectionObjectMapperMetadataFactory implements ObjectMapperMetadat
2324
{
2425
private array $reflectionClassCache = [];
2526
private array $attributesCache = [];
27+
/** @var array<class-string, list<string|callable>> */
28+
private array $classAttributesCache = [];
2629

2730
public function create(object $object, ?string $property = null, array $context = []): array
2831
{
@@ -35,15 +38,59 @@ public function create(object $object, ?string $property = null, array $context
3538

3639
$refl = $this->reflectionClassCache[$object::class] ??= new \ReflectionClass($object);
3740
$attributes = ($property ? $refl->getProperty($property) : $refl)->getAttributes(Map::class, \ReflectionAttribute::IS_INSTANCEOF);
41+
42+
$globalTransforms = null !== $property ? $this->getClassPropertyTransforms($refl) : [];
3843
$mappings = [];
44+
3945
foreach ($attributes as $attribute) {
4046
$map = $attribute->newInstance();
41-
$mappings[] = new Mapping($map->target, $map->source, $map->if, $map->transform);
47+
$transforms = $map->transform;
48+
if (is_callable($transforms) || !is_array($transforms)) {
49+
$transforms = [$transforms];
50+
}
51+
52+
$mappings[] = new Mapping($map->target, $map->source, $map->if, [...$globalTransforms, ...$transforms]);
53+
}
54+
55+
if ($globalTransforms && !$mappings) {
56+
$mappings[] = new Mapping(null, null, null, $globalTransforms);
4257
}
4358

4459
return $this->attributesCache[$key] = $mappings;
4560
} catch (\ReflectionException $e) {
4661
throw new MappingException($e->getMessage(), $e->getCode(), $e);
4762
}
4863
}
64+
65+
/**
66+
* @return list<string|callable>
67+
*/
68+
private function getClassPropertyTransforms(\ReflectionClass $refl): array
69+
{
70+
$key = $refl->getName();
71+
if (isset($this->classAttributesCache[$key])) {
72+
return $this->classAttributesCache[$key];
73+
}
74+
75+
$attributes = $refl->getAttributes(TransformAllProperties::class, \ReflectionAttribute::IS_INSTANCEOF);
76+
if (!$attributes) {
77+
return $this->classAttributesCache[$key] = [];
78+
}
79+
80+
$globalTransforms = [];
81+
foreach ($attributes as $attribute) {
82+
if ($t = $attribute->newInstance()->transform) {
83+
if (is_callable($t) || !is_array($t)) {
84+
$t = [$t];
85+
}
86+
87+
foreach ($t as $transform) {
88+
$globalTransforms[] = $transform;
89+
}
90+
}
91+
}
92+
93+
return $this->classAttributesCache[$key] = $globalTransforms;
94+
}
4995
}
96+

src/Symfony/Component/ObjectMapper/ObjectMapper.php

Lines changed: 24 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@
1212
namespace Symfony\Component\ObjectMapper;
1313

1414
use Psr\Container\ContainerInterface;
15+
use Symfony\Component\ObjectMapper\DepthAwareInterface;
1516
use Symfony\Component\ObjectMapper\Exception\MappingException;
1617
use Symfony\Component\ObjectMapper\Exception\MappingTransformException;
1718
use Symfony\Component\ObjectMapper\Exception\NoSuchPropertyException;
@@ -27,12 +28,13 @@
2728
*
2829
* @author Antoine Bluchet <soyuka@gmail.com>
2930
*/
30-
final class ObjectMapper implements ObjectMapperInterface, ObjectMapperAwareInterface
31+
final class ObjectMapper implements ObjectMapperInterface, ObjectMapperAwareInterface, DepthAwareInterface
3132
{
3233
/**
3334
* Tracks recursive references.
3435
*/
3536
private ?\SplObjectStorage $objectMap = null;
37+
private int $depth = 0;
3638

3739
public function __construct(
3840
private readonly ObjectMapperMetadataFactoryInterface $metadataFactory = new ReflectionObjectMapperMetadataFactory(),
@@ -43,6 +45,11 @@ public function __construct(
4345
) {
4446
}
4547

48+
public function setDepth(int $depth): void
49+
{
50+
$this->depth = $depth;
51+
}
52+
4653
public function map(object $source, object|string|null $target = null): object
4754
{
4855
$objectMapInitialized = false;
@@ -237,7 +244,11 @@ private function getSourceValue(object $source, object $target, mixed $value, \S
237244
} elseif ($objectMap->offsetExists($value)) {
238245
$value = $objectMap[$value];
239246
} else {
240-
$value = ($this->objectMapper ?? $this)->map($value, $mapTo->target);
247+
$mapper = $this->objectMapper ?? $this;
248+
if ($mapper instanceof DepthAwareInterface) {
249+
$mapper->setDepth($this->depth + 1);
250+
}
251+
$value = $mapper->map($value, $mapTo->target);
241252
}
242253
}
243254

@@ -317,11 +328,20 @@ private function applyTransforms(Mapping $map, mixed $value, object $source, ?ob
317328
private function getCallable(string|callable $fn, ?ContainerInterface $locator = null): ?callable
318329
{
319330
if (\is_callable($fn)) {
331+
if ($fn instanceof DepthAwareInterface) {
332+
$fn->setDepth($this->depth);
333+
}
334+
320335
return $fn;
321336
}
322337

323338
if ($locator?->has($fn)) {
324-
return $locator->get($fn);
339+
$callable = $locator->get($fn);
340+
if ($callable instanceof DepthAwareInterface) {
341+
$callable->setDepth($this->depth);
342+
}
343+
344+
return $callable;
325345
}
326346

327347
return null;
@@ -371,3 +391,4 @@ public function withObjectMapper(ObjectMapperInterface $objectMapper): static
371391
return $clone;
372392
}
373393
}
394+
Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,40 @@
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\Tests\Fixtures\DepthProxy;
13+
14+
use Symfony\Component\VarExporter\LazyObjectInterface;
15+
16+
class MockLazyProxy implements LazyObjectInterface
17+
{
18+
private bool $initialized = false;
19+
20+
public function __construct(
21+
private string $exceptionMessage,
22+
) {
23+
}
24+
25+
public function initializeLazyObject(): object
26+
{
27+
$this->initialized = true;
28+
throw new \RuntimeException($this->exceptionMessage);
29+
}
30+
31+
public function isLazyObjectInitialized(bool $partial = false): bool
32+
{
33+
return $this->initialized;
34+
}
35+
36+
public function resetLazyObject(): bool
37+
{
38+
return false;
39+
}
40+
}
Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
<?php
2+
3+
namespace Symfony\Component\ObjectMapper\Tests\Fixtures\DepthProxy;
4+
5+
use Symfony\Component\ObjectMapper\Attribute\Map;
6+
use Symfony\Component\ObjectMapper\Attribute\TransformAllProperties;
7+
use Symfony\Component\ObjectMapper\Transform\UninitializeProxy;
8+
9+
#[Map(target: PostDto::class)]
10+
#[TransformAllProperties(transform: new UninitializeProxy(maxDepth: 1))]
11+
class Post
12+
{
13+
public function __construct(
14+
public string $title,
15+
public object $comments, // This will be the lazy proxy
16+
) {
17+
}
18+
}
Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
<?php
2+
3+
namespace Symfony\Component\ObjectMapper\Tests\Fixtures\DepthProxy;
4+
5+
/*
6+
* This file is part of the Symfony package.
7+
*
8+
* (c) Fabien Potencier <fabien@symfony.com>
9+
*
10+
* For the full copyright and license information, please view the LICENSE
11+
* file that was distributed with this source code.
12+
*/
13+
14+
class PostDto
15+
{
16+
public string $title;
17+
public ?object $comments = null;
18+
}
Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
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\Tests\Fixtures\DepthProxy;
13+
14+
use Symfony\Component\ObjectMapper\Attribute\Map;
15+
use Symfony\Component\ObjectMapper\Attribute\TransformAllProperties;
16+
use Symfony\Component\ObjectMapper\Transform\UninitializeProxy;
17+
18+
#[Map(target: UserDto::class)]
19+
#[TransformAllProperties(transform: new UninitializeProxy(maxDepth: 1))]
20+
class User
21+
{
22+
public function __construct(
23+
public string $name,
24+
public object $post,
25+
) {
26+
}
27+
}
Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
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\Tests\Fixtures\DepthProxy;
13+
14+
class UserDto
15+
{
16+
public string $name;
17+
public ?PostDto $post = null;
18+
}

0 commit comments

Comments
 (0)