Développement logiciel

Refonte du site TrailRun

MooiWeer

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

Une plateforme d'enregistrement et de billetterie complète pour un événement de trail running sur une île néerlandaise de la mer du Wadden construite à partir de zéro avec un framework PHP personnalisé, traitement des paiements en direct, génération de billets PDF et livraison automatique de courrier électronique.

3Distances de race
~ 22kLes lignes de PHP
Le livre PDFlibMoteur de billetterie
L'idéalPaiement primaire
2025Année de refonte

C' est quoi ça ? Le projet ?

TrailRun Terschelling est le site Web d'enregistrement de course pour un événement annuel de course sur piste sur l'île de Terschelling aux Pays-Bas. Les participants peuvent s'inscrire en ligne pour la distance de 10 km, 15 km ou 25 km, payer par plusieurs méthodes de paiement néerlandaises et recevoir instantanément un billet de course PDF avec une clé d'accès unique.

Le projet couvre l'ensemble du parcours des participants de bout en bout: un front-end informatif avec des itinéraires de course visualisés sur des cartes interactives, un formulaire d'inscription AJAX en plusieurs étapes avec une validation côté serveur, une intégration en direct de passerelle de paiement, un pipeline de génération PDF et un système de messagerie transactionnelle automatisé tous reliés par un framework MVC léger développé à la maison.

En 2025, l'ensemble du front-end a été redessiné (nouvelle architecture CSS, propriétés personnalisées, mise en page de flexbox, points de rupture mobiles-premiers) et le PHP sous-jacent a été audité pour PHP 8 compatibilité tout en gardant la base de code déployable sur le serveur de production qui est limité à PHP 5.6.

Les résultats de l'enquête sont les suivants:MySQLLe livre PDFlibPHPMailerSMTP de fusil de messageriePorte de paiement du système de paiement électroniqueL'idéaljQueryLe dépliant.jsPropriétés personnalisées du CSS

VMC personnalisé Cadre

Au lieu d'atteindre Laravel ou Symfony, ce projet est construit sur un framework personnalisé léger développé spécifiquement pour ce type de cas d'utilisation d'enregistrement d'événements.

NSWebpage Pages de base Shell

Renvoie le squelette du document HTML, injecte des méta-tags SEO, charge des actifs CSS/JS et délègue le contenu à des sous-classes via un résumé drawPage() méthode.

NSAactiveWebpage Dispatcher AJAX

Extend NSWebpage avec la gestion de session et une couche AJAX axée sur les événements. NSEvent les objets et parcourus à travers un handleEvent() Le dépêcheur.

NSP persistentObject ORM léger

Classe de base pour tous les modèles pris en charge par la base de données. Implémentation d'un schéma de drapeau sale Les propriétés ne sont écrites dans la base de données que lorsqu'elles sont modifiées. Fournit loadObject(), store(), et delete() sans aucune réflexion de schéma.

Base de données NSD Connexion Singleton

Une enveloppe de singleton autour mysqli Il s'agit d'un système qui assure une seule connexion de base de données par requête. escape() assistant et une interface de requête cohérente utilisée par tous les modèles.

Classes de pages

Chaque itinéraire correspond à une classe de pages en béton qui s'étend TrailrunWebpage (dans le même ordre d'étendue) NSActiveWebpageLe singleton de la page est stocké dans $_SESSION, préservant l'état du formulaire à travers les appels AJAX sans champs cachés ou des allers-retours répétés de base de données.

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

La réservation Système

L'enregistrement est géré via un formulaire AJAX en plusieurs étapes qui synchronise l'état avec le serveur à chaque changement, donc rien n'est perdu si le navigateur est fermé ou si la session expire avant le paiement.

Flux de paiement

01Sélectionner la distance10 / 15 / 25 km
02Détails des participantsMise à jour d'adresse AJAX
03Sélectionner la méthodeiDEAL / MC / PayPal
04Portée de l'EMSRedirigé vers l'extérieur
05Retour d'appelPOST → inscrrijven.php
06PDF + courrier électroniqueProduite automatiquement

Modèle de données

La réservation est décomposée en trois entités liées, en gardant les préoccupations nettement séparées:

ModèleRôlechamps clés
Le bouclierRegistre de réservation primaire, informations sur les participants, état de paiementnuméro de livraison, clé d'accès, état, courrier électronique, date de naissance
Élément de bowingLes points de ligne individuels (un par distance/slot sélectionné)événement_id, prix, réduction, quantité
Réservation du paiementRegistre immutable des transactions de paiementnom de l'entreprise, montant, monnaie, marque, statut, adresse IP
ÉvénementConfiguration de course chargée à partir de la base de donnéesNom de l'événement, prix pour adultes, espaces, âge minimum, activité

Machinerie d'État

Chaque réservation passe par un ensemble strict d'États, empêchant les doubles paiements et rendant les requêtes d'audit triviales:

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

Gestion des machines à sous

Les États membres EventFactory Il effectue des requêtes dans la base de données au chargement de la page et calcule le nombre de fentes disponibles par distance en soustrayant les réservations confirmées du maximum configuré. Lorsqu'une distance s'épuise, elle est automatiquement retirée de l'interface utilisateur de sélection sans aucune intervention manuelle.

La clé d'accès stockée sur chaque BoekingElement est généré par Boîte à outils::MakeRandomPassword(8) une chaîne cryptographiquement aléatoire de 8 caractères imprimée sur le billet PDF et utilisée par le personnel de course pour vérifier les participants à la ligne de départ.

Porte de paiement L'intégration

Les paiements sont effectués par l'intermédiaire de la passerelle de commerce électronique EMS, qui prend en charge les trois méthodes les plus pertinentes pour un événement sportif néerlandais: iDEAL (transfert bancaire direct), Mastercard et PayPal.

  1. 1
    Création de l'ordre Lorsque le participant clique sur "Payez", 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 sur les paramètres de commande et renvoie une URL de redirection.
  2. 2
    Redirection de passerelle Le navigateur est redirigé vers la page de paiement hébergée par EMS. Tout le traitement des cartes sensibles se fait sur le domaine de la passerelle l'application ne touche jamais les numéros de cartes.
  3. 3
    Retour d'appel asynchrone Après l'autorisation, l'EMS publie le résultat inschrijven.php. Le gestionnaire valide la signature HMAC pour confirmer que la réponse est authentique, puis lit chargetotal, txndatetime, et le code d'approbation.
  4. 4
    Finalisation À l'intérieur approval_code == 'Y' et status == 'APPROVED', la réservation est marquée payée, l'enregistrement complet de la transaction est écrit à boeking_payment, et le pipeline de courrier électronique PDF + est déclenché. PAYMENTFAILURE et l'utilisateur voit une erreur amicale.
La base de codes contient également une intégration de paiement Ogone la passerelle antérieure. Elle n'est plus active mais a été conservée dans la base de codes comme référence, car certaines réservations historiques ont été effectuées à travers elle et le format de réponse est toujours documenté dans les commentaires.

Génération de billets PDF avec PDFlib

Chaque inscription confirmée produit un billet de course PDF personnalisé, prêt à être imprimé. Le livre PDFlib, une bibliothèque commerciale de création de PDF choisie pour son contrôle de mise en page précis et son rendu de polices fiable.

L'usine de billets (framework/factories/PDFTicket.phpIl n'y a pas de fichier de modèle, aucune conversion HTML-PDF et aucune étape de rendu intermédiaire. Chaque élément est placé avec des coordonnées de point exactes sur une toile A4 (595 × 842 pt).

Qu' est-ce qui est écrit sur le billet ?

Image d'en-tête

Une image de marque à largeur complète (pdf_ticketkop.jpg) placé en haut de la page établit l'identité de la race et l'année.

Carte de route

Une image de carte spécifique à la distance (pdf_10KM.jpg, pdf_15KM.jpg, pdf_25KM.jpg) est chargé en temps d'exécution en fonction de l'événement réservé, ce qui confère à chaque billet une signature visuelle unique.

Bloc des participants

Le nom, l'adresse, le code postal, la ville et la distance sélectionnée sont typés dans Helvetica-Bold. La date de la course (28-02-2027) et l'heure de départ spécifique à la distance (13:00 / 12:30 / 11:45) sont imprimés en gros type.

clé d'accès + numéro de réservation

La clé d'accès de 8 caractères est imprimée de manière prominente pour la vérification de la ligne de départ. Le numéro de réservation est inversé en blanc sur l'arrière-plan du pied de page sombre en utilisant Courier afin qu'il soit facilement scanné par le personnel de course.

Comment fonctionne la construction

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

Le PDF fini est enregistré à pdf_gen_tickets/{BookingNumber}.pdf sur le disque, où il est immédiatement disponible pour être attaché à l'e-mail de confirmation.

PDFlib a été choisi par rapport aux convertisseurs HTML en PDF comme wkhtmltopdf car il offre Détermination de la mise en page parfaite des pixels Pour un système de billetterie où un élément mal aligné pourrait confondre le personnel de course, ce compromis vaut la peine.

E-mail de transaction L'oléoduc

Les e-mails de confirmation sont envoyés dès que le paiement est vérifié pas de file d'attente, pas de retard. PostOffice l'usine enveloppe PHPMailer et route le courrier sortant à travers le relais SMTP de Mailgun pour une livrabilité fiable.

  1. 1
    Chargement du modèle Le modèle de courrier électronique HTML est lu à partir framework/mail_templates/reserveringsbevestiging.html un fichier HTML autonome qui peut être édité par des non-développeurs sans toucher PHP.
  2. 2
    Remplacement des jetons Tokens de détenteur de place tels que [FIRSTNAME], [RESERVATIONNUMBER], et [BOOKINGDATA] Le tableau de résumé des réservations est rendu en HTML en ligne.
  3. 3
    Affichage PDF Le billet fraîchement généré PDF (pdf_gen_tickets/{BookingNumber}.pdfLes participants reçoivent leur billet dans les secondes suivant la confirmation du paiement.
  4. 4
    Livraison SMTP PHPMailer envoie à l'adresse du participant via smtp.eu.mailgun.org:25Un BCC silencieux est copié aux organisateurs de l'événement pour leur propre enregistrement. Inscrivez la confirmation de l'inscription dans le formulaire TERM043__ 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 Redesign

La refonte de 2025 a remplacé une mise en page datée par un système de conception moderne, mobile-first tout en gardant le PHP sous-jacent intact lorsque cela est possible. Le langage de conception reflète délibérément le caractère extérieur rugueux de l'événement.

Propriétés personnalisées du CSS

Un célibataire :root Le bloc définit l'ensemble de la palette de couleurs et de l'échelle d'espacement.

La première boîte flexible mobile

Les points de rupture à 767 px et 1023 px. La navigation s'effondre à un menu hamburger sur mobile; les cartes de course refluent d'une grille à plusieurs colonnes à une seule colonne sans aucune mise en page JavaScript.

Aucun rayon frontalier intentionnellement

Chaque carte, chaque bouton et chaque entrée ont des coins carrés pour faire écho à la sensation industrielle des cartes de course, des marqueurs de sentiers et des cartes de terrain.

Carte des itinéraires de la brochure.js

Chaque distance de course dispose d'un itinéraire GPX dédié rendu sur une couche de carreaux OpenStreetMap via Leaflet.#99c038) et les marqueurs animés indiquent les points clés.

Formulaire d'enregistrement AJAX

Les États membres RegisterPage La classe JavaScript envoie une addressupdate événement au serveur lors de chaque changement de champ. Les erreurs de validation du côté serveur sont renvoyées comme JSON et affichées en ligne pas de chargement complet de la page, pas de données perdues.

Jssor héros glisser

Un curseur d'image de port de vue complet sur la page d'accueil passe par la photographie de course.

Construire des objets

CSS est écrit en css/design.css (1752 lignes) et minifiées à design.min.css pour la production. JavaScript pour le formulaire d'enregistrement est minifié à partir de js/inschrijven.js à inschrijven.min.jsLe . .htaccess applique la compression gzip pour les fichiers CSS, JS et de police et définit des en-têtes de cache agressifs pour les actifs statiques.

PHP Compatibilité Le défi

L'environnement d'hébergement de production est limité à PHP 5.6 mais la base de code doit également passer une vérification de syntaxe PHP 8 et fonctionner correctement sous PHP 8 sur la machine de développement.

Le travail de 2025 consistait à vérifier chaque fichier PHP pour les fonctionnalités qui se comportent différemment dans l'écart de version:

  1. 1
    mysqli_* sur mysql* Tout l' héritage mysql_query(), mysql_fetch_array(), et les appels connexes ont été remplacés par leurs mysqli_* équivalents, qui sont disponibles dans PHP 5.6 et PHP 8.
  2. 2
    Interface de l'itérateur PHP 8 divise l'ancien Iterator l'interface dans Iterator et IteratorAggregate Les classes de collecte personnalisées ont été mises à jour pour mettre en œuvre l'interface correcte et les déclarations de type de retour ont été ajoutées lorsque PHP 5.6 les ignore sans danger.
  3. 3
    Syntaxe d'indexation des chaînes PHP 8 déprécie l'indexation des chaînes à brace bouclée ($str{0}Toutes les occurrences ont été converties en syntaxe de parenthèses ($str[0]), qui est valable pour les deux versions.
  4. 4
    Aucune indication de type sur les paramètres PHP 5.6 ne prend en charge ni les indices de type scalaire ni les types de retour dans les signatures de fonction. Tout le code a été écrit sans ceux-ci, en s'appuyant sur les commentaires de docblock pour le support IDE.

Écrire PHP qui fonctionne sur les deux 5.6 et 8 La stratégie était: utiliser uniquement le sous-ensemble de PHP que les deux versions partagent, et éviter toutes les fonctionnalités introduites après 5.6 pour lesquelles PHP 8 a changé de comportement plus tard.

Technologie complète La pile

Chaque composante choisie pour des raisons pratiques pas de cadre pour elle-même, pas de dépendance qui ne tire pas son poids.

CoucheTechnologiePourquoi ?
Langue du serveurLes résultats de l'enquête sont les suivants:Restrictions d'hébergement; aucun choix possible
Base de données- Le nom de l'établissement:Disponible sur l'hôte; suffisant pour le volume de données
CadrePour les personnes âgées et âgéesAucun Laravel / Symfony sur PHP 5.6 sans résolution
Génération PDFPDFlib (commercial)L'affichage parfait des pixels; sortie déterministe; intégration de polices
Livraison de courrier électroniquePHPMailer + mailgun SMTPDélivrabilité fiable; suivi de l'audit du BCC auprès des organisateurs
Porte de paiementEMS du commerce électronique (HMAC)Prend en charge iDEAL, Mastercard, PayPal dans une seule intégration
CartesLe dépliant.js + OpenStreetMapSource ouverte; aucune clé API requise; superposition GPX triviale
Manipulation du DOMjQuery 1.11.1Stable; fonctionne sur l'ancienne gamme de navigateurs utilisée par les participants à l'événement
Le glisser de hérosJssorAppliquez-le à l'extérieur de la boîte; Zéro dépendance au serveur
L'architecture CSSPropriétés personnalisées + CSSAucun préprocesseur n'est nécessaire; propriétés personnalisées prises en charge natively
Détection de l'appareilMobile_Detect.phpIndice de point de rupture côté serveur en plus des requêtes multimédias

Les leçons Réservations

Ce que ce projet démontre au-delà de la liste des caractéristiques.

Les contraintes conduisent à une bonne conception

La contrainte PHP 5.6 a forcé un framework personnalisé qui est petit, entièrement compréhensible et facile à vérifier. Aucune magie cachée, aucune incompatibilité de version enfouie dans le code du fournisseur.

ORM cultivé chez soi ≠ mauvais ORM

Le modèle de drapeau sale dans NSPersistentObject C'est un modèle légitime utilisé dans de nombreux grands cadres.

La synchronisation de l'état AJAX bat les champs cachés

Enregistrement du singleton de la page dans $_SESSION et la synchronisation sur chaque changement de champ signifie que les données des participants sont sécurisées même si le navigateur s'écrase au milieu du formulaire.

La génération PDF n'est pas une marchandise

Les convertisseurs HTML-PDF sont pratiques mais produisent des sorties variables. Pour un système de billetterie où la précision de la mise en page est importante pour une utilisation opérationnelle, une bibliothèque PDF appropriée valait les frais de licence.

Les contraintes de conception en tant que décisions de marque

La règle de l'absence de radius frontaliers a commencé comme un raccourci pour gagner du temps et est devenue un choix délibéré de marque.

La double version PHP est résolvable

L'écriture pour PHP 5.6 et PHP 8 simultanément est rare mais traitable. La clé est de savoir exactement quelles fonctionnalités éviter et de documenter clairement la 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.