Application mobile Transporter
TST Terschelling
MooiWeer
Une plateforme complète d’inscription et de billetterie pour un trail sur une île néerlandaise de la mer des Wadden, développée de toutes pièces avec un framework PHP sur mesure, le traitement des paiements en ligne, la génération de billets PDF et l’envoi automatique d’e-mails.
TrailRun Terschelling est le site d’inscription à un trail organisé chaque année sur l’île de Terschelling, aux Pays-Bas. Les participants peuvent s’inscrire en ligne aux épreuves de 10 km, 15 km ou 25 km, régler leur inscription avec plusieurs moyens de paiement utilisés aux Pays-Bas et recevoir immédiatement un billet de course PDF muni d’une clé d’accès unique.
Le projet couvre l’ensemble du parcours des participants : une interface d’information présentant les itinéraires sur des cartes interactives, un formulaire d’inscription AJAX en plusieurs étapes avec validation côté serveur, l’intégration d’une passerelle de paiement opérationnelle, une chaîne de génération de PDF et un système automatisé d’e-mails transactionnels. Le tout repose sur un framework MVC léger développé en interne.
En 2025, toute l’interface a été repensée : nouvelle architecture CSS, propriétés personnalisées, mise en page flexbox et points de rupture conçus d’abord pour le mobile. Le code PHP sous-jacent a également été audité pour sa compatibilité avec PHP 8, tout en restant déployable sur le serveur de production, limité à PHP 5.6.
Ce projet repose sur un framework léger développé spécialement pour ce type d’inscription à des événements, plutôt que sur Laravel ou Symfony. Il est compact, rapide et entièrement maîtrisé par son développeur, sans mécanismes opaques ni complexité superflue.
Génère la structure du document HTML, insère les balises méta pour le référencement, charge les ressources CSS/JS et délègue le contenu aux sous-classes au moyen d’une méthode abstraite drawPage().
Étend NSWebpage avec la gestion des sessions et une couche AJAX événementielle. Les requêtes entrantes sont analysées et transformées en objets NSEvent, puis acheminées vers le répartiteur handleEvent().
Classe de base de tous les modèles adossés à la base de données. Elle utilise un indicateur de modification : seules les propriétés modifiées sont écrites dans la base. Elle fournit loadObject(), store()et delete() sans surcoût lié à l’introspection du schéma.
Une classe enveloppe de type singleton autour de mysqli qui garantit une seule connexion à la base de données par requête. Elle expose une méthode utilitaire escape() et une interface de requêtes cohérente utilisée par tous les modèles.
Chaque route correspond à une classe de page concrète qui étend TrailrunWebpage (qui étend elle-même NSActiveWebpage). L’instance singleton de la page est stockée dans $_SESSION, ce qui conserve l’état du formulaire entre les appels AJAX sans champs cachés ni allers-retours répétés vers la base de données.
// Tout le routage passe par .htaccess → index.php
// index.php dirige la requête vers la bonne classe de page
IndexPage → Accueil / diaporama principal / actualités
RegisterPage → Formulaire d’inscription en plusieurs étapes
RoutesPage → Carte Leaflet + tracé GPX superposé
InformatiePage → Informations statiques sur la course L’inscription s’effectue à l’aide d’un formulaire AJAX en plusieurs étapes dont l’état est synchronisé avec le serveur à chaque modification. Aucune donnée n’est ainsi perdue si le navigateur est fermé ou si la session expire avant le paiement.
La réservation est répartie entre trois entités liées, afin de séparer clairement les responsabilités :
| Modèle | Rôle | Champs principaux |
|---|---|---|
| Boeking | Enregistrement principal de la réservation, informations du participant, état du paiement | boekingsnummer, accesskey, state, email, birthdate |
| BoekingElement | Lignes individuelles (une par distance ou place sélectionnée) | event_id, price, discount, quantity |
| BoekingPayment | Journal immuable des transactions de paiement | paymentid, amount, currency, brand, status, ipaddress |
| Event | Configuration de la course chargée depuis la base de données | eventname, adultprice, slots, minimumage, active |
Chaque réservation suit une série d’états strictement définie, ce qui évite les doubles paiements et simplifie les requêtes d’audit :
STATENOPAYMENT (1) → Créée, en attente de paiement
STATEPAID (2) → Paiement confirmé, PDF et e-mail envoyés
STATECHANGE (3) → Réservation modifiée après paiement
STATECANCELED (4) → Annulée / remboursée La classe EventFactory interroge la base de données au chargement de la page et calcule les places disponibles pour chaque distance en soustrayant les réservations confirmées du maximum configuré. Lorsqu’une distance est complète, elle disparaît automatiquement de l’interface de sélection, sans intervention manuelle.
La clé d’accès enregistrée dans chaque BoekingElement est générée avec Toolbox::MakeRandomPassword(8). Il s’agit d’une chaîne de 8 caractères aléatoires générée de manière cryptographiquement sûre, imprimée sur le billet PDF et utilisée par l’équipe de la course pour vérifier les participants sur la ligne de départ.
Les paiements sont traités par la passerelle EMS e-Commerce, qui prend en charge les trois moyens de paiement les plus pertinents pour un événement sportif aux Pays-Bas : iDEAL (virement bancaire direct), Mastercard et PayPal.
payment_ideal, payment_mastercardou payment_paypal) via AJAX. Le serveur appelle EMSPayment::sendPaymentRequest(), qui calcule une signature HMAC à partir des paramètres de la commande et renvoie une URL de redirection. inschrijven.php. Le gestionnaire valide la signature HMAC pour vérifier l’authenticité de la réponse, puis lit chargetotal, txndatetimeet le code d’approbation. approval_code == 'Y' et status == 'APPROVED', la réservation est marquée comme payée, l’intégralité de la transaction est enregistrée dans boeking_paymentet la chaîne de génération du PDF et d’envoi de l’e-mail est déclenchée. Tout autre résultat entraîne le passage à l’état PAYMENTFAILURE et l’affichage d’un message d’erreur compréhensible pour l’utilisateur. Le code contient également une ancienne intégration de paiement Ogone, la passerelle utilisée précédemment. Elle n’est plus active, mais a été conservée comme référence : certaines réservations historiques ont été réglées par son intermédiaire et le format de ses réponses reste documenté dans les commentaires.
Chaque inscription confirmée donne lieu à un billet de course PDF personnalisé, prêt à imprimer. Ce billet est généré côté serveur avec PDFlib, une bibliothèque commerciale de création de PDF choisie pour son contrôle précis de la mise en page et la fiabilité du rendu des polices.
La fabrique de billets (framework/factories/PDFTicket.php) compose le document par programmation, sans fichier modèle, sans conversion HTML vers PDF et sans étape de rendu intermédiaire. Chaque élément est placé à des coordonnées précises, exprimées en points, sur une page A4 (595 × 842 pt).
Une image à l’identité de l’événement, sur toute la largeur (pdf_ticketkop.jpg), placée en haut de la page, présente la course et son année.
Une carte propre à la distance (pdf_10KM.jpg, pdf_15KM.jpg, pdf_25KM.jpg) est chargée à l’exécution en fonction de l’épreuve réservée, ce qui donne à chaque billet une identité visuelle distincte.
Le nom, l’adresse, le code postal, la ville et la distance choisie sont composés en Helvetica-Bold. La date de la course (28-02-2027) et l’heure de départ propre à chaque distance (13:00 / 12:30 / 11:45) sont imprimées en grands caractères.
La clé d’accès de 8 caractères est imprimée de façon bien visible pour les contrôles sur la ligne de départ. Le numéro de réservation apparaît en blanc sur le fond sombre du pied de page, en police Courier, pour permettre à l’équipe de la course de le repérer facilement.
// Extrait simplifié de 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
// Placer l’image d’en-tête à l’identité de l’événement
$img = $pdf->load_image("jpeg", "pdf_ticketkop.jpg", "");
$pdf->fit_image($img, 0, 700, "boxsize={595 130}");
// Carte du parcours correspondant à la distance
$routeImg = $pdf->load_image("jpeg", "pdf_{$km}KM.jpg", "");
$pdf->fit_image($routeImg, 300, 480, "boxsize={280 200}");
// Nom du participant en Helvetica-Bold 14 pt
$font = $pdf->load_font("Helvetica-Bold", "winansi", "");
$pdf->setfont($font, 14);
$pdf->show_xy($naam, 40, 650);
// Montant en grands caractères de 24 pt
$pdf->setfont($font, 24);
$pdf->show_xy("EUR {$amount}", 40, 560);
$pdf->end_page_ext("");
$pdf->end_document(""); Le PDF terminé est enregistré sur disque dans pdf_gen_tickets/{BookingNumber}.pdf et peut immédiatement être joint à l’e-mail de confirmation.
PDFlib a été préféré aux convertisseurs HTML vers PDF tels que wkhtmltopdf pour sa mise en page précise au pixel près et reproductible, en contrepartie d’une licence commerciale. Pour une billetterie où un élément mal aligné pourrait perturber l’équipe de la course, ce compromis est justifié.
Les e-mails de confirmation sont envoyés dès que le paiement est vérifié, sans file d’attente ni délai. La fabrique PostOffice encapsule PHPMailer et achemine les messages sortants via le relais SMTP de Mailgun pour assurer leur bonne distribution.
framework/mail_templates/reserveringsbevestiging.html : un fichier HTML autonome que des personnes ne développant pas en PHP peuvent modifier sans toucher au code PHP. [FIRSTNAME], [RESERVATIONNUMBER]et [BOOKINGDATA], sont remplacées à l’exécution par les valeurs propres au participant. Le tableau récapitulatif de la réservation est intégré directement au HTML. pdf_gen_tickets/{BookingNumber}.pdf) est joint avant l’envoi. Les participants reçoivent leur billet quelques secondes après la confirmation du paiement. smtp.eu.mailgun.org:25. Une copie cachée est adressée aux organisateurs pour leurs archives. L’objet du message contient le numéro de réservation : Inschrijf bevestiging TrailRun Terschelling 2027 [TRT-00123]. // PostOffice::sendConformation() — logique principale
$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(); La refonte de 2025 a remplacé une mise en page vieillissante par un système de design moderne, conçu d’abord pour le mobile, tout en conservant le PHP sous-jacent lorsque c’était possible. Le langage visuel reprend volontairement le caractère brut et naturel de l’événement.
Un seul bloc :root définit l’ensemble de la palette de couleurs et de l’échelle d’espacement. Adapter les couleurs de l’événement pour une prochaine édition demande une seule modification, au lieu d’un rechercher-remplacer sur 2000 lignes.
Les points de rupture sont fixés à 767 px et 1023 px. Sur mobile, la navigation se replie en menu hamburger ; les cartes des courses passent d’une grille à plusieurs colonnes à une seule colonne, sans JavaScript de mise en page.
Des angles nets partout. Chaque carte, bouton et champ de saisie possède des coins carrés, en écho à l’esthétique utilitaire des dossards, des balises de parcours et des cartes de terrain. Il s’agit d’une contrainte de design délibérée, pas d’un oubli.
Chaque distance possède son propre tracé GPX, affiché sur un fond cartographique OpenStreetMap avec Leaflet. Le style personnalisé des polylignes reprend le vert de l’identité visuelle (#99c038) et des marqueurs animés indiquent les principaux points de passage.
La classe JavaScript RegisterPage envoie un événement addressupdate au serveur à chaque modification d’un champ. Les erreurs de validation côté serveur sont renvoyées au format JSON et affichées à proximité des champs, sans rechargement complet de la page ni perte de données.
Un diaporama couvrant toute la fenêtre fait défiler des photos de la course sur la page d’accueil. Le balayage tactile fonctionne immédiatement sur mobile, sans configuration supplémentaire.
Le CSS est écrit dans css/design.css (1752 lignes), puis minifié dans design.min.css pour la production. Le JavaScript du formulaire d’inscription est minifié de js/inschrijven.js vers inschrijven.min.js. Le fichier .htaccess active la compression gzip des fichiers CSS, JS et de polices, et définit des en-têtes de mise en cache de longue durée pour les ressources statiques.
L’hébergement de production est limité à PHP 5.6, alors que le code doit également passer une vérification de syntaxe PHP 8 et fonctionner correctement sous PHP 8 sur la machine de développement. C’est la contrainte la plus délicate de ce projet.
Le travail de 2025 a consisté à auditer chaque fichier PHP pour repérer les fonctionnalités dont le comportement diffère entre ces versions :
mysql_query(), mysql_fetch_array()et aux fonctions associées ont été remplacés par leurs équivalents mysqli_*, disponibles à la fois dans PHP 5.6 et PHP 8. Iterator appropriée. Iterator et IteratorAggregate existaient toutes deux avant PHP 8. Le code devant fonctionner sous PHP 5.6 ne peut pas utiliser de déclarations de type de retour : cette version n’en prend pas en charge la syntaxe. Les types sont donc documentés dans des blocs de commentaires. À partir de PHP 8.1, les types de retour provisoires des interfaces internes demandent également une attention particulière. $str{0}). Toutes les occurrences ont été converties à la syntaxe avec crochets ($str[0]), valide dans les deux versions. Écrire du PHP qui fonctionne à la fois sous 5.6 et 8 est plus difficile que de cibler une seule de ces versions. La stratégie a consisté à n’utiliser que le sous-ensemble commun aux deux versions et à éviter toute fonctionnalité introduite après PHP 5.6 dont PHP 8 a ensuite modifié le comportement.
Chaque composant a été choisi pour des raisons pratiques : aucun framework sans nécessité, aucune dépendance sans utilité réelle.
| Couche | Technologie | Motif du choix |
|---|---|---|
| Langage serveur | PHP 5.6 / 8 | Contrainte d’hébergement ; aucun autre choix possible |
| Base de données | MySQL (mysqli) | Disponible chez l’hébergeur ; suffisante pour le volume de données |
| Framework | Sur mesure (NSWebpage / MVC) | Pas de Laravel / Symfony sous PHP 5.6 sans contournements |
| Génération de PDF | PDFlib (commercial) | Mise en page au pixel près ; résultat reproductible ; incorporation des polices |
| Envoi d’e-mails | PHPMailer + Mailgun SMTP | Distribution fiable ; copie cachée aux organisateurs pour la traçabilité |
| Passerelle de paiement | EMS e-Commerce (HMAC) | Prise en charge d’iDEAL, de Mastercard et de PayPal dans une seule intégration |
| Cartographie | Leaflet.js + OpenStreetMap | Open source ; aucune clé API requise ; superposition GPX simple |
| Manipulation du DOM | jQuery 1.11.1 | Stable ; compatible avec les navigateurs anciens utilisés par les participants |
| Diaporama principal | Jssor | Balayage tactile intégré ; aucune dépendance côté serveur |
| Architecture CSS | Sur mesure + propriétés CSS personnalisées | Aucun préprocesseur nécessaire ; prise en charge native des propriétés personnalisées |
| Détection des appareils | Mobile_Detect.php | Indication du point de rupture côté serveur, en complément des requêtes média |
Ce que ce projet démontre au-delà de ses fonctionnalités.
La contrainte de PHP 5.6 a imposé un framework sur mesure, compact, entièrement maîtrisé et facile à auditer. Aucun mécanisme opaque ni incompatibilité de version dissimulée dans le code d’une dépendance.
L’indicateur de modification utilisé dans NSPersistentObject est un mécanisme reconnu, présent dans de nombreux grands frameworks. Parfois, recréer un petit composant est préférable à l’intégration d’un ensemble beaucoup plus lourd.
Le stockage de l’instance singleton de la page dans $_SESSION et la synchronisation à chaque modification d’un champ protègent les données du participant, même si le navigateur plante pendant la saisie. Des champs cachés ne survivraient pas au rechargement de l’onglet.
Les convertisseurs HTML vers PDF sont pratiques, mais leur résultat peut varier. Pour une billetterie où la précision de la mise en page compte dans l’utilisation sur le terrain, une véritable bibliothèque PDF justifiait le coût de la licence.
L’absence de border-radius, initialement un raccourci pour gagner du temps, est devenue un choix délibéré d’identité visuelle. Les angles nets conviennent à une course qui traverse dunes, forêts et plages.
Développer simultanément pour PHP 5.6 et PHP 8 est peu courant, mais réalisable. Il faut surtout savoir exactement quelles fonctionnalités éviter et documenter clairement cette contrainte pour les futurs contributeurs.
Autres réalisations
TST Terschelling
TransportMaster
TransportMaster