From 964c9eaebff3c823ab760e9932120d9938678749 Mon Sep 17 00:00:00 2001 From: Joel Wurtz Date: Wed, 20 Mar 2024 00:04:37 +0100 Subject: [PATCH 1/2] feat(if): add if feature to map to / map from attributes --- composer.json | 1 + src/Attribute/MapFrom.php | 2 ++ src/Attribute/MapTo.php | 2 ++ src/Event/PropertyMetadataEvent.php | 1 + src/EventListener/MapFromListener.php | 1 + src/EventListener/MapToListener.php | 1 + .../MapMethodStatementsGenerator.php | 4 ++- src/Generator/MapperGenerator.php | 3 ++ src/Generator/PropertyConditionsGenerator.php | 33 +++++++++++++++++++ src/Generator/PropertyStatementsGenerator.php | 4 ++- src/Metadata/MetadataRegistry.php | 1 + src/Metadata/PropertyMetadata.php | 1 + tests/AutoMapperMapToTest.php | 16 +++++++++ tests/Fixtures/MapTo/FooMapTo.php | 3 ++ 14 files changed, 71 insertions(+), 2 deletions(-) diff --git a/composer.json b/composer.json index 2a65523d..9883403c 100644 --- a/composer.json +++ b/composer.json @@ -19,6 +19,7 @@ "php": "^8.2", "nikic/php-parser": "^4.18 || ^5.0", "symfony/event-dispatcher": "^5.4 || ^6.0 || ^7.0", + "symfony/expression-language": "^5.4 || ^6.0 || ^7.0", "symfony/deprecation-contracts": "^2.0|^3.0", "symfony/property-info": "^5.4.23 || ^6.2.10 || ^7.0" }, diff --git a/src/Attribute/MapFrom.php b/src/Attribute/MapFrom.php index e0cd6bb8..e277d085 100644 --- a/src/Attribute/MapFrom.php +++ b/src/Attribute/MapFrom.php @@ -16,6 +16,7 @@ * @param int|null $maxDepth The maximum depth of the mapping. If null, the default max depth will be used. * @param string|callable(mixed $value, object $object): mixed|null $transformer A transformer id or a callable that transform the value during mapping * @param bool|null $ignore if true, the property will be ignored during mapping + * @param string|null $if The condition to map the property, using the expression language */ public function __construct( public ?string $source = null, @@ -23,6 +24,7 @@ public function __construct( public ?int $maxDepth = null, public mixed $transformer = null, public ?bool $ignore = null, + public ?string $if = null, ) { } } diff --git a/src/Attribute/MapTo.php b/src/Attribute/MapTo.php index 8355c0ef..59915974 100644 --- a/src/Attribute/MapTo.php +++ b/src/Attribute/MapTo.php @@ -16,6 +16,7 @@ * @param int|null $maxDepth The maximum depth of the mapping. If null, the default max depth will be used. * @param string|callable(mixed $value, object $object): mixed|null $transformer A transformer id or a callable that transform the value during mapping * @param bool|null $ignore if true, the property will be ignored during mapping + * @param string|null $if The condition to map the property, using the expression language */ public function __construct( public ?string $target = null, @@ -23,6 +24,7 @@ public function __construct( public ?int $maxDepth = null, public mixed $transformer = null, public ?bool $ignore = null, + public ?string $if = null, ) { } } diff --git a/src/Event/PropertyMetadataEvent.php b/src/Event/PropertyMetadataEvent.php index 475e0b86..1d0d614e 100644 --- a/src/Event/PropertyMetadataEvent.php +++ b/src/Event/PropertyMetadataEvent.php @@ -21,6 +21,7 @@ public function __construct( public ?int $maxDepth = null, public ?TransformerInterface $transformer = null, public ?bool $ignored = null, + public ?string $if = null, ) { } } diff --git a/src/EventListener/MapFromListener.php b/src/EventListener/MapFromListener.php index 4163922b..f6e0b194 100644 --- a/src/EventListener/MapFromListener.php +++ b/src/EventListener/MapFromListener.php @@ -62,6 +62,7 @@ private function addPropertyFromTarget(GenerateMapperEvent $event, MapFrom $mapF maxDepth: $mapFrom->maxDepth, transformer: $this->getTransformerFromMapAttribute($event->mapperMetadata->target, $mapFrom), ignored: $mapFrom->ignore, + if: $mapFrom->if, ); if (\array_key_exists($property->target->name, $event->properties)) { diff --git a/src/EventListener/MapToListener.php b/src/EventListener/MapToListener.php index 4868e363..f0db4b97 100644 --- a/src/EventListener/MapToListener.php +++ b/src/EventListener/MapToListener.php @@ -62,6 +62,7 @@ private function addPropertyFromSource(GenerateMapperEvent $event, MapTo $mapTo, maxDepth: $mapTo->maxDepth, transformer: $this->getTransformerFromMapAttribute($event->mapperMetadata->source, $mapTo), ignored: $mapTo->ignore, + if: $mapTo->if, ); if (\array_key_exists($property->target->name, $event->properties)) { diff --git a/src/Generator/MapMethodStatementsGenerator.php b/src/Generator/MapMethodStatementsGenerator.php index eb3f9457..28134579 100644 --- a/src/Generator/MapMethodStatementsGenerator.php +++ b/src/Generator/MapMethodStatementsGenerator.php @@ -15,6 +15,7 @@ use PhpParser\Node\Name; use PhpParser\Node\Scalar; use PhpParser\Node\Stmt; +use Symfony\Component\ExpressionLanguage\ExpressionLanguage; /** * @internal @@ -27,13 +28,14 @@ public function __construct( DiscriminatorStatementsGenerator $discriminatorStatementsGenerator, CachedReflectionStatementsGenerator $cachedReflectionStatementsGenerator, + ExpressionLanguage $expressionLanguage = new ExpressionLanguage(), private bool $allowReadOnlyTargetToPopulate = false, ) { $this->createObjectStatementsGenerator = new CreateTargetStatementsGenerator( $discriminatorStatementsGenerator, $cachedReflectionStatementsGenerator, ); - $this->propertyStatementsGenerator = new PropertyStatementsGenerator(); + $this->propertyStatementsGenerator = new PropertyStatementsGenerator($expressionLanguage); } /** diff --git a/src/Generator/MapperGenerator.php b/src/Generator/MapperGenerator.php index 63b5e87d..2633a69f 100644 --- a/src/Generator/MapperGenerator.php +++ b/src/Generator/MapperGenerator.php @@ -18,6 +18,7 @@ use PhpParser\Node\Name; use PhpParser\Node\Param; use PhpParser\Node\Stmt; +use Symfony\Component\ExpressionLanguage\ExpressionLanguage; /** * Generates code for a mapping class. @@ -36,6 +37,7 @@ public function __construct( ClassDiscriminatorResolver $classDiscriminatorResolver, Configuration $configuration, + ExpressionLanguage $expressionLanguage = new ExpressionLanguage() ) { $this->mapperConstructorGenerator = new MapperConstructorGenerator( $cachedReflectionStatementsGenerator = new CachedReflectionStatementsGenerator() @@ -44,6 +46,7 @@ public function __construct( $this->mapMethodStatementsGenerator = new MapMethodStatementsGenerator( $discriminatorStatementsGenerator = new DiscriminatorStatementsGenerator($classDiscriminatorResolver), $cachedReflectionStatementsGenerator, + $expressionLanguage, $configuration->allowReadOnlyTargetToPopulate, ); diff --git a/src/Generator/PropertyConditionsGenerator.php b/src/Generator/PropertyConditionsGenerator.php index 87aa5850..b271875f 100644 --- a/src/Generator/PropertyConditionsGenerator.php +++ b/src/Generator/PropertyConditionsGenerator.php @@ -13,6 +13,10 @@ use PhpParser\Node\Expr\ArrayItem as OldArrayItem; use PhpParser\Node\Name; use PhpParser\Node\Scalar; +use PhpParser\Node\Stmt; +use PhpParser\Parser; +use PhpParser\ParserFactory; +use Symfony\Component\ExpressionLanguage\ExpressionLanguage; /** * We generate a list of conditions that will allow the field to be mapped to the target. @@ -21,6 +25,15 @@ */ final readonly class PropertyConditionsGenerator { + private Parser $parser; + + public function __construct( + private ExpressionLanguage $expressionLanguage = new ExpressionLanguage(), + Parser $parser = null, + ) { + $this->parser = $parser ?? (new ParserFactory())->createForHostVersion(); + } + public function generate(GeneratorMetadata $metadata, PropertyMetadata $propertyMetadata): ?Expr { $conditions = []; @@ -32,6 +45,7 @@ public function generate(GeneratorMetadata $metadata, PropertyMetadata $property $conditions[] = $this->targetGroupsCheck($metadata, $propertyMetadata); $conditions[] = $this->noGroupsCheck($metadata, $propertyMetadata); $conditions[] = $this->maxDepthCheck($metadata, $propertyMetadata); + $conditions[] = $this->customCondition($metadata, $propertyMetadata); $conditions = array_values(array_filter($conditions)); @@ -254,4 +268,23 @@ private function maxDepthCheck(GeneratorMetadata $metadata, PropertyMetadata $pr new Scalar\LNumber($propertyMetadata->maxDepth) ); } + + /** + * When there is a if condition we check if the condition is true. + */ + private function customCondition(GeneratorMetadata $metadata, PropertyMetadata $propertyMetadata): ?Expr + { + if (null === $propertyMetadata->if) { + return null; + } + + $expression = $this->expressionLanguage->compile($propertyMetadata->if, ['value' => 'source', 'context']); + $expr = $this->parser->parse('expr; + } + + throw new \LogicException('Cannot create condition from expression "' . $propertyMetadata->if . "'"); + } } diff --git a/src/Generator/PropertyStatementsGenerator.php b/src/Generator/PropertyStatementsGenerator.php index 89685c6f..9830cc15 100644 --- a/src/Generator/PropertyStatementsGenerator.php +++ b/src/Generator/PropertyStatementsGenerator.php @@ -10,6 +10,7 @@ use AutoMapper\Transformer\AssignedByReferenceTransformerInterface; use AutoMapper\Transformer\CustomTransformer\CustomPropertyTransformer; use PhpParser\Node\Stmt; +use Symfony\Component\ExpressionLanguage\ExpressionLanguage; /** * @internal @@ -19,8 +20,9 @@ private PropertyConditionsGenerator $propertyConditionsGenerator; public function __construct( + ExpressionLanguage $expressionLanguage = new ExpressionLanguage() ) { - $this->propertyConditionsGenerator = new PropertyConditionsGenerator(); + $this->propertyConditionsGenerator = new PropertyConditionsGenerator($expressionLanguage); } /** diff --git a/src/Metadata/MetadataRegistry.php b/src/Metadata/MetadataRegistry.php index fe71ebe2..32089228 100644 --- a/src/Metadata/MetadataRegistry.php +++ b/src/Metadata/MetadataRegistry.php @@ -218,6 +218,7 @@ private function createGeneratorMetadata(MapperMetadata $mapperMetadata): Genera $propertyMappedEvent->transformer, $propertyMappedEvent->ignored, $propertyMappedEvent->maxDepth, + $propertyMappedEvent->if, ); } diff --git a/src/Metadata/PropertyMetadata.php b/src/Metadata/PropertyMetadata.php index 710e8538..0790a85e 100644 --- a/src/Metadata/PropertyMetadata.php +++ b/src/Metadata/PropertyMetadata.php @@ -22,6 +22,7 @@ public function __construct( public TransformerInterface $transformer, public bool $isIgnored = false, public ?int $maxDepth = null, + public ?string $if = null, ) { } diff --git a/tests/AutoMapperMapToTest.php b/tests/AutoMapperMapToTest.php index fd764bb1..9af8b376 100644 --- a/tests/AutoMapperMapToTest.php +++ b/tests/AutoMapperMapToTest.php @@ -44,6 +44,22 @@ public function testMapToArray() $this->assertSame('transformFromStringInstance_foo', $bar['transformFromStringInstance']); $this->assertSame('transformFromStringStatic_foo', $bar['transformFromStringStatic']); $this->assertSame('bar', $bar['transformFromCustomTransformerService']); + $this->assertSame('if', $bar['if']); + + $foo = new FooMapTo('bar'); + $this->autoMapper->bindCustomTransformer(new TransformerWithDependency(new FooDependency())); + $bar = $this->autoMapper->map($foo, 'array'); + + $this->assertIsArray($bar); + $this->assertArrayNotHasKey('bar', $bar); + $this->assertArrayNotHasKey('a', $bar); + $this->assertArrayNotHasKey('if', $bar); + $this->assertSame('bar', $bar['baz']); + $this->assertSame('bar', $bar['foo']); + $this->assertSame('transformFromIsCallable_bar', $bar['transformFromIsCallable']); + $this->assertSame('transformFromStringInstance_bar', $bar['transformFromStringInstance']); + $this->assertSame('transformFromStringStatic_bar', $bar['transformFromStringStatic']); + $this->assertSame('bar', $bar['transformFromCustomTransformerService']); } public function testMapFromArray() diff --git a/tests/Fixtures/MapTo/FooMapTo.php b/tests/Fixtures/MapTo/FooMapTo.php index 161a456d..73517ad3 100644 --- a/tests/Fixtures/MapTo/FooMapTo.php +++ b/tests/Fixtures/MapTo/FooMapTo.php @@ -20,6 +20,9 @@ public function __construct( ) { } + #[MapTo('array', if: 'source.foo == "foo"')] + public string $if = 'if'; + #[MapTo('array', ignore: true)] public function getA(): string { From c3a2e9dd894ecbf327360f9966289d958cef4319 Mon Sep 17 00:00:00 2001 From: Joel Wurtz Date: Wed, 20 Mar 2024 10:17:42 +0100 Subject: [PATCH 2/2] feat(if): allow to use a callable for if --- src/Generator/PropertyConditionsGenerator.php | 53 ++++++++++++++++++- tests/AutoMapperMapToTest.php | 6 +++ tests/Fixtures/MapTo/FooMapTo.php | 19 +++++++ 3 files changed, 77 insertions(+), 1 deletion(-) diff --git a/src/Generator/PropertyConditionsGenerator.php b/src/Generator/PropertyConditionsGenerator.php index b271875f..3aedd6b8 100644 --- a/src/Generator/PropertyConditionsGenerator.php +++ b/src/Generator/PropertyConditionsGenerator.php @@ -278,6 +278,57 @@ private function customCondition(GeneratorMetadata $metadata, PropertyMetadata $ return null; } + $callableName = null; + + if (\is_callable($propertyMetadata->if, false, $callableName)) { + if (\function_exists($callableName)) { + // Get arguments count of the function + $reflectionFunction = new \ReflectionFunction($callableName); + $argumentsCount = $reflectionFunction->getNumberOfRequiredParameters(); + + if ($argumentsCount === 1) { + return new Expr\FuncCall( + new Name($callableName), + [ + new Arg(new Expr\Variable('value')), + ] + ); + } elseif ($argumentsCount > 2) { + throw new \LogicException('Callable condition must have 1 or 2 arguments required, but it has ' . $argumentsCount); + } + } + + return new Expr\FuncCall( + new Name($callableName), + [ + new Arg(new Expr\Variable('value')), + new Arg(new Expr\Variable('context')), + ] + ); + } elseif ($metadata->mapperMetadata->sourceReflectionClass !== null && $metadata->mapperMetadata->sourceReflectionClass->hasMethod($propertyMetadata->if)) { + $reflectionMethod = $metadata->mapperMetadata->sourceReflectionClass->getMethod($propertyMetadata->if); + + if ($reflectionMethod->isStatic()) { + return new Expr\StaticCall( + new Name\FullyQualified($metadata->mapperMetadata->source), + $propertyMetadata->if, + [ + new Arg(new Expr\Variable('value')), + new Arg(new Expr\Variable('context')), + ] + ); + } + + return new Expr\MethodCall( + new Expr\Variable('value'), + $propertyMetadata->if, + [ + new Arg(new Expr\Variable('value')), + new Arg(new Expr\Variable('context')), + ] + ); + } + $expression = $this->expressionLanguage->compile($propertyMetadata->if, ['value' => 'source', 'context']); $expr = $this->parser->parse('expr; } - throw new \LogicException('Cannot create condition from expression "' . $propertyMetadata->if . "'"); + throw new \LogicException('Cannot use callback or create expression language condition from expression "' . $propertyMetadata->if . "'"); } } diff --git a/tests/AutoMapperMapToTest.php b/tests/AutoMapperMapToTest.php index 9af8b376..a50f8a9f 100644 --- a/tests/AutoMapperMapToTest.php +++ b/tests/AutoMapperMapToTest.php @@ -45,6 +45,9 @@ public function testMapToArray() $this->assertSame('transformFromStringStatic_foo', $bar['transformFromStringStatic']); $this->assertSame('bar', $bar['transformFromCustomTransformerService']); $this->assertSame('if', $bar['if']); + $this->assertSame('if', $bar['ifCallableStatic']); + $this->assertSame('if', $bar['ifCallable']); + $this->assertSame('if', $bar['ifCallableOther']); $foo = new FooMapTo('bar'); $this->autoMapper->bindCustomTransformer(new TransformerWithDependency(new FooDependency())); @@ -54,6 +57,9 @@ public function testMapToArray() $this->assertArrayNotHasKey('bar', $bar); $this->assertArrayNotHasKey('a', $bar); $this->assertArrayNotHasKey('if', $bar); + $this->assertArrayNotHasKey('ifCallableStatic', $bar); + $this->assertArrayNotHasKey('ifCallable', $bar); + $this->assertSame('if', $bar['ifCallableOther']); $this->assertSame('bar', $bar['baz']); $this->assertSame('bar', $bar['foo']); $this->assertSame('transformFromIsCallable_bar', $bar['transformFromIsCallable']); diff --git a/tests/Fixtures/MapTo/FooMapTo.php b/tests/Fixtures/MapTo/FooMapTo.php index 73517ad3..92f053d5 100644 --- a/tests/Fixtures/MapTo/FooMapTo.php +++ b/tests/Fixtures/MapTo/FooMapTo.php @@ -23,6 +23,15 @@ public function __construct( #[MapTo('array', if: 'source.foo == "foo"')] public string $if = 'if'; + #[MapTo('array', if: 'shouldMapStatic')] + public string $ifCallableStatic = 'if'; + + #[MapTo('array', if: 'shouldMapNotStatic')] + public string $ifCallable = 'if'; + + #[MapTo('array', if: 'is_object')] + public string $ifCallableOther = 'if'; + #[MapTo('array', ignore: true)] public function getA(): string { @@ -48,4 +57,14 @@ public static function transformFromStringStatic($value) { return 'transformFromStringStatic_' . $value; } + + public static function shouldMapStatic($source): bool + { + return $source->foo === 'foo'; + } + + public function shouldMapNotStatic(): bool + { + return $this->foo === 'foo'; + } }