sebastian/phpdcd
PHP Dead Code Detector (phpdcd) is a command-line tool that scans PHP codebases for unused functions and methods, helping teams find dead code to remove. Part of Sebastian Bergmann's tooling, it is now archived and largely superseded by modern static analysis tools.
sebastian/phpdcd is a CLI tool, not a library. You will never use it in application code and it exposes no API — you run a binary against a directory and read a list of functions/methods it cannot find a caller for.
Install outside your runtime dependencies:
composer require --dev sebastian/phpdcd
⚠️ The last tagged release predates PHP 8 and pins an old nikic/php-parser. On a modern Laravel stack (PHP 8.2+) Composer will likely refuse to resolve. Plan for one of: --ignore-platform-reqs, the distributed .phar, or running it in a throwaway PHP 7.x container. Check this before you write any tooling around it.
Confirm the flag set for your installed version — it is small and version-dependent:
vendor/bin/phpdcd --help
Expect roughly --recursive, --exclude <dirs>, --names <list>, --names-exclude <list>.
First run: do not point it at app/. Start with a slice of plain PHP that has no framework magic — app/Support, app/Services, app/Actions, app/Helpers:
vendor/bin/phpdcd --recursive --exclude vendor,tests,storage app/Support
Read the output as leads, not verdicts. Every line is "no static call site found", which in a Laravel app is very often a false positive.
Where to look first: private/protected helpers and *Service/*Action methods left behind after a refactor. Those are the highest-signal, lowest-risk findings. Treat anything public on a controller, command, job, listener, policy, or middleware as guilty until proven innocent — the framework calls those reflectively and phpdcd cannot see it.
Triage sprint, not CI gate. phpdcd's output is too noisy to block merges on. The realistic pattern is a periodic "dead code audit" branch: run it, hand-verify findings, delete, open one PR.
Scope by directory, run multiple times. There is no config file, so encode your scope in a script or Make target:
deadcode:
@for d in app/Support app/Services app/Actions; do \
vendor/bin/phpdcd --recursive --exclude vendor,tests "$$d"; \
done
Whitelist framework entry points via --names. Framework-invoked methods will flood the report. Feed them in as "considered used" (verify the exact semantics against --help on your version, since the flag's meaning shifted between releases):
__construct,__invoke,handle,register,boot,up,down,run,render,toArray,rules,authorize,via,scope*
Cross-check with a runtime signal. Combine with an Xdebug/PCOV coverage run of your test suite. A method that is both never executed by tests and flagged by phpdcd is a high-confidence deletion. A method flagged by phpdcd but hot in coverage is a dynamic-dispatch false positive — keep it.
Verify with grep before deleting. phpdcd cannot see string dispatch. Confirm with:
git grep -n "methodName" # string-based calls, container keys, route arrays
git log -S "methodName" --oneline # who added it and when
Then delete mechanically with Rector (RemoveUnusedPrivateMethodRector in --dry-run --diff mode) rather than by hand, so the diff is reviewable.
Report-only CI. If you wire it up at all, publish the output as a job-summary/artifact:
- run: vendor/bin/phpdcd --recursive app/Support > phpdcd.txt || true
- uses: actions/upload-artifact@v4
with: { name: phpdcd, path: phpdcd.txt }
Long-term: for a maintained Laravel project, use PHPStan (unused private method/property detection), phan --dead-code-detection (methods, functions, classes), or Rector's dead-code rules. Keep phpdcd only as a one-off archaeology tool on legacy code.
Massive false-positive rate in Laravel. phpdcd's resolver is name-based, not type-based: a single $x->handle() call marks every handle() in the codebase as used, and conversely it cannot see anything invoked reflectively. Methods invoked by the framework and therefore invisible to it include:
| Area | Methods it will wrongly flag |
|---|---|
| Controllers | everything routed to, __invoke |
| Console commands | handle |
| Jobs / listeners | handle, __invoke, failed |
| Policies | viewAny, view, create, update, delete |
| Middleware | handle, terminate |
| Form requests | authorize, rules, messages, attributes |
| Models | scope*, accessors/mutators, casts, booted |
| Mailables / notifications | build, via, toMail |
| Blade components | render, shouldRender |
| Providers | register, boot |
Name collisions hide real dead code. Because resolution is name-only, common names (get, run, make, handle, toArray) are effectively never reported even when genuinely dead. Do not read a clean report as "no dead code."
It detects only functions and methods. Not unused classes, properties, constants, parameters, local variables, or imports. Use PHPStan/phpmd for those.
Invisible call styles. call_user_func([$o,'foo']), $o->$name(), Closure::fromCallable, app(Foo::class), container string bindings, event class strings, and route 'Controller@method' string syntax are all undetectable.
Modern syntax may not parse. Enums, readonly properties, attributes, and especially first-class callable syntax ($this->foo(...)) postdate the last release's parser. A parse failure can silently abort analysis of a whole file — sanity-check by introducing a deliberate unused method and confirming it is reported.
Interfaces, abstracts, and traits. Interface-mandated implementations are frequently reported; trait methods can be attributed to the wrong class. Exclude app/Contracts and trait directories.
Debugging. No verbosity or config; if results look wrong, shrink the input to one file and re-run. There are no extension points — no plugin API, no config file. Your only levers are --exclude and --names. If you need more, fork it or move on.
Archived and unmaintained. No fixes for new PHP versions, no security response, and the parser dependency will drift further out of date. Do not build a durable pipeline on it.
License. Packagist metadata reports NOASSERTION, which will trip automated license scanners in a commercial repo even though the upstream source is BSD-3-Clause. Read the LICENSE file in the repo before adding it to a shipped monorepo; a --dev-only, locally-run tool is usually fine, but confirm with whoever owns compliance.
How can I help you explore Laravel packages today?