← Blog

Drupal

Drupal 7 naar WordPress: een migratie die elf dagen stilstond

Elf dagen migratie-stilstand door één serialized PHP-array. De exporter draaide schoon, de import zag er goed uit, en de leadrouting deed niks.

Jacob Molkenboer· Oprichter · A Brand New Company· 6 dec 2025· 11 min
Open leren logboek met groen lint, koperen sleutel op kaart, gebroken zegel, rode stempel, inktkussen op ivoor bureau.

De klok op de monitor van de verkoop-binnendienst in Brugge stond op 17:48 op een dinsdag in maart, toen de inside-sales lead één droge boodschap naar haar projectmanager stuurde. "Vier dagen, geen enkele offerte-aanvraag." Vier dagen, nul offerte-aanvragen die via de nieuwe website bij haar team binnenkwamen. Het formulier stond zichtbaar op de homepage. De verzendknop werkte. Testinzendingen kwamen aan in de catch-all inbox. Echte inzendingen, van echte bezoekers, verdwenen. De site draaide negen jaar op Drupal 7. Sinds twee dinsdagen terug op WordPress.

Wij hadden de migratie twintig dagen eerder overgenomen van een ander bureau dat zonder budget en zonder geduld zat. De briefing op papier was simpel: zet een Drupal 7-site voor een bouw-installateur met 27 medewerkers over naar WordPress 6.5 voordat Drupal 7 end of life ging, behoud de SEO, behoud de leadintake. Het formulier op de homepage had ongeveer 3.800 historische offerte-aanvragen vastgelegd en routeerde de live aanvragen via 1.240 conditional branches naar acht queues van de verkoop-binnendienst, op basis van postcode, type installatie, en een verborgen veld dat aangaf of de bezoeker een particulier was of een bouwbedrijf.

De export die schoon leek

Het vorige bureau had een bekende Drupal-naar-WordPress exporter gebruikt. De dry run gaf nette WXR-bestanden. De staging-omgeving zag er goed uit. Het live formulier renderde zonder fouten. Het overdrachtsdocument zei "Webform-migratie compleet, velden geverifieerd". We controleerden de velden. We controleerden de rendering. We controleerden de redirect na submit. Alles voldeed.

Wat we niet hadden gecontroleerd, omdat het niet bij ons opkwam om dat te controleren, was of de conditional logic tussen de velden de overstap had overleefd. De velden stonden er. De labels stonden er. De required-vlaggen stonden er. De als-dit-dan-toon regels die bepaalden welke verkoper de lead zou zien, waren stilletjes weggepoetst.

Dus het formulier werkte. Elke inzending triggerde precies één routing-branch: de default. De default-bestemming was de catch-all inbox die niemand van de verkoop-binnendienst in de gaten hield, want de afgelopen negen jaar kreeg die inbox alleen fallback-ruis binnen. Het team bleef hun benoemde queues volgen. De benoemde queues bleven leeg. Vier dagen aan leads, weg.

Elf dagen valse starts

Dag één zaten we binnen WordPress. We controleerden de logica-editor van de formplugin, de e-mailrouting-regels, de notificatie-instellingen, de SMTP-logs. De conditional rules ontbraken volledig. We gingen uit van configuratie-drift, bouwden drie sample-branches met de hand opnieuw op, zagen ze correct vuren, en voelden ons veertig minuten lang slim.

Op dag twee verbreedden we de zoekopdracht. We trokken SMTP-logs uit de catch-all inbox: 47 inzendingen over vier dagen, allemaal auto-gerouteerd naar de default-bestemming, geen één naar een benoemde queue. We diften elke tabel die de exporter had aangeraakt tegen een staging-snapshot om te bevestigen dat er niks anders stilletjes was herschreven. De conditional rules zaten niet in een neventabel. Ze zaten niet in een transient. Ze zaten ook niet in postmeta onder een rare sleutel. Ze waren simpelweg nooit weggeschreven.

Toen openden we de spreadsheet met branches die de klant ons had gegeven. 1.240 rijen. Elke rij droeg twee tot vijf condities, een output-queue, en in ruwweg een derde van de gevallen een verborgen tag die naar het CRM ging. Handmatig herbouwen was geen oplossing. Het was een klus van zes weken die geprijsd was als een tweeweekse, betaald uit eigen zak omdat we de migratie als vaste prijs hadden geoffreerd.

Die spreadsheet had zelf ook een geschiedenis. Hij lag begraven in een mailthread uit maart 2022 tussen het vorige bureau en de inside-sales lead, op dag twee naar ons doorgestuurd met een verontschuldigend bericht dat de klant had aangenomen dat iedereen in de keten hem al had. Wij hadden hem niet. Het vorige bureau had hem als referentiemateriaal voor de verkoop-binnendienst behandeld, niet als migratie-artefact, en had hem tijdens hun poging nooit geopend. Tot we erom vroegen, hadden wij dat ook niet.

Op dag drie gingen we terug naar de Drupal 7-database. Het plan was rechttoe rechtaan: trek de routing-regels rechtstreeks uit de bron, transformeer ze, push ze naar het schema van de WordPress-formplugin. Dat plan loste op zodra we de webform-tabel openden.

Wat Webform 4.x werkelijk opsloeg

De Webform 4.x-module voor Drupal 7 bewaarde per formulier een configuratieblob in een LONGBLOB-kolom genaamd conditionals. Die kolom bevatte de hele conditional ruleset, inclusief action targets, operators, source components en gegroepeerde AND/OR-logica, als één serialized PHP-string. Geen JSON. Geen XML. Een serialized PHP-array, het type dat PHP's serialize() produceert en unserialize() weer kan teruglezen.

De daadwerkelijke payload voor deze site begon zo, nadat we de BLOB hadden uitgelezen en door strings hadden gehaald:

a:3:{s:7:"enabled";b:1;s:6:"groups";a:1240:{i:0;a:4:{s:6:"andor";s:3:"and";s:5:"rules";a:3:{i:0;a:3:{s:6:"source";s:8:"postcode";s:8:"operator";s:11:"starts_with";s:5:"value";s:1:"8";}i:1;a:3:{s:6:"source";s:12:"install_type";s:8:"operator";s:5:"equal";s:5:"value";s:10:"warmtepomp";}i:2;a:3:{s:6:"source";s:11:"customer_ty";s:8:"operator";s:5:"equal";s:5:"value";s:7:"private";}}s:7:"actions";a:1:{i:0;a:3:{s:6:"target";s:13:"queue_zeeland";s:6:"action";s:4:"show";s:5:"value";s:0:"";}}s:6:"weight";i:0;}...

Dat ene veld, voor dat ene formulier, was 412 KB aan dichte serialized PHP die 1.240 routing-beslissingen beschreef. De structuur was drie niveaus diep, met cross-references tussen rule-groepen en component-ID's die in een aparte webform_component-tabel woonden. Drupal kon het lezen omdat Drupal het had geschreven. Al het andere had een vertaler nodig.

Waarom de exporter het platsloeg

De Drupal-naar-WordPress exporter die we hadden overgenomen was een community-plugin, onderhouden door één persoon in de weekenden. Hij verwerkte posts, taxonomieën, gebruikers, media en de zichtbare structuur van Webform-velden. Zodra hij de conditionals-blob tegenkwam, deed hij wat naïeve migratietools doen: hij behandelde de kolom als string, draaide er een sanity-regex overheen, en als die regex geen herkenbaar patroon matchte, schreef hij een lege waarde naar de equivalente kolom in de WordPress-formplugin.

De regex zelf was niet onredelijk. Hij zocht naar het canonieke Drupal "source / operator / value"-drietal in de vorm die de Webform 3.x-tak had gebruikt, waar conditionals waren opgeslagen als een platter array van associatieve entries. De 4.x-herschrijving voegde een buitenste groups-wrapper toe met per-groep weight-ordering en een AND/OR-toggle, en duwde de drietallen een niveau dieper. De regex was nooit bijgewerkt om de nieuwe vorm te matchen. Alles wat in het 4.x-formaat geserialiseerd was, glipte zonder hit langs hem heen, wat de exporter interpreteerde als "niets te migreren".

Geen waarschuwing. Geen logregel. Geen rij in het migratierapport. De export-summary zei Webform: 1 form, 47 components, 3,812 submissions en een groen vinkje. De conditionals waren stilletjes nul.

Waarschuwing

Elke migratietool die Drupal 7's Webform 4.x aanraakt zonder conditionals expliciet in de mapping op te nemen, heeft je routing-logica vrijwel zeker laten vallen. Diff het aantal branches voor en na, niet alleen het aantal velden.

Dit is het stuk van het verhaal dat achteraf het meest pijn doet. De exporter loog niet. Hij vertelde ons, in de samenvatting, precies wat hij gemigreerd had. Hij vertelde ons, door weglating, ook precies wat hij niet had gedaan. We lazen niet tussen de regels door, omdat de spreadsheet met branches die week niet op ons bureau lag, en omdat we het groene vinkje meer vertrouwden dan een gratis weekend-plugin verdiende.

Het reparatiescript

Toen we de structuur eenmaal hadden, was de fix driehonderd regels PHP en twee dagen testen. De vorm:

<?php
// pull-conditionals.php, run from the Drupal 7 docroot
require_once 'includes/bootstrap.inc';
drupal_bootstrap(DRUPAL_BOOTSTRAP_FULL);

$nid = 142; // node id of the offerte-aanvraag form
$row = db_query(
  "SELECT conditionals FROM {webform} WHERE nid = :nid",
  [':nid' => $nid]
)->fetchObject();

$conditionals = unserialize($row->conditionals);
$components = db_query(
  "SELECT cid, form_key, name FROM {webform_component} WHERE nid = :nid",
  [':nid' => $nid]
)->fetchAllAssoc('cid');

$out = [];
foreach ($conditionals['groups'] as $i => $group) {
  $rules = array_map(function ($r) use ($components) {
    return [
      'field'    => $components[$r['source']]->form_key ?? $r['source'],
      'operator' => map_operator($r['operator']),
      'value'    => $r['value'],
    ];
  }, $group['rules']);

  $out[] = [
    'logic'   => strtoupper($group['andor']),
    'rules'   => $rules,
    'actions' => array_map('normalise_action', $group['actions']),
  ];
}

file_put_contents('conditionals.json', json_encode($out, JSON_PRETTY_PRINT));

De output was een JSON-bestand met 1.240 rule-groepen in een plat, transportvriendelijk formaat. Vandaaruit importeerde een tweede script in het conditional-logic schema van Gravity Forms via de REST API, één formulier per keer, met checksums op het aantal regels.

De mapping was niet een-op-een. Drupal Webform 4.x ondersteunde operators die Gravity Forms niet had: regex_matches voor postcodepatronen, een CIDR-achtige range-matcher voor IP-filtering, en een datumvergelijking die een eigen tokenformaat gebruikte. De rule-engine van Gravity Forms draaide op simpele gelijkheid en substring-matching. We documenteerden zeven mismatched operators, vertaalden er vijf naar contains-benaderingen waar de inside-sales lead na een videocall over de betrokken branches mee akkoord ging, en flagden er twee voor handmatige review per inzending tot we ze twee weken later vervingen door een kleine Gravity Forms-add-on.

De replay-set was het waardevolste artefact van die elf dagen. We trokken 200 historische inzendingen uit Drupals webform_submitted_data-tabel, gekozen om de zeldzaamste postcodeprefixen en elke unieke combinatie van klanttype en installatietype uit de spreadsheet te oefenen. Elke inzending ging door de oorspronkelijke Drupal-logica én door de nieuwe Gravity Forms-regels, en de bestemmings-queue moest matchen. De diff hit nul op poging zeven. De eerste zes runs vingen twee off-by-one fouten in de weight-ordering en een typo in onze operator-mapping tabel die voor één batch branches stilletjes elke starts_with in ends_with had omgeklapt.

Wat we uit het wrak hielden

Elf dagen. Dat was de totale uitloop vanaf het moment dat het bericht van de inside-sales lead binnenkwam tot het moment dat haar queues weer correct-gerouteerde leads ontvingen. In die elf dagen verwerkte de verkoop-binnendienst inkomende leads handmatig vanuit de catch-all, werkend met een geprinte versie van de routing-spreadsheet. Ze verloren geen leads waarvan ze het wisten. Waarschijnlijk verloren ze er een handvol waarvan ze het niet wisten.

De eerste maand na de cutover draaiden we een parallelle verifier. Elke formulierinzending postte naar een kleine Cloudflare Worker die de originele Drupal-ruleset tegen de payload evalueerde en elke onenigheid met de nieuwe Gravity Forms-routing logde. Veertigduizend inzendingen later: drie onenigheden, allemaal op de regex-operators die we al hadden gevlagd voor handmatige rebuild. De Worker ging in mei uit dienst, toen de add-on live was en het disagreement-log twee weken stil was gebleven.

Dat kwartaal herschreven we ook onze migratie-checklist. De versie die we hadden, droeg één regel die zei "formulierdata" met een checkbox. De nieuwe versie heeft veertien regels, waarvan er zes alleen al over conditional logic en routing-bestemmingen gaan. Het eerste punt staat nu: open zelf het bron-schema, met je eigen ogen, voordat je het rapport van welke tool dan ook vertrouwt.

Drie gewoontes die we nu op elke Drupal-naar-iets-anders migratie toepassen:

  • Diff de regels, niet alleen de velden. Tel conditional branches, validatieregels, email handlers en webhook-bestemmingen voor en na. Een groen vinkje op het aantal velden zegt niks als de logica tussen die velden weg is.
  • Lees de BLOB-kolommen. Elke kolom genaamd data, config, conditionals, settings of extra is verdacht. Open hem. Als hij begint met a: of O:, is het serialized PHP en kan jouw exporter er waarschijnlijk niet bij.
  • Vraag op dag één om de routing-spreadsheet. Voor de kickoff-call vraag je de klant om elke routing-regel, elke conditional e-mail, elke CRM-tag. Als ze het niet kunnen produceren, is die afwezigheid de grootste onbekende van je project.

De bouw-installateur in Brugge draait nog steeds op WordPress 6.5. De offerte-aanvragen routeren goed. De verkoop-binnendienst heeft nu een geprinte checklist op de monitor geplakt die zegt "als een formulier verandert, tel de regels." Dat briefje hing er voor maart niet.

Toen wij de legacy migratie voor deze Brugse klant overnamen, was het zwaarst kapotte stuk precies het stuk dat het meest gerepareerd leek. We hebben het uiteindelijk opgelost door elke serialized kolom als onbetrouwbaar te behandelen, hem zelf te unserializen, en de structuur tegen de bron te diffen voordat we welk groen vinkje dan ook van een community-exporter vertrouwden.

Als je dit kwartaal een Drupal 7-migratie plant, is het kleinste nuttige wat je vandaag kunt doen: open je webform-tabel, kopieer de conditionals-kolom voor je belangrijkste formulier en tel de rule-groepen erin. Welk getal je krijgt, is het getal waarop je migratie aan de andere kant moet landen.

Kern

Bij een Drupal 7 Webform-migratie zijn de velden het makkelijke deel. De conditional logic woont in een serialized PHP-blob, en de meeste exporters laten hem geluidloos vallen.

FAQ

Waarom mislukte de Webform-migratie zonder waarschuwing?

De Drupal 7 Webform 4.x-module sloeg conditional logic op als serialized PHP in één BLOB-kolom. De community-exporter behandelde het als string, matchte geen herkenbaar patroon, en schreef een lege waarde weg zonder waarschuwing in het migratierapport.

Hoe controleer ik of mijn Drupal 7-migratie de conditional logic van Webform heeft laten vallen?

Open de webform-tabel in de bron-database, unserialize de conditionals-kolom voor elk formulier, tel de rule-groepen, en bevestig dat hetzelfde aantal in het schema van de formplugin op de bestemming bestaat voordat je live gaat.

Is dit alleen een Drupal 7-probleem?

Nee. Elk CMS dat configuratie als serialized PHP opslaat, inclusief oudere WordPress-plugins en meerdere Joomla-componenten, draagt hetzelfde risico. Behandel elke BLOB die begint met a: of O: als een vertaalprobleem, niet als een kopieerprobleem.

Wat is de veiligste manier om Drupal 7 end of life aan te pakken?

Kies een bestemming, audit elk formulier, elke view en elk block met custom logic, en plan dubbel zoveel tijd in als de exporter belooft. Drupal 7 bereikte end of life in januari 2025, dus niet-ondersteunde sites zijn al een security-risico.

drupalmigrationlegacy siteswordpressphpcase study

Iets bouwen?

Start een project