Transporter mobiele applicatie
TST Terschelling
MooiWeer
Een full-stack registratie- en ticketingplatform voor een trail running evenement op een Nederlandse Waddenzee-eiland gebouwd vanaf het niets met een aangepaste PHP-raamwerk, live betalingsverwerking, PDF-ticketgeneratie en geautomatiseerde e-maillevering.
TrailRun Terschelling is de registratiewebsite voor een jaarlijkse trail running-evenement op het eiland Terschelling in Nederland. Deelnemers kunnen zich online aanmelden voor de 10 km, 15 km of 25 km afstand, betalen via meerdere Nederlandse betaalmethoden en onmiddellijk een PDF-wedstrijdkaartje ontvangen met een unieke toegangsleutel.
Het project omvat de gehele reis van eind tot eind van de deelnemer: een informatieve front-end met raceroutes op interactieve kaarten, een AJAX-registratieformulier in meerdere stappen met server-side validatie, een live payment gateway-integratie, een PDF-generatiepijpleiding en een geautomatiseerd transactional e-mail systeem allemaal gebonden door een thuis ontwikkeld lichtgewicht MVC-framework.
In 2025 werd de gehele front-end opnieuw ontworpen (nieuwe CSS-architectuur, aangepaste eigenschappen, flexbox-layout, breakpoints voor mobiele apparaten) en de onderliggende PHP werd gecontroleerd op PHP 8 compatibiliteit terwijl de codebase op de productieserver werd ingezet die beperkt is tot PHP 5.6.
In plaats van Laravel of Symfony te bereiken, is dit project gebouwd op een lichtgewicht custom framework dat specifiek is ontwikkeld voor dit type event-registratie-gebruiksgeval. Het framework is klein, snel en volledig begrepen door de ontwikkelaar geen magie, geen bloat.
Het rendert het HTML-document skelet, injecteert SEO-meta-tags, laadt CSS/JS-assets en delegeert inhoud naar subklassen via een abstract drawPage() methode.
Extent NSWebpage met sessiebeheer en een evenement-gedreven AJAX-laag. NSEvent voorwerpen en geleid door een handleEvent() - Ik ben de afzender.
Basisklasse voor alle modellen ondersteund door database. Implementeert een vuil vlag patroon eigenschappen worden alleen in de database geschreven wanneer ze worden gewijzigd. Biedt loadObject(), store(), en delete() zonder schema-reflectie overhead.
Een singleton wrapper rond mysqli Dit is een van de belangrijkste factoren in de ontwikkeling van de databank. escape() helper en een consistente zoekinterface die door alle modellen wordt gebruikt.
Elk route kaart tot een concrete pagina klasse die zich uitstrekt TrailrunWebpage (zelf uitbreiding) NSActiveWebpageDe pagina-singleton wordt opgeslagen in $_SESSION, het behoud van de formulier staat over AJAX-oproepen zonder verborgen velden of herhaalde databank rondreizen.
// All routing goes through .htaccess → index.php
// index.php routes to the correct page class
IndexPage → Home / hero slider / news
RegisterPage → Multi-step registration form
RoutesPage → Leaflet map + GPX route overlay
InformatiePage → Static race info content Registratie wordt verwerkt via een AJAX-formulier met meerdere stappen dat de staat bij elke wijziging met de server synchroniseert, zodat er niets verloren gaat als de browser wordt gesloten of de sessie afloopt voor betaling.
De boeking wordt in drie verbonden entiteiten verdeeld, waarbij de bezorgdheid zuiver gescheiden wordt gehouden:
| Model | Rol | Belangrijke velden |
|---|---|---|
| Boeking | Eerste boekingsregister, informatie over de deelnemer, betalingsstatus | boekingsnummer, toegangsleutel, staat, e-mail, geboortedatum |
| BoekingElement | Afzonderlijke lijnstukken (één per afstand/slot geselecteerd) | event_id, prijs, korting, hoeveelheid |
| BoekingBetaling | Onveranderlijk log van betalingstransacties | betaald, bedrag, valuta, merk, status, ipaddress |
| Gebeurtenis | Uit database geladen raceconfiguratie | Eventnaam, volwassenprijs, slots, minimumleeftijd, actief |
Elke boeking gaat door een strikte reeks staten, waardoor dubbele betalingen worden voorkomen en auditsvragen triviaal worden gemaakt:
STATENOPAYMENT (1) → Created, awaiting payment
STATEPAID (2) → Payment confirmed, PDF + email sent
STATECHANGE (3) → Booking modified post-payment
STATECANCELED (4) → Cancelled / refunded De EventFactory de gegevensbank wordt bij paginabelasting geraadpleegd en het aantal beschikbare slots per afstand berekend door bevestigde boekingen af te trekken van het geconfigureerde maximum. Wanneer een afstand uitverkocht, wordt deze automatisch verwijderd uit de selectie-interface zonder handmatige ingreep.
De toegangsleutel opgeslagen op elke BoekingElement wordt gegenereerd met Toolbox::MakeRandomPassword(8) een cryptografisch willekeurige 8-tekens string gedrukt op het PDF-ticket en gebruikt door het racepersoneel om deelnemers aan de startlijn te verifiëren.
Betalingen worden verwerkt via de EMS e-Commerce Gateway, die de drie meest relevante methoden voor een Nederlandse sportgelegenheid ondersteunt: iDEAL (directe bankoverschrijving), Mastercard en PayPal.
payment_ideal, payment_mastercard, of payment_paypal) via AJAX. De server roept EMSPayment::sendPaymentRequest(), die een HMAC-handtekening berekent over de orderparameters en een redirect-URL retourneert. inschrijven.php. De beheerder valideert de HMAC-ondertekening om te bevestigen dat de reactie authentiek is, en leest vervolgens chargetotal, txndatetime, en de goedkeuringskode. approval_code == 'Y' en status == 'APPROVED', de boeking is gemarkeerd als betaald, de volledige transactiegegegevens worden geschreven aan boeking_payment, en de PDF + e-mail pijplijn wordt geactiveerd. PAYMENTFAILURE en de gebruiker ziet een vriendelijke fout. De codebase bevat ook een oude Ogone-betalingsintegratie de voorganger gateway. Het is niet langer actief, maar werd bewaard in de codebase als referentie, omdat er via het een aantal historische boekingen werden gemaakt en het responsformaat nog steeds wordt gedocumenteerd in opmerkingen.
Elke bevestigde registratie produceert een gepersonaliseerd, gedrukte PDF-race ticket. PDFlib, een commerciële PDF-creatiebibliotheek die is gekozen voor zijn nauwkeurige layoutbeheersing en betrouwbare font rendering.
De kaartfabriek (framework/factories/PDFTicket.php) componeert het document programmatisch er is geen sjabloonbestand, geen HTML-to-PDF-conversie en geen tussenliggende renderingsstap. Elk element wordt met exacte puntcoördinaten op een A4-doek (595 × 842 pt) geplaatst.
Een full-width branded image (pdf_ticketkop.jpg) aan de bovenkant van de pagina wordt de rasidentiteit en het jaar vastgesteld.
Een afstands-specifiek kaartbeeld (pdf_10KM.jpg, pdf_15KM.jpg, pdf_25KM.jpg) wordt op tijd geladen op basis van het geboekte evenement, waardoor elk ticket een unieke visuele handtekening krijgt.
Naam, adres, postcode, stad en geselecteerde afstand worden in Helvetica-Bold getypt.
De toegangsleutel van 8 tekens wordt voor de startlijn verificatie opvallend afgedrukt. Het boekingsnummer wordt op de donkere voet achtergrond met behulp van Courier in het wit omgedraaid, zodat het gemakkelijk wordt gescand door het racepersoneel.
// 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 afgewerkte PDF wordt opgeslagen op pdf_gen_tickets/{BookingNumber}.pdf op de schijf, waar deze onmiddellijk beschikbaar is om aan de bevestigings-e-mail te worden gehecht.
PDFlib werd gekozen boven HTML-to-PDF-converters zoals wkhtmltopdf omdat het biedt deterministische pixel-perfecte lay-out Voor een ticketingsysteem waar een verkeerd afgestemd element het racepersoneel zou kunnen verwarren, is die afhandeling de moeite waard.
Er worden bevestigings-e-mails verzonden zodra een betaling is geverifieerd geen wachtrij, geen vertraging. PostOffice Factory wrappes PHPMailer en routes outbound mail via Mailgun's SMTP relais voor betrouwbare leverbaarheid.
framework/mail_templates/reserveringsbevestiging.html een standalone HTML-bestand dat door niet-ontwikkelaars kan worden bewerkt zonder PHP aan te raken. [FIRSTNAME], [RESERVATIONNUMBER], en [BOOKINGDATA] De reserveringsopsommingstabel wordt weergegeven als inline HTML. pdf_gen_tickets/{BookingNumber}.pdfDe deelnemers ontvangen hun ticket binnen enkele seconden na bevestiging van de betaling. smtp.eu.mailgun.org:25Een stilstaande BCC wordt naar de organisatoren van het evenement voor hun eigen gegevens gekopieerd. 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 ontwerp van 2025 heeft een gedateerde lay-out vervangen door een modern, mobiel-eerste ontwerpsysteem terwijl de onderliggende PHP waar mogelijk intact blijft.
Een single. :root Het wisselen van merkkleur voor een komend jaar vereist één bewerking, niet een vind-en-vervanging over 2000 lijnen.
Breakpoints op 767 px en 1023 px. De navigatie stort neer op een hamburger menu op mobiel; race kaarten stromen terug van een multi-kolom net naar een enkele kolom zonder enige layout JavaScript.
Elke kaart, knop en input heeft vierkante hoeken om het industriële gevoel van wedstrijdbeelden, trailmarkers en terreinkaarten te weerspiegelen.
Elke raceafstand heeft een specifieke GPX-route die wordt weergegeven op een OpenStreetMap-tegellaag via een Leaflet.#99c038) en geanimeerde markeringen geven belangrijke wegpunten aan.
De RegisterPage JavaScript-klasse stuurt een addressupdate Event naar de server bij elke veldwijziging. Server-side validatiefouten worden teruggestuurd als JSON en weergegeven in lijn geen volledige pagina herladen, geen verloren gegevens.
Een full-viewport beeldslide op de startpagina door middel van racefotografie. Touch-swipe-ondersteuning werkt uit de doos op mobiel zonder extra configuratie.
CSS is geschreven in css/design.css (1752 lijnen) en minificeerd tot design.min.css JavaScript voor het registratieformulier wordt geminificeerd van: js/inschrijven.js aan inschrijven.min.jsDe . .htaccess gebruikt gzip-compressie voor CSS-, JS- en lettertypebestanden en stelt agressieve cache-opskriften voor statische activa.
De productie-hostingomgeving is beperkt tot PHP 5.6 , maar de codebase moet ook een PHP 8 syntaxiscontrole doorstaan en op de ontwikkelingsmachine schoon lopen onder PHP 8. Dit is de moeilijkste beperking op het project.
Het werk van 2025 omvatte de controle van elk PHP-bestand op functies die zich anders gedragen in het versiegap:
mysql_query(), mysql_fetch_array(), en gerelateerde oproepen werden vervangen door hun mysqli_* equivalenten, die beschikbaar zijn in zowel PHP 5.6 als PHP 8. Iterator interface in Iterator en IteratorAggregate Er zijn oplossingen toegevoegd voor het toepassen van de juiste interface, waarbij PHP 5.6 ze onveilig negeert. $str{0}) Alle gebeurtenissen werden omgezet in bracket syntax ($str[0]), die geldig is voor beide versies. Schrijven PHP die op beide wordt uitgevoerd 5.6 en 8 De strategie was: gebruik alleen de subset van PHP die beide versies delen, en vermijd elke functie die na 5.6 werd geïntroduceerd waarvoor PHP 8 later het gedrag veranderde.
Elke uit praktische redenen gekozen component heeft geen kader voor zichzelf, geen afhankelijkheid die zijn gewicht niet trekt.
| Layer | Technologie | Waarom ? |
|---|---|---|
| Servertaal | PHP 5.6 / 8 | Beperking van de aanbieding; geen keuze mogelijk |
| Database | MySQL (mysqli) | Beschikbaar op de host; voldoende voor het gegevensvolume |
| Kader | Op maat (NSWebpagina / MVC) | Geen Laravel / Symfony op PHP 5.6 zonder oplossingen |
| PDF-generatie | PDFlib (commerciële) | Pixel-perfecte lay-out; deterministische output; lettertypen |
| Aflevering van e-mails | PHPMailer + Mailgun SMTP | Betrouwbare leverbaarheid; BCC-auditsroute naar organisatoren |
| Betalingspoort | EMS e-commerce (HMAC) | Ondersteunt iDEAL, Mastercard, PayPal in één enkele integratie |
| Kaarten | Leaflet.js + OpenStreetMap | Open source; geen API-sleutel vereist; GPX-overlay triviaal |
| DOM-manipulatie | jQuery 1.11.1 | Stabiel; werkt op de oude browser-reeks event deelnemers gebruiken |
| Held glijmiddel | Jssor | Touch-swipe uit de doos; nul serverafhankelijkheid |
| CSS-architectuur | Custom + CSS aangepaste eigenschappen | Geen preprocessor nodig; op maat gemaakte eigenschappen ondersteund nationaal |
| Identificatie van het apparaat | Mobile_Detect.php | Serverside breakpoint hint in aanvulling op media queries |
Wat dit project aantoont buiten de lijst met kenmerken.
De PHP 5.6 beperking dwong een aangepaste framework dat klein is, volledig te begrijpen en gemakkelijk te controleren. Geen verborgen magie, geen versie-incompatibiliteit begraven in de leverancier code.
Het smerige vlaggenpatroon in NSPersistentObject Soms is het opnieuw uitvinden van een klein wiel beter dan het trekken van een groot wiel.
Het opslaan van de pagina-singleton in $_SESSION En synchroniseren bij elke veldwijziging betekent dat deelnemergegevens veilig zijn, zelfs als de browser mid-form crasht.
HTML-to-PDF-converters zijn handig, maar produceren variabele output. Voor een ticketingsysteem waar layout nauwkeurigheid belangrijk is voor operationele gebruik, was een goede PDF-bibliotheek de licentiekosten waard.
De no-border-radius-regel begon als een tijdbesparende snelweg en werd een opzettelijke merkkeuze.
Het schrijven voor PHP 5.6 en PHP 8 tegelijkertijd is ongewoon maar hanteerbaar. De sleutel is om precies te weten welke functies te vermijden en de beperking duidelijk te documenteren voor toekomstige medewerkers.
Meer werk
TST Terschelling
TransportMaster
TransportMaster