diff --git a/src/Analyze/EntityAnalyzer.php b/src/Analyze/EntityAnalyzer.php index c902758..19522b4 100644 --- a/src/Analyze/EntityAnalyzer.php +++ b/src/Analyze/EntityAnalyzer.php @@ -37,6 +37,8 @@ readonly class EntityAnalyzer */ public function analyze(): EntityResult { + (new RecursionCheck($this->entity))->check(); + $refClass = new ReflectionClass($this->entity); $fields = $this->fields($refClass); diff --git a/src/Analyze/RecursionCheck.php b/src/Analyze/RecursionCheck.php new file mode 100644 index 0000000..0ca91d6 --- /dev/null +++ b/src/Analyze/RecursionCheck.php @@ -0,0 +1,74 @@ + $fqcn + */ + public function __construct( + private string $fqcn + ) {} + + public function check(): void + { + /** @var list> $seenClasses */ + $seenClasses = [$this->fqcn]; + $toProcess = [...$this->declaredListTypes($this->fqcn)]; + + while (!empty($toProcess)) { + $current = array_pop($toProcess); + + try { + // is there a better workaround? + new ReflectionClass($current); + + if (in_array($current, $seenClasses)) { + throw new InvalidEntity( + "Entity $this->fqcn declares a recursive mapping. This is not supported." + ); + } + + $seenClasses[] = $current; + } catch (ReflectionException) { + // $current does not reference a class, skip + } + } + } + + /** + * @param class-string $fqcn + * @return list> + */ + private function declaredListTypes(string $fqcn): array + { + $params = (new ReflectionClass($fqcn))->getConstructor()?->getParameters(); + + if ($params === null) { + return []; + } + + $result = []; + + foreach ($params as $param) { + $listAttr = $param->getAttributes(ListOf::class); + if (empty($listAttr)) { + continue; + } + + /** @var ListOf $inst */ + $inst = $listAttr[0]->newInstance(); + $result[] = $inst->entitiyFqcn; + } + + return $result; + } +} diff --git a/tests/Analyze/EntityAnalyzerTest.php b/tests/Analyze/EntityAnalyzerTest.php index 0a2fa38..ce5f684 100644 --- a/tests/Analyze/EntityAnalyzerTest.php +++ b/tests/Analyze/EntityAnalyzerTest.php @@ -168,4 +168,17 @@ class EntityAnalyzerTest extends TestCase $result->fields[1] ); } + + public function testInfiniteRecursionIsDetected(): void + { + // Given + $entity = EntityWithInfiniteRecursion::class; + + // Then + $this->expectException(InvalidEntity::class); + $this->expectExceptionMessageMatches('/.*recursive.*/'); + + // When + (new EntityAnalyzer($entity))->analyze(); + } } diff --git a/tests/Analyze/EntityWithInfiniteRecursion.php b/tests/Analyze/EntityWithInfiniteRecursion.php new file mode 100644 index 0000000..bcf60b1 --- /dev/null +++ b/tests/Analyze/EntityWithInfiniteRecursion.php @@ -0,0 +1,18 @@ +