Skip to content

Commit de36c79

Browse files
committed
feat: Added Matcher and support for TargetProperty::name
1 parent 92b8348 commit de36c79

13 files changed

+275
-26
lines changed

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

+5
Original file line numberDiff line numberDiff line change
@@ -61,6 +61,11 @@ public function isBlueprintAttribute(): bool
6161
return $this->parent instanceof ClassBlueprint;
6262
}
6363

64+
public function newInstance(): object
65+
{
66+
return new $this->class(...$this->arguments);
67+
}
68+
6469
public function getReflection(): \ReflectionAttribute
6570
{
6671
$attr = $this->parent->getReflection()->getAttributes($this->class);

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

+5
Original file line numberDiff line numberDiff line change
@@ -91,6 +91,11 @@ public function getReflection(): \ReflectionClass
9191
return new \ReflectionClass($this->name);
9292
}
9393

94+
public function getPath(): string
95+
{
96+
return $this->parent?->getPath() ?? '';
97+
}
98+
9499
public function hasDeclarationFile(): bool
95100
{
96101
return false !== $this->filePath;

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

+24
Original file line numberDiff line numberDiff line change
@@ -60,6 +60,28 @@ public static function createCollection(ClassBlueprint $root): AssetsAggregate
6060
return new AssetsAggregate($root, $properties);
6161
}
6262

63+
public function getName(): string
64+
{
65+
return $this->options['name'] ?? $this->originName;
66+
}
67+
68+
public function getPath(): string
69+
{
70+
$path = $this->parent->getPath();
71+
72+
if (!str_ends_with($path, '[]') && '' !== $path) {
73+
$path .= '.';
74+
}
75+
76+
$path .= $this->getName();
77+
78+
if ($this->type->isCollection()) {
79+
$path .= '[]';
80+
}
81+
82+
return $path;
83+
}
84+
6385
public function getReflection(): \ReflectionProperty
6486
{
6587
return new \ReflectionProperty($this->parent->name, $this->originName);
@@ -69,10 +91,12 @@ public function normalize(): array
6991
{
7092
return [
7193
'originName' => $this->originName,
94+
'path' => $this->getPath(),
7295
'visibility' => $this->visibility->value,
7396
'type' => $this->type->normalize(),
7497
'isStatic' => $this->isStatic,
7598
'isReadOnly' => $this->isReadOnly,
99+
'isCollection' => $this->type->isCollection(),
76100
'hasDefaultValue' => $this->hasDefaultValue,
77101
'defaultValue' => $this->defaultValue,
78102
'docBlock' => $this->docBlock,

src/Blueprint/Application/Model/Type.php

+5
Original file line numberDiff line numberDiff line change
@@ -72,6 +72,11 @@ public function getReflection(): ?\ReflectionType
7272
return $this->parent->getReflection()->getType();
7373
}
7474

75+
public function isCollection(): bool
76+
{
77+
return array_key_exists('int', $this->innerTypes);
78+
}
79+
7580
public function normalize(): array
7681
{
7782
return [

src/Mapper/Application/Attribute/TargetProperty.php

+14-6
Original file line numberDiff line numberDiff line change
@@ -5,16 +5,25 @@
55
namespace PBaszak\UltraMapper\Mapper\Application\Attribute;
66

77
use PBaszak\UltraMapper\Mapper\Application\Contract\AttributeInterface;
8+
use PBaszak\UltraMapper\Mapper\Application\Contract\TypeInterface;
89
use PBaszak\UltraMapper\Mapper\Application\Exception\ThrowAttributeValidationExceptionTrait;
910

1011
#[\Attribute(\Attribute::TARGET_PROPERTY)]
1112
class TargetProperty implements AttributeInterface
1213
{
1314
use ThrowAttributeValidationExceptionTrait;
1415

15-
public const MAPPING = 0; // 00
16-
public const DENORMALIZATION = 1; // 01
17-
public const NORMALIZATION = 2; // 10
16+
public const DENORMALIZATION = 1; // 0001
17+
public const NORMALIZATION = 2; // 0010
18+
public const MAPPING = 4; // 0100
19+
public const TRANSFORMATION = 8; // 1000
20+
21+
public const PROCESS_TYPE_MAP = [
22+
TypeInterface::DENORMALIZATION_PROCESS => self::DENORMALIZATION,
23+
TypeInterface::NORMALIZATION_PROCESS => self::NORMALIZATION,
24+
TypeInterface::MAPPING_PROCESS => self::MAPPING,
25+
TypeInterface::TRANSFORMATION_PROCESS => self::TRANSFORMATION,
26+
];
1827

1928
/**
2029
* @param string $name the name of the target property
@@ -26,15 +35,14 @@ public function __construct(
2635
public readonly string $name,
2736
public readonly int $useNameFor = self::MAPPING | self::DENORMALIZATION | self::NORMALIZATION,
2837
public readonly ?string $path = null,
29-
public bool $useForDenormalization = true,
30-
public bool $useForMapping = true,
31-
public bool $useForNormalization = true,
38+
public readonly int $usePathFor = self::DENORMALIZATION,
3239
public readonly array $options = []
3340
) {
3441
}
3542

3643
public function validate(\ReflectionProperty|\ReflectionClass $reflection): void
3744
{
45+
// there cannot be two target properties with the same processType
3846
// todo implement
3947
}
4048
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
namespace PBaszak\UltraMapper\Mapper\Domain\Exception;
6+
7+
class PropertyNotMatchedException extends \RuntimeException
8+
{
9+
public function __construct(
10+
string $originPropertyPath,
11+
string $message,
12+
string $advice,
13+
int $code = 0,
14+
) {
15+
parent::__construct(
16+
sprintf(
17+
'Property "%s" not matched. %s %s',
18+
$originPropertyPath,
19+
$message,
20+
$advice,
21+
),
22+
$code,
23+
);
24+
}
25+
}

src/Mapper/Domain/Service/Matcher.php

+19-20
Original file line numberDiff line numberDiff line change
@@ -5,15 +5,22 @@
55
namespace PBaszak\UltraMapper\Mapper\Domain\Service;
66

77
use PBaszak\UltraMapper\Blueprint\Application\Model\Assets\ClassBlueprint;
8-
use PBaszak\UltraMapper\Blueprint\Application\Model\Assets\ParameterBlueprint;
98
use PBaszak\UltraMapper\Blueprint\Application\Model\Assets\PropertyBlueprint;
109
use PBaszak\UltraMapper\Blueprint\Application\Model\Blueprint;
11-
use PBaszak\UltraMapper\Mapper\Application\Attribute\TargetProperty;
1210
use PBaszak\UltraMapper\Mapper\Domain\Contract\MatcherInterface;
11+
use PBaszak\UltraMapper\Mapper\Domain\Exception\PropertyNotMatchedException;
12+
use PBaszak\UltraMapper\Mapper\Domain\Service\Matcher\SameNameStrategy;
13+
use PBaszak\UltraMapper\Mapper\Domain\Service\Matcher\TargetPropertyAttributeStrategy;
1314
use Symfony\Component\Uid\Uuid;
1415

1516
class Matcher implements MatcherInterface
1617
{
18+
/** @var class-string<Matcher\MatchingStrategyInterface>[] */
19+
protected const MATCHING_STRATEGIES = [
20+
TargetPropertyAttributeStrategy::class,
21+
SameNameStrategy::class,
22+
];
23+
1724
public function matchBlueprints(string $processType, Blueprint $origin, Blueprint $source, Blueprint $target): void
1825
{
1926
$this->addLinks($origin, $source, $target);
@@ -36,28 +43,20 @@ protected function matchClassBlueprints(string $processType, ClassBlueprint $ori
3643

3744
protected function matchProperties(string $processType, PropertyBlueprint $originProperty, ClassBlueprint $source, ClassBlueprint $target): void
3845
{
39-
}
46+
foreach ($source->properties as $sourceProperty) {
47+
foreach ($target->properties as $targetProperty) {
48+
foreach ($this::MATCHING_STRATEGIES as $strategy) {
49+
$strategyInstance = new $strategy();
50+
if ($strategyInstance->confirmPropertiesMatching($processType, $originProperty, $sourceProperty, $targetProperty)) {
51+
$this->addLinks($originProperty, $sourceProperty, $targetProperty);
4052

41-
protected function searchForPropertyWithSameName(PropertyBlueprint $originProperty, ClassBlueprint $blueprint): ?PropertyBlueprint
42-
{
43-
/** @var PropertyBlueprint $property */
44-
foreach ($blueprint->properties->assets as $property) {
45-
if ($property->originName === $originProperty->originName) {
46-
return $property;
53+
return;
54+
}
55+
}
4756
}
4857
}
4958

50-
return null;
51-
}
52-
53-
protected function searchForPropertyBasedOnTargetPropertyAttribute(PropertyBlueprint $originProperty, ClassBlueprint $blueprint): ?PropertyBlueprint
54-
{
55-
return null;
56-
}
57-
58-
protected function hasTargetPropertyAttribute(PropertyBlueprint|ParameterBlueprint $blueprint): bool
59-
{
60-
return isset($blueprint->attributes->assets[TargetProperty::class]) && count($blueprint->attributes->assets[TargetProperty::class]) > 0;
59+
throw new PropertyNotMatchedException($originProperty->getPath(), sprintf('Property "%s" from origin class "%s" could not be matched with any property from source and target classes.', $originProperty->originName, $originProperty->parent->name), sprintf('Check Your classes: origin:"%s", source:"%s" and target:"%s" for properties with the same name or with the same attributes. Use #[TargetProperty] attribute to match properties if the names cannot be same.', $originProperty->parent->name, $source->name, $target->name));
6160
}
6261

6362
protected function addLinks(
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
namespace PBaszak\UltraMapper\Mapper\Domain\Service\Matcher;
6+
7+
use PBaszak\UltraMapper\Blueprint\Application\Model\Assets\PropertyBlueprint;
8+
9+
interface MatchingStrategyInterface
10+
{
11+
public function confirmPropertiesMatching(string $processType, PropertyBlueprint $origin, PropertyBlueprint $source, PropertyBlueprint $target): bool;
12+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
namespace PBaszak\UltraMapper\Mapper\Domain\Service\Matcher;
6+
7+
use PBaszak\UltraMapper\Blueprint\Application\Model\Assets\PropertyBlueprint;
8+
9+
class SameNameStrategy implements MatchingStrategyInterface
10+
{
11+
public function confirmPropertiesMatching(string $processType, PropertyBlueprint $origin, PropertyBlueprint $source, PropertyBlueprint $target): bool
12+
{
13+
return $origin->getName() === $source->getName() && $origin->getName() === $target->getName();
14+
}
15+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,91 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
namespace PBaszak\UltraMapper\Mapper\Domain\Service\Matcher;
6+
7+
use PBaszak\UltraMapper\Blueprint\Application\Model\Assets\ClassBlueprint;
8+
use PBaszak\UltraMapper\Blueprint\Application\Model\Assets\PropertyBlueprint;
9+
use PBaszak\UltraMapper\Mapper\Application\Attribute\TargetProperty;
10+
11+
class TargetPropertyAttributeStrategy implements MatchingStrategyInterface
12+
{
13+
public function confirmClassMatching(string $processType, ClassBlueprint $origin, ClassBlueprint $source, ClassBlueprint $target): bool
14+
{
15+
return false; // todo based on `path` argument
16+
}
17+
18+
public function confirmPropertiesMatching(string $processType, PropertyBlueprint $origin, PropertyBlueprint $source, PropertyBlueprint $target): bool
19+
{
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;
24+
25+
return true;
26+
}
27+
}
28+
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+
34+
return true;
35+
}
36+
}
37+
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+
43+
return true;
44+
}
45+
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
50+
51+
return true;
52+
}
53+
}
54+
55+
return false;
56+
}
57+
58+
protected function getPropertyTargetPropertyAttribute(PropertyBlueprint $blueprint, string $processType): ?TargetProperty
59+
{
60+
foreach ($blueprint->attributes[TargetProperty::class] as $attribute) {
61+
/** @var TargetProperty $instance */
62+
$instance = $attribute->newInstance();
63+
64+
$binaryProcessType = $instance::PROCESS_TYPE_MAP[$processType];
65+
if ($instance->useNameFor & $binaryProcessType === $binaryProcessType) {
66+
return $instance;
67+
}
68+
}
69+
70+
return null;
71+
}
72+
73+
protected function getClassTargetPropertyAttribute(ClassBlueprint $blueprint, string $processType): ?TargetProperty
74+
{
75+
if (!$blueprint->parent) {
76+
return null;
77+
}
78+
79+
foreach ($blueprint->parent->attributes[TargetProperty::class] as $attribute) {
80+
/** @var TargetProperty $instance */
81+
$instance = $attribute->newInstance();
82+
83+
$binaryProcessType = $instance::PROCESS_TYPE_MAP[$processType];
84+
if ($instance->usePathFor & $binaryProcessType === $binaryProcessType) {
85+
return $instance;
86+
}
87+
}
88+
89+
return null;
90+
}
91+
}

0 commit comments

Comments
 (0)