Skip to content

Commit 5d78b14

Browse files
authored
feat(mapper): allow to merge existing values by extracting identifiers (#260)
This introduce the possibility to deep merge object with collections and fetching the correct value to update it instead of adding a new one into the collection
2 parents a109163 + b97fa96 commit 5d78b14

Some content is hidden

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

46 files changed

+634
-122
lines changed

src/Attribute/MapFrom.php

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@
2020
* @param string[]|null $groups The groups to map the property
2121
* @param string|null $dateTimeFormat The date-time format to use when transforming this property
2222
* @param bool|null $extractTypesFromGetter If true, the types will be extracted from the getter method
23+
* @param bool|null $identifier If true, the property will be used as an identifier
2324
*/
2425
public function __construct(
2526
public string|array|null $source = null,
@@ -32,6 +33,7 @@ public function __construct(
3233
public int $priority = 0,
3334
public ?string $dateTimeFormat = null,
3435
public ?bool $extractTypesFromGetter = null,
36+
public ?bool $identifier = null,
3537
) {
3638
}
3739
}

src/Attribute/MapTo.php

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@
2020
* @param string[]|null $groups The groups to map the property
2121
* @param string|null $dateTimeFormat The date-time format to use when transforming this property
2222
* @param bool|null $extractTypesFromGetter If true, the types will be extracted from the getter method
23+
* @param bool|null $identifier If true, the property will be used as an identifier
2324
*/
2425
public function __construct(
2526
public string|array|null $target = null,
@@ -32,6 +33,7 @@ public function __construct(
3233
public int $priority = 0,
3334
public ?string $dateTimeFormat = null,
3435
public ?bool $extractTypesFromGetter = null,
36+
public ?bool $identifier = null,
3537
) {
3638
}
3739
}

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/EventListener/MapFromListener.php

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -84,6 +84,7 @@ private function addPropertyFromTarget(GenerateMapperEvent $event, MapFrom $mapF
8484
groups: $mapFrom->groups,
8585
priority: $mapFrom->priority,
8686
extractTypesFromGetter: $mapFrom->extractTypesFromGetter,
87+
identifier: $mapFrom->identifier,
8788
);
8889

8990
if (\array_key_exists($propertyMetadata->target->property, $event->properties) && $event->properties[$propertyMetadata->target->property]->priority >= $propertyMetadata->priority) {

src/EventListener/MapToListener.php

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -85,6 +85,7 @@ private function addPropertyFromSource(GenerateMapperEvent $event, MapTo $mapTo,
8585
groups: $mapTo->groups,
8686
priority: $mapTo->priority,
8787
extractTypesFromGetter: $mapTo->extractTypesFromGetter,
88+
identifier: $mapTo->identifier,
8889
);
8990

9091
if (\array_key_exists($propertyMetadata->target->property, $event->properties) && $event->properties[$propertyMetadata->target->property]->priority >= $propertyMetadata->priority) {

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: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,16 @@ public function registerMappers(AutoMapperRegistryInterface $registry): void
3636
{
3737
}
3838

39+
public function getSourceHash(mixed $value): ?string
40+
{
41+
return null;
42+
}
43+
44+
public function getTargetHash(mixed $value): ?string
45+
{
46+
return null;
47+
}
48+
3949
/** @var array<string, MapperInterface<object, object>|MapperInterface<object, array<mixed>>|MapperInterface<array<mixed>, object>> */
4050
protected array $mappers = [];
4151

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

64+
/** @var array<string, callable(): mixed> */
65+
protected array $extractTargetCallbacks = [];
66+
67+
/** @var array<string, callable(): bool>) */
68+
protected array $extractTargetIsNullCallbacks = [];
69+
70+
/** @var array<string, callable(): bool>) */
71+
protected array $extractTargetIsUndefinedCallbacks = [];
72+
5473
/** @var Target|\ReflectionClass<object> */
5574
protected mixed $cachedTarget;
5675
}
Lines changed: 100 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,100 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
namespace AutoMapper\Generator;
6+
7+
use AutoMapper\Metadata\GeneratorMetadata;
8+
use PhpParser\Node\Arg;
9+
use PhpParser\Node\Expr;
10+
use PhpParser\Node\Name;
11+
use PhpParser\Node\Scalar;
12+
use PhpParser\Node\Stmt;
13+
14+
final readonly class IdentifierHashGenerator
15+
{
16+
/**
17+
* @return list<Stmt>
18+
*/
19+
public function getStatements(GeneratorMetadata $metadata, bool $fromSource): array
20+
{
21+
$identifiers = [];
22+
23+
foreach ($metadata->propertiesMetadata as $propertyMetadata) {
24+
if (!$propertyMetadata->identifier) {
25+
continue;
26+
}
27+
28+
if (null === $propertyMetadata->target->readAccessor) {
29+
continue;
30+
}
31+
32+
if (null === $propertyMetadata->source->accessor) {
33+
continue;
34+
}
35+
36+
$identifiers[] = $propertyMetadata;
37+
}
38+
39+
if (empty($identifiers)) {
40+
return [];
41+
}
42+
43+
$hashCtxVariable = new Expr\Variable('hashCtx');
44+
45+
$statements = [
46+
new Stmt\Expression(new Expr\Assign($hashCtxVariable, new Expr\FuncCall(new Name('hash_init'), [
47+
new Arg(new Scalar\String_('sha256')),
48+
]))),
49+
];
50+
51+
$valueVariable = new Expr\Variable('value');
52+
53+
// foreach property we check
54+
foreach ($identifiers as $property) {
55+
if (null === $property->source->accessor || null === $property->target->readAccessor) {
56+
continue;
57+
}
58+
59+
// check if the source is defined
60+
if ($fromSource) {
61+
if ($property->source->checkExists) {
62+
$statements[] = new Stmt\If_($property->source->accessor->getIsUndefinedExpression($valueVariable), [
63+
'stmts' => [
64+
new Stmt\Return_(new Expr\ConstFetch(new Name('null'))),
65+
],
66+
]);
67+
}
68+
69+
// add identifier to hash
70+
$statements[] = new Stmt\Expression(new Expr\FuncCall(new Name('hash_update'), [
71+
new Arg($hashCtxVariable),
72+
new Arg($property->source->accessor->getExpression($valueVariable)),
73+
]));
74+
} else {
75+
$statements[] = new Stmt\If_($property->target->readAccessor->getIsUndefinedExpression($valueVariable, true), [
76+
'stmts' => [
77+
new Stmt\Return_(new Expr\ConstFetch(new Name('null'))),
78+
],
79+
]);
80+
81+
$statements[] = new Stmt\Expression(new Expr\FuncCall(new Name('hash_update'), [
82+
new Arg($hashCtxVariable),
83+
new Arg($property->target->readAccessor->getExpression($valueVariable, true)),
84+
]));
85+
}
86+
}
87+
88+
if (\count($statements) < 2) {
89+
return [];
90+
}
91+
92+
// return hash as string
93+
$statements[] = new Stmt\Return_(new Expr\FuncCall(new Name('hash_final'), [
94+
new Arg($hashCtxVariable),
95+
new Arg(new Scalar\String_('true')),
96+
]));
97+
98+
return $statements;
99+
}
100+
}

0 commit comments

Comments
 (0)