From cc400c7143df262a27cace1672b891a0bbe24bc9 Mon Sep 17 00:00:00 2001 From: Joel Wurtz Date: Thu, 21 Mar 2024 00:43:26 +0100 Subject: [PATCH 1/2] feat(transformer): allow expression language for transformer, add provider for custom functions --- src/AutoMapper.php | 19 +++++++- src/EventListener/MapListener.php | 23 +++++----- src/GeneratedMapper.php | 8 ++++ .../MapMethodStatementsGenerator.php | 2 +- src/Generator/MapperGenerator.php | 2 +- src/Generator/PropertyConditionsGenerator.php | 10 +++-- src/Generator/PropertyStatementsGenerator.php | 2 +- src/Metadata/MetadataRegistry.php | 6 ++- src/Symfony/Bundle/config/event.php | 4 +- src/Symfony/Bundle/config/generator.php | 1 + src/Symfony/Bundle/config/symfony.php | 3 ++ src/Symfony/ExpressionLanguageProvider.php | 43 +++++++++++++++++++ .../ExpressionLanguageTransformer.php | 38 ++++++++++++++++ tests/AutoMapperBaseTest.php | 18 +++++--- tests/AutoMapperMapToTest.php | 17 +++++++- tests/Fixtures/MapTo/FooMapTo.php | 2 + 16 files changed, 169 insertions(+), 29 deletions(-) create mode 100644 src/Symfony/ExpressionLanguageProvider.php create mode 100644 src/Transformer/ExpressionLanguageTransformer.php diff --git a/src/AutoMapper.php b/src/AutoMapper.php index 44886f64..4eb045fe 100644 --- a/src/AutoMapper.php +++ b/src/AutoMapper.php @@ -11,10 +11,12 @@ use AutoMapper\Loader\EvalLoader; use AutoMapper\Loader\FileLoader; use AutoMapper\Metadata\MetadataRegistry; +use AutoMapper\Symfony\ExpressionLanguageProvider; use AutoMapper\Transformer\PropertyTransformer\PropertyTransformerInterface; use AutoMapper\Transformer\PropertyTransformer\PropertyTransformerRegistry; use AutoMapper\Transformer\TransformerFactoryInterface; use Doctrine\Common\Annotations\AnnotationReader; +use Symfony\Component\ExpressionLanguage\ExpressionLanguage; use Symfony\Component\Serializer\Mapping\ClassDiscriminatorFromClassMetadata; use Symfony\Component\Serializer\Mapping\Factory\ClassMetadataFactory; use Symfony\Component\Serializer\Mapping\Loader\AnnotationLoader; @@ -42,6 +44,7 @@ public function __construct( private readonly ClassLoaderInterface $classLoader, private readonly PropertyTransformerRegistry $propertyTransformerRegistry, private readonly MetadataRegistry $metadataRegistry, + private readonly ?ExpressionLanguageProvider $expressionLanguageProvider = null, ) { } @@ -75,6 +78,10 @@ public function getMapper(string $source, string $target): MapperInterface $mapper->injectMappers($this); $mapper->setPropertyTransformers($this->propertyTransformerRegistry->getPropertyTransformers()); + if (null !== $this->expressionLanguageProvider) { + $mapper->setExpressionLanguageProvider($this->expressionLanguageProvider); + } + /** @var GeneratedMapper|GeneratedMapper, Target>|GeneratedMapper> */ return $this->mapperRegistry[$className]; } @@ -126,6 +133,7 @@ public static function create( AdvancedNameConverterInterface $nameConverter = null, array $transformerFactories = [], iterable $propertyTransformers = [], + ExpressionLanguageProvider $expressionLanguageProvider = null, ): self { if (class_exists(AttributeLoader::class)) { $loaderClass = new AttributeLoader(); @@ -135,6 +143,12 @@ public static function create( $loaderClass = null; } + $expressionLanguage = new ExpressionLanguage(); + + if (null !== $expressionLanguageProvider) { + $expressionLanguage->registerProvider($expressionLanguageProvider); + } + $classMetadataFactory = null; $classDiscriminatorFromClassMetadata = null; @@ -144,11 +158,12 @@ public static function create( } $customTransformerRegistry = new PropertyTransformerRegistry($propertyTransformers); - $metadataRegistry = MetadataRegistry::create($configuration, $customTransformerRegistry, $transformerFactories, $classMetadataFactory, $nameConverter); + $metadataRegistry = MetadataRegistry::create($configuration, $customTransformerRegistry, $transformerFactories, $classMetadataFactory, $nameConverter, $expressionLanguage); $mapperGenerator = new MapperGenerator( new ClassDiscriminatorResolver($classDiscriminatorFromClassMetadata), $configuration, + $expressionLanguage, ); if (null === $cacheDirectory) { @@ -157,6 +172,6 @@ public static function create( $loader = new FileLoader($mapperGenerator, $metadataRegistry, $cacheDirectory); } - return new self($loader, $customTransformerRegistry, $metadataRegistry); + return new self($loader, $customTransformerRegistry, $metadataRegistry, $expressionLanguageProvider); } } diff --git a/src/EventListener/MapListener.php b/src/EventListener/MapListener.php index 43d9465e..f1600610 100644 --- a/src/EventListener/MapListener.php +++ b/src/EventListener/MapListener.php @@ -8,10 +8,13 @@ use AutoMapper\Attribute\MapTo; use AutoMapper\Exception\BadMapDefinitionException; use AutoMapper\Transformer\CallableTransformer; +use AutoMapper\Transformer\ExpressionLanguageTransformer; use AutoMapper\Transformer\PropertyTransformer\PropertyTransformer; use AutoMapper\Transformer\PropertyTransformer\PropertyTransformerInterface; use AutoMapper\Transformer\PropertyTransformer\PropertyTransformerRegistry; use AutoMapper\Transformer\TransformerInterface; +use Symfony\Component\ExpressionLanguage\ExpressionLanguage; +use Symfony\Component\ExpressionLanguage\SyntaxError; use Symfony\Component\PropertyInfo\Extractor\ReflectionExtractor; use Symfony\Component\String\Inflector\EnglishInflector; use Symfony\Component\String\Inflector\InflectorInterface; @@ -23,6 +26,7 @@ { public function __construct( private PropertyTransformerRegistry $propertyTransformerRegistry, + private ExpressionLanguage $expressionLanguage, private InflectorInterface $inflector = new EnglishInflector(), ) { } @@ -46,16 +50,7 @@ protected function getTransformerFromMapAttribute(string $class, MapTo|MapFrom $ $transformer = new PropertyTransformer($transformerCallable); } elseif (\is_callable($transformerCallable, false, $callableName)) { $transformer = new CallableTransformer($callableName); - } elseif (\is_string($transformerCallable)) { - // Check the method exist on the class - if (!method_exists($class, $transformerCallable)) { - if (class_exists($transformerCallable)) { - throw new BadMapDefinitionException(sprintf('Transformer "%s" targeted by %s transformer does not exist on class "%s", did you register it ?.', $transformerCallable, $attribute::class, $class)); - } - - throw new BadMapDefinitionException(sprintf('Method "%s" targeted by %s transformer does not exist on class "%s".', $transformerCallable, $attribute::class, $class)); - } - + } elseif (\is_string($transformerCallable) && method_exists($class, $transformerCallable)) { $reflMethod = new \ReflectionMethod($class, $transformerCallable); if ($reflMethod->isStatic()) { @@ -63,6 +58,14 @@ protected function getTransformerFromMapAttribute(string $class, MapTo|MapFrom $ } else { $transformer = new CallableTransformer($transformerCallable, $fromSource, !$fromSource); } + } elseif (\is_string($transformerCallable)) { + try { + $expression = $this->expressionLanguage->compile($transformerCallable, ['value' => 'source', 'context']); + } catch (SyntaxError $e) { + throw new BadMapDefinitionException(sprintf('Transformer "%s" targeted by %s transformer on class "%s" is not valid.', $transformerCallable, $attribute::class, $class), 0, $e); + } + + $transformer = new ExpressionLanguageTransformer($expression); } else { throw new BadMapDefinitionException(sprintf('Callable "%s" targeted by %s transformer on class "%s" is not valid.', json_encode($transformerCallable), $attribute::class, $class)); } diff --git a/src/GeneratedMapper.php b/src/GeneratedMapper.php index e96bd7f9..eef69d2d 100644 --- a/src/GeneratedMapper.php +++ b/src/GeneratedMapper.php @@ -4,6 +4,7 @@ namespace AutoMapper; +use AutoMapper\Symfony\ExpressionLanguageProvider; use AutoMapper\Transformer\PropertyTransformer\PropertyTransformerInterface; /** @@ -38,6 +39,8 @@ abstract class GeneratedMapper implements MapperInterface /** @var array */ protected array $transformers = []; + protected ?ExpressionLanguageProvider $expressionLanguageProvider = null; + /** * Inject sub mappers. */ @@ -62,4 +65,9 @@ public function setPropertyTransformers(array $transformers): void { $this->transformers = $transformers; } + + public function setExpressionLanguageProvider(ExpressionLanguageProvider $expressionLanguageProvider): void + { + $this->expressionLanguageProvider = $expressionLanguageProvider; + } } diff --git a/src/Generator/MapMethodStatementsGenerator.php b/src/Generator/MapMethodStatementsGenerator.php index 28134579..4744d57e 100644 --- a/src/Generator/MapMethodStatementsGenerator.php +++ b/src/Generator/MapMethodStatementsGenerator.php @@ -28,7 +28,7 @@ public function __construct( DiscriminatorStatementsGenerator $discriminatorStatementsGenerator, CachedReflectionStatementsGenerator $cachedReflectionStatementsGenerator, - ExpressionLanguage $expressionLanguage = new ExpressionLanguage(), + ExpressionLanguage $expressionLanguage, private bool $allowReadOnlyTargetToPopulate = false, ) { $this->createObjectStatementsGenerator = new CreateTargetStatementsGenerator( diff --git a/src/Generator/MapperGenerator.php b/src/Generator/MapperGenerator.php index 2633a69f..e7cb5c77 100644 --- a/src/Generator/MapperGenerator.php +++ b/src/Generator/MapperGenerator.php @@ -37,7 +37,7 @@ public function __construct( ClassDiscriminatorResolver $classDiscriminatorResolver, Configuration $configuration, - ExpressionLanguage $expressionLanguage = new ExpressionLanguage() + ExpressionLanguage $expressionLanguage, ) { $this->mapperConstructorGenerator = new MapperConstructorGenerator( $cachedReflectionStatementsGenerator = new CachedReflectionStatementsGenerator() diff --git a/src/Generator/PropertyConditionsGenerator.php b/src/Generator/PropertyConditionsGenerator.php index ba621847..fcc43ab3 100644 --- a/src/Generator/PropertyConditionsGenerator.php +++ b/src/Generator/PropertyConditionsGenerator.php @@ -32,7 +32,7 @@ class_alias(Expr\ArrayItem::class, ArrayItem::class); private Parser $parser; public function __construct( - private ExpressionLanguage $expressionLanguage = new ExpressionLanguage(), + private ExpressionLanguage $expressionLanguage, Parser $parser = null, ) { $this->parser = $parser ?? (new ParserFactory())->createForHostVersion(); @@ -247,7 +247,9 @@ private function customCondition(GeneratorMetadata $metadata, PropertyMetadata $ new Arg(new Expr\Variable('value')), ] ); - } elseif ($argumentsCount > 2) { + } + + if ($argumentsCount > 2) { throw new \LogicException('Callable condition must have 1 or 2 arguments required, but it has ' . $argumentsCount); } } @@ -259,7 +261,9 @@ private function customCondition(GeneratorMetadata $metadata, PropertyMetadata $ new Arg(new Expr\Variable('context')), ] ); - } elseif ($metadata->mapperMetadata->sourceReflectionClass !== null && $metadata->mapperMetadata->sourceReflectionClass->hasMethod($propertyMetadata->if)) { + } + + if ($metadata->mapperMetadata->sourceReflectionClass !== null && $metadata->mapperMetadata->sourceReflectionClass->hasMethod($propertyMetadata->if)) { $reflectionMethod = $metadata->mapperMetadata->sourceReflectionClass->getMethod($propertyMetadata->if); if ($reflectionMethod->isStatic()) { diff --git a/src/Generator/PropertyStatementsGenerator.php b/src/Generator/PropertyStatementsGenerator.php index 9917eb6f..0a492224 100644 --- a/src/Generator/PropertyStatementsGenerator.php +++ b/src/Generator/PropertyStatementsGenerator.php @@ -22,7 +22,7 @@ private PropertyConditionsGenerator $propertyConditionsGenerator; public function __construct( - ExpressionLanguage $expressionLanguage = new ExpressionLanguage() + ExpressionLanguage $expressionLanguage ) { $this->propertyConditionsGenerator = new PropertyConditionsGenerator($expressionLanguage); } diff --git a/src/Metadata/MetadataRegistry.php b/src/Metadata/MetadataRegistry.php index d7a0e41d..0a54564a 100644 --- a/src/Metadata/MetadataRegistry.php +++ b/src/Metadata/MetadataRegistry.php @@ -36,6 +36,7 @@ use AutoMapper\Transformer\UniqueTypeTransformerFactory; use Symfony\Component\EventDispatcher\EventDispatcher; use Symfony\Component\EventDispatcher\EventDispatcherInterface; +use Symfony\Component\ExpressionLanguage\ExpressionLanguage; use Symfony\Component\PropertyInfo\Extractor\PhpStanExtractor; use Symfony\Component\PropertyInfo\Extractor\ReflectionExtractor; use Symfony\Component\PropertyInfo\PropertyInfoExtractor; @@ -232,6 +233,7 @@ public static function create( array $transformerFactories = [], ClassMetadataFactory $classMetadataFactory = null, AdvancedNameConverterInterface $nameConverter = null, + ExpressionLanguage $expressionLanguage = new ExpressionLanguage(), ): self { // Create property info extractors $flags = ReflectionExtractor::ALLOW_PUBLIC; @@ -255,8 +257,8 @@ public static function create( } $eventDispatcher->addListener(PropertyMetadataEvent::class, new MapToContextListener($reflectionExtractor)); - $eventDispatcher->addListener(GenerateMapperEvent::class, new MapToListener($customTransformerRegistry)); - $eventDispatcher->addListener(GenerateMapperEvent::class, new MapFromListener($customTransformerRegistry)); + $eventDispatcher->addListener(GenerateMapperEvent::class, new MapToListener($customTransformerRegistry, $expressionLanguage)); + $eventDispatcher->addListener(GenerateMapperEvent::class, new MapFromListener($customTransformerRegistry, $expressionLanguage)); $propertyInfoExtractor = new PropertyInfoExtractor( listExtractors: [$reflectionExtractor], diff --git a/src/Symfony/Bundle/config/event.php b/src/Symfony/Bundle/config/event.php index 49a2cc86..1b1d43a1 100644 --- a/src/Symfony/Bundle/config/event.php +++ b/src/Symfony/Bundle/config/event.php @@ -19,10 +19,10 @@ ->args([service('automapper.property_info.reflection_extractor')]) ->tag('kernel.event_listener', ['event' => PropertyMetadataEvent::class, 'priority' => 64]) ->set(MapToListener::class) - ->args([service(PropertyTransformerRegistry::class)]) + ->args([service(PropertyTransformerRegistry::class), service('automapper.expression_language')]) ->tag('kernel.event_listener', ['event' => GenerateMapperEvent::class, 'priority' => 64]) ->set(MapFromListener::class) - ->args([service(PropertyTransformerRegistry::class)]) + ->args([service(PropertyTransformerRegistry::class), service('automapper.expression_language')]) ->tag('kernel.event_listener', ['event' => GenerateMapperEvent::class, 'priority' => 32]) ->set(AdvancedNameConverterListener::class) diff --git a/src/Symfony/Bundle/config/generator.php b/src/Symfony/Bundle/config/generator.php index f8cb0031..9f1822e2 100644 --- a/src/Symfony/Bundle/config/generator.php +++ b/src/Symfony/Bundle/config/generator.php @@ -15,6 +15,7 @@ ->args([ service(ClassDiscriminatorResolver::class), service(Configuration::class), + service('automapper.expression_language'), ]) ->set(ClassDiscriminatorResolver::class) diff --git a/src/Symfony/Bundle/config/symfony.php b/src/Symfony/Bundle/config/symfony.php index 2d4f3500..3ed85364 100644 --- a/src/Symfony/Bundle/config/symfony.php +++ b/src/Symfony/Bundle/config/symfony.php @@ -7,6 +7,7 @@ use AutoMapper\AutoMapperRegistryInterface; use AutoMapper\Symfony\Bundle\CacheWarmup\CacheWarmer; use AutoMapper\Symfony\Bundle\CacheWarmup\ConfigurationCacheWarmerLoader; +use Symfony\Component\DependencyInjection\ExpressionLanguage; return static function (ContainerConfigurator $container) { $container->services() @@ -23,5 +24,7 @@ [], // mappers list from config ]) ->tag('automapper.cache_warmer_loader') + + ->set('automapper.expression_language', ExpressionLanguage::class) ; }; diff --git a/src/Symfony/ExpressionLanguageProvider.php b/src/Symfony/ExpressionLanguageProvider.php new file mode 100644 index 00000000..f16156fd --- /dev/null +++ b/src/Symfony/ExpressionLanguageProvider.php @@ -0,0 +1,43 @@ + $functions + */ + public function __construct( + private ServiceProviderInterface $functions + ) { + } + + public function getFunctions(): array + { + $functions = []; + + foreach ($this->functions->getProvidedServices() as $function => $type) { + $functions[] = new ExpressionFunction( + $function, + static fn (...$args) => sprintf('($this->expressionLanguageProvider->get(%s)(%s))', var_export($function, true), implode(', ', $args)), + fn ($values, ...$args) => $this->get($function)(...$args) + ); + } + + return $functions; + } + + public function get(string $function): callable + { + return $this->functions->get($function); + } +} diff --git a/src/Transformer/ExpressionLanguageTransformer.php b/src/Transformer/ExpressionLanguageTransformer.php new file mode 100644 index 00000000..57923add --- /dev/null +++ b/src/Transformer/ExpressionLanguageTransformer.php @@ -0,0 +1,38 @@ +parser = $parser ?? (new ParserFactory())->createForHostVersion(); + } + + public function transform(Expr $input, Expr $target, PropertyMetadata $propertyMapping, UniqueVariableScope $uniqueVariableScope, Expr\Variable $source): array + { + $expr = $this->parser->parse('expression . ';')[0] ?? null; + + if ($expr instanceof Stmt\Expression) { + return [$expr->expr, []]; + } + + throw new \LogicException('Cannot use callback or create expression language condition from expression "' . $this->expression . "'"); + } +} diff --git a/tests/AutoMapperBaseTest.php b/tests/AutoMapperBaseTest.php index 1c464a93..8159b393 100644 --- a/tests/AutoMapperBaseTest.php +++ b/tests/AutoMapperBaseTest.php @@ -6,6 +6,7 @@ use AutoMapper\AutoMapper; use AutoMapper\Configuration; +use AutoMapper\Symfony\ExpressionLanguageProvider; use PHPUnit\Framework\TestCase; use Symfony\Component\Filesystem\Filesystem; @@ -29,19 +30,26 @@ protected function buildAutoMapper( string $classPrefix = 'Mapper_', array $transformerFactories = [], array $propertyTransformers = [], - string $dateTimeFormat = \DateTimeInterface::RFC3339 + string $dateTimeFormat = \DateTimeInterface::RFC3339, + ?ExpressionLanguageProvider $expressionLanguageProvider = null, ): AutoMapper { - $fs = new Filesystem(); - $fs->remove(__DIR__ . '/cache/'); + // $fs = new Filesystem(); + // $fs->remove(__DIR__ . '/cache/'); $configuration = new Configuration( classPrefix: $classPrefix, allowConstructor: $allowConstructor, dateTimeFormat: $dateTimeFormat, mapPrivateProperties: $mapPrivatePropertiesAndMethod, - allowReadOnlyTargetToPopulate: $allowReadOnlyTargetToPopulate + allowReadOnlyTargetToPopulate: $allowReadOnlyTargetToPopulate, ); - return $this->autoMapper = AutoMapper::create($configuration, cacheDirectory: __DIR__ . '/cache/', transformerFactories: $transformerFactories, propertyTransformers: $propertyTransformers); + return $this->autoMapper = AutoMapper::create( + $configuration, + cacheDirectory: __DIR__ . '/cache/', + transformerFactories: $transformerFactories, + propertyTransformers: $propertyTransformers, + expressionLanguageProvider: $expressionLanguageProvider, + ); } } diff --git a/tests/AutoMapperMapToTest.php b/tests/AutoMapperMapToTest.php index c70f6ca8..e42cb6db 100644 --- a/tests/AutoMapperMapToTest.php +++ b/tests/AutoMapperMapToTest.php @@ -6,12 +6,14 @@ use AutoMapper\Exception\BadMapDefinitionException; use AutoMapper\MapperContext; +use AutoMapper\Symfony\ExpressionLanguageProvider; use AutoMapper\Tests\Fixtures\MapTo\BadMapTo; use AutoMapper\Tests\Fixtures\MapTo\BadMapToTransformer; use AutoMapper\Tests\Fixtures\MapTo\Bar; use AutoMapper\Tests\Fixtures\MapTo\FooMapTo; use AutoMapper\Tests\Fixtures\Transformer\CustomTransformer\FooDependency; use AutoMapper\Tests\Fixtures\Transformer\CustomTransformer\TransformerWithDependency; +use Symfony\Component\DependencyInjection\ServiceLocator; /** * @author Joel Wurtz @@ -32,7 +34,11 @@ public function testMapTo() public function testMapToArray() { - $this->buildAutoMapper(propertyTransformers: [new TransformerWithDependency(new FooDependency())]); + $expressionLanguageProvider = new ExpressionLanguageProvider(new ServiceLocator([ + 'transformerWithDependency' => fn () => fn () => new TransformerWithDependency(new FooDependency()), + ])); + + $this->buildAutoMapper(propertyTransformers: [new TransformerWithDependency(new FooDependency())], expressionLanguageProvider: $expressionLanguageProvider); $foo = new FooMapTo('foo'); $bar = $this->autoMapper->map($foo, 'array'); @@ -50,6 +56,8 @@ public function testMapToArray() $this->assertSame('if', $bar['ifCallableStatic']); $this->assertSame('if', $bar['ifCallable']); $this->assertSame('if', $bar['ifCallableOther']); + $this->assertSame('transformed', $bar['transformFromExpressionLanguage']); + $this->assertSame('bar', $bar['transformWithExpressionFunction']); $foo = new FooMapTo('bar'); $bar = $this->autoMapper->map($foo, 'array'); @@ -67,11 +75,16 @@ public function testMapToArray() $this->assertSame('transformFromStringInstance_bar', $bar['transformFromStringInstance']); $this->assertSame('transformFromStringStatic_bar', $bar['transformFromStringStatic']); $this->assertSame('bar', $bar['transformFromCustomTransformerService']); + $this->assertSame('not transformed', $bar['transformFromExpressionLanguage']); } public function testMapToArrayGroups() { - $this->buildAutoMapper(propertyTransformers: [new TransformerWithDependency(new FooDependency())]); + $expressionLanguageProvider = new ExpressionLanguageProvider(new ServiceLocator([ + 'transformerWithDependency' => fn () => fn () => new TransformerWithDependency(new FooDependency()), + ])); + + $this->buildAutoMapper(propertyTransformers: [new TransformerWithDependency(new FooDependency())], expressionLanguageProvider: $expressionLanguageProvider); $foo = new FooMapTo('foo'); $bar = $this->autoMapper->map($foo, 'array'); diff --git a/tests/Fixtures/MapTo/FooMapTo.php b/tests/Fixtures/MapTo/FooMapTo.php index a4053b77..1c4c8a0d 100644 --- a/tests/Fixtures/MapTo/FooMapTo.php +++ b/tests/Fixtures/MapTo/FooMapTo.php @@ -8,6 +8,7 @@ use AutoMapper\Tests\Fixtures\Transformer\CustomTransformer\TransformerWithDependency; #[MapTo('array', name: 'externalProperty', transformer: 'transformExternalProperty', groups: ['group1'])] +#[MapTo('array', name: 'transformWithExpressionFunction', transformer: "transformerWithDependency().transform('foo', source, context)")] class FooMapTo { public function __construct( @@ -17,6 +18,7 @@ public function __construct( #[MapTo('array', name: 'transformFromStringInstance', transformer: 'transformFromStringInstance')] #[MapTo('array', name: 'transformFromStringStatic', transformer: 'transformFromStringStatic')] #[MapTo('array', name: 'transformFromCustomTransformerService', transformer: TransformerWithDependency::class)] + #[MapTo('array', name: 'transformFromExpressionLanguage', transformer: "source.foo === 'foo' ? 'transformed' : 'not transformed'")] public string $foo ) { } From acec495fe7a4c52f459c7898f9da6dc023b38951 Mon Sep 17 00:00:00 2001 From: Joel Wurtz Date: Thu, 21 Mar 2024 09:49:46 +0100 Subject: [PATCH 2/2] feat(transformer): add attribute to automatically bind service to expression language in bundle --- .../Symfony/AdvancedNameConverterListener.php | 4 +- .../AsAutoMapperExpressionService.php | 48 ++++++++++++ .../AutoMapperExtension.php | 1 + src/Symfony/Bundle/config/automapper.php | 2 + .../Bundle/config/expression_language.php | 38 ++++++++++ src/Symfony/Bundle/config/symfony.php | 3 - tests/AutoMapperBaseTest.php | 4 +- tests/Bundle/Fixtures/FooMapTo.php | 76 +++++++++++++++++++ tests/Bundle/Resources/App/AppKernel.php | 70 +---------------- .../Resources/App/Service/FooService.php | 16 ++++ .../Resources/App/Service/IdNameConverter.php | 52 +++++++++++++ .../App/Service/YearOfBirthTransformer.php | 29 +++++++ tests/Bundle/Resources/App/config.yml | 10 +-- tests/Bundle/ServiceInstantiationTest.php | 28 ++++++- 14 files changed, 299 insertions(+), 82 deletions(-) create mode 100644 src/Symfony/Attribute/AsAutoMapperExpressionService.php create mode 100644 src/Symfony/Bundle/config/expression_language.php create mode 100644 tests/Bundle/Fixtures/FooMapTo.php create mode 100644 tests/Bundle/Resources/App/Service/FooService.php create mode 100644 tests/Bundle/Resources/App/Service/IdNameConverter.php create mode 100644 tests/Bundle/Resources/App/Service/YearOfBirthTransformer.php diff --git a/src/EventListener/Symfony/AdvancedNameConverterListener.php b/src/EventListener/Symfony/AdvancedNameConverterListener.php index 24edfd26..579da857 100644 --- a/src/EventListener/Symfony/AdvancedNameConverterListener.php +++ b/src/EventListener/Symfony/AdvancedNameConverterListener.php @@ -16,11 +16,11 @@ public function __construct( public function __invoke(PropertyMetadataEvent $event): void { - if ($event->mapperMetadata->source === 'array' || $event->mapperMetadata->source === \stdClass::class) { + if (($event->mapperMetadata->source === 'array' || $event->mapperMetadata->source === \stdClass::class) && $event->source->name === $event->target->name) { $event->source->name = $this->nameConverter->denormalize($event->target->name, $event->mapperMetadata->target); } - if ($event->mapperMetadata->target === 'array' || $event->mapperMetadata->target === \stdClass::class) { + if (($event->mapperMetadata->target === 'array' || $event->mapperMetadata->target === \stdClass::class) && $event->source->name === $event->target->name) { $event->target->name = $this->nameConverter->normalize($event->source->name, $event->mapperMetadata->source); } } diff --git a/src/Symfony/Attribute/AsAutoMapperExpressionService.php b/src/Symfony/Attribute/AsAutoMapperExpressionService.php new file mode 100644 index 00000000..d7d61c88 --- /dev/null +++ b/src/Symfony/Attribute/AsAutoMapperExpressionService.php @@ -0,0 +1,48 @@ + $alias, 'priority' => $priority]); + } +} diff --git a/src/Symfony/Bundle/DependencyInjection/AutoMapperExtension.php b/src/Symfony/Bundle/DependencyInjection/AutoMapperExtension.php index 6bbf4dc1..a3945955 100644 --- a/src/Symfony/Bundle/DependencyInjection/AutoMapperExtension.php +++ b/src/Symfony/Bundle/DependencyInjection/AutoMapperExtension.php @@ -43,6 +43,7 @@ public function load(array $configs, ContainerBuilder $container): void $loader->load('automapper.php'); $loader->load('custom_transformers.php'); $loader->load('event.php'); + $loader->load('expression_language.php'); $loader->load('generator.php'); $loader->load('metadata.php'); $loader->load('symfony.php'); diff --git a/src/Symfony/Bundle/config/automapper.php b/src/Symfony/Bundle/config/automapper.php index ddb0b734..05fc3593 100644 --- a/src/Symfony/Bundle/config/automapper.php +++ b/src/Symfony/Bundle/config/automapper.php @@ -13,6 +13,7 @@ use AutoMapper\Loader\EvalLoader; use AutoMapper\Loader\FileLoader; use AutoMapper\Metadata\MetadataRegistry; +use AutoMapper\Symfony\ExpressionLanguageProvider; use AutoMapper\Transformer\PropertyTransformer\PropertyTransformerRegistry; return static function (ContainerConfigurator $container) { @@ -22,6 +23,7 @@ service(ClassLoaderInterface::class), service(PropertyTransformerRegistry::class), service(MetadataRegistry::class), + service(ExpressionLanguageProvider::class), ]) ->alias(AutoMapperInterface::class, AutoMapper::class)->public() ->alias(AutoMapperRegistryInterface::class, AutoMapper::class)->public() diff --git a/src/Symfony/Bundle/config/expression_language.php b/src/Symfony/Bundle/config/expression_language.php new file mode 100644 index 00000000..4a2cefc6 --- /dev/null +++ b/src/Symfony/Bundle/config/expression_language.php @@ -0,0 +1,38 @@ +services() + ->set('automapper.expression_language', ExpressionLanguage::class) + ->call('registerProvider', [ + service(ExpressionLanguageProvider::class), + ]) + + ->set(ExpressionLanguageProvider::class) + ->args([ + tagged_locator('automapper.expression_language_function', 'function'), + ]) + ->tag('automapper.expression_language_provider') + + ->set('automapper.expression_language.env', \Closure::class) + ->factory([\Closure::class, 'fromCallable']) + ->args([ + [service('service_container'), 'getEnv'], + ]) + ->tag('automapper.expression_language_function', ['function' => 'env']) + + ->set('automapper.expression_language.service', \Closure::class) + ->public() + ->factory([\Closure::class, 'fromCallable']) + ->args([ + [tagged_locator('automapper.expression_service', 'alias'), 'get'], + ]) + ->tag('automapper.expression_language_function', ['function' => 'service']) + ; +}; diff --git a/src/Symfony/Bundle/config/symfony.php b/src/Symfony/Bundle/config/symfony.php index 3ed85364..2d4f3500 100644 --- a/src/Symfony/Bundle/config/symfony.php +++ b/src/Symfony/Bundle/config/symfony.php @@ -7,7 +7,6 @@ use AutoMapper\AutoMapperRegistryInterface; use AutoMapper\Symfony\Bundle\CacheWarmup\CacheWarmer; use AutoMapper\Symfony\Bundle\CacheWarmup\ConfigurationCacheWarmerLoader; -use Symfony\Component\DependencyInjection\ExpressionLanguage; return static function (ContainerConfigurator $container) { $container->services() @@ -24,7 +23,5 @@ [], // mappers list from config ]) ->tag('automapper.cache_warmer_loader') - - ->set('automapper.expression_language', ExpressionLanguage::class) ; }; diff --git a/tests/AutoMapperBaseTest.php b/tests/AutoMapperBaseTest.php index 8159b393..1dbe2071 100644 --- a/tests/AutoMapperBaseTest.php +++ b/tests/AutoMapperBaseTest.php @@ -33,8 +33,8 @@ protected function buildAutoMapper( string $dateTimeFormat = \DateTimeInterface::RFC3339, ?ExpressionLanguageProvider $expressionLanguageProvider = null, ): AutoMapper { - // $fs = new Filesystem(); - // $fs->remove(__DIR__ . '/cache/'); + $fs = new Filesystem(); + $fs->remove(__DIR__ . '/cache/'); $configuration = new Configuration( classPrefix: $classPrefix, diff --git a/tests/Bundle/Fixtures/FooMapTo.php b/tests/Bundle/Fixtures/FooMapTo.php new file mode 100644 index 00000000..746e4972 --- /dev/null +++ b/tests/Bundle/Fixtures/FooMapTo.php @@ -0,0 +1,76 @@ +foo === 'foo'; + } + + public function shouldMapNotStatic(): bool + { + return $this->foo === 'foo'; + } + + public function transformExternalProperty($value) + { + return 'external'; + } +} diff --git a/tests/Bundle/Resources/App/AppKernel.php b/tests/Bundle/Resources/App/AppKernel.php index da1bb961..96489294 100644 --- a/tests/Bundle/Resources/App/AppKernel.php +++ b/tests/Bundle/Resources/App/AppKernel.php @@ -2,23 +2,14 @@ declare(strict_types=1); -namespace DummyApp; +namespace AutoMapper\Tests\Bundle\Resources\App; -use AutoMapper\Metadata\MapperMetadata; -use AutoMapper\Metadata\SourcePropertyMetadata; -use AutoMapper\Metadata\TargetPropertyMetadata; -use AutoMapper\Metadata\TypesMatching; use AutoMapper\Symfony\Bundle\AutoMapperBundle; -use AutoMapper\Tests\Bundle\Fixtures\User; -use AutoMapper\Tests\Bundle\Fixtures\UserDTO; -use AutoMapper\Transformer\PropertyTransformer\PropertyTransformerInterface; -use AutoMapper\Transformer\PropertyTransformer\PropertyTransformerSupportInterface; use Symfony\Bundle\FrameworkBundle\FrameworkBundle; use Symfony\Bundle\FrameworkBundle\Kernel\MicroKernelTrait; use Symfony\Component\Config\Loader\LoaderInterface; use Symfony\Component\DependencyInjection\ContainerBuilder; use Symfony\Component\HttpKernel\Kernel; -use Symfony\Component\Serializer\NameConverter\AdvancedNameConverterInterface; class AppKernel extends Kernel { @@ -44,62 +35,3 @@ public function getProjectDir(): string return __DIR__ . '/..'; } } - -class YearOfBirthTransformer implements PropertyTransformerInterface, PropertyTransformerSupportInterface -{ - public function transform(mixed $value, object|array $user, array $context): mixed - { - \assert($user instanceof User); - - return ((int) date('Y')) - ((int) $user->age); - } - - public function supports(TypesMatching $types, SourcePropertyMetadata $source, TargetPropertyMetadata $target, MapperMetadata $mapperMetadata): bool - { - return User::class === $mapperMetadata->source && UserDTO::class === $mapperMetadata->target && 'yearOfBirth' === $source->name; - } -} - -if (Kernel::MAJOR_VERSION < 6) { - class IdNameConverter implements AdvancedNameConverterInterface - { - public function normalize($propertyName, ?string $class = null, ?string $format = null, array $context = []): string - { - if ('id' === $propertyName) { - return '@id'; - } - - return $propertyName; - } - - public function denormalize($propertyName, ?string $class = null, ?string $format = null, array $context = []): string - { - if ('@id' === $propertyName) { - return 'id'; - } - - return $propertyName; - } - } -} else { - class IdNameConverter implements AdvancedNameConverterInterface - { - public function normalize(string $propertyName, ?string $class = null, ?string $format = null, array $context = []): string - { - if ('id' === $propertyName) { - return '@id'; - } - - return $propertyName; - } - - public function denormalize(string $propertyName, ?string $class = null, ?string $format = null, array $context = []): string - { - if ('@id' === $propertyName) { - return 'id'; - } - - return $propertyName; - } - } -} diff --git a/tests/Bundle/Resources/App/Service/FooService.php b/tests/Bundle/Resources/App/Service/FooService.php new file mode 100644 index 00000000..adcd92ad --- /dev/null +++ b/tests/Bundle/Resources/App/Service/FooService.php @@ -0,0 +1,16 @@ +age); + } + + public function supports(TypesMatching $types, SourcePropertyMetadata $source, TargetPropertyMetadata $target, MapperMetadata $mapperMetadata): bool + { + return User::class === $mapperMetadata->source && UserDTO::class === $mapperMetadata->target && 'yearOfBirth' === $source->name; + } +} diff --git a/tests/Bundle/Resources/App/config.yml b/tests/Bundle/Resources/App/config.yml index 31a5455e..514f52e8 100644 --- a/tests/Bundle/Resources/App/config.yml +++ b/tests/Bundle/Resources/App/config.yml @@ -5,7 +5,7 @@ framework: automapper: normalizer: true - name_converter: DummyApp\IdNameConverter + name_converter: AutoMapper\Tests\Bundle\Resources\App\Service\IdNameConverter map_private_properties: true warmup: - { source: 'AutoMapper\Tests\Bundle\Fixtures\NestedObject', target: 'array' } @@ -15,8 +15,8 @@ services: autoconfigure: true autowire: true - DummyApp\YearOfBirthTransformer: ~ + AutoMapper\Tests\Bundle\Resources\App\Service\FooService: ~ + AutoMapper\Tests\Bundle\Resources\App\Service\YearOfBirthTransformer: ~ dummy_app.year_of_birth_transformer: - class: DummyApp\YearOfBirthTransformer - DummyApp\IdNameConverter: ~ - AutoMapper\Tests\Bundle\Resources\App\Transformer\ArrayToMoneyTransformer: ~ + class: AutoMapper\Tests\Bundle\Resources\App\Service\YearOfBirthTransformer + AutoMapper\Tests\Bundle\Resources\App\Service\IdNameConverter: ~ diff --git a/tests/Bundle/ServiceInstantiationTest.php b/tests/Bundle/ServiceInstantiationTest.php index 8f4a4f83..6aeab1a3 100644 --- a/tests/Bundle/ServiceInstantiationTest.php +++ b/tests/Bundle/ServiceInstantiationTest.php @@ -10,6 +10,7 @@ use AutoMapper\Tests\Bundle\Fixtures\ClassWithMapToContextAttribute; use AutoMapper\Tests\Bundle\Fixtures\ClassWithPrivateProperty; use AutoMapper\Tests\Bundle\Fixtures\DTOWithEnum; +use AutoMapper\Tests\Bundle\Fixtures\FooMapTo; use AutoMapper\Tests\Bundle\Fixtures\SomeEnum; use AutoMapper\Tests\Bundle\Fixtures\User; use AutoMapper\Tests\Bundle\Fixtures\UserDTO; @@ -22,7 +23,7 @@ protected function setUp(): void { static::$class = null; $_SERVER['KERNEL_DIR'] = __DIR__ . '/Resources/App'; - $_SERVER['KERNEL_CLASS'] = 'DummyApp\AppKernel'; + $_SERVER['KERNEL_CLASS'] = 'AutoMapper\Tests\Bundle\Resources\App\AppKernel'; (new Filesystem())->remove(__DIR__ . '/Resources/var/cache/test'); } @@ -145,4 +146,29 @@ public function testMapToContextAttribute(): void ) ); } + + public function testMapTo() + { + static::bootKernel(); + $container = static::$kernel->getContainer(); + $autoMapper = $container->get(AutoMapperInterface::class); + + $foo = new FooMapTo('foo'); + $bar = $autoMapper->map($foo, 'array'); + + $this->assertIsArray($bar); + $this->assertArrayNotHasKey('bar', $bar); + $this->assertArrayNotHasKey('a', $bar); + $this->assertSame('foo', $bar['baz']); + $this->assertSame('foo', $bar['foo']); + $this->assertSame('transformFromIsCallable_foo', $bar['transformFromIsCallable']); + $this->assertSame('transformFromStringInstance_foo', $bar['transformFromStringInstance']); + $this->assertSame('transformFromStringStatic_foo', $bar['transformFromStringStatic']); + $this->assertSame('if', $bar['if']); + $this->assertSame('if', $bar['ifCallableStatic']); + $this->assertSame('if', $bar['ifCallable']); + $this->assertSame('if', $bar['ifCallableOther']); + $this->assertSame('transformed', $bar['transformFromExpressionLanguage']); + $this->assertSame('foo', $bar['transformWithExpressionFunction']); + } }