Skip to content

[ObjectMapper] Condition to target a specific class #60028

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 1 commit into from
May 6, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@
*/
final class TransformCallable implements TransformCallableInterface
{
public function __invoke(mixed $value, object $object): mixed
public function __invoke(mixed $value, object $source, ?object $target): mixed
{
return 'transformed';
}
Expand Down
34 changes: 34 additions & 0 deletions src/Symfony/Component/ObjectMapper/Condition/TargetClass.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
<?php

/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <[email protected]>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/

namespace Symfony\Component\ObjectMapper\Condition;

use Symfony\Component\ObjectMapper\ConditionCallableInterface;

/**
* @template T of object
*
* @implements ConditionCallableInterface<object, T>
*/
final class TargetClass implements ConditionCallableInterface
{
/**
* @param class-string<T> $className
*/
public function __construct(private readonly string $className)
{
}

public function __invoke(mixed $value, object $source, ?object $target): bool
{
return $target instanceof $this->className;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@
* Service used by "Map::if".
*
* @template T of object
* @template T2 of object
*
* @experimental
*
Expand All @@ -25,6 +26,7 @@ interface ConditionCallableInterface
/**
* @param mixed $value The value being mapped
* @param T $source The object we're working on
* @param T2|null $target The target we're mapping to
*/
public function __invoke(mixed $value, object $source): bool;
public function __invoke(mixed $value, object $source, ?object $target): bool;
}
24 changes: 12 additions & 12 deletions src/Symfony/Component/ObjectMapper/ObjectMapper.php
Original file line number Diff line number Diff line change
Expand Up @@ -50,7 +50,7 @@ public function map(object $source, object|string|null $target = null): object
}

$metadata = $this->metadataFactory->create($source);
$map = $this->getMapTarget($metadata, null, $source);
$map = $this->getMapTarget($metadata, null, $source, null);
$target ??= $map?->target;
$mappingToObject = \is_object($target);

Expand All @@ -70,7 +70,7 @@ public function map(object $source, object|string|null $target = null): object

$mappedTarget = $mappingToObject ? $target : $targetRefl->newInstanceWithoutConstructor();
if ($map && $map->transform) {
$mappedTarget = $this->applyTransforms($map, $mappedTarget, $mappedTarget);
$mappedTarget = $this->applyTransforms($map, $mappedTarget, $mappedTarget, null);

if (!\is_object($mappedTarget)) {
throw new MappingTransformException(\sprintf('Cannot map "%s" to a non-object target of type "%s".', get_debug_type($source), get_debug_type($mappedTarget)));
Expand Down Expand Up @@ -123,7 +123,7 @@ public function map(object $source, object|string|null $target = null): object
}

$value = $this->getRawValue($source, $sourcePropertyName);
if (($if = $mapping->if) && ($fn = $this->getCallable($if, $this->conditionCallableLocator)) && !$this->call($fn, $value, $source)) {
if (($if = $mapping->if) && ($fn = $this->getCallable($if, $this->conditionCallableLocator)) && !$this->call($fn, $value, $source, $mappedTarget)) {
continue;
}

Expand Down Expand Up @@ -173,16 +173,16 @@ private function getRawValue(object $source, string $propertyName): mixed
private function getSourceValue(object $source, object $target, mixed $value, \SplObjectStorage $objectMap, ?Mapping $mapping = null): mixed
{
if ($mapping?->transform) {
$value = $this->applyTransforms($mapping, $value, $source);
$value = $this->applyTransforms($mapping, $value, $source, $target);
}

if (
\is_object($value)
&& ($innerMetadata = $this->metadataFactory->create($value))
&& ($mapTo = $this->getMapTarget($innerMetadata, $value, $source))
&& ($mapTo = $this->getMapTarget($innerMetadata, $value, $source, $target))
&& (\is_string($mapTo->target) && class_exists($mapTo->target))
) {
$value = $this->applyTransforms($mapTo, $value, $source);
$value = $this->applyTransforms($mapTo, $value, $source, $target);

if ($value === $source) {
$value = $target;
Expand Down Expand Up @@ -216,23 +216,23 @@ private function storeValue(string $propertyName, array &$mapToProperties, array
/**
* @param callable(): mixed $fn
*/
private function call(callable $fn, mixed $value, object $object): mixed
private function call(callable $fn, mixed $value, object $source, ?object $target = null): mixed
{
if (\is_string($fn)) {
return \call_user_func($fn, $value);
}

return $fn($value, $object);
return $fn($value, $source, $target);
}

/**
* @param Mapping[] $metadata
*/
private function getMapTarget(array $metadata, mixed $value, object $source): ?Mapping
private function getMapTarget(array $metadata, mixed $value, object $source, ?object $target): ?Mapping
{
$mapTo = null;
foreach ($metadata as $mapAttribute) {
if (($if = $mapAttribute->if) && ($fn = $this->getCallable($if, $this->conditionCallableLocator)) && !$this->call($fn, $value, $source)) {
if (($if = $mapAttribute->if) && ($fn = $this->getCallable($if, $this->conditionCallableLocator)) && !$this->call($fn, $value, $source, $target)) {
continue;
}

Expand All @@ -242,7 +242,7 @@ private function getMapTarget(array $metadata, mixed $value, object $source): ?M
return $mapTo;
}

private function applyTransforms(Mapping $map, mixed $value, object $object): mixed
private function applyTransforms(Mapping $map, mixed $value, object $source, ?object $target): mixed
{
if (!$transforms = $map->transform) {
return $value;
Expand All @@ -256,7 +256,7 @@ private function applyTransforms(Mapping $map, mixed $value, object $object): mi

foreach ($transforms as $transform) {
if ($fn = $this->getCallable($transform, $this->transformCallableLocator)) {
$value = $this->call($fn, $value, $object);
$value = $this->call($fn, $value, $source, $target);
}
}

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
<?php

/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <[email protected]>
*
* 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\MultipleTargetProperty;

use Symfony\Component\ObjectMapper\Attribute\Map;
use Symfony\Component\ObjectMapper\Condition\TargetClass;

#[Map(target: B::class)]
#[Map(target: C::class)]
class A
{
#[Map(target: 'foo', transform: 'strtoupper', if: new TargetClass(B::class))]
#[Map(target: 'bar')]
public string $something = 'test';

public string $doesNotExistInTargetB = 'foo';
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
<?php

/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <[email protected]>
*
* 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\MultipleTargetProperty;

class B
{
public string $foo;
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
<?php

/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <[email protected]>
*
* 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\MultipleTargetProperty;

class C
{
public string $foo = 'donotmap';
public string $bar;
public string $doesNotExistInTargetB;
}
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@
*/
class ConditionCallable implements ConditionCallableInterface
{
public function __invoke(mixed $value, object $object): bool
public function __invoke(mixed $value, object $source, ?object $target): bool
{
return 'ok' === $value;
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@
*/
class TransformCallable implements TransformCallableInterface
{
public function __invoke(mixed $value, object $object): mixed
public function __invoke(mixed $value, object $source, ?object $target): mixed
{
return "transformed$value";
}
Expand Down
18 changes: 18 additions & 0 deletions src/Symfony/Component/ObjectMapper/Tests/ObjectMapperTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,9 @@
use Symfony\Component\ObjectMapper\Tests\Fixtures\MapStruct\Target;
use Symfony\Component\ObjectMapper\Tests\Fixtures\MapTargetToSource\A as MapTargetToSourceA;
use Symfony\Component\ObjectMapper\Tests\Fixtures\MapTargetToSource\B as MapTargetToSourceB;
use Symfony\Component\ObjectMapper\Tests\Fixtures\MultipleTargetProperty\A as MultipleTargetPropertyA;
use Symfony\Component\ObjectMapper\Tests\Fixtures\MultipleTargetProperty\B as MultipleTargetPropertyB;
use Symfony\Component\ObjectMapper\Tests\Fixtures\MultipleTargetProperty\C as MultipleTargetPropertyC;
use Symfony\Component\ObjectMapper\Tests\Fixtures\MultipleTargets\A as MultipleTargetsA;
use Symfony\Component\ObjectMapper\Tests\Fixtures\MultipleTargets\C as MultipleTargetsC;
use Symfony\Component\ObjectMapper\Tests\Fixtures\Recursion\AB;
Expand Down Expand Up @@ -273,4 +276,19 @@ public function testMapTargetToSource()
$this->assertInstanceOf(MapTargetToSourceB::class, $b);
$this->assertSame('str', $b->target);
}

public function testMultipleTargetMapProperty()
{
$u = new MultipleTargetPropertyA();

$mapper = new ObjectMapper();
$b = $mapper->map($u, MultipleTargetPropertyB::class);
$this->assertInstanceOf(MultipleTargetPropertyB::class, $b);
$this->assertEquals($b->foo, 'TEST');
$c = $mapper->map($u, MultipleTargetPropertyC::class);
$this->assertInstanceOf(MultipleTargetPropertyC::class, $c);
$this->assertEquals($c->bar, 'test');
$this->assertEquals($c->foo, 'donotmap');
$this->assertEquals($c->doesNotExistInTargetB, 'foo');
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@
* Service used by "Map::transform".
*
* @template T of object
* @template T2 of object
*
* @experimental
*
Expand All @@ -25,6 +26,7 @@ interface TransformCallableInterface
/**
* @param mixed $value The value being mapped
* @param T $source The object we're working on
* @param T2|null $target The target we're mapping to
*/
public function __invoke(mixed $value, object $source): mixed;
public function __invoke(mixed $value, object $source, ?object $target): mixed;
}
Loading