Skip to content

Commit

Permalink
282 discover all nested objects (#287)
Browse files Browse the repository at this point in the history
* add tests

* update dumper

* fix styles

* improve + tests

* fix phpdoc

* fix

* fix

---------

Co-authored-by: Sergei Predvoditelev <[email protected]>
  • Loading branch information
olegbaturin and vjik authored Nov 23, 2024
1 parent d46d4a8 commit b532c44
Show file tree
Hide file tree
Showing 2 changed files with 280 additions and 23 deletions.
48 changes: 27 additions & 21 deletions src/Dumper.php
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,10 @@
use ReflectionException;
use Yiisoft\VarDumper\ClosureExporter;

use function array_key_exists;
use function is_array;
use function is_object;

final class Dumper
{
private array $objects = [];
Expand Down Expand Up @@ -47,42 +51,44 @@ public function asJson(int $depth = 50, bool $format = false): string

/**
* Export variable as JSON summary of topmost items.
* Dumper goes into the variable full depth to search all objects.
*
* @param int $depth Maximum depth that the dumper should go into the variable.
* @param int $depth Maximum depth that the dumper should print out arrays.
* @param bool $prettyPrint Whatever to format exported code.
*
* @return string JSON string containing summary.
*/
public function asJsonObjectsMap(int $depth = 50, bool $prettyPrint = false): string
{
$this->buildObjectsCache($this->variable, $depth);
return $this->asJsonInternal($this->objects, $prettyPrint, $depth, 1, true);
$this->buildObjectsCache($this->variable);
return $this->asJsonInternal($this->objects, $prettyPrint, $depth + 2, 1, true);
}

private function buildObjectsCache(mixed $variable, int $depth, int $level = 0): void
private function buildObjectsCache(mixed $variable, ?int $depth = null, int $level = 0): void
{
if ($depth <= $level) {
return;
}
if (is_object($variable)) {
if (array_key_exists($variable::class, $this->excludedClasses) ||
array_key_exists($objectDescription = $this->getObjectDescription($variable), $this->objects)
) {
return;
}
$this->objects[$objectDescription] = $variable;
$variable = $this->getObjectProperties($variable);
}

$nextLevel = $level + 1;
if ($depth !== null && $depth <= $nextLevel) {
return;
}

if (is_object($variable)) {
$variable = $this->getObjectProperties($variable);
foreach ($variable as $value) {
$this->buildObjectsCache($value, $depth, 0);
$this->buildObjectsCache($value, $depth, $nextLevel);
}
return;
}

if (is_array($variable)) {
$nextLevel = $level + 1;
if ($depth <= $nextLevel) {
return;
}
foreach ($variable as $value) {
$this->buildObjectsCache($value, $depth, $nextLevel);
}
Expand Down Expand Up @@ -149,10 +155,6 @@ private function dumpNestedInternal(
break;
case 'object':
$objectDescription = $this->getObjectDescription($variable);
if ($depth <= $level || array_key_exists($variable::class, $this->excludedClasses)) {
$output = $objectDescription . ' (...)';
break;
}

if ($variable instanceof Closure) {
$output = $inlineObject
Expand All @@ -161,13 +163,17 @@ private function dumpNestedInternal(
break;
}

if (!array_key_exists($objectDescription, $this->objects)) {
if ($objectCollapseLevel < $level && array_key_exists($objectDescription, $this->objects)) {
$output = 'object@' . $objectDescription;
$this->objects[$objectDescription] = $variable;
break;
}
if ($objectCollapseLevel < $level) {
$output = 'object@' . $objectDescription;

if (
$depth <= $level
|| array_key_exists($variable::class, $this->excludedClasses)
|| !array_key_exists($objectDescription, $this->objects)
) {
$output = $objectDescription . ' (...)';
break;
}

Expand Down
255 changes: 253 additions & 2 deletions tests/Unit/DumperTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,256 @@

final class DumperTest extends TestCase
{
public function testAsJsonObjectsMapLevelOne(): void
{
$object = new stdClass();
$object->var = 'test';
$objectId = spl_object_id($object);

$this->assertSame(
<<<JSON
{
"stdClass#$objectId": {
"public \$var": "test"
}
}
JSON,
Dumper::create($object)->asJsonObjectsMap(1, true)
);
}

public function testAsJsonObjectsMapNestedObject(): void
{
$nested2 = new stdClass();
$nested2->name = 'nested2';
$nested2Id = spl_object_id($nested2);

$nested1 = new stdClass();
$nested1->name = 'nested1';
$nested1->var = $nested2;
$nested1Id = spl_object_id($nested1);

$object = new stdClass();
$object->name = 'root';
$object->var = $nested1;
$objectId = spl_object_id($object);

$this->assertSame(
<<<JSON
{
"stdClass#$objectId": {
"public \$name": "root",
"public \$var": "object@stdClass#$nested1Id"
},
"stdClass#$nested1Id": {
"public \$name": "nested1",
"public \$var": "object@stdClass#$nested2Id"
},
"stdClass#$nested2Id": {
"public \$name": "nested2"
}
}
JSON,
Dumper::create($object)->asJsonObjectsMap(1, true)
);
}

public function testAsJsonObjectsMapArrayWithObject(): void
{
$nested2 = new stdClass();
$nested2->name = 'nested2';
$nested2Id = spl_object_id($nested2);

$nested1 = new stdClass();
$nested1->name = 'nested1';
$nested1->var = [$nested2];
$nested1Id = spl_object_id($nested1);

$object = new stdClass();
$object->name = 'root';
$object->var = $nested1;
$objectId = spl_object_id($object);

$this->assertSame(
<<<JSON
{
"stdClass#$objectId": {
"public \$name": "root",
"public \$var": "object@stdClass#$nested1Id"
},
"stdClass#$nested1Id": {
"public \$name": "nested1",
"public \$var": "array (1 item) [...]"
},
"stdClass#$nested2Id": {
"public \$name": "nested2"
}
}
JSON,
Dumper::create($object)->asJsonObjectsMap(0, true)
);
}

/**
* @dataProvider loopAsJsonObjectMapDataProvider
*/
public function testLoopAsJsonObjectsMap(mixed $var, int $depth, $expectedResult): void
{
$exportResult = Dumper::create($var)->asJsonObjectsMap($depth, true);
$this->assertEquals($expectedResult, $exportResult);
}

public static function loopAsJsonObjectMapDataProvider(): iterable
{
// parent->child->parent structure
$nested1 = new stdClass();
$nested1->id = 'nested1';
$nested2 = new stdClass();
$nested2->id = 'nested2';
$nested2->nested1 = $nested1;
$nested1->nested2 = $nested2;

$nested1Id = spl_object_id($nested1);
$nested2Id = spl_object_id($nested2);

// 5 is a min level to reproduce buggy dumping of parent->child->parent structure
[$object1, $ids1] = self::getNested(5, $nested1);
yield 'nested loop - object' => [
$object1,
5,
<<<S
{
"stdClass#$ids1[0]": {
"public \$id": "lvl0",
"public \$lvl1": "object@stdClass#$ids1[1]"
},
"stdClass#$ids1[1]": {
"public \$id": "lvl1",
"public \$lvl2": "object@stdClass#$ids1[2]"
},
"stdClass#$ids1[2]": {
"public \$id": "lvl2",
"public \$lvl3": "object@stdClass#$ids1[3]"
},
"stdClass#$ids1[3]": {
"public \$id": "lvl3",
"public \$lvl4": "object@stdClass#$ids1[4]"
},
"stdClass#$ids1[4]": {
"public \$id": "lvl4",
"public \$lvl5": "object@stdClass#$nested1Id"
},
"stdClass#$nested1Id": {
"public \$id": "nested1",
"public \$nested2": "object@stdClass#$nested2Id"
},
"stdClass#$nested2Id": {
"public \$id": "nested2",
"public \$nested1": "object@stdClass#$nested1Id"
}
}
S,
];

// array loop must be 1 level deeper to parse loop objects
[$object2, $ids2] = self::getNested(6, [$nested1, $nested2]);
yield 'nested loop - array' => [
$object2,
6,
<<<S
{
"stdClass#$ids2[0]": {
"public \$id": "lvl0",
"public \$lvl1": "object@stdClass#$ids2[1]"
},
"stdClass#$ids2[1]": {
"public \$id": "lvl1",
"public \$lvl2": "object@stdClass#$ids2[2]"
},
"stdClass#$ids2[2]": {
"public \$id": "lvl2",
"public \$lvl3": "object@stdClass#$ids2[3]"
},
"stdClass#$ids2[3]": {
"public \$id": "lvl3",
"public \$lvl4": "object@stdClass#$ids2[4]"
},
"stdClass#$ids2[4]": {
"public \$id": "lvl4",
"public \$lvl5": "object@stdClass#$ids2[5]"
},
"stdClass#$ids2[5]": {
"public \$id": "lvl5",
"public \$lvl6": [
"object@stdClass#$nested1Id",
"object@stdClass#$nested2Id"
]
},
"stdClass#$nested1Id": {
"public \$id": "nested1",
"public \$nested2": "object@stdClass#$nested2Id"
},
"stdClass#$nested2Id": {
"public \$id": "nested2",
"public \$nested1": "object@stdClass#$nested1Id"
}
}
S,
];

// nested loop to inner array
$object3 = new stdClass();
$object3->id = 'lvl0';
$object3->lv11 = [
'id' => 'lvl1',
'loop' => $nested1,
];
$object3Id = spl_object_id($object3);

yield 'nested loop to object->array' => [
$object3,
3,
<<<S
{
"stdClass#$object3Id": {
"public \$id": "lvl0",
"public \$lv11": {
"id": "lvl1",
"loop": "object@stdClass#$nested1Id"
}
},
"stdClass#$nested1Id": {
"public \$id": "nested1",
"public \$nested2": "object@stdClass#$nested2Id"
},
"stdClass#$nested2Id": {
"public \$id": "nested2",
"public \$nested1": "object@stdClass#$nested1Id"
}
}
S,
];
}

private static function getNested(int $depth, mixed $data): array
{
$objectIds = [];
$head = $lvl = new stdClass();
$objectIds[] = spl_object_id($head);
$lvl->id = 'lvl0';

for ($i = 1; $i < $depth; $i++) {
$nested = new stdClass();
$nested->id = 'lvl' . $i;
$lvl->{'lvl' . $i} = $nested;
$lvl = $nested;
$objectIds[] = spl_object_id($nested);
}
$lvl->{'lvl' . $i} = $data;

return [$head, $objectIds];
}

public function testObjectExpanding(): void
{
$var = $this->createNested(10, [[[[[[[[['key' => 'end']]]]]]]]]);
Expand Down Expand Up @@ -93,7 +343,7 @@ public function testObjectExpanding(): void
}
JSON;

$actualResult = Dumper::create($var)->asJsonObjectsMap(4, true);
$actualResult = Dumper::create($var)->asJsonObjectsMap(2, true);

$this->assertEquals($expectedResult, $actualResult);
}
Expand Down Expand Up @@ -178,6 +428,7 @@ public function testCacheDoesNotCoversObjectOutOfDumpDepth(): void
$object1 = new stdClass();
$object1Id = spl_object_id($object1);
$object2 = new stdClass();
$object2Id = spl_object_id($object2);

$variable = [$object1, [[$object2]]];
$expectedResult = sprintf('["object@stdClass#%d",["array (1 item) [...]"]]', $object1Id);
Expand All @@ -189,7 +440,7 @@ public function testCacheDoesNotCoversObjectOutOfDumpDepth(): void
$map = $dumper->asJsonObjectsMap(2);
$this->assertEqualsWithoutLE(
<<<S
{"stdClass#{$object1Id}":"{stateless object}"}
{"stdClass#$object1Id":"{stateless object}","stdClass#$object2Id":"{stateless object}"}
S,
$map,
);
Expand Down

0 comments on commit b532c44

Please sign in to comment.