Skip to content

Commit 1151b7f

Browse files
committed
feat(mapper): allow to merge existing values by extracting identifiers
1 parent 321cc75 commit 1151b7f

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

42 files changed

+579
-60
lines changed

src/Attribute/MapIdentifier.php

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
namespace AutoMapper\Attribute;
6+
7+
#[\Attribute(\Attribute::TARGET_PROPERTY | \Attribute::TARGET_METHOD | \Attribute::TARGET_PARAMETER)]
8+
final readonly class MapIdentifier
9+
{
10+
}

src/Event/PropertyMetadataEvent.php

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,7 @@ public function __construct(
3232
public int $priority = 0,
3333
public readonly bool $isFromDefaultExtractor = false,
3434
public ?bool $extractTypesFromGetter = null,
35+
public ?bool $identifier = null,
3536
) {
3637
}
3738
}

src/Extractor/MappingExtractor.php

Lines changed: 70 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,8 +4,10 @@
44

55
namespace AutoMapper\Extractor;
66

7+
use AutoMapper\Attribute\MapIdentifier;
78
use AutoMapper\Configuration;
89
use AutoMapper\Event\PropertyMetadataEvent;
10+
use AutoMapper\Metadata\TargetPropertyMetadata;
911
use Symfony\Component\PropertyInfo\PropertyInfoExtractorInterface;
1012
use Symfony\Component\PropertyInfo\PropertyReadInfo;
1113
use Symfony\Component\PropertyInfo\PropertyReadInfoExtractorInterface;
@@ -187,4 +189,72 @@ public function getDateTimeFormat(PropertyMetadataEvent $propertyMetadataEvent):
187189

188190
return $this->configuration->dateTimeFormat;
189191
}
192+
193+
public function isIdentifier(?\ReflectionClass $targetReflectionClass, TargetPropertyMetadata $target): bool
194+
{
195+
if ($targetReflectionClass === null) {
196+
return false;
197+
}
198+
199+
// check on reflection property
200+
if ($targetReflectionClass->hasProperty($target->property)) {
201+
$reflectionProperty = $targetReflectionClass->getProperty($target->property);
202+
203+
if (\count($reflectionProperty->getAttributes(MapIdentifier::class)) > 0) {
204+
return true;
205+
}
206+
}
207+
208+
// check on the getter to read, it may not be defined yet so we cannot skip this everytime
209+
if ($target->readAccessor !== null) {
210+
// check with read accessor on property
211+
if ($target->readAccessor->type === ReadAccessor::TYPE_PROPERTY && $targetReflectionClass->hasProperty(
212+
$target->readAccessor->property
213+
)) {
214+
$reflectionProperty = $targetReflectionClass->getProperty(
215+
$target->readAccessor->property
216+
);
217+
218+
if (\count($reflectionProperty->getAttributes(MapIdentifier::class)) > 0) {
219+
return true;
220+
}
221+
}
222+
223+
// check with read accessor on method
224+
if ($target->readAccessor->type === ReadAccessor::TYPE_METHOD && $targetReflectionClass->hasMethod(
225+
$target->readAccessor->property
226+
)) {
227+
$reflectionMethod = $targetReflectionClass->getMethod(
228+
$target->readAccessor->property
229+
);
230+
231+
if (\count($reflectionMethod->getAttributes(MapIdentifier::class)) > 0) {
232+
return true;
233+
}
234+
}
235+
}
236+
237+
// same for the write mutator, we cannot globally skip this check as it may be defined latter
238+
if ($target->writeMutator !== null) {
239+
// check for property
240+
if ($target->writeMutator->type === WriteMutator::TYPE_PROPERTY && $targetReflectionClass->hasProperty($target->writeMutator->property)) {
241+
$reflectionProperty = $targetReflectionClass->getProperty(
242+
$target->writeMutator->property
243+
);
244+
245+
if (\count($reflectionProperty->getAttributes(MapIdentifier::class)) > 0) {
246+
return true;
247+
}
248+
}
249+
250+
// check for parameter in constructor
251+
if ($target->writeMutator->type === WriteMutator::TYPE_CONSTRUCTOR && $target->writeMutator->parameter !== null) {
252+
if (\count($target->writeMutator->parameter->getAttributes(MapIdentifier::class)) > 0) {
253+
return true;
254+
}
255+
}
256+
}
257+
258+
return false;
259+
}
190260
}

src/Extractor/MappingExtractorInterface.php

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -49,4 +49,9 @@ public function getReadAccessor(string $class, string $property): ?ReadAccessor;
4949
* @param array<string, mixed> $context
5050
*/
5151
public function getWriteMutator(string $source, string $target, string $property, array $context = []): ?WriteMutator;
52+
53+
/**
54+
* Check if a property is an identifier.
55+
*/
56+
public function isIdentifier(?\ReflectionClass $targetReflectionClass, TargetPropertyMetadata $target): bool;
5257
}

src/Extractor/ReadAccessor.php

Lines changed: 22 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -32,17 +32,24 @@ final class ReadAccessor
3232
public const TYPE_SOURCE = 4;
3333
public const TYPE_ARRAY_ACCESS = 5;
3434

35+
public const EXTRACT_IS_UNDEFINED_CALLBACK = 'extractIsUndefinedCallbacks';
36+
public const EXTRACT_IS_NULL_CALLBACK = 'extractIsNullCallbacks';
37+
public const EXTRACT_CALLBACK = 'extractCallbacks';
38+
public const EXTRACT_TARGET_IS_UNDEFINED_CALLBACK = 'extractTargetIsUndefinedCallbacks';
39+
public const EXTRACT_TARGET_IS_NULL_CALLBACK = 'extractTargetIsNullCallbacks';
40+
public const EXTRACT_TARGET_CALLBACK = 'extractTargetCallbacks';
41+
3542
/**
3643
* @param array<string, string> $context
3744
*/
3845
public function __construct(
39-
private readonly int $type,
40-
private readonly string $accessor,
41-
private readonly ?string $sourceClass = null,
42-
private readonly bool $private = false,
43-
private readonly ?string $property = null,
46+
public readonly int $type,
47+
public readonly string $accessor,
48+
public readonly ?string $sourceClass = null,
49+
public readonly bool $private = false,
50+
public readonly ?string $property = null,
4451
// will be the name of the property if different from accessor
45-
private readonly array $context = [],
52+
public readonly array $context = [],
4653
) {
4754
if (self::TYPE_METHOD === $this->type && null === $this->sourceClass) {
4855
throw new InvalidArgumentException('Source class must be provided when using "method" type.');
@@ -54,7 +61,7 @@ public function __construct(
5461
*
5562
* @throws CompileException
5663
*/
57-
public function getExpression(Expr $input): Expr
64+
public function getExpression(Expr $input, bool $target = false): Expr
5865
{
5966
if (self::TYPE_METHOD === $this->type) {
6067
$methodCallArguments = [];
@@ -99,7 +106,7 @@ public function getExpression(Expr $input): Expr
99106
* $this->extractCallbacks['method_name']($input)
100107
*/
101108
return new Expr\FuncCall(
102-
new Expr\ArrayDimFetch(new Expr\PropertyFetch(new Expr\Variable('this'), 'extractCallbacks'), new Scalar\String_($this->property ?? $this->accessor)),
109+
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)),
103110
[
104111
new Arg($input),
105112
]
@@ -124,7 +131,7 @@ public function getExpression(Expr $input): Expr
124131
* $this->extractCallbacks['property_name']($input)
125132
*/
126133
return new Expr\FuncCall(
127-
new Expr\ArrayDimFetch(new Expr\PropertyFetch(new Expr\Variable('this'), 'extractCallbacks'), new Scalar\String_($this->accessor)),
134+
new Expr\ArrayDimFetch(new Expr\PropertyFetch(new Expr\Variable('this'), $target ? self::EXTRACT_TARGET_CALLBACK : self::EXTRACT_CALLBACK), new Scalar\String_($this->accessor)),
128135
[
129136
new Arg($input),
130137
]
@@ -155,7 +162,7 @@ public function getExpression(Expr $input): Expr
155162
throw new CompileException('Invalid accessor for read expression');
156163
}
157164

158-
public function getIsDefinedExpression(Expr\Variable $input, bool $nullable = false): ?Expr
165+
public function getIsDefinedExpression(Expr\Variable $input, bool $nullable = false, bool $target = false): ?Expr
159166
{
160167
// It is not possible to check if the underlying data is defined, assumes it is, php will throw an error if it is not
161168
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
172179
* !$this->extractIsUndefinedCallbacks['property_name']($input)
173180
*/
174181
return new Expr\BooleanNot(new Expr\FuncCall(
175-
new Expr\ArrayDimFetch(new Expr\PropertyFetch(new Expr\Variable('this'), 'extractIsUndefinedCallbacks'), new Scalar\String_($this->accessor)),
182+
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)),
176183
[
177184
new Arg($input),
178185
]
@@ -212,7 +219,7 @@ public function getIsDefinedExpression(Expr\Variable $input, bool $nullable = fa
212219
return null;
213220
}
214221

215-
public function getIsNullExpression(Expr\Variable $input): Expr
222+
public function getIsNullExpression(Expr\Variable $input, bool $target = false): Expr
216223
{
217224
if (self::TYPE_METHOD === $this->type) {
218225
$methodCallExpr = $this->getExpression($input);
@@ -236,7 +243,7 @@ public function getIsNullExpression(Expr\Variable $input): Expr
236243
* $this->extractIsNullCallbacks['property_name']($input)
237244
*/
238245
return new Expr\FuncCall(
239-
new Expr\ArrayDimFetch(new Expr\PropertyFetch(new Expr\Variable('this'), 'extractIsNullCallbacks'), new Scalar\String_($this->accessor)),
246+
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)),
240247
[
241248
new Arg($input),
242249
]
@@ -270,7 +277,7 @@ public function getIsNullExpression(Expr\Variable $input): Expr
270277
throw new CompileException('Invalid accessor for read expression');
271278
}
272279

273-
public function getIsUndefinedExpression(Expr\Variable $input): Expr
280+
public function getIsUndefinedExpression(Expr\Variable $input, bool $target = false): Expr
274281
{
275282
if (\in_array($this->type, [self::TYPE_METHOD, self::TYPE_SOURCE])) {
276283
/*
@@ -289,7 +296,7 @@ public function getIsUndefinedExpression(Expr\Variable $input): Expr
289296
* $this->extractIsUndefinedCallbacks['property_name']($input)
290297
*/
291298
return new Expr\FuncCall(
292-
new Expr\ArrayDimFetch(new Expr\PropertyFetch(new Expr\Variable('this'), 'extractIsUndefinedCallbacks'), new Scalar\String_($this->accessor)),
299+
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)),
293300
[
294301
new Arg($input),
295302
]

src/Extractor/WriteMutator.php

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -32,8 +32,8 @@ final class WriteMutator
3232

3333
public function __construct(
3434
public readonly int $type,
35-
private readonly string $property,
36-
private readonly bool $private = false,
35+
public readonly string $property,
36+
public readonly bool $private = false,
3737
public readonly ?\ReflectionParameter $parameter = null,
3838
private readonly ?string $removeMethodName = null,
3939
) {

src/GeneratedMapper.php

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,11 @@ public function registerMappers(AutoMapperRegistryInterface $registry): void
3636
{
3737
}
3838

39+
public function areIdentifiersEquals(mixed $source, mixed $target): bool
40+
{
41+
return false;
42+
}
43+
3944
/** @var array<string, MapperInterface<object, object>|MapperInterface<object, array<mixed>>|MapperInterface<array<mixed>, object>> */
4045
protected array $mappers = [];
4146

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

59+
/** @var array<string, callable(): mixed> */
60+
protected array $extractTargetCallbacks = [];
61+
62+
/** @var array<string, callable(): bool>) */
63+
protected array $extractTargetIsNullCallbacks = [];
64+
65+
/** @var array<string, callable(): bool>) */
66+
protected array $extractTargetIsUndefinedCallbacks = [];
67+
5468
/** @var Target|\ReflectionClass<object> */
5569
protected mixed $cachedTarget;
5670
}
Lines changed: 83 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,83 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
namespace AutoMapper\Generator;
6+
7+
use AutoMapper\Metadata\GeneratorMetadata;
8+
use PhpParser\Node\Expr;
9+
use PhpParser\Node\Name;
10+
use PhpParser\Node\Stmt;
11+
12+
final readonly class IdentifierEqualGenerator
13+
{
14+
/**
15+
* @return list<Stmt>
16+
*/
17+
public function getStatements(GeneratorMetadata $metadata): array
18+
{
19+
$identifiers = [];
20+
21+
foreach ($metadata->propertiesMetadata as $propertyMetadata) {
22+
if (!$propertyMetadata->identifier) {
23+
continue;
24+
}
25+
26+
if (null === $propertyMetadata->target->readAccessor) {
27+
continue;
28+
}
29+
30+
if (null === $propertyMetadata->source->accessor) {
31+
continue;
32+
}
33+
34+
$identifiers[] = $propertyMetadata;
35+
}
36+
37+
if (empty($identifiers)) {
38+
return [];
39+
}
40+
41+
$statements = [];
42+
43+
$sourceVariable = new Expr\Variable('source');
44+
$targetVariable = new Expr\Variable('target');
45+
46+
// foreach property we check
47+
foreach ($identifiers as $property) {
48+
// check if the source is defined
49+
if ($property->source->checkExists) {
50+
$statements[] = new Stmt\If_($property->source->accessor->getIsUndefinedExpression($sourceVariable), [
51+
'stmts' => [
52+
new Stmt\Return_(new Expr\ConstFetch(new Name('false'))),
53+
],
54+
]);
55+
}
56+
57+
// check if the target is defined
58+
$statements[] = new Stmt\If_($property->target->readAccessor->getIsUndefinedExpression($targetVariable, true), [
59+
'stmts' => [
60+
new Stmt\Return_(new Expr\ConstFetch(new Name('false'))),
61+
],
62+
]);
63+
64+
// add the identifier check
65+
$statements[] = new Stmt\If_(
66+
new Expr\BinaryOp\NotIdentical(
67+
$property->source->accessor->getExpression($sourceVariable),
68+
$property->target->readAccessor->getExpression($targetVariable, true)
69+
),
70+
[
71+
'stmts' => [
72+
new Stmt\Return_(new Expr\ConstFetch(new Name('false'))),
73+
],
74+
]
75+
);
76+
}
77+
78+
// return true as everything is ok
79+
$statements[] = new Stmt\Return_(new Expr\ConstFetch(new Name('true')));
80+
81+
return $statements;
82+
}
83+
}

0 commit comments

Comments
 (0)