Softwareentwicklung

Neugestaltung der TrailRun-Website

MooiWeer

Auftraggeber
MooiWeer
Kategorie
Softwareentwicklung
Jahr
2025
Technologien
PHPJavascriptMySQLPDFLib
TrailRun Terschelling

Eine vollständig entwickelte Anmelde- und Ticketplattform für einen Traillauf auf einer niederländischen Watteninsel — von Grund auf umgesetzt mit einem eigenen PHP-Framework, direkter Zahlungsabwicklung, PDF-Tickets und automatischem E-Mail-Versand.

3Laufdistanzen
~22kPHP-Codezeilen
PDFlibTicket-Erstellung
iDEALHauptzahlungsart
2025Jahr der Neugestaltung

Worum geht es bei diesem Projekt?

TrailRun Terschelling ist die Anmeldewebsite für einen jährlichen Traillauf auf der niederländischen Insel Terschelling. Teilnehmer können sich online für 10 km, 15 km oder 25 km anmelden, mit verschiedenen in den Niederlanden üblichen Zahlungsarten bezahlen und sofort ein PDF-Ticket mit eindeutigem Zugangsschlüssel erhalten.

Das Projekt deckt den gesamten Teilnehmerablauf ab: Informationen mit interaktiven Streckenkarten, ein mehrstufiges AJAX-Anmeldeformular mit serverseitiger Prüfung, eine produktive Zahlungsanbindung, PDF-Erzeugung und automatische Transaktions-E-Mails. Alles wird durch ein eigenes schlankes MVC-Framework verbunden.

2025 wurde das gesamte Frontend neu gestaltet: mit neuer CSS-Architektur, Custom Properties, Flexbox-Layout und auf Mobilgeräte ausgerichteten Breakpoints. Zugleich wurde der PHP-Code auf PHP-8-Kompatibilität geprüft, während er weiterhin auf dem Produktionsserver mit seiner Beschränkung auf PHP 5.6 einsetzbar bleiben musste.

PHP 5.6 / 8MySQLPDFlibPHPMailerMailgun SMTPEMS Payment GatewayiDEALjQueryLeaflet.jsCSS Custom Properties

Eigenes MVC-Framework

Statt Laravel oder Symfony verwendet das Projekt ein schlankes eigenes Framework, das speziell für solche Veranstaltungsanmeldungen entwickelt wurde. Es ist klein, schnell und technisch vollständig nachvollziehbar — ohne verborgene Mechanismen und unnötigen Ballast.

NSWebpage — Grundgerüst der Seite

Erzeugt das HTML-Dokumentgerüst, ergänzt SEO-Metatags, lädt CSS- und JavaScript-Dateien und übergibt die Inhaltserstellung über eine abstrakte drawPage()-Methode an Unterklassen.

NSActiveWebpage — AJAX-Verteilung

Erweitert NSWebpage um Sitzungsverwaltung und eine ereignisgesteuerte AJAX-Schicht. Eingehende Anfragen werden als NSEvent-Objekte ausgewertet und über einen handleEvent()-Dispatcher weitergeleitet.

NSPersistentObject — schlankes ORM

Basisklasse aller datenbankgestützten Modelle. Ein Änderungsflag sorgt dafür, dass Eigenschaften nur bei Änderungen in die Datenbank geschrieben werden. Die Klasse bietet loadObject(), store() und delete() ohne zusätzlichen Aufwand für die dynamische Untersuchung des Schemas.

NSDatabase — zentrale Verbindung

Eine Singleton-Hülle um mysqli stellt eine einzige Datenbankverbindung je Anfrage sicher. Sie bietet eine escape()-Hilfsfunktion und eine einheitliche Abfrageschnittstelle für alle Modelle.

Seitenklassen

Jede Route führt zu einer konkreten Seitenklasse, die TrailrunWebpage erweitert; diese Klasse erweitert wiederum NSActiveWebpage. Die einzelne Seiteninstanz wird in $_SESSION gespeichert. So bleibt der Formularzustand über AJAX-Aufrufe hinweg erhalten, ohne versteckte Felder oder wiederholte Datenbankzugriffe.

// 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

Das Buchungssystem

Die Anmeldung erfolgt über ein mehrstufiges AJAX-Formular, das jede Änderung mit dem Server abgleicht. Dadurch gehen vor der Zahlung keine Angaben verloren, wenn der Browser geschlossen wird oder die Sitzung abläuft.

Zahlungsablauf

01Distanz wählen10 / 15 / 25 km
02TeilnehmerdatenAJAX addressupdate
03Zahlungsart wähleniDEAL / MC / PayPal
04EMS GatewayWeiterleitung
05RückmeldungPOST → inschrijven.php
06PDF und E-MailAutomatisch erzeugt

Datenmodell

Die Buchung ist in drei zusammenhängende Entitäten aufgeteilt, damit die Zuständigkeiten klar getrennt bleiben:

ModellAufgabeZentrale Felder
BoekingHauptdatensatz der Buchung, Teilnehmerdaten und Zahlungsstatusboekingsnummer, accesskey, state, email, birthdate
BoekingElementEinzelpositionen, eine je gewählter Distanz beziehungsweise Startplatzevent_id, price, discount, quantity
BoekingPaymentUnveränderbares Protokoll der Zahlungstransaktionenpaymentid, amount, currency, brand, status, ipaddress
EventAus der Datenbank geladene Veranstaltungseinstellungeneventname, adultprice, slots, minimumage, active

Zustandsautomat

Jede Buchung durchläuft festgelegte Zustände. Das verhindert doppelte Zahlungen und erleichtert Prüfungsabfragen:

STATENOPAYMENT  (1)  → Created, awaiting payment
STATEPAID       (2)  → Payment confirmed, PDF + email sent
STATECHANGE     (3)  → Booking modified post-payment
STATECANCELED   (4)  → Cancelled / refunded

Verwaltung der Startplätze

Die EventFactory fragt beim Laden der Seite die Datenbank ab und berechnet die verfügbaren Plätze je Distanz, indem sie bestätigte Buchungen vom festgelegten Maximum abzieht. Ist eine Distanz ausgebucht, wird sie automatisch aus der Auswahl entfernt, ohne manuellen Eingriff.

Der in jedem BoekingElement gespeicherte Zugangsschlüssel wird mit Toolbox::MakeRandomPassword(8) erzeugt: eine kryptografisch zufällige Zeichenfolge mit 8 Zeichen, die auf dem PDF-Ticket steht und vom Veranstaltungsteam am Start zur Teilnehmerprüfung genutzt wird.

Anbindung des Zahlungsanbieters

Zahlungen laufen über das EMS e-Commerce Gateway. Es unterstützt die drei für eine niederländische Sportveranstaltung besonders relevanten Verfahren: iDEAL als direkte Bankzahlung, Mastercard und PayPal.

  1. 1
    Auftrag anlegenWenn ein Teilnehmer „Bezahlen“ wählt, sendet der Client per AJAX ein Zahlungsereignis (payment_ideal, payment_mastercard oder payment_paypal). Der Server ruft EMSPayment::sendPaymentRequest() auf. Diese Methode berechnet eine HMAC-Signatur über die Auftragsparameter und liefert eine Weiterleitungs-URL zurück.
  2. 2
    Weiterleitung zum ZahlungsanbieterDer Browser wird auf die von EMS gehostete Zahlungsseite weitergeleitet. Alle sensiblen Kartendaten werden auf der Domain des Zahlungsanbieters verarbeitet; die Anwendung selbst erhält keine Kartennummern.
  3. 3
    Asynchrone RückmeldungNach der Autorisierung sendet EMS das Ergebnis per POST an inschrijven.php. Die Verarbeitung prüft die HMAC-Signatur, um die Echtheit der Antwort sicherzustellen, und liest anschließend chargetotal, txndatetime und den Freigabecode aus.
  4. 4
    AbschlussBei approval_code == 'Y' und status == 'APPROVED' wird die Buchung als bezahlt markiert, der vollständige Transaktionsdatensatz in boeking_payment gespeichert und die PDF- und E-Mail-Verarbeitung gestartet. Jedes andere Ergebnis führt zu PAYMENTFAILURE und einer verständlichen Fehlermeldung für den Benutzer.
Die Codebasis enthält außerdem noch die alte Ogone-Zahlungsanbindung als Vorgänger. Sie ist nicht mehr aktiv, bleibt aber als Referenz erhalten, weil frühere Buchungen darüber abgewickelt wurden und das Antwortformat weiterhin in Kommentaren dokumentiert ist.

PDF-Tickets mit PDFlib erzeugen

Jede bestätigte Anmeldung erzeugt ein persönliches, druckfertiges PDF-Ticket. Es wird serverseitig mit PDFlib erstellt, einer kommerziellen PDF-Bibliothek, die wegen ihrer präzisen Layoutsteuerung und zuverlässigen Schriftdarstellung gewählt wurde.

Die Ticket-Factory (framework/factories/PDFTicket.php) erstellt das Dokument per Code — ohne Vorlagendatei, HTML-zu-PDF-Konvertierung oder zwischengeschalteten Renderschritt. Jedes Element wird mit exakten Punktkoordinaten auf einer A4-Seite mit 595 × 842 pt platziert.

Was auf dem Ticket steht

Kopfbild

Ein markentypisches Bild über die gesamte Seitenbreite (pdf_ticketkop.jpg) steht am oberen Rand und macht Veranstaltung und Jahr erkennbar.

Streckenkarte

Je nach gebuchter Distanz wird zur Laufzeit die passende Karte (pdf_10KM.jpg, pdf_15KM.jpg oder pdf_25KM.jpg) geladen. So erhält jedes Ticket eine eindeutige visuelle Zuordnung zur Strecke.

Teilnehmerdaten

Name, Adresse, Postleitzahl, Ort und gewählte Distanz werden in Helvetica-Bold gesetzt. Das Veranstaltungsdatum 28-02-2027 und die jeweilige Startzeit 13:00 / 12:30 / 11:45 stehen in großer Schrift.

Zugangsschlüssel und Buchungsnummer

Der Zugangsschlüssel mit 8 Zeichen wird für die Kontrolle am Start gut sichtbar gedruckt. Die Buchungsnummer erscheint in weißer Courier-Schrift auf dem dunklen Fußbereich, damit das Veranstaltungsteam sie schnell erfassen kann.

So wird das Dokument erstellt

// 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("");

Das fertige PDF wird unter pdf_gen_tickets/{BookingNumber}.pdf gespeichert und steht sofort als Anhang der Bestätigungs-E-Mail bereit.

PDFlib wurde gegenüber HTML-zu-PDF-Konvertern wie wkhtmltopdf bevorzugt, weil es ein deterministisches, pixelgenaues Layout bietet. Dafür ist eine kommerzielle Lizenz erforderlich. Bei einem Ticketsystem, dessen verschobene Elemente das Veranstaltungsteam verwirren könnten, lohnt sich diese Abwägung.

Der Ablauf der Transaktions-E-Mails

Bestätigungs-E-Mails werden unmittelbar nach der Zahlungsprüfung versendet — ohne Warteschlange und Verzögerung. Die PostOffice-Factory kapselt PHPMailer und leitet ausgehende Nachrichten für eine zuverlässige Zustellung über das SMTP-Relay von Mailgun.

  1. 1
    Vorlage ladenDie HTML-E-Mail-Vorlage wird aus framework/mail_templates/reserveringsbevestiging.html gelesen. Diese eigenständige HTML-Datei kann auch ohne Programmierkenntnisse bearbeitet werden, ohne PHP-Code anzutasten.
  2. 2
    Platzhalter ersetzenPlatzhalter wie [FIRSTNAME], [RESERVATIONNUMBER] und [BOOKINGDATA] werden zur Laufzeit durch die Angaben des Teilnehmers ersetzt. Die tabellarische Buchungsübersicht wird als direkt eingebettetes HTML erzeugt.
  3. 3
    PDF anhängenDas neu erzeugte PDF-Ticket (pdf_gen_tickets/{BookingNumber}.pdf) wird vor dem Versand angehängt. Teilnehmer erhalten es innerhalb weniger Sekunden nach der Zahlungsbestätigung.
  4. 4
    Versand per SMTPPHPMailer sendet über smtp.eu.mailgun.org:25 an die Adresse des Teilnehmers. Eine verdeckte BCC-Kopie geht für die Dokumentation an die Veranstalter. Der Betreff enthält die Buchungsnummer: 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 und Neugestaltung 2025

Die Neugestaltung 2025 ersetzte ein veraltetes Layout durch ein modernes Designsystem mit Schwerpunkt auf Mobilgeräten. Der zugrunde liegende PHP-Code blieb dabei nach Möglichkeit unverändert. Die Bildsprache greift bewusst den rauen Outdoor-Charakter der Veranstaltung auf.

CSS Custom Properties

Ein einziger :root-Block definiert die gesamte Farbpalette und Abstandsskala. Markenfarben für ein künftiges Veranstaltungsjahr lassen sich damit an einer Stelle ändern, statt 2000 Zeilen durchsuchen und ersetzen zu müssen.

Flexbox mit Schwerpunkt auf Mobilgeräten

Breakpoints liegen bei 767 px und 1023 px. Auf Mobilgeräten wird die Navigation zum Hamburger-Menü. Die Distanzkarten wechseln ohne JavaScript für das Layout vom mehrspaltigen Raster zu einer einzigen Spalte.

Bewusst ohne abgerundete Ecken

Durchgängig klare Kanten: Jede Karte, Schaltfläche und Eingabe hat rechteckige Ecken, passend zur sachlichen Gestaltung von Startnummern, Wegmarkierungen und Geländekarten. Das ist eine bewusste Gestaltungsregel, kein Versehen.

Streckenkarten mit Leaflet.js

Für jede Distanz wird eine eigene GPX-Strecke über Leaflet auf OpenStreetMap-Karten dargestellt. Die Liniengestaltung verwendet das Markengrün #99c038; animierte Markierungen zeigen wichtige Wegpunkte.

AJAX-Anmeldeformular

Die JavaScript-Klasse RegisterPage sendet bei jeder Feldänderung ein addressupdate-Ereignis an den Server. Serverseitige Validierungsfehler werden als JSON zurückgegeben und direkt am Formular angezeigt — ohne vollständiges Neuladen und ohne Datenverlust.

Großer Bildslider mit Jssor

Auf der Startseite wechselt ein bildschirmfüllender Slider zwischen Veranstaltungsfotos. Wischgesten auf Mobilgeräten werden ohne zusätzliche Konfiguration unterstützt.

Erzeugte Produktionsdateien

Das CSS wird in css/design.css mit 1752 Zeilen gepflegt und für den Produktivbetrieb zu design.min.css minimiert. Das JavaScript des Anmeldeformulars wird von js/inschrijven.js zu inschrijven.min.js minimiert. Die .htaccess aktiviert gzip-Komprimierung für CSS-, JavaScript- und Schriftdateien und setzt weitreichende Cache-Header für statische Dateien.

Die Herausforderung der PHP-Kompatibilität

Die produktive Hosting-Umgebung ist auf PHP 5.6 beschränkt. Gleichzeitig muss der Code eine PHP-8-Syntaxprüfung bestehen und auf dem Entwicklungsrechner sauber unter PHP 8 laufen. Das ist die anspruchsvollste einzelne Vorgabe dieses Projekts.

2025 wurde jede PHP-Datei auf Funktionen geprüft, die sich zwischen diesen Versionen unterschiedlich verhalten:

  1. 1
    mysqli_* statt mysql_*Alle alten Aufrufe von mysql_query(), mysql_fetch_array() und verwandten Funktionen wurden durch ihre mysqli_*-Entsprechungen ersetzt, die sowohl unter PHP 5.6 als auch unter PHP 8 verfügbar sind.
  2. 2
    Iterator-Schnittstelle Collection-Klassen müssen die passende Iterator-Schnittstelle implementieren. Sowohl Iterator als auch IteratorAggregate gab es bereits vor PHP 8. Code, der unter PHP 5.6 laufen muss, kann keine Rückgabetypdeklarationen verwenden: Diese Version unterstützt die entsprechende Syntax nicht. Die Typen werden stattdessen in Docblocks dokumentiert. Ab PHP 8.1 sind zudem vorläufige Rückgabetypen interner Schnittstellen zu berücksichtigen.
  3. 3
    Syntax für ZeichenkettenindizesPHP 8 unterstützt die Indizierung mit geschweiften Klammern ($str{0}) nicht mehr. Alle Vorkommen wurden auf die Syntax mit eckigen Klammern ($str[0]) umgestellt, die in beiden Versionen gültig ist.
  4. 4
    Keine Typangaben für ParameterPHP 5.6 unterstützt weder skalare Typangaben noch Rückgabetypen in Funktionssignaturen. Der gesamte Code wurde ohne diese geschrieben; stattdessen unterstützen Docblock-Kommentare die Entwicklungsumgebung.

PHP-Code, der sowohl unter 5.6 als auch unter 8 läuft, ist schwieriger zu schreiben als Code für nur eine Version. Die Strategie lautete: ausschließlich den gemeinsamen Funktionsumfang verwenden und Funktionen vermeiden, die nach 5.6 eingeführt und deren Verhalten später in PHP 8 verändert wurde.

Alle Technologien

Jede Komponente wurde aus praktischen Gründen gewählt — kein Framework um seiner selbst willen und keine Abhängigkeit ohne konkreten Nutzen.

SchichtTechnologieBegründung
ServersprachePHP 5.6 / 8Vorgabe der Hosting-Umgebung; keine Wahlmöglichkeit
DatenbankMySQL (mysqli)Auf dem Host verfügbar und für das Datenvolumen ausreichend
FrameworkEigenentwicklung (NSWebpage / MVC)Laravel / Symfony auf PHP 5.6 nicht ohne Umwege einsetzbar
PDF-ErzeugungPDFlib (kommerziell)Pixelgenaues Layout, deterministische Ausgabe und Schrifteinbettung
E-Mail-VersandPHPMailer + Mailgun SMTPZuverlässige Zustellung und BCC-Dokumentation für die Veranstalter
ZahlungsanbindungEMS e-Commerce (HMAC)Unterstützt iDEAL, Mastercard und PayPal über eine einzige Anbindung
KartenLeaflet.js + OpenStreetMapOpen Source, kein API-Schlüssel erforderlich, einfache GPX-Einblendung
DOM-ManipulationjQuery 1.11.1Stabil und kompatibel mit den älteren Browsern der Teilnehmer
Bildslider im KopfbereichJssorWischgesten ohne Zusatzkonfiguration, keine Serverabhängigkeit
CSS-ArchitekturEigenentwicklung und CSS Custom PropertiesKein Präprozessor nötig, native Unterstützung für Custom Properties
GeräteerkennungMobile_Detect.phpServerseitiger Hinweis für Breakpoints ergänzend zu Media Queries

Erkenntnisse und Lehren

Was dieses Projekt über seine Funktionsliste hinaus zeigt.

Vorgaben fördern gutes Design

Die Beschränkung auf PHP 5.6 führte zu einem eigenen Framework, das klein, vollständig nachvollziehbar und leicht prüfbar ist. Keine verborgenen Mechanismen und keine Versionskonflikte, die tief im Drittanbietercode stecken.

Ein eigenes ORM ist nicht automatisch ein schlechtes ORM

Das Änderungsflag-Muster in NSPersistentObject ist ein bewährter Ansatz, den auch viele große Frameworks verwenden. Manchmal ist es sinnvoller, ein kleines Rad neu zu bauen, als ein großes hinzuzunehmen.

AJAX-Zustandsabgleich statt versteckter Felder

Die Speicherung der Seiteninstanz in $_SESSION und die Synchronisierung nach jeder Feldänderung schützen Teilnehmerdaten selbst dann, wenn der Browser mitten im Formular abstürzt. Versteckte Eingabefelder würden das Neuladen des Tabs nicht überstehen.

PDF-Erzeugung ist keine austauschbare Nebensache

HTML-zu-PDF-Konverter sind bequem, liefern aber schwankende Ergebnisse. Für ein Ticketsystem, dessen Layoutgenauigkeit im Betrieb wichtig ist, war eine vollwertige PDF-Bibliothek die Lizenzgebühr wert.

Gestaltungsregeln als Markenentscheidung

Der Verzicht auf abgerundete Ecken begann als zeitsparende Vereinfachung und wurde zur bewussten Markenentscheidung. Klare Kanten passen zu einem Lauf durch Dünen, Wald und Strand.

PHP für zwei Versionen ist machbar

Gleichzeitig für PHP 5.6 und PHP 8 zu entwickeln ist ungewöhnlich, aber lösbar. Entscheidend ist, genau zu wissen, welche Funktionen vermieden werden müssen, und diese Vorgabe für künftige Mitwirkende klar zu dokumentieren.

Projektbilder

Weitere Arbeiten

Ähnliche Projekte

Starten Sie ein Projekt mit uns

Sie haben ein Projekt im Sinn? Sprechen wir darüber, wie wir Sie unterstützen können.