Skip to content

Commit bde92ab

Browse files
committed
better way for target property
1 parent de36c79 commit bde92ab

File tree

7 files changed

+345
-31
lines changed

7 files changed

+345
-31
lines changed

README.md

+21-9
Original file line numberDiff line numberDiff line change
@@ -2,19 +2,31 @@
22

33
// description
44

5-
## Requirements
5+
## Usage
66

7-
### Environments
7+
// in progress
88

9-
| name | usage |
10-
| - | - |
11-
| `APP_DEBUG` | If `true`, the blueprint will be checked for changes in targeted classes. |
12-
| `ULTRAMAPPER_BLUEPRINTS_DIR` | Default is `%kernel.project_dir%/var/ultra-mapper/blueprints/` |
13-
| `ULTRAMAPPER_MAPPERS_DIR` | Default is `%kernel.project_dir%/var/ultra-mapper/mappers/` |
9+
## Attributes
1410

15-
## Usage
11+
### TargetProperty<hr>
1612

17-
// in progress
13+
The behavior of the `#[TargetProperty()]` attribute depends on the class in which you declare it (*origin*, *source*, *target*). The table below presents the relationship between the declaration place and the active process, and how the attribute changes the name (or path) of the property. Placing an attribute in an origin class has no effect on any process unless the origin class is also a source class, a target class, or both.
14+
15+
##### NAME
16+
17+
| Declaration place | Normalization | Denormalization | Mapping | Transformation |
18+
|:-:|:-:|:-:|:-:|:-:|
19+
| **Source** | Source: ✖️<br>Target: ✖️ | Source: ✔️<br>Target: ✖️ | Source: ✖️<br>Target: ✔️ | Source: ✖️<br>Target: ✔️ |
20+
| **Target** | Source: ✖️<br>Target: ✔️ | Source: ✖️<br>Target: ✖️ | Source: ✔️<br>Target: ✖️ | Source: ✔️<br>Target: ✖️ |
21+
22+
##### PATH
23+
24+
| Declaration place | Normalization | Denormalization | Mapping | Transformation |
25+
|:-:|:-:|:-:|:-:|:-:|
26+
| **Source** | Source: ✖️<br>Target: ✖️ | Source: ✔️<br>Target: ✖️ | Source: ✖️<br>Target: ➖ | Source: ✖️<br>Target: ➖ |
27+
| **Target** | Source: ✖️<br>Target: ➖ | Source: ✖️<br>Target: ✖️ | Source: ➖<br>Target: ✖️ | Source: ✔️<br>Target: ✖️ |
28+
29+
###### legend: ✖️ - *has no effect*, ✔️ - *affects*, ➖ - *not implemented*
1830

1931
## How it works
2032

src/Blueprint/Application/Model/Assets/AssetsAggregate.php

+1-1
Original file line numberDiff line numberDiff line change
@@ -30,7 +30,7 @@ public function offsetGet(mixed $offset): mixed
3030
throw new \InvalidArgumentException('Aggregate key must be a string');
3131
}
3232

33-
return $this->assets[$offset];
33+
return $this->assets[$offset] ?? null;
3434
}
3535

3636
public function offsetSet(mixed $offset, mixed $value): void

src/Blueprint/Application/Model/Assets/PropertyBlueprint.php

+7
Original file line numberDiff line numberDiff line change
@@ -112,6 +112,13 @@ public function __clone(): void
112112
$this->attributes = clone $this->attributes;
113113
$this->attributes->root = $this;
114114
foreach ($this->attributes as $attribute) {
115+
if (is_array($attribute)) {
116+
foreach ($attribute as $attr) {
117+
$attr->parent = $this;
118+
}
119+
120+
continue;
121+
}
115122
$attribute->parent = $this;
116123
}
117124
}

src/Mapper/Domain/Service/Matcher/TargetPropertyAttributeStrategy.php

+70-21
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@
77
use PBaszak\UltraMapper\Blueprint\Application\Model\Assets\ClassBlueprint;
88
use PBaszak\UltraMapper\Blueprint\Application\Model\Assets\PropertyBlueprint;
99
use PBaszak\UltraMapper\Mapper\Application\Attribute\TargetProperty;
10+
use PBaszak\UltraMapper\Mapper\Application\Contract\TypeInterface;
1011

1112
class TargetPropertyAttributeStrategy implements MatchingStrategyInterface
1213
{
@@ -17,36 +18,38 @@ public function confirmClassMatching(string $processType, ClassBlueprint $origin
1718

1819
public function confirmPropertiesMatching(string $processType, PropertyBlueprint $origin, PropertyBlueprint $source, PropertyBlueprint $target): bool
1920
{
20-
if ($sourceTargetProperty = $this->getPropertyTargetPropertyAttribute($source, $processType)) {
21-
// source has same name as origin, source has target property attribute
22-
if ($origin->originName === $source->originName && $target->originName === $sourceTargetProperty->name) {
23-
$target->options['name'] ??= $sourceTargetProperty->name;
21+
$sourceTargetProperty = $this->getPropertyTargetPropertyAttribute($source, $processType);
22+
$hasSourceTargetProperty = null !== $sourceTargetProperty;
23+
$targetTargetProperty = $this->getPropertyTargetPropertyAttribute($target, $processType);
24+
$hasTargetTargetProperty = null !== $targetTargetProperty;
25+
26+
// source affects source
27+
if ($this->isTargetPropertyAttrHasAffect($processType, 'source', 'source', $hasSourceTargetProperty, $hasTargetTargetProperty)) {
28+
if ($origin->originName === $source->originName && $origin->originName === $target->originName) {
29+
$source->options['name'] = $sourceTargetProperty->name;
2430

2531
return true;
2632
}
2733
}
2834

29-
if ($originTargetProperty = $this->getPropertyTargetPropertyAttribute($origin, $processType)) {
30-
// source has same name as origin, but the origin has target property attribute
31-
if ($origin->originName === $source->originName && $source->originName === $originTargetProperty->name) {
32-
$target->options['name'] ??= $originTargetProperty->name;
33-
35+
// source affects target
36+
if ($this->isTargetPropertyAttrHasAffect($processType, 'source', 'target', $hasSourceTargetProperty, $hasTargetTargetProperty)) {
37+
if ($origin->originName === $source->originName && $sourceTargetProperty->name === $target->originName) {
3438
return true;
3539
}
3640
}
3741

38-
if ($targetTargetProperty = $this->getPropertyTargetPropertyAttribute($target, $processType)) {
39-
// target has same name as origin, target has target property attribute
40-
if ($origin->originName === $target->originName && $source->originName === $targetTargetProperty->name) {
41-
$source->options['name'] ??= $targetTargetProperty->name;
42-
42+
// target affects source
43+
if ($this->isTargetPropertyAttrHasAffect($processType, 'target', 'source', $hasSourceTargetProperty, $hasTargetTargetProperty)) {
44+
if ($origin->originName === $target->originName && $targetTargetProperty->name === $source->originName) {
4345
return true;
4446
}
47+
}
4548

46-
// target has same name as source, but the target has target property attribute
47-
if ($source->originName === $target->originName && $origin->originName === $targetTargetProperty->name) {
48-
// do nothing, source and target are already matched, only origin has different originName but it's match
49-
// based on target property attribute with both source and target
49+
// target affects target
50+
if ($this->isTargetPropertyAttrHasAffect($processType, 'target', 'target', $hasSourceTargetProperty, $hasTargetTargetProperty)) {
51+
if ($origin->originName === $target->originName && $origin->originName === $source->originName) {
52+
$target->options['name'] = $targetTargetProperty->name;
5053

5154
return true;
5255
}
@@ -55,14 +58,60 @@ public function confirmPropertiesMatching(string $processType, PropertyBlueprint
5558
return false;
5659
}
5760

61+
/**
62+
* @param string<"source"|"target"> $declarationPlace
63+
* @param string<"source"|"target"> $context (the resource which is possible affected by target property attribute)
64+
* @param bool $sourceHas whether source has target property attribute
65+
* @param bool $targetHas whether target has target property attribute
66+
*/
67+
protected function isTargetPropertyAttrHasAffect(string $processType, string $declarationPlace, string $context, bool $sourceHas, bool $targetHas): bool
68+
{
69+
return match ($processType) {
70+
TypeInterface::NORMALIZATION_PROCESS => match ($declarationPlace) {
71+
'source' => false,
72+
'target' => match ($context) {
73+
'source' => false,
74+
'target' => $targetHas,
75+
}
76+
},
77+
TypeInterface::DENORMALIZATION_PROCESS => match ($declarationPlace) {
78+
'source' => match ($context) {
79+
'source' => $sourceHas,
80+
'target' => false,
81+
},
82+
'target' => false,
83+
},
84+
TypeInterface::MAPPING_PROCESS => match ($declarationPlace) {
85+
'source' => match ($context) {
86+
'source' => false,
87+
'target' => $targetHas,
88+
},
89+
'target' => match ($context) {
90+
'source' => $sourceHas,
91+
'target' => false,
92+
}
93+
},
94+
TypeInterface::TRANSFORMATION_PROCESS => match ($declarationPlace) {
95+
'source' => match ($context) {
96+
'source' => false,
97+
'target' => $targetHas,
98+
},
99+
'target' => match ($context) {
100+
'source' => $sourceHas,
101+
'target' => false,
102+
}
103+
},
104+
};
105+
}
106+
58107
protected function getPropertyTargetPropertyAttribute(PropertyBlueprint $blueprint, string $processType): ?TargetProperty
59108
{
60-
foreach ($blueprint->attributes[TargetProperty::class] as $attribute) {
109+
foreach ($blueprint->attributes[TargetProperty::class] ?? [] as $attribute) {
61110
/** @var TargetProperty $instance */
62111
$instance = $attribute->newInstance();
63112

64113
$binaryProcessType = $instance::PROCESS_TYPE_MAP[$processType];
65-
if ($instance->useNameFor & $binaryProcessType === $binaryProcessType) {
114+
if (($instance->useNameFor & $binaryProcessType) === $binaryProcessType) {
66115
return $instance;
67116
}
68117
}
@@ -81,7 +130,7 @@ protected function getClassTargetPropertyAttribute(ClassBlueprint $blueprint, st
81130
$instance = $attribute->newInstance();
82131

83132
$binaryProcessType = $instance::PROCESS_TYPE_MAP[$processType];
84-
if ($instance->usePathFor & $binaryProcessType === $binaryProcessType) {
133+
if (($instance->usePathFor & $binaryProcessType) === $binaryProcessType) {
85134
return $instance;
86135
}
87136
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,85 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
namespace PBaszak\UltraMapper\Tests\Mapper\Unit\Domain\Service\Matcher;
6+
7+
use PBaszak\UltraMapper\Blueprint\Application\Model\Assets\ClassBlueprint;
8+
use PBaszak\UltraMapper\Mapper\Application\Attribute\TargetProperty;
9+
use PBaszak\UltraMapper\Mapper\Application\Contract\TypeInterface;
10+
use PHPUnit\Framework\Attributes\Group;
11+
use PHPUnit\Framework\TestCase;
12+
13+
#[Group('unit')]
14+
class TargetPropertyAttributeStrategyTest extends TestCase
15+
{
16+
public static function getDataForConfirmPropertiesMatchingMethod(): array
17+
{
18+
$classA = new class() {
19+
public $propertyA;
20+
public $propertyB;
21+
public $propertyC;
22+
};
23+
$classABlueprint = ClassBlueprint::create(get_class($classA), null);
24+
25+
$classB = new class() {
26+
public $propertyA;
27+
#[TargetProperty('propertyA', TargetProperty::NORMALIZATION | TargetProperty::DENORMALIZATION | TargetProperty::MAPPING | TargetProperty::TRANSFORMATION)]
28+
public $propertyB;
29+
public $propertyC;
30+
};
31+
$classBBlueprint = ClassBlueprint::create(get_class($classB), null);
32+
33+
return [
34+
// normalization
35+
[
36+
'processType' => TypeInterface::NORMALIZATION_PROCESS,
37+
'origin' => clone $classABlueprint->properties['propertyA'],
38+
'source' => clone $classABlueprint->properties['propertyA'],
39+
'target' => clone $classABlueprint->properties['propertyA'],
40+
'expectedResult' => false,
41+
],
42+
[
43+
'processType' => TypeInterface::NORMALIZATION_PROCESS,
44+
'origin' => clone $classBBlueprint->properties['propertyB'],
45+
'source' => clone $classBBlueprint->properties['propertyB'],
46+
'target' => clone $classABlueprint->properties['propertyA'],
47+
'expectedResult' => false,
48+
],
49+
[
50+
'processType' => TypeInterface::NORMALIZATION_PROCESS,
51+
'origin' => clone $classBBlueprint->properties['propertyA'],
52+
'source' => clone $classBBlueprint->properties['propertyA'],
53+
'target' => clone $classABlueprint->properties['propertyB'],
54+
'expectedResult' => false,
55+
],
56+
57+
// denormalization
58+
[
59+
'processType' => TypeInterface::DENORMALIZATION_PROCESS,
60+
'origin' => clone $classABlueprint->properties['property'],
61+
'source' => clone $classABlueprint->properties['property'],
62+
'target' => clone $classABlueprint->properties['property'],
63+
'expectedResult' => false,
64+
],
65+
66+
// mapping
67+
[
68+
'processType' => TypeInterface::MAPPING_PROCESS,
69+
'origin' => clone $classABlueprint->properties['property'],
70+
'source' => clone $classABlueprint->properties['property'],
71+
'target' => clone $classABlueprint->properties['property'],
72+
'expectedResult' => false,
73+
],
74+
75+
// transformation
76+
[
77+
'processType' => TypeInterface::TRANSFORMATION_PROCESS,
78+
'origin' => clone $classABlueprint->properties['property'],
79+
'source' => clone $classABlueprint->properties['property'],
80+
'target' => clone $classABlueprint->properties['property'],
81+
'expectedResult' => false,
82+
],
83+
];
84+
}
85+
}

0 commit comments

Comments
 (0)