This package provides a lightweight RecursionContext class to safely traverse and process nested PHP arrays/objects without infinite loops from circular references. It’s designed for internal use — especially in testing or debugging tooling — where you need to inspect or serialize complex variable structures.
First use case: Imagine you’re writing a custom test assertion that needs to compare deeply nested object graphs (e.g., mocking framework output or DTOs with bidirectional relationships). Manual recursion risks hitting Maximum function nesting level or infinite loops.
Start by requiring it (usually as --dev):
composer require --dev sebastian/recursion-context
Then use it like this:
use SebastianBergmann\RecursionContext\Context;
$context = new Context();
$items = [
(object) ['name' => 'Alice', 'friend' => null],
(object) ['name' => 'Bob', 'friend' => null],
];
$items[0]->friend = $items[1];
$items[1]->friend = $items[0]; // circular reference!
$context->add($items[0]); // register to track recursion
// Safe traversal: detect cycles, skip already-seen objects
function inspect($var, Context $context): array {
$result = [];
if (is_object($var)) {
if ($context->contains($var)) {
$result[] = '[Recursion]';
return $result;
}
$context->add($var);
foreach ((array) $var as $prop => $value) {
$result[$prop] = inspect($value, $context);
}
} elseif (is_array($var)) {
foreach ($var as $key => $value) {
$result[$key] = inspect($value, $context);
}
} else {
$result = $var;
}
return $result;
}
print_r(inspect($items[0], $context));
✅ First tip: Read the README — it’s tiny, clear, and contains all API you’ll ever need.
Wrap var_export()-like output with cycle detection:
use SebastianBergmann\RecursionContext\Context;
function safeExport($var, Context $context = null): string {
$context ??= new Context();
switch (true) {
case is_object($var):
if ($context->contains($var)) {
return sprintf(' object(%s)@%s', get_class($var), '[recursion]');
}
$context->add($var);
$props = [];
foreach ((array) $var as $key => $value) {
$key = is_int($key) ? $key : "'{$key}'";
$props[] = "$key => " . safeExport($value, $context);
}
return 'object(' . get_class($var) . ') {' . implode(', ', $props) . '}';
case is_array($var):
return '[' . implode(', ', array_map(fn($v) => safeExport($v, $context), $var)) . ']';
default:
return var_export($var, true);
}
}
Use in logs or PHPUnit assertEquals() diffs where default exporters crash or hang.
Build reusable constraint helpers for complex nested comparisons (e.g., in integration tests):
use SebastianBergmann\RecursionContext\Context;
final class DeepEqualsConstraint extends \PHPUnit\Framework\Constraint\Constraint
{
private Context $context;
public function __construct()
{
$this->context = new Context();
}
public function evaluate($other, string $description = '', bool $returnResult = false): ?bool
{
$this->context->reset(); // 👈 crucial for reuse across tests
$result = $this->doCompare($this->value, $other, $this->context);
return $this->returnResult($result, $description, $returnResult);
}
private function doCompare($expected, $actual, Context $context): bool
{
if (is_object($expected) && is_object($actual)) {
if ($context->contains($expected) || $context->contains($actual)) {
return $context->contains($expected) === $context->contains($actual);
}
$context->add($expected);
$context->add($actual);
foreach ((array) $expected as $prop => $val) {
if ($val !== $actual->$prop ?? null) {
return false;
}
}
return true;
}
// ... handle arrays/scalars recursively
return $expected === $actual;
}
}
Used in tooling layers like:
assertModelSynced())⚠️ It is not meant for public app logic — it’s a low-level utility.
recursion-context directly in production code unless you control the PHP runtime version — check composer platform output.Context instance must be reset between independent operations ($context->reset()), otherwise stale references cause false cycles (e.g., comparing unrelated variables).spl_object_id() (since v6.0.1) — efficient and stable across requests.SplObjectStorage methods in ≥v5 (v6/v7/v8).If you’re seeing [Recursion] unexpectedly:
$context->contains() is called before $context->add() — order matters!xdebug_debug_zval() or var_dump($var, $context) to inspect internal state (temporarily).symfony/var-dumper for general dev UI.💡 Pro Tip: Wrap usage in a traits or helper like
HasRecursionContext()and share contexts per-request in CLI jobs or long-running processes.
How can I help you explore Laravel packages today?