phptramp / phptramp
Static analyzer that detects tramp data: parameters passed through chains of PHP methods that never use them.
Requires
- php: >=8.2
- nikic/php-parser: ^5.0
Requires (Dev)
- infection/infection: ^0.29.14 || ^0.34
- phpmd/phpmd: ^2.15
- phpstan/phpstan: ^2.0
- phpunit/phpunit: ^11.5 || ^12.0
- squizlabs/php_codesniffer: ^3.13
This package is auto-updated.
Last update: 2026-08-14 10:34:49 UTC
README
Detect tramp data in PHP codebases: parameters that get passed through chains of methods that never use them — "this parameter was passed through 4 classes / 5 methods before being used."
What it does
$ vendor/bin/phptramp --folder src --limit 3
FINDING $config: 3 pass-through hops across 4 classes
origin App\Http\Controller::handle($config) src/Http/Controller.php:21→23
hop 2 App\Service\ServiceA::process($config) src/Service/ServiceA.php:14→16
hop 3 App\Service\ServiceB::run($config) src/Service/ServiceB.php:9→11
terminal App\Mail\Mailer::__construct($config) src/Mail/Mailer.php:12 (stored)
The origin is hop 1, so a 3-hop chain runs origin → hop 2 → hop 3 → terminal; the
summary counts the origin. Add --explain to see, per edge, how each call was
resolved.
A hop is a method that receives a parameter and purely forwards it — never reads a property, never calls a method on it, never uses it in an expression. Long hop chains are the "tramp data" smell: every method in the middle is coupled to a value it has no business knowing about. The fix is usually a parameter object, a context object, or dependency injection at the terminal — this tool tells you where.
Construction and delegation don't count: a constructor forwarding to
parent::__construct() (or any parent:: call) is the same object
handling its own value, so that node scores neither a hop nor a class and
is marked (parent) in the chain.
Diff-aware CI mode
The flagship feature is diff-aware CI mode: run it on a pull request and it reports
"your edit made this chain longer", marking exactly which hops are yours.
--changed-only restricts findings to chains that intersect a diff — a hop matches iff
its declaration line or its forwarding call-site line was touched — and each matching
hop's location line grows a *YOURS* annotation:
$ vendor/bin/phptramp --folder src --changed-only --git-base origin/main
FINDING $config: 3 pass-through hops across 4 classes
origin App\Http\Controller::handle($config) src/Http/Controller.php:21→23
hop 2 App\Service\ServiceA::process($config) src/Service/ServiceA.php:14→16 *YOURS*
hop 3 App\Service\ServiceB::run($config) src/Service/ServiceB.php:9→11
terminal App\Mail\Mailer::__construct($config) src/Mail/Mailer.php:12 (stored)
1 finding (limit: 3 hops).
The →N after a hop's file:line is the forwarding call-site line — the line
inside that method where the parameter is forwarded on. Terminal hops have no
forwarding call, so they show only file:line. When a method forwards the same
parameter to the same callee via multiple call sites, each produces its own
finding distinguished by this →N value.
--git-base <ref> (default origin/main) supplies the diff via
git diff --unified=0 <ref>...HEAD (three-dot, merge-base semantics); --diff <path|->
reads a unified diff from a file, or from stdin with -, instead — either always implies
--changed-only. See docs/ci.md for GitHub Actions and GitLab recipes.
Installation
composer require --dev phptramp/phptramp
composer tramp
Add a script to your project's composer.json to invoke it the short way:
{
"scripts": {
"tramp": "phptramp --folder src"
}
}
composer tramp composer tramp -- --format json --warn-limit 2
This repository gates itself with exactly this script — see its
own phptramp.dist.json.
CLI
phptramp [options]
--folder <dir> Analyze all .php files under <dir> (repeatable)
--file <path> Analyze a single file
--files <a,b,c> Comma-separated list of files
--limit <n> Fail on chains with >= n pass-through hops (default: 6)
--warn-limit <n> Warn (do not fail CI) on chains with >= n hops (default: 4; 0 = off)
--min-classes <n> Only report chains traversing >= n distinct classes (default: 0 = off)
--format <fmt> text|pretty|json|github|checkstyle|sarif|summary (default: pretty on TTY, text otherwise)
--color <mode> always|auto|never (default: auto; honors NO_COLOR in auto mode)
--explain Show why chains ended (call resolution trace)
--exclude-terminal <kind> Do not report chains ending in <kind> (repeatable; used|stored|&-terminated|unused-end|external|truncated -- quote '&-terminated', or the shell backgrounds the command)
--changed-only Only report chains touching changed lines
--git-base <ref> Diff base for --changed-only (default: origin/main)
--diff <path|-> Read the diff from a file, or stdin with '-' (implies --changed-only)
--baseline <file> Ignore findings recorded in the baseline file
--generate-baseline <f> Write current findings to a baseline file
--fail-on-stale Exit 1 when stale baseline entries or stale suppressions are found
--no-cache Disable the per-file index cache (re-parse every file)
--no-config Ignore phptramp.json / phptramp.dist.json (drive everything from flags)
--dump-index Print the classified method index as JSON and exit
Exit codes: 0 no finding at error severity, 1 at least one finding at/over
--limit, 2 tool error — so it drops straight into any CI pipeline.
Configuration
phptramp reads phptramp.json (falling back to phptramp.dist.json) from
the current working directory; CLI flags always win. All keys, the index
cache, and performance numbers are documented in
docs/configuration.md.
{
"paths": ["src"],
"limit": 3,
"warnLimit": 2
}
Output formats
--format selects the renderer; all seven are implemented and each has an exact-string
unit test.
pretty is the default on a TTY; pipes and CI fall back to text automatically.
--color=always|auto|never overrides (NO_COLOR is honored in auto mode).
| Format | One-line example |
|---|---|
pretty |
src/Demo.php (bold-blue file header) / FINDING $config: 3 pass-through hops across 4 classes (colored, grouped by file) |
text |
FINDING $config: 3 pass-through hops across 4 classes |
json |
{"limit":3,"warnLimit":null,"findings":[{"param":"config","severity":"error","hops":3,...}]} |
github |
::error file=src/Demo.php,line=12,title=phptramp%3A%3A$config%3A 3 pass-through hops across 4 classes (terminal%3A Demo\Mailer%3A%3A__construct [stored]) |
checkstyle |
<error line="12" severity="error" message="$config: 3 pass-through hops across 4 classes (terminal: Demo\Mailer::__construct [stored])" source="phptramp.trampData"/> |
sarif |
{"ruleId":"phptramp.trampData","level":"error","message":{"text":"$config: 3 pass-through hops across 4 classes (terminal: Demo\\Mailer::__construct [stored])"}} |
summary |
12 chains total; 5 at or over the limit (limit: 3 hops). |
In a --changed-only run, each json chain entry additionally carries a "changed": true|false field marking whether that hop intersects the diff; a normal full run omits
the field entirely rather than sending it as always-false noise.
Suppression
Mark a specific false positive as intentional instead of raising --limit project-wide:
#[TrampIgnore]on a class, method, function, or parameter. Matching is by the attribute name's short name, so an analyzed codebase never has torequireor autoload phptramp's ownPhpTramp\Ignore\TrampIgnoreclass to use it.// phptramp-ignoreas a real PHP comment (a marker inside a string literal does not count) on a hop's declaration line, the line directly above it, or a forwarding call-site line.
Either drops the entire chain passing through that hop, not just the one parameter reaching the flagged declaration.
--warn-limit
--warn-limit <n> adds a second, lower threshold below --limit. Chains whose hop
count falls in [warn-limit, limit) render at severity: "warning" (WARNING in text,
::warning in the GitHub format, "level": "warning" in SARIF, …) but never fail the
run — only chains at or over --limit set exit code 1. Useful for tightening a hop
budget gradually: warn today, promote to a hard failure once the codebase is clean.
0 disables the warn tier entirely (no warnings emitted); --limit 0 likewise disables
the fail tier, leaving only warnings. --min-classes <n> is a complementary filter:
it suppresses any chain traversing fewer than n distinct classes regardless of
severity, so you can scope a noisy warn tier to genuinely wide chains.
Baseline
Adopting phptramp on a codebase with existing tramp data? --baseline
snapshots today's findings so CI only gates new chains, with
refactor-stable fingerprints that survive renames and moved lines — and
--fail-on-stale nudges you to prune entries as you fix them. The full
workflow is in docs/baseline.md.
IDE integration
PhpStorm (and any IDE with external-tool support) can run phptramp per file via
--file; a documented External Tool + File Watcher recipe is in
docs/phpstorm.md. --format=checkstyle and --format=sarif
cover CI annotation ecosystems, including GitHub Code Scanning.
Non-goals
- No auto-fix. Fixing tramp data means refactoring (parameter objects, DI rewiring), which is not mechanically safe. phptramp reports; you decide.
- No second production dependency.
nikic/php-parseris the only one, deliberately.
Contributing
Contributions are welcome — see the contributing guide for the workflow and the CI gates, and the code of conduct. Bugs are best reported with the bug report form.