Skip to content

Commit

Permalink
Merge pull request #84 from jolicode/feat/transformer-expression-lang…
Browse files Browse the repository at this point in the history
…uage-and-provider

feat(transformer): allow expression language for transformer, add provider for custom functions
  • Loading branch information
joelwurtz committed Mar 21, 2024
2 parents 76ffa2b + acec495 commit c216372
Show file tree
Hide file tree
Showing 27 changed files with 461 additions and 104 deletions.
19 changes: 17 additions & 2 deletions src/AutoMapper.php
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -42,6 +44,7 @@ public function __construct(
private readonly ClassLoaderInterface $classLoader,
private readonly PropertyTransformerRegistry $propertyTransformerRegistry,
private readonly MetadataRegistry $metadataRegistry,
private readonly ?ExpressionLanguageProvider $expressionLanguageProvider = null,
) {
}

Expand Down Expand Up @@ -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<Source, Target>|GeneratedMapper<array<mixed>, Target>|GeneratedMapper<Source, array<mixed>> */
return $this->mapperRegistry[$className];
}
Expand Down Expand Up @@ -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();
Expand All @@ -135,6 +143,12 @@ public static function create(
$loaderClass = null;
}

$expressionLanguage = new ExpressionLanguage();

if (null !== $expressionLanguageProvider) {
$expressionLanguage->registerProvider($expressionLanguageProvider);
}

$classMetadataFactory = null;
$classDiscriminatorFromClassMetadata = null;

Expand All @@ -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) {
Expand All @@ -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);
}
}
23 changes: 13 additions & 10 deletions src/EventListener/MapListener.php
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -23,6 +26,7 @@
{
public function __construct(
private PropertyTransformerRegistry $propertyTransformerRegistry,
private ExpressionLanguage $expressionLanguage,
private InflectorInterface $inflector = new EnglishInflector(),
) {
}
Expand All @@ -46,23 +50,22 @@ 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()) {
$transformer = new CallableTransformer($class . '::' . $transformerCallable);
} 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));
}
Expand Down
4 changes: 2 additions & 2 deletions src/EventListener/Symfony/AdvancedNameConverterListener.php
Original file line number Diff line number Diff line change
Expand Up @@ -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);
}
}
Expand Down
8 changes: 8 additions & 0 deletions src/GeneratedMapper.php
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@

namespace AutoMapper;

use AutoMapper\Symfony\ExpressionLanguageProvider;
use AutoMapper\Transformer\PropertyTransformer\PropertyTransformerInterface;

/**
Expand Down Expand Up @@ -38,6 +39,8 @@ abstract class GeneratedMapper implements MapperInterface
/** @var array<string, PropertyTransformerInterface> */
protected array $transformers = [];

protected ?ExpressionLanguageProvider $expressionLanguageProvider = null;

/**
* Inject sub mappers.
*/
Expand All @@ -62,4 +65,9 @@ public function setPropertyTransformers(array $transformers): void
{
$this->transformers = $transformers;
}

public function setExpressionLanguageProvider(ExpressionLanguageProvider $expressionLanguageProvider): void
{
$this->expressionLanguageProvider = $expressionLanguageProvider;
}
}
2 changes: 1 addition & 1 deletion src/Generator/MapMethodStatementsGenerator.php
Original file line number Diff line number Diff line change
Expand Up @@ -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(
Expand Down
2 changes: 1 addition & 1 deletion src/Generator/MapperGenerator.php
Original file line number Diff line number Diff line change
Expand Up @@ -37,7 +37,7 @@
public function __construct(
ClassDiscriminatorResolver $classDiscriminatorResolver,
Configuration $configuration,
ExpressionLanguage $expressionLanguage = new ExpressionLanguage()
ExpressionLanguage $expressionLanguage,
) {
$this->mapperConstructorGenerator = new MapperConstructorGenerator(
$cachedReflectionStatementsGenerator = new CachedReflectionStatementsGenerator()
Expand Down
10 changes: 7 additions & 3 deletions src/Generator/PropertyConditionsGenerator.php
Original file line number Diff line number Diff line change
Expand Up @@ -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();
Expand Down Expand Up @@ -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);
}
}
Expand All @@ -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()) {
Expand Down
2 changes: 1 addition & 1 deletion src/Generator/PropertyStatementsGenerator.php
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@
private PropertyConditionsGenerator $propertyConditionsGenerator;

public function __construct(
ExpressionLanguage $expressionLanguage = new ExpressionLanguage()
ExpressionLanguage $expressionLanguage
) {
$this->propertyConditionsGenerator = new PropertyConditionsGenerator($expressionLanguage);
}
Expand Down
6 changes: 4 additions & 2 deletions src/Metadata/MetadataRegistry.php
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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;
Expand All @@ -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],
Expand Down
48 changes: 48 additions & 0 deletions src/Symfony/Attribute/AsAutoMapperExpressionService.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
<?php

declare(strict_types=1);

namespace AutoMapper\Symfony\Attribute;

use Symfony\Component\DependencyInjection\Attribute\AutoconfigureTag;

/**
* Service tag to autoconfigure auto mapper expression services.
*
* You can tag a service:
*
* #[AsAutoMapperExpressionService('foo')]
* class SomeFooService
* {
* public function bar(DummyObject $object): string
* {
* // ...
* }
*
* public function isOk(): bool
* {
* // ...
* }
*
* Then you can use the tagged service in the transformer or check property of MapTo / MapFrom attribute
*
* class DummyObject
* {
* #[MapTo('array', transformer: 'service("foo").bar(source)', if: 'service("foo").isOk()')]
* public string $foo;
* }
*/
#[\Attribute(\Attribute::TARGET_CLASS)]
class AsAutoMapperExpressionService extends AutoconfigureTag
{
/**
* @param string|null $alias The alias of the service to use it in routing condition expressions
* @param int $priority Defines a priority that allows the routing condition service to override a service with the same alias
*/
public function __construct(
?string $alias = null,
int $priority = 0,
) {
parent::__construct('automapper.expression_service', ['alias' => $alias, 'priority' => $priority]);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -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');
Expand Down
2 changes: 2 additions & 0 deletions src/Symfony/Bundle/config/automapper.php
Original file line number Diff line number Diff line change
Expand Up @@ -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) {
Expand All @@ -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()
Expand Down
4 changes: 2 additions & 2 deletions src/Symfony/Bundle/config/event.php
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down
38 changes: 38 additions & 0 deletions src/Symfony/Bundle/config/expression_language.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
<?php

declare(strict_types=1);

namespace Symfony\Component\DependencyInjection\Loader\Configurator;

use AutoMapper\Symfony\ExpressionLanguageProvider;
use Symfony\Component\DependencyInjection\ExpressionLanguage;

return static function (ContainerConfigurator $container) {
$container->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'])
;
};
1 change: 1 addition & 0 deletions src/Symfony/Bundle/config/generator.php
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@
->args([
service(ClassDiscriminatorResolver::class),
service(Configuration::class),
service('automapper.expression_language'),
])

->set(ClassDiscriminatorResolver::class)
Expand Down
Loading

0 comments on commit c216372

Please sign in to comment.