Skip to content

Commit d4efd76

Browse files
committed
feat: Added LoopDetector
1 parent 56c257b commit d4efd76

File tree

7 files changed

+164
-5
lines changed

7 files changed

+164
-5
lines changed

src/Blueprint/Domain/Resolver/TypeResolver.php

+20
Original file line numberDiff line numberDiff line change
@@ -415,6 +415,10 @@ private function addType(string $type): void
415415
$this->addType('null');
416416
}
417417

418+
if ('self' === $type || 'static' === $type) {
419+
$type = $this->changeSelfStaticTypeToClass($type);
420+
}
421+
418422
if (!in_array($type, $this->types)) {
419423
$this->types[] = $type;
420424
}
@@ -423,6 +427,9 @@ private function addType(string $type): void
423427
private function addInnerType(string $type, string $keyType): void
424428
{
425429
$key = explode('|', $keyType);
430+
if ('self' === $type || 'static' === $type) {
431+
$type = $this->changeSelfStaticTypeToClass($type);
432+
}
426433

427434
foreach ($key as $keyType) {
428435
if (!isset($this->innerTypes[$keyType])) {
@@ -443,4 +450,17 @@ private function addInnerType(string $type, string $keyType): void
443450
}
444451
}
445452
}
453+
454+
private function changeSelfStaticTypeToClass(string $type): string
455+
{
456+
$classReflection = $this->reflection->getDeclaringClass();
457+
458+
if (null === $classReflection) {
459+
throw new \LogicException('It never should happen.');
460+
}
461+
462+
$type = $classReflection->getName();
463+
464+
return $type;
465+
}
446466
}

src/Mapper/Application/Attribute/Ignore.php

+14-3
Original file line numberDiff line numberDiff line change
@@ -6,19 +6,30 @@
66

77
use PBaszak\UltraMapper\Mapper\Application\Contract\AttributeInterface;
88
use PBaszak\UltraMapper\Mapper\Application\Exception\ThrowAttributeValidationExceptionTrait;
9+
use PBaszak\UltraMapper\Mapper\Domain\Model\Process;
910

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

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+
Process::DENORMALIZATION_PROCESS => self::DENORMALIZATION,
23+
Process::NORMALIZATION_PROCESS => self::NORMALIZATION,
24+
Process::MAPPING_PROCESS => self::MAPPING,
25+
Process::TRANSFORMATION_PROCESS => self::TRANSFORMATION,
26+
];
27+
1528
/**
1629
* @param array<string, mixed> $options Options are for modificators of the mapping process. If You need them, You can use them.
1730
*/
1831
public function __construct(
19-
public bool $useForDenormalization = false,
20-
public bool $useForMapping = false,
21-
public bool $useForNormalization = true,
32+
public int $processType = self::DENORMALIZATION | self::NORMALIZATION | self::TRANSFORMATION | self::MAPPING,
2233
public readonly array $options = []
2334
) {
2435
}

src/Mapper/Application/Attribute/MaxDepth.php

+14
Original file line numberDiff line numberDiff line change
@@ -6,12 +6,25 @@
66

77
use PBaszak\UltraMapper\Mapper\Application\Contract\AttributeInterface;
88
use PBaszak\UltraMapper\Mapper\Application\Exception\ThrowAttributeValidationExceptionTrait;
9+
use PBaszak\UltraMapper\Mapper\Domain\Model\Process;
910

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

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+
Process::DENORMALIZATION_PROCESS => self::DENORMALIZATION,
23+
Process::NORMALIZATION_PROCESS => self::NORMALIZATION,
24+
Process::MAPPING_PROCESS => self::MAPPING,
25+
Process::TRANSFORMATION_PROCESS => self::TRANSFORMATION,
26+
];
27+
1528
/**
1629
* @param int $maxDepth the maximum depth of the object graph that should be mapped
1730
* @param mixed $fillWith the value that should be used to fill the property if the depth is exceeded
@@ -20,6 +33,7 @@ class MaxDepth implements AttributeInterface
2033
public function __construct(
2134
public readonly int $maxDepth,
2235
public mixed $fillWith = null,
36+
public int $processType = self::DENORMALIZATION | self::NORMALIZATION | self::TRANSFORMATION | self::MAPPING,
2337
public readonly array $options = []
2438
) {
2539
}
+79
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,79 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
namespace PBaszak\UltraMapper\Mapper\Domain\Service;
6+
7+
use PBaszak\UltraMapper\Blueprint\Application\Model\Assets\ClassBlueprint;
8+
use PBaszak\UltraMapper\Blueprint\Application\Model\Assets\PropertyBlueprint;
9+
use PBaszak\UltraMapper\Blueprint\Application\Model\Blueprint;
10+
use PBaszak\UltraMapper\Mapper\Application\Attribute\Ignore;
11+
use PBaszak\UltraMapper\Mapper\Application\Attribute\MaxDepth;
12+
use PBaszak\UltraMapper\Mapper\Domain\Model\Process;
13+
14+
class LoopDetector
15+
{
16+
protected Blueprint $root;
17+
18+
/**
19+
* @param Blueprint $blueprint to check
20+
*
21+
* @throws \RuntimeException if loop is detected
22+
*/
23+
public function checkBlueprint(Blueprint $blueprint, Process $process): void
24+
{
25+
$this->root = $blueprint;
26+
foreach ($blueprint->blueprints as $index => $classBlueprint) {
27+
foreach ($process->processes as $processType) {
28+
$this->checkClassBlueprint($classBlueprint, $classBlueprint->name, $processType);
29+
}
30+
}
31+
}
32+
33+
protected function checkClassBlueprint(ClassBlueprint $blueprint, string $actualCheckedClass, string $processType): void
34+
{
35+
/** @var PropertyBlueprint $property */
36+
foreach ($blueprint->properties as $property) {
37+
$types = $property->type->getAllClassTypes();
38+
if (empty($types)) {
39+
continue;
40+
}
41+
42+
foreach ($types as $classType) {
43+
if ($actualCheckedClass == $classType) {
44+
if (
45+
$this->hasAttribute($property, Ignore::class, $processType)
46+
|| $this->hasAttribute($property, MaxDepth::class, $processType)
47+
) {
48+
continue;
49+
}
50+
51+
throw new \RuntimeException('Loop detected');
52+
}
53+
54+
foreach ($this->root->blueprints as $classBlueprint) {
55+
if ($classType == $classBlueprint->name) {
56+
$this->checkClassBlueprint($classBlueprint, $actualCheckedClass, $processType);
57+
}
58+
}
59+
}
60+
}
61+
}
62+
63+
protected function hasAttribute(PropertyBlueprint $property, string $attr, string $processType): bool
64+
{
65+
return !empty($property->attributes[$attr]);
66+
67+
foreach ($property->attributes[$attr] ?? [] as $attribute) {
68+
/** @var MaxDepth|Ignore $instance */
69+
$instance = $attribute->newInstance();
70+
71+
$binaryProcessType = $instance::PROCESS_TYPE_MAP[$processType];
72+
if (($instance->processType & $binaryProcessType) === $binaryProcessType) {
73+
return $instance;
74+
}
75+
}
76+
77+
return null;
78+
}
79+
}

tests/Assets/pbaszak_ultramapper_tests_assets_dummysimplewithattribute.yaml

+2-2
Original file line numberDiff line numberDiff line change
@@ -107,11 +107,11 @@ blueprints:
107107
filesHashes:
108108
/app/tests/Assets/DummySimpleWithAttribute.php: 5de157db33ad5536dfb49fe841a058a3
109109
/app/src/Mapper/Application/Attribute/Callback.php: 99643441133906d4dcd15b67a24e47f7
110-
/app/src/Mapper/Application/Attribute/Ignore.php: 880e009e690c1e203898cd1a0ac5c026
110+
/app/src/Mapper/Application/Attribute/Ignore.php: e38c1b054ff976591a460c3084c22209
111111
events:
112112
- 'Blueprint created. Root class: PBaszak\UltraMapper\Tests\Assets\DummySimpleWithAttribute.'
113113
- 'Class Blueprint PBaszak\UltraMapper\Tests\Assets\DummySimpleWithAttribute added. Blueprint name: pbaszak_ultramapper_tests_assets_dummysimplewithattribute.'
114114
- 'File hash added. File: /app/tests/Assets/DummySimpleWithAttribute.php, hash: 5de157db33ad5536dfb49fe841a058a3.'
115115
- 'File hash added. File: /app/src/Mapper/Application/Attribute/Callback.php, hash: 99643441133906d4dcd15b67a24e47f7.'
116116
- 'File hash already exists. File: /app/src/Mapper/Application/Attribute/Callback.php, hash: 99643441133906d4dcd15b67a24e47f7.'
117-
- 'File hash added. File: /app/src/Mapper/Application/Attribute/Ignore.php, hash: 880e009e690c1e203898cd1a0ac5c026.'
117+
- 'File hash added. File: /app/src/Mapper/Application/Attribute/Ignore.php, hash: e38c1b054ff976591a460c3084c22209.'
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,30 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
namespace PBaszak\UltraMapper\Tests\Mapper\Unit\Domain\Service;
6+
7+
use PBaszak\UltraMapper\Blueprint\Application\Model\Blueprint;
8+
use PBaszak\UltraMapper\Mapper\Domain\Model\Process;
9+
use PBaszak\UltraMapper\Mapper\Domain\Service\LoopDetector;
10+
use PHPUnit\Framework\Attributes\Group;
11+
use PHPUnit\Framework\Attributes\Test;
12+
use PHPUnit\Framework\TestCase;
13+
14+
#[Group('unit')]
15+
class LoopDetectorTest extends TestCase
16+
{
17+
#[Test]
18+
public function shouldThrowExceptionOnLoopDetected(): void
19+
{
20+
$class = new class() {
21+
public self $property;
22+
};
23+
24+
$this->expectException(\RuntimeException::class);
25+
(new LoopDetector())->checkBlueprint(
26+
Blueprint::create(get_class($class)),
27+
new Process([Process::DENORMALIZATION_PROCESS])
28+
);
29+
}
30+
}

tools/phpstan/fpm-baseline.neon

+5
Original file line numberDiff line numberDiff line change
@@ -135,6 +135,11 @@ parameters:
135135
count: 1
136136
path: ../../src/Mapper/Domain/Resolver/MapperResolver.php
137137

138+
-
139+
message: "#^Unreachable statement \\- code above always terminates\\.$#"
140+
count: 1
141+
path: ../../src/Mapper/Domain/Service/LoopDetector.php
142+
138143
-
139144
message: "#^Cannot access property \\$name on PBaszak\\\\UltraMapper\\\\Mapper\\\\Application\\\\Attribute\\\\TargetProperty\\|null\\.$#"
140145
count: 4

0 commit comments

Comments
 (0)