Skip to content
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

feat(transformer): allow expression language for transformer, add provider for custom functions #84

Merged
merged 2 commits into from
Mar 21, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
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