Application mobile Transporter
TST Terschelling
Kiewiet Rijwielverhuur BV
Sur l’île néerlandaise d’Ameland, presque tous les touristes arrivent en ferry et ont immédiatement besoin d’un vélo. Kiewiet Cyclisme est le loueur le mieux établi de l’île. Derrière son site se trouve une plateforme conçue sur mesure pour gérer toutes les dimensions de l’activité : réservations en ligne, transport des bagages depuis le débarcadère, ventes en boutique, partenariats avec les agences, paiements, facturation et parc de postes de bureau dans les magasins. Voici l’histoire de ce système.
La plupart des plateformes de réservation répondent à un seul besoin. Kiewiet devait en couvrir quatre simultanément et les réunir dans une expérience cohérente.
Ces quatre parcours utilisent le même panier, la même passerelle de paiement, le même système de numérotation des commandes et le même moteur de facturation.
La plateforme repose sur PHP 8.4, sans framework externe : ni Laravel ni Symfony. Chaque couche, du routage à l’ORM, a été conçue et écrite pour ce domaine métier.
Toutes les URL publiques passent par un point d’entrée index.php unique. Le fichier .htaccess d’Apache y redirige les requêtes qui ne désignent pas un fichier. Un routeur sur mesure, NSSwitchBoard, analyse ensuite jusqu’à six niveaux du chemin URI, cherche le premier segment dans une table de routage et charge le contrôleur de page approprié. Les routes inconnues reçoivent une réponse 400 Bad Request et sont redirigées vers l’accueil, sans page d’erreur générique exposant une trace d’exécution.
Chaque contrôleur de page est une classe dérivée d’une base partagée, CyclismeWebPage, et conservée comme instance unique dans la session PHP. L’ensemble de l’état de la page — panier, dates choisies, filtres produits actifs et connexion de l’agence — persiste ainsi entre les requêtes, sans lecture de base de données pour l’état de l’interface. Un client peut parcourir le site pendant dix minutes, ajouter des vélos de plusieurs catégories puis passer au paiement : le serveur reconstitue précisément l’état de sa session depuis la mémoire à chaque requête.
Les modèles de données suivent une organisation en deux fichiers. Un fichier _base.php est généré automatiquement depuis le schéma de la base ; il ne contient que les propriétés typées et les accesseurs, et n’est jamais modifié manuellement. Un fichier complémentaire ajoute la logique métier. Cette séparation permet de régénérer les modèles après un changement de schéma sans toucher au code spécifique.
La classe ORM de base, NSPersistentObject, suit les modifications par deux mécanismes : un indicateur explicite positionné manuellement par le code et une somme de contrôle MD5 de toutes les propriétés sérialisées, calculée au chargement. Si l’indicateur est actif ou si la somme a changé, un appel à store() enregistre les données. Cela évite les doubles écritures accidentelles et sécurise les mises à jour partielles.
La logique d’enregistrement choisit entre INSERT et UPDATE à l’exécution en vérifiant si l’UUID de l’enregistrement existe déjà dans la base. Il n’y a pas de méthodes de création et de mise à jour distinctes : l’ORM décide. Les collections de modèles liés, par exemple les lignes d’une réservation, sont rattachées aux objets parents et enregistrées par un seul appel coordonné.
Un constructeur léger, DataStore, complète l’ORM avec un paramétrage positionnel. Les requêtes prennent la forme query("Groep_uuid=:1 AND Verhuur>0", $groupUUID). Les types sont déterminés automatiquement : les entiers sont transmis tels quels, les chaînes passent par la fonction d’échappement de la base, les tableaux sont développés en listes SQL IN (...) et les types de date et d’heure spécifiques sont sérialisés au format MySQL. Le résultat est une EntitySelection : un itérateur à chargement différé ou immédiat sur des objets de modèle entièrement chargés.
EntitySelection prend aussi en charge les opérations ensemblistes en mémoire : intersection, union et différence des résultats à partir de listes d’identifiants. Plusieurs requêtes peuvent ainsi être combinées sans échanges supplémentaires avec la base.
La disponibilité à la location n’est pas un simple compteur de stock. Un vélo réservé du mardi au vendredi est indisponible pour toute période qui chevauche ces dates. En revanche, un vélo rendu le lundi matin peut être disponible le lundi après-midi si le délai de remise à disposition le permet. Producten::buildAvailabilityMatrix() calcule une grille de disponibilité par produit et par jour sur toute la période demandée, en tenant compte des réservations existantes. Cette matrice alimente le calendrier en temps réel de la page de réservation.
Les prix dépendent de la catégorie du produit, du nombre de jours de location et de la période de l’année. Un modèle tarifaire distinct, Prijzen, conserve les règles par catégorie, plage de durée et plage de dates. Le moteur détermine le prix applicable à chaque article du panier lors de la commande. Les comptes d’agences disposent d’un groupe tarifaire spécifique : un même vélo peut donc avoir un tarif différent selon qu’il est réservé directement ou par une agence partenaire.
Les codes de réduction sont validés en temps réel pendant la réservation selon leur date d’expiration, le montant minimal de commande et les catégories de produits admissibles. La réduction déclenche le recalcul du total du panier et figure dans l’enregistrement de réservation ainsi que sur la facture.
Le transport de bagages dépend des horaires de la compagnie Wagenborg. Le client choisit une traversée, puis le système vérifie que l’horaire de transport demandé est compatible. Une classe de fabrique dédiée, FerryScheduleFactory, récupère les horaires par une API externe, les insère ou les actualise et les conserve localement pour une consultation rapide.
La plateforme intègre deux passerelles de paiement : OGone (Ingenico) et EMS. Les clients choisissent leur méthode à la commande. Après la notification de la passerelle, Reservering::ValidateReservation() confirme l’état du paiement, génère le numéro de commande, enregistre la réservation en base, produit une confirmation PDF et envoie l’e-mail de confirmation, au sein d’une séquence atomique unique après paiement.
Une fabrique génère les numéros de commande à l’aide de compteurs propres à chaque type — réservations, ventes en boutique, commandes de bagages et factures — avec remise à zéro automatique au changement d’année et de mois. Des zéros initiaux donnent aux numéros une longueur fixe et un format cohérent sur tous les documents.
La plateforme génère directement trois types de documents PDF :
Une classe PostOffice s’appuie sur PHPMailer pour les envoyer en pièces jointes avec des messages de confirmation HTML construits à partir de modèles.
Un point d’entrée REST distinct, ApiEntrypoint.php, dessert l’application mobile de lecture utilisée par les équipes de transport sur l’île. Authentifiée par l’en-tête X-Auth-Token, l’API propose des points de terminaison pour charger les données des colis, transmettre les lectures de livraison et actualiser leur emplacement. Chaque lecture met à jour l’état du bagage en temps réel et devient visible pour le client qui consulte sa commande.
La plateforme web ne fonctionne pas seule. Une application de bureau développée en 4D v20R8 est utilisée sur environ vingt postes macOS dans les magasins Kiewiet d’Ameland. 4D est une plateforme applicative relationnelle complète, avec sa propre interface, sa couche de données et sa logique métier.
L’application de bureau gère toutes les opérations de l’entreprise : accueil des clients et remise des vélos à l’aide de bons de location (VerhuurBonnen), prise en charge des réparations sans rendez-vous, gestion des pièces de l’atelier, caisse et planification des tournées de bagages.
Les deux systèmes partagent une base MySQL, mais leur relation va au-delà d’une connexion commune. Les modifications passent par une couche de synchronisation dédiée reposant sur une table RecordSync qui sert de journal des changements.
Lorsque la boutique PHP crée ou modifie un enregistrement — réservation, confirmation de paiement ou nouveau client — elle ajoute une entrée dans RecordSync avec un numéro de séquence et l’identifiant d’application "Online". Un processus en arrière-plan dans 4D consulte ces changements via bridge.php, récupère les enregistrements modifiés et les insère ou les actualise dans les données 4D avec ORDA (Object Relational Data Access). Le numéro de séquence n’avance qu’après un traitement réussi, pour qu’aucune modification ne soit oubliée.
Le chemin inverse utilise les déclencheurs de 4D. Chacune des 62 tables possède des déclencheurs exécutés à chaque enregistrement, qui délèguent leur traitement à un orchestrateur central DataBridge. Celui-ci met le changement en file d’attente sous forme d’entrée RecordSync ; le processus en arrière-plan l’envoie ensuite à bridge.php, qui l’insère ou l’actualise dans MySQL. Après une réponse HTTP 200, les entrées de synchronisation sont supprimées.
Cette architecture utilise SharedStorage pour la communication entre processus dans l’environnement 4D. Un mécanisme de déduplication empêche qu’une même modification soit mise en attente plusieurs fois lors d’enregistrements successifs rapprochés.
La planification du transport des bagages dans l’application de bureau intègre un moteur d’optimisation. La classe LuggageRouting affecte les colis aux tournées et une intégration OpenRouteService interroge l’API ORS pour calculer les distances routières. Une classe Routing applique un algorithme d’amélioration 2-opt afin de réduire la longueur totale du parcours entre les arrêts intermédiaires (tussenstops).
Le système de design est réparti en 32 fichiers partiels SCSS, des variables de design aux composants et aux mises en page propres à chaque page. Les propriétés personnalisées regroupent toutes les valeurs — couleurs, espacements et typographie — pour répercuter les changements de façon cohérente. La feuille de style compilée représente environ 49 Ko.
Les interactions sont gérées par 26 modules JavaScript : calendrier de réservation, panneau du panier, filtres produits, choix des horaires de bagages, parcours des agences et paiements. Le calendrier associe FullCalendar.js à un affichage personnalisé des disponibilités alimenté par la matrice PHP. Moment.js réalise les calculs de dates côté client en cohérence avec les validations côté serveur.
Les échanges AJAX utilisent un format de réponse structuré : un statut, une URL de redirection facultative, un tableau de fragments HTML associés à des sélecteurs CSS pour leur insertion dans le DOM par jQuery et un tableau de valeurs pour renseigner les formulaires. Le code client traite ainsi une seule structure de réponse, quelle que soit l’action appelée sur le serveur.
Un backend d’administration distinct, office.php, offre une interface complète : consultation et modification de tous les modèles de données, gestion des tarifs et des stocks, consultation des réservations et des paiements et production de rapports métier. Plus de 65 fichiers de schéma JSON, un par table, définissent les libellés des champs, leurs types, le tri et leur visibilité dans les listes. Ils alimentent un moteur dynamique de listes et de formulaires : ajouter un champ en base le fait apparaître automatiquement dans l’administration sans modifier les gabarits.
Kiewiet dépasse le simple site équipé d’un module de réservation. C’est une plateforme opérationnelle complète, développée selon les besoins précis d’une entreprise à forte activité saisonnière où la marge d’erreur est faible : un vélo réservé deux fois ou une livraison de bagages manquée affecte les vacances d’une famille. Chaque couche, de la table de routage au moteur de synchronisation et à l’optimisation des tournées, a été conçue pour cette réalité.
Les choix techniques — ORM sur mesure avec sommes de contrôle pour détecter les modifications, graphe d’objets de pages conservé en session, journal bidirectionnel entre deux environnements d’exécution hétérogènes — ne visaient pas l’élégance pour elle-même. Ils répondaient à un comportement précisément exigé par l’activité, qu’aucune solution prête à l’emploi ne proposait.
Autres réalisations
TST Terschelling
TransportMaster
TransportMaster