Weave Code
Weaver
Helps Laravel developers discover, compare, and choose open-source packages. See popularity, security, maintainers, and scores at a glance to make better decisions.
Feedback
Share your thoughts, report bugs, or suggest improvements.
Subject
Message

Dead Code Detector Laravel Package

shipmonk/dead-code-detector

PHPStan extension that finds unused PHP code: dead methods, constants, enum cases, properties, dead cycles, and transitively dead members. Detects dead tested code and can automatically remove unused code. Supports Symfony and other libraries with customizable usage providers.

View on GitHub
Deep Wiki
Context7

Getting Started

Minimal viable setup for a Laravel app. Install as a dev dependency alongside Larastan (you want Laravel's type inference, not just dead-code rules):

composer require --dev shipmonk/dead-code-detector larastan/larastan

Then wire it into phpstan.neon.dist. The single most important detail is paths: the detector only sees usage it scans, so routes/, database/, config/ and tests/ must be in scope, not just app/.

includes:
    - vendor/larastan/larastan/extension.neon
    - vendor/shipmonk/dead-code-detector/rules.neon

parameters:
    level: 6
    paths:
        - app
        - routes
        - database
        - config
        - tests
    tmpDir: var/phpstan
    shipmonkDeadCode:
        usageExcluders:
            tests:
                enabled: true

Run it:

vendor/bin/phpstan analyse --memory-limit=2G

Where to look first. Don't read the full report. Triaging the first 20–30 errors by category tells you everything about how much setup you still owe:

  • Unused App\Jobs\Foo::handle / controllers / commands → your routes/, app/Console/Kernel.php or bootstrap/app.php aren't being scanned, or routing is done in a package.
  • Model accessors, scopeXxx, boot() → usually fine out of the box (Eloquent provider), but check for custom traits.
  • View-model / DTO getters → almost always Blade usage the tool can't see (see Gotchas).
  • Genuinely dead: legacy service methods, orphaned enum cases, unused constants.

First use case worth doing today. Enable the tests excluder (as above) and run the whole codebase. It surfaces the class of code that looks alive only because a test asserts on it — legacy validators, unused formatter branches, defensive helpers — which is exactly the dead weight that accumulates in mature Laravel apps and which no other PHPStan rule reports.

Verdict for a Laravel team: high value, medium setup effort. Most of the effort is not installing the tool but teaching it the parts of your app that use framework magic the Laravel provider doesn't cover (Blade, Livewire/Filament/Inertia/Nova if present).

Implementation Patterns

Baseline config that survives a real app. Keep dead-code detection in the same PHPStan run as everything else initially (one report, one CI job), and only split it out later if the noise bothers you:

parameters:
    shipmonkDeadCode:
        detect:
            deadMethods: true
            deadConstants: true
            deadEnumCases: true
            deadProperties:
                neverRead: true
                neverWritten: false   # models often write attributes Eloquent reads magically
        reportTransitivelyDeadMethodAsSeparateError: false

Leave reportTransitivelyDeadMethodAsSeparateError: false (the default): reporting one root cause with "💡 Thus …" tips is far more actionable than 200 errors for a single orphaned facade method.

CI pattern: observe → ratchet → enforce.

  1. Week 1: run with continue-on-error: true, collect the report as an artifact, triage categories.
  2. Week 2: fix or exclude everything, then flip to blocking. Since dead code only appears when code is deleted/refactored, the gate is cheap to maintain.
  3. Run it as a separate job over the whole tree (it's slower than a per-diff run and gives no value on partial paths).
- run: vendor/bin/phpstan analyse --memory-limit=2G --error-format=github

Automatic removal workflow. This is the feature that earns the package its place, but treat it as a destructive migration:

git switch -c chore/dead-code
vendor/bin/phpstan analyse --error-format removeDeadCode --memory-limit=2G
git diff --stat          # review before anything else
vendor/bin/phpunit       # tests are intentionally NOT removed
git commit

Because the tests excluder keeps test-only usages in place, the removal output annotates what it kept and why (! Exclud…) — useful for finding tests that exist solely to keep dead code green. Delete those tests deliberately afterwards.

Custom usage provider for app-specific magic. Livewire, Filament, Inertia pages, Nova resources and any in-house __call-style dispatcher are invisible to the detector. Start with the reflection-based provider — it covers the majority of "this method is called by naming convention" cases with ten lines:

use ReflectionMethod;
use ShipMonk\PHPStan\DeadCode\Provider\ReflectionBasedMemberUsageProvider;
use ShipMonk\PHPStan\DeadCode\Provider\VirtualUsageData;

final class LivewireUsageProvider extends ReflectionBasedMemberUsageProvider
{
    public function shouldMarkMethodAsUsed(ReflectionMethod $method): ?VirtualUsageData
    {
        $class = $method->getDeclaringClass();

        if ($class->isSubclassOf(\Livewire\Component::class)) {
            // mount(), hydrate(), updatedX(), rules(), render(), computed props...
            return VirtualUsageData::withNote('Livewire lifecycle / action');
        }

        return null;
    }
}
services:
    -
        class: App\Phpstan\LivewireUsageProvider
        tags:
            - shipmonk.deadCode.memberUsageProvider

Reach for the raw MemberUsageProvider (AST-based, getUsages(Node $node, Scope $scope)) only when the usage is deducible from a call site — e.g. a View::make('users.show', ['user' => $dto]) convention, or a serializer that hydrates DTOs by class-string. Always emit UsageOrigin::createRegular($node, $scope) so transitive elimination stays correct.

Excluder pattern for public surface. Packages and modules with a deliberately public API (a src/Support namespace consumed by other repos, or methods called from config strings) are better excluded than annotated:

final class PublicApiExcluder implements MemberUsageExcluder
{
    public function shouldExclude(ClassMemberUsage $usage, Node $node, Scope $scope): bool
    {
        return str_starts_with($usage->getMember()->getClassName(), 'App\\Support\\');
    }
}

Tag it with shipmonk.deadCode.memberUsageExcluder. Excluders run after providers, so they win.

Laravel features that already work — don't reimplement them in a provider: route registration (callable, Controller@method, invokable), Event::listen/subscribe and auto-discovered handle*, Schedule::job(), gates/policies incl. $this->authorize(), real-time facades, console commands, jobs, providers, middleware, notifications, form requests, mailables, broadcast events, JSON resources, Eloquent scopes/relations/accessors, observers (Model::observe() and #[ObservedBy]), factories, seeders and migrations (up/down). Also rely on the vendor/builtin providers: any override of a vendor/ or PHP-core method is considered used — so handle(), getIterator(), boot() from a package base class are never false positives.

Gotchas and Tips

  • Scan the whole codebase, always. The single biggest source of false positives is a partial paths list. Route files, bootstrap/app.php, app/Console/Kernel.php, provider register() bodies and tests must all be analysed. vendor/bin/phpstan analyse app will lie to you.
  • Blade is not supported (Twig is). Any method or property touched only from .blade.php will be reported. Three options, in order of preference: (1) an excluder for view-model/DTO namespaces, (2) a reflection-based provider marking ViewModel-marked classes as used, (3) php artisan view:cache then adding storage/framework/views to paths — it works but pollutes the report with compiled-file paths and must not be committed.
  • Livewire, Filament, Inertia, Nova, Orchid are not covered by the Laravel provider. Without a custom provider you'll get hundreds of false "unused" reports on lifecycle hooks and public properties. Do this before enabling the rule in CI, or scope those directories out initially.
  • String class references are invisible. config/queue.php, config/auth.php provider arrays, 'App\Jobs\Foo' in DB rows, class_exists() strings. Put config/ in paths (helps for ::class constants) and keep a provider/excluder for the rest.
  • Dynamic dispatch hides dead code. $this->{$method}(), app($class), resolve(), container make() with a variable — these tend to mark things used. That's a false negative, not a false positive: the tool is conservative, so a clean report is strong evidence, a dirty one isn't proof.
  • Property detection is noisy on Eloquent models. neverWritten fires on properties Eloquent writes magically. Keep neverRead: true, neverWritten: false until you've triaged. Likewise, #[Column]-style enum cases are safe (Doctrine provider), but enum cases referenced only via Rule::enum(Status::class) may need a small provider — prefer Status::cases() in the rule, or add the provider.
  • Pest closures aren't methods. PHPUnit test* methods, data providers, #[Test]/#[Before] attributes and annotations are covered; Pest's it('…', fn () => …) bodies aren't class members, so nothing is lost — but unused functions aren't detected at all by this package, only class members.
  • removeDeadCode is destructive and format-specific. Run it on a clean working tree, never with --generate-baseline, and re-run PHPStan afterwards: removal invalidates the result cache and can expose a second wave of transitively dead members. Review the diff — it removes constants/enum cases along with methods.
  • Don't baseline dead code. It defeats the purpose. If you must defer, use identifier-based ignores (shipmonk.deadMethod, plus the constant/enum/property identifiers) scoped by path:, and treat them as a TODO list.
  • Performance. Full-tree analysis with Larastan is slow. Set tmpDir inside the project, raise --memory-limit, keep PHPStan's result cache warm in CI, and accept that this job runs on the merge target rather than per-push.
  • Extension points to prefer, in order: usageExcluders.tests → path-scoped ignoreErrors → reflection-based provider → AST provider → custom excluder. Only write AST providers for calls you can verify against the framework source; a provider that over-reports silently hides real dead code forever.
  • Version hygiene. Requires PHP 8.1+ and PHPStan 2.x; the Laravel provider auto-enables when laravel/framework is in composer deps. Pin the package alongside larastan and bump them together — provider coverage grows release to release (the Laravel/Eloquent providers are relatively recent, so a report that looks wrong may simply be an old version).
Weaver

How can I help you explore Laravel packages today?

Conversation history is not saved when not logged in.
Prompt
Add packages to context
No packages found.
oxygen/data
oxygen/core
jengo/base
xiloryx/log-collector
a21ns1g4ts/filament-stripe
la-php/laravel-admin
artisan-build/community-prompts
vleroy/smart
ayles-software/laravel-australian-public-holidays
cboxdk/laravel-id-spatie
cboxdk/laravel-id-billing
cboxdk/laravel-id-billing-bridge
simple-as-fuck/php-performance-log
hsluv/hsluv
seampact/codegen
seampact/hooks
seampact/ir
mehedimi/laravel-amp-mail
happones/laravel-vue-i18n-generator
jwerd/inertia-table