Skip to content

Commit

Permalink
Merge pull request #80 from jolicode/feat/if
Browse files Browse the repository at this point in the history
feat(if): add if feature to map to / map from attributes
  • Loading branch information
joelwurtz committed Mar 20, 2024
2 parents ae20e70 + c3a2e9d commit 687a2be
Show file tree
Hide file tree
Showing 14 changed files with 147 additions and 2 deletions.
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 @@ -12,6 +12,7 @@
use PhpParser\Node\Expr;
use PhpParser\Node\Name;
use PhpParser\Node\Stmt;
use Symfony\Component\ExpressionLanguage\ExpressionLanguage;

/**
* @internal
Expand All @@ -21,8 +22,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 @@ -219,6 +219,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 @@ -45,6 +45,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';
}
}

0 comments on commit 687a2be

Please sign in to comment.