Softwareontwikkeling

Herontwerp van de TrailRun-website

MooiWeer

Opdrachtgever
MooiWeer
Categorie
Softwareontwikkeling
Jaar
2025
Technologieën
PHPJavascriptMySQLPDFLib
TrailRun Terschelling

Een volledig registratie- en ticketplatform voor een trailrun op een Nederlands Waddeneiland, vanaf de basis gebouwd met een eigen PHP-framework, live betalingsverwerking, pdf-tickets en automatische e-mailbezorging.

3Afstanden
~22kRegels PHP
PDFlibTicketgeneratie
iDEALBelangrijkste betaalwijze
2025Jaar van herontwerp

Wat houdt dit project in?

TrailRun Terschelling is de inschrijfwebsite voor een jaarlijks trailrunevenement op het Nederlandse eiland Terschelling. Deelnemers schrijven zich online in voor 10 km, 15 km of 25 km, betalen met verschillende Nederlandse betaalmethoden en ontvangen direct een pdf-startbewijs met een unieke toegangscode.

Het project omvat de hele deelnemersreis: een informatieve website met routes op interactieve kaarten, een AJAX-inschrijfformulier in meerdere stappen met controle op de server, een koppeling met een betaalgateway, pdf-generatie en automatische transactiemails. Alles wordt verbonden door een zelfgebouwd, licht MVC-framework.

In 2025 is de volledige frontend opnieuw ontworpen, met een nieuwe CSS-architectuur, custom properties, flexbox en breekpunten vanuit mobiel gebruik. De onderliggende PHP-code is gecontroleerd op compatibiliteit met PHP 8, terwijl de code inzetbaar moest blijven op de productieserver met PHP 5.6.

PHP 5.6 / 8MySQLPDFlibPHPMailerMailgun SMTPEMS-betaalgatewayiDEALjQueryLeaflet.jsCSS custom properties

Eigen MVC- framework

Dit project gebruikt geen Laravel of Symfony, maar een licht framework dat specifiek is ontwikkeld voor dit type evenementenregistratie. Het framework is klein, snel en volledig te doorgronden voor de ontwikkelaar, zonder verborgen gedrag of overbodige ballast.

NSWebpage — Basis voor pagina’s

Bouwt de HTML-documentstructuur op, voegt SEO-metatags toe, laadt CSS- en JavaScript-bestanden en laat subklassen de inhoud verzorgen via een abstracte drawPage() -methode.

NSActiveWebpage — AJAX-afhandeling

Breidt NSWebpage uit met sessiebeheer en een gebeurtenisgestuurde AJAX-laag. Binnenkomende verzoeken worden verwerkt tot NSEvent -objecten en doorgegeven aan een handleEvent() -verwerker.

NSPersistentObject — Lichte ORM

Basisklasse voor alle databasemodellen. Gebruikt een wijzigingsvlag: eigenschappen worden alleen naar de database geschreven als ze zijn veranderd. Biedt loadObject(), store()en delete() zonder extra werk om het schema tijdens uitvoering te onderzoeken.

NSDatabase — Eén gedeelde verbinding

Een singleton-wrapper rond mysqli die zorgt voor één databaseverbinding per verzoek. Biedt een escape() -hulpfunctie en een uniforme query-interface voor alle modellen.

Paginaklassen

Elke route verwijst naar een concrete paginaklasse die voortbouwt op TrailrunWebpage (zelf een uitbreiding van NSActiveWebpage). Het paginaobject wordt als singleton bewaard in $_SESSION, zodat formuliergegevens tussen AJAX-aanroepen behouden blijven zonder verborgen velden of herhaalde databaseverzoeken.

// All routing goes through .htaccess → index.php
// index.php routes to the correct page class

IndexPage      → Homepage / grote beeldslider / nieuws
RegisterPage   → Inschrijfformulier in meerdere stappen
RoutesPage     → Leaflet-kaart met GPX-route
InformatiePage → Vaste informatie over de wedstrijd

Het boekings- systeem

Inschrijven gebeurt via een AJAX-formulier in meerdere stappen dat elke wijziging met de server synchroniseert. Daardoor gaat niets verloren als de browser wordt gesloten of de sessie vóór de betaling verloopt.

Betaalproces

01Afstand kiezen10 / 15 / 25 km
02DeelnemersgegevensAJAX addressupdate
03Betaalwijze kiezeniDEAL / MC / PayPal
04EMS-betaalgatewayDoorverwijzen
05TerugmeldingPOST → inschrijven.php
06Pdf en e-mailAutomatisch gemaakt

Datamodel

De boeking is verdeeld over drie gekoppelde entiteiten, zodat de verantwoordelijkheden duidelijk gescheiden blijven:

ModelRolBelangrijkste velden
BoekingHoofdrecord voor de boeking, deelnemersgegevens en betaalstatusboekingsnummer, accesskey, state, email, birthdate
BoekingElementAfzonderlijke regels, één per gekozen afstand of deelnameplekevent_id, price, discount, quantity
BoekingPaymentOnwijzigbaar logboek van betaaltransactiespaymentid, amount, currency, brand, status, ipaddress
EventWedstrijdinstellingen uit de databaseeventname, adultprice, slots, minimumage, active

Toestandsmodel

Elke boeking doorloopt een vaste reeks statussen. Dit voorkomt dubbele betalingen en maakt controles eenvoudig:

STATENOPAYMENT  (1)  → Aangemaakt, wacht op betaling
STATEPAID       (2)  → Betaling bevestigd, pdf en e-mail verstuurd
STATECHANGE     (3)  → Boeking na betaling gewijzigd
STATECANCELED   (4)  → Geannuleerd / terugbetaald

Beheer van deelnameplekken

De EventFactory vraagt bij het laden van de pagina de database op en berekent per afstand hoeveel plekken over zijn door bevestigde boekingen af te trekken van het ingestelde maximum. Als een afstand vol is, verdwijnt deze automatisch uit de keuzelijst, zonder handmatige ingreep.

De toegangscode in ieder BoekingElement wordt gemaakt met Toolbox::MakeRandomPassword(8) — een cryptografisch willekeurige tekenreeks van 8 tekens die op het pdf-ticket staat en waarmee medewerkers bij de start deelnemers controleren.

Koppeling met de betaalgateway

Betalingen lopen via de EMS e-Commerce Gateway, die drie voor een Nederlands sportevenement relevante methoden ondersteunt: iDEAL (rechtstreeks via de bank), Mastercard en PayPal.

  1. 1
    Order aanmaken Wanneer de deelnemer op “Betalen” drukt, verstuurt de browser via AJAX een betaalgebeurtenis (payment_ideal, payment_mastercardof payment_paypal). De server roept EMSPayment::sendPaymentRequest()aan. Deze methode berekent een HMAC-handtekening over de orderparameters en geeft een doorverwijs-URL terug.
  2. 2
    Doorverwijzen naar de betaalgateway De browser gaat naar de betaalpagina van EMS. Gevoelige kaartgegevens worden volledig op het domein van de betaalgateway verwerkt; de applicatie verwerkt zelf geen kaartnummers.
  3. 3
    Asynchrone terugmelding Na autorisatie verstuurt EMS het resultaat via POST naar inschrijven.php. De verwerker controleert de HMAC-handtekening om de echtheid van het antwoord vast te stellen en leest vervolgens chargetotal, txndatetimeen de goedkeuringscode uit.
  4. 4
    Afronden Bij approval_code == 'Y' en status == 'APPROVED'wordt de boeking als betaald gemarkeerd, de volledige transactie opgeslagen in boeking_paymenten de pdf- en e-mailverwerking gestart. Elke andere uitkomst leidt tot PAYMENTFAILURE en een begrijpelijke foutmelding voor de gebruiker.
De codebase bevat ook een oudere Ogone-betaalkoppeling, de voorganger van EMS. Deze is niet meer actief, maar blijft bewaard als referentie omdat historische boekingen ermee zijn betaald en het antwoordformaat nog in commentaar is gedocumenteerd.

Pdf-tickets maken met PDFlib

Elke bevestigde inschrijving krijgt een persoonlijk pdf-startbewijs dat klaar is om af te drukken. Het ticket wordt op de server gemaakt met PDFlib, een commerciële PDF-bibliotheek die is gekozen vanwege de nauwkeurige lay-outcontrole en betrouwbare weergave van lettertypen.

De ticketfactory (framework/factories/PDFTicket.php) bouwt het document programmatisch op, zonder templatebestand, HTML-naar-PDF-conversie of tussenliggende renderstap. Elk onderdeel wordt met exacte puntcoördinaten op een A4-pagina van 595 × 842 pt geplaatst.

Wat op het ticket staat

Kopafbeelding

Een afbeelding in de huisstijl over de volledige breedte (pdf_ticketkop.jpg) staat bovenaan en maakt duidelijk om welke wedstrijd en welk jaar het gaat.

Routekaart

Een kaartafbeelding voor de gekozen afstand (pdf_10KM.jpg, pdf_15KM.jpg, pdf_25KM.jpg) wordt tijdens uitvoering geladen op basis van de geboekte wedstrijd, zodat ieder ticket een herkenbaar beeld heeft.

Deelnemersgegevens

Naam, adres, postcode, plaats en gekozen afstand worden gezet in Helvetica-Bold. De wedstrijddatum (28-02-2027) en starttijd per afstand (13:00 / 12:30 / 11:45) staan groot vermeld.

Toegangscode en boekingsnummer

De toegangscode van 8 tekens staat duidelijk zichtbaar voor controle bij de start. Het boekingsnummer staat in wit op de donkere footerachtergrond, gezet in Courier, zodat medewerkers het snel kunnen aflezen.

Hoe de opbouw werkt

// Simplified excerpt from PDFTicket::BuildTicket()

$pdf = new PDFlib();
$pdf->set_option("license=m900102-...");
$pdf->begin_document("pdf_gen_tickets/{$nr}.pdf", "");
$pdf->begin_page_ext(595, 842, "");      // A4

// Place branded header image
$img = $pdf->load_image("jpeg", "pdf_ticketkop.jpg", "");
$pdf->fit_image($img, 0, 700, "boxsize={595 130}");

// Distance-specific route map
$routeImg = $pdf->load_image("jpeg", "pdf_{$km}KM.jpg", "");
$pdf->fit_image($routeImg, 300, 480, "boxsize={280 200}");

// Participant name in Helvetica-Bold 14pt
$font = $pdf->load_font("Helvetica-Bold", "winansi", "");
$pdf->setfont($font, 14);
$pdf->show_xy($naam, 40, 650);

// Amount in large 24pt type
$pdf->setfont($font, 24);
$pdf->show_xy("EUR {$amount}", 40, 560);

$pdf->end_page_ext("");
$pdf->end_document("");

De voltooide pdf wordt op schijf opgeslagen als pdf_gen_tickets/{BookingNumber}.pdf en kan direct als bijlage bij de bevestigingsmail worden gevoegd.

PDFlib kreeg de voorkeur boven HTML-naar-PDF-converters zoals wkhtmltopdf vanwege de voorspelbare, tot op de pixel nauwkeurige opmaak . Daar staat een commerciële licentie tegenover. Voor een ticketsysteem waarin een verkeerd geplaatst onderdeel medewerkers kan verwarren, is dat een zinvolle afweging.

Verwerking van transactiemails

Bevestigingsmails worden verzonden zodra de betaling is gecontroleerd, zonder wachtrij of vertraging. De PostOffice -factory gebruikt PHPMailer en stuurt uitgaande e-mail via de SMTP-relay van Mailgun voor betrouwbare bezorging.

  1. 1
    Template laden De HTML-e-mailtemplate wordt gelezen uit framework/mail_templates/reserveringsbevestiging.html — een zelfstandig HTML-bestand dat ook zonder programmeerkennis kan worden aangepast zonder PHP te wijzigen.
  2. 2
    Variabelen vervangen Plaatshouders zoals [FIRSTNAME], [RESERVATIONNUMBER]en [BOOKINGDATA] worden tijdens uitvoering vervangen door deelnemersgegevens. De boekingstabel wordt als inline HTML opgebouwd.
  3. 3
    Pdf als bijlage Het zojuist gemaakte pdf-ticket (pdf_gen_tickets/{BookingNumber}.pdf) wordt vóór verzending toegevoegd. Deelnemers ontvangen hun startbewijs binnen enkele seconden na de betaalbevestiging.
  4. 4
    Bezorgen via SMTP PHPMailer verstuurt het bericht naar de deelnemer via smtp.eu.mailgun.org:25. De organisatie ontvangt een verborgen BCC-kopie voor de eigen administratie. De onderwerpregel bevat het boekingsnummer: Inschrijf bevestiging TrailRun Terschelling 2027 [TRT-00123].
// PostOffice::sendConformation() — core logic

$body = file_get_contents('framework/mail_templates/reserveringsbevestiging.html');
$body = str_replace('[FIRSTNAME]',          $boeking->Voorletters(),   $body);
$body = str_replace('[RESERVATIONNUMBER]',   $boeking->Boekingsnummer(), $body);
$body = str_replace('[BOOKINGDATA]',         $itemTable,               $body);

$mail->AddAttachment("pdf_gen_tickets/{$nr}.pdf");
$mail->AddBCC('info@mooi-weer.nl');
$mail->Send();

Frontend & herontwerp in 2025

Het herontwerp van 2025 verving een verouderde lay-out door een modern ontwerpsysteem met mobiel als uitgangspunt. De onderliggende PHP bleef waar mogelijk ongewijzigd. De beeldtaal sluit bewust aan bij het ruige buitenkarakter van het evenement.

CSS custom properties

Eén :root -blok definieert het hele kleurenpalet en de afstandsschaal. Merkkleuren aanpassen voor een volgend jaar vraagt één wijziging, in plaats van zoeken en vervangen in 2000 regels.

Flexbox met mobiel als uitgangspunt

Breekpunten op 767 px en 1023 px. Op mobiel wordt de navigatie een hamburgermenu en gaan wedstrijdkaarten van meerdere kolommen naar één kolom, zonder JavaScript voor de lay-out.

Bewust geen afgeronde hoeken

Overal rechte randen: kaarten, knoppen en invoervelden hebben vierkante hoeken, passend bij de praktische uitstraling van startnummers, routemarkeringen en terreinkaarten. Een bewuste ontwerpkeuze, geen vergeten detail.

Routekaarten met Leaflet.js

Iedere afstand heeft een eigen GPX-route op OpenStreetMap-kaarttegels, weergegeven via Leaflet. De lijnstijl sluit aan bij het merkgroen (#99c038) en bewegende markeringen tonen belangrijke routepunten.

AJAX-inschrijfformulier

De JavaScript-klasse RegisterPage verstuurt bij iedere veldwijziging een addressupdate -gebeurtenis naar de server. Validatiefouten komen terug als JSON en verschijnen direct bij het formulier, zonder de pagina opnieuw te laden of gegevens te verliezen.

Grote beeldslider met Jssor

Een beeldslider over het volledige venster wisselt op de homepage wedstrijdbeelden af. Vegen op een aanraakscherm werkt op mobiel direct, zonder extra instellingen.

Bestanden voor publicatie

De CSS wordt geschreven in css/design.css (1752 regels) en voor productie verkleind tot design.min.css . De JavaScript-code voor het inschrijfformulier wordt verkleind van js/inschrijven.js naar inschrijven.min.js. Het bestand .htaccess past gzip-compressie toe op CSS, JavaScript en lettertypen en stelt ruime cachetermijnen in voor statische bestanden.

De uitdaging van PHP-compatibiliteit

De productieomgeving is beperkt tot PHP 5.6. Toch moet de code ook door een PHP 8-syntaxiscontrole komen en op de ontwikkelmachine goed onder PHP 8 werken. Dit is de lastigste randvoorwaarde van het project.

Het werk in 2025 omvatte een controle van ieder PHP-bestand op functies die tussen de versies anders werken:

  1. 1
    mysqli_* in plaats van mysql_* Alle oudere aanroepen zoals mysql_query(), mysql_fetch_array()en verwante functies zijn vervangen door hun mysqli_* -equivalenten, die beschikbaar zijn in zowel PHP 5.6 als PHP 8.
  2. 2
    Iterator-interface Collectieklassen moeten de juiste Iterator-interface implementeren. Zowel Iterator als IteratorAggregate bestonden al vóór PHP 8. Code die op PHP 5.6 moet draaien, kan geen retourtypedeclaraties gebruiken: die versie ondersteunt de syntaxis daarvoor niet. De typen worden daarom in docblocks beschreven. Vanaf PHP 8.1 vragen ook voorlopige retourtypen van interne interfaces aandacht.
  3. 3
    Tekens in tekenreeksen benaderen PHP 8 ondersteunt indexering met accolades niet meer ($str{0}). Alle gevallen zijn omgezet naar vierkante haken ($str[0]), die in beide versies geldig zijn.
  4. 4
    Geen typeaanduidingen voor parameters PHP 5.6 ondersteunt geen scalaire typeaanduidingen en geen retourtypen in functiesignaturen. De code is daarom zonder deze declaraties geschreven, met docblock-commentaar voor ondersteuning in de ontwikkelomgeving.

PHP schrijven die zowel op 5.6 als 8 werkt, is lastiger dan voor één versie ontwikkelen. De aanpak was om alleen het gedeelde deel van PHP te gebruiken en functies te vermijden die na 5.6 zijn ingevoerd en waarvan PHP 8 later het gedrag wijzigde.

De volledige technische basis

Elk onderdeel is om praktische redenen gekozen, zonder frameworks om het framework en zonder afhankelijkheden die onvoldoende waarde toevoegen.

LaagTechnologieReden
ServertaalPHP 5.6 / 8Beperking van de hosting; geen andere keuze
DatabaseMySQL (mysqli)Beschikbaar bij de host en voldoende voor de hoeveelheid gegevens
FrameworkEigen framework (NSWebpage / MVC)Geen Laravel of Symfony op PHP 5.6 zonder omwegen
Pdf-generatiePDFlib (commercieel)Nauwkeurige opmaak, voorspelbare uitvoer en ingebedde lettertypen
E-mailbezorgingPHPMailer + Mailgun SMTPBetrouwbare bezorging en BCC-kopie voor de administratie van de organisatie
BetaalgatewayEMS e-Commerce (HMAC)iDEAL, Mastercard en PayPal via één koppeling
KaartenLeaflet.js + OpenStreetMapOpen source, geen API-sleutel en eenvoudig GPX-routes tonen
DOM-bewerkingjQuery 1.11.1Stabiel en geschikt voor de oudere browsers die deelnemers gebruiken
Grote beeldsliderJssorVegen op aanraakschermen werkt direct, zonder serverafhankelijkheid
CSS-architectuurEigen CSS en CSS custom propertiesGeen preprocessor nodig; custom properties worden direct ondersteund
ApparaatdetectieMobile_Detect.phpAanwijzing voor het breekpunt op de server, naast mediaquery’s

Lessen & inzichten

Wat dit project laat zien, naast de lijst met functies.

Randvoorwaarden leiden tot doordacht ontwerp

De beperking tot PHP 5.6 dwong tot een klein, volledig te begrijpen en eenvoudig te controleren framework, zonder verborgen gedrag of versieconflicten in externe code.

Een eigen ORM is niet vanzelf een slechte ORM

Het patroon met wijzigingsvlaggen in NSPersistentObject is een gangbare aanpak die ook veel grote frameworks gebruiken. Soms is een kleine eigen oplossing beter dan een omvangrijke afhankelijkheid.

AJAX-synchronisatie werkt beter dan verborgen velden

Door het paginaobject in $_SESSION te bewaren en elke veldwijziging te synchroniseren, blijven deelnemersgegevens bewaard als de browser halverwege crasht. Verborgen invoervelden zouden het verversen van een tabblad niet overleven.

Pdf-generatie is geen inwisselbaar detail

HTML-naar-PDF-converters zijn handig, maar de uitvoer kan verschillen. Voor een ticketsysteem waarbij precieze opmaak belangrijk is in het gebruik, was een goede PDF-bibliotheek de licentiekosten waard.

Ontwerpbeperkingen als merkkeuze

Het weglaten van hoekafrondingen begon als tijdbesparing en werd een bewuste merkkeuze. Rechte randen passen bij een wedstrijd door duinen, bos en over het strand.

PHP voor twee versies is mogelijk

Tegelijk voor PHP 5.6 en PHP 8 ontwikkelen is ongebruikelijk, maar haalbaar. De sleutel is precies weten welke functies vermeden moeten worden en deze beperking duidelijk documenteren voor toekomstige ontwikkelaars.

Projectafbeeldingen

Meer werk

Gerelateerde projecten

Start een project met ons

Heeft u een project in gedachten? Bespreek met ons hoe we kunnen helpen.