-
-
Notifications
You must be signed in to change notification settings - Fork 9.7k
[ObjectMapper] Add MappingException, MapCollection and MapTree attributes #60432
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
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
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 <fabien@symfony.com> | ||
* | ||
* For the full copyright and license information, please view the LICENSE | ||
* file that was distributed with this source code. | ||
*/ | ||
|
||
namespace Symfony\Component\ObjectMapper\Attribute; | ||
|
||
use Attribute; | ||
|
||
/** | ||
* Maps an array of associative data to an array of typed objects. | ||
* | ||
* Example: | ||
* #[MapCollection(of: ProductDTO::class)] | ||
* public array $products; | ||
* | ||
* @experimental | ||
* | ||
* @author Devoton <oton.traore@email.com> | ||
*/ | ||
#[Attribute(Attribute::TARGET_PROPERTY)] | ||
class MapCollection | ||
{ | ||
/** | ||
* @param class-string $of The class to map each element to. | ||
*/ | ||
public function __construct(public string $of) {} | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,38 @@ | ||
<?php | ||
|
||
/* | ||
* This file is part of the Symfony package. | ||
* | ||
* (c) Fabien Potencier <fabien@symfony.com> | ||
* | ||
* For the full copyright and license information, please view the LICENSE | ||
* file that was distributed with this source code. | ||
*/ | ||
|
||
namespace Symfony\Component\ObjectMapper\Attribute; | ||
|
||
use Attribute; | ||
|
||
/** | ||
* Maps a tree-structured array to a nested object graph. | ||
* | ||
* Example: | ||
* #[MapTree(of: CategoryDTO::class)] | ||
* public array $children; | ||
* | ||
* @experimental | ||
* | ||
* @author Devoton <oton.traore@email.com> | ||
*/ | ||
#[Attribute(Attribute::TARGET_PROPERTY)] | ||
class MapTree | ||
{ | ||
/** | ||
* @param class-string $of | ||
* @param string $childrenProperty the property name that holds children | ||
*/ | ||
public function __construct( | ||
public string $of, | ||
public string $childrenProperty = 'children' | ||
) {} | ||
} | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I'm not sure about this, could you show the use case? The ObjectMapper alread supports recursive structures. |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,5 +1,4 @@ | ||
<?php | ||
|
||
/* | ||
* This file is part of the Symfony package. | ||
* | ||
|
@@ -15,7 +14,21 @@ | |
* @experimental | ||
* | ||
* @author Antoine Bluchet <soyuka@gmail.com> | ||
* @author Devoton <oton.traore@gmail.com> | ||
*/ | ||
class MappingException extends RuntimeException | ||
class MappingException extends \RuntimeException | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. why the change? |
||
{ | ||
public static function forProperty(string $sourcePath, string $targetPath, string $expected, string $actual, $value = null): self | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. this is interesting but not used anywhere? |
||
{ | ||
$message = sprintf( | ||
'Cannot map `%s` => `%s`: Expected %s, got %s%s', | ||
$sourcePath, | ||
$targetPath, | ||
$expected, | ||
$actual, | ||
$value !== null ? sprintf(' (value: %s)', var_export($value, true)) : '' | ||
); | ||
|
||
return new self($message); | ||
} | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -14,6 +14,8 @@ | |
use Psr\Container\ContainerInterface; | ||
use Symfony\Component\ObjectMapper\Exception\MappingException; | ||
use Symfony\Component\ObjectMapper\Exception\MappingTransformException; | ||
use Symfony\Component\ObjectMapper\Mapping\MapCollection; | ||
use Symfony\Component\ObjectMapper\Mapping\MapTree; | ||
use Symfony\Component\ObjectMapper\Metadata\Mapping; | ||
use Symfony\Component\ObjectMapper\Metadata\ObjectMapperMetadataFactoryInterface; | ||
use Symfony\Component\ObjectMapper\Metadata\ReflectionObjectMapperMetadataFactory; | ||
|
@@ -25,6 +27,7 @@ | |
* @experimental | ||
* | ||
* @author Antoine Bluchet <soyuka@gmail.com> | ||
* @author Devoton <oton.traore@gmail.com> | ||
nicolas-grekas marked this conversation as resolved.
Show resolved
Hide resolved
|
||
*/ | ||
final class ObjectMapper implements ObjectMapperInterface | ||
{ | ||
|
@@ -55,11 +58,11 @@ | |
$mappingToObject = \is_object($target); | ||
|
||
if (!$target) { | ||
throw new MappingException(\sprintf('Mapping target not found for source "%s".', get_debug_type($source))); | ||
throw new MappingException(\sprintf('Mapping target not found for source \"%s\".', get_debug_type($source))); | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. why the There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. fabbot's suggestion, but this is a false-positive, they should all be removed please @devoton |
||
} | ||
|
||
if (\is_string($target) && !class_exists($target)) { | ||
throw new MappingException(\sprintf('Mapping target class "%s" does not exist for source "%s".', $target, get_debug_type($source))); | ||
throw new MappingException(\sprintf('Mapping target class \"%s\" does not exist for source \"%s\".', $target, get_debug_type($source))); | ||
} | ||
|
||
try { | ||
|
@@ -73,12 +76,12 @@ | |
$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))); | ||
throw new MappingTransformException(\sprintf('Cannot map \"%s\" to a non-object target of type \"%s\".', get_debug_type($source), get_debug_type($mappedTarget))); | ||
} | ||
} | ||
|
||
if (!is_a($mappedTarget, $targetRefl->getName(), false)) { | ||
throw new MappingException(\sprintf('Expected the mapped object to be an instance of "%s" but got "%s".', $targetRefl->getName(), get_debug_type($mappedTarget))); | ||
throw new MappingException(\sprintf('Expected the mapped object to be an instance of \"%s\" but got \"%s\".', $targetRefl->getName(), get_debug_type($mappedTarget))); | ||
} | ||
|
||
$this->objectMap[$source] = $mappedTarget; | ||
|
@@ -96,7 +99,6 @@ | |
continue; | ||
} | ||
|
||
// this may be filled later on see storeValue | ||
$ctorArguments[$parameterName] = $parameter->isDefaultValueAvailable() ? $parameter->getDefaultValue() : null; | ||
} | ||
|
||
|
@@ -176,6 +178,58 @@ | |
$value = $this->applyTransforms($mapping, $value, $source, $target); | ||
} | ||
|
||
if ($mapping instanceof MapCollection) { | ||
Check failure on line 181 in src/Symfony/Component/ObjectMapper/ObjectMapper.php
|
||
if (!\is_iterable($value)) { | ||
return $value; | ||
} | ||
|
||
$mappedCollection = []; | ||
foreach ($value as $item) { | ||
if (\is_object($item)) { | ||
$mappedCollection[] = $this->map($item, $mapping->target); | ||
} else { | ||
$mappedCollection[] = $item; | ||
} | ||
} | ||
|
||
return $mappedCollection; | ||
} | ||
|
||
if ($mapping instanceof MapTree) { | ||
if (!\is_iterable($value)) { | ||
return $value; | ||
} | ||
|
||
$mappedTree = []; | ||
foreach ($value as $node) { | ||
if (!\is_object($node)) { | ||
$mappedTree[] = $node; | ||
continue; | ||
} | ||
|
||
$mappedNode = $this->map($node, $mapping->target); | ||
|
||
$children = $this->getRawValue($node, $mapping->childrenProperty); | ||
$mappedChildren = []; | ||
|
||
if (\is_iterable($children)) { | ||
foreach ($children as $child) { | ||
$mappedChildren[] = $this->map($child, $mapping->target); | ||
} | ||
} | ||
|
||
if ($this->propertyAccessor) { | ||
$this->propertyAccessor->setValue($mappedNode, $mapping->childrenProperty, $mappedChildren); | ||
} else { | ||
$mappedNode->{$mapping->childrenProperty} = $mappedChildren; | ||
} | ||
|
||
$mappedTree[] = $mappedNode; | ||
} | ||
|
||
return $mappedTree; | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I don't think its a good idea to include these functionalities directly in the ObjectMapper, we should instead create new kinds of Mappers that compose with the ObjectMapper |
||
} | ||
|
||
if ( | ||
\is_object($value) | ||
&& ($innerMetadata = $this->metadataFactory->create($value)) | ||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,119 @@ | ||
<?php | ||
/* | ||
* This file is part of the Symfony package. | ||
* | ||
* (c) Fabien Potencier <fabien@symfony.com> | ||
* | ||
* For the full copyright and license information, please view the LICENSE | ||
* file that was distributed with this source code. | ||
*/ | ||
|
||
|
||
namespace Symfony\Component\ObjectMapper\Tests; | ||
|
||
use PHPUnit\Framework\TestCase; | ||
use Symfony\Component\ObjectMapper\ObjectMapper; | ||
use Symfony\Component\ObjectMapper\Attribute\Map; | ||
use Symfony\Component\ObjectMapper\Attribute\MapCollection; | ||
use Symfony\Component\ObjectMapper\Attribute\MapTree; | ||
|
||
class ObjectMapperCollectionTreeTest extends TestCase | ||
{ | ||
public function testMapCollectionOfObjects(): void | ||
{ | ||
$mapper = new ObjectMapper(); | ||
$source = new SourceWithCollection([ | ||
new SourceItem('A'), | ||
new SourceItem('B'), | ||
]); | ||
|
||
$result = $mapper->map($source, TargetWithCollection::class); | ||
|
||
$this->assertInstanceOf(TargetWithCollection::class, $result); | ||
$this->assertCount(2, $result->items); | ||
$this->assertInstanceOf(TargetItem::class, $result->items[0]); | ||
$this->assertInstanceOf(TargetItem::class, $result->items[1]); | ||
$this->assertEquals('A', $result->items[0]->label); | ||
$this->assertEquals('B', $result->items[1]->label); | ||
} | ||
|
||
public function testMapTreeStructure(): void | ||
{ | ||
$source = new SourceNode('Root', [ | ||
new SourceNode('Child 1'), | ||
new SourceNode('Child 2', [new SourceNode('Grandchild 1')]), | ||
]); | ||
|
||
$mapper = new ObjectMapper(); | ||
$result = $mapper->map($source, TargetNode::class); | ||
|
||
var_dump($result); // Debug pour voir le résultat du mapping | ||
|
||
$this->assertInstanceOf(TargetNode::class, $result); | ||
$this->assertEquals('Root', $result->name); | ||
$this->assertCount(2, $result->children); | ||
|
||
$this->assertEquals('Child 1', $result->children[0]->name); | ||
$this->assertCount(0, $result->children[0]->children); | ||
|
||
$this->assertEquals('Child 2', $result->children[1]->name); | ||
$this->assertCount(1, $result->children[1]->children); | ||
$this->assertEquals('Grandchild 1', $result->children[1]->children[0]->name); | ||
} | ||
} | ||
|
||
class SourceItem | ||
{ | ||
#[Map(to: 'label')] | ||
public string $name; | ||
|
||
public function __construct(string $name) | ||
{ | ||
$this->name = $name; | ||
} | ||
} | ||
|
||
class TargetItem | ||
{ | ||
public function __construct(public string $label) {} | ||
} | ||
|
||
class SourceWithCollection | ||
{ | ||
#[MapCollection(of: TargetItem::class)] | ||
public array $items; | ||
|
||
public function __construct(array $items) | ||
{ | ||
$this->items = $items; | ||
} | ||
} | ||
|
||
class TargetWithCollection | ||
{ | ||
public array $items = []; | ||
} | ||
|
||
class SourceNode | ||
{ | ||
#[MapTree(of: TargetNode::class, childrenProperty: 'children')] | ||
public array $children = []; | ||
|
||
public function __construct( | ||
#[Map] | ||
public string $name, | ||
array $children = [] | ||
) { | ||
$this->children = $children; | ||
} | ||
} | ||
|
||
class TargetNode | ||
{ | ||
public array $children = []; | ||
|
||
public function __construct(public string $name = '', array $children = []) | ||
{ | ||
$this->children = $children; | ||
} | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I'm 👎 for this as I don't really see the point, could you give me a real life example where you'd want something like this?
If we start supporting arrays in the ObjectMapper it can become quite complex, we should instead use a CollectionMapper but I'm quite not sure of the functional benefits behind it.