Mobiele Transporter-app
TST Terschelling
MooiWeer
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.
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.
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.
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.
Breidt NSWebpage uit met sessiebeheer en een gebeurtenisgestuurde AJAX-laag. Binnenkomende verzoeken worden verwerkt tot NSEvent -objecten en doorgegeven aan een handleEvent() -verwerker.
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.
Een singleton-wrapper rond mysqli die zorgt voor één databaseverbinding per verzoek. Biedt een escape() -hulpfunctie en een uniforme query-interface voor alle modellen.
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 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.
De boeking is verdeeld over drie gekoppelde entiteiten, zodat de verantwoordelijkheden duidelijk gescheiden blijven:
| Model | Rol | Belangrijkste velden |
|---|---|---|
| Boeking | Hoofdrecord voor de boeking, deelnemersgegevens en betaalstatus | boekingsnummer, accesskey, state, email, birthdate |
| BoekingElement | Afzonderlijke regels, één per gekozen afstand of deelnameplek | event_id, price, discount, quantity |
| BoekingPayment | Onwijzigbaar logboek van betaaltransacties | paymentid, amount, currency, brand, status, ipaddress |
| Event | Wedstrijdinstellingen uit de database | eventname, adultprice, slots, minimumage, active |
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 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.
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.
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. 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. 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.
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.
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.
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.
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.
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.
// 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.
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.
framework/mail_templates/reserveringsbevestiging.html — een zelfstandig HTML-bestand dat ook zonder programmeerkennis kan worden aangepast zonder PHP te wijzigen. [FIRSTNAME], [RESERVATIONNUMBER]en [BOOKINGDATA] worden tijdens uitvoering vervangen door deelnemersgegevens. De boekingstabel wordt als inline HTML opgebouwd. pdf_gen_tickets/{BookingNumber}.pdf) wordt vóór verzending toegevoegd. Deelnemers ontvangen hun startbewijs binnen enkele seconden na de betaalbevestiging. 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(); 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.
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.
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.
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.
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.
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.
Een beeldslider over het volledige venster wisselt op de homepage wedstrijdbeelden af. Vegen op een aanraakscherm werkt op mobiel direct, zonder extra instellingen.
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 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:
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. 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. $str{0}). Alle gevallen zijn omgezet naar vierkante haken ($str[0]), die in beide versies geldig zijn. 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.
Elk onderdeel is om praktische redenen gekozen, zonder frameworks om het framework en zonder afhankelijkheden die onvoldoende waarde toevoegen.
| Laag | Technologie | Reden |
|---|---|---|
| Servertaal | PHP 5.6 / 8 | Beperking van de hosting; geen andere keuze |
| Database | MySQL (mysqli) | Beschikbaar bij de host en voldoende voor de hoeveelheid gegevens |
| Framework | Eigen framework (NSWebpage / MVC) | Geen Laravel of Symfony op PHP 5.6 zonder omwegen |
| Pdf-generatie | PDFlib (commercieel) | Nauwkeurige opmaak, voorspelbare uitvoer en ingebedde lettertypen |
| E-mailbezorging | PHPMailer + Mailgun SMTP | Betrouwbare bezorging en BCC-kopie voor de administratie van de organisatie |
| Betaalgateway | EMS e-Commerce (HMAC) | iDEAL, Mastercard en PayPal via één koppeling |
| Kaarten | Leaflet.js + OpenStreetMap | Open source, geen API-sleutel en eenvoudig GPX-routes tonen |
| DOM-bewerking | jQuery 1.11.1 | Stabiel en geschikt voor de oudere browsers die deelnemers gebruiken |
| Grote beeldslider | Jssor | Vegen op aanraakschermen werkt direct, zonder serverafhankelijkheid |
| CSS-architectuur | Eigen CSS en CSS custom properties | Geen preprocessor nodig; custom properties worden direct ondersteund |
| Apparaatdetectie | Mobile_Detect.php | Aanwijzing voor het breekpunt op de server, naast mediaquery’s |
Wat dit project laat zien, naast de lijst met functies.
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.
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.
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.
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.
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.
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.
Meer werk
TST Terschelling
TransportMaster
TransportMaster