Magento
Magento naar Vendure: één serialized blob bevroor onze import
Een Nijmeegse tweedehands marketplace met 22 mensen, 6.800 luxe consignment-records en één serialized blob uit 2019 die een schone Vendure-import veranderde in twee weken speurwerk.

Het was donderdagmiddag in maart, tegen het einde van de dag. De derde importrun was net klaar. We refreshten de Vendure-admin en zagen onder Collections een boom die er zo uitzag:
a → 6 → {s:5 → "brand" → s:6 → "Hermès"
Zesduizend achthonderd van die dingen. Elk een luxe consignment-record dat een fantoomcategorie was geworden. De catalogus van de marketplace was door PHP's serialize() gedraaid en er weer uitgekomen als taxonomie.
Dit is het verhaal van hoe dat gebeurde, waarom we er veertien werkdagen aan kwijt waren om het terug te draaien, en de goedkope audit die we hadden moeten doen vóór de eerste import.
De marketplace
De klant is een tweedehands business met 22 mensen in Nijmegen. Het type bedrijf dat luxe tassen en designerkleding in consignatie aanneemt, elk stuk fotografeert en authenticeert en het vervolgens via de eigen storefront verkoopt. Ze draaiden al jaren op Magento 2.3 met PHP 7.4 eronder. Het inruil-portaal, de kant waar verkopers binnenkomen, was een eigen PHP-module die in 2019 bovenop Magento's EAV-laag was gebouwd.
EAV (entity-attribute-value) is Magento's manier om product-attributen toe te voegen zonder het schema aan te raken. Je voegt geen kolom toe aan catalog_product_entity; je schrijft een rij naar catalog_product_entity_varchar met de juiste attribute_id. Flexibel, traag, en de reden dat elke Magento-veteraan er minstens één stevige mening over heeft. Adobe ziet het nog altijd als de aanbevolen manier om productdata uit te breiden.
In 2019 schreef iemand, allang van het team af, een extensie om echtheidscertificaten per item bij te houden: merk, grade, naam verifier, datum, pad naar PDF, hologram-vlag. Zes velden. In plaats van zes EAV-attributen te schrijven, schreven ze er één, noemden het authenticity_certificate, en serialiseerden het hele record naar één PHP-blob die werd opgeslagen als text-attribuut. Het werkte. De storefront unserialised het bij het renderen. Zeven jaar heeft niemand het aangeraakt.
Waarom we migreerden
Magento 2.3 ging op 8 september 2022 end-of-life. De lifecycle policy van Adobe is glashelder: geen securitypatches, geen backports. De klant was voorbij EOL aan het hinken — daar staan ze niet alleen in; duizenden shops draaien nog op een Magento zonder support — maar het inruil-portaal was zo gegroeid dat de redactie het halve dagen doorbracht in een backend waar ze geen vat op hadden.
Vendure was logisch: TypeScript van top tot teen, een GraphQL-admin en shop-API, een aanpasbare admin-UI, en een SvelteKit-storefront die de eigen designer zelf kon onderhouden. Het plan was netjes. Productcatalogus exporteren, klanten, orders, consignment-records. Door Vendure's importpipeline halen. SvelteKit-storefront op een staging-URL zetten. Op zaterdag DNS omzetten. Drie weken, begroot.
Dag 11: een schone import
De eerste elf dagen verliepen zoals het voorstel beloofde. We schreven een Magento-naar-CSV-exporter die door de EAV-tabellen liep en de attributen van elk product in kolommen platsloeg. We mapten Magento-attributen één voor één naar Vendure custom fields. De eigen tabellen van het inruil-portaal — verkopers, items, uitbetalingen — kregen hun eigen Vendure-plugin met fatsoenlijke entities.
De import draaide. Achtduizend vierhonderd producten. Vijfduizend klanten. Veertigduizend orders. We trokken willekeurig een sample van vijftig producten, vergeleken elk met de live Magento-shop, vonden geen discrepanties en gingen tevreden over onszelf het weekend in.
Dag 14: de categorieën die de dinsdag opaten
Dinsdagochtend logde de category manager van de klant in om de nieuwe homepage in te richten. Ze stuurde ons een screenshot. Onder Tassen had ze een sub-collectie gevonden met de naam a. Onder a zat een sub-collectie 6. Onder 6 stond {s.
De categorieboom van Vendure was geïnfecteerd. Zesenhalfduizend collecties, allemaal genoemd naar brokken PHP-serialize()-syntax, twintig niveaus diep genest. De echte categorieën stonden er ook nog. Alleen begraven.
Zo zag het authenticity_certificate-attribuut er in Magento uit voor één Hermès Birkin-record:
a:6:{s:5:"brand";s:6:"Hermès";s:5:"grade";s:2:"AA";s:11:"verified_by";s:11:"GoVerify NL";s:9:"verified";s:10:"2019-04-12";s:13:"cert_doc_path";s:32:"/var/certs/2019/HRM-449201.pdf";s:8:"hologram";b:1;}
En zo zag de regel in de mapping van onze exporter eruit die het aan Vendure aanleverde:
// vendure-csv-mapping.ts
export const mapping = {
name: { column: "name" },
sku: { column: "sku" },
price: { column: "price", currency: "EUR" },
collections: { column: "categories", separator: "|", pathSeparator: ">" },
assets: { column: "images", separator: "|" },
};
De CSV-importer splitst de collections-kolom op pipes en behandelt elk fragment als een categoriepad, met > als padscheidingsteken. De serialized blob bevatte geen pipes. Wel dubbele punten, puntkomma's, accolades en gequote strings, en de path-parser van Vendure tokeniseerde ze vrolijk. Elke dubbele punt werd een niveau. Elke top-level string een sibling.
We hadden de importer-mapping gereviewd. We hadden de exporter gereviewd. We hadden niet gezien dat authenticity_certificate óók in de category-id-kolom van het product werd geschreven door een oude data-quality cron die elke nacht in Magento liep om certificaat-metadata te "normaliseren". Onze exporter vertrouwde op Magento's category-assignment. Magento wees sinds 2019 stilletjes elk consignment-item toe aan een "categorie" die feitelijk een serialized blob was. De storefront las het nooit. De Magento-admin filterde het uit de boomweergave. Het was onzichtbaar tot we iets anders vroegen om het te lezen.
Als jouw CSV-importer een categories-kolom accepteert en je bronsysteem ooit cron jobs heeft gehad die categorieën automatisch toewijzen: dump die kolom en lees elke waarde voordat de importer hem aanraakt. Geen sample. Elke waarde.
De fix
We deden drie dingen, in deze volgorde.
Eén: de blob parsen naar getypeerde kolommen
We schreven een kleine PHP CLI die de ruwe waarde las voor elk product met een niet-lege authenticity_certificate, daar unserialize() op losliet en zes schone kolommen uitspuugde. PHP's unserialize is goed gedocumenteerd en deterministisch — het enige goede aan dit hele verhaal.
#!/usr/bin/env php
<?php
declare(strict_types=1);
$pdo = new PDO('mysql:host=localhost;dbname=magento', $u, $p);
$rows = $pdo->query(
"SELECT entity_id, value
FROM catalog_product_entity_text
WHERE attribute_id = 247
AND value IS NOT NULL"
);
$out = fopen('certificates.csv', 'w');
fputcsv($out, ['sku','brand','grade','verified_by','verified_at','cert_path','hologram']);
foreach ($rows as $r) {
$cert = @unserialize($r['value'], ['allowed_classes' => false]);
if (!is_array($cert) || count($cert) !== 6) {
error_log("quarantine: {$r['entity_id']}");
continue;
}
fputcsv($out, [
sku_for($r['entity_id']),
$cert['brand'] ?? '',
$cert['grade'] ?? '',
$cert['verified_by'] ?? '',
$cert['verified'] ?? '',
$cert['cert_doc_path'] ?? '',
!empty($cert['hologram']) ? '1' : '0',
]);
}
We vertrouwden de data niet. We valideerden dat elke rij decodeerde naar een array met de verwachte zes keys, logden 47 gedeeltelijke rijen voor handmatige review en zetten ze in quarantaine. Drieënveertig bleken uit een bulk-edit uit 2020 te komen die lange verifier-namen had afgekapt; vier waren echt corrupt en die heeft het authenticatieteam van de klant met de hand gereconstrueerd uit het papieren dossier.
Twee: de echte categorieboom met de hand opnieuw opbouwen
Apart daarvan exporteerden we de werkelijke categorieboom uit catalog_category_product, gejoind met catalog_category_entity_varchar voor de namen. Zes top-level categorieën. Vierentwintig sub-categorieën. Niets exotisch. We vergeleken de boom met de live storefront in een videocall met de category manager van de klant. We mapten elke Magento-categorie op naam aan een Vendure Collection en wezen producten toe via het normale mechanisme van de importer — met de collections-mapping gericht op een kolom die we zelf uit catalog_category_product genereerden, niet op het vervuilde categories-attribuut.
Drie: het certificaat opslaan waar het hoort
Echtheidscertificaten zijn geen taxonomie. Het is metadata per variant. In Vendure betekent dat een custom field group op de ProductVariant-entity met zes getypeerde velden, plus een kleine admin-UI-extensie om ze te tonen.
// src/vendure-config.ts
customFields: {
ProductVariant: [
{ name: 'certBrand', type: 'string', label: [{ languageCode: LanguageCode.en, value: 'Brand' }] },
{ name: 'certGrade', type: 'string', label: [{ languageCode: LanguageCode.en, value: 'Grade' }] },
{ name: 'certVerifiedBy', type: 'string', label: [{ languageCode: LanguageCode.en, value: 'Verified by' }] },
{ name: 'certVerifiedAt', type: 'datetime', label: [{ languageCode: LanguageCode.en, value: 'Verified at' }] },
{ name: 'certDocPath', type: 'string', label: [{ languageCode: LanguageCode.en, value: 'Certificate PDF' }] },
{ name: 'certHologram', type: 'boolean', label: [{ languageCode: LanguageCode.en, value: 'Hologram present' }] },
],
},
Toen draaiden we de import een vierde keer. Achtduizend vierhonderd producten. Zes top-level categorieën. Vierentwintig sub-categorieën. Zesduizend achthonderd getypeerde certificaat-records op de varianten die ze nodig hadden. Nul fantoomcollecties.
Wat we anders zouden doen
Lees de EAV, tel hem niet alleen
Onze pre-migratie-audit telde attributen en somde extensies op. Hij las geen attribuutwaardes. Een query van twee minuten had het eruit gehaald:
SELECT LEFT(value, 6) AS prefix, COUNT(*) AS n
FROM catalog_product_entity_text
WHERE attribute_id = 247
GROUP BY 1
ORDER BY n DESC
LIMIT 20;
a:6:{ had bovenaan dat resultaat gestaan met 6.800 rijen eronder en we hadden het op dag één geweten.
Diff gestratificeerd, niet willekeurig
Onze steekproef van dag 11 gebruikte vijftig willekeurige producten. Geen daarvan was een consignment-item, omdat consignment-items een gegroepeerde SKU-prefix hadden en onze random sampler, met alle goede bedoelingen, toevallig niet uit dat bereik trok. Consignment-items waren 81% van het catalogusvolume en 12% van de SKU-prefix-spreiding. Na dag 14 vereist onze acceptatiecheck sampling gestratificeerd per categorie en per attribute set, niet per rij.
Laat één importer-pass niet twee dingen tegelijk doen
Onze exporter had de categories-kolom leeg moeten laten, en we hadden de categorie-mapping als een aparte, kleinere, handmatig gereviewde pass moeten draaien. In één stap voelde elegant. Elegant kostte ons twee weken.
De kosten
We overschreden het budget met veertien werkdagen. De klant nam de helft op zich; de andere helft schreven we af. De storefront ging acht weken later live dan gepland. Het inruil-portaal, herbouwd als een kleine Vendure-plugin met zijn eigen GraphQL-oppervlak, was er vóór de nieuwe storefront en draaide de hele migratieperiode parallel zonder incidenten.
Niemand is ontslagen. De developer die in 2019 de blob-extensie schreef, zat niet meer in het team. De serialize()-beslissing was redelijk voor de constraints van toen: lever het certificaat-feature af in een sprint, draai geen schema-migratie. De kosten kwamen zeven jaar later naar boven, op de planning van iemand anders.
Toen we het inruil-portaal voor deze Nijmeegse marketplace herbouwden, was het ding waar we steeds tegenaan liepen: impliciete categorie-toewijzingen door cron jobs die niemand zich herinnerde geschreven te hebben. We hebben dat opgelost door op elk text-attribuut een distinct-prefix-query te draaien voordat de importer de data ook maar aanraakte — het type audit dat we inmiddels in elke legacy-migratie standaard inbouwen.
Als je een Magento-shop hebt waarvan je overweegt te migreren: draai die LEFT(value, 6)-query vanmiddag tegen catalog_product_entity_text en lees de bovenste twintig rijen. Het antwoord is informatie die je zes maanden geleden al had willen hebben.
Kern
Voordat een CSV-importer een verouderde Magento-catalogus aanraakt: draai een distinct-prefix-query op elk text-attribuut. Vind de blob voordat hij jouw categorieboom vindt.
FAQ
Wat is een EAV-blob in Magento?
EAV is Magento's entity-attribute-value patroon om product-attributen toe te voegen zonder schemawijzigingen. Een blob-waarde is een kolom die gestructureerde data — JSON, serialized PHP, XML — in één veld propt.
Waarom las de Vendure-importer het certificaat als categorie?
Een oude Magento cron job had het certificaat-attribuut automatisch toegewezen aan de category-kolom van het product. Onze exporter vertrouwde die mapping, en de CSV-importer splitste de serialized string op de dubbele punten en bouwde per fragment een geneste collectie.
Hoe lang moet een migratie van Magento 2 naar Vendure duren?
Voor een catalogus met één storefront onder de 10.000 producten en een schone attribute set is drie tot vijf weken realistisch. Tel er een week bij op voor elke custom EAV-extensie en meer voor consignment- of marketplace-logica.
Is Magento 2.3 in 2026 nog veilig om te draaien?
Nee. Magento 2.3 ging op 8 september 2022 end-of-life en kreeg sindsdien geen securitypatches meer. Shops die nog op 2.3 staan zijn blootgesteld aan bekende CVE's die Adobe niet meer backport.