diff --git a/src/Attribute/MapFrom.php b/src/Attribute/MapFrom.php index 9a60a6ca..29ecdb7a 100644 --- a/src/Attribute/MapFrom.php +++ b/src/Attribute/MapFrom.php @@ -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, ) { } } diff --git a/src/Attribute/MapTo.php b/src/Attribute/MapTo.php index 3f3c21ec..f87c134b 100644 --- a/src/Attribute/MapTo.php +++ b/src/Attribute/MapTo.php @@ -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, ) { } } diff --git a/src/Event/PropertyMetadataEvent.php b/src/Event/PropertyMetadataEvent.php index 87639e0b..2668b39c 100644 --- a/src/Event/PropertyMetadataEvent.php +++ b/src/Event/PropertyMetadataEvent.php @@ -32,6 +32,7 @@ public function __construct( public int $priority = 0, public readonly bool $isFromDefaultExtractor = false, public ?bool $extractTypesFromGetter = null, + public ?bool $identifier = null, ) { } } diff --git a/src/EventListener/MapFromListener.php b/src/EventListener/MapFromListener.php index 901fb23f..782e5a09 100644 --- a/src/EventListener/MapFromListener.php +++ b/src/EventListener/MapFromListener.php @@ -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) { diff --git a/src/EventListener/MapToListener.php b/src/EventListener/MapToListener.php index 3c892083..0a436d1f 100644 --- a/src/EventListener/MapToListener.php +++ b/src/EventListener/MapToListener.php @@ -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) { diff --git a/src/Extractor/ReadAccessor.php b/src/Extractor/ReadAccessor.php index 98b9901c..4dbe4e36 100644 --- a/src/Extractor/ReadAccessor.php +++ b/src/Extractor/ReadAccessor.php @@ -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 $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), ] diff --git a/src/Extractor/WriteMutator.php b/src/Extractor/WriteMutator.php index 075bc130..39ca0172 100644 --- a/src/Extractor/WriteMutator.php +++ b/src/Extractor/WriteMutator.php @@ -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, ) { diff --git a/src/GeneratedMapper.php b/src/GeneratedMapper.php index 8a009e4b..a3b3eab2 100644 --- a/src/GeneratedMapper.php +++ b/src/GeneratedMapper.php @@ -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|MapperInterface>|MapperInterface, object>> */ protected array $mappers = []; @@ -51,6 +61,15 @@ public function registerMappers(AutoMapperRegistryInterface $registry): void /** @var array) */ protected array $extractIsUndefinedCallbacks = []; + /** @var array */ + protected array $extractTargetCallbacks = []; + + /** @var array) */ + protected array $extractTargetIsNullCallbacks = []; + + /** @var array) */ + protected array $extractTargetIsUndefinedCallbacks = []; + /** @var Target|\ReflectionClass */ protected mixed $cachedTarget; } diff --git a/src/Generator/IdentifierHashGenerator.php b/src/Generator/IdentifierHashGenerator.php new file mode 100644 index 00000000..1bd70066 --- /dev/null +++ b/src/Generator/IdentifierHashGenerator.php @@ -0,0 +1,100 @@ + + */ + 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; + } +} diff --git a/src/Generator/MapperConstructorGenerator.php b/src/Generator/MapperConstructorGenerator.php index 75c73340..2c83dcb4 100644 --- a/src/Generator/MapperConstructorGenerator.php +++ b/src/Generator/MapperConstructorGenerator.php @@ -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. * diff --git a/src/Generator/MapperGenerator.php b/src/Generator/MapperGenerator.php index d43467cc..33b3eee7 100644 --- a/src/Generator/MapperGenerator.php +++ b/src/Generator/MapperGenerator.php @@ -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(); + } } diff --git a/src/Metadata/MetadataFactory.php b/src/Metadata/MetadataFactory.php index 429048ec..e260a8f9 100644 --- a/src/Metadata/MetadataFactory.php +++ b/src/Metadata/MetadataFactory.php @@ -326,6 +326,7 @@ private function createGeneratorMetadata(MapperMetadata $mapperMetadata): Genera $propertyMappedEvent->if, $propertyMappedEvent->groups, $propertyMappedEvent->disableGroupsCheck, + $propertyMappedEvent->identifier ?? false, ); } diff --git a/src/Metadata/PropertyMetadata.php b/src/Metadata/PropertyMetadata.php index 5bb8c2d7..1036a147 100644 --- a/src/Metadata/PropertyMetadata.php +++ b/src/Metadata/PropertyMetadata.php @@ -29,6 +29,7 @@ public function __construct( public ?string $if = null, public ?array $groups = null, public ?bool $disableGroupsCheck = null, + public bool $identifier = false, ) { } } diff --git a/src/Transformer/AbstractArrayTransformer.php b/src/Transformer/AbstractArrayTransformer.php index 6a7376b6..ecb5bf24 100644 --- a/src/Transformer/AbstractArrayTransformer.php +++ b/src/Transformer/AbstractArrayTransformer.php @@ -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,14 +62,39 @@ 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' => [ @@ -94,13 +102,42 @@ public function transform(Expr $input, Expr $target, PropertyMetadata $propertyM ], ]); } 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)); } diff --git a/src/Transformer/ArrayToDoctrineCollectionTransformer.php b/src/Transformer/ArrayToDoctrineCollectionTransformer.php index 0dbd9069..cb527bda 100644 --- a/src/Transformer/ArrayToDoctrineCollectionTransformer.php +++ b/src/Transformer/ArrayToDoctrineCollectionTransformer.php @@ -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, [ diff --git a/src/Transformer/BuiltinTransformer.php b/src/Transformer/BuiltinTransformer.php index 9ddd5ec6..5dd8d6ad 100644 --- a/src/Transformer/BuiltinTransformer.php +++ b/src/Transformer/BuiltinTransformer.php @@ -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(); diff --git a/src/Transformer/CallableTransformer.php b/src/Transformer/CallableTransformer.php index c3e2cff5..0af8c33c 100644 --- a/src/Transformer/CallableTransformer.php +++ b/src/Transformer/CallableTransformer.php @@ -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( diff --git a/src/Transformer/CopyEnumTransformer.php b/src/Transformer/CopyEnumTransformer.php index 18c63632..fb5a407d 100644 --- a/src/Transformer/CopyEnumTransformer.php +++ b/src/Transformer/CopyEnumTransformer.php @@ -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, []]; diff --git a/src/Transformer/CopyTransformer.php b/src/Transformer/CopyTransformer.php index f54d298a..728540da 100644 --- a/src/Transformer/CopyTransformer.php +++ b/src/Transformer/CopyTransformer.php @@ -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, []]; diff --git a/src/Transformer/DateTimeInterfaceToImmutableTransformer.php b/src/Transformer/DateTimeInterfaceToImmutableTransformer.php index 436af353..31e30b8e 100644 --- a/src/Transformer/DateTimeInterfaceToImmutableTransformer.php +++ b/src/Transformer/DateTimeInterfaceToImmutableTransformer.php @@ -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. diff --git a/src/Transformer/DateTimeInterfaceToMutableTransformer.php b/src/Transformer/DateTimeInterfaceToMutableTransformer.php index 69ed3819..e2878eeb 100644 --- a/src/Transformer/DateTimeInterfaceToMutableTransformer.php +++ b/src/Transformer/DateTimeInterfaceToMutableTransformer.php @@ -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. diff --git a/src/Transformer/DateTimeToStringTransformer.php b/src/Transformer/DateTimeToStringTransformer.php index e6bdd710..d8906568 100644 --- a/src/Transformer/DateTimeToStringTransformer.php +++ b/src/Transformer/DateTimeToStringTransformer.php @@ -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. diff --git a/src/Transformer/ExpressionLanguageTransformer.php b/src/Transformer/ExpressionLanguageTransformer.php index f65b8c85..f685abb1 100644 --- a/src/Transformer/ExpressionLanguageTransformer.php +++ b/src/Transformer/ExpressionLanguageTransformer.php @@ -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('expression . ';')[0] ?? null; diff --git a/src/Transformer/FixedValueTransformer.php b/src/Transformer/FixedValueTransformer.php index 04c824aa..11c6c0a8 100644 --- a/src/Transformer/FixedValueTransformer.php +++ b/src/Transformer/FixedValueTransformer.php @@ -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('value, true) . ';')[0] ?? null; diff --git a/src/Transformer/IdentifierHashInterface.php b/src/Transformer/IdentifierHashInterface.php new file mode 100644 index 00000000..970d14a6 --- /dev/null +++ b/src/Transformer/IdentifierHashInterface.php @@ -0,0 +1,33 @@ +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; diff --git a/src/Transformer/NullableTransformer.php b/src/Transformer/NullableTransformer.php index f770be69..a681df62 100644 --- a/src/Transformer/NullableTransformer.php +++ b/src/Transformer/NullableTransformer.php @@ -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 = []; diff --git a/src/Transformer/ObjectTransformer.php b/src/Transformer/ObjectTransformer.php index b61f1723..7a88b3a6 100644 --- a/src/Transformer/ObjectTransformer.php +++ b/src/Transformer/ObjectTransformer.php @@ -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), + ]); + } } diff --git a/src/Transformer/PropertyTransformer/PropertyTransformer.php b/src/Transformer/PropertyTransformer/PropertyTransformer.php index e4bb9876..9b970bd2 100644 --- a/src/Transformer/PropertyTransformer/PropertyTransformer.php +++ b/src/Transformer/PropertyTransformer/PropertyTransformer.php @@ -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'); diff --git a/src/Transformer/SourceEnumTransformer.php b/src/Transformer/SourceEnumTransformer.php index 30b7a0aa..f281e550 100644 --- a/src/Transformer/SourceEnumTransformer.php +++ b/src/Transformer/SourceEnumTransformer.php @@ -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'), []]; diff --git a/src/Transformer/StringToDateTimeTransformer.php b/src/Transformer/StringToDateTimeTransformer.php index 8febe623..d55c417e 100644 --- a/src/Transformer/StringToDateTimeTransformer.php +++ b/src/Transformer/StringToDateTimeTransformer.php @@ -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; diff --git a/src/Transformer/StringToSymfonyUidTransformer.php b/src/Transformer/StringToSymfonyUidTransformer.php index c209dd10..5aee2569 100644 --- a/src/Transformer/StringToSymfonyUidTransformer.php +++ b/src/Transformer/StringToSymfonyUidTransformer.php @@ -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. diff --git a/src/Transformer/SymfonyUidCopyTransformer.php b/src/Transformer/SymfonyUidCopyTransformer.php index 35fab8ce..eaf7e6a8 100644 --- a/src/Transformer/SymfonyUidCopyTransformer.php +++ b/src/Transformer/SymfonyUidCopyTransformer.php @@ -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. diff --git a/src/Transformer/SymfonyUidToStringTransformer.php b/src/Transformer/SymfonyUidToStringTransformer.php index 104572c9..90f86094 100644 --- a/src/Transformer/SymfonyUidToStringTransformer.php +++ b/src/Transformer/SymfonyUidToStringTransformer.php @@ -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. diff --git a/src/Transformer/TargetEnumTransformer.php b/src/Transformer/TargetEnumTransformer.php index d957796e..6c25ff24 100644 --- a/src/Transformer/TargetEnumTransformer.php +++ b/src/Transformer/TargetEnumTransformer.php @@ -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. diff --git a/src/Transformer/TransformerInterface.php b/src/Transformer/TransformerInterface.php index 6347783d..de437350 100644 --- a/src/Transformer/TransformerInterface.php +++ b/src/Transformer/TransformerInterface.php @@ -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; } diff --git a/src/Transformer/VoidTransformer.php b/src/Transformer/VoidTransformer.php index ca4fb098..29814731 100644 --- a/src/Transformer/VoidTransformer.php +++ b/src/Transformer/VoidTransformer.php @@ -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, []]; } diff --git a/tests/AutoMapperTest/DeepPopulateMergeExisting/expected.adder.data b/tests/AutoMapperTest/DeepPopulateMergeExisting/expected.adder.data new file mode 100644 index 00000000..9d164c5b --- /dev/null +++ b/tests/AutoMapperTest/DeepPopulateMergeExisting/expected.adder.data @@ -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" + } + ] +} \ No newline at end of file diff --git a/tests/AutoMapperTest/DeepPopulateMergeExisting/expected.array.data b/tests/AutoMapperTest/DeepPopulateMergeExisting/expected.array.data new file mode 100644 index 00000000..70b7f8e8 --- /dev/null +++ b/tests/AutoMapperTest/DeepPopulateMergeExisting/expected.array.data @@ -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" + } + ] +} \ No newline at end of file diff --git a/tests/AutoMapperTest/DeepPopulateMergeExisting/expected.collection.data b/tests/AutoMapperTest/DeepPopulateMergeExisting/expected.collection.data new file mode 100644 index 00000000..9171dad9 --- /dev/null +++ b/tests/AutoMapperTest/DeepPopulateMergeExisting/expected.collection.data @@ -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" + } + ] + } +} \ No newline at end of file diff --git a/tests/AutoMapperTest/DeepPopulateMergeExisting/map.php b/tests/AutoMapperTest/DeepPopulateMergeExisting/map.php new file mode 100644 index 00000000..080782c3 --- /dev/null +++ b/tests/AutoMapperTest/DeepPopulateMergeExisting/map.php @@ -0,0 +1,103 @@ +id = $id; + } + + public function getId(): int + { + return $this->id; + } +} + +class Foo +{ + /** @var array */ + public array $bars; +} + +class FooAdder +{ + /** @var array */ + 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 */ + 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]); +})(); diff --git a/tests/AutoMapperTest/DeepPopulateWithArrayCollection/expected.array.data b/tests/AutoMapperTest/DeepPopulateWithArrayCollection/expected.array.data index bf010e4c..5aa92835 100644 --- a/tests/AutoMapperTest/DeepPopulateWithArrayCollection/expected.array.data +++ b/tests/AutoMapperTest/DeepPopulateWithArrayCollection/expected.array.data @@ -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" } diff --git a/tests/AutoMapperTest/DeepPopulateWithArrayCollection/expected.collection.data b/tests/AutoMapperTest/DeepPopulateWithArrayCollection/expected.collection.data index a3e50cd5..df498afc 100644 --- a/tests/AutoMapperTest/DeepPopulateWithArrayCollection/expected.collection.data +++ b/tests/AutoMapperTest/DeepPopulateWithArrayCollection/expected.collection.data @@ -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" } diff --git a/tests/Fixtures/Transformer/ArrayToMoneyTransformer.php b/tests/Fixtures/Transformer/ArrayToMoneyTransformer.php index dd4286cb..d9c7b2cf 100644 --- a/tests/Fixtures/Transformer/ArrayToMoneyTransformer.php +++ b/tests/Fixtures/Transformer/ArrayToMoneyTransformer.php @@ -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'))), diff --git a/tests/Fixtures/Transformer/MoneyToArrayTransformer.php b/tests/Fixtures/Transformer/MoneyToArrayTransformer.php index 51235734..74b5929d 100644 --- a/tests/Fixtures/Transformer/MoneyToArrayTransformer.php +++ b/tests/Fixtures/Transformer/MoneyToArrayTransformer.php @@ -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')); diff --git a/tests/Fixtures/Transformer/MoneyToMoneyTransformer.php b/tests/Fixtures/Transformer/MoneyToMoneyTransformer.php index a3eaff1f..264e610b 100644 --- a/tests/Fixtures/Transformer/MoneyToMoneyTransformer.php +++ b/tests/Fixtures/Transformer/MoneyToMoneyTransformer.php @@ -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), [