- Published on
Call graphs from your PHP code (with PHPStan)
- Author

- Illia Vasylevskyi
If you ever wanted a map of "who calls what" in a PHP project, this post is about call-graph (ineersa/call-graph). It is a small PHPStan extension plus CLI tools.
I took inspiration from:
- Using PHPStan to Extract Data About Your Codebase. The idea is: collectors gather facts during analysis, a rule wraps them, and a custom error formatter writes structured output. Smart hack, and it works well.
- stella-maris/callmap on GitLab is an earlier extension that outputs a JSON call map from PHPStan. call-graph builds on that idea and adds more detail and visualization.
What is this package?
call-graph is two things together:
- A PHPStan extension. When you run
phpstan analysewith its config, it walks your code like normal PHPStan, but it also records calls: instance methods ($x->foo()), static calls (Foo::bar()), and plain functions (baz()). It uses PHPStan types and reflection when it can, so it often knows the declaring class of a method, not only what the text looks like. - CLI tools. They read the JSON and produce either an interactive HTML graph (no Graphviz needed) or Graphviz DOT/SVG for docs and pipelines.
So you get one file, callgraph.json, that describes edges like "this class/method called that class/method", with extra fields such as file, line, call type, and whether something was unresolved.
Why care about coupling, risks, and hotspots (and the AI era)?
In a big PHP app, coupling is often invisible until it hurts you. A class looks small, but twenty other classes call into it. Two namespaces look separate, but the call graph shows a dense web between them. That is how you get change risk: you touch one method and you do not know how wide the blast radius is. Hotspots show up the same way: a few methods or modules sit in the middle of too many edges. Those are the places that deserve extra tests, careful refactors, or a plan before you "just fix a bug."
A real call graph helps you detect that structure: heavy dependencies, surprising directions of calls, and candidates for splitting or stabilizing. Namespace-level views and edge counts (like in the HTML and Graphviz modes) are a cheap way to scan for those patterns without reading every file.
Why not Tree-sitter (or other syntax-only parsers)?
Tree-sitter is great at syntax: it builds an AST fast and works for many languages. It does not understand PHP types, instanceof, generics, or "this variable is actually a UserRepository here." So for PHP, a syntax-only graph often shows wrong or vague edges: wrong class on a call, or everything looks like a generic something->method().
PHPStan already did the hard work to model your code. call-graph reuses that. You trade a slower, analysis-heavy step for a graph that is much more faithful for refactoring, onboarding, and for feeding structured facts into docs or AI workflows (for example: "list all callers of this service method" with fewer false positives).
It is not magic. If PHPStan cannot resolve something, the JSON can mark it as unresolved. Still, the baseline is closer to how the code is really wired.
What is inside callgraph.json?
The file has three blocks:
meta: when and how it was generated.edges: the full graph. Each row has caller/callee, kinds,callType,file,line,unresolved, and similar fields. This is what the HTML and Graphviz commands use.data: a shorter list in the callmap layout (same idea as stella-maris/callmap). Each item hascallingClass,callingMethod,calledClass,calledMethod. If you already have scripts or tools that expect that shape, you can keep using them without switching toedges.
Interface and options (the parts you actually touch)
Install and PHPStan
composer require --dev ineersa/call-graph
PHP 8.2+ and PHPStan 2.1+. If you use phpstan/extension-installer, the config is picked up automatically. Otherwise include vendor/ineersa/call-graph/callgraph.neon in your phpstan.neon.
Generate JSON
./vendor/bin/phpstan analyse -c vendor/ineersa/call-graph/callgraph.neon path/to/src
By default this writes callgraph.json in the current directory. You can override the path by redefining the errorFormatter.callgraph service and setting outputFile (for example build/callgraph.json).
Interactive HTML (good for clicking around)
./vendor/bin/callgraph-viz-html --input callgraph.json --html callgraph.html
The page supports URL parameters so you can bookmark or share a view:
mode=class|method|namespace: how to group or label nodesnamespaceDepth=<n>: depth for namespace modeminEdgeWeight=<n>: hide weak edges (useful for big apps)maxNodes=<n>: cap sizeincludeFunctions=1: include function calls in the graph (by default visualization often focuses on methods)strictNamespaces=1: when filtering byns, keep only edges where both sides matchns=App\\Service,App\\Domain: comma-separated namespace prefixes
Graphviz (DOT / SVG)
./vendor/bin/callgraph-viz --input callgraph.json --dot out.dot
./vendor/bin/callgraph-viz --input callgraph.json --dot out.dot --svg out.svg
Common CLI flags include --mode, --include / --exclude (regex), --max-nodes, --namespace-depth, --min-edge-weight, and --include-functions when you want functions in the diagram.
Closing thoughts
You do not need a call graph every day. But when you onboard someone, refactor a core service, or explain the system to a human or an AI helper, having a structured, analysis-backed map helps a lot. call-graph is a practical way to get that map from a stack many PHP teams already run: PHPStan.
