Magento
Magento 2.2 overnemen: 6.400 SKU's redden in Groningen
Een woonwinkel uit Groningen met 14 mensen geeft je SSH-toegang tot een Magento 2.2-server. Laatste commit uit 2019. Cron al 18 maanden dood. En nu?

Een retailer uit Groningen belt op een dinsdag. Hun boekhouder zoekt al drie weken een verdwenen order-export, de marketingmanager krijgt geen nieuwe productfeed naar Google Shopping gepusht, en configurable products tonen op de storefront soms €0. De laatste developer vertrok in 2022. Of we even konden kijken. We krijgen een SSH-key via Signal en loggen in.
Wat volgt is de triage die elke studio die aan legacy e-commerce werkt vroeg of laat doet. We deden vergelijkbare klussen voor woonaccessoires, verlichtingswinkels, B2B-onderdelenshops en een tegelimporteur. Het patroon rijmt. De details bijten. Hier ging het om Magento 2.2, veertien mensen in het team, 6.400 actieve SKU's en een cron die sinds november 2024 dood was zonder dat iemand het doorhad.
De eerste twintig minuten op de server
De shell prompt vertelt je het meeste wat je moet weten. PHP 7.2 op de app server. MySQL 5.7. Nginx ervoor. Eén VPS bij een Nederlandse host, geen staging, geen load balancer, geen read replica. bin/magento --version geeft 2.2.7. Die release kwam medio 2018 uit, en volgens het lifecycle-beleid van Adobe Commerce stopte Adobe in 2019 met security patches voor de hele 2.2-tak. Zeven jaar ongepatchte code is geen metafoor. Het is een CVE-lijst.
We doen een snelle check: composer show | grep magento. De output is half officiële packages, half een vendor-folder die iemand recht in app/code/ heeft gekopieerd zonder namespace-discipline. Een directory app/code/Local/Patches/ bevat 11 bestanden uit de jaren 2019 tot 2022, elk met een initiaal van een developer en een one-liner als commentaar. Een paar patchen de klasse Magento\Sales\Model\Order direct. Eén patcht Magento\Catalog\Model\Product\Type\Configurable om een prijsweergavebug te fixen. Geen ervan staat in version control.
We maken een snapshot voor we ook maar iets aanraken. Altijd. De eerste regel bij het overnemen van een legacy stack: het draaiende systeem is de enige documentatie die nog overeenkomt met de werkelijkheid, en breek je het, dan ben je die documentatie kwijt.
ssh root@host "tar czf /root/snapshot-$(date +%F).tar.gz \
/var/www/html /etc/nginx /etc/php /var/log && \
mysqldump --single-transaction --routines magento2 | \
gzip > /root/db-$(date +%F).sql.gz"
scp root@host:/root/snapshot-*.tar.gz ./incoming/
scp root@host:/root/db-*.sql.gz ./incoming/Tweehonderdtwaalf gigabyte aan media, voornamelijk productfoto's op volle DSLR-resolutie omdat niemand ze ooit had verkleind. Een database van 9 GB. Daarna gaan we lezen.
Wat de cron verborg
De eigenaar zei dat "sommige e-mails niet meer werkten". Dat is bijna altijd cron. In Magento 2 draait de hele indexing-, queue-, klantnotificatie- en stock-sync-pipeline op drie cron groups (default, index, consumers). Stopt cron, dan gooit niets een error. De shop blijft pagina's serveren. Orders blijven binnenkomen. Voorraad drift. Indexers raken stale. Orderbevestigingen stapelen zich op in de queue en verlopen stilletjes.
We checken crontab -l voor de user www-data. Leeg. We checken op een system cron in /etc/cron.d/. Ook leeg. Het commando magento cron:install was nooit gedraaid, of was bij een server-migratie weggepoetst. In plaats daarvan stond er één regel in de crontab van root die naar een wrapper script /opt/magento-cron.sh wees. Het script bestond. De laatste regel riep php /var/www/html/bin/magento cron:run aan. Op papier prima. In de praktijk was de php-binary in de PATH van root PHP 8.1, vorig jaar geïnstalleerd voor iets anders. Magento 2.2.7 weigert op PHP 8.1 te booten. Het script faalde sinds die upgrade elke minuut en schreef een stack trace naar een logbestand dat niemand las.
Als je een Magento-shop erft en de eigenaar zegt "sommige dingen werken gewoon niet meer", check dan eerst cron, daarna de PHP-binary in de PATH van root, en als derde de tabel cron_schedule. Die tabel vertelt je exact wanneer de stilte begon.
We queryen de tabel cron_schedule op de meest recente succesvolle run.
SELECT job_code, MAX(finished_at) AS last_success
FROM cron_schedule
WHERE status = 'success'
GROUP BY job_code
ORDER BY last_success DESC;De nieuwste rij met success was van 14 november 2024. Negentien maanden stille failure. De indexers hadden niet gedraaid. De queue met klant-e-mails was tot boven de 80.000 rijen gegroeid. De sitemap was sinds diezelfde novemberdatum niet meer geregenereerd, wat verklaarde waarom Google al ruim een jaar stilletjes product-URL's uit de index sloopte.
De handmatig gepatchte payment module
De retailer gebruikte een Nederlandse payment provider met iDEAL, Bancontact en SEPA. De officiële module had in 2020 een bekende bug die een race condition veroorzaakte bij gedeeltelijke terugbetalingen. De vorige developer fixte het door direct in de vendor-folder te editen. De fix werkte. Een jaar later draaide diezelfde developer composer update, wat stilletjes de helft van de patch overschreef maar niet alles. Tegen de tijd dat wij arriveerden werkte de payment flow voor nieuwe orders, brak bij gedeeltelijke terugbetalingen in zo'n 30 procent van de gevallen, en brak volledig wanneer een klant met SEPA betaalde voor een configurable product dat via een discount rule was gekocht.
Dit is het punt waar je gas moet terugnemen. Een gepatchte payment module is geen bug die je even "vooruit fixt". Het is een dragende muur. We deden drie dingen, in deze volgorde.
- We bevroren de gepatchte bestanden in git op een branch
archaeology/payment-2020, met een commit message die per regel beschreef welk symptoom werd geadresseerd (gebaseerd op git blame tegen de upstream vendor en de bug tracker van de payment provider). - We schreven een Cypress-test die de refund- en SEPA-failure-cases tegen een staging-kopie van de shop afspeelde, zodat we elke wijziging in minder dan drie minuten konden regression-checken.
- We mailden de developer support van de payment provider, stuurden de diff, en vroegen welke fixes upstream waren beland. Drie van de vijf wel. Twee niet, die stonden op hun backlog. We forkten de module, applieden de overige twee patches op de huidige upstream tag, pinden de fork in
composer.jsonvia een satis repo, en documenteerden de upgrade-trigger.
De hele exercitie kostte twee dagen. Twee goed bestede dagen. Het duurste wat je met een payment module kunt doen, is hem aanraken zonder een reproduceerbare test voor de failure case.
6.400 SKU's en geen bron van waarheid
De catalogus bleek het lastigste onderdeel. 6.400 actieve SKU's verdeeld over 84 categorieën, vier configurable product types, en een custom attribute set voor "made in"-landen. De retailer verkocht zo'n 200 van die SKU's ook via een Bol.com-integratie, nog eens 50 via een Duitse marketplace, en de rest alleen via de eigen storefront. Drie flat files hielden dit min of meer in sync: een nachtelijke CSV van hun groothandel, een handmatige Google Sheet voor marketing-bewerkingen, en een met de hand bewerkt JSON-bestand dat de homepage-carousels voedde.
Geen van de drie was de bron van waarheid. Elk had gedeeltelijke autoriteit over andere velden. De CSV bestuurde voorraad en inkoopprijs. De Google Sheet bestuurde productnaam, omschrijving en categorie-mapping. Het JSON-bestand bestuurde welke producten op de homepage stonden. Toen cron stierf, stopte de CSV-importjob, en de storefront dreef langzaam uit sync met de werkelijke magazijnvoorraad. Tegen de tijd dat wij arriveerden, waren 412 SKU's óf oververkocht óf verborgen op de storefront ondanks dat ze op voorraad waren.
We probeerden de sync-logica niet te fixen op het stervende Magento. In plaats daarvan bouwden we een klein Python reconciliation-script dat alle drie de bronnen, de live database en de magazijn-export las, en één CSV produceerde: drift.csv. De operations lead van de retailer kon dat in Numbers openen en per SKU zien waar de systemen het oneens waren. Dat rapport werd de eerste oplevering.
import pandas as pd
supplier = pd.read_csv("supplier_2026-06-05.csv", dtype=str)
sheet = pd.read_csv("marketing_sheet.csv", dtype=str)
magento = pd.read_sql(
"SELECT sku, qty, price, is_active FROM catalog_state", conn
)
drift = (
magento
.merge(supplier[["sku", "qty", "cost"]], on="sku",
how="outer", suffixes=("_mg", "_sup"))
.merge(sheet[["sku", "name", "category"]], on="sku", how="left")
)
drift["qty_mismatch"] = drift["qty_mg"].astype(float) != drift["qty_sup"].astype(float)
drift["missing_in_mg"] = drift["qty_mg"].isna()
drift.to_csv("drift.csv", index=False)412 mismatches op dag één. 38 op dag veertien. Op dat moment vertrouwde de retailer het rapport genoeg om het te gebruiken als catalogus-handoff voor het uiteindelijke replatform.
Patchen, forken of replatformen
Het eerlijke antwoord op "blijven we op Magento 2.2 of weggaan" wordt bijna nooit door de technologie bepaald. Het wordt bepaald door wie het antwoord kan onderhouden. Een retailer van 14 mensen met één marketingmanager en nul developers kan geen geforkte Magento onderhouden. Geen handmatig gepatchte payment module. En zeker geen nieuwe negentien maanden verlies aan een stille cron.
Wat ze wel konden permitteren was een roadmap. We splitsten het werk in drie buckets.
- Stabiliseer de draaiende shop. Cron terug op een gesuperviseerde PHP 7.4-binary, sitemap herbouwd, payment module geforkt en getest, image directory verkleind met één resize-pass, basic monitoring (Uptime Kuma plus een Slack-webhook voor cron job-failures) toegevoegd.
- Haal de data eruit. Reconciliation-script in productie, dagelijks drift-rapport, een schone export van de 6.400 SKU's en 84 categorieën naar een draagbaar schema. Order history geëxporteerd naar Parquet.
- Kies de bestemming. Voor deze retailer was het juiste antwoord een moderne headless commerce backend met een dunne storefront, geen één-op-één-upgrade naar Magento 2.4. De catalogus was klein genoeg dat een Magento-upgrade meer zou kosten dan schoner opnieuw beginnen.
De patch-val is reëel. Er staat een lange lijst ongepatchte Magento-kwetsbaarheden die in het wild nog steeds worden uitgebuit, waaronder de pre-auth RCE bekend als CVE-2022-24086 en de bypass CVE-2022-24087. Beide worden dagelijks door geautomatiseerde bots gescand en de 2.2-tak krijgt er nooit een officiële fix voor. Blijf je op een unsupported branch, dan "draai je geen Magento". Je draait een artefact. Er is geen upstream.
Hoe de redding er in de praktijk uitzag
Zes weken totaal. Twee dagen triage. Eén week stabiliseren (cron, payment module fork, image directory). Twee weken aan de reconciliation-pipeline. Drie weken plannen en starten van het replatform. De retailer bleef de hele tijd verkopen. Hun boekhouder had haar order-exports terug op dag drie.
De eerste taak als je een stervende shop erft, is niet iets fixen. Het is uitvinden wat gestopt is met werken en wanneer, zodat je de eigenaar de waarheid kunt vertellen over wat er eigenlijk draait.
Toen we voor deze klant het reddingsplan bouwden, was niet de code het langste werk. Het was de eigenaar overtuigen dat de gepatchte payment module een echt risico was, geen klein ongemak. We losten het op door hem de diff tussen zijn vendor-folder en de upstream tag naast elkaar te sturen, met de SEPA-edge case op video gereproduceerd. Hij tekende het replatform-contract de volgende ochtend. Dit is het werk dat we doen onder legacy migratie, en het oorlogsverhaal hierboven is er één van zo'n dertig vergelijkbare uit de afgelopen twee jaar.
Vermoed je dat je eigen Magento-installatie er zo bij ligt, dan is dit de audit van vijf minuten: SSH erin, draai bin/magento --version, check de laatste succesvolle rij in cron_schedule, en grep je vendor-folder op elk bestand dat recenter is aangepast dan je laatste composer install. Wat je daar vindt, is je echte startpunt.
Kern
Als je een legacy Magento-shop erft, is de eerste taak niet iets fixen. Het is precies uitvinden wat is gestopt met werken en wanneer.
FAQ
Hoe weet ik of mijn Magento-cron echt heeft gedraaid?
Query de cron_schedule-tabel op de meest recente rij met status = 'success'. Is die jongste success-rij meer dan een dag oud, dan staat je cron stil, ongeacht wat crontab -l laat zien.
Is het veilig om Magento 2.2 direct te upgraden naar 2.4?
Nee. De PHP- en database-vereisten tussen 2.2 en 2.4 verschillen genoeg dat je de upgrade via 2.3 moet faseren, en je moet bij elke stap elke third-party module testen. Reken op twee tot vier weken.
Wat doe ik als ik handmatig gepatchte bestanden in de vendor-folder vind?
Bevries ze in git op een aparte branch, schrijf een test die reproduceert wat de patch fixt, en beslis pas daarna of je de module forkt, de fix upstreamt of de patch helemaal verwijdert.
Magento houden of replatformen?
Beslis op basis van teamcapaciteit, niet van tech-voorkeur. Heb je geen in-house developer, dan kost een unsupported Magento-installatie je in twaalf maanden stilletjes meer dan een migratie.