Transporter-Mobilanwendung
TST Terschelling
MooiWeer
Eine vollständige Registrierungs- und Ticketplattform für eine Trail Running-Veranstaltung auf einer niederländischen Waddensee-Insel , die von Grund auf mit einem benutzerdefinierten PHP-Framework, Live-Zahlungsverarbeitung, PDF-Ticket-Generation und automatisierter E-Mail-Lieferung gebaut wurde.
TrailRun Terschelling ist die Rennregistrierungs-Website für ein jährliches Wettlaufveranstaltung auf der Insel Terschelling in den Niederlanden. Teilnehmer können sich online für die 10 km, 15 km oder 25 km Entfernung anmelden, über mehrere niederländische Zahlungsmethoden bezahlen und sofort ein PDF-Rennticket mit einem einzigartigen Zugangsschlüssel erhalten.
Das Projekt umfasst die gesamte Teilnehmerreise von Ende zu Ende: ein informatives Frontend mit Rennrouten, die auf interaktiven Karten visualisiert werden, ein mehrstufiges AJAX-Registrierungsformular mit Server-Bestätigung, eine Live-Payment-Gateway-Integration, eine PDF-Generations-Pipeline und ein automatisiertes Transaktions-E-Mail-System alle durch ein heimisches leichtes MVC-Framework miteinander verbunden.
Im Jahr 2025 wurde das gesamte Frontend neu gestaltet (neue CSS-Architektur, benutzerdefinierte Eigenschaften, Flexbox-Layout, Mobile-First-Breakpoints) und das zugrunde liegende PHP wurde auf PHP 8 Kompatibilität geprüft, während die Codebase auf dem Produktionsserver bereitgestellt werden konnte, der auf PHP 5.6 beschränkt ist.
Anstatt Laravel oder Symfony zu erreichen, basiert dieses Projekt auf einem leichten benutzerdefinierten Framework, das speziell für diese Art von Veranstaltungsregistrierungs-Anwendungsfall entwickelt wurde. Das Framework ist klein, schnell und vom Entwickler vollständig verstanden.
Erstellt das HTML-Dokument-Skelett, injiziert SEO-Meta-Tags, lädt CSS/JS-Assets und delegiert Inhalte in Unterklassen durch eine Abstraktion drawPage() Methode.
Erweitert NSWebpage mit Sitzungsmanagement und einer ereignisorientierten AJAX-Schicht. NSEvent Gegenstände und durch eine handleEvent() Der Dispatcher.
Basisklasse für alle Datenbankmodelle. Implementiert ein schmutziges Flaggenmuster Eigenschaften werden nur in die Datenbank geschrieben, wenn sie geändert werden. loadObject(), store()und delete() ohne Schema-Reflexionsüberschuss.
Ein Singleton-Wrapper herum mysqli Das System stellt eine einzige Datenbankverbindung pro Anfrage sicher. escape() Hilfe und eine einheitliche Abfragenoberfläche, die von allen Modellen verwendet wird.
Jede Route führt zu einer Betonseitenklasse, die sich erstreckt TrailrunWebpage (sich selbst erstreckt) NSActiveWebpageDas Singleton der Seite wird in $_SESSION, erhalten Sie den Formularzustand über AJAX-Anrufe ohne versteckte Felder oder wiederholte Datenbank-Rundfahrten.
// 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 Die Registrierung erfolgt über ein mehrstufiges AJAX-Formular, das den Status bei jeder Änderung mit dem Server synchronisiert, so dass nichts verloren geht, wenn der Browser geschlossen wird oder die Sitzung vor der Zahlung abläuft.
Die Buchung wird in drei verwandte Einheiten aufgeteilt, wobei die Anliegen eindeutig getrennt sind:
| Modell | Rolle | Schlüsselfelder |
|---|---|---|
| Die Schlagzeile | Hauptbuchungsregister, Teilnehmerinformationen, Zahlungszustand | Buchingsnummer, Zugangsschlüssel, Staat, E-Mail, Geburtsdatum |
| Elemente für die Bewegung | Einzelne Linienpunkte (eine pro Abstand/Slot ausgewählt) | Event_id, Preis, Rabatt, Menge |
| Bezahlung | Unveränderliche Zahlungstransaktionsprotokolle | Zahlungsgegenstand, Betrag, Währung, Marke, Status, IP-Adresse |
| Ereignis | Aus der Datenbank geladene Rennkonfiguration | Name der Veranstaltung, Preis für Erwachsene, Slots, Mindestalter, Aktivität |
Jede Buchung führt durch eine strenge Reihe von Staaten, verhindert Doppelzahlungen und macht Auditfragen trivial:
STATENOPAYMENT (1) → Created, awaiting payment
STATEPAID (2) → Payment confirmed, PDF + email sent
STATECHANGE (3) → Booking modified post-payment
STATECANCELED (4) → Cancelled / refunded Die EventFactory die Datenbank bei Seitebelastung abfragt und die Anzahl der verfügbaren Slots pro Entfernung berechnet, indem die bestätigten Buchungen vom konfigurierten Maximum abgezogen werden. Wenn eine Entfernung ausverkauft wird, wird sie automatisch ohne manuelle Eingriffe aus der Auswahloberfläche entfernt.
Der Zugriffsschlüssel, der auf jeder BoekingElement wird mit Toolbox::MakeRandomPassword(8) eine kryptographisch zufällige 8-Zeichen-String, die auf dem PDF-Ticket gedruckt und vom Rennpersonal zur Überprüfung der Teilnehmer an der Startlinie verwendet wird.
Die Zahlungen werden über das EMS-E-Commerce-Gateway verarbeitet, das die drei wichtigsten Methoden für eine niederländische Sportveranstaltung unterstützt: iDEAL (direkte Banküberweisung), Mastercard und PayPal.
payment_ideal, payment_mastercardoder payment_paypal) über AJAX. Der Server ruft EMSPayment::sendPaymentRequest(), die eine HMAC-Signatur über die Auftragsparameter berechnet und eine Umleitung URL zurückgibt. inschrijven.php. Der Handler bestätigt die HMAC-Signatur, um zu bestätigen, dass die Antwort echt ist, und liest dann chargetotal, txndatetime, und der Genehmigungscode. approval_code == 'Y' und status == 'APPROVED', die Buchung ist gezahlt gekennzeichnet, die vollständige Transaktionsunterlagen werden an boeking_payment, und die PDF + E-Mail-Pipeline wird ausgelöst. PAYMENTFAILURE und der Benutzer sieht einen freundlichen Fehler. Die Codebase enthält auch eine vergangene Ogone-Zahlungsintegration das Vorgänger-Gateway. Sie ist nicht mehr aktiv, sondern wurde als Referenz in der Codebase aufbewahrt, da einige historische Buchungen über sie gemacht wurden und das Antwortformat immer noch in Kommentaren dokumentiert wird.
Jede bestätigte Registrierung erzeugt eine personalisierte, druckbereite PDF-Rennenkarte. PDFlib, eine kommerzielle PDF-Schaffungsbibliothek, die wegen ihrer präzisen Layout-Steuerung und zuverlässiger Schriftwiedergabe ausgewählt wurde.
Die Fahrkartenfabrik (framework/factories/PDFTicket.php) komponiert das Dokument programmatisch es gibt keine Vorlagedatei, keine HTML-zu-PDF-Konvertierung und kein Zwischen-Render-Schritt. Jedes Element wird mit exakten Punktkoordinaten auf einer A4-Leinwand (595 × 842 pt) platziert.
Ein vollwertiges Markenbild (pdf_ticketkop.jpg) oben auf der Seite wird die Rassenidentität und das Jahr festgelegt.
Ein entfernungsspezifisches Kartenbild (pdf_10KM.jpg, pdf_15KM.jpg, pdf_25KM.jpg) wird auf der Grundlage der gebuchten Veranstaltung zur Laufzeit geladen und jedem Ticket eine einzigartige visuelle Signatur verleiht.
Name, Adresse, Postcode, Stadt und ausgewählte Entfernung werden in Helvetica-Bold eingestuft. Das Renndatum (28-02-2027) und die entfernungsspezifische Startzeit (13:00 / 12:30 / 11:45) werden in großem Typ gedruckt.
Der 8-Zeichen-Zugangsschlüssel wird für die Start-Line-Verifizierung hervorragend gedruckt.
// 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 auf pdf_gen_tickets/{BookingNumber}.pdf auf der Festplatte, wo sie sofort zur Verfügung steht, um die Bestätigungs-E-Mail beizufügen.
PDFlib wurde über HTML-zu-PDF-Konverter wie wkhtmltopdf gewählt, weil es bietet Deterministische Pixel-perfekte Layout Für ein Ticketing-System, in dem ein fehlerhafter Element das Rennpersonal verwirren könnte, lohnt sich dieser Kompromiss.
Die E-Mails zur Bestätigung werden in dem Moment verschickt, in dem eine Zahlung überprüft wird. PostOffice Fabrik wickelt PHPMailer und routert die ausgehende Mail durch Mailguns SMTP-Relee für eine zuverlässige Lieferbarkeit.
framework/mail_templates/reserveringsbevestiging.html eine eigenständige HTML-Datei, die von Nichtentwicklern bearbeitet werden kann, ohne PHP zu berühren. [FIRSTNAME], [RESERVATIONNUMBER]und [BOOKINGDATA] Die Reservierungsaufschlusstabelle wird als Inline-HTML dargestellt. pdf_gen_tickets/{BookingNumber}.pdfDie Teilnehmer erhalten ihr Ticket innerhalb von Sekunden nach der Zahlungsbestätigung. smtp.eu.mailgun.org:25Eine schweigende BCC wird an die Veranstalter der Veranstaltung für ihre eigenen Aufzeichnungen kopiert. Schreiben Sie die Bestätigung 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(); Die Neugestaltung von 2025 ersetzte ein datiertes Layout durch ein modernes, mobil-erst-Design-System, während die zugrunde liegende PHP möglichst unberührt gehalten wurde. Die Designsprache spiegelt absichtlich den robusten Außencharakter der Veranstaltung wider.
Ein einzelner :root Die Art und Weise, wie die Markenfarben für ein zukünftiges Jahr ausgetauscht werden, erfordert eine Bearbeitung, nicht eine Finding-and-Replacement über 2000 Zeilen.
Die Navigation kollabiert zu einem Hamburgermenü auf dem Handy; Rennkarten fließen von einem mehrspaltenden Gitter zu einer einzigen Spalte ohne Layout JavaScript zurück.
Jede Karte, jeder Knopf und jeder Eingabe haben quadratische Ecken, um das industrielle Gefühl von Rennfahrzeugen, Streckenmarkierungen und Geländemappen nachzudrücken.
Jede Rennstrecke verfügt über eine spezielle GPX-Route, die auf einer OpenStreetMap-Fliesen-Schicht über das Leaflet erstellt wird.#99c038) und animierte Markierungen zeigen wichtige Wegpunkte an.
Die RegisterPage JavaScript-Klasse sendet eine addressupdate Bei jeder Feldänderung wird ein Ereignis an den Server übertragen. Serverseitige Validierungsfehler werden als JSON zurückgegeben und inline angezeigt keine vollständige Seite neu geladen, keine verlorenen Daten.
Ein Full-Viewport-Bild-Slider auf der Startseite durch Rennfotografie. Touch-Swipe-Unterstützung funktioniert aus der Box auf Mobilgeräten ohne zusätzliche Konfiguration.
CSS ist in css/design.css (1752 Zeilen) und minifiziert design.min.css JavaScript für das Registrierungsformular wird von js/inschrijven.js zu inschrijven.min.jsDie ... .htaccess verwendet gzip-Kompression für CSS-, JS- und Schriftdateien und setzt aggressive Cache-Header für statische Assets.
Die Produktions-Hosting-Umgebung ist auf PHP 5.6 beschränkt, aber die Codebase muss auch eine PHP 8 Syntaxprüfung bestehen und sauber unter PHP 8 auf der Entwicklungsmaschine laufen. Dies ist die schwierigste Einschränkung für das Projekt.
Die Arbeiten 2025 beinhalteten die Prüfung jeder PHP-Datei auf Features, die sich über die Versionlücke hinweg unterschiedlich verhalten:
mysql_query(), mysql_fetch_array(), und damit verbundene Anrufe wurden durch ihre mysqli_* Äquivalente, die sowohl in PHP 5.6 als auch in PHP 8 erhältlich sind. Iterator Schnittstelle in Iterator und IteratorAggregate Mit einer strengeren Durchsetzung des Return-Types wurden benutzerdefinierte Sammelklassen aktualisiert, um die richtige Schnittstelle zu implementieren, und Return-Typ-Erklärungen wurden hinzugefügt, wenn PHP 5.6 sie harmlos ignoriert. $str{0}) Alle Ereignisse wurden in Bracket-Syntax umgewandelt ($str[0]), die für beide Versionen gültig ist. Schreiben PHP, die auf beiden läuft 5.6 und 8 Die Strategie bestand darin, nur die Teilmenge von PHP zu verwenden, die beide Versionen teilen, und jede Funktion zu vermeiden, die nach 5.6 eingeführt wurde, für die PHP 8 später das Verhalten geändert hat.
Jede aus praktischen Gründen gewählte Komponente kein Rahmen für sich selbst, keine Abhängigkeit, die ihr Gewicht nicht zieht.
| Schicht | Technologie | Warum? |
|---|---|---|
| Serversprache | PHP 5.6 / 8 | Aufnahmegeschränkung; keine Wahl möglich |
| Datenbank | MySQL (mysqli) | Verfügbar auf Host; ausreichend für das Datenvolumen |
| Rahmenbedingungen | Benutzerdefinierte (NSWebseite / MVC) | Keine Laravel / Symfony auf PHP 5.6 ohne Lösungen |
| PDF-Generation | PDFlib (kommerziell) | Pixel-perfekte Layout; deterministische Ausgabe; Schriftzeichen Einbettung |
| E-Mail-Lieferung | PHPMailer + Mailgun SMTP | Zuverlässige Lieferfähigkeit; BCC-Audit-Pfad an die Organisatoren |
| Zahlungsgateway | EMS E-Commerce (HMAC) | Unterstützt iDEAL, Mastercard, PayPal in einer einzigen Integration |
| Karten | Leaflet.js + OpenStreetMap | Open-Source; keine API-Schlüssel erforderlich; GPX-Overlay trivial |
| DOM-Manipulation | jQuery 1.11.1 | Stabil; Arbeiten auf dem alten Browser-Range Event-Teilnehmer verwenden |
| Heldenschieber | Jssor | Touch-Swipe aus der Box; Null-Server-Abhängigkeit |
| CSS-Architektur | benutzerdefinierte CSS-Eigenschaften | Keine Vorverarbeitung benötigt; benutzerdefinierte Eigenschaften unterstützt nativ |
| Nachweis von Geräten | Mobile_Detect.php | Hinweis auf Server-Breakpoint zusätzlich zu Medienanfragen |
Was dieses Projekt über die Feature-Liste hinaus zeigt.
Die PHP 5.6 Einschränkung zwang ein benutzerdefiniertes Framework, das klein, vollständig verständlich und leicht zu überprüfen ist. Keine versteckte Magie, keine Versioninkompatibilitäten, die im Lieferantenkode vergraben sind.
Das schmutzige Flagge-Muster in NSPersistentObject Es ist ein legitimes Muster, das in vielen großen Rahmenwerken verwendet wird. Manchmal ist es besser, ein kleines Rad neu zu erfinden, als ein großes zu ziehen.
Speichern Sie die Seite Singleton in $_SESSION Die Synchronisierung bei jeder Feldänderung bedeutet, dass die Teilnehmerdaten sicher sind, auch wenn der Browser im mittleren Format abstürzt.
Für ein Ticketing-System, in dem die Layoutgenauigkeit für den betrieblichen Gebrauch wichtig ist, war eine richtige PDF-Bibliothek die Lizenzgebühr wert.
Die Regel, keine Grenzstraßen zu haben, begann als eine Zeitersparung und wurde zu einer bewussten Markenwahl.
Das Schreiben für PHP 5.6 und PHP 8 gleichzeitig ist selten, aber handlungsfähig. Der Schlüssel ist, genau zu wissen, welche Funktionen zu vermeiden sind und die Beschränkung für zukünftige Mitarbeiter klar zu dokumentieren.
Weitere Arbeiten
TST Terschelling
TransportMaster
TransportMaster