P
imvdmolen.nl
Blog

Dead code detecteren en opruimen met statische code analyse in PHP

In mijn PHP-projecten loop ik regelmatig tegen code aan die nergens meer wordt aangeroepen. Functies die ooit belangrijk waren, klassen die door refactoring overbodig zijn geworden, of variabelen die wel worden toegewezen maar nooit gelezen. Deze dode code zorgt niet alleen voor onnodige ballast in je codebase, maar maakt ook het onderhoud een stuk lastiger. Wat veel developers onderschatten is hoeveel impact dit heeft op de leesbaarheid en prestaties van hun applicatie.

Waarom dead code een groter probleem is dan je denkt

Dode code lijkt op het eerste gezicht onschuldig, maar in de praktijk zorgt het voor meer problemen dan je zou verwachten. Elke ongebruikte klasse moet nog steeds worden ingeladen door PHP's autoloader, wat extra geheugen kost en de opstarttijd van je applicatie vertraagt. Bij grotere projecten kan dit oplopen tot honderden kilobytes aan onnodige code die bij elke request wordt geladen.

Het grootste probleem zit echter in de cognitieve belasting. Wanneer ik door een codebase navigeer op zoek naar een specifieke functionaliteit, stuit ik constant op methods en klassen die lijken alsof ze relevant zijn. Ik besteed tijd aan het begrijpen van code die helemaal geen invloed heeft op het eindresultaat. Dit wordt nog erger bij code reviews, waar collega's kostbare tijd verspillen aan het doorlopen van functies die niemand meer aanroept.

Security is een ander aspect waar dead code problemen kan veroorzaken. Ongebruikte endpoints of methods blijven vaak achter zonder de juiste beveiligingsmaatregelen. Als er later bugs worden ontdekt in deze vergeten code, kunnen ze alsnog een ingang vormen voor kwaadwillenden. Ik heb projecten gezien waar oude admin-functies jarenlang ongebruikt in de codebase stonden, compleet met verouderde authenticatie.

Statische analyse tools voor dead code detectie

PHP biedt verschillende tools om automatisch ongebruikte code op te sporen. Psalm is een van de krachtigste opties voor dit doel. Het analyseert niet alleen type-errors, maar kan ook detecteren welke methods, properties en klassen nergens worden aangeroepen. De configuratie is vrij straightforward en je kunt het direct in je CI/CD pipeline integreren.

// psalm.xml configuratie
<?xml version="1.0"?>
<psalm
    errorLevel="1"
    resolveFromConfigFile="true"
    xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
    xmlns="https://getpsalm.org/schema/config"
    xsi:schemaLocation="https://getpsalm.org/schema/config vendor/vimeo/psalm/config.xsd"
    findUnusedVariables="true"
    findUnusedCode="true"
>
    <projectFiles>
        <directory name="src" />
        <ignoreFiles>
            <directory name="vendor" />
        </ignoreFiles>
    </projectFiles>
</psalm>

PHPStan biedt ook dead code detectie, hoewel dit minder uitgebreid is dan Psalm. De kracht van PHPStan ligt meer in type-checking, maar het kan wel ongebruikte private methods en properties detecteren. Voor een complete analyse combineer ik vaak beide tools, waarbij PHPStan zich richt op type safety en Psalm op code coverage.

Een interessante aanvulling is Rector, dat niet alleen dead code kan detecteren maar ook automatisch kan verwijderen. Dit is vooral handig bij grote refactoring sessies waar je weet dat bepaalde code niet meer nodig is. Rector kan bijvoorbeeld automatisch ongebruikte imports verwijderen of private methods elimineren die nergens worden aangeroepen.

# Rector configuratie voor dead code removal
composer require rector/rector --dev
vendor/bin/rector init

# In rector.php
use Rector\DeadCode\Rector\ClassMethod\RemoveUnusedPrivateMethodRector;
use Rector\Config\RectorConfig;

return function (RectorConfig $rectorConfig): void {
    $rectorConfig->rule(RemoveUnusedPrivateMethodRector::class);
};

Handmatige technieken voor complexere situaties

Automatische tools dekken lang niet alle gevallen af. Dynamic method calls, reflection en magic methods kunnen ertoe leiden dat legitieme code wordt gemarkeerd als ongebruikt. In Laravel projecten zie ik dit vaak bij Eloquent relationships die via magic methods worden aangeroepen, of bij event listeners die dynamisch worden geregistreerd.

Voor deze situaties ontwikkel ik een meer handmatige aanpak. Ik start met een grep-search door de hele codebase om te kijken waar een specifieke method of klasse wordt genoemd. Dit geeft een eerste indicatie, maar je moet oppassen voor string-based references of dynamische aanroepen die niet door reguliere expressies worden opgevangen.

# Zoeken naar alle references naar een method
grep -r "methodName" --include="*.php" /path/to/project

# Zoeken naar class instantiations
grep -r "new ClassName" --include="*.php" /path/to/project

# Zoeken naar static calls
grep -r "ClassName::" --include="*.php" /path/to/project

Een techniek die ik vaak toepas is het tijdelijk toevoegen van logging statements aan verdachte methods. Door een simpele Log::info() statement toe te voegen met de method naam, kan ik over een periode van een week monitoren of bepaalde code wel wordt aangeroepen in productie. Dit is vooral nuttig voor edge cases of seizoensgebonden functionaliteit die misschien maar één keer per jaar wordt gebruikt.

Xdebug's code coverage functionaliteit biedt ook inzicht in welke regels code daadwerkelijk worden uitgevoerd tijdens je test suite. Hoewel dit geen volledige garantie geeft dat code ongebruikt is in productie, helpt het wel om methods te identificeren die niet door tests worden gedekt en mogelijk kandidaten zijn voor verwijdering.

Strategisch omgaan met gevonden dead code

Het vinden van dode code is één ding, maar het veilig verwijderen vereist een doordachte strategie. Ik maak altijd eerst een lijst van alle gedetecteerde items en categoriseer ze naar risico. Private methods van een klasse zijn meestal veilig om te verwijderen, terwijl public methods potentieel door externe code kunnen worden aangeroepen.

Bij Laravel projecten controleer ik altijd de route files en event listeners voordat ik controller methods verwijder. Een method die niet door andere PHP-code wordt aangeroepen kan nog steeds een geldig endpoint zijn. Hetzelfde geldt voor Artisan commands, jobs en andere componenten die dynamisch worden geladen door het framework.

Database migrations vormen een speciaal geval. Oude migratie bestanden lijken vaak ongebruikt omdat ze in productie al zijn uitgevoerd, maar verwijdering kan problemen veroorzaken bij fresh installations of tijdens rollbacks. Hier handhoof ik een conservatievere aanpak waarbij ik alleen migrations verwijder als ik zeker weet dat de gehele database opnieuw kan worden opgezet zonder deze files.

Voor grote codebase cleanups werk ik altijd in kleine batches. In plaats van in één keer alle gedetecteerde dead code te verwijderen, pak ik per week een kleine subset aan. Dit geeft tijd om eventuele regressies op te merken voordat ze zich opstapelen. Feature toggles kunnen hierbij helpen om nieuwe functionality geleidelijk uit te rollen terwijl oude code nog beschikbaar blijft als fallback.

Het documenteren van verwijderde code is ook belangrijk. Ik houd bij welke methods en klassen ik heb weggehaald en waarom, zodat teamgenoten begrijpen wat er is gebeurd als ze naar oude code zoeken. Git history helpt hierbij, maar een changelog of migration document maakt het nog duidelijker.

Statische code analyse voor dead code detectie heeft mijn development workflow aanzienlijk verbeterd. Mijn projecten zijn schoner, sneller en makkelijker te begrijpen. Het vergt wat discipline om de tools regelmatig te draaien en de resultaten serieus te nemen, maar de investering in tijd betaalt zich dubbel en dwars terug in verminderde complexiteit en betere prestaties.