Développement logiciel

Refonte du site TrailRun

MooiWeer

Client
MooiWeer
Catégorie
Développement logiciel
Année
2025
Technologies
PHPJavascriptMySQLPDFLib
TrailRun Terschelling

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.

3Distances de course
~22kLignes de PHP
PDFlibMoteur de billetterie
iDEALPaiement principal
2025Année de refonte

Quel est ce projet ?

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.

PHP 5.6 / 8MySQLPDFlibPHPMailerMailgun SMTPPasserelle de paiement EMSiDEALjQueryLeaflet.jsPropriétés CSS personnalisées

Un framework MVC sur mesure

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.

NSWebpage — Structure de base des pages

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().

NSActiveWebpage — Répartition des événements AJAX

É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().

NSPersistentObject — ORM léger

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.

NSDatabase — Connexion singleton

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.

Classes de pages

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

Le système d’inscription

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.

Parcours de paiement

01Choix de la distance10 / 15 / 25 km
02Coordonnées du participantaddressupdate via AJAX
03Choix du moyen de paiementiDEAL / MC / PayPal
04Passerelle EMSRedirection externe
05Notification de retourPOST → inschrijven.php
06PDF + e-mailGénération automatique

Modèle de données

La réservation est répartie entre trois entités liées, afin de séparer clairement les responsabilités :

ModèleRôleChamps principaux
BoekingEnregistrement principal de la réservation, informations du participant, état du paiementboekingsnummer, accesskey, state, email, birthdate
BoekingElementLignes individuelles (une par distance ou place sélectionnée)event_id, price, discount, quantity
BoekingPaymentJournal immuable des transactions de paiementpaymentid, amount, currency, brand, status, ipaddress
EventConfiguration de la course chargée depuis la base de donnéeseventname, adultprice, slots, minimumage, active

Machine à états

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

Gestion des places

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.

Intégration de la passerelle de paiement

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.

  1. 1
    Création de la commande Lorsque le participant clique sur « Payer », le client envoie un événement de paiement (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.
  2. 2
    Redirection vers la passerelle Le navigateur est redirigé vers la page de paiement hébergée par EMS. Toutes les opérations sensibles liées aux cartes se déroulent sur le domaine de la passerelle : l’application ne traite jamais les numéros de carte.
  3. 3
    Notification asynchrone Après autorisation, EMS transmet le résultat par une requête POST à 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.
  4. 4
    Finalisation Lorsque 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.

Génération de billets PDF avec PDFlib

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).

Contenu du billet

Image d’en-tête

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.

Carte du parcours

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.

Coordonnées du participant

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.

Clé d’accès + numéro de réservation

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.

Fonctionnement de la génération

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

Chaîne d’envoi des e-mails transactionnels

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.

  1. 1
    Chargement du modèle Le modèle d’e-mail HTML est lu depuis 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.
  2. 2
    Remplacement des variables Les variables de substitution, telles que [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.
  3. 3
    Ajout du PDF en pièce jointe Le billet PDF qui vient d’être généré (pdf_gen_tickets/{BookingNumber}.pdf) est joint avant l’envoi. Les participants reçoivent leur billet quelques secondes après la confirmation du paiement.
  4. 4
    Envoi par SMTP PHPMailer envoie le message à l’adresse du participant via 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();

Interface & refonte de 2025

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.

Propriétés CSS personnalisées

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.

Flexbox conçu d’abord pour le mobile

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.

Sans border-radius, par choix

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.

Cartes de parcours avec Leaflet.js

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.

Formulaire d’inscription AJAX

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.

Diaporama principal Jssor

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.

Fichiers produits pour le déploiement

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.

Le défi de la compatibilité PHP

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 :

  1. 1
    mysqli_* à la place de mysql_* Tous les anciens appels à 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.
  2. 2
    Interface Iterator Les classes de collection doivent implémenter l’interface 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.
  3. 3
    Syntaxe d’indexation des chaînes PHP 8 supprime la prise en charge de l’indexation des chaînes avec des accolades ($str{0}). Toutes les occurrences ont été converties à la syntaxe avec crochets ($str[0]), valide dans les deux versions.
  4. 4
    Pas de déclarations de type pour les paramètres PHP 5.6 ne prend en charge ni les déclarations de types scalaires ni les types de retour dans les signatures de fonctions. Tout le code a donc été écrit sans ces déclarations, avec des commentaires docblock pour l’assistance dans l’IDE.

É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.

L’ensemble des technologies

Chaque composant a été choisi pour des raisons pratiques : aucun framework sans nécessité, aucune dépendance sans utilité réelle.

CoucheTechnologieMotif du choix
Langage serveurPHP 5.6 / 8Contrainte d’hébergement ; aucun autre choix possible
Base de donnéesMySQL (mysqli)Disponible chez l’hébergeur ; suffisante pour le volume de données
FrameworkSur mesure (NSWebpage / MVC)Pas de Laravel / Symfony sous PHP 5.6 sans contournements
Génération de PDFPDFlib (commercial)Mise en page au pixel près ; résultat reproductible ; incorporation des polices
Envoi d’e-mailsPHPMailer + Mailgun SMTPDistribution fiable ; copie cachée aux organisateurs pour la traçabilité
Passerelle de paiementEMS e-Commerce (HMAC)Prise en charge d’iDEAL, de Mastercard et de PayPal dans une seule intégration
CartographieLeaflet.js + OpenStreetMapOpen source ; aucune clé API requise ; superposition GPX simple
Manipulation du DOMjQuery 1.11.1Stable ; compatible avec les navigateurs anciens utilisés par les participants
Diaporama principalJssorBalayage tactile intégré ; aucune dépendance côté serveur
Architecture CSSSur mesure + propriétés CSS personnaliséesAucun préprocesseur nécessaire ; prise en charge native des propriétés personnalisées
Détection des appareilsMobile_Detect.phpIndication du point de rupture côté serveur, en complément des requêtes média

Enseignements & retours d’expérience

Ce que ce projet démontre au-delà de ses fonctionnalités.

Les contraintes favorisent une conception réfléchie

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.

Un ORM développé en interne peut être un bon ORM

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.

Synchroniser l’état par AJAX plutôt que par des champs cachés

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.

La génération de PDF mérite un outil adapté

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.

Les contraintes de design comme choix d’identité visuelle

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.

La compatibilité avec deux versions de PHP est possible

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.

Images du projet

Autres réalisations

Projets similaires

Démarrez un projet avec nous

Vous avez un projet en tête ? Échangeons sur la manière dont nous pouvons vous aider.