Skip to content
Merged
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
2 changes: 2 additions & 0 deletions src/Attribute/MapFrom.php
Original file line number Diff line number Diff line change
@@ -20,6 +20,7 @@
* @param string[]|null $groups The groups to map the property
* @param string|null $dateTimeFormat The date-time format to use when transforming this property
* @param bool|null $extractTypesFromGetter If true, the types will be extracted from the getter method
* @param bool|null $identifier If true, the property will be used as an identifier
*/
public function __construct(
public string|array|null $source = null,
@@ -32,6 +33,7 @@ public function __construct(
public int $priority = 0,
public ?string $dateTimeFormat = null,
public ?bool $extractTypesFromGetter = null,
public ?bool $identifier = null,
) {
}
}
2 changes: 2 additions & 0 deletions src/Attribute/MapTo.php
Original file line number Diff line number Diff line change
@@ -20,6 +20,7 @@
* @param string[]|null $groups The groups to map the property
* @param string|null $dateTimeFormat The date-time format to use when transforming this property
* @param bool|null $extractTypesFromGetter If true, the types will be extracted from the getter method
* @param bool|null $identifier If true, the property will be used as an identifier
*/
public function __construct(
public string|array|null $target = null,
@@ -32,6 +33,7 @@ public function __construct(
public int $priority = 0,
public ?string $dateTimeFormat = null,
public ?bool $extractTypesFromGetter = null,
public ?bool $identifier = null,
) {
}
}
1 change: 1 addition & 0 deletions src/Event/PropertyMetadataEvent.php
Original file line number Diff line number Diff line change
@@ -32,6 +32,7 @@ public function __construct(
public int $priority = 0,
public readonly bool $isFromDefaultExtractor = false,
public ?bool $extractTypesFromGetter = null,
public ?bool $identifier = null,
) {
}
}
1 change: 1 addition & 0 deletions src/EventListener/MapFromListener.php
Original file line number Diff line number Diff line change
@@ -84,6 +84,7 @@ private function addPropertyFromTarget(GenerateMapperEvent $event, MapFrom $mapF
groups: $mapFrom->groups,
priority: $mapFrom->priority,
extractTypesFromGetter: $mapFrom->extractTypesFromGetter,
identifier: $mapFrom->identifier,
);

if (\array_key_exists($propertyMetadata->target->property, $event->properties) && $event->properties[$propertyMetadata->target->property]->priority >= $propertyMetadata->priority) {
1 change: 1 addition & 0 deletions src/EventListener/MapToListener.php
Original file line number Diff line number Diff line change
@@ -85,6 +85,7 @@ private function addPropertyFromSource(GenerateMapperEvent $event, MapTo $mapTo,
groups: $mapTo->groups,
priority: $mapTo->priority,
extractTypesFromGetter: $mapTo->extractTypesFromGetter,
identifier: $mapTo->identifier,
);

if (\array_key_exists($propertyMetadata->target->property, $event->properties) && $event->properties[$propertyMetadata->target->property]->priority >= $propertyMetadata->priority) {
37 changes: 22 additions & 15 deletions src/Extractor/ReadAccessor.php
Original file line number Diff line number Diff line change
@@ -32,17 +32,24 @@ final class ReadAccessor
public const TYPE_SOURCE = 4;
public const TYPE_ARRAY_ACCESS = 5;

public const EXTRACT_IS_UNDEFINED_CALLBACK = 'extractIsUndefinedCallbacks';
public const EXTRACT_IS_NULL_CALLBACK = 'extractIsNullCallbacks';
public const EXTRACT_CALLBACK = 'extractCallbacks';
public const EXTRACT_TARGET_IS_UNDEFINED_CALLBACK = 'extractTargetIsUndefinedCallbacks';
public const EXTRACT_TARGET_IS_NULL_CALLBACK = 'extractTargetIsNullCallbacks';
public const EXTRACT_TARGET_CALLBACK = 'extractTargetCallbacks';

/**
* @param array<string, string> $context
*/
public function __construct(
private readonly int $type,
private readonly string $accessor,
private readonly ?string $sourceClass = null,
private readonly bool $private = false,
private readonly ?string $property = null,
public readonly int $type,
public readonly string $accessor,
public readonly ?string $sourceClass = null,
public readonly bool $private = false,
public readonly ?string $property = null,
// will be the name of the property if different from accessor
private readonly array $context = [],
public readonly array $context = [],
) {
if (self::TYPE_METHOD === $this->type && null === $this->sourceClass) {
throw new InvalidArgumentException('Source class must be provided when using "method" type.');
@@ -54,7 +61,7 @@ public function __construct(
*
* @throws CompileException
*/
public function getExpression(Expr $input): Expr
public function getExpression(Expr $input, bool $target = false): Expr
{
if (self::TYPE_METHOD === $this->type) {
$methodCallArguments = [];
@@ -99,7 +106,7 @@ public function getExpression(Expr $input): Expr
* $this->extractCallbacks['method_name']($input)
*/
return new Expr\FuncCall(
new Expr\ArrayDimFetch(new Expr\PropertyFetch(new Expr\Variable('this'), 'extractCallbacks'), new Scalar\String_($this->property ?? $this->accessor)),
new Expr\ArrayDimFetch(new Expr\PropertyFetch(new Expr\Variable('this'), $target ? self::EXTRACT_TARGET_CALLBACK : self::EXTRACT_CALLBACK), new Scalar\String_($this->property ?? $this->accessor)),
[
new Arg($input),
]
@@ -124,7 +131,7 @@ public function getExpression(Expr $input): Expr
* $this->extractCallbacks['property_name']($input)
*/
return new Expr\FuncCall(
new Expr\ArrayDimFetch(new Expr\PropertyFetch(new Expr\Variable('this'), 'extractCallbacks'), new Scalar\String_($this->accessor)),
new Expr\ArrayDimFetch(new Expr\PropertyFetch(new Expr\Variable('this'), $target ? self::EXTRACT_TARGET_CALLBACK : self::EXTRACT_CALLBACK), new Scalar\String_($this->accessor)),
[
new Arg($input),
]
@@ -155,7 +162,7 @@ public function getExpression(Expr $input): Expr
throw new CompileException('Invalid accessor for read expression');
}

public function getIsDefinedExpression(Expr\Variable $input, bool $nullable = false): ?Expr
public function getIsDefinedExpression(Expr\Variable $input, bool $nullable = false, bool $target = false): ?Expr
{
// It is not possible to check if the underlying data is defined, assumes it is, php will throw an error if it is not
if (!$nullable && \in_array($this->type, [self::TYPE_METHOD, self::TYPE_SOURCE])) {
@@ -172,7 +179,7 @@ public function getIsDefinedExpression(Expr\Variable $input, bool $nullable = fa
* !$this->extractIsUndefinedCallbacks['property_name']($input)
*/
return new Expr\BooleanNot(new Expr\FuncCall(
new Expr\ArrayDimFetch(new Expr\PropertyFetch(new Expr\Variable('this'), 'extractIsUndefinedCallbacks'), new Scalar\String_($this->accessor)),
new Expr\ArrayDimFetch(new Expr\PropertyFetch(new Expr\Variable('this'), $target ? self::EXTRACT_TARGET_IS_UNDEFINED_CALLBACK : self::EXTRACT_IS_UNDEFINED_CALLBACK), new Scalar\String_($this->accessor)),
[
new Arg($input),
]
@@ -212,7 +219,7 @@ public function getIsDefinedExpression(Expr\Variable $input, bool $nullable = fa
return null;
}

public function getIsNullExpression(Expr\Variable $input): Expr
public function getIsNullExpression(Expr\Variable $input, bool $target = false): Expr
{
if (self::TYPE_METHOD === $this->type) {
$methodCallExpr = $this->getExpression($input);
@@ -236,7 +243,7 @@ public function getIsNullExpression(Expr\Variable $input): Expr
* $this->extractIsNullCallbacks['property_name']($input)
*/
return new Expr\FuncCall(
new Expr\ArrayDimFetch(new Expr\PropertyFetch(new Expr\Variable('this'), 'extractIsNullCallbacks'), new Scalar\String_($this->accessor)),
new Expr\ArrayDimFetch(new Expr\PropertyFetch(new Expr\Variable('this'), $target ? self::EXTRACT_TARGET_IS_NULL_CALLBACK : self::EXTRACT_IS_NULL_CALLBACK), new Scalar\String_($this->accessor)),
[
new Arg($input),
]
@@ -270,7 +277,7 @@ public function getIsNullExpression(Expr\Variable $input): Expr
throw new CompileException('Invalid accessor for read expression');
}

public function getIsUndefinedExpression(Expr\Variable $input): Expr
public function getIsUndefinedExpression(Expr\Variable $input, bool $target = false): Expr
{
if (\in_array($this->type, [self::TYPE_METHOD, self::TYPE_SOURCE])) {
/*
@@ -289,7 +296,7 @@ public function getIsUndefinedExpression(Expr\Variable $input): Expr
* $this->extractIsUndefinedCallbacks['property_name']($input)
*/
return new Expr\FuncCall(
new Expr\ArrayDimFetch(new Expr\PropertyFetch(new Expr\Variable('this'), 'extractIsUndefinedCallbacks'), new Scalar\String_($this->accessor)),
new Expr\ArrayDimFetch(new Expr\PropertyFetch(new Expr\Variable('this'), $target ? self::EXTRACT_TARGET_IS_UNDEFINED_CALLBACK : self::EXTRACT_IS_UNDEFINED_CALLBACK), new Scalar\String_($this->accessor)),
[
new Arg($input),
]
4 changes: 2 additions & 2 deletions src/Extractor/WriteMutator.php
Original file line number Diff line number Diff line change
@@ -32,8 +32,8 @@ final class WriteMutator

public function __construct(
public readonly int $type,
private readonly string $property,
private readonly bool $private = false,
public readonly string $property,
public readonly bool $private = false,
public readonly ?\ReflectionParameter $parameter = null,
private readonly ?string $removeMethodName = null,
) {
19 changes: 19 additions & 0 deletions src/GeneratedMapper.php
Original file line number Diff line number Diff line change
@@ -36,6 +36,16 @@ public function registerMappers(AutoMapperRegistryInterface $registry): void
{
}

public function getSourceHash(mixed $value): ?string
{
return null;
}

public function getTargetHash(mixed $value): ?string
{
return null;
}

/** @var array<string, MapperInterface<object, object>|MapperInterface<object, array<mixed>>|MapperInterface<array<mixed>, object>> */
protected array $mappers = [];

@@ -51,6 +61,15 @@ public function registerMappers(AutoMapperRegistryInterface $registry): void
/** @var array<string, callable(): bool>) */
protected array $extractIsUndefinedCallbacks = [];

/** @var array<string, callable(): mixed> */
protected array $extractTargetCallbacks = [];

/** @var array<string, callable(): bool>) */
protected array $extractTargetIsNullCallbacks = [];

/** @var array<string, callable(): bool>) */
protected array $extractTargetIsUndefinedCallbacks = [];

/** @var Target|\ReflectionClass<object> */
protected mixed $cachedTarget;
}
100 changes: 100 additions & 0 deletions src/Generator/IdentifierHashGenerator.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,100 @@
<?php

declare(strict_types=1);

namespace AutoMapper\Generator;

use AutoMapper\Metadata\GeneratorMetadata;
use PhpParser\Node\Arg;
use PhpParser\Node\Expr;
use PhpParser\Node\Name;
use PhpParser\Node\Scalar;
use PhpParser\Node\Stmt;

final readonly class IdentifierHashGenerator
{
/**
* @return list<Stmt>
*/
public function getStatements(GeneratorMetadata $metadata, bool $fromSource): array
{
$identifiers = [];

foreach ($metadata->propertiesMetadata as $propertyMetadata) {
if (!$propertyMetadata->identifier) {
continue;
}

if (null === $propertyMetadata->target->readAccessor) {
continue;
}

if (null === $propertyMetadata->source->accessor) {
continue;
}

$identifiers[] = $propertyMetadata;
}

if (empty($identifiers)) {
return [];
}

$hashCtxVariable = new Expr\Variable('hashCtx');

$statements = [
new Stmt\Expression(new Expr\Assign($hashCtxVariable, new Expr\FuncCall(new Name('hash_init'), [
new Arg(new Scalar\String_('sha256')),
]))),
];

$valueVariable = new Expr\Variable('value');

// foreach property we check
foreach ($identifiers as $property) {
if (null === $property->source->accessor || null === $property->target->readAccessor) {
continue;
}

// check if the source is defined
if ($fromSource) {
if ($property->source->checkExists) {
$statements[] = new Stmt\If_($property->source->accessor->getIsUndefinedExpression($valueVariable), [
'stmts' => [
new Stmt\Return_(new Expr\ConstFetch(new Name('null'))),
],
]);
}

// add identifier to hash
$statements[] = new Stmt\Expression(new Expr\FuncCall(new Name('hash_update'), [
new Arg($hashCtxVariable),
new Arg($property->source->accessor->getExpression($valueVariable)),
]));
} else {
$statements[] = new Stmt\If_($property->target->readAccessor->getIsUndefinedExpression($valueVariable, true), [
'stmts' => [
new Stmt\Return_(new Expr\ConstFetch(new Name('null'))),
],
]);

$statements[] = new Stmt\Expression(new Expr\FuncCall(new Name('hash_update'), [
new Arg($hashCtxVariable),
new Arg($property->target->readAccessor->getExpression($valueVariable, true)),
]));
}
}

if (\count($statements) < 2) {
return [];
}

// return hash as string
$statements[] = new Stmt\Return_(new Expr\FuncCall(new Name('hash_final'), [
new Arg($hashCtxVariable),
new Arg(new Scalar\String_('true')),
]));

return $statements;
}
}
70 changes: 70 additions & 0 deletions src/Generator/MapperConstructorGenerator.php
Original file line number Diff line number Diff line change
@@ -4,6 +4,7 @@

namespace AutoMapper\Generator;

use AutoMapper\Extractor\ReadAccessor;
use AutoMapper\Generator\Shared\CachedReflectionStatementsGenerator;
use AutoMapper\Metadata\GeneratorMetadata;
use AutoMapper\Metadata\PropertyMetadata;
@@ -32,6 +33,9 @@ public function getStatements(GeneratorMetadata $metadata): array
$constructStatements[] = $this->extractCallbackForProperty($metadata, $propertyMetadata);
$constructStatements[] = $this->extractIsNullCallbackForProperty($metadata, $propertyMetadata);
$constructStatements[] = $this->extractIsUndefinedCallbackForProperty($metadata, $propertyMetadata);
$constructStatements[] = $this->extractTargetCallbackForProperty($metadata, $propertyMetadata);
$constructStatements[] = $this->extractTargetIsNullCallbackForProperty($metadata, $propertyMetadata);
$constructStatements[] = $this->extractTargetIsUndefinedCallbackForProperty($metadata, $propertyMetadata);
$constructStatements[] = $this->hydrateCallbackForProperty($metadata, $propertyMetadata);
}

@@ -106,6 +110,72 @@ private function extractIsUndefinedCallbackForProperty(GeneratorMetadata $metada
));
}

/**
* Add read callback to the constructor of the generated mapper.
*
* ```php
* $this->extractCallbacks['propertyName'] = $extractCallback;
* ```
*/
private function extractTargetCallbackForProperty(GeneratorMetadata $metadata, PropertyMetadata $propertyMetadata): ?Stmt\Expression
{
$extractCallback = $propertyMetadata->target->readAccessor?->getExtractCallback($metadata->mapperMetadata->target);

if (!$extractCallback) {
return null;
}

return new Stmt\Expression(
new Expr\Assign(
new Expr\ArrayDimFetch(new Expr\PropertyFetch(new Expr\Variable('this'), ReadAccessor::EXTRACT_TARGET_CALLBACK), new Scalar\String_($propertyMetadata->target->property)),
$extractCallback
));
}

/**
* Add read callback to the constructor of the generated mapper.
*
* ```php
* $this->extractIsNullCallbacks['propertyName'] = $extractIsNullCallback;
* ```
*/
private function extractTargetIsNullCallbackForProperty(GeneratorMetadata $metadata, PropertyMetadata $propertyMetadata): ?Stmt\Expression
{
$extractNullCallback = $propertyMetadata->target->readAccessor?->getExtractIsNullCallback($metadata->mapperMetadata->target);

if (!$extractNullCallback) {
return null;
}

return new Stmt\Expression(
new Expr\Assign(
new Expr\ArrayDimFetch(new Expr\PropertyFetch(new Expr\Variable('this'), ReadAccessor::EXTRACT_TARGET_IS_NULL_CALLBACK), new Scalar\String_($propertyMetadata->target->property)),
$extractNullCallback
));
}

/**
* Add read callback to the constructor of the generated mapper.
*
* ```php
* $this->extractIsUndefinedCallbacks['propertyName'] = $extractIsNullCallback;
* ```
*/
private function extractTargetIsUndefinedCallbackForProperty(GeneratorMetadata $metadata, PropertyMetadata $propertyMetadata): ?Stmt\Expression
{
$extractUndefinedCallback = $propertyMetadata->target->readAccessor?->getExtractIsUndefinedCallback($metadata->mapperMetadata->target);

if (!$extractUndefinedCallback) {
return null;
}

return new Stmt\Expression(
new Expr\Assign(
new Expr\ArrayDimFetch(new Expr\PropertyFetch(new Expr\Variable('this'), ReadAccessor::EXTRACT_TARGET_IS_UNDEFINED_CALLBACK), new Scalar\String_($propertyMetadata->target->property)),
$extractUndefinedCallback
));
}

/**
* Add hydrate callback to the constructor of the generated mapper.
*
74 changes: 71 additions & 3 deletions src/Generator/MapperGenerator.php
Original file line number Diff line number Diff line change
@@ -35,6 +35,7 @@
private MapperConstructorGenerator $mapperConstructorGenerator;
private InjectMapperMethodStatementsGenerator $injectMapperMethodStatementsGenerator;
private MapMethodStatementsGenerator $mapMethodStatementsGenerator;
private IdentifierHashGenerator $identifierHashGenerator;
private bool $disableGeneratedMapper;

public function __construct(
@@ -54,6 +55,7 @@ public function __construct(
);

$this->injectMapperMethodStatementsGenerator = new InjectMapperMethodStatementsGenerator();
$this->identifierHashGenerator = new IdentifierHashGenerator();

$this->disableGeneratedMapper = !$configuration->autoRegister;
}
@@ -76,13 +78,23 @@ public function generate(GeneratorMetadata $metadata): array
if ($metadata->strictTypes) {
$statements[] = new Stmt\Declare_([create_declare_item('strict_types', create_scalar_int(1))]);
}
$statements[] = (new Builder\Class_($metadata->mapperMetadata->className))

$builder = (new Builder\Class_($metadata->mapperMetadata->className))
->makeFinal()
->extend(GeneratedMapper::class)
->addStmt($this->constructorMethod($metadata))
->addStmt($this->mapMethod($metadata))
->addStmt($this->registerMappersMethod($metadata))
->getNode();
->addStmt($this->registerMappersMethod($metadata));

if ($sourceHashMethod = $this->getSourceHashMethod($metadata)) {
$builder->addStmt($sourceHashMethod);
}

if ($targetHashMethod = $this->getTargetHashMethod($metadata)) {
$builder->addStmt($targetHashMethod);
}

$statements[] = $builder->getNode();

return $statements;
}
@@ -162,4 +174,60 @@ private function registerMappersMethod(GeneratorMetadata $metadata): Stmt\ClassM
->addStmts($this->injectMapperMethodStatementsGenerator->getStatements($param, $metadata))
->getNode();
}

/**
* Create the getSourceHash method for this mapper.
*
* ```php
* public function getSourceHash(mixed $source, mixed $target): ?string {
* ... // statements
* }
* ```
*/
private function getSourceHashMethod(GeneratorMetadata $metadata): ?Stmt\ClassMethod
{
$stmts = $this->identifierHashGenerator->getStatements($metadata, true);

if (empty($stmts)) {
return null;
}

return (new Builder\Method('getSourceHash'))
->makePublic()
->setReturnType('?string')
->addParam(new Param(
var: new Expr\Variable('value'),
type: new Name('mixed'))
)
->addStmts($stmts)
->getNode();
}

/**
* Create the getTargetHash method for this mapper.
*
* ```php
* public function getSourceHash(mixed $source, mixed $target): ?string {
* ... // statements
* }
* ```
*/
private function getTargetHashMethod(GeneratorMetadata $metadata): ?Stmt\ClassMethod
{
$stmts = $this->identifierHashGenerator->getStatements($metadata, false);

if (empty($stmts)) {
return null;
}

return (new Builder\Method('getTargetHash'))
->makePublic()
->setReturnType('?string')
->addParam(new Param(
var: new Expr\Variable('value'),
type: new Name('mixed'))
)
->addStmts($stmts)
->getNode();
}
}
1 change: 1 addition & 0 deletions src/Metadata/MetadataFactory.php
Original file line number Diff line number Diff line change
@@ -326,6 +326,7 @@ private function createGeneratorMetadata(MapperMetadata $mapperMetadata): Genera
$propertyMappedEvent->if,
$propertyMappedEvent->groups,
$propertyMappedEvent->disableGroupsCheck,
$propertyMappedEvent->identifier ?? false,
);
}

1 change: 1 addition & 0 deletions src/Metadata/PropertyMetadata.php
Original file line number Diff line number Diff line change
@@ -29,6 +29,7 @@ public function __construct(
public ?string $if = null,
public ?array $groups = null,
public ?bool $disableGroupsCheck = null,
public bool $identifier = false,
) {
}
}
109 changes: 73 additions & 36 deletions src/Transformer/AbstractArrayTransformer.php
Original file line number Diff line number Diff line change
@@ -27,47 +27,30 @@ public function __construct(

abstract protected function getAssignExpr(Expr $valuesVar, Expr $outputVar, Expr $loopKeyVar, bool $assignByRef): Expr;

public function transform(Expr $input, Expr $target, PropertyMetadata $propertyMapping, UniqueVariableScope $uniqueVariableScope, Expr\Variable $source): array
public function transform(Expr $input, Expr $target, PropertyMetadata $propertyMapping, UniqueVariableScope $uniqueVariableScope, Expr\Variable $source, ?Expr $existingValue = null): array
{
/**
* $values = [];.
*/
$valuesVar = new Expr\Variable($uniqueVariableScope->getUniqueName('values'));
$baseAssign = new Expr\Array_();

if ($propertyMapping->target->readAccessor !== null) {
$isDefined = $propertyMapping->target->readAccessor->getIsDefinedExpression(new Expr\Variable('result'));
$existingValue = $propertyMapping->target->readAccessor->getExpression(new Expr\Variable('result'));

if (null !== $isDefined) {
$existingValue = new Expr\Ternary(
$isDefined,
$existingValue,
$baseAssign
);
}

$baseAssign = new Expr\Ternary(
new Expr\BinaryOp\Coalesce(
new Expr\ArrayDimFetch(new Expr\Variable('context'), new Scalar\String_(MapperContext::DEEP_TARGET_TO_POPULATE)),
new Expr\ConstFetch(new Name('false'))
),
$existingValue,
$baseAssign
);
}
$exisingValuesIndexed = new Expr\Variable($uniqueVariableScope->getUniqueName('existingValuesIndexed'));

$statements = [
new Stmt\Expression(new Expr\Assign($valuesVar, $baseAssign)),
new Stmt\Expression(new Expr\Assign($valuesVar, new Expr\Array_())),
new Stmt\Expression(new Expr\Assign($exisingValuesIndexed, new Expr\Array_())),
];

$loopValueVar = new Expr\Variable($uniqueVariableScope->getUniqueName('value'));
$loopKeyVar = new Expr\Variable($uniqueVariableScope->getUniqueName('key'));

$itemStatements = [];
$existingValue = new Expr\Variable($uniqueVariableScope->getUniqueName('existingValue'));
$assignByRef = $this->itemTransformer instanceof AssignedByReferenceTransformerInterface && $this->itemTransformer->assignByRef();

/* Get the transform statements for the source property */
[$output, $itemStatements] = $this->itemTransformer->transform($loopValueVar, $target, $propertyMapping, $uniqueVariableScope, $source);
[$output, $transformStatements] = $this->itemTransformer->transform($loopValueVar, $target, $propertyMapping, $uniqueVariableScope, $source, $existingValue);

$itemStatements = array_merge($itemStatements, $transformStatements);

if (null === $propertyMapping->target->parameterInConstructor && $propertyMapping->target->writeMutator && $propertyMapping->target->writeMutator->type === WriteMutator::TYPE_ADDER_AND_REMOVER) {
/**
@@ -79,28 +62,82 @@ public function transform(Expr $input, Expr $target, PropertyMetadata $propertyM
$removeExpr = $propertyMapping->target->writeMutator->getRemoveExpression($target, $loopRemoveValueVar);

if ($propertyMapping->target->readAccessor !== null && $removeExpr !== null) {
$loopExistingStatements = [];
$isDeepPopulateExpr = new Expr\BinaryOp\Coalesce(
new Expr\ArrayDimFetch(new Expr\Variable('context'), new Scalar\String_(MapperContext::DEEP_TARGET_TO_POPULATE)),
new Expr\ConstFetch(new Name('false'))
);

if ($propertyMapping->target->readAccessor !== null && $this->itemTransformer instanceof IdentifierHashInterface) {
$targetHashVar = new Expr\Variable($uniqueVariableScope->getUniqueName('targetHash'));

$loopExistingStatements[] = new Stmt\If_($isDeepPopulateExpr, [
'stmts' => [
new Stmt\Expression(new Expr\Assign($targetHashVar, $this->itemTransformer->getTargetHashExpression($loopRemoveValueVar))),
new Stmt\If_(new Expr\BinaryOp\NotIdentical(new Expr\ConstFetch(new Name('null')), $targetHashVar), [
'stmts' => [new Stmt\Expression(new Expr\Assign(new Expr\ArrayDimFetch($exisingValuesIndexed, $targetHashVar), $loopRemoveValueVar))],
]),
],
]);
}

$loopExistingStatements[] = new Stmt\Expression($removeExpr);

$statements[] = new Stmt\Foreach_($propertyMapping->target->readAccessor->getExpression($target), $loopRemoveValueVar, [
'stmts' => [
new Stmt\Expression($removeExpr),
],
'stmts' => $loopExistingStatements,
]);
}

$mappedValueVar = new Expr\Variable($uniqueVariableScope->getUniqueName('mappedValue'));
$hashValueTargetVariable = new Expr\Variable($uniqueVariableScope->getUniqueName('hashValueTarget'));

if ($propertyMapping->target->readAccessor !== null && $this->itemTransformer instanceof IdentifierHashInterface) {
$itemStatements[] = new Stmt\Expression(new Expr\Assign($hashValueTargetVariable, $this->itemTransformer->getSourceHashExpression($loopValueVar)));
$itemStatements[] = new Stmt\Expression(new Expr\Assign($existingValue, new Expr\BinaryOp\Coalesce(new Expr\ArrayDimFetch($exisingValuesIndexed, $hashValueTargetVariable), new Expr\ConstFetch(new Name('null')))));
}
$itemStatements[] = new Stmt\Expression(new Expr\Assign($mappedValueVar, $output));
$itemStatements[] = new Stmt\If_(new Expr\BinaryOp\NotIdentical(new Expr\ConstFetch(new Name('null')), $mappedValueVar), [
'stmts' => [
new Stmt\Expression($propertyMapping->target->writeMutator->getExpression($target, $mappedValueVar, $assignByRef)),
],
]);
} else {
/*
* Assign the value to the array.
*
* $values[] = $output;
* or
* $values[$key] = $output;
*/
$loopExistingValueVar = new Expr\Variable($uniqueVariableScope->getUniqueName('existingValue'));

if ($propertyMapping->target->readAccessor !== null && $this->itemTransformer instanceof IdentifierHashInterface) {
$hashValueVariable = new Expr\Variable($uniqueVariableScope->getUniqueName('hashValue'));

$isDeepPopulateExpr = new Expr\BinaryOp\Coalesce(
new Expr\ArrayDimFetch(new Expr\Variable('context'), new Scalar\String_(MapperContext::DEEP_TARGET_TO_POPULATE)),
new Expr\ConstFetch(new Name('false'))
);

$isDefinedExpr = $propertyMapping->target->readAccessor->getIsDefinedExpression(new Expr\Variable('result'));

if ($isDefinedExpr !== null) {
$isDeepPopulateExpr = new Expr\BinaryOp\BooleanAnd($isDeepPopulateExpr, $isDefinedExpr);
}

$statements[] = new Stmt\If_($isDeepPopulateExpr, [
'stmts' => [
new Stmt\Foreach_($propertyMapping->target->readAccessor->getExpression($target), $loopExistingValueVar, [
'stmts' => [
new Stmt\Expression(new Expr\Assign($hashValueVariable, $this->itemTransformer->getTargetHashExpression($loopExistingValueVar))),
new Stmt\If_(new Expr\BinaryOp\NotIdentical(new Expr\ConstFetch(new Name('null')), $hashValueVariable), [
'stmts' => [
new Stmt\Expression(new Expr\Assign(new Expr\ArrayDimFetch($exisingValuesIndexed, $hashValueVariable), $loopExistingValueVar)),
],
]),
],
]),
],
]);

$hashValueTargetVariable = new Expr\Variable($uniqueVariableScope->getUniqueName('hashValueTarget'));
$itemStatements[] = new Stmt\Expression(new Expr\Assign($hashValueTargetVariable, $this->itemTransformer->getSourceHashExpression($loopValueVar)));
$itemStatements[] = new Stmt\Expression(new Expr\Assign($existingValue, new Expr\BinaryOp\Coalesce(new Expr\ArrayDimFetch($exisingValuesIndexed, $hashValueTargetVariable), new Expr\ConstFetch(new Name('null')))));
}

$itemStatements[] = new Stmt\Expression($this->getAssignExpr($valuesVar, $output, $loopKeyVar, $assignByRef));
}

62 changes: 35 additions & 27 deletions src/Transformer/ArrayToDoctrineCollectionTransformer.php
Original file line number Diff line number Diff line change
@@ -26,43 +26,51 @@ public function __construct(
) {
}

public function transform(Expr $input, Expr $target, PropertyMetadata $propertyMapping, UniqueVariableScope $uniqueVariableScope, Expr\Variable $source): array
public function transform(Expr $input, Expr $target, PropertyMetadata $propertyMapping, UniqueVariableScope $uniqueVariableScope, Expr\Variable $source, ?Expr $existingValue = null): array
{
/**
* $collection = new ArrayCollection();.
*/
$collectionVar = new Expr\Variable($uniqueVariableScope->getUniqueName('collection'));

$baseAssign = new Expr\New_(new Name(ArrayCollection::class));

if ($propertyMapping->target->readAccessor !== null) {
$isDefined = $propertyMapping->target->readAccessor->getIsDefinedExpression(new Expr\Variable('result'));
$existingValue = $propertyMapping->target->readAccessor->getExpression(new Expr\Variable('result'));

if (null !== $isDefined) {
$existingValue = new Expr\Ternary(
$isDefined,
$existingValue,
$baseAssign
);
}

$baseAssign = new Expr\Ternary(
new Expr\BinaryOp\Coalesce(
new Expr\ArrayDimFetch(new Expr\Variable('context'), new Scalar\String_(MapperContext::DEEP_TARGET_TO_POPULATE)),
new Expr\ConstFetch(new Name('false'))
),
$existingValue,
$baseAssign,
);
}
$exisingValuesIndexed = new Expr\Variable($uniqueVariableScope->getUniqueName('existingValuesIndexed'));

$statements = [
new Stmt\Expression(new Expr\Assign($collectionVar, $baseAssign)),
new Stmt\Expression(new Expr\Assign($collectionVar, new Expr\New_(new Name(ArrayCollection::class)))),
new Stmt\Expression(new Expr\Assign($exisingValuesIndexed, new Expr\Array_())),
];

$loopValueVar = new Expr\Variable($uniqueVariableScope->getUniqueName('value'));
[$output, $itemStatements] = $this->itemTransformer->transform($loopValueVar, $target, $propertyMapping, $uniqueVariableScope, $source);

$existingValue = new Expr\Variable($uniqueVariableScope->getUniqueName('existingValue'));
$loopExistingValueVar = new Expr\Variable($uniqueVariableScope->getUniqueName('existingValue'));

[$output, $itemStatements] = $this->itemTransformer->transform($loopValueVar, $target, $propertyMapping, $uniqueVariableScope, $source, $existingValue);

if ($propertyMapping->target->readAccessor !== null && $this->itemTransformer instanceof IdentifierHashInterface) {
$hashValueVariable = new Expr\Variable($uniqueVariableScope->getUniqueName('hashValue'));
$statements[] = new Stmt\If_(new Expr\BinaryOp\Coalesce(
new Expr\ArrayDimFetch(new Expr\Variable('context'), new Scalar\String_(MapperContext::DEEP_TARGET_TO_POPULATE)),
new Expr\ConstFetch(new Name('false'))
), [
'stmts' => [
new Stmt\Foreach_($propertyMapping->target->readAccessor->getExpression($target), $loopExistingValueVar, [
'stmts' => [
new Stmt\Expression(new Expr\Assign($hashValueVariable, $this->itemTransformer->getTargetHashExpression($loopExistingValueVar))),
new Stmt\If_(new Expr\BinaryOp\NotIdentical(new Expr\ConstFetch(new Name('null')), $hashValueVariable), [
'stmts' => [
new Stmt\Expression(new Expr\Assign(new Expr\ArrayDimFetch($exisingValuesIndexed, $hashValueVariable), $loopExistingValueVar)),
],
]),
],
]),
],
]);

$hashValueTargetVariable = new Expr\Variable($uniqueVariableScope->getUniqueName('hashValueTarget'));
$itemStatements[] = new Stmt\Expression(new Expr\Assign($hashValueTargetVariable, $this->itemTransformer->getSourceHashExpression($loopValueVar)));
$itemStatements[] = new Stmt\Expression(new Expr\Assign($existingValue, new Expr\BinaryOp\Coalesce(new Expr\ArrayDimFetch($exisingValuesIndexed, $hashValueTargetVariable), new Expr\ConstFetch(new Name('null')))));
}

$itemStatements[] = new Stmt\Expression(new Expr\MethodCall($collectionVar, 'add', [new Arg($output)]));

$statements[] = new Stmt\Foreach_(new Expr\BinaryOp\Coalesce($input, new Expr\Array_()), $loopValueVar, [
2 changes: 1 addition & 1 deletion src/Transformer/BuiltinTransformer.php
Original file line number Diff line number Diff line change
@@ -80,7 +80,7 @@ public function __construct(
) {
}

public function transform(Expr $input, Expr $target, PropertyMetadata $propertyMapping, UniqueVariableScope $uniqueVariableScope, Expr\Variable $source): array
public function transform(Expr $input, Expr $target, PropertyMetadata $propertyMapping, UniqueVariableScope $uniqueVariableScope, Expr\Variable $source, ?Expr $existingValue = null): array
{
$targetTypes = array_map(function (Type $type) {
return $type->getBuiltinType();
2 changes: 1 addition & 1 deletion src/Transformer/CallableTransformer.php
Original file line number Diff line number Diff line change
@@ -22,7 +22,7 @@ public function __construct(
) {
}

public function transform(Expr $input, Expr $target, PropertyMetadata $propertyMapping, UniqueVariableScope $uniqueVariableScope, Expr\Variable $source): array
public function transform(Expr $input, Expr $target, PropertyMetadata $propertyMapping, UniqueVariableScope $uniqueVariableScope, Expr\Variable $source, ?Expr $existingValue = null): array
{
if ($this->callableIsMethodFromSource || $this->callableIsMethodFromTarget) {
return [new Expr\MethodCall(
2 changes: 1 addition & 1 deletion src/Transformer/CopyEnumTransformer.php
Original file line number Diff line number Diff line change
@@ -17,7 +17,7 @@
*/
final class CopyEnumTransformer implements TransformerInterface
{
public function transform(Expr $input, Expr $target, PropertyMetadata $propertyMapping, UniqueVariableScope $uniqueVariableScope, Expr\Variable $source): array
public function transform(Expr $input, Expr $target, PropertyMetadata $propertyMapping, UniqueVariableScope $uniqueVariableScope, Expr\Variable $source, ?Expr $existingValue = null): array
{
/* No transform here it's the same value and it's a copy so we do not need to clone */
return [$input, []];
2 changes: 1 addition & 1 deletion src/Transformer/CopyTransformer.php
Original file line number Diff line number Diff line change
@@ -17,7 +17,7 @@
*/
final class CopyTransformer implements TransformerInterface
{
public function transform(Expr $input, Expr $target, PropertyMetadata $propertyMapping, UniqueVariableScope $uniqueVariableScope, Expr\Variable $source): array
public function transform(Expr $input, Expr $target, PropertyMetadata $propertyMapping, UniqueVariableScope $uniqueVariableScope, Expr\Variable $source, ?Expr $existingValue = null): array
{
/* No transform here it's the same value and it's a copy so we do not need to clone */
return [$input, []];
Original file line number Diff line number Diff line change
@@ -19,7 +19,7 @@
*/
final class DateTimeInterfaceToImmutableTransformer implements TransformerInterface
{
public function transform(Expr $input, Expr $target, PropertyMetadata $propertyMapping, UniqueVariableScope $uniqueVariableScope, Expr\Variable $source): array
public function transform(Expr $input, Expr $target, PropertyMetadata $propertyMapping, UniqueVariableScope $uniqueVariableScope, Expr\Variable $source, ?Expr $existingValue = null): array
{
/*
* Handles all DateTime instance types using createFromInterface.
2 changes: 1 addition & 1 deletion src/Transformer/DateTimeInterfaceToMutableTransformer.php
Original file line number Diff line number Diff line change
@@ -19,7 +19,7 @@
*/
final class DateTimeInterfaceToMutableTransformer implements TransformerInterface
{
public function transform(Expr $input, Expr $target, PropertyMetadata $propertyMapping, UniqueVariableScope $uniqueVariableScope, Expr\Variable $source): array
public function transform(Expr $input, Expr $target, PropertyMetadata $propertyMapping, UniqueVariableScope $uniqueVariableScope, Expr\Variable $source, ?Expr $existingValue = null): array
{
/*
* Handles all DateTime instance types using createFromInterface.
2 changes: 1 addition & 1 deletion src/Transformer/DateTimeToStringTransformer.php
Original file line number Diff line number Diff line change
@@ -26,7 +26,7 @@ public function __construct(
) {
}

public function transform(Expr $input, Expr $target, PropertyMetadata $propertyMapping, UniqueVariableScope $uniqueVariableScope, Expr\Variable $source): array
public function transform(Expr $input, Expr $target, PropertyMetadata $propertyMapping, UniqueVariableScope $uniqueVariableScope, Expr\Variable $source, ?Expr $existingValue = null): array
{
/*
* Format the date time object to a string.
2 changes: 1 addition & 1 deletion src/Transformer/ExpressionLanguageTransformer.php
Original file line number Diff line number Diff line change
@@ -26,7 +26,7 @@ public function __construct(
$this->parser = $parser ?? (new ParserFactory())->createForHostVersion();
}

public function transform(Expr $input, Expr $target, PropertyMetadata $propertyMapping, UniqueVariableScope $uniqueVariableScope, Expr\Variable $source): array
public function transform(Expr $input, Expr $target, PropertyMetadata $propertyMapping, UniqueVariableScope $uniqueVariableScope, Expr\Variable $source, ?Expr $existingValue = null): array
{
$expr = $this->parser->parse('<?php ' . $this->expression . ';')[0] ?? null;

2 changes: 1 addition & 1 deletion src/Transformer/FixedValueTransformer.php
Original file line number Diff line number Diff line change
@@ -26,7 +26,7 @@ public function __construct(
$this->parser = $parser ?? (new ParserFactory())->createForHostVersion();
}

public function transform(Expr $input, Expr $target, PropertyMetadata $propertyMapping, UniqueVariableScope $uniqueVariableScope, Expr\Variable $source): array
public function transform(Expr $input, Expr $target, PropertyMetadata $propertyMapping, UniqueVariableScope $uniqueVariableScope, Expr\Variable $source, ?Expr $existingValue = null): array
{
$expr = $this->parser->parse('<?php ' . var_export($this->value, true) . ';')[0] ?? null;

33 changes: 33 additions & 0 deletions src/Transformer/IdentifierHashInterface.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
<?php

declare(strict_types=1);

namespace AutoMapper\Transformer;

use PhpParser\Node\Expr;

/**
* @internal
*/
interface IdentifierHashInterface
{
/**
* Return an expression to get the hash of the source.
*
* As an example for a string type:
* ```php
* hash('sha256', $input)
* ```
*/
public function getSourceHashExpression(Expr $source): Expr;

/**
* Return an expression to get the hash of the target.
*
* As an example for a string type:
* ```php
* hash('sha256', $input)
* ```
*/
public function getTargetHashExpression(Expr $target): Expr;
}
4 changes: 2 additions & 2 deletions src/Transformer/MultipleTransformer.php
Original file line number Diff line number Diff line change
@@ -30,7 +30,7 @@ public function __construct(
) {
}

public function transform(Expr $input, Expr $target, PropertyMetadata $propertyMapping, UniqueVariableScope $uniqueVariableScope, Expr\Variable $source): array
public function transform(Expr $input, Expr $target, PropertyMetadata $propertyMapping, UniqueVariableScope $uniqueVariableScope, Expr\Variable $source, ?Expr $existingValue = null): array
{
$output = new Expr\Variable($uniqueVariableScope->getUniqueName('value'));
$statements = [
@@ -57,7 +57,7 @@ public function transform(Expr $input, Expr $target, PropertyMetadata $propertyM
$transformer = $transformerData['transformer'];
$type = $transformerData['type'];

[$transformerOutput, $transformerStatements] = $transformer->transform($input, $target, $propertyMapping, $uniqueVariableScope, $source);
[$transformerOutput, $transformerStatements] = $transformer->transform($input, $target, $propertyMapping, $uniqueVariableScope, $source, $existingValue);

$assignClass = ($transformer instanceof AssignedByReferenceTransformerInterface && $transformer->assignByRef()) ? Expr\AssignRef::class : Expr\Assign::class;
$condition = null;
4 changes: 2 additions & 2 deletions src/Transformer/NullableTransformer.php
Original file line number Diff line number Diff line change
@@ -25,9 +25,9 @@ public function __construct(
) {
}

public function transform(Expr $input, Expr $target, PropertyMetadata $propertyMapping, UniqueVariableScope $uniqueVariableScope, Expr\Variable $source): array
public function transform(Expr $input, Expr $target, PropertyMetadata $propertyMapping, UniqueVariableScope $uniqueVariableScope, Expr\Variable $source, ?Expr $existingValue = null): array
{
[$output, $itemStatements] = $this->itemTransformer->transform($input, $target, $propertyMapping, $uniqueVariableScope, $source);
[$output, $itemStatements] = $this->itemTransformer->transform($input, $target, $propertyMapping, $uniqueVariableScope, $source, $existingValue);

$newOutput = null;
$statements = [];
30 changes: 28 additions & 2 deletions src/Transformer/ObjectTransformer.php
Original file line number Diff line number Diff line change
@@ -20,7 +20,7 @@
*
* @internal
*/
final class ObjectTransformer implements TransformerInterface, DependentTransformerInterface, AssignedByReferenceTransformerInterface, CheckTypeInterface
final class ObjectTransformer implements TransformerInterface, DependentTransformerInterface, AssignedByReferenceTransformerInterface, CheckTypeInterface, IdentifierHashInterface
{
public function __construct(
private readonly Type $sourceType,
@@ -29,7 +29,7 @@ public function __construct(
) {
}

public function transform(Expr $input, Expr $target, PropertyMetadata $propertyMapping, UniqueVariableScope $uniqueVariableScope, Expr\Variable $source): array
public function transform(Expr $input, Expr $target, PropertyMetadata $propertyMapping, UniqueVariableScope $uniqueVariableScope, Expr\Variable $source, ?Expr $existingValue = null): array
{
$mapperName = $this->getDependencyName();

@@ -61,6 +61,8 @@ public function transform(Expr $input, Expr $target, PropertyMetadata $propertyM
new Expr\ConstFetch(new Name('null'))
)
);
} elseif ($existingValue !== null) {
$newContextArgs[] = new Arg($existingValue);
}

/*
@@ -143,4 +145,28 @@ private function getTarget(): string

return $targetTypeName;
}

public function getSourceHashExpression(Expr $source): Expr
{
$mapperName = $this->getDependencyName();

return new Expr\MethodCall(new Expr\ArrayDimFetch(
new Expr\PropertyFetch(new Expr\Variable('this'), 'mappers'),
new Scalar\String_($mapperName)
), 'getSourceHash', [
new Arg($source),
]);
}

public function getTargetHashExpression(Expr $target): Expr
{
$mapperName = $this->getDependencyName();

return new Expr\MethodCall(new Expr\ArrayDimFetch(
new Expr\PropertyFetch(new Expr\Variable('this'), 'mappers'),
new Scalar\String_($mapperName)
), 'getTargetHash', [
new Arg($target),
]);
}
}
Original file line number Diff line number Diff line change
@@ -34,7 +34,7 @@ public function __construct(
$this->parser = $parser ?? (new ParserFactory())->createForHostVersion();
}

public function transform(Expr $input, Expr $target, PropertyMetadata $propertyMapping, UniqueVariableScope $uniqueVariableScope, Expr\Variable $source): array
public function transform(Expr $input, Expr $target, PropertyMetadata $propertyMapping, UniqueVariableScope $uniqueVariableScope, Expr\Variable $source, ?Expr $existingValue = null): array
{
$context = new Expr\Variable('context');

2 changes: 1 addition & 1 deletion src/Transformer/SourceEnumTransformer.php
Original file line number Diff line number Diff line change
@@ -17,7 +17,7 @@
*/
final class SourceEnumTransformer implements TransformerInterface
{
public function transform(Expr $input, Expr $target, PropertyMetadata $propertyMapping, UniqueVariableScope $uniqueVariableScope, Expr\Variable $source): array
public function transform(Expr $input, Expr $target, PropertyMetadata $propertyMapping, UniqueVariableScope $uniqueVariableScope, Expr\Variable $source, ?Expr $existingValue = null): array
{
/* $input->value */
return [new Expr\PropertyFetch($input, 'value'), []];
2 changes: 1 addition & 1 deletion src/Transformer/StringToDateTimeTransformer.php
Original file line number Diff line number Diff line change
@@ -27,7 +27,7 @@ public function __construct(
) {
}

public function transform(Expr $input, Expr $target, PropertyMetadata $propertyMapping, UniqueVariableScope $uniqueVariableScope, Expr\Variable $source): array
public function transform(Expr $input, Expr $target, PropertyMetadata $propertyMapping, UniqueVariableScope $uniqueVariableScope, Expr\Variable $source, ?Expr $existingValue = null): array
{
$className = \DateTimeInterface::class === $this->className ? \DateTimeImmutable::class : $this->className;

2 changes: 1 addition & 1 deletion src/Transformer/StringToSymfonyUidTransformer.php
Original file line number Diff line number Diff line change
@@ -24,7 +24,7 @@ public function __construct(
) {
}

public function transform(Expr $input, Expr $target, PropertyMetadata $propertyMapping, UniqueVariableScope $uniqueVariableScope, Expr\Variable $source): array
public function transform(Expr $input, Expr $target, PropertyMetadata $propertyMapping, UniqueVariableScope $uniqueVariableScope, Expr\Variable $source, ?Expr $existingValue = null): array
{
/*
* Create a Symfony Uid object from a string.
2 changes: 1 addition & 1 deletion src/Transformer/SymfonyUidCopyTransformer.php
Original file line number Diff line number Diff line change
@@ -21,7 +21,7 @@
*/
final class SymfonyUidCopyTransformer implements TransformerInterface
{
public function transform(Expr $input, Expr $target, PropertyMetadata $propertyMapping, UniqueVariableScope $uniqueVariableScope, Expr\Variable $source): array
public function transform(Expr $input, Expr $target, PropertyMetadata $propertyMapping, UniqueVariableScope $uniqueVariableScope, Expr\Variable $source, ?Expr $existingValue = null): array
{
/*
* Create a Symfony Uid object from another Symfony Uid object.
2 changes: 1 addition & 1 deletion src/Transformer/SymfonyUidToStringTransformer.php
Original file line number Diff line number Diff line change
@@ -22,7 +22,7 @@ public function __construct(
) {
}

public function transform(Expr $input, Expr $target, PropertyMetadata $propertyMapping, UniqueVariableScope $uniqueVariableScope, Expr\Variable $source): array
public function transform(Expr $input, Expr $target, PropertyMetadata $propertyMapping, UniqueVariableScope $uniqueVariableScope, Expr\Variable $source, ?Expr $existingValue = null): array
{
/*
* Create a string from a Symfony Uid object.
2 changes: 1 addition & 1 deletion src/Transformer/TargetEnumTransformer.php
Original file line number Diff line number Diff line change
@@ -24,7 +24,7 @@ public function __construct(
) {
}

public function transform(Expr $input, Expr $target, PropertyMetadata $propertyMapping, UniqueVariableScope $uniqueVariableScope, Expr\Variable $source): array
public function transform(Expr $input, Expr $target, PropertyMetadata $propertyMapping, UniqueVariableScope $uniqueVariableScope, Expr\Variable $source, ?Expr $existingValue = null): array
{
/*
* Transform a string into a BackendEnum.
2 changes: 1 addition & 1 deletion src/Transformer/TransformerInterface.php
Original file line number Diff line number Diff line change
@@ -23,5 +23,5 @@ interface TransformerInterface
*
* @return array{0: Expr, 1: Stmt[]} First value is the output expression, second value is an array of stmt needed to get the output
*/
public function transform(Expr $input, Expr $target, PropertyMetadata $propertyMapping, UniqueVariableScope $uniqueVariableScope, Expr\Variable $source): array;
public function transform(Expr $input, Expr $target, PropertyMetadata $propertyMapping, UniqueVariableScope $uniqueVariableScope, Expr\Variable $source, ?Expr $existingValue = null): array;
}
2 changes: 1 addition & 1 deletion src/Transformer/VoidTransformer.php
Original file line number Diff line number Diff line change
@@ -13,7 +13,7 @@
*/
final readonly class VoidTransformer implements TransformerInterface
{
public function transform(Expr $input, Expr $target, PropertyMetadata $propertyMapping, UniqueVariableScope $uniqueVariableScope, Expr\Variable $source): array
public function transform(Expr $input, Expr $target, PropertyMetadata $propertyMapping, UniqueVariableScope $uniqueVariableScope, Expr\Variable $source, ?Expr $existingValue = null): array
{
return [$input, []];
}
14 changes: 14 additions & 0 deletions tests/AutoMapperTest/DeepPopulateMergeExisting/expected.adder.data
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
AutoMapper\Tests\AutoMapperTest\DeepPopulateMergeExisting\FooAdder {
-bars: [
AutoMapper\Tests\AutoMapperTest\DeepPopulateMergeExisting\Bar {
-id: 1
+bar: "bar3"
+foo: "foo1"
}
AutoMapper\Tests\AutoMapperTest\DeepPopulateMergeExisting\Bar {
-id: 10
+bar: "bar4"
+foo: "default"
}
]
}
14 changes: 14 additions & 0 deletions tests/AutoMapperTest/DeepPopulateMergeExisting/expected.array.data
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
AutoMapper\Tests\AutoMapperTest\DeepPopulateMergeExisting\Foo {
+bars: [
AutoMapper\Tests\AutoMapperTest\DeepPopulateMergeExisting\Bar {
-id: 1
+bar: "bar3"
+foo: "foo1"
}
AutoMapper\Tests\AutoMapperTest\DeepPopulateMergeExisting\Bar {
-id: 10
+bar: "bar4"
+foo: "default"
}
]
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
AutoMapper\Tests\AutoMapperTest\DeepPopulateMergeExisting\FooWithArrayCollection {
+bars: Doctrine\Common\Collections\ArrayCollection {
-elements: [
AutoMapper\Tests\AutoMapperTest\DeepPopulateMergeExisting\Bar {
-id: 1
+bar: "bar3"
+foo: "foo1"
}
AutoMapper\Tests\AutoMapperTest\DeepPopulateMergeExisting\Bar {
-id: 10
+bar: "bar4"
+foo: "default"
}
]
}
}
103 changes: 103 additions & 0 deletions tests/AutoMapperTest/DeepPopulateMergeExisting/map.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,103 @@
<?php

declare(strict_types=1);

namespace AutoMapper\Tests\AutoMapperTest\DeepPopulateMergeExisting;

use AutoMapper\Attribute\MapFrom;
use AutoMapper\Tests\AutoMapperBuilder;
use Doctrine\Common\Collections\ArrayCollection;

class Bar
{
#[MapFrom(source: 'array', identifier: true)]
private int $id;

public string $bar;

public string $foo = 'default';

public function setId(int $id): void
{
$this->id = $id;
}

public function getId(): int
{
return $this->id;
}
}

class Foo
{
/** @var array<Bar> */
public array $bars;
}

class FooAdder
{
/** @var array<Bar> */
private array $bars;

public function addBar(Bar $value): void
{
$this->bars[] = $value;
}

public function removeBar(Bar $value): void
{
foreach ($this->bars as $key => $existing) {
if ($existing->getId() === $value->getId()) {
unset($this->bars[$key]);
$this->bars = array_values($this->bars);
}
}
}

public function getBars(): array
{
return $this->bars;
}
}

class FooWithArrayCollection
{
/** @var ArrayCollection<Bar> */
public ArrayCollection $bars;
}

return (function () {
$autoMapper = AutoMapperBuilder::buildAutoMapper(mapPrivatePropertiesAndMethod: true);

$data = [
'bars' => [
['bar' => 'bar3', 'id' => 1],
['bar' => 'bar4', 'id' => 10],
],
];

$bar1 = new Bar();
$bar1->bar = 'bar1';
$bar1->foo = 'foo1';
$bar1->setId(1);

$bar2 = new Bar();
$bar2->bar = 'bar2';
$bar2->setId(2);

$existingObject = new Foo();
$existingObject->bars = [$bar1, $bar2];

$existingObjectWithArrayCollection = new FooWithArrayCollection();
$existingObjectWithArrayCollection->bars = new ArrayCollection([$bar1, $bar2]);

$existingObjectWithAdder = new FooAdder();
$existingObjectWithAdder->addBar($bar1);
$existingObjectWithAdder->addBar($bar2);

yield 'array' => $autoMapper->map($data, $existingObject, ['deep_target_to_populate' => true]);

yield 'collection' => $autoMapper->map($data, $existingObjectWithArrayCollection, ['deep_target_to_populate' => true]);

yield 'adder' => $autoMapper->map($data, $existingObjectWithAdder, ['deep_target_to_populate' => true]);
})();
Original file line number Diff line number Diff line change
@@ -1,11 +1,5 @@
AutoMapper\Tests\AutoMapperTest\DeepPopulateWithArrayCollection\Foo {
+bars: [
AutoMapper\Tests\AutoMapperTest\DeepPopulateWithArrayCollection\Bar {
+bar: "bar1"
}
AutoMapper\Tests\AutoMapperTest\DeepPopulateWithArrayCollection\Bar {
+bar: "bar2"
}
AutoMapper\Tests\AutoMapperTest\DeepPopulateWithArrayCollection\Bar {
+bar: "bar3"
}
Original file line number Diff line number Diff line change
@@ -1,12 +1,6 @@
AutoMapper\Tests\AutoMapperTest\DeepPopulateWithArrayCollection\FooWithArrayCollection {
+bars: Doctrine\Common\Collections\ArrayCollection {
-elements: [
AutoMapper\Tests\AutoMapperTest\DeepPopulateWithArrayCollection\Bar {
+bar: "bar1"
}
AutoMapper\Tests\AutoMapperTest\DeepPopulateWithArrayCollection\Bar {
+bar: "bar2"
}
AutoMapper\Tests\AutoMapperTest\DeepPopulateWithArrayCollection\Bar {
+bar: "bar3"
}
2 changes: 1 addition & 1 deletion tests/Fixtures/Transformer/ArrayToMoneyTransformer.php
Original file line number Diff line number Diff line change
@@ -21,7 +21,7 @@
*/
final class ArrayToMoneyTransformer implements TransformerInterface
{
public function transform(Expr $input, Expr $target, PropertyMetadata $propertyMapping, UniqueVariableScope $uniqueVariableScope, Expr\Variable $source): array
public function transform(Expr $input, Expr $target, PropertyMetadata $propertyMapping, UniqueVariableScope $uniqueVariableScope, Expr\Variable $source, ?Expr $existingValue = null): array
{
return [new Expr\New_(new Name\FullyQualified(Money::class), [
new Arg(new Expr\ArrayDimFetch($input, new String_('amount'))),
2 changes: 1 addition & 1 deletion tests/Fixtures/Transformer/MoneyToArrayTransformer.php
Original file line number Diff line number Diff line change
@@ -18,7 +18,7 @@
*/
final class MoneyToArrayTransformer implements TransformerInterface
{
public function transform(Expr $input, Expr $target, PropertyMetadata $propertyMapping, UniqueVariableScope $uniqueVariableScope, Expr\Variable $source): array
public function transform(Expr $input, Expr $target, PropertyMetadata $propertyMapping, UniqueVariableScope $uniqueVariableScope, Expr\Variable $source, ?Expr $existingValue = null): array
{
$moneyVar = new Expr\Variable($uniqueVariableScope->getUniqueName('money'));

2 changes: 1 addition & 1 deletion tests/Fixtures/Transformer/MoneyToMoneyTransformer.php
Original file line number Diff line number Diff line change
@@ -20,7 +20,7 @@
*/
final class MoneyToMoneyTransformer implements TransformerInterface
{
public function transform(Expr $input, Expr $target, PropertyMetadata $propertyMapping, UniqueVariableScope $uniqueVariableScope, Expr\Variable $source): array
public function transform(Expr $input, Expr $target, PropertyMetadata $propertyMapping, UniqueVariableScope $uniqueVariableScope, Expr\Variable $source, ?Expr $existingValue = null): array
{
return [
new Expr\New_(new Name\FullyQualified(Money::class), [