Joomla
Joomla naar Shopware: hoe één K2-veld 9.200 SKU's sloopte
Dag zeventien van een Joomla-naar-Shopware-freeze. Een Zaltbommelse meubelfabrikant staart naar een productpagina met 412 stofvarianten. Geen ervan verkoopt. Dit is waarom.

Dag zeventien van de freeze. De founder van een Zaltbommelse meubelfabrikant staat achter een dealerlaptop bij ons op staging. Op het scherm: een Shopware 6-productpagina van een made-to-order loungestoel. 412 variantopties. Allemaal kleurcodes. Niemand koopt iets.
Dit was geen Shopware-bug. Dit was een K2-extensie uit 2014 die met een importer uit 2026 praatte, dwars door elf jaar goedbedoelde aanpassingen heen.
De opdracht, kort
Op papier simpel. Weg van Joomla 2.5, end-of-life sinds eind 2014. Weg van PHP 5.6. VirtueMart 2 vervangen door Shopware 6. Vue Storefront ervoor, zodat de dealer-catalogus niet langer aanvoelt als een pdf uit 2009 in een browserframe. Alle 9.200 made-to-order-configuraties intact houden. Vijfentwintig mensen gebruiken dit elke dag, 180 dealers in de Benelux schieten orders erdoorheen, niet stuk maken.
De catalogus is het bedrijf. Elke stoel, bank en lounger heeft een base-SKU, een frame-optie, zes of zeven kussenopties, en één NCS-kleurcode per stofstaal. De kleurcodes zijn niet optioneel. De dealer kiest er één, de order genereert een snijlijst voor de naaikamer, de snijlijst gaat naar de laser. Verkeerde code, verkeerde stof, zes weken herwerk.
Wat we in de export vonden
De export uit VirtueMart 2 was redelijk netjes. Producten, varianten, prijzen, voorraad. Leesbare CSV, te verdedigen.
De stofdata zat alleen nergens waar we keken. Elf jaar geleden had iemand in het team een K2 item type gebouwd met de naam stofstaal. K2 was destijds dé content-construction kit voor Joomla 2.5, toen Joomla 3 nog scherpe randjes had. De dev had de stofmetadata in een custom K2 extra field gestopt. Eén veld per stofstaalset, maar de waarde was een string zoals deze:
NCS-S-1500-N\tNCS-S-2502-Y\tNCS-S-3010-R10B\tNCS-S-4040-G30Y…Tab-separated. Soms 4 codes, soms 60. Opgeslagen als één TEXT-veld in de tabel #__k2_attribs, in de K2-laag gewrapt in JSON, maar op rijniveau een tab-string.
Ergens aan de Joomla-kant parste een PHP 5.6-helper die string tijdens render-time en matchte elke code tegen een aparte VirtueMart custom field op het hoofdproduct. De dealer-catalogus-UI tekende vervolgens een raster van stofstalen. Hier was niets van gedocumenteerd. Het helperbestand heette stofstaal_v3_final_FINAL.php.
Hoe Shopware het veld at
Shopware 6 heeft een property-importer die voor variantdata grofweg het juiste doet. Je geeft 'm een product, een lijst property-waardes, en hij maakt varianten. Wij wezen hem op de gemigreerde export. Die export bevatte nog steeds het K2-veld, nog steeds tab-separated, want niemand had het migratiescript verteld dat de tabs "lijst van stofstaal-referenties" betekenden en niet "lijst van variantopties".
Shopware deed wat Shopware doet. Hij zag een multi-value field. Hij behandelde elk tab-stuk als een property-waarde. Voor een bank met 60 stofstalen leverde dat 60 kleurvarianten op, vermenigvuldigd met frame- en kussenopties. Eén product ontplofte tot ongeveer 14.000 variantrijen. De hele catalogus ontplofte tot 9.200 stukke configuraties en zo'n 1,7 miljoen variantrijen.
De Shopware-admin liep vast op de product-detailpagina. De Vue Storefront-PDP tekende een kleurraster zo groot als een klein zwembad. Dealers konden geen enkele order plaatsen.
De zeventien dagen
Dag 1 tot 3. We dachten aan een property-cleanup-probleem in Shopware. We schreven een migratie om property-waardes te dedupliceren, NCS-codes te normaliseren, whitespace samen te trekken. Niets structureels veranderde. De varianten bleven bestaan.
Dag 4 tot 6. We verdachten de importer-config. We bouwden de import-pipeline twee keer opnieuw, één keer direct via de entity repository, één keer via de Sync API. Dezelfde explosie, iets sneller.
Dag 7. We lazen eindelijk stofstaal_v3_final_FINAL.php. De helper had een comment rond regel 240:
// LET OP: tabs zijn pointers, geen waardes
stofstaal_v3_final_FINAL.php, 2014
Pas op: de tabs zijn pointers, geen waardes. Die comment kostte ons zes dagen.
Wat de data eigenlijk betekende
Een stofstaal-rij in K2 was geen kleur. Het was een verwijzing naar een fysiek stofstaal dat het bedrijf naar dealers stuurt. Elk monster heeft een NCS-code, een prijstier, een levertijd en een leverancier. De tabs in het veld waren een ingepakte referentielijst: "deze bank kun je maken met elke staal in deze set." De set was de entiteit, niet de codes.
In een schoon datamodel is de stofstaal een aparte resource en heeft een product een many-to-many-relatie met stofstalen. In het K2-model uit 2014 was de stofstaal een content item en was de relatie een tab-string in een attribs-veld. De Shopware-importer kan dat niet afleiden. Geen importer kan dat.
De fix
We splitsten de migratie in twee passes.
Pass één: stofstalen. We trokken elk K2 stofstaal-item over naar een Shopware custom entity genaamd swatch, één rij per NCS-code, met prijstier en leveranciersmetadata erbij. Custom entities zijn precies wat het klinkt: first-class objecten in het Shopware-datamodel die naast producten leven zonder de variant-as te vervuilen.
Pass twee: de koppeling. Elk product kreeg een many-to-many-associatie met stofstalen, in de admin zichtbaar als een tab "stofbeschikbaarheid". De Vue Storefront-PDP querde de associatie via de Store API en tekende het stofstaalraster client-side, met een kleurpicker die filterde op prijstier.
De variant-as bleef waarvoor die bedoeld was: frame- en kussenopties. Acht tot twaalf echte varianten per product, geen veertienduizend nepvarianten.
De extractor voor pass één zag er ongeveer zo uit. Versimpeld, maar dit is de vorm:
<?php
// joomla-side extractor, run once against the old DB
$pdo = new PDO('mysql:host=legacy;dbname=joomla25', $user, $pass);
$rows = $pdo->query("
SELECT i.id, i.title, a.value
FROM jos_k2_items i
JOIN jos_k2_attribs a ON a.itemID = i.id
WHERE i.catid = 17 -- stofstaal category
")->fetchAll(PDO::FETCH_ASSOC);
$swatches = [];
foreach ($rows as $r) {
$codes = preg_split("/\t+/", trim($r['value']));
foreach ($codes as $code) {
if (!preg_match('/^NCS-S-/', $code)) continue;
$swatches[$code] = [
'ncs_code' => $code,
'source_id' => (int) $r['id'],
'source_set' => $r['title'],
];
}
}
file_put_contents('swatches.json', json_encode(array_values($swatches), JSON_PRETTY_PRINT));Aan de Shopware-kant werd swatches.json daarna via de Admin API in de custom entity geïmporteerd, en een tweede script liep de producttabel door om uit dezelfde brondata de many-to-many-koppelingen op te bouwen.
Zodra de swatch-entity bestond, zag de property-importer kleurcodes niet meer als varianten, omdat we het veld volledig uit de product-import-payload hadden gesloopt. De explosie verdween. De variantaantallen daalden van 1,7 miljoen naar ongeveer 84.000 in de hele catalogus, het echte getal voor een made-to-order-lijn met 9.200 configuraties.
Wat we op dag één hadden moeten doen
Twee dingen.
Eén: een rendering-audit van één pagina. Voor je één rij migreert, lees je elke regel code die de legacy data aanraakt op weg naar een pixel. Niet de database. Niet de admin-UI. Het renderpad. Als een comment in een helperbestand zes dagen had bespaard, dan was dat helperbestand het belangrijkste bestand van het project, en wij lazen het pas op dag zeven.
Twee: een dry run met een steekproef van één. Pak het meest gecompliceerde product. Migreer alleen dat. Render het in de nieuwe stack. Ziet het er fout uit, dan heb je een probleem met één product. Migreer je eerst 9.200 en kijk je dan, dan heb je een probleem met het project.
Een notitie over Joomla in 2026
Joomla 2.5 wordt al meer dan tien jaar niet meer ondersteund. De eigen versie-matrix van het Joomla-project is hier sinds 2014 helder over. Joomla 4 en 5 zijn gezonde, moderne stacks. Het probleem is zelden Joomla zelf. Het probleem is het extensie-kerkhof uit 2014 dat erbovenop is gegroeid: K2, JCE, JomSocial, RokSprocket, Sourcerer, tientallen andere, half verlaten, half geforkt, allemaal met kritieke bedrijfsdata in velden die niemand documenteerde.
Draai je in 2026 nog Joomla 2.5 of 3.x met meer dan vijf third-party extensies, dan is jouw migratie geen Joomla-migratie. Het is een archeologieproject, en je moet het ook zo offreren.
Het lastigste deel van een legacy-migratie is niet de database. Het is de ongedocumenteerde PHP-helper die de database betekenis geeft.
Wat dit kostte, in platte getallen
Zeventien dagen projectvertraging. Ongeveer 90 uur debug-tijd die er niet had hoeven zijn. Eén rollback-oefening op een vrijdagavond die we niet hoefden te gebruiken, het enige goede nieuws in dat venster. De klant ging drie weken later live en schoon. Geen dealerorder ging verloren. De naaikamer kreeg nooit een verkeerde snijlijst. Dat noemen we de winst.
Toen we de dealer-catalogus voor deze Zaltbommelse fabrikant herbouwden, liepen we op tegen een CCK-veld dat loog over z'n eigen vorm. We losten het op door de renderingcode als canonical schema te behandelen, niet de database. Die gewoonte is nu dag één van elke legacy-migratie die we aannemen.
De audit van vijf minuten die je vandaag kunt draaien
Open je legacy-CMS. Zoek één custom field, attribs- of "extra"-kolom op je belangrijkste content type. Grep je codebase op de veldnaam. Lees elk bestand dat de grep teruggeeft. Wordt het veld ergens in render-code geparset, gesplit, geëxplodeerd of via regex gematcht, schrijf op wat de parser verwacht. Dat document is de spec die je migratie nodig heeft. Niets anders is dat.
Kern
Het lastigste deel van een legacy-migratie is niet de database. Het is de ongedocumenteerde PHP-helper die de database betekenis geeft.
FAQ
Kun je Joomla 2.5 direct naar Joomla 5 migreren?
Niet in één sprong. Het officiële pad loopt van 2.5 naar 3.10 naar 4.x naar 5.x. Met extensies uit 2014 in de mix is een schoon re-platform meestal goedkoper dan vier upgrades aan elkaar plakken.
Waarom Shopware 6 en niet Magento voor een B2B-dealercatalogus?
Het custom-entity-model en de B2B-suite van Shopware 6 regelen dealerspecifieke prijzen en many-to-many product-metadata zonder pluginwoekering. Magento kan het ook, maar de total cost of ownership ligt hoger.
Hoe voorkom je de variant-explosie bij import?
Strip niet-variantvelden volledig uit de product-import-payload. Is een waarde geen frame, maat of afwerking, modelleer hem dan als custom entity of property en koppel hem achteraf.
Wat deed K2 dat core Joomla in 2014 niet deed?
K2 voegde een configurable content-construction kit toe met extra fields, item types en categorietemplates, jaren voordat de Joomla-core dat inhaalde. Het is ook waar de meeste ongedocumenteerde legacy bedrijfsdata zich verstopt.