Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions composer.json
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,7 @@
"symfony/framework-bundle": "^7.4 || ^8.0",
"symfony/http-client": "^7.4 || ^8.0",
"symfony/http-kernel": "^7.4 || ^8.0",
"symfony/object-mapper": "^7.4 || ^8.0",
"symfony/phpunit-bridge": "^8.0",
"symfony/serializer": "^7.4 || ^8.0",
"symfony/stopwatch": "^7.4 || ^8.0",
Expand Down
2 changes: 1 addition & 1 deletion src/Event/GenerateMapperEvent.php
Original file line number Diff line number Diff line change
Expand Up @@ -15,10 +15,10 @@
/**
* @param PropertyMetadataEvent[] $properties A list of properties to add to this mapping
*/
public function __construct(

Check failure on line 18 in src/Event/GenerateMapperEvent.php

View workflow job for this annotation

GitHub Actions / phpstan

Method AutoMapper\Event\GenerateMapperEvent::__construct() has parameter $provider with no value type specified in iterable type array.
public readonly MapperMetadata $mapperMetadata,
public array $properties = [],
public ?string $provider = null,
public null|string|array $provider = null,
public ?bool $checkAttributes = null,
public ?ConstructorStrategy $constructorStrategy = null,
public ?bool $allowReadOnlyTargetToPopulate = null,
Expand Down
133 changes: 133 additions & 0 deletions src/EventListener/ObjectMapper/MapClassListener.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,133 @@
<?php

namespace AutoMapper\EventListener\ObjectMapper;

use AutoMapper\Event\GenerateMapperEvent;
use AutoMapper\Event\PropertyMetadataEvent;
use AutoMapper\Event\SourcePropertyMetadata;
use AutoMapper\Event\TargetPropertyMetadata;
use AutoMapper\Exception\BadMapDefinitionException;
use AutoMapper\Transformer\CallableTransformer;
use AutoMapper\Transformer\ExpressionLanguageTransformer;
use AutoMapper\Transformer\TransformerInterface;
use Symfony\Component\ExpressionLanguage\ExpressionLanguage;
use Symfony\Component\ExpressionLanguage\SyntaxError;
use Symfony\Component\ObjectMapper\Attribute\Map;

final readonly class MapClassListener
{
public function __construct(
private ExpressionLanguage $expressionLanguage,
) {
}

public function __invoke(GenerateMapperEvent $event): void
{
// only handle class to class mapping
if (!$event->mapperMetadata->sourceReflectionClass || !$event->mapperMetadata->targetReflectionClass) {
return;
}

$mapAttribute = null;
$reflectionClass = null;
$isSource = false;

foreach ($event->mapperMetadata->sourceReflectionClass->getAttributes(Map::class) as $sourceAttribute) {
/** @var Map $attribute */
$attribute = $sourceAttribute->newInstance();

if (!$attribute->target || $attribute->target === $event->mapperMetadata->target) {
$mapAttribute = $attribute;
$reflectionClass = $event->mapperMetadata->sourceReflectionClass;
$isSource = true;
break;
}
}

if (!$mapAttribute) {
foreach ($event->mapperMetadata->targetReflectionClass->getAttributes(Map::class) as $targetAttribute) {
/** @var Map $attribute */
$attribute = $targetAttribute->newInstance();

if (!$attribute->source || $attribute->source === $event->mapperMetadata->source) {
$mapAttribute = $attribute;
$reflectionClass = $event->mapperMetadata->targetReflectionClass;
break;
}
}
}

if (!$mapAttribute || !$reflectionClass) {
return;
}

// get all properties
$properties = [];

foreach ($reflectionClass->getProperties() as $property) {
foreach ($property->getAttributes(Map::class) as $propertyAttribute) {
/** @var Map $attribute */
$attribute = $propertyAttribute->newInstance();
$propertyMetadata = new PropertyMetadataEvent(
/**
* public ?string $if = null,// @TODO
*/
$event->mapperMetadata,
new SourcePropertyMetadata($isSource ? $property->getName() : ($attribute->source ?? $property->getName())),
new TargetPropertyMetadata($isSource ? ($attribute->target ?? $property->getName()) : $property->getName()),
transformer: $this->getTransformerFromMapAttribute($reflectionClass->getName(), $attribute, $isSource),
if: $attribute->if,

Check failure on line 79 in src/EventListener/ObjectMapper/MapClassListener.php

View workflow job for this annotation

GitHub Actions / phpstan

Parameter $if of class AutoMapper\Event\PropertyMetadataEvent constructor expects string|null, bool|(callable)|string|null given.
);

$properties[] = $propertyMetadata;
}
}

$event->properties = $properties;

if ($mapAttribute->transform) {
$event->provider = $mapAttribute->transform;

Check failure on line 89 in src/EventListener/ObjectMapper/MapClassListener.php

View workflow job for this annotation

GitHub Actions / phpstan

Property AutoMapper\Event\GenerateMapperEvent::$provider (array|string|null) does not accept array<(callable)|string>|(callable)|string.
}
}

protected function getTransformerFromMapAttribute(string $class, Map $attribute, bool $fromSource = true): ?TransformerInterface
{
$transformer = null;

if ($attribute->transform !== null) {
$callableName = null;
$transformerCallable = $attribute->transform;

if ($transformerCallable instanceof \Closure) {
// This is not supported because we cannot generate code from a closure
// However this should never be possible since attributes does not allow to pass a closure
// Let's keep this check for future proof
throw new BadMapDefinitionException('Closure transformer is not supported.');
}

if (\is_callable($transformerCallable, false, $callableName)) {
$transformer = new CallableTransformer($callableName);
} 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));
}
}

return $transformer;
}
}
7 changes: 7 additions & 0 deletions src/EventListener/ObjectMapper/MapPropertyListener.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
<?php

namespace AutoMapper\EventListener\ObjectMapper;

final readonly class MapPropertyListener
{
}
78 changes: 57 additions & 21 deletions src/Generator/MapMethodStatementsGenerator.php
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@

namespace AutoMapper\Generator;

use AutoMapper\Exception\CompileException;
use AutoMapper\Exception\ReadOnlyTargetException;
use AutoMapper\Generator\Shared\CachedReflectionStatementsGenerator;
use AutoMapper\Generator\Shared\DiscriminatorStatementsGenerator;
Expand Down Expand Up @@ -294,35 +295,70 @@ private function initializeTargetFromProvider(GeneratorMetadata $metadata): arra
}

$variableRegistry = $metadata->variableRegistry;
$statements = [];

if (is_array($metadata->provider) || is_callable($metadata->provider)) {
$callableName = null;

if (!is_callable($metadata->provider, false, $callableName)) {
return [];
}

/*
* Get result from callable if available
*
* ```php
* $result ??= callable(Target::class, $value, $context, $this->getTargetIdentifiers($value));
* ```
*/
$statements[] = new Stmt\Expression(
new Expr\AssignOp\Coalesce(
$variableRegistry->getResult(),
new Expr\FuncCall(
new Name($callableName), [
new Arg(new Scalar\String_($metadata->mapperMetadata->target)),
new Arg($variableRegistry->getSourceInput()),
new Arg($variableRegistry->getContext()),
new Arg(new Expr\MethodCall(new Expr\Variable('this'), 'getTargetIdentifiers', [
new Arg(new Expr\Variable('value')),
])),
]),
)
);
} else {

/*
* Get result from provider if available
*
* ```php
* $result ??= $this->providerRegistry->getProvider($metadata->provider)->provide($source, $context);
* ```
*/
$statements[] = new Stmt\Expression(
new Expr\AssignOp\Coalesce(
$variableRegistry->getResult(),
new Expr\MethodCall(new Expr\MethodCall(new Expr\PropertyFetch(new Expr\Variable('this'), 'providerRegistry'), 'getProvider', [
new Arg(new Scalar\String_($metadata->provider)),
]), 'provide', [
new Arg(new Scalar\String_($metadata->mapperMetadata->target)),
new Arg($variableRegistry->getSourceInput()),
new Arg($variableRegistry->getContext()),
new Arg(new Expr\MethodCall(new Expr\Variable('this'), 'getTargetIdentifiers', [
new Arg(new Expr\Variable('value')),
])),
]),
)
);
}

/*
* Get result from provider if available
*
* ```php
* $result ??= $this->providerRegistry->getProvider($metadata->provider)->provide($source, $context);
* Return early if the result is an EarlyReturn instance
*
* if ($result instanceof EarlyReturn) {
* return $result->value;
* }
* ```
*/
$statements = [];
$statements[] = new Stmt\Expression(
new Expr\AssignOp\Coalesce(
$variableRegistry->getResult(),
new Expr\MethodCall(new Expr\MethodCall(new Expr\PropertyFetch(new Expr\Variable('this'), 'providerRegistry'), 'getProvider', [
new Arg(new Scalar\String_($metadata->provider)),
]), 'provide', [
new Arg(new Scalar\String_($metadata->mapperMetadata->target)),
new Arg($variableRegistry->getSourceInput()),
new Arg($variableRegistry->getContext()),
new Arg(new Expr\MethodCall(new Expr\Variable('this'), 'getTargetIdentifiers', [
new Arg(new Expr\Variable('value')),
])),
]),
)
);

$statements[] = new Stmt\If_(
new Expr\Instanceof_($variableRegistry->getResult(), new Name(EarlyReturn::class)),
[
Expand Down
16 changes: 11 additions & 5 deletions src/Generator/PropertyConditionsGenerator.php
Original file line number Diff line number Diff line change
Expand Up @@ -246,6 +246,12 @@ private function customCondition(GeneratorMetadata $metadata, PropertyMetadata $
}

$callableName = null;
$value = $metadata->variableRegistry->getSourceInput();

// use read accessor
if ($propertyMetadata->source->accessor !== null) {
$value = $propertyMetadata->source->accessor->getExpression($metadata->variableRegistry->getSourceInput());
}

if (\is_callable($propertyMetadata->if, false, $callableName)) {
if (\function_exists($callableName)) {
Expand All @@ -257,7 +263,7 @@ private function customCondition(GeneratorMetadata $metadata, PropertyMetadata $
return new Expr\FuncCall(
new Name($callableName),
[
new Arg(new Expr\Variable('value')),
new Arg($value),
]
);
}
Expand All @@ -270,7 +276,7 @@ private function customCondition(GeneratorMetadata $metadata, PropertyMetadata $
return new Expr\FuncCall(
new Name($callableName),
[
new Arg(new Expr\Variable('value')),
new Arg($value),
new Arg(new Expr\Variable('context')),
]
);
Expand All @@ -284,17 +290,17 @@ private function customCondition(GeneratorMetadata $metadata, PropertyMetadata $
new Name\FullyQualified($metadata->mapperMetadata->source),
$propertyMetadata->if,
[
new Arg(new Expr\Variable('value')),
new Arg($value),
new Arg(new Expr\Variable('context')),
]
);
}

return new Expr\MethodCall(
new Expr\Variable('value'),
$metadata->variableRegistry->getSourceInput(),
$propertyMetadata->if,
[
new Arg(new Expr\Variable('value')),
new Arg($value),
new Arg(new Expr\Variable('context')),
]
);
Expand Down
2 changes: 1 addition & 1 deletion src/Metadata/GeneratorMetadata.php
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@
/** @var array<string, Dependency> */
private array $dependencies = [];

public function __construct(

Check failure on line 21 in src/Metadata/GeneratorMetadata.php

View workflow job for this annotation

GitHub Actions / phpstan

Method AutoMapper\Metadata\GeneratorMetadata::__construct() has parameter $provider with no value type specified in iterable type array.
public readonly MapperMetadata $mapperMetadata,
/** @var PropertyMetadata[] */
public readonly array $propertiesMetadata,
Expand All @@ -26,7 +26,7 @@
public readonly ConstructorStrategy $constructorStrategy = ConstructorStrategy::AUTO,
public readonly bool $allowReadOnlyTargetToPopulate = false,
public readonly bool $strictTypes = false,
public readonly ?string $provider = null,
public readonly null|string|array $provider = null,
) {
$this->variableRegistry = new VariableRegistry();
}
Expand Down
6 changes: 6 additions & 0 deletions src/Metadata/MetadataFactory.php
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@
use AutoMapper\EventListener\MapProviderListener;
use AutoMapper\EventListener\MapToContextListener;
use AutoMapper\EventListener\MapToListener;
use AutoMapper\EventListener\ObjectMapper\MapClassListener;
use AutoMapper\EventListener\Symfony\ClassDiscriminatorListener;
use AutoMapper\EventListener\Symfony\NameConverterListener;
use AutoMapper\EventListener\Symfony\SerializerGroupListener;
Expand Down Expand Up @@ -51,6 +52,7 @@
use Symfony\Component\EventDispatcher\EventDispatcher;
use Symfony\Component\EventDispatcher\EventDispatcherInterface;
use Symfony\Component\ExpressionLanguage\ExpressionLanguage;
use Symfony\Component\ObjectMapper\ObjectMapperInterface;
use Symfony\Component\PropertyInfo\Extractor\PhpStanExtractor;
use Symfony\Component\PropertyInfo\Extractor\ReflectionExtractor;
use Symfony\Component\PropertyInfo\PropertyInfoExtractor;
Expand Down Expand Up @@ -394,6 +396,10 @@ public static function create(
$eventDispatcher->addListener(GenerateMapperEvent::class, new MapperListener());
$eventDispatcher->addListener(GenerateMapperEvent::class, new MapProviderListener());

if (interface_exists(ObjectMapperInterface::class)) {
$eventDispatcher->addListener(GenerateMapperEvent::class, new MapClassListener($expressionLanguage));
}

// Create transformer factories
$factories = [
new DoctrineCollectionTransformerFactory(),
Expand Down
Loading
Loading