PHP-Entwickler in Frankfurt

PHP-Entwicklung für Unternehmen in Frankfurt am Main mit Schwerpunkt Geldlogik: Beträge ohne float, Cent als Ganzzahl, BCMath, Rundungsmodi und SEPA-Überweisungsdateien im Format pain.001. Ein Einzelentwickler, remote, schriftlich.

PHP-Entwickler

Mein Platz in der WordPress-Community

  • Mein wordpress.org-Konto besteht seit 2006.
  • Ich habe zum WordPress-Core in den Versionen 7.0 und 7.1 beigetragen.
  • Ich betreue 30 Plugins auf wordpress.org.
  • Ich organisiere WordCamps und trete dort als Speaker auf.
  • Ich war bei jedem WordCamp Europe von 2017 bis 2026 dabei.

Mein WordPress.org-Profil

#Geldbeträge in PHP: worum es auf dieser Seite geht

Ich bin Mariusz Szatkowski, schreibe PHP seit 2006, damals für WordPress, und arbeite als einzelner Entwickler von Gdynia aus für Unternehmen in Deutschland. Ich arbeite remote und schriftlich, ohne Termine vor Ort und ohne Telefonrunden. Grafik, Layout und Wireframes liefern Sie oder Ihre Agentur, ich setze die Logik dahinter um.

In Frankfurt sitzen Banken, Zahlungsdienstleister und ihre Dienstleister. Die Finanzplatzinitiative Frankfurt Main Finance hat ihren Sitz am Walther-von-Cronberg-Platz. Mit Kunden aus diesem Umfeld rechnet man viel. Die PHP-Anwendungen, die mir bei solchen Unternehmen begegnen, sind aber selten Bankensoftware. Es sind Abrechnungsportale, Provisionsberechnungen, Gutschriftenläufe, Shops mit Rabattlogik und interne Werkzeuge, die am Monatsende eine Überweisungsdatei für die Bank erzeugen. In all diesen Anwendungen entscheidet dieselbe Frage über Ärger mit der Buchhaltung: wie der Code mit Geldbeträgen umgeht.

Darum geht es hier. Andere Frankfurter Seiten von mir behandeln den Checkout mit starker Kundenauthentifizierung (WooCommerce-Entwickler Frankfurt) und die laufende Pflege einer Website (Wartung und Support Frankfurt). Diese Seite bleibt beim Rechnen und bei der Datei, die am Ende zur Bank geht.

#Warum float für Beträge die falsche Wahl ist

Die PHP-Dokumentation ist an dieser Stelle deutlich. Im Kapitel über Gleitkommazahlen steht, dass PHP in der Regel das Format IEEE 754 mit doppelter Genauigkeit nutzt und dass Zahlen wie 0.1 oder 0.7 im Binärformat nicht exakt darstellbar sind, egal wie groß die Mantisse ist. Das Beispiel aus der Dokumentation:

<?php
var_dump(floor((0.1 + 0.7) * 10)); // float(7), nicht 8
var_dump(0.1 + 0.2 == 0.3);        // bool(false)

php.net zieht daraus zwei Regeln: float-Ergebnissen nicht bis zur letzten Stelle trauen und float-Werte nie direkt auf Gleichheit vergleichen. Für eine Rechnung heißt das: Wenn Sie zwanzig Positionen als float addieren und mit einer Summe aus der Datenbank vergleichen, kann der Vergleich scheitern, obwohl auf dem Papier alles stimmt. Der Fehler zeigt sich selten beim Test mit drei Positionen. Er zeigt sich im Monatsabschluss.

Dazu kommt ein zweiter Stolperstein. Die BCMath-Dokumentation warnt, dass ein float, der an eine bc-Funktion übergeben wird, wegen der Umwandlung in einen String nicht das gewünschte Ergebnis liefern kann. Wer also BCMath einführt, aber die Werte vorher als float aus dem Formular oder der Datenbank holt, hat das Problem nur verschoben.

#Cent als Ganzzahl speichern

Die einfachste robuste Lösung ist, Beträge in der kleinsten Einheit der Währung zu speichern, also in Cent, als Ganzzahl. Addition und Subtraktion sind dann exakt. Schwierig wird es erst beim Multiplizieren mit einem Prozentsatz, denn dabei entstehen Bruchteile eines Cents, und jemand muss entscheiden, wohin gerundet wird.

<?php
$nettoCent = 1999;                                 // 19,99 in Cent
$steuerCent = intdiv($nettoCent * 19 + 50, 100);   // 380, kaufmännisch gerundet

Die Funktion intdiv() teilt ganzzahlig. Der Trick mit plus 50 rundet nur bei positiven Beträgen korrekt. Bei Gutschriften und Stornos mit negativen Werten braucht es eine eigene Behandlung, und genau dort entstehen in gewachsenem Code die meisten Abweichungen. Deshalb setze ich die Ganzzahl-Variante nur ein, wenn die Logik schmal bleibt. Sobald Rabatte, Staffeln oder mehrere Steuersätze dazukommen, ist eine der beiden folgenden Wege übersichtlicher.

In der Datenbank gilt dasselbe Prinzip: eine Spalte für den Betrag als Ganzzahl in Cent und eine für den Währungscode. Eine float-Spalte für Beträge ist der Punkt, an dem ich bei einer Übernahme zuerst nachfrage.

#BCMath und BcMath\Number seit PHP 8.4

BCMath rechnet mit Zahlen als Strings und laut php.net mit beliebiger Genauigkeit, begrenzt nur durch den Speicher. Unter Windows ist die Erweiterung eingebaut, unter Linux muss PHP mit --enable-bcmath übersetzt sein; bei Ihrem Hoster prüfe ich das zuerst (Installationshinweise).

Seit PHP 8.4 gibt es die Klasse BcMath\Number. Ihre Objekte sind unveränderlich und unterstützen die normalen Rechen- und Vergleichsoperatoren. Laut Dokumentation hängt die Klasse nicht von der INI-Einstellung bcmath.scale ab, was eine häufige Fehlerquelle in älterem Code beseitigt.

<?php
use BcMath\Number;

$netto  = new Number('19.99');
$steuer = $netto->mul('0.19')->round(2);   // 3.80

Ich habe das Beispiel unter PHP 8.4 ausgeführt. Der Zwischenwert ist 3.7981, gerundet wird erst am Ende und nur einmal. Wer noch PHP 8.2 oder 8.3 betreibt, nutzt die klassischen Funktionen wie bcadd() und bcmul() mit einer festen Nachkommastellenzahl. Laut php.net enden die Sicherheitsupdates für PHP 8.2 am 31. Dezember 2026, für 8.3 am 31. Dezember 2027 und für 8.4 am 31. Dezember 2028. Ein Upgrade auf 8.4 lohnt sich also ohnehin, und die neue Klasse ist ein Grund mehr.

#Rundungsmodi: welche Rundung Ihr Code wirklich macht

PHP 8.4 hat das Enum RoundingMode eingeführt. Es gilt für round(), bcround() und BcMath\Number::round() und kennt acht Arten: HalfAwayFromZero, HalfTowardsZero, HalfEven, HalfOdd, TowardsZero, AwayFromZero, NegativeInfinity und PositiveInfinity. Voreinstellung bei bcround() und round() ist HalfAwayFromZero, also die Rundung, die man in der Schule als kaufmännisch lernt.

<?php
echo bcround('2.345', 2);                          // 2.35
echo bcround('2.345', 2, RoundingMode::HalfEven);  // 2.34

Der Unterschied ist ein Cent, aber er wiederholt sich bei jeder Position. Welcher Modus gilt, ist keine Programmierfrage. Das Umsatzsteuergesetz verlangt in § 14 Absatz 4 Nummer 8 UStG, dass die Rechnung den anzuwendenden Steuersatz und den auf das Entgelt entfallenden Steuerbetrag nennt. Ob Ihre Anwendung die Steuer pro Position oder auf die Summe je Steuersatz berechnet und wie sie rundet, klären Sie mit Ihrer Steuerberatung. Meine Aufgabe ist, diese Entscheidung an genau einer Stelle im Code festzuhalten, mit einem ausdrücklich genannten Modus statt einer Voreinstellung, die niemand mehr kennt.

Ein Hinweis zu round(): Die Funktion nimmt und liefert float. Sie eignet sich zum Anzeigen, nicht als Grundlage für weitere Buchungsschritte.

#Eine Geldbibliothek statt einer eigenen Klasse

Wer viel mit Beträgen rechnet, baut sich früher oder später eine eigene Money-Klasse. Zwei verbreitete Bibliotheken nehmen Ihnen das ab, beide unter MIT-Lizenz.

Die Bibliothek brick/money beschreibt sich in seiner Dokumentation als Bibliothek mit exakter Arithmetik und ausdrücklicher Kontrolle über die Rundung. Die Version 0.15.2 vom 29. September 2026 verlangt laut Packagist PHP 8.2 oder neuer. Für mich entscheidend: Muss ein Ergebnis gerundet werden und ist kein Rundungsmodus angegeben, wirft die Bibliothek eine RoundingNecessaryException. Stille Rundung kann so gar nicht entstehen. Dazu kommen split() und allocate(), die einen Betrag auf Teile verteilen, ohne dass ein Cent verloren geht.

<?php
use Brick\Money\Money;
use Brick\Money\SplitMode;

$betrag = Money::of(100, 'EUR');
[$a, $b, $c] = $betrag->split(3, SplitMode::ToFirst); // 33.34, 33.33, 33.33

Die Bibliothek moneyphp/money speichert laut seiner Einführung alle Beträge in der kleinsten Einheit, also in Cent, und nimmt bei der Erzeugung nur ganzzahlige Werte an. Die Dokumentation zur Aufteilung erklärt das Verfahren: erst jeden Anteil abrunden, dann die übrigen Cents einzeln verteilen. Die aktuelle Version 4.9.0 vom 4. Mai 2026 unterstützt laut Packagist PHP 8.1 bis 8.5.

Welche der beiden passt, hängt vom vorhandenen Code ab. In einem Laravel- oder Symfony-Projekt, das schon Ganzzahlen in Cent speichert, passt moneyphp/money oft mit weniger Umbau. Wo bisher mit Dezimalstrings gerechnet wurde, ist brick/money näher dran. Ich schreibe Ihnen die Abwägung für Ihren Code schriftlich auf, bevor ich etwas umbaue.

#SEPA-Überweisungen als pain.001-Datei erzeugen

Das Börsengebäude in Frankfurt am Main bei Nacht

Foto: Norbert Nagel, CC BY-SA 3.0, Wikimedia Commons.

Viele Anwendungen enden nicht bei der Rechnung, sondern bei der Auszahlung: Provisionen an Vermittler, Erstattungen an Kunden, Gutschriften an Partner. Statt jede Überweisung einzeln im Onlinebanking zu tippen, erzeugt die Anwendung eine XML-Datei, die per EBICS oder Upload an die Bank geht. Für deutsche Banken ist die maßgebliche Quelle die Spezifikation der Deutschen Kreditwirtschaft. Auf ebics.de steht die ab dem 15. November 2026 gültige Fassung, Version 26.11 der Anlage 3 des DFÜ-Abkommens. Sie beschreibt die Überweisung als Nachricht pain.001.001.09. Die folgenden Punkte stammen aus diesem Dokument.

#Kontrollsumme und Anzahl

Auf Ebene des Group Header und der Payment Information sind die Anzahl der Transaktionen (NbOfTxs) und die Kontrollsumme (CtrlSum) Pflicht. Die Felder IBAN, CtrlSum und NbOfTxs sollen vor dem Versand maschinell geprüft werden. Bei Verstößen darf die Bank die gesamte Datei abweisen. Hier zahlt sich die Ganzzahl in Cent aus: Ich summiere die Einzelbeträge als Ganzzahlen und formatiere erst ganz am Ende.

#Betragsformat

Der Betrag in InstdAmt steht mit einem Punkt als Dezimaltrennzeichen und höchstens zwei Nachkommastellen. Für SCT und SCT Inst liegt er laut Spezifikation zwischen 0,01 und 999.999.999,99. Ein Betrag aus number_format() mit deutschem Tausenderpunkt gehört also nicht in die Datei.

#Zeichensatz

Erlaubt ist UTF-8 ohne Byte Order Mark, eingeschränkt auf den Latin Character Set des EPC: Ziffern, Buchstaben A bis Z in Groß- und Kleinschreibung, Leerzeichen und wenige Satzzeichen. Umlaute, ß, das kaufmännische Und und der Stern müssen deutsche Institute annehmen, dürfen sie aber umsetzen, etwa ä in a oder ae. Für alles andere, zum Beispiel einen polnischen oder französischen Namen mit Akzent, braucht die Anwendung eine eigene Umschreibung, bevor die Datei entsteht.

#Referenzen und Textfelder

Die EndToEndId ist auf 35 Zeichen begrenzt und läuft unverändert bis zum Zahlungsempfänger. Der unstrukturierte Verwendungszweck (Ustrd) hat höchstens 140 Zeichen. Referenzen dürfen weder mit einem Schrägstrich beginnen oder enden noch zwei Schrägstriche hintereinander enthalten. Ein Textfeld, das nur aus Leerzeichen besteht, kann zur Abweisung der ganzen Datei führen.

#Empfängerüberprüfung

Die Spezifikation unterscheidet bei der Einreichung, ob eine Prüfung des Empfängernamens (VOP) erfolgen soll. Für Opt-in nennt sie die BTF-Parameter SCT//VOI/pain.001/, für Opt-out SCT//VOO/pain.001/. Als Opt-out eingereichte Sammler mit nur einer Zahlung kann die Bank zurückweisen, weil dann eine Prüfung verpflichtend ist. Wie Ihre Bank das umsetzt, steht in ihren Unterlagen zur Datenfernübertragung, und das klären Sie mit ihr.

Vor der Übergabe an die Bank prüfe ich jede erzeugte Datei gegen das XML-Schema. PHP bringt dafür DOMDocument::schemaValidate() mit. Das ersetzt nicht den Test mit der Bank, fängt aber die meisten Fehler ab, bevor ein Mensch den Abweisungsbericht lesen muss.

#Checkliste für die Geldlogik in Ihrer PHP-Anwendung

Diese Fragen können Sie auch ohne mich durchgehen:

  • Steht irgendwo ein Betrag in einer float-Spalte der Datenbank oder in einer float-Variable, die später gebucht wird?
  • Gibt es genau eine Stelle im Code, an der gerundet wird, und ist der Rundungsmodus dort ausdrücklich genannt?
  • Ergibt die Summe der Positionen auf jeder Rechnung exakt die Rechnungssumme, auch bei Rabatt und mehreren Steuersätzen?
  • Werden negative Beträge bei Gutschriften und Stornos genauso gerundet wie positive?
  • Wird die Kontrollsumme der SEPA-Datei aus Ganzzahlen berechnet und vor dem Versand geprüft?
  • Werden Namen und Verwendungszwecke vor dem Export auf den erlaubten Zeichensatz gebracht?
  • Gibt es automatische Tests mit Grenzfällen: ein Cent, Beträge mit 5 an der dritten Nachkommastelle, sehr große Summen?
  • Läuft die Anwendung auf einer PHP-Version, die noch Sicherheitsupdates bekommt?

#Zahlungsdateien und Datenschutz in Hessen

Eine Überweisungsdatei enthält Namen und IBANs, bei Erstattungen an Privatkunden also personenbezogene Daten. Ich lege solche Dateien nicht dauerhaft auf dem Webserver ab und schreibe sie nicht in Logs. Zuständige Aufsicht für Unternehmen mit Sitz in Hessen ist der Hessische Beauftragte für Datenschutz und Informationsfreiheit in Wiesbaden. Wie lange Sie Zahlungsdaten aufbewahren müssen, entscheidet Ihre Steuerberatung, nicht die Anwendung.

#Ablauf: vom Code-Befund bis zur Bankdatei

  1. Sie beschreiben mir schriftlich, wo in Ihrer Anwendung Beträge entstehen, wie sie gespeichert werden und wohin sie gehen.
  2. Ich lese den Code, die Datenbankstruktur und, falls vorhanden, eine abgewiesene Bankdatei. Meine Befunde schreibe ich Ihnen auf, mit Dateinamen und Zeilen.
  3. Wir legen schriftlich Umfang, Annahmen und Ausschlüsse fest, zum Beispiel ob nur die Rundung vereinheitlicht wird oder auch der SEPA-Export neu entsteht.
  4. Ich arbeite auf einer Testumgebung. Für jede geänderte Berechnung schreibe ich Tests, die alte und neue Ergebnisse gegenüberstellen, damit Sie Abweichungen vor dem Livegang sehen.
  5. Sie prüfen, Ihre Buchhaltung gleicht Stichproben ab, Sie testen die Datei mit Ihrer Bank. Danach geht die Änderung live.
  6. Auf Wunsch übernehme ich danach die laufende Pflege.

Rückfragen stelle ich schriftlich und antworte an Werktagen. Alles, was wir vereinbaren, steht in der Korrespondenz und lässt sich später nachlesen.

#Was ich von Ihnen brauche

  • Zugang zum Repository oder einen Export des Codes
  • eine Testdatenbank ohne echte Kundendaten oder mit pseudonymisierten Daten
  • die PHP-Version auf dem Server und den Namen des Hosters
  • für den SEPA-Export die Unterlagen Ihrer Bank zur Datenfernübertragung
  • eine Ansprechperson in der Buchhaltung, die Ergebnisse gegenprüft

#Häufige Fragen

#Darf ich Geldbeträge in PHP als float speichern?

Für Anzeige und Schätzungen geht das, für Buchungen nicht. php.net schreibt, dass Zahlen wie 0.1 und 0.7 im Binärformat nicht exakt darstellbar sind, und rät davon ab, float-Ergebnissen bis zur letzten Stelle zu trauen. Für Beträge nehme ich Cent als Ganzzahl, BCMath oder eine Geldbibliothek.

#Brauche ich PHP 8.4 für BCMath?

Nein. Die bc-Funktionen wie bcadd() gibt es schon lange. Neu in PHP 8.4 sind die Klasse BcMath\Number, die Funktion bcround() und das Enum RoundingMode. Laut php.net bekommt PHP 8.4 Sicherheitsupdates bis zum 31. Dezember 2028.

#Welche Rundung ist für Rechnungen richtig?

Das legt nicht der Code fest, sondern Ihre Steuerberatung und Ihre Verträge. Technisch sorge ich dafür, dass an genau einer Stelle gerundet wird, mit einem ausdrücklich genannten Rundungsmodus, und dass die Summe der Positionen zur Rechnungssumme passt.

#Welche Version von pain.001 verlangen deutsche Banken?

Die Spezifikation der Deutschen Kreditwirtschaft, Anlage 3 des DFÜ-Abkommens in Version 26.11, gilt ab dem 15. November 2026 und beschreibt die Überweisung als pain.001.001.09. Welche Versionen Ihre Bank parallel annimmt, steht in deren Unterlagen zum Datenaustausch.

#Warum lehnt die Bank meine SEPA-Datei ab?

Häufige Ursachen sind eine Kontrollsumme, die nicht zu den Einzelbeträgen passt, Zeichen außerhalb des erlaubten Zeichensatzes, Textfelder nur aus Leerzeichen oder Referenzen mit Schrägstrich am Anfang oder Ende. Die Spezifikation erlaubt der Bank in solchen Fällen, die ganze Datei abzuweisen.

#Was soll ich für eine erste Einschätzung schicken?

Eine kurze Beschreibung der Anwendung, wo Beträge entstehen und wohin sie gehen, die PHP-Version, eine Frist und wenn möglich eine von der Bank abgewiesene Datei mit geschwärzten Kontodaten. Zugänge brauche ich dafür nicht.

#Weitere Leistungen in Frankfurt

#Schreiben Sie mir

Rechnet Ihre PHP-Anwendung mit float, rundet sie an mehreren Stellen oder weist Ihre Bank die Überweisungsdatei zurück? Schreiben Sie mir über das Kontaktformular, welche Anwendung es ist, wo die Beträge herkommen, welche PHP-Version läuft und bis wann Sie eine Lösung brauchen. Ich lese den Fall, stelle meine Rückfragen schriftlich und schicke Ihnen danach eine erste Einschätzung. Die ist kostenlos.

Brauchen Sie die Leistung: PHP-Entwickler in Frankfurt?

Beschreiben Sie Ihr Projekt per E-Mail. Antwort innerhalb von 48 Stunden.

Projekt beschreiben

Weitere WordPress-Dienste und Wissensbasis entdecken