Transporter: mobile Anwendung für Fahrer
TST Terschelling
Kiewiet Rijwielverhuur BV
Auf der niederländischen Insel Ameland kommen fast alle Touristen mit der Fähre an und benötigen gleich danach ein Fahrrad. Kiewiet Cyclisme ist der etablierteste Fahrradverleih der Insel. Hinter seiner Website steht eine eigens entwickelte Plattform, die sämtliche Geschäftsbereiche abdeckt: Online-Reservierungen, Gepäcktransport ab Fähranleger, Einzelhandel, Vermittlerpartnerschaften, Zahlungsabwicklung, Rechnungserstellung und zahlreiche Desktop-Arbeitsplätze in den Geschäften. Dies ist die Geschichte dieses Systems.
Die meisten Buchungsplattformen lösen eine Aufgabe. Kiewiet benötigte vier gleichzeitig, verbunden zu einem stimmigen Gesamtablauf.
Alle vier Abläufe münden in denselben Warenkorb, dieselbe Zahlungsanbindung, dasselbe Bestellnummernsystem und dieselbe Rechnungserstellung.
Die Plattform basiert auf PHP 8.4 ohne externes Framework — weder Laravel noch Symfony. Jede Schicht vom Routing bis zum ORM wurde eigens für diesen Anwendungsbereich entworfen und geschrieben.
Jede öffentliche URL führt durch den zentralen Einstiegspunkt index.php. Die .htaccess-Regeln von Apache leiten alle Anfragen, die keine Dateien betreffen, dorthin um. Ein eigener Router (NSSwitchBoard) wertet anschließend bis zu sechs URI-Pfadebenen aus, gleicht das erste Segment mit einer Routingtabelle ab und lädt den passenden Seitencontroller. Unbekannte Routen erhalten 400 Bad Request und werden zur Startseite umgeleitet — ohne allgemeine Fehlerseiten, die Stacktraces offenlegen.
Jeder Seitencontroller erweitert die gemeinsame Basisklasse CyclismeWebPage und wird als Singleton in der PHP-Sitzung gespeichert. Dadurch bleibt der gesamte Seitenzustand über Anfragen hinweg erhalten: Warenkorb, gewählte Termine, aktive Produktfilter und Vermittleranmeldung. Für diesen Oberflächenzustand ist kein Datenbankzugriff erforderlich. Kunden können zehn Minuten stöbern, Fahrräder aus mehreren Kategorien hinzufügen und anschließend bezahlen. Der Server rekonstruiert bei jeder Anfrage den genauen Sitzungszustand aus dem Speicher.
Datenbankmodelle folgen einem Muster aus zwei Dateien. Eine _base.php-Datei wird automatisch aus dem Datenbankschema erzeugt und enthält nur typisierte Eigenschaften sowie Getter und Setter. Sie wird niemals manuell bearbeitet. Eine zweite Datei ergänzt die Geschäftslogik. So lassen sich Schemaänderungen neu generieren, ohne individuellen Code anzutasten.
Die ORM-Basisklasse NSPersistentObject erkennt Änderungen über zwei Mechanismen: ein ausdrücklich im Code gesetztes Änderungsflag und eine beim Laden erzeugte MD5-Prüfsumme sämtlicher serialisierter Eigenschaften. Ist das Flag gesetzt oder die Prüfsumme verändert, speichert ein store()-Aufruf die Daten. Das verhindert unbeabsichtigte doppelte Schreibvorgänge und macht Teilaktualisierungen sicher.
Die Speicherlogik entscheidet zur Laufzeit zwischen INSERT und UPDATE, indem sie prüft, ob die UUID des Datensatzes bereits in der Datenbank existiert. Separate Methoden zum Erstellen und Aktualisieren gibt es nicht — das ORM entscheidet. Verknüpfte Modellsammlungen, etwa die Positionen einer Reservierung, gehören zu ihren übergeordneten Objekten und werden mit einem einzigen abgestimmten Aufruf gespeichert.
Ein schlanker Query Builder namens DataStore ergänzt das ORM um positionsbasierte Parametrisierung. Abfragen sehen beispielsweise so aus: query("Groep_uuid=:1 AND Verhuur>0", $groupUUID). Der Builder löst Typen automatisch auf: Ganzzahlen werden unverändert übernommen, Zeichenketten durch die Escape-Funktion der Datenbank geführt, Arrays zu SQL-IN-(...)-Listen erweitert und eigene Datums- und Zeittypen im MySQL-Format serialisiert. Das Ergebnis ist eine EntitySelection — ein verzögert oder unmittelbar ladender Iterator über vollständig geladene Modellobjekte.
EntitySelection unterstützt außerdem Mengenoperationen im Arbeitsspeicher: Schnittmenge, Vereinigung und Differenz von Ergebnismengen anhand ihrer ID-Listen. So lassen sich mehrere Abfragen ohne weitere Serverzugriffe kombinieren.
Die Verfügbarkeit von Mieträdern ist kein einfacher Bestandszähler. Ein von Dienstag bis Freitag reserviertes Rad ist in jedem sich überschneidenden Zeitraum nicht verfügbar. Ein am Montagmorgen zurückgegebenes Rad kann dagegen am Montagnachmittag wieder frei sein, wenn die Aufbereitungszeit ausreicht. Producten::buildAvailabilityMatrix() berechnet für jedes Produkt und jeden Tag im angefragten Zeitraum ein Verfügbarkeitsraster unter Berücksichtigung aller bestehenden Reservierungen. Diese Matrix versorgt den Echtzeitkalender der Buchungsseite.
Preise hängen von Produktgruppe, Anzahl der Miettage und Jahreszeit ab. Ein separates Preismodell (Prijzen) speichert Regeln nach Gruppe, Dauer und Zeitraum. Die Engine ermittelt beim Bezahlen den geltenden Preis jeder Warenkorbposition. Vermittlerkonten erhalten eine eigene Preisgruppe; dasselbe Rad kann deshalb bei Direktbuchung und Buchung über einen Partner unterschiedliche Tarife haben.
Rabattcodes werden während der Buchung in Echtzeit geprüft, mit Regeln zu Ablaufdatum, Mindestbestellwert und gültigen Produktkategorien. Der Rabatt wird in der Warenkorbsumme neu berechnet und in Reservierung sowie Rechnung gespeichert.
Der Gepäcktransport richtet sich nach dem Fahrplan der Wagenborg-Fähren. Kunden wählen eine Überfahrt, und das System prüft, ob die gewünschte Transportzeit dazu passt. Eine eigene Factory-Klasse namens FerryScheduleFactory ruft den Fahrplan über eine externe API ab, fügt neue Einträge ein oder aktualisiert bestehende und speichert sie lokal für schnelle Abfragen.
Die Plattform bindet zwei Zahlungsanbieter an: OGone (Ingenico) und EMS. Kunden wählen ihre bevorzugte Zahlungsart an der Kasse. Nach der Rückmeldung des Zahlungsanbieters bestätigt Reservering::ValidateReservation() den Zahlungsstatus, erzeugt die Bestellnummer, schreibt die Reservierung in die Datenbank, erstellt eine PDF-Bestätigung und versendet die Bestätigungs-E-Mail — in einer einzigen atomaren Nachzahlungssequenz.
Eine Factory erzeugt Bestellnummern mit getrennten Zählern für Reservierungen, Shopverkäufe, Gepäckaufträge und Rechnungen. Die Zähler werden an Jahres- und Monatsgrenzen automatisch zurückgesetzt. Führende Nullen sorgen für eine feste Stellenzahl und einheitliche Nummern in allen Dokumenten.
Die Plattform erzeugt drei Arten von PDF-Dokumenten direkt:
Eine PostOffice-Klasse kapselt PHPMailer und versendet diese Dokumente als E-Mail-Anhänge zusammen mit vorlagenbasierten HTML-Bestätigungen.
Ein eigener REST-API-Einstiegspunkt (ApiEntrypoint.php) bedient die mobile Scananwendung der Transportmitarbeiter auf der Insel. Die Authentifizierung erfolgt über den Header X-Auth-Token. Endpunkte laden Gepäckdaten, nehmen Zustellscans entgegen und aktualisieren den Standort von Gepäckstücken. Jedes Scanereignis aktualisiert den Status des Gepäckdatensatzes in Echtzeit; Kunden sehen ihn beim Aufrufen ihres Auftrags.
Die Webplattform arbeitet nicht isoliert. Auf ungefähr zwanzig macOS-Arbeitsplätzen in Kiewiets Geschäften auf Ameland läuft parallel eine Desktopanwendung in 4D v20R8 — einer vollständigen relationalen Anwendungsplattform mit eigener Oberfläche, Datenschicht und Geschäftslogik.
Die Desktopanwendung deckt den gesamten operativen Betrieb ab: Mitarbeiter melden Kunden an und geben Fahrräder anhand von Mietbelegen (VerhuurBonnen) aus, bearbeiten spontan eingehende Reparaturen, verwalten Werkstattteile, bedienen die Ladenkasse und planen Gepäcktransportrouten.
Beide Systeme nutzen eine MySQL-Datenbank, teilen aber nicht einfach nur eine Verbindung. Änderungen laufen über eine eigene Synchronisierungsschicht auf Basis einer RecordSync-Tabelle, die als Änderungsjournal dient.
Wenn der PHP-Webshop einen Datensatz erstellt oder verändert — etwa eine Reservierung, Zahlungsbestätigung oder einen Kunden — schreibt er einen RecordSync-Eintrag mit Sequenznummer und Anwendungskennung „Online“. Ein Hintergrundprozess der 4D-Anwendung fragt diese Änderungen über bridge.php ab, holt die veränderten Datensätze und fügt sie mit ORDA (Object Relational Data Access) in den 4D-Datastore ein oder aktualisiert sie. Die Sequenznummer wird erst nach erfolgreicher Verarbeitung weitergesetzt, damit keine Änderung verloren geht.
Der Rückweg nutzt das Triggersystem von 4D. Alle 62 Datenbanktabellen besitzen Trigger, die bei jedem Speichern auslösen und an eine zentrale DataBridge-Koordination delegieren. Diese reiht die Änderung als RecordSync-Eintrag ein. Der Hintergrundprozess sendet sie an bridge.php, das sie in MySQL einfügt oder aktualisiert. Nach HTTP 200 werden die Synchronisierungseinträge entfernt.
Die Triggerarchitektur verwendet SharedStorage für die Kommunikation zwischen Prozessen innerhalb der 4D-Laufzeit. Eine Duplikatprüfung verhindert, dass dieselbe Datensatzänderung bei schnell aufeinanderfolgenden Speichervorgängen mehrfach in der Warteschlange landet.
Die Gepäcktransportplanung in der Desktopanwendung enthält eine Routenoptimierungs-Engine. Eine LuggageRouting-Klasse ordnet Gepäckstücke Transportfahrten zu. Eine OpenRouteService-Anbindung fragt die ORS-API nach Straßenentfernungen ab. Eine Routing-Klasse implementiert einen 2-opt-Verbesserungsalgorithmus, um die gesamte Streckenlänge über alle Zwischenstopps (tussenstops) zu minimieren.
Das Designsystem besteht aus 32 SCSS-Teildateien, gegliedert von Designtokens über Komponenten bis zu seitenspezifischen Layouts. CSS Custom Properties enthalten alle Farben, Abstände und Typografiewerte, damit sich Gestaltungsänderungen einheitlich auswirken. Das kompilierte Stylesheet ist ungefähr 49 KB groß.
26 JavaScript-Module steuern die interaktiven Funktionen: Buchungskalender, Warenkorbleiste, Produktfilter, Gepäckzeitwahl, Vermittlerabläufe und Zahlungen. Der Kalender verbindet FullCalendar.js mit einer eigenen Verfügbarkeitsdarstellung, deren Daten aus der PHP-Verfügbarkeitsmatrix stammen. Moment.js übernimmt sämtliche Datumsberechnungen im Browser, abgestimmt auf die serverseitige Datumsprüfung.
Die AJAX-Kommunikation zwischen Frontend und Backend verwendet ein strukturiertes Antwortformat. Jede Antwort enthält einen Status, eine optionale Weiterleitungs-URL, ein Array von HTML-Fragmenten mit CSS-Selektoren für das Einfügen ins DOM durch jQuery und ein Felddatenarray zum Befüllen von Formularen. Der Client verarbeitet damit stets dieselbe Antwortstruktur, unabhängig von der aufgerufenen Serveraktion.
Ein eigenes Administrationsbackend (office.php) bietet eine vollständige Verwaltungsoberfläche: Datenmodelle durchsuchen und bearbeiten, Preise und Bestände verwalten, Reservierungen und Zahlungen einsehen sowie Geschäftsberichte erzeugen. Mehr als 65 JSON-Schemadateien — eine je Tabelle — definieren Feldbezeichnungen, Datentypen, Sortierverhalten und Sichtbarkeit in Listen. Sie steuern eine dynamische Listen- und Formularerzeugung. Neue Datenbankfelder erscheinen dadurch automatisch in der Verwaltung, ohne dass Templates angepasst werden müssen.
Kiewiet ist keine Website mit einem Buchungswidget. Es ist eine vollständige, nach konkreten Anforderungen entwickelte Betriebsplattform für ein stark saisonales Unternehmen mit wenig Spielraum für Fehler. Ein doppelt gebuchtes Fahrrad oder ein nicht geliefertes Gepäckstück beeinträchtigt den Urlaub einer Familie. Jede Schicht — von der Routingtabelle über die Synchronisierung bis zur Routenoptimierung — wurde für diese Realität entworfen und entwickelt.
Die technischen Entscheidungen — ein eigenes ORM mit Prüfsummen zur Änderungserkennung, ein in der Sitzung gespeicherter Objektgraph der Seiten und ein bidirektionales Änderungsjournal zwischen zwei unterschiedlichen Laufzeitumgebungen — entstanden nicht aus dem Wunsch nach Eleganz. Das Unternehmen brauchte genau dieses Verhalten, und keine Standardlösung bot es.
Weitere Arbeiten
TST Terschelling
TransportMaster
TransportMaster