8000 [FrameworkBundle] Object Mapper component bindings · symfony/symfony@17e35fd · GitHub
[go: up one dir, main page]

Skip to content

Commit 17e35fd

Browse files
committed
[FrameworkBundle] Object Mapper component bindings
1 parent 199baf7 commit 17e35fd

File tree

12 files changed

+219
-8
lines changed

12 files changed

+219
-8
lines changed

src/Symfony/Bundle/FrameworkBundle/DependencyInjection/FrameworkExtension.php

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -125,6 +125,8 @@
125125
use Symfony\Component\Notifier\Recipient\Recipient;
126126
use Symfony\Component\Notifier\TexterInterface;
127127
use Symfony\Component\Notifier\Transport\TransportFactoryInterface as NotifierTransportFactoryInterface;
128+
use Symfony\Component\ObjectMapper\CallableInterface;
129+
use Symfony\Component\ObjectMapper\ObjectMapperInterface;
128130
use Symfony\Component\Process\Messenger\RunProcessMessageHandler;
129131
use Symfony\Component\PropertyAccess\PropertyAccessor;
130132
use Symfony\Component\PropertyInfo\Extractor\PhpDocExtractor;
@@ -772,6 +774,12 @@ private function registerFormConfiguration(array $config, ContainerBuilder $cont
772774
if (!ContainerBuilder::willBeAvailable('symfony/translation', Translator::class, ['symfony/framework-bundle', 'symfony/form'])) {
773775
$container->removeDefinition('form.type_extension.upload.validator');
774776
}
777+
778+
if (interface_exists(ObjectMapperInterface::class)) {
779+
$loader->load('object_mapper.php');
780+
$container->registerForAutoconfiguration(CallableInterface::class)
781+
->addTag('object_mapper.callable');
782+
}
775783
}
776784

777785
private function registerHttpCacheConfiguration(array $config, ContainerBuilder $container, bool $httpMethodOverride): void
Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,35 @@
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\DependencyInjection\Loader\Configurator;
13+
14+
use Symfony\Component\ObjectMapper\CallablesLocator;
15+
use Symfony\Component\ObjectMapper\Metadata\MapperMetadataFactoryInterface;
16+
use Symfony\Component\ObjectMapper\Metadata\ReflectionMapperMetadataFactory;
17+
use Symfony\Component\ObjectMapper\ObjectMapper;
18+
use Symfony\Component\ObjectMapper\ObjectMapperInterface;
19+
20+
return static function (ContainerConfigurator $container) {
21+
$container->services()
22+
->set('object_mapper.metadata_factory', ReflectionMapperMetadataFactory::class)
23+
->alias(ReflectionMapperMetadataFactory::class, 'object_mapper.metadata_factory')
24+
->alias(MapperMetadataFactoryInterface::class, 'object_mapper.metadata_factory')
25+
26+
->set('object_mapper', ObjectMapper::class)
27+
->args([
28+
service('object_mapper.metadata_factory')->ignoreOnInvalid(),
29+
service('property_accessor')->ignoreOnInvalid(),
30+
tagged_locator('object_mapper.callable'),
31+
])
32+
->alias(ObjectMapper::class, 'object_mapper')
33+
->alias(ObjectMapperInterface::class, 'object_mapper')
34+
;
35+
};
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\Bundle\FrameworkBundle\Tests\Fixtures\ObjectMapper;
13+
14+
final class ObjectMapped
15+
{
16+
public string $a;
17+
}
Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
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\Bundle\FrameworkBundle\Tests\Fixtures\ObjectMapper;
13+
14+
use Symfony\Component\ObjectMapper\Attribute\Map;
15+
16+
#[Map(target: ObjectMapped::class)]
17+
final class ObjectToBeMapped
18+
{
19+
#[Map(transform: TransformCallable::class)]
20+
public string $a = 'nottransformed';
21+
}
Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
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\Bundle\FrameworkBundle\Tests\Fixtures\ObjectMapper;
13+
14+
use Symfony\Component\ObjectMapper\CallableInterface;
15+
16+
final class TransformCallable implements CallableInterface
17+
{
18+
public function __invoke(mixed $value, object $object): mixed
19+
{
20+
return 'transformed';
21+
}
22+
}
Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,31 @@
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\Bundle\FrameworkBundle\Tests\Functional;
13+
14+
use Symfony\Bundle\FrameworkBundle\Tests\Fixtures\ObjectMapper\ObjectMapped;
15+
use Symfony\Bundle\FrameworkBundle\Tests\Fixtures\ObjectMapper\ObjectToBeMapped;
16+
17+
/**
18+
* @author Kévin Dunglas <dunglas@gmail.com>
19+
*/
20+
class ObjectMapperTest extends AbstractWebTestCase
21+
{
22+
public function testObjectMapper(): void
23+
{
24+
static::bootKernel(['test_case' => 'ObjectMapper']);
25+
26+
/** @var Symfony\Component\ObjectMapper\ObjectMapperInterface<ObjectMapped> */
27+
$objectMapper = static::getContainer()->get('object_mapper.alias');
28+
$mapped = $objectMapper->map(new ObjectToBeMapped());
29+
$this->assertSame($mapped->a, 'transformed');
30+
}
31+
}
Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
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+
use Symfony\Bundle\FrameworkBundle\FrameworkBundle;
13+
14+
return [
15+
new FrameworkBundle(),
16+
];
Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
imports:
2+
- { resource: ../config/default.yml }
3+
4+
services:
5+
object_mapper.alias:
6+
alias: object_mapper
7+
public: true
8+
Symfony\Bundle\FrameworkBundle\Tests\Fixtures\ObjectMapper\TransformCallable:
9+
autoconfigure: true

src/Symfony/Bundle/FrameworkBundle/composer.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -60,6 +60,7 @@
6060
"symfony/security-bundle": "^6.4|^7.0",
6161
"symfony/semaphore": "^6.4|^7.0",
6262
"symfony/serializer": "^6.4|^7.0",
63+
"symfony/object-mapper": "^6.4|^7.0",
6364
"symfony/stopwatch": "^6.4|^7.0",
6465
"symfony/string": "^6.4|^7.0",
6566
"symfony/translation": "^6.4|^7.0",

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

Copy file name to clipboard
Lines changed: 6 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,8 @@
1111

1212
namespace Symfony\Component\ObjectMapper\Attribute;
1313

14+
use Symfony\Component\ObjectMapper\CallableInterface;
15+
1416
/**
1517
* Configures a class or a property to map to.
1618
*
@@ -24,10 +26,10 @@
2426
readonly class Map
2527
{
2628
/**
27-
* @param string|class-string|null $source The property or the class to map from
28-
* @param string|class-string|null $target The property or the class to map to
29-
* @param string|bool|callable(mixed $value, object $object): bool|null $if A boolean, Symfony service name or a callable that instructs whether to map
30-
* @param (string|callable(mixed $value, object $object): mixed)|(string|callable(mixed $value, object $object): mixed)[]|null $transform A Symfony service name or a callable that transform the value during mapping
29+
* @param string|class-string|null $source The property or the class to map from
30+
* @param string|class-string|null $target The property or the class to map to
31+
* @param string|bool|callable(mixed $value, object $object): bool|null $if A boolean, Symfony service name or a callable that instructs whether to map
32+
* @param (CallableInterface|string|callable(mixed $value, object $object): mixed)|(CallableInterface|string|callable(mixed $value, object $object): mixed)[]|null $transform A Symfony service name or a callable that transform the value during mapping
3133
*/
3234
public function __construct(
3335
public ?string $target = null,
Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
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+
* @experimental
16+
*
17+
* An interface representing that gets called by "Map::if" and "Map::transform".
18+
*
19+
* {@see Symfony\Component\ObjectMapper\Attribute\Map}
20+
*/
21+
interface CallableInterface
22+
{
23+
/**
24+
* @param mixed $value the value being mapped
25+
* @param mixed $object the object we're working on
26+
*/
27+
public function __invoke(mixed $value, object $object): mixed;
28+
}

src/Symfony/Component/ObjectMapper/ObjectMapper.php

Lines changed: 25 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@
1111

1212
namespace Symfony\Component\ObjectMapper;
1313

14+
use Psr\Container\ContainerInterface;
1415
use Symfony\Component\ObjectMapper\Exception\MappingException;
1516
use Symfony\Component\ObjectMapper\Exception\MappingTransformException;
1617
use Symfony\Component\ObjectMapper\Exception\ReflectionException;
@@ -32,9 +33,13 @@
3233
*/
3334
final class ObjectMapper implements ObjectMapperInterface
3435
{
36+
/**
37+
* @param ContainerInterface<CallableInterface> $callableLocator
38+
*/
3539
public function __construct(
3640
private readonly MapperMetadataFactoryInterface $metadataFactory = new ReflectionMapperMetadataFactory(),
3741
private readonly ?PropertyAccessorInterface $propertyAccessor = null,
42+
private readonly ?ContainerInterface $callableLocator = null,
3843
) {
3944
}
4045

@@ -114,7 +119,7 @@ public function map(object $source, object|string|null $target = null): object
114119
$propertyName = $property->getName();
115120
$mappings = $this->metadataFactory->create($source, $propertyName);
116121
foreach ($mappings as $mapping) {
117-
if (($fn = $mapping->if) && !$this->call($fn, null, $source)) {
122+
if (($if = $mapping->if) && ($fn = $this->getCallable($if)) && !$this->call($fn, null, $source)) {
118123
continue;
119124
}
120125

@@ -228,7 +233,7 @@ private function getMapTarget(array $metadata, mixed $value, object $source): ?M
228233
{
229234
$mapTo = null;
230235
foreach ($metadata as $mapAttribute) {
231-
if (($fn = $mapAttribute->if) && !$this->call($fn, $value, $source)) {
236+
if (($if = $mapAttribute->if) && ($fn = $this->getCallable($if)) && !$this->call($fn, $value, $source)) {
232237
continue;
233238
}
234239

@@ -251,11 +256,27 @@ private function applyTransforms(Mapping $map, mixed $value, object $object): mi
251256
}
252257

253258
foreach ($transforms as $transform) {
254-
if (\is_callable($transform)) {
255-
$value = $this->call($transform, $value, $object);
259+
if ($fn = $this->getCallable($transform)) {
260+
$value = $this->call($fn, $value, $object);
256261
}
257262
}
258263

259264
return $value;
260265
}
266+
267+
/**
268+
* @param (CallableInterface|string|callable(mixed $value, object $object): mixed) $fn
269+
*/
270+
private function getCallable(string|callable|CallableInterface $fn): ?callable
271+
{
272+
if (is_callable($fn)) {
273+
return $fn;
274+
}
275+
276+
if ($this->callableLocator?->has($fn)) {
277+
return $this->callableLocator->get($fn);
278+
}
279+
280+
return null;
281+
}
261282
}

0 commit comments

Comments
 (0)
0