Joomla
Joomla 3 naar 5: een vastgelopen federatiesite redden
Het vorige bureau was negen maanden bezig met de migratie. De staging stond op Joomla 4.4. Productie nog op 3.10. Op geen van beide werkte er iets.

De IT-verantwoordelijke van de bond belde op een dinsdag in februari. Het vorige bureau was negen maanden bezig met de Joomla-migratie. De staging-site stond op Joomla 4.4. Productie draaide nog op Joomla 3.10. Geen van beide werkte. Het competitieseizoen begon over zes weken, en 47.000 leden moesten kunnen inloggen, hun contributie betalen, en zien wie zaterdag hun wedstrijd zou fluiten.
Het Akeeba Backup Professional-abonnement was in 2022 verlopen. De custom scheidsrechtermodule was in 2014 geschreven door een developer die inmiddels was overleden. De MySQL-database was 11 GB en draaide op een shared host met een query-timeout van 30 seconden. Het vorige bureau had een Slack-kanaal vol halfafgemaakte branches achtergelaten en een Trello-bord met 84 kaarten in 'In Progress'.
Dit is de post-mortem van hoe we die bond op Joomla 5 hebben gekregen, wat we hebben weggegooid, en wat we hebben behouden. Het is geen nette geschiedenis.
Wat we aantroffen toen we de doos opentrokken
Dag één was lezen, geen code. We trokken een volledige mysqldump met --single-transaction, gzipten het bestand, en zetten het op een lokale Docker-stack. Daarna openden we het admin paneel van de staging-site en begonnen we te klikken. Binnen een uur hadden we een lijst van zeventien extensies die op staging laadden maar op productie niet, en elf die op productie laadden maar op staging niet. Het vorige bureau had dingen op staging geïnstalleerd om bugs te repareren die ze op productie nog niet eens hadden gereproduceerd.
De scheidsrechtermodule was waar iedereen bang voor was. Het was een mod_-extensie die drie custom tabellen las, joinde tegen de core #__users-tabel, en een weekoverzicht van aanstellingen renderde. De code bestond uit procedureel PHP 5.3-idioom met een mosLoadAdminMenus-aanroep er nog in. Die functie was in Joomla 1.6 verwijderd. De reden dat hij toch nog werkte: een polyfill die iemand, ergens, op enig moment in configuration.php had geplakt. We vonden hem op regel 412.
Als een verouderde Joomla-site 'gewoon werkt' maar je kunt niet uitleggen waarom, open dan eerst configuration.php en defines.php. Verborgen polyfills daarin breken stilletjes onder Joomla 4 en 5, en de fatal error die je dan krijgt lijkt op een extensiebug.
De drietrapsval
Het officiële pad van Joomla 3 naar 5 gaat in twee stappen: 3.10 naar 4.4 LTS, daarna 4.4 naar 5.x. Het vorige bureau had geprobeerd beide in één weekend te doen door de database direct te bewerken. De #__update_sites-tabel was met de hand aangepast. In de #__extensions-tabel stonden rijen voor componenten die niet meer op schijf bestonden. In de #__assets-tabel ontbrak de nested-set-integriteit waar Joomla's ACL op steunt.
We probeerden de kapotte staging-site niet te repareren. We gooiden hem weg. Het correcte pad, zoals Joomla's eigen migratiedocumentatie beschrijft, loopt via de Pre-Update Check in het Joomla Update-component. Die check weigert door te gaan zodra ook maar één geïnstalleerde extensie niet als J4-ready gemarkeerd staat. Het vorige bureau had die check omzeild door de database direct te bewerken. Wij begonnen met een verse kopie van productie en lieten de checker de waarheid vertellen.
De waarheid: van de 43 geïnstalleerde extensies hadden er 19 een J4-ready release, 11 waren door de maker opgegeven, 7 hadden betaalde upgrades nodig, en 6 waren custom gebouwd voor deze bond en hadden geen onderhouder.
Het Akeeba-probleem
Akeeba Backup Professional was de back-uptool van de bond. Het abonnement was in 2022 verlopen. De gratis Akeeba Backup Core release is prima voor de meeste sites, maar de bond gebruikte JoomlaPack-tijdperk profielen met gesplitste archieven, native S3-upload, en pre-flight database dumps die in de gratis release niet zitten. Zonder die opties zou een restore op een nieuwe host veertig minuten kosten, in plaats van de 6 minuten die het nu deed.
We deden het verstandige: we kochten een jaarabonnement op Akeeba Backup Professional op naam van de bond, updateten de extensie, en draaiden een volledige back-up voordat we ergens anders aan kwamen. Het bestuur keurde de factuur van €120 binnen twintig minuten goed. Negen maanden bureautijd had ze meer gekost dan dat in één ochtend aan standups.
Voordat je een vastgelopen migratie gaat debuggen, koop de licenties terug die het vorige team heeft laten verlopen. De jaarkosten van alle Joomla-extensie-abonnementen op een middelgrote site komen op zo'n €600 uit. Dat is één uur bureautijd.
De scheidsrechtermodule herschrijven
De scheidsrechtermodule was het enige stuk code dat niet vervangen kon worden door een kant-en-klare extensie. Daarin zaten de specifieke regels van de bond gecodeerd: een scheidsrechter mag geen wedstrijd fluiten voor een club waar hij ooit gespeeld heeft; senior-scheidsrechters mogen als eerste regionale finales kiezen; reisafstand boven de 80 km triggert een onkostenvergoeding. Elf jaar aan edge cases zat in één bestand van 1.400 regels.
We schreven hem opnieuw als Joomla 5-module met het moderne Joomla\CMS\Helper\ModuleHelper dispatch-patroon, met een service provider, een DI-injected database driver, en de regels eruit getrokken in een gewone PHP-klasse die we volledig buiten Joomla konden unit-testen.
<?php
namespace Federation\Module\Referee\Site\Helper;
use Joomla\CMS\Helper\ModuleHelper;
use Joomla\Database\DatabaseInterface;
final class RefereeHelper
{
public function __construct(
private readonly DatabaseInterface $db,
private readonly AssignmentRules $rules,
) {}
public function getAssignmentsForWeek(\DateTimeImmutable $week): array
{
$query = $this->db->getQuery(true)
->select($this->db->quoteName(['a.id', 'a.match_id', 'a.referee_id']))
->from($this->db->quoteName('#__federation_assignments', 'a'))
->where($this->db->quoteName('a.week_start') . ' = :week')
->bind(':week', $week->format('Y-m-d'));
$rows = $this->db->setQuery($query)->loadAssocList();
return array_filter($rows, fn(array $row) => $this->rules->isEligible($row));
}
}
De herschrijving kostte elf werkdagen. We hielden het oorspronkelijke databaseschema aan, omdat 47.000 leden er jaren aan aanstellingshistorie in hadden staan, en die data migreren was een risico dat we niet hoefden te nemen. Dezelfde tabellen, nieuwe code.
De database van elf gigabyte
De MySQL-database was uitgegroeid tot 11 GB. Twee derde daarvan was de #__session-tabel (Joomla 3 was nooit ingesteld om sessions te garbage-collecten) en de #__action_logs-tabel (elke login sinds 2017). Die rijen migreerden we niet. We truncateden #__session op de nieuwe instance en archiveerden #__action_logs ouder dan 18 maanden naar een cold-storage tabel die de bond kon raadplegen als er ooit een tuchtzaak speelde.
De daadwerkelijke leden-, club- en competitiedata waren 3,8 GB. Dat paste ruim onder de query-timeout van de nieuwe host. We zijn diezelfde week van de shared host af gegaan naar een managed VPS bij een Nederlandse provider, mede omdat Joomla 5 PHP 8.1+ vereist en de oude shared host op 7.4 stopte.
Het migratieweekend
De cutover vond plaats op een zaterdag tussen twee competitieweekenden in. Het plan besloeg vier pagina's bash en SQL. De repetitie had 4 uur en 12 minuten geduurd. De live run duurde 4 uur en 41 minuten, waarbij de overschrijding bijna helemaal opging aan wachten tot DNS bij één hardnekkige ISP was gepropageerd. We monitorden de nieuwe site 72 uur lang met een uptime-check elke vijf minuten en een synthetische login die de scheidsrechtermodule elk kwartier doorliep.
In de eerste week kwamen twee bugs binnen. De ene was een CSS-regressie in een third-party kalendercomponent. De andere was een missende vertaalstring in het Nederlandse taalpack. Geen van beide blokkeerde ook maar één scheidsrechteraanstelling.
Wat we de volgende bond zouden zeggen
Vastgelopen Joomla-migraties zien er bijna altijd hetzelfde uit. Een site die voorbij zijn end-of-life-datum nog op 3.10 hangt. Een bureau dat de tussenstap naar 4.x heeft willen overslaan. Een custom extensie waar niemand eigenaar van wil zijn. Een back-uptool waarvan de licentie verlopen is. De oplossing is zelden heroïsch. Het is de documentatie lezen, de halfkapotte staging-site weggooien, en budget vrijmaken voor de saaie dagen.
Toen we deze legacy-migratie overnamen, was waar we tegenaan liepen de aanname dat het werk van het vorige bureau een fundament was waar we op verder konden bouwen. Dat was het niet. We hebben het opgelost door productie als enige bron van waarheid te behandelen en vanaf dat punt het pad vooruit opnieuw op te bouwen.
Zit je deze week op een Joomla 3-site, dan is het kleinste wat je vandaag kunt doen: het admin paneel openen, naar Componenten → Joomla Update gaan, het kanaal omzetten naar 'Joomla Next', en lezen wat de Pre-Update Check zegt. Repareer nog niets. Lees alleen de lijst. Die lijst is de echte vorm van je migratie.
Kern
Vastgelopen Joomla-migraties zijn zelden een codeprobleem. Het zijn een documentatie-, licentie- en disciplineprobleem dat zich als codeprobleem voordoet.
FAQ
Hoe lang duurt een migratie van Joomla 3 naar 5 echt?
Voor een site met 30 tot 50 extensies en een matig aangepaste template kun je rekenen op zes tot tien werkweken, inclusief het herschrijven van elke custom extensie die ouder is dan vijf jaar.
Kun je Joomla 4 overslaan en direct van 3 naar 5 gaan?
Nee. Het Joomla Update-component vereist een tussenstap via 4.4 LTS. Probeer je dat te omzeilen door de database direct te bewerken, dan laat je de asset-tree en ACL in een inconsistente staat achter.
Wat doe je met een custom extensie waarvan de maker onbereikbaar is?
Trek de bedrijfsregels eruit in een gewone PHP-klasse, schrijf daar tests omheen, en wikkel die klasse vervolgens in een nieuwe Joomla 5-module. Houd het databaseschema waar mogelijk in stand, om het risico van datamigratie te vermijden.
Is Akeeba Backup Core voldoende voor een site op de schaal van een bond?
Voor sites onder een gigabyte: ja. Voor sites van meerdere gigabytes met gesplitste archieven, native S3-upload en strakke restore-tijden verdient het Professional-abonnement zichzelf bij de eerste snelle restore al terug.
Hoe ga je in een Joomla-migratie om met de session- en action-log-tabellen?
Truncate de session-tabel op de nieuwe instance. Archiveer action logs ouder dan 18 maanden naar een aparte tabel die je kunt raadplegen als een compliance-zaak dat vraagt. Migreer geen van beide tabellen integraal.