← Blog

Magento

Magento 1.9 klantexport: de serialized-array-valkuil

De onderdelen-shop van een Hengelose machinebouwer liep elf dagen vast omdat een gepagineerde Magento 1 EAV-exporter stil een serialized PHP-array liet vallen vanaf de tweede batch.

Jacob Molkenboer· Oprichter · A Brand New Company· 11 feb 2026· 10 min
Half-open leren grootboek met messing label, doorslagfactuur, groen tabblad en lakstompje op ivoorlinnen, donkergroene achtergrond.

Het eerste signaal was een mail van een machinemonteur in Vlaardingen om 09:14 op een dinsdag. Hij kon het 18mm M-serie flensbeugel STEP-bestand niet meer downloaden dat hij de maand daarvoor twee keer had opgehaald. Om 10:00 stonden er nog veertig van dezelfde soort in de support-inbox. Allemaal B2B-accounts. Allemaal met een CAD-licentierecht op hun klantprofiel. Allemaal voor een Medusa-storefront die we de vrijdag ervoor live hadden gezet.

De shop is van een Hengelose machinebouwer met 26 man personeel. Hun onderdelen-shop draait sinds 2015 op Magento 1.9, vastgeplakt aan een custom PHP 7.0 entitlement-service die downloads van SolidWorks Toolbox-onderdelen, STEP-exports en een 9 GB DWG-bibliotheek afschermt. We waren elf dagen onderweg in een migratie naar Medusa + Astro. Elf dagen die er drie hadden moeten zijn.

De stack die we aantroffen

Magento 1 ging in juni 2020 end-of-life. Adobe stopte met security-patches. De community houdt het aan de praat met OpenMage LTS, maar de instance van onze klant was verder ontspoord dan dat: hun vorige integrator had Magento 1.9.3.10 in 2018 geforked en was gestopt met upstream pullen. PHP 7.0, drie majors achter. MySQL 5.6. Een CAD-entitlement-module van 2.000 regels observer chain vastgeschroefd op customer_save_after, die een serialized PHP-array schreef in een custom klantattribuut genaamd cad_entitlements met deze vorm:

a:3:{
  s:8:"toolbox";a:2:{i:0;s:7:"din-933";i:1;s:7:"din-934";}
  s:5:"step";b:1;
  s:7:"expires";s:10:"2027-03-31";
}

4.600 B2B-accounts in de database. 1.140 daarvan hadden een niet-lege cad_entitlements-waarde. De rest waren quote-only klanten die nooit een download deden. Die verhouding doet ertoe voor wat hierna komt.

De migratie die we planden

Medusa slaat klant-metadata op als JSON-kolom op de customer-tabel. Het mappen van de serialized PHP-array naar JSON is mechanisch: unserialize() eruit, json_encode() erin. We spiegelden de entitlement-check-logica naar een Medusa-workflow die liep bij order placement en op een aparte /api/store/cad/download/[sku]-route in Astro.

Voor de export deden we wat elke Magento-integrator doet: we grepen naar n98-magerun. Voor 4.600 rijen wil je niet de kale customer:list CSV-dump — je wil een custom script dat haakt in Mage_Customer_Model_Resource_Customer_Collection en addAttributeToSelect('*') gebruikt. Zo'n script hadden we in onze migratie-toolkit uit een project van 2023. Het pagineerde 1.000 rijen per batch. Het had op elke Magento 1-export gewerkt die we eerder hadden gedraaid.

De dry-run op staging duurde acht minuten. Rij-aantallen kwamen overeen. We zetten vrijdagavond DNS om, stuurden zaterdagochtend de welkomstmail, en gingen naar huis.

De inbox op dinsdag

Het eerste wat we deden was het verkeerde. We namen aan dat de entitlement-check-workflow aan de Medusa-kant een bug had. We zijn een halve dag door de Node-code gestapt, schreven fixture-tests en deployden een debug-logger die de geparste metadata-vorm printte bij elke download-poging.

De metadata-vorm was leeg. Niet misvormd. Niet corrupt. Leeg. Voor 1.140 accounts. De andere 3.460 klanten hadden hun entitlement-objecten intact.

Dat aantal — 1.140 — kwam te schoon overeen met het aantal niet-lege entitlements in de bron-database om toeval te zijn. De bug zat aan de export-kant, en die zat er sinds vrijdagochtend in.

De drop reproduceren

We draaiden het export-script in een verse Docker-container tegen een kopie van de productie-database. We voegden per-batch logging toe: rijen erin, rijen eruit, hit-count per attribuut. Het patroon was meteen duidelijk:

batch 1 (offset 0, limit 1000):    cad_entitlements present in 312 rows
batch 2 (offset 1000, limit 1000): cad_entitlements present in 0 rows
batch 3 (offset 2000, limit 1000): cad_entitlements present in 0 rows
batch 4 (offset 3000, limit 1000): cad_entitlements present in 0 rows
batch 5 (offset 4000, limit 600):  cad_entitlements present in 0 rows

312 van de 1.140. De eerste batch was de enige batch waarin het attribuut überhaupt verscheen. De overige 828 entitlements waren stil weggevallen, en onze rij-aantal-diff tegen customer_entity had het niet gevangen, omdat de klantrijen wel bestonden — alleen één kolom op elke rij was leeg.

Let op

Rij-aantal-diffs beschermen je niet tegen dataverlies op attribuut-niveau in een EAV-systeem. Elke kolom heeft zijn eigen count nodig, voor en na.

Wat er werkelijk gebeurde

De EAV-loader van Magento voor klantcollecties cached attribuut-metadata op de collectie-instantie. Wanneer je addAttributeToSelect('*') aanroept, loopt de loader door het klant-entiteitstype, resolved elk attribuut in de relevante attribute set en bouwt een join-plan. cad_entitlements zat op een niet-standaard attribute set die de vorige integrator in 2018 specifiek had aangemaakt om B2B-accounts te markeren.

Bij de eerste batch maakte de paginator een verse collectie aan, het join-plan bevatte cad_entitlements, en de 312 B2B-accounts in offset 0–999 kregen hun data mee. Bij de tweede batch hergebruikte de paginator het collectie-object over setPageSize()-aanroepen heen. Het gecachte join-plan was vastgepind op een non-B2B-rij die laat in batch 1 was binnengekomen. De attribute set op die gecachte rij verschilde van de attribute set op elke rij in batches twee tot en met vijf. De loader liet de join stilletjes vallen.

Dit is geen bug in n98-magerun — de eigen commands van n98-magerun maken een verse collectie aan per invocation en omzeilen de val. De val zit in elk custom script dat een Magento 1 EAV-collectie over pagina's heen vasthoudt. De scherpe rand staat indirect beschreven in Adobe's EAV-documentatie voor Magento 2, waar het attribute-set-gedrag precies om die reden is herzien, omdat het Magento 1-model state liet lekken tussen rijen.

De fix

We gooiden de collectie-gebaseerde exporter weg en schreven een directe SQL-extract:

SELECT
  ce.entity_id,
  ce.email,
  cev.value AS cad_entitlements
FROM customer_entity ce
LEFT JOIN customer_entity_text cev
  ON cev.entity_id = ce.entity_id
  AND cev.attribute_id = (
    SELECT attribute_id FROM eav_attribute
    WHERE attribute_code = 'cad_entitlements'
    AND entity_type_id = (
      SELECT entity_type_id FROM eav_entity_type
      WHERE entity_type_code = 'customer'
    )
  )
ORDER BY ce.entity_id;

De result set was 4.600 rijen, 1.140 met een non-null cad_entitlements. Identiek aan het bron-aantal dat we inmiddels in rode stift op de muur hadden staan. We pijpten het door een Node-script dat de PHP-array per rij unserialized, valideerde tegen een Zod-schema, en de Medusa-vormige JSON uitvouwde. Geen paginatie. Geen EAV-collectie. Geen attribute-set state om te laten lekken.

Opnieuw importeren duurde 22 minuten. De monteur in Vlaardingen kreeg zijn STEP-bestand diezelfde middag terug. De CFO in Hengelo nam het beter op dan we hadden verwacht.

De audit die we op dag één hadden moeten draaien

Voor elke productie-cutover draaien we nu een non-null-diff per attribuut. Voor Magento 1-klanten betekent dat deze query op de bron-database, en de equivalente JSON-key-count op de bestemming:

SELECT
  ea.attribute_code,
  COUNT(cev.value_id) AS non_null_count
FROM eav_attribute ea
LEFT JOIN customer_entity_text cev
  ON cev.attribute_id = ea.attribute_id
WHERE ea.entity_type_id = (
  SELECT entity_type_id FROM eav_entity_type
  WHERE entity_type_code = 'customer'
)
GROUP BY ea.attribute_code
ORDER BY non_null_count DESC;

Daarna dezelfde query tegen customer_entity_varchar, customer_entity_int, customer_entity_datetime en customer_entity_decimal. De hele audit duurt dertig seconden. We hadden hem overgeslagen omdat de rij-aantal-diff voldoende voelde. Dat was hij niet.

Een kanttekening bij waar CAD-onderdelenportalen heen gaan

De stille ironie van elf dagen besteden aan het repareren van CAD-entitlement-plumbing in 2026 is dat de upstream onder de voeten van onze klant verschuift. De eerste golf AI-ondersteunde CAD-tooling wijst op een toekomst waarin het artefact dat een B2B-onderdelenportaal serveert minder vaak een statisch STEP-bestand is en vaker een parametrisch model dat de klant in de browser regenereert. Het entitlement-schema van onze klant — toolbox-onderdeelnummers per account afgeschermd — gaat die verschuiving niet ongewijzigd overleven. We hebben het ze verteld. Ze kijken er in 2027 opnieuw naar.

Wat je vandaag kunt doen

Zit je op een Magento 1-shop met custom klant- of productattributen, neem dan dertig seconden en draai de non-null-count per attribuut hierboven. Bewaar het resultaat ergens waar je migratieteam het kan vinden. Als de export-tooling draait, draai dan dezelfde query tegen de bestemming en diff. Dat is de goedkoopste verzekering die je ooit op een legacy-migratie afsluit.

Toen we de export-runner bouwden voor de onderdelen-shop van de Hengelose machinebouwer, was wat we tegenkwamen precies deze attribute-set blind spot in de EAV-collection loader. We hebben het uiteindelijk opgelost met een SQL-first extract en een per-attribuut diff die nu meegaat met elke legacy-migratie die we draaien.

Kern

Rij-aantal-diffs detecteren geen verlies op attribuut-niveau in een EAV-systeem. Elke kolom heeft zijn eigen non-null-count nodig, voor en na de cutover.

FAQ

Waarom exporteerde n98-magerun de klantrijen wel, maar liet het custom attribuut vallen?

n98-magerun zelf was niet de schuldige. De drop zat in een custom gepagineerde exporter die een Magento 1 EAV-klantcollectie over pagina's heen hergebruikte. Het gecachte attribute-set join-plan liet state lekken tussen batches.

Kan dezelfde EAV-attribute-set-bug ook in Magento 2 optreden?

Magento 2 heeft het attribute-set-gedrag op klantcollecties herzien, dus de specifieke Magento 1-val reproduceert niet. EAV blijft EAV, dus per-attribuut non-null-diffs blijven het juiste vangnet.

Wat is de veiligste manier om Magento 1-klant-EAV-data te exporteren?

Omzeil de collection loader en lees direct uit customer_entity plus de customer_entity_varchar/text/int/datetime/decimal-tabellen, gejoind op attribute_id. Paginatie heeft dan geen state om te laten lekken.

Hoe migreer je serialized PHP-arrays naar een JSON-kolom?

Unserialize elke rij in een kort script, valideer de vorm tegen een schema zoals Zod of JSON Schema, en daarna json_encode en insert. Validatie doet er meer toe dan de encode-stap, omdat legacy modules inconsistente vormen wegschrijven.

Moet je in 2026 nog Magento 1 draaien?

Nee. Magento 1 heeft sinds juni 2020 geen first-party security-patches meer gehad. Als een volledig re-platform niet haalbaar is, is OpenMage LTS de minst slechte tussenoplossing terwijl je de move plant.

magentomigrationphpmysqlcase studye-commerce

Iets bouwen?

Start een project