Weave Code
Code Weaver
Helps Laravel developers discover, compare, and choose open-source packages. See popularity, security, maintainers, and scores at a glance to make better decisions.
Feedback
Share your thoughts, report bugs, or suggest improvements.
Subject
Message

Recursion Context Laravel Package

sebastian/recursion-context

View on GitHub
Deep Wiki
Context7

Getting Started

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.


Implementation Patterns

1. Safe Variable Export for Debugging / Logging

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.

2. Custom Test Constraints

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;
    }
}

3. Integration with frameworks

Used in tooling layers like:

  • Laravel's test helpers (e.g., custom assertModelSynced())
  • DTO validation frameworks needing deep comparison
  • Debug bar panels or stacktraces that serialize request/state objects

⚠️ It is not meant for public app logic — it’s a low-level utility.


Gotchas and Tips

🚨 PHP Version Compliance is Strict

  • v8.0.0+ requires PHP 8.4+ (per README and release notes).
  • Older versions (7.x, 6.x) are deprecated and lack modern PHP support.
  • ❗ Never use recursion-context directly in production code unless you control the PHP runtime version — check composer platform output.

🔄 Context Reuse & Reset

  • The Context instance must be reset between independent operations ($context->reset()), otherwise stale references cause false cycles (e.g., comparing unrelated variables).
  • Misusing it across multiple test assertions without reset is the #1 cause of "mysterious" recursion detection.

⚙️ Internal Implementation Details (for debugging)

  • Internally uses spl_object_id() (since v6.0.1) — efficient and stable across requests.
  • Avoids deprecated SplObjectStorage methods in ≥v5 (v6/v7/v8).
  • Only tracks objects, not resources (arrays/scalars handled via native recursion).

🔍 Debugging Recursion Issues

If you’re seeing [Recursion] unexpectedly:

  1. Check if $context->contains() is called before $context->add() — order matters!
  2. Ensure you’re not reusing the same context inside a recursive traversal across multiple root variables.
  3. Use xdebug_debug_zval() or var_dump($var, $context) to inspect internal state (temporarily).

📦 Composer Strategy

  • As a dev dependency, it’s ideal for test suites, dump macros, or CLI tools.
  • As a runtime dependency, only use if you explicitly need deep variable processing in production (rare — e.g., debugging middleware). Prefer alternatives like 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.

Weaver

How can I help you explore Laravel packages today?

Conversation history is not saved when not logged in.
Prompt
Add packages to context
No packages found.
davejamesmiller/laravel-breadcrumbs
artisanry/parsedown
christhompsontldr/phpsdk
enqueue/dsn
bunny/bunny
enqueue/test
enqueue/null
enqueue/amqp-tools
bower-asset/punycode
bower-asset/inputmask
bower-asset/jquery
bower-asset/yii2-pjax
laravel/nova
spatie/laravel-mailcoach
spatie/laravel-superseeder
laravel/liferaft
nst/json-test-suite
danielmiessler/sec-lists
jackalope/jackalope-transport
twbs/bootstrap4