Logo
Loading...
Published on

Call graphs from your PHP code (with PHPStan)

Author

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:

What is this package?

call-graph is two things together:

  1. A PHPStan extension. When you run phpstan analyse with 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.
  2. 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 has callingClass, callingMethod, calledClass, calledMethod. If you already have scripts or tools that expect that shape, you can keep using them without switching to edges.

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 nodes
  • namespaceDepth=<n>: depth for namespace mode
  • minEdgeWeight=<n>: hide weak edges (useful for big apps)
  • maxNodes=<n>: cap size
  • includeFunctions=1: include function calls in the graph (by default visualization often focuses on methods)
  • strictNamespaces=1: when filtering by ns, keep only edges where both sides match
  • ns=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.