8000 [ObjectMapper] Target in the transform and condition callback · symfony/symfony@564b44b · GitHub
[go: up one dir, main page]

Skip to content
8000

Commit 564b44b

Browse files
committed
[ObjectMapper] Target in the transform and condition callback
1 parent 21b4dd7 commit 564b44b

File tree

9 files changed

+117
-28
lines changed

9 files changed

+117
-28
lines changed

src/Symfony/Component/ObjectMapper/Attribute/Map.php

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -22,8 +22,8 @@
2222
readonly class Map
2323
{
2424
/**
25-
* @param string|class-string|null $source The property or the class to map from
26-
* @param string|class-string|null $target The property or the class to map to
25+
* @param string|class-string|null $source The property or the class to map from
26+
* @param string|class-string|null $target The property or the class to map to
2727
* @param string|bool|callable(mixed, object): bool|null $if A boolean, a service id or a callable that instructs whether to map
2828
* @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
2929
*/

src/Symfony/Component/ObjectMapper/ConditionCallableInterface.php

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -23,8 +23,9 @@
2323
interface ConditionCallableInterface
2424
{
2525
/**
26-
* @param mixed $value The value being mapped
27-
* @param T $object The object we're working on
26+
* @param mixed $value The value being mapped
27+
* @param T $source The object we're working on
28+
* @param T|null $target The target object to which the value will be mapped
2829
*/
29-
public function __invoke(mixed $value, object $object): bool;
30+
public function __invoke(mixed $value, object $source, ?object $target): bool;
3031
}

src/Symfony/Component/ObjectMapper/ObjectMapper.php

Lines changed: 7 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -33,10 +33,6 @@ final class ObjectMapper implements ObjectMapperInterface
3333
*/
3434
private ?\SplObjectStorage $objectMap = null;
3535

36-
/**
37-
* @param ContainerInterface $transformCallableLocator
38-
* @param ContainerInterface $conditionCallableLocator
39-
*/
4036
public function __construct(
4137
private readonly ObjectMapperMetadataFactoryInterface $metadataFactory = new ReflectionObjectMapperMetadataFactory(),
4238
private readonly ?PropertyAccessorInterface $propertyAccessor = null,
@@ -77,7 +73,7 @@ public function map(object $source, object|string|null $target = null): object
7773
$mappedTarget = $this->applyTransforms($map, $mappedTarget, $mappedTarget);
7874

7975
if (!\is_object($mappedTarget)) {
80-
throw new MappingTransformException(sprintf('Cannot map "%s" to a non-object target of type "%s".', get_debug_type($source), get_debug_type($mappedTarget)));
76+
throw new MappingTransformException(\sprintf('Cannot map "%s" to a non-object target of type "%s".', get_debug_type($source), get_debug_type($mappedTarget)));
8177
}
8278
}
8379

@@ -127,7 +123,7 @@ public function map(object $source, object|string|null $target = null): object
127123
}
128124

129125
$value = $this->getRawValue($source, $sourcePropertyName);
130-
if (($if = $mapping->if) && ($fn = $this->getCallable($if, $this->conditionCallableLocator)) && !$this->call($fn, $value, $source)) {
126+
if (($if = $mapping->if) && ($fn = $this->getCallable($if, $this->conditionCallableLocator)) && !$this->call($fn, $value, $source, $mappedTarget)) {
131127
continue;
132128
}
133129

@@ -183,7 +179,7 @@ private function getSourceValue(object $source, object $target, mixed $value, \S
183179
if (
184180
\is_object($value)
185181
&& ($innerMetadata = $this->metadataFactory->create($value))
186-
&& ($mapTo = $this->getMapTarget($innerMetadata, $value, $source))
182+
&& ($mapTo = $this->getMapTarget($innerMetadata, $value, $source, $target))
187183
&& (\is_string($mapTo->target) && class_exists($mapTo->target))
188184
) {
189185
$value = $this->applyTransforms($mapTo, $value, $source);
@@ -220,23 +216,23 @@ private function storeValue(string $propertyName, array &$mapToProperties, array
220216
/**
221217
* @param callable(): mixed $fn
222218
*/
223-
private function call(callable $fn, mixed $value, object $object): mixed
219+
private function call(callable $fn, mixed $value, object $source, ?object $target = null): mixed
224220
{
225221
if (\is_string($fn)) {
226222
return \call_user_func($fn, $value);
227223
}
228224

229-
return $fn($value, $object);
225+
return $fn($value, $source, $target);
230226
}
231227

232228
/**
233229
* @param Mapping[] $metadata
234230
*/
235-
private function getMapTarget(array $metadata, mixed $value, object $source): ?Mapping
231+
private function getMapTarget(array $metadata, mixed $value, object $source, ?object $target = null): ?Mapping
236232
{
237233
$mapTo = null;
238234
foreach ($metadata as $mapAttribute) {
239-
if (($if = $mapAttribute->if) && ($fn = $this->getCallable($if, $this->conditionCallableLocator)) && !$this->call($fn, $value, $source)) {
235+
if (($if = $mapAttribute->if) && ($fn = $this->getCallable($if, $this->conditionCallableLocator)) && !$this->call($fn, $value, $source, $target)) {
240236
continue;
241237
}
242238

src/Symfony/Component/ObjectMapper/Tests/Fixtures/ClassWithoutTarget.php

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,14 @@
11
<?php
22

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+
312
namespace Symfony\Component\ObjectMapper\Tests\Fixtures;
413

514
class ClassWithoutTarget
Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,33 @@
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\MultipleTargetProperty;
13+
14+
use Symfony\Component\ObjectMapper\Attribute\Map;
15+
16+
#[Map(target: B::class)]
17+
#[Map(target: C::class)]
18+
class A
19+
{
20+
#[Map(target: 'foo', transform: 'strtoupper', if: [self::class, 'targetsB'])]
21+
#[Map(target: 'bar')]
22+
public string $something = 'test';
23+
24+
public static function targetsB(mixed $value, object $source, ?object $target)
25+
{
26+
return $target instanceof B;
27+
}
28+
29+
public static function targetsA(mixed $value, object $source, ?object $target)
30+
{
31+
return $target instanceof self;
32+
}
33+
}
Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
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\MultipleTargetProperty;
13+
14+
class B
15+
{
16+
public string $foo;
17+
}
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\MultipleTargetProperty;
13+
14+
class C
15+
{
16+
public string $foo = 'donotmap';
17+
public string $bar;
18+
}

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

Lines changed: 23 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -38,6 +38,9 @@
3838
use Symfony\Component\ObjectMapper\Tests\Fixtures\MapStruct\MapStructMapperMetadataFactory;
3939
use Symfony\Component\ObjectMapper\Tests\Fixtures\MapStruct\Source;
4040
use Symfony\Component\ObjectMapper\Tests\Fixtures\MapStruct\Target;
41+
use Symfony\Component\ObjectMapper\Tests\Fixtures\MultipleTargetProperty\A as MultipleTargetPropertyA;
42+
use Symfony\Component\ObjectMapper\Tests\Fixtures\MultipleTargetProperty\B as MultipleTargetPropertyB;
43+
use Symfony\Component\ObjectMapper\Tests\Fixtures\MultipleTargetProperty\C as MultipleTargetPropertyC;
4144
use Symfony\Component\ObjectMapper\Tests\Fixtures\MultipleTargets\A as MultipleTargetsA;
4245
use Symfony\Component\ObjectMapper\Tests\Fixtures\MultipleTargets\C as MultipleTargetsC;
4346
use Symfony\Component\ObjectMapper\Tests\Fixtures\Recursion\AB;
@@ -100,7 +103,7 @@ public function testHasNothingToMapTo()
100103
{
101104
$this->expectException(MappingException::class);
102105
$this->expectExceptionMessage('Mapping target not found for source "class@anonymous".');
103-
(new ObjectMapper())->map(new class () {});
106+
(new ObjectMapper())->map(new class {});
104107
}
105108

106109
public function testHasNothingToMapToWithNamedClass()
@@ -194,7 +197,7 @@ public function testServiceLocator()
194197

195198
protected function getServiceLocator(array $factories): ContainerInterface
196199
{
197-
return new class ($factories) implements ContainerInterface {
200+
return new class($factories) implements ContainerInterface {
198201
public function __construct(private array $factories)
199202
{
200203
}
@@ -220,7 +223,7 @@ public function testSourceOnly(): void
220223
$this->assertInstanceOf(SourceOnly::class, $mapped);
221224
$this->assertSame('test', $mapped->mappedName);
222225

223-
$a = new class () {
226+
$a = new class {
224227
public function __get(string $key): string
225228
{
226229
return match ($key) {
@@ -235,32 +238,43 @@ public function __get(string $key): string
235238
$this->assertSame('test', $mapped->mappedName);
236239
}
237240

238-
239241
public function testTransformToWrongValueType(): void
240242
{
241243
$this->expectException(MappingTransformException::class);
242244
$this->expectExceptionMessage('Cannot map "stdClass" to a non-object target of type "string".');
243245

244-
$u = new \stdClass;
246+
$u = new \stdClass();
245247
$u->foo = 'bar';
246248

247249
$metadata = $this->createStub(ObjectMapperMetadataFactoryInterface::class);
248-
$metadata->method('create')->with($u)->willReturn([new Mapping(target: \stdClass::class, transform: fn() => 'str')]);
250+
$metadata->method('create')->with($u)->willReturn([new Mapping(target: \stdClass::class, transform: fn () => 'str')]);
249251
$mapper = new ObjectMapper($metadata);
250252
$mapper->map($u);
251253
}
252254

253255
public function testTransformToWrongObject(): void
254256
{
255257
$this->expectException(MappingException::class);
256-
$this->expectExceptionMessage(sprintf('Expected the mapped object to be an instance of "%s" but got "stdClass".', ClassWithoutTarget::class));
258+
$this->expectExceptionMessage(\sprintf('Expected the mapped object to be an instance of "%s" but got "stdClass".', ClassWithoutTarget::class));
257259

258-
$u = new \stdClass;
260+
$u = new \stdClass();
259261
$u->foo = 'bar';
260262

261263
$metadata = $this->createStub(ObjectMapperMetadataFactoryInterface::class);
262-
$metadata->method('create')->with($u)->willReturn([new Mapping(target: ClassWithoutTarget::class, transform: fn() => new \stdClass)]);
264+
$metadata->method('create')->with($u)->willReturn([new Mapping(target: ClassWithoutTarget::class, transform: fn () => new \stdClass())]);
263265
$mapper = new ObjectMapper($metadata);
264266
$mapper->map($u);
265267
}
268+
269+
public function testMultipleTargetMapProperty(): void
270+
{
271+
$u = new MultipleTargetPropertyA();
272+
273+
$mapper = new ObjectMapper();
274+
$this->assertInstanceOf(MultipleTargetPropertyB::class, $mapper->map($u, MultipleTargetPropertyB::class));
275+
$c = $mapper->map($u, MultipleTargetPropertyC::class);
276+
$this->assertInstanceOf(MultipleTargetPropertyC::class, $c);
277+
$this->assertEquals($c->bar, 'test');
278+
$this->assertEquals($c->foo, 'donotmap');
279+
}
266280
}

src/Symfony/Component/ObjectMapper/TransformCallableInterface.php

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -23,8 +23,9 @@
2323
interface TransformCallableInterface
2424
{
2525
/**
26-
* @param mixed $value The value being mapped
27-
* @param T $object The object we're working on
26+
* @param mixed $value The value being mapped
27+
* @param T $source The object we're working on
28+
* @param T|null $target The target object to which the value will be mapped
2829
*/
29-
public function __invoke(mixed $value, object $object): mixed;
30+
public function __invoke(mixed $value, object $source, ?object $target): mixed;
3031
}

0 commit comments

Comments
 (0)
0