innmind/object-graph
A lightweight PHP library for modeling objects and their relationships as a graph. Build nodes and edges, traverse connections, and inspect object graphs through a small API suited to domain models, dependency analysis, and debugging tools.
Install it with Composer:
composer require innmind/object-graph
The package is meant to be used as a debugging and introspection tool rather than as application infrastructure. The whole entry point is a single call that returns a graph structure representing the root object and everything reachable from it:
use Innmind\ObjectGraph\Graph;
$graph = Graph::of($order); // an Order aggregate, a Doctrine entity, etc.
Look first at:
Graph class in src/ — this is the only thing you'll instantiate directly.Node / Edge types — these are what you'll interact with when walking the graph.Innmind\ObjectGraph\Visitor or the concrete visitors shipped in src/Visitor/) — this is where 90% of your integration work happens.A good first use case is a one-off Tinker/php artisan tinker session:
php artisan tinker
>>> $graph = Innmind\ObjectGraph\Graph::of($user);
>>> $graph->nodes()->size(); // how deep does my object tree actually go?
That alone catches surprising things like an Eloquent model that transitively holds half your container because of a service resolved at boot.
Walking the graph with a visitor. The idiomatic way to inspect the graph is to accept a visitor rather than to traverse manually. A visitor lets you accumulate a set of classes, detect cycles, or project into another graph library:
use Innmind\ObjectGraph\Visitor;
$classes = new class implements Visitor {
public array $seen = [];
public function __invoke(Node $node): void
{
$this->seen[$node->class()->toString()] = true;
}
};
$graph->accept($classes);
Once you have a visitor like the above, "which classes are reachable from my root object?" becomes a one-liner — extremely useful for catching accidental coupling in a Laravel container.
Rendering to Graphviz. Innmind ships innmind/graphviz, and object-graph composes with it. The pattern is: extract the graph, then map nodes to Graphviz nodes and render to SVG. Wire this behind an artisan command so you can regenerate the diagram on demand:
// app/Console/Commands/ObjectGraph.php
public function handle()
{
$graph = Graph::of(app($this->argument('class')));
// map Node -> Innmind\Graphviz\Node, then render
}
Cycle detection. Because the graph explicitly records edges, you can implement "find all back-references" as a visitor that keeps a visited-set keyed by spl_object_id. This is one of the main reasons to reach for this package instead of var_dump: var_dump truncates, this doesn't.
Integration with tests. Snapshot a normalized graph (class names + edge labels) and assert against it. This catches regressions where a service that used to depend on Mailer now depends on Notification transitively. Keep the assertions at the class-granularity level, not instance granularity, since object IDs change between runs.
Lazy boundary marking. When traversing a large Laravel app graph, you often want to stop descending at framework classes (e.g. Illuminate\*). Implement a visitor that raises a sentinel when it sees a namespace prefix, and skip children. This is the pattern to reach for before you start getting OOMs on big controllers.
Debugging HTTP requests. Bind a small middleware in a non-production environment that, when the ?__graph=<class> query param is present, extracts and dumps the graph as JSON or Mermaid. Mermaid renders inline in most Markdown viewers and GitHub PRs, which is handy for documentation.
Documentation generation. Combine the visitor pattern with reflection over your App\* classes to produce "what does this aggregate actually reference?" docs. Commit the generated .mmd/.svg files so PRs that change the shape of the graph are reviewable.
Order::customer(), Customer::orders()) will blow up naive recursion. The graph extraction handles this via instance identity internally, but any custom traversal you write on top must track spl_object_id yourself.$this, which can pull in the entire service container. Expect this to be the #1 source of "why is my graph so big?" surprises. Consider a visitor that prunes \Closure and \PDO instances.app() returns the container; the container references everything. Always extract from a freshly constructed object, not from something that has been through the container, when you care about the real dependency shape.$node->addEdge(...); look for ->withEdge(...) or equivalent.@psalm-immutable annotations. If you're on PHPStan at max level, you'll want the corresponding extension or to downgrade the graph code to level 6 for the introspection layer.^X.Y) in your app and treat upgrades as deliberate.Storage::put(...) or write to storage/app/debug/ and point a dev route at it.src/Visitor/ directory — those are your documentation.innmind/graphviz for human consumption and with plain PHP arrays for machine consumption. Don't try to make one output serve both; the shapes want different things.How can I help you explore Laravel packages today?