Skip to content

Commit a6a9b01

Browse files
committed
fix(reflect): improve circular reference detection in Reflector
1 parent 673ea42 commit a6a9b01

4 files changed

Lines changed: 74 additions & 2 deletions

File tree

src/Core/Reflect/Reflector.php

Lines changed: 20 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -92,6 +92,8 @@ protected function introspect(array $parameters, Schema $schema, ?Field $parent
9292
*/
9393
private function introspectSource(string $source, Schema $schema, Field $parent, array $path): void
9494
{
95+
$originalSource = $source;
96+
9597
$result = $this->introspector->analyze($source);
9698
$introspection = $result->introspectable();
9799
if ($introspection !== null) {
@@ -104,9 +106,25 @@ private function introspectSource(string $source, Schema $schema, Field $parent,
104106
return;
105107
}
106108

107-
$this->sources[] = $source;
109+
$this->pushSources($originalSource, $source);
108110
$this->introspect($nestedParameters, $schema, $parent, $path);
111+
$this->popSources($originalSource, $source);
112+
}
113+
114+
private function pushSources(string $originalSource, string $source): void
115+
{
116+
$this->sources[] = $originalSource;
117+
if ($originalSource !== $source) {
118+
$this->sources[] = $source;
119+
}
120+
}
121+
122+
private function popSources(string $originalSource, string $source): void
123+
{
109124
array_pop($this->sources);
125+
if ($originalSource !== $source) {
126+
array_pop($this->sources);
127+
}
110128
}
111129

112130
private function wouldCauseCircularReference(string $source): bool
@@ -125,7 +143,7 @@ private function extractParameters(string $source): array
125143
if (is_array($parameters)) {
126144
return $parameters;
127145
}
128-
$parameters = Target::createFrom((string)$source)
146+
$parameters = Target::createFrom((string) $source)
129147
->getReflectionParameters();
130148
return $this->cache->set($key, $parameters);
131149
}

tests/Core/Reflect/ReflectorTest.php

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@
1616
use Constructo\Test\Stub\Domain\Entity\Command\GameCommand;
1717
use Constructo\Test\Stub\Domain\Entity\Command\PersonCommand;
1818
use Constructo\Test\Stub\Domain\Entity\EmptyClass;
19+
use Constructo\Test\Stub\Reflector\Node;
1920
use Constructo\Test\Stub\Reflector\Sample;
2021
use Constructo\Testing\MakeExtension;
2122
use PHPUnit\Framework\TestCase;
@@ -312,4 +313,20 @@ public function __construct(
312313
$nestedKeys = array_filter(array_keys($rules), fn ($key) => str_starts_with((string) $key, 'empty_field.'));
313314
$this->assertEmpty($nestedKeys);
314315
}
316+
317+
public function testReflectorHandlesCircularReferenceViaCollection(): void
318+
{
319+
$schema = $this->reflector->reflect(Node::class);
320+
$rules = $schema->rules();
321+
322+
$json = '{
323+
"name": ["required", "string"],
324+
"children": ["sometimes", "nullable", "array"],
325+
"children.*.name": ["sometimes", "string"],
326+
"children.*.children": ["sometimes", "nullable", "array"]
327+
}';
328+
$expected = json_decode($json, true);
329+
330+
$this->assertEquals($expected, $rules);
331+
}
315332
}

tests/Stub/Reflector/Node.php

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
namespace Constructo\Test\Stub\Reflector;
6+
7+
class Node
8+
{
9+
public function __construct(
10+
public readonly string $name,
11+
public readonly ?NodeCollection $children = null,
12+
) {
13+
}
14+
}
Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
namespace Constructo\Test\Stub\Reflector;
6+
7+
use Constructo\Type\Collection;
8+
9+
/**
10+
* @extends Collection<Node>
11+
*/
12+
final class NodeCollection extends Collection
13+
{
14+
public function current(): Node
15+
{
16+
return $this->validate($this->datum());
17+
}
18+
19+
protected function validate(mixed $datum): Node
20+
{
21+
return ($datum instanceof Node) ? $datum : throw $this->exception(Node::class, $datum);
22+
}
23+
}

0 commit comments

Comments
 (0)