← Blog

Joomla

Joomla 3 naar 5 migreren: zes PHP-valkuilen die weken kosten

Een upgrade van Joomla 3 naar 5 is ook een sprong van PHP 7 naar 8. Hieronder de zes versievalkuilen die we bij elke legacy-migratie raken, plus de audit die je niets kost.

Jacob Molkenboer· Oprichter · A Brand New Company· 4 jun 2026· 7 min
Leren logboek met messing sleutel, groen lint en gebroken rood lakzegel op ivoorpapier bij een raam.

Het is een dinsdag in november. Je bureau verkocht een 'Joomla 3 naar 5 refresh' als weekendklus. De klant verwachtte maandag weer online te zijn met een snellere admin en een opgeruimder dashboard. Je draaide de upgrade-preflight, zag een groene checklist en had er een goed gevoel bij. Tegen vrijdag gooit de staging-site een 500 bij elke artikelweergave, weigert het inlogformulier stilletjes geldige credentials, en loopt je errorlog vol met 4.200 deprecation-warnings van een CCK-extensie die voor het laatst is aangeraakt in 2017.

Dit is het gat tussen een 'Joomla-migratie' en een 'PHP 7 naar 8 migratie'. Het is dezelfde klus, maar er staat er maar één op de offerte.

Joomla 3 sites draaien meestal op PHP 7.x of, vaker dan ons lief is, op PHP 5.6. Joomla 5 vereist PHP 8.1 of hoger. Die ene sprong raakt elke regel third-party code op de site. De zes valkuilen hieronder zijn degene die we tegenkomen, gerangschikt op hoeveel tijd ze kosten.

Valkuil één: de string-naar-getal vergelijking klapt om

Dit is degene die logins breekt zonder dat er iets crasht. In PHP 7 was de expressie "abc" == 0 waar, omdat PHP de string stilletjes naar 0 castte voor de vergelijking. In PHP 8 is dezelfde expressie onwaar. De semantiek van string-naar-getal vergelijkingen is veranderd in PHP 8.0 en die verschuiving rimpelde door in duizenden plugins.

Je ziet het in tokenchecks, rollenchecks en overal waar iemand een loose equality schreef tegen een string die uit de database komt.

// In an old Joomla 3 user-role helper
if ($user->access_level == 0) {
    // PHP 7: matched "public", "guest", any string
    // PHP 8: only matches integer 0 or string "0"
    return $this->deny();
}

Op PHP 7 liet de functie ingelogde gebruikers gewoon door, omdat hun access-level-string los vergeleken als 0. Op PHP 8 weigert ze hen nu. Het inlogformulier lijkt het wachtwoord te accepteren en stuitert direct terug. Geen error, geen logregel, geen aanknopingspunt.

Fix het door te grep'en op == 0, == "" en == null door elke third-party map onder components/, modules/ en plugins/. Cast expliciet, of stap over op ===.

Valkuil twee: verwijderde functies die oude extensies hard-coden

PHP 8.0 verwijderde each(), create_function() en de hele mcrypt_* familie. De eerste twee waren al deprecated in 7.2, maar genoeg Joomla 3 extensies hebben dat memo nooit gelezen. mcrypt zat vaak in encryptie-helpers binnen component-installers en license-check-stubs die je vanuit de admin niet eens ziet.

// Found in a 2016-era custom auth plugin
$cipher = mcrypt_encrypt(MCRYPT_RIJNDAEL_256, $key, $payload, MCRYPT_MODE_ECB);
$check  = create_function('$a,$b', 'return $a === $b;');
while (list($k, $v) = each($map)) { /* ... */ }

Op PHP 8 komt de parser niet voorbij regel één. Fatal error, wit scherm. De fix is mechanisch, maar moet voor elke extensie gebeuren, niet alleen die waar je om geeft. each() wordt een foreach. create_function wordt een closure. mcrypt wordt openssl_encrypt met een expliciete IV, en je gaat elke opgeslagen record opnieuw versleutelen. Dat is op zichzelf een avond werk.

Valkuil drie: dynamic properties triggeren een deprecation-stortvloed

PHP 8.2 markeerde het schrijven naar niet-gedeclareerde objectproperties als deprecated. Joomla 3 extensies deden dit in zo'n beetje elk model:

// Component model
$item = new stdClass();
$item->id      = $row->id;
$item->custom1 = $row->custom1;   // fine, stdClass is allowlisted

$model = JModelLegacy::getInstance('Article', 'ContentModel');
$model->customFlag = true;        // DEPRECATED in 8.2, fatal in a future version

stdClass en klassen met #[AllowDynamicProperties] zijn prima. Al het andere gooit een deprecation. De Joomla-core is gepatcht. De veertig extensies op de site van je klant niet. Logs ontploffen. De page load zakt in omdat de deprecation-handler op elke request draait. De fix is óf de properties op elke klasse declareren, óf het attribute toevoegen, en dat betekent dat je elke extensie aanraakt.

Valkuil vier: constructors in PHP 4-stijl

Code uit het Joomla 1.5 tijdperk gebruikte constructors met de naam van de klasse:

class MyComponentHelper {
    function MyComponentHelper() {
        // this was a constructor in PHP 4
    }
}

PHP 7 markeerde dit als deprecated. PHP 8 heeft het verwijderd. De methode draait nu als een gewone methode, wat betekent dat het object nooit initialiseert en je tien frames diep null-property reads krijgt. Symptoom: een extensie laadt, de pagina rendert leeg, de log zegt 'call to a member function on null' binnen code die sinds 2014 niet meer is aangeraakt.

Vind ze in één ripgrep-pas:

rg -nU 'class\s+(\w+)[^{]*\{[^}]*function\s+\1\s*\(' \
  components/ modules/ plugins/ libraries/

Hernoem ze allemaal naar __construct. De fix is vijf minuten zodra je ze gevonden hebt. Het vinden is het werk.

Valkuil vijf: string-access met accolades

Oude extensies grepen met accolades in strings:

// Slug generator from an SEF extension
for ($i = 0; $i < strlen($title); $i++) {
    $char = $title{$i};
    // ...
}

Verwijderd in PHP 7.4, harde error in 8.0. De site laadt niet. Vervang door blokhaken. Het loont om elke third-party library te grep'en, niet alleen die waar je een vermoeden hebt, want dit patroon duikt op in routers, taal-helpers en image-resize utilities.

Valkuil zes: de PHP 8.4 implicit-nullable valkuil voor late projecten

Je plande de migratie voor twee weken. Het werden er vier. Ondertussen rolde je hosting-provider de staging-server van PHP 8.2 naar 8.4. Nu zie je dit in zo'n beetje elke extensielog:

// Deprecated in PHP 8.4
function load(string $key = null) { /* ... */ }

// Correct in 8.4 and up
function load(?string $key = null) { /* ... */ }

De verandering staat gedocumenteerd in de implicitly nullable types RFC. Het is een deprecation in 8.4 en een fatal in 9.0. Het breekt de site vandaag niet, maar het vult logs tijdens een launch-window waarin je juist schone output nodig hebt.

Waarschuwing

Pin je staging PHP-versie op het productie-doel op de dag dat je begint met de audit. Een PHP point-bump midden in de migratie verandert een bekend setje fixes in een bewegend doel.

De audit die je niets kost

Voordat je een Joomla 3 naar 5 klus offreert, draai PHPStan op level 0 tegen de hele components/, modules/, plugins/ en libraries/ boom. Draai daarna Rector met de PHP_80, PHP_82 en PHP_84 sets in dry-run mode. De getallen die het print zijn je echte scope.

Ruwe ijking op basis van onze laatste twaalf Joomla-migraties:

  • Minder dan 50 issues over alle third-party code: dit is de weekendklus die je klant verwachtte.
  • 50 tot 500 issues: reken op één geconcentreerde week.
  • 500 of meer issues, of een verlaten commerciële extensie: reken op drie weken en voer het vervang-of-fork gesprek voordat je begint.

Het getal dat je niet wilt ontdekken nadat de aanbetaling binnen is, is het aantal verlaten extensies. Een extensie waarvan de ontwikkelaar in 2019 is gestopt met shippen krijgt geen Joomla 5 release. Je vindt óf een onderhouden vervanger, óf je forkt 'm en patcht zelf door, óf je herbouwt die feature op core Joomla 5. Alle drie zijn echt werk dat niet op de oorspronkelijke offerte staat.

Het gesprek dat het project redt

Toen we vorig kwartaal een legacy-migratie deden voor een Nederlandse uitgever op Joomla 3.10, veranderde de audit hierboven een geofferde 'tien dagen' in een eerlijke schatting van tweeëntwintig dagen, met twee daarvan apart gezet voor het vervangen van een stopgezette ACL-extensie. De klant ging akkoord met het hogere getal zodra ze de PHPStan-output zagen. Dat gesprek is een stuk makkelijker te voeren op maandagochtend dan op de avond van de lancering.

Als je vandaag één ding doet: draai vendor/bin/rector process --dry-run --config rector-php82.php tegen de third-party extensiemappen van je site. De samenvatting onderaan is je migratie-schatting, in uren.

Kern

Een Joomla 3 naar 5 upgrade is een vermomde PHP 7 naar 8 migratie. Audit je third-party code met PHPStan en Rector voordat je offreert, niet erna.

FAQ

Kan ik direct van Joomla 3 naar Joomla 5 springen?

Nee. Je migreert van Joomla 3 naar 4, en daarna van 4 naar 5. Elke stap heeft eigen databasewijzigingen en een compatibiliteitsronde voor extensies, en de tussenstap overslaan wordt niet ondersteund.

Welke PHP-versie kies ik op de nieuwe server?

PHP 8.2 is vandaag het veiligste productie-doel voor Joomla 5. 8.1 is de ondergrens. 8.4 laat third-party extensies nog struikelen op implicit-nullable parameters.

Hoe scope ik een extensie-audit voordat ik offreer?

Draai PHPStan op level 0 en Rector met de PHP_80, PHP_82 en PHP_84 sets in dry-run mode tegen components, modules, plugins en libraries. Het aantal issues is je scope in uren.

Wat als een commerciële extensie verlaten is?

Vind een onderhouden vervanger, fork de originele en patch 'm vooruit, of herbouw de feature op core Joomla 5. Alle drie horen op de offerte voordat het werk begint.

joomlamigrationphplegacy sitessecurityarchitecture

Iets bouwen?

Start een project