Skip to content

Commit 139a158

Browse files
committed
[AutoMapper] Object to Object mapper component
1 parent fbf6f56 commit 139a158

19 files changed

+491
-0
lines changed
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
/Tests export-ignore
2+
/phpunit.xml.dist export-ignore
3+
/.gitattributes export-ignore
4+
/.gitignore export-ignore
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
vendor/
2+
composer.lock
3+
phpunit.xml
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
<?php
2+
3+
namespace Symfony\Component\AutoMapper\Attributes;
4+
5+
use Attribute;
6+
7+
/**
8+
* Condition whether to map a property.
9+
*
10+
* @experimental
11+
* @author Antoine Bluchet <soyuka@gmail.com>
12+
*/
13+
#[Attribute(Attribute::TARGET_PROPERTY)]
14+
final class MapIf
15+
{
16+
/**
17+
* @param callable $if
18+
*/
19+
public function __construct(public mixed $if)
20+
{
21+
}
22+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
<?php
2+
3+
namespace Symfony\Component\AutoMapper\Attributes;
4+
5+
use Attribute;
6+
7+
/**
8+
* Configure a class or a property to map to.
9+
*
10+
* @experimental
11+
* @author Antoine Bluchet <soyuka@gmail.com>
12+
*/
13+
#[Attribute(Attribute::TARGET_CLASS | Attribute::TARGET_PROPERTY)]
14+
final class MapTo
15+
{
16+
public function __construct(public string $to)
17+
{
18+
}
19+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
<?php
2+
3+
namespace Symfony\Component\AutoMapper\Attributes;
4+
5+
use Attribute;
6+
7+
/**
8+
* Transform the mapped value with a funtion.
9+
*
10+
* @experimental
11+
* @author Antoine Bluchet <soyuka@gmail.com>
12+
*/
13+
#[Attribute(Attribute::TARGET_PROPERTY)]
14+
final class MapWith
15+
{
16+
/**
17+
* @param callable $with
18+
*/
19+
public function __construct(public mixed $with)
20+
{
21+
}
22+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,122 @@
1+
<?php
2+
3+
namespace Symfony\Component\AutoMapper;
4+
5+
use Psr\Container\ContainerInterface;
6+
use Symfony\Component\AutoMapper\Exception\RuntimeException;
7+
use Symfony\Component\AutoMapper\Attributes\MapIf;
8+
use Symfony\Component\AutoMapper\Attributes\MapTo;
9+
use Symfony\Component\AutoMapper\Attributes\MapWith;
10+
use Symfony\Component\PropertyAccess\PropertyAccessorInterface;
11+
12+
/**
13+
* Object to object mapper.
14+
*
15+
* @experimental
16+
* @author Antoine Bluchet <soyuka@gmail.com>
17+
*/
18+
final class AutoMapper implements AutoMapperInterface
19+
{
20+
public function __construct(private ?PropertyAccessorInterface $propertyAccessor = null, private ?ContainerInterface $serviceLocator = null)
21+
{
22+
}
23+
24+
/**
25+
* {@inheritdoc}
26+
*/
27+
public function map(object $object, object|string|null $to = null): mixed
28+
{
29+
$refl = new \ReflectionClass($object);
30+
31+
if (!$to) {
32+
$to = $this->getAttribute($refl, MapTo::class, true)->to;
33+
}
34+
35+
$arguments = [];
36+
if (is_object($to)) {
37+
$toRefl = new \ReflectionClass(get_class($to));
38+
$mapped = $to;
39+
} else {
40+
$toRefl = new \ReflectionClass($to);
41+
$mapped = $toRefl->newInstanceWithoutConstructor();
42+
}
43+
44+
$constructor = $toRefl->getConstructor();
45+
foreach ($constructor?->getParameters() ?? [] as $parameter) {
46+
$arguments[$parameter->getName()] = $parameter->isDefaultValueAvailable() ? $parameter->getDefaultValue() : null;
47+
}
48+
49+
foreach ($refl->getProperties() as $property) {
50+
$propertyName = $property->getName();
51+
$mapTo = $this->getAttribute($property, MapTo::class)?->to ?? $propertyName;
52+
if (!$toRefl->hasProperty($mapTo)) {
53+
continue;
54+
}
55+
56+
$value = $this->propertyAccessor ? $this->propertyAccessor->getValue($object, $propertyName) : $object->{$propertyName};
57+
$mapIf = $this->getCallable($this->getAttribute($property, MapIf::class)?->if);
58+
if (is_callable($mapIf) && false === $this->call($mapIf, $value, $object)) {
59+
continue;
60+
}
61+
62+
$mapWith = $this->getCallable($this->getAttribute($property, MapWith::class)?->with);
63+
if (is_callable($mapWith)) {
64+
$value = $this->call($mapWith, $value, $object);
65+
}
66+
67+
if (is_object($value) && $to = $this->getAttribute(new \ReflectionClass(get_class($value)), MapTo::class)?->to) {
68+
$value = $this->map($value, $to);
69+
}
70+
71+
if (array_key_exists($mapTo, $arguments)) {
72+
$arguments[$mapTo] = $value;
73+
} else {
74+
$this->propertyAccessor ? $this->propertyAccessor->setValue($mapped, $mapTo, $value) : ($mapped->{$mapTo} = $value);
75+
}
76+
}
77+
78+
$constructor->invokeArgs($mapped, $arguments);
79+
80+
return $mapped;
81+
}
82+
83+
private function call(callable $fn, mixed $value, object $object): mixed
84+
{
85+
$refl = new \ReflectionFunction(\Closure::fromCallable($fn));
86+
$withParameters = $refl->getParameters();
87+
$withArgs = [$value];
88+
89+
// Let's not send object if we don't need to, gives the ability to call native functions
90+
foreach ($withParameters as $parameter) {
91+
if ($parameter->getName() === 'object') {
92+
$withArgs['object'] = $object;
93+
break;
94+
}
95+
}
96+
97+
return call_user_func_array($fn, $withArgs);
98+
}
99+
100+
private function getCallable(string|callable|null $fn): ?callable
101+
{
102+
if ($this->serviceLocator && is_string($fn) && $this->serviceLocator->has($fn)) {
103+
return $this->serviceLocator->get($fn);
104+
}
105+
106+
return $fn;
107+
}
108+
109+
/**
110+
* @param class-string $name
111+
*/
112+
private function getAttribute(mixed $refl, string $name, bool $throw = false): mixed
113+
{
114+
$a = $refl->getAttributes($name)[0] ?? null;
115+
116+
if ($throw && !$a) {
117+
throw new RuntimeException(sprintf('Attribute of type "%s" expected on "%s.', $name, $refl->getName()));
118+
}
119+
120+
return $a ? $a->newInstance() : $a;
121+
}
122+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
<?php
2+
3+
namespace Symfony\Component\AutoMapper;
4+
5+
/**
6+
* Object to object mapper.
7+
*
8+
* @experimental
9+
* @author Antoine Bluchet <soyuka@gmail.com>
10+
*/
11+
interface AutoMapperInterface {
12+
/**
13+
* @param object $object The object to map from
14+
* @param null|class-string|object $to The object or class to map to
15+
*/
16+
public function map(object $object, object|string|null $to = null): mixed;
17+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
CHANGELOG
2+
=========
3+
4+
6.4
5+
---
6+
7+
* Automapper component
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
<?php
2+
3+
namespace Symfony\Component\AutoMapper\Exception;
4+
5+
/**
6+
* @experimental
7+
* @author Antoine Bluchet <soyuka@gmail.com>
8+
*/
9+
interface ExceptionInterface extends \Throwable
10+
{
11+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
<?php
2+
3+
namespace Symfony\Component\AutoMapper\Exception;
4+
5+
/**
6+
* @experimental
7+
* @author Antoine Bluchet <soyuka@gmail.com>
8+
*/
9+
class RuntimeException extends \RuntimeException implements ExceptionInterface
10+
{
11+
}
+19
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
Copyright (c) 2004-present Fabien Potencier
2+
3+
Permission is hereby granted, free of charge, to any person obtaining a copy
4+
of this software and associated documentation files (the "Software"), to deal
5+
in the Software without restriction, including without limitation the rights
6+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
7+
copies of the Software, and to permit persons to whom the Software is furnished
8+
to do so, subject to the following conditions:
9+
10+
The above copyright notice and this permission notice shall be included in all
11+
copies or substantial portions of the Software.
12+
13+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
14+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
15+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
16+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
17+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
18+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
19+
THE SOFTWARE.
+14
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
AutoMapper Component
2+
================
3+
4+
The AutoMapper component allows you to map an object to another object,
5+
facilitating the mapping using attributes.
6+
7+
Resources
8+
---------
9+
10+
* [Documentation](https://symfony.com/doc/current/components/automapper.html)
11+
* [Contributing](https://symfony.com/doc/current/contributing/index.html)
12+
* [Report issues](https://github.com/symfony/symfony/issues) and
13+
[send Pull Requests](https://github.com/symfony/symfony/pulls)
14+
in the [main Symfony repository](https://github.com/symfony/symfony)
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,41 @@
1+
<?php
2+
3+
namespace Symfony\Component\Automapper\Tests\Fixtures;
4+
5+
use Symfony\Component\AutoMapper\Attributes\MapIf;
6+
use Symfony\Component\AutoMapper\Attributes\MapTo;
7+
use Symfony\Component\AutoMapper\Attributes\MapWith;
8+
9+
#[MapTo(B::class)]
10+
class A
11+
{
12+
#[MapTo('bar')]
13+
public string $foo;
14+
15+
public string $baz;
16+
17+
public string $notinb;
18+
19+
#[MapWith('strtoupper')]
20+
public string $transform;
21+
22+
#[MapWith([A::class, 'concatFn'])]
23+
public ?string $concat = null;
24+
25+
#[MapIf('boolval')]
26+
public bool $nomap = false;
27+
28+
public C $relation;
29+
30+
public D $relationNotMapped;
31+
32+
public function getConcat()
33+
{
34+
return 'should';
35+
}
36+
37+
public static function concatFn($v, $object): string
38+
{
39+
return $v . $object->foo . $object->baz;
40+
}
41+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
<?php
2+
3+
namespace Symfony\Component\Automapper\Tests\Fixtures;
4+
5+
class B
6+
{
7+
public function __construct(private string $bar)
8+
{
9+
}
10+
public string $baz;
11+
public string $transform;
12+
public string $concat;
13+
public bool $nomap = true;
14+
public int $id;
15+
public D $relation;
16+
public D $relationNotMapped;
17+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
<?php
2+
3+
namespace Symfony\Component\Automapper\Tests\Fixtures;
4+
5+
use Symfony\Component\AutoMapper\Attributes\MapTo;
6+
7+
#[MapTo(D::class)]
8+
class C
9+
{
10+
public function __construct(#[MapTo('baz')] public string $foo, #[MapTo('bat')] public string $bar)
11+
{
12+
}
13+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
<?php
2+
3+
namespace Symfony\Component\AutoMapper\Tests\Fixtures;
4+
5+
class D
6+
{
7+
public function __construct(public string $baz, public string $bat)
8+
{
9+
}
10+
}

0 commit comments

Comments
 (0)