Softwareontwikkeling

Vernieuwing van de TrailRun-website

MooiWeer

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

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.

3Rassafstanden
- 22kLijnen van PHP
PDFlibTicketmachine
IDEALVoorlopige betaling
2025Jaar van herontwerp

Wat is dit ? project?

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.

PHP 5.6 / 8MySQLPDFlibPHPMailerMailgun SMTPEMS-betalingspoortIDEALjQueryBrochure.jsCSS aangepaste eigenschappen

Op maat gemaakte MVC Kader

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.

NSWebpagina Basepagina Shell

Het rendert het HTML-document skelet, injecteert SEO-meta-tags, laadt CSS/JS-assets en delegeert inhoud naar subklassen via een abstract drawPage() methode.

NSAactiveWebpage AJAX Dispatcher

Extent NSWebpage met sessiebeheer en een evenement-gedreven AJAX-laag. NSEvent voorwerpen en geleid door een handleEvent() - Ik ben de afzender.

NSPpersistentObject Lichtgewicht ORM

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.

NSDatabase Singleton-verbinding

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.

Page klassen

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

De boeking Systeem

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.

Betalingsstroom

01Afstand Selecteer10 / 15 / 25 km
02DeelnemersgegevensAJAX adresupdate
03Metode SelecteeriDEAL / MC / PayPal
04EMS GatewayVerwijder naar buiten
05terugroepenPOST → inschrijven.php
06PDF + E-mailAutomatisch geproduceerd

Gegevensmodel

De boeking wordt in drie verbonden entiteiten verdeeld, waarbij de bezorgdheid zuiver gescheiden wordt gehouden:

ModelRolBelangrijke velden
BoekingEerste boekingsregister, informatie over de deelnemer, betalingsstatusboekingsnummer, toegangsleutel, staat, e-mail, geboortedatum
BoekingElementAfzonderlijke lijnstukken (één per afstand/slot geselecteerd)event_id, prijs, korting, hoeveelheid
BoekingBetalingOnveranderlijk log van betalingstransactiesbetaald, bedrag, valuta, merk, status, ipaddress
GebeurtenisUit database geladen raceconfiguratieEventnaam, volwassenprijs, slots, minimumleeftijd, actief

Staatsmachine

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

Slotbeheer

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.

Betalingspoort Integratie

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.

  1. 1
    Oprichting van de orde Wanneer de deelnemer op "Betaal" klikt, stuurt de cliënt een betalingsgebeurtenis (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.
  2. 2
    Gateway-omleiding De browser wordt doorgestuurd naar de door EMS gehoste betalingspagina. Alle gevoelige kaarthantering vindt plaats op het domein van de gateway de toepassing raakt nooit kaartnummers aan.
  3. 3
    Asynchrone terugroepen Na een vergunning geeft het EMS het resultaat aan: inschrijven.php. De beheerder valideert de HMAC-ondertekening om te bevestigen dat de reactie authentiek is, en leest vervolgens chargetotal, txndatetime, en de goedkeuringskode.
  4. 4
    Beëindiging Op 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.

PDF-ticketgeneratie met PDFlib

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.

Wat staat op het ticket ?

Foto van de kop

Een full-width branded image (pdf_ticketkop.jpg) aan de bovenkant van de pagina wordt de rasidentiteit en het jaar vastgesteld.

Routekaart

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.

Deelnemerblok

Naam, adres, postcode, stad en geselecteerde afstand worden in Helvetica-Bold getypt.

Toegangsleutel + boekingsnummer

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.

Hoe de bouw 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 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.

Transactionele e-mail Pipeline

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.

  1. 1
    Sjabloonbelasting De HTML-e-mail sjabloon wordt gelezen vanaf framework/mail_templates/reserveringsbevestiging.html een standalone HTML-bestand dat door niet-ontwikkelaars kan worden bewerkt zonder PHP aan te raken.
  2. 2
    Vervanging van tokens Plaatshouderstokens zoals [FIRSTNAME], [RESERVATIONNUMBER], en [BOOKINGDATA] De reserveringsopsommingstabel wordt weergegeven als inline HTML.
  3. 3
    PDF-bijlage Het pas gegenereerde ticket PDF (pdf_gen_tickets/{BookingNumber}.pdfDe deelnemers ontvangen hun ticket binnen enkele seconden na bevestiging van de betaling.
  4. 4
    SMTP-levering PHPMailer stuurt naar het adres van de deelnemer via 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();

Front End & 2025 Herontwerp

Het ontwerp van 2025 heeft een gedateerde lay-out vervangen door een modern, mobiel-eerste ontwerpsysteem terwijl de onderliggende PHP waar mogelijk intact blijft.

CSS aangepaste eigenschappen

Een single. :root Het wisselen van merkkleur voor een komend jaar vereist één bewerking, niet een vind-en-vervanging over 2000 lijnen.

Flexbox voor mobiele gebruik

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.

Geen grensradius opzettelijk

Elke kaart, knop en input heeft vierkante hoeken om het industriële gevoel van wedstrijdbeelden, trailmarkers en terreinkaarten te weerspiegelen.

Routekaarten van Leaflet.js

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.

AJAX-registratieformulier

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.

Jssor helden slider

Een full-viewport beeldslide op de startpagina door middel van racefotografie. Touch-swipe-ondersteuning werkt uit de doos op mobiel zonder extra configuratie.

Bouwen van artefacten

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.

PHP Verenigbaarheid De uitdaging

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:

  1. 1
    mysqli_* over mysql* Alle erfenis 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.
  2. 2
    Iteratorinterface PHP 8 splijt de oude Iterator interface in Iterator en IteratorAggregate Er zijn oplossingen toegevoegd voor het toepassen van de juiste interface, waarbij PHP 5.6 ze onveilig negeert.
  3. 3
    Syntax voor het indexeren van stringen PHP 8 depreceert het indexeren van strengen met krullende knoppen ($str{0}) Alle gebeurtenissen werden omgezet in bracket syntax ($str[0]), die geldig is voor beide versies.
  4. 4
    Geen type aanwijzingen voor parameters PHP 5.6 ondersteunt noch scalar type hints noch return types in functie handtekeningen.

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.

Volledige techniek Stapel

Elke uit praktische redenen gekozen component heeft geen kader voor zichzelf, geen afhankelijkheid die zijn gewicht niet trekt.

LayerTechnologieWaarom ?
ServertaalPHP 5.6 / 8Beperking van de aanbieding; geen keuze mogelijk
DatabaseMySQL (mysqli)Beschikbaar op de host; voldoende voor het gegevensvolume
KaderOp maat (NSWebpagina / MVC)Geen Laravel / Symfony op PHP 5.6 zonder oplossingen
PDF-generatiePDFlib (commerciële)Pixel-perfecte lay-out; deterministische output; lettertypen
Aflevering van e-mailsPHPMailer + Mailgun SMTPBetrouwbare leverbaarheid; BCC-auditsroute naar organisatoren
BetalingspoortEMS e-commerce (HMAC)Ondersteunt iDEAL, Mastercard, PayPal in één enkele integratie
KaartenLeaflet.js + OpenStreetMapOpen source; geen API-sleutel vereist; GPX-overlay triviaal
DOM-manipulatiejQuery 1.11.1Stabiel; werkt op de oude browser-reeks event deelnemers gebruiken
Held glijmiddelJssorTouch-swipe uit de doos; nul serverafhankelijkheid
CSS-architectuurCustom + CSS aangepaste eigenschappenGeen preprocessor nodig; op maat gemaakte eigenschappen ondersteund nationaal
Identificatie van het apparaatMobile_Detect.phpServerside breakpoint hint in aanvulling op media queries

Lezingen & Afleveringen

Wat dit project aantoont buiten de lijst met kenmerken.

Beperkingen leiden tot een goed ontwerp

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.

Huisgebouwde ORM ≠ slechte ORM

Het smerige vlaggenpatroon in NSPersistentObject Soms is het opnieuw uitvinden van een klein wiel beter dan het trekken van een groot wiel.

AJAX staat synchronisatie slaat verborgen velden

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.

PDF-generatie is geen commodity

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.

Ontwerpbeperkingen als merkbeslissingen

De no-border-radius-regel begon als een tijdbesparende snelweg en werd een opzettelijke merkkeuze.

Dubbele versie PHP is oplosbaar

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.

Projectafbeeldingen

Meer werk

Gerelateerde projecten

Start een project met ons

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