Legacy PHP refactoren voelt vaak als een mijnenveld. Je opent een bestand van 2000 regels, ziet globale variabelen, directe database-calls en HTML vermengd met business logic, en je weet: één verkeerde wijziging en de hele webshop ligt plat. Toch is deze code je bedrijfskritische applicatie die gewoon werkt.
In deze gids laat ik je zien hoe je legacy PHP stap voor stap moderniseert zonder dat productie in de fik vliegt. Je leert welke technieken écht werken, welke valkuilen je moet vermijden en hoe je met moderne tooling een vangnet bouwt dat je de moed geeft om die oude monoliet aan te pakken.
Wat maakt PHP-code eigenlijk "legacy"?
Legacy code is niet per se oude code. Michael Feathers definieert het in zijn boek Working Effectively with Legacy Code als "code zonder tests". Die definitie raakt de kern: zonder tests kun je geen enkele wijziging met vertrouwen doen.
In de PHP-wereld zie je vaak dezelfde kenmerken terugkomen bij legacy projecten:
- Oude PHP-versies (5.6, 7.0) zonder types of moderne syntax
- Geen autoloading, overal
require_oncebovenaan bestanden - Globale state via
$_GLOBALS,$_SESSIONen statische classes - Business logic vermengd met HTML en SQL in dezelfde file
- Geen Composer, geen namespaces, geen dependency injection
- Een
functions.phpvan 5000 regels waar niemand meer aan durft te komen
Herken je dit? Dan is dit artikel voor jou geschreven. De goede news: met de juiste aanpak kun je dit soort codebases stapsgewijs moderniseren.
De gouden regel: nooit refactoren zonder vangnet
De grootste fout die ik developers zie maken is direct beginnen met herschrijven. Je denkt: "Ik snap deze functie wel, ik ga hem even opschonen." Drie dagen later staat productie stil omdat een obscure edge case die niemand kende nu anders werkt.
Voordat je ook maar één regel aanpast, heb je een vangnet nodig. Dat vangnet bestaat uit drie lagen:
- Characterization tests, tests die het huidige gedrag vastleggen, ook als dat gedrag "fout" lijkt
- Static analysis, tools die type-fouten en bugs opsporen zonder code uit te voeren
- Monitoring in productie, logging en error tracking zodat je direct ziet wanneer iets misgaat
Pas als deze drie op orde zijn, kun je beginnen met wijzigen.
Stap 1: breng de codebase in kaart
Voordat je iets aanraakt, moet je begrijpen wat er is. Begin met een inventarisatie:
- Welke PHP-versie draait er in productie?
- Zijn er externe dependencies? Hoe worden die beheerd?
- Welke onderdelen zijn bedrijfskritisch en welke nauwelijks gebruikt?
- Waar zitten de meeste bugs en pijnpunten?
Gebruik tooling om dit systematisch te doen. PhpMetrics geeft je inzicht in complexiteit per klasse. Een analyse van je productie-logs laat zien welke endpoints écht veel verkeer krijgen, daar loont refactoring het meest.
Maak ook een afhankelijkheidskaart: welke onderdelen roepen elkaar aan? Vaak ontdek je dat 80% van de code nauwelijks gebruikt wordt en dat je je energie moet richten op die kritieke 20%.
Stap 2: zet moderne tooling op
Zelfs op een legacy codebase kun je moderne tools draaien. Installeer als eerste Composer, zie ook onze uitleg over Composer en dependency management. Composer is de poort naar alles: autoloading, packages, tools.
Voeg vervolgens deze tools toe als dev-dependencies:
composer require --dev phpstan/phpstan
composer require --dev rector/rector
composer require --dev phpunit/phpunit
composer require --dev friendsofphp/php-cs-fixer
Begin met PHPStan op level 0, zelfs dat vindt al onverwachte bugs in legacy code. Lees onze gids over static analysis met PHPStan en Psalm voor de details. Het idee: elk level hoger voeg je meer strictheid toe, maar forceer jezelf niet direct naar level 9.
Rector is een bijzonder krachtige tool voor legacy projecten. Het voert automatische code-transformaties uit: PHP 5.6 naar 7.4, procedureel naar object-georiënteerd, oude array-syntax naar nieuwe. Je krijgt gratis duizenden kleine verbeteringen waar je anders weken aan kwijt bent.
Stap 3: schrijf characterization tests
Dit is de belangrijkste stap en tegelijk de meest overgeslagen. Een characterization test beschrijft wat de code nu doet, niet wat hij zou moeten doen. Je legt het bestaande gedrag vast, bugs en al.
Stel je hebt een legacy factuurbereking:
function berekenFactuurTotaal($items, $korting, $btw) {
$subtotaal = 0;
foreach ($items as $item) {
$subtotaal += $item['prijs'] * $item['aantal'];
}
if ($korting > 0) {
$subtotaal = $subtotaal - ($subtotaal * ($korting / 100));
}
return $subtotaal + ($subtotaal * ($btw / 100));
}
Voordat je deze functie refactort, schrijf je tests die alle paden dekken:
public function testFactuurZonderKortingEnBtw(): void
{
$items = [['prijs' => 100, 'aantal' => 2]];
$this->assertEquals(200, berekenFactuurTotaal($items, 0, 0));
}
public function testFactuurMetKortingEnBtw(): void
{
$items = [['prijs' => 100, 'aantal' => 1]];
$this->assertEquals(108.9, berekenFactuurTotaal($items, 10, 21));
}
Deze tests vormen je vangnet. Lees meer over het schrijven van goede tests in onze gids testing in PHP met PHPUnit.
Stap 4: pas het strangler pattern toe
Martin Fowler's strangler fig pattern is de veiligste refactoring-strategie voor grote legacy systemen. Het idee: laat oude en nieuwe code naast elkaar bestaan, en verplaats functionaliteit stuk voor stuk.
In de praktijk werkt dat zo:
- Zet een routing-laag voor je applicatie (zie middleware en routing in PHP)
- Schrijf nieuwe features in een moderne module, volgens bijvoorbeeld MVC pattern of domain-driven design
- Route specifieke URLs naar de nieuwe module, de rest blijft naar de oude code gaan
- Migreer stapsgewijs steeds meer routes totdat de oude code "gewurgd" is
Deze aanpak heeft enorme voordelen: je levert continu waarde op, productie blijft draaien, en je kunt elke stap terugdraaien als er iets misgaat. Geen big bang rewrite die 18 maanden duurt en dan half werkt.
Stap 5: extraheer puur logisch werk
Een praktische techniek die direct waarde oplevert: trek pure business logic uit je legacy bestanden. Zoek naar functies die input krijgen, iets berekenen, en output teruggeven, zonder database-calls of globale state.
Dit soort logica is makkelijk testbaar en makkelijk te verplaatsen naar moderne klassen. Pas object-oriented PHP principes toe en volg de SOLID principes voor de nieuwe code.
Voorbeeld: een prijsberekening die nu in een 800-regels controller zit, kun je extraheren naar een PriceCalculator class met één verantwoordelijkheid. De controller roept die class aan, de logica is getest, en je hebt een stukje moderne code in een zee van legacy.
Stap 6: introduceer namespaces en autoloading
Veel legacy PHP-projecten hebben geen namespaces. Elk bestand begint met een rij require_once statements en globale functies botsen vrolijk met elkaar. Dit is een van de makkelijkste dingen om te moderniseren.
Voeg Composer's PSR-4 autoloading toe in je composer.json:
{
"autoload": {
"psr-4": {
"App\\": "src/"
},
"files": [
"legacy/functions.php"
]
}
}
Nieuwe code komt in src/ met namespaces, oude require_once-bestanden blijven werken via de files autoloader. Zo heb je een brug tussen oud en nieuw.
Stap 7: isoleer database-toegang
Directe mysql_query() of mysqli_query() aanroepen verspreid door je codebase zijn een nachtmerrie. Consolideer database-toegang in een laag die je kunt vervangen. Gebruik PDO met prepared statements als minimum.
Een praktische strategie: wrap bestaande queries in een repository-achtige klasse. De legacy code verandert nauwelijks, maar je hebt nu één plek waar alle queries langskomen. Vanaf daar kun je caching, logging en type-safety toevoegen zonder overal wijzigingen te maken.
Veelgemaakte valkuilen bij legacy refactoring
Een paar klassieke fouten die je wilt vermijden:
De perfectionisme-val. Je wilt álles tegelijk opruimen: types, tests, naming, architectuur. Dat werkt niet. Kies één dimensie per pull request en houd wijzigingen klein.
De rewrite-hubris. "Ik kan dit in een weekend opnieuw bouwen." Nee, dat kun je niet. Elke legacy codebase bevat jaren aan bug fixes en edge cases die niet in specs staan. Een rewrite duurt altijd langer dan je denkt.
Te snel nieuwe frameworks introduceren. Stap je direct over op Laravel of Symfony? Lees eerst Symfony vs Laravel. Een framework-migratie is een enorme onderneming die je alleen moet doen als de business case glashelder is.
Testen die implementatie toetsen, niet gedrag. Je characterization tests moeten zo geschreven zijn dat ze blijven werken als je de interne implementatie verandert. Test via de publieke interface, niet via private methods.
Refactoring als continue praktijk
De grootste mindset-shift: refactoring is geen project, het is een gewoonte. De Boy Scout Rule is simpel, laat code schoner achter dan je hem aantrof. Elke keer dat je een bug fixt of feature toevoegt in legacy code, pak je een klein stukje technical debt mee.
Zet refactoring vast in je CI/CD pipeline: PHPStan moet slagen, tests moeten slagen, code style moet kloppen. Zo voorkom je dat nieuwe legacy erbij komt terwijl je de oude opruimt.
Wanneer is je refactoring "klaar"?
Eerlijk antwoord: nooit helemaal. Maar je hebt concrete mijlpalen waar je naartoe werkt:
- PHP-versie up-to-date (minimaal 8.2)
- PHPStan op level 6 of hoger
- Test coverage boven 60% voor kritieke paden
- Geen globale state meer in nieuwe code
- Duidelijke scheiding tussen domein, infrastructuur en presentatie
Als je daar bent aangekomen, heb je geen legacy codebase meer, je hebt een volwassen PHP-applicatie die nog jaren meekan.
Veelgestelde vragen
Wat is legacy PHP-code precies?
Legacy PHP is code die nog draait in productie maar lastig te onderhouden is. Vaak gaat het om oudere PHP-versies, geen tests, geen dependency management en sterk verweven logica. Het werkt, maar elke wijziging voelt risicovol.
Moet ik eerst upgraden naar PHP 8 of eerst refactoren?
Begin met een PHP-versie upgrade waar mogelijk, want moderne tooling zoals PHPStan en Rector werkt beter op nieuwere versies. Daarna kun je stapsgewijs refactoren met veel meer veiligheid en betere types.
Hoe voorkom ik dat refactoring productie breekt?
Schrijf eerst characterization tests die het huidige gedrag vastleggen, werk in kleine stappen en deploy vaak. Gebruik feature flags en het strangler pattern om oude en nieuwe code naast elkaar te laten draaien.
Is een big bang rewrite ooit een goed idee?
Zelden. Een complete herschrijving duurt meestal twee tot drie keer langer dan geschat en levert vaak minder features op dan het origineel. Incrementele refactoring met het strangler pattern is bijna altijd veiliger en sneller.
Welke tools helpen bij het refactoren van legacy PHP?
PHPStan en Psalm voor static analysis, Rector voor automatische code-transformaties, PHPUnit voor tests en Composer voor dependency management. Samen geven ze je een vangnet om veilig te refactoren.