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(if): add if feature to map to / map from attributes #80

Merged
merged 2 commits into from
Mar 20, 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
1 change: 1 addition & 0 deletions composer.json
Original file line number Diff line number Diff line change
Expand Up @@ -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"
},
Expand Down
2 changes: 2 additions & 0 deletions src/Attribute/MapFrom.php
Original file line number Diff line number Diff line change
Expand Up @@ -16,13 +16,15 @@
* @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,
public ?string $name = null,
public ?int $maxDepth = null,
public mixed $transformer = null,
public ?bool $ignore = null,
public ?string $if = null,
) {
}
}
2 changes: 2 additions & 0 deletions src/Attribute/MapTo.php
Original file line number Diff line number Diff line change
Expand Up @@ -16,13 +16,15 @@
* @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,
public ?string $name = null,
public ?int $maxDepth = null,
public mixed $transformer = null,
public ?bool $ignore = null,
public ?string $if = null,
) {
}
}
1 change: 1 addition & 0 deletions src/Event/PropertyMetadataEvent.php
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ public function __construct(
public ?int $maxDepth = null,
public ?TransformerInterface $transformer = null,
public ?bool $ignored = null,
public ?string $if = null,
) {
}
}
1 change: 1 addition & 0 deletions src/EventListener/MapFromListener.php
Original file line number Diff line number Diff line change
Expand Up @@ -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)) {
Expand Down
1 change: 1 addition & 0 deletions src/EventListener/MapToListener.php
Original file line number Diff line number Diff line change
Expand Up @@ -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)) {
Expand Down
4 changes: 3 additions & 1 deletion src/Generator/MapMethodStatementsGenerator.php
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@
use PhpParser\Node\Name;
use PhpParser\Node\Scalar;
use PhpParser\Node\Stmt;
use Symfony\Component\ExpressionLanguage\ExpressionLanguage;

/**
* @internal
Expand All @@ -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);
}

/**
Expand Down
3 changes: 3 additions & 0 deletions src/Generator/MapperGenerator.php
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand All @@ -36,6 +37,7 @@
public function __construct(
ClassDiscriminatorResolver $classDiscriminatorResolver,
Configuration $configuration,
ExpressionLanguage $expressionLanguage = new ExpressionLanguage()
) {
$this->mapperConstructorGenerator = new MapperConstructorGenerator(
$cachedReflectionStatementsGenerator = new CachedReflectionStatementsGenerator()
Expand All @@ -44,6 +46,7 @@ public function __construct(
$this->mapMethodStatementsGenerator = new MapMethodStatementsGenerator(
$discriminatorStatementsGenerator = new DiscriminatorStatementsGenerator($classDiscriminatorResolver),
$cachedReflectionStatementsGenerator,
$expressionLanguage,
$configuration->allowReadOnlyTargetToPopulate,
);

Expand Down
84 changes: 84 additions & 0 deletions src/Generator/PropertyConditionsGenerator.php
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand All @@ -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 = [];
Expand All @@ -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));

Expand Down Expand Up @@ -254,4 +268,74 @@ 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;
}

$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('<?php ' . $expression . ';')[0] ?? null;

if ($expr instanceof Stmt\Expression) {
return $expr->expr;
}

throw new \LogicException('Cannot use callback or create expression language condition from expression "' . $propertyMetadata->if . "'");
}
}
4 changes: 3 additions & 1 deletion src/Generator/PropertyStatementsGenerator.php
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@
use AutoMapper\Transformer\AssignedByReferenceTransformerInterface;
use AutoMapper\Transformer\CustomTransformer\CustomPropertyTransformer;
use PhpParser\Node\Stmt;
use Symfony\Component\ExpressionLanguage\ExpressionLanguage;

/**
* @internal
Expand All @@ -19,8 +20,9 @@
private PropertyConditionsGenerator $propertyConditionsGenerator;

public function __construct(
ExpressionLanguage $expressionLanguage = new ExpressionLanguage()
) {
$this->propertyConditionsGenerator = new PropertyConditionsGenerator();
$this->propertyConditionsGenerator = new PropertyConditionsGenerator($expressionLanguage);
}

/**
Expand Down
1 change: 1 addition & 0 deletions src/Metadata/MetadataRegistry.php
Original file line number Diff line number Diff line change
Expand Up @@ -218,6 +218,7 @@ private function createGeneratorMetadata(MapperMetadata $mapperMetadata): Genera
$propertyMappedEvent->transformer,
$propertyMappedEvent->ignored,
$propertyMappedEvent->maxDepth,
$propertyMappedEvent->if,
);
}

Expand Down
1 change: 1 addition & 0 deletions src/Metadata/PropertyMetadata.php
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ public function __construct(
public TransformerInterface $transformer,
public bool $isIgnored = false,
public ?int $maxDepth = null,
public ?string $if = null,
) {
}

Expand Down
22 changes: 22 additions & 0 deletions tests/AutoMapperMapToTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,28 @@ 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']);
$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()));
$bar = $this->autoMapper->map($foo, 'array');

$this->assertIsArray($bar);
$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']);
$this->assertSame('transformFromStringInstance_bar', $bar['transformFromStringInstance']);
$this->assertSame('transformFromStringStatic_bar', $bar['transformFromStringStatic']);
$this->assertSame('bar', $bar['transformFromCustomTransformerService']);
}

public function testMapFromArray()
Expand Down
22 changes: 22 additions & 0 deletions tests/Fixtures/MapTo/FooMapTo.php
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,18 @@ 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
{
Expand All @@ -45,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';
}
}