ticketswap/phpstan-error-formatter
A minimalistic PHPStan error formatter that makes each error a clickable file and line link, avoids wrapping so errors use your full terminal width, highlights variables and class names, and visually truncates long file paths.
1. Install (dev-only):
composer require --dev ticketswap/phpstan-error-formatter
If you already use phpstan/extension-installer (very common in Laravel projects), the extension.neon is registered automatically. If not, add the include manually — forgetting this is the #1 "formatter not found" cause:
includes:
- vendor/ticketswap/phpstan-error-formatter/extension.neon
2. Look at your phpstan.neon.dist first. That's the only place you need to touch. In a typical Laravel project:
includes:
- vendor/larastan/larastan/extension.neon
parameters:
level: 6
paths:
- app
- config
- routes
- database
errorFormat: ticketswap
editorUrl: 'phpstorm://open?file=%%file%%&line=%%line%%'
Note the formatter name is ticketswap — not ticketswap-error-formatter, not minimal. That's the string you pass to errorFormat.
3. Set editorUrl for your editor (otherwise the clickable links fall back to file:///… paths):
# PhpStorm (global handler registered by the IDE/CLI launcher)
editorUrl: 'phpstorm://open?file=%%file%%&line=%%line%%'
# VS Code
editorUrl: 'vscode://file/%%file%%:%%line%%'
4. Run it:
vendor/bin/phpstan analyse
First use case to try: take your noisiest Laravel module (usually app/Models or app/Http/Controllers) at your current level, run vendor/bin/phpstan analyse app/Models, and click through the output. That single loop — click link, land on the offending line, fix, re-run — is the entire value proposition of this package. If that loop doesn't feel faster than the table formatter after ten errors, don't adopt it.
Shared config, local override. Commit the formatter into phpstan.neon.dist so the whole team gets it, and let individuals tweak via phpstan.neon including the dist file. Don't rely on everyone remembering to add it locally — a formatter nobody enabled does nothing.
CI vs. local split. This formatter is optimised for humans in a terminal: unwrapped errors, per-error clickable links, colour highlighting. In CI you usually want machine annotations instead. The pragmatic pattern:
# phpstan.neon.dist — the human default
parameters:
errorFormat: ticketswap
# .github/workflows/static.yml
- run: vendor/bin/phpstan analyse --no-progress --error-format=github
The CLI flag wins over the config value, so local runs stay pretty and CI still gets PR annotations. Never parse the human formatter's output in a script — it changes with terminal decoration state (see gotchas). Use --error-format=json or github for anything programmatic.
Focused sub-runs during refactors. Because every error line carries its own file+line, running PHPStan against a narrow path is cheap and useful:
vendor/bin/phpstan analyse app/Domain/Billing
Add Composer scripts for the loops you repeat:
"scripts": {
"analyse": "phpstan analyse --memory-limit=1G",
"analyse:changed": "phpstan analyse app/Domain/Billing"
}
Level-ratchet workflow. Bump level one step at a time; use --generate-baseline when the backlog is too big to clear in one PR. The formatter doesn't change baseline semantics at all — it only changes presentation — so it composes cleanly with phpstan-baseline.neon:
vendor/bin/phpstan analyse --generate-baseline
Editor integration. Wire PHPStan into a PhpStorm File Watcher or a VS Code on-save task running vendor/bin/phpstan analyse --no-progress <file>. The clickable-link output makes a narrow-scope, on-save run genuinely pleasant; a full-project run is not.
Splitting identifiers from messages. Modern PHPStan emits error identifiers alongside messages. The formatter prints the identifier when output isn't decorated, which makes it easy to copy into ignoreErrors entries when you're suppressing a rule deliberately rather than fixing it.
AI/agent workflows. Since 1.3.0 the formatter detects AI coding agents via environment variables and stops truncating paths, so an agent sees app/Models/User.php:42 verbatim and can parse it. If you drive PHPStan from a script or agent wrapper, that behaviour is automatic — no config needed.
Vendoring as an escape hatch. The package is essentially a single formatter class plus a NEON file. If you need custom behaviour (JIRA links, different truncation), extending TicketSwapErrorFormatter and registering your own errorFormat name is a realistic afternoon, not a rewrite. Keep that in mind before you reject it for lacking a knob.
Output differs between terminal and CI. When output is not decorated (piped, redirected, most CI runners), colours and hyperlinks are dropped and the formatter prints the tip and error identifier instead. This is correct behaviour, not a bug — but it means vendor/bin/phpstan analyse > out.txt will not look like your terminal, and any diff-based snapshot test of the output is fragile.
The formatter doesn't prevent terminal wrapping. "Errors don't wrap" means the formatter itself doesn't add a table or hard-wrap columns. If your terminal window is 80 columns wide and the message is 200 characters, your terminal still wraps it and the clickable link can be visually split. Widen the terminal, or use tmux/a wide pane, or accept the wrap. This trips people up because the README oversells it slightly.
Highlighting is deliberately naive. Variables, FQCNs (including leading-slash and comma-adjacent forms), stdClass, Stringable, and Class::CONSTANT are matched by regex. FQCN-looking strings inside quoted literals and heredocs get coloured too. Treat colour as a hint, never as parsed data. Don't file bugs for mis-highlighting unless it's actively misleading.
Line 0 errors print no line number. Config errors, "ignored error pattern was not matched", and file-level problems have no meaningful line; since 1.2.5 the line is omitted entirely. If you're hunting for "line 1", stop — there is none.
Path truncation breaks copy-paste. Paths render like src/App/../Entity/User.php — visually shortened, link intact. In shells where you'd normally copy the path out, that's annoying. Newer versions skip truncation entirely when an AI agent is detected. If neither behaviour suits you, that's the point to fork or override the formatter class.
editorUrl needs doubled percent signs in NEON. %%file%% and %%line%%, not %file%. A single % will make NEON try to resolve a parameter and fail with a confusing parse error.
Relative vs. absolute paths. PHPStan reports paths relative to the working directory unless you set editorUrl. Links derived from relative paths work in some terminals and not others. Setting editorUrl explicitly with your editor's URI scheme is the reliable option — do it rather than relying on the file:/// fallback.
Docker / Sail breaks OSC 8 links. Running PHPStan via docker compose exec app ./vendor/bin/phpstan analyse can strip the terminal hyperlink sequences or produce file:///var/www/html/... paths that don't exist on your host. Either run PHPStan on the host (with a host-compatible autoloader/container) or accept plain paths and use your IDE's "go to file" instead.
PHPStan 2 compatibility landed in 1.1.4. If you're on PHPStan 2.x, pin ^1.3 (or at least ^1.1.4); older tags will fail on version constraints. Also check the PHP floor — the package settled on PHP 8.1+ after some back-and-forth in early releases, so PHP 7.4 projects are out of luck.
Windows support only arrived in 1.2.5. Older versions mangle path separators and links. If you have Windows developers, don't let a stale lockfile pin them below that.
Missing include = cryptic failure. Without phpstan/extension-installer and without the manual includes: entry, PHPStan complains about an unknown errorFormat. The message doesn't always say "you forgot the extension include", so check that first.
Extension points are thin, by design. There's no config for colours, no truncation length setting, no severity mapping. What you get is what you get. If you need any of that, register a second formatter name in your own extension NEON pointing at a subclass — don't try to bend this package's config.
Vendor risk assessment. 87 stars, zero listed dependents, effectively a one-maintainer TicketSwap project. Mitigations: MIT licence, tiny surface area (one formatter class + one NEON file), and it's easy to vendor-copy into app/Support/Phpstan/ if it's ever abandoned. Treat it as an optional DX nicety, not infrastructure — never let your build depend on it being available.
How can I help you explore Laravel packages today?