Softwareentwicklung

Kiewiet: Buchungssystem, Shop und Gepäcktransport

Kiewiet Rijwielverhuur BV

Auftraggeber
Kiewiet Rijwielverhuur BV
Kategorie
Softwareentwicklung
Jahr
2025
Technologien
4DPHPJavascriptHTMLCSS
Kiewiet Cyclisme — vollständige Buchungsplattform für Amelands größten Fahrradverleih

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 Herausforderung: eine Plattform, vier Geschäftsbereiche

Die meisten Buchungsplattformen lösen eine Aufgabe. Kiewiet benötigte vier gleichzeitig, verbunden zu einem stimmigen Gesamtablauf.

  1. Fahrradverleih — Kunden wählen den Zeitraum, sehen sich Fahrradkategorien wie Stadt-, Mountain-, Elektro-, Tandem- und Kinderräder an und reservieren konkrete Räder aus dem aktuellen Bestand. Preise variieren nach Gruppentyp, Mietdauer und Zeitraum. Die Verfügbarkeit wird in Echtzeit anhand aller bestehenden Reservierungen berechnet.
  2. Gepäcktransport — Ankommende Gäste können den Gepäcktransport vom Fährhafen bis zu ihrer Urlaubsadresse buchen. Aufträge berücksichtigen den Fährfahrplan, gewichtsabhängige Preise, QR-Code-Etiketten zur Nachverfolgung und eine Routenoptimierung, die jedes Gepäckstück einer Transportfahrt zuordnet.
  3. Einzelhandel — Helme, Schlösser, Beleuchtung, Kindersitze und Zubehör werden in einem separaten Shopablauf verkauft, der denselben Warenkorb wie die Mietbuchung nutzt. So entstehen eine gemeinsame Bestellung und Rechnung.
  4. Vermittler- und Partnernetzwerk — Registrierte Reisebüros und Unterkunftspartner melden sich in einem eigenen Portal an, buchen im Namen ihrer Gäste und erhalten vermittlerspezifische Preise und Provisionsmodelle.

Alle vier Abläufe münden in denselben Warenkorb, dieselbe Zahlungsanbindung, dasselbe Bestellnummernsystem und dieselbe Rechnungserstellung.


Die Architektur: ein von Grund auf entwickeltes Framework

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.

Der Ablauf einer Anfrage

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.

Die ORM-Schicht

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.

Der Query Builder

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 Buchungs-Engine

Verfügbarkeitsmatrix

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.

Preismatrix

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.

Aktionscodes

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.

Fährfahrplan-Anbindung

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.


Zahlungsabwicklung

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.


Dokumenterstellung

Die Plattform erzeugt drei Arten von PDF-Dokumenten direkt:

  • Reservierungsbestätigungen — eine Buchungsübersicht mit Zeitraum, Fahrrädern, Preisaufschlüsselung und Bestellnummer.
  • Rechnungen — Rechnungen mit Einzelpositionen und Umsatzsteuerausweis für alle Auftragstypen.
  • Gepäcketiketten — gedruckte QR-Code-Etiketten an den Gepäckstücken, die an jedem Transportkontrollpunkt gescannt werden.

Eine PostOffice-Klasse kapselt PHPMailer und versendet diese Dokumente als E-Mail-Anhänge zusammen mit vorlagenbasierten HTML-Bestätigungen.


Die API zur Gepäckverfolgung

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 Desktopanwendung: 4D auf zwanzig Rechnern

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.

Die bidirektionale Synchronisierungs-Engine

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.

Routenoptimierung

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 Frontend

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.


Der Verwaltungsbereich

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.


Das Projekt in Zahlen

  • 233 PHP-Quelldateien
  • Über 70 Datenmodelle für alle Geschäftsbereiche
  • Über 65 Datenbanktabellen
  • 50 zentrale Framework-Basisklassen
  • 11 Seitencontroller für öffentliche Seiten
  • 32 SCSS-Teildateien, kompiliert zu einem einheitlichen Designsystem
  • 26 JavaScript-Funktionsmodule
  • 96 4D-Klassendateien und 1.099 ältere Methoden in der Desktopanwendung
  • 62 Datenbanktrigger für die bidirektionale Synchronisierung
  • Rund 20 Desktop-Arbeitsplätze mit der 4D-Anwendung an mehreren Standorten auf Ameland
  • Zwei Zahlungsanbieter, drei PDF-Dokumenttypen und eine REST-API zur Nachverfolgung

Wofür dieses Projekt steht

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.

Projektbilder

Weitere Arbeiten

Ähnliche Projekte

Starten Sie ein Projekt mit uns

Sie haben ein Projekt im Sinn? Sprechen wir darüber, wie wir Sie unterstützen können.