Dokumentation durchblättern
Module entwickeln
Formate & Einheiten
Wie ein Modul Zahlen, Geld, Datum und Messwerte anzeigt, ohne der installationsweiten Einstellung zu widersprechen — kanonisch speichern, beim Anzeigen formatieren.
Seit 1.2.0. Währung, Zahlenformat, Datum, Uhrzeit, Temperatur, Geschwindigkeit, Entfernung und Gewicht folgen einer einzigen Einstellung, die für die ganze Installation gilt. Ein Modul, das einen Wert selbst formatiert, hebelt damit die Entscheidung des Betreibers aus — auf einer amerikanischen Installation steht dann auf Ihrem Bildschirm Kilogramm und auf jedem anderen Pfund.
Die Regel dahinter passt in einen Satz: kanonisch speichern, beim Anzeigen formatieren.
Das klingt selbstverständlich, ist es in der Praxis aber nicht. Die Versuchung ist groß, eine Zahl gleich beim Speichern „richtig“ abzulegen — in der Einheit, in der sie später erscheinen soll. Genau das macht eine Umstellung später unmöglich, weil die Datenbank dann nicht mehr weiß, was der Wert eigentlich bedeutet.
Die kanonischen Einheiten
So liegen die Werte in der Datenbank, im Core wie in den Tabellen Ihres Moduls. Sie ändern sich nie, wenn eine Einstellung sich ändert: Ein Wechsel von Celsius auf Fahrenheit schreibt keine einzige Zeile um.
| Größe | Gespeichert als | Spaltentyp |
|---|---|---|
| Geld | ganzzahlige Cent | integer (z. B. price_amount_cents) |
| Temperatur | Celsius | decimal / float |
| Geschwindigkeit | km/h | decimal / float |
| Entfernung | Meter | decimal / float |
| Gewicht | Kilogramm | decimal / float |
| Schneehöhe | Zentimeter | decimal / float |
| Niederschlag | Millimeter | decimal / float |
| Zeitstempel | UTC | timestamp / datetime |
Eine Einstellung umzustellen ist damit eine Umbeschriftung, keine Umrechnung. Das gilt
ausdrücklich auch für Geld: Es findet keine Währungsumrechnung statt. Wer
currency_code von EUR auf USD stellt, sieht 12345 weiterhin als
einhundertdreiundzwanzig Komma fünfundvierzig, nur mit einem anderen Zeichen davor. Ein
Betrieb, der tatsächlich die Währung wechselt, muss seine Preise neu festlegen — und das
soll er bewusst tun, nicht durch einen Kurs, den eine Software sich irgendwo geholt hat.
Die neun Einstellungen
Sie lassen sich auslesen, aber in aller Regel brauchen Sie das nicht: Der Formatter hält
sie bereits.
| Schlüssel | Erlaubte Werte |
|---|---|
format_region | de at ch us ca gb (dazu, was Module registrieren) |
currency_code | ISO 4217, z. B. EUR USD CAD GBP CHF |
currency_position | before after |
number_format | dot_comma (1.234,56) · comma_dot (1,234.56) · apostrophe_dot (1’234.56) |
date_format | dmy_dot (06.08.2026) · dmy_slash (06/08/2026) · mdy_slash (08/06/2026) · iso (2026-08-06) |
time_format | 24h 12h |
unit_distance | metric imperial |
unit_temperature | celsius fahrenheit |
unit_weight | metric imperial |
Ist nichts gesetzt, gelten die Werte der de-Vorlage. Bei einer Container-Installation über
Docker oder Coolify läuft der Browser-Installer nicht mit; dort füllt eine Migration die
Werte aus APP_LOCALE.
Geschwindigkeit hat bewusst keinen eigenen Schlüssel, sie folgt unit_distance. Kein
Land misst in Meilen und fährt in km/h — ein eigener Schalter wäre nur eine zusätzliche
Möglichkeit, sich zu widersprechen.
Der Formatter
Klasse: App\Services\Format\Formatter, in AppServiceProvider als Singleton
gebunden.
public const PLACEHOLDER = '—';
date(?DateTimeInterface $moment): string
time(?DateTimeInterface $moment, bool $withSeconds = false): string
dateTime(?DateTimeInterface $moment, bool $withSeconds = false): string
number(?float $value, int $decimals = 1): string
money(?int $cents): string
currencySymbol(): string
temperature(?float $celsius): string
speed(?float $kmh): string
distance(?float $metres): string
weight(?float $kilograms): string
snowDepth(?float $centimetres): string
precipitation(?float $millimetres): string
parseDecimal(string $input): ?float // die Gegenrichtung — siehe unten
parseMoneyToCents(string $input): ?int
browserSettings(): array // die drei Werte, die das Layout an JS reicht
Zwei Eigenheiten, die Ihnen Arbeit ersparen:
Jede Anzeigemethode nimmt null entgegen und gibt dafür PLACEHOLDER zurück. Der alte
Dreisatz $x ? format_date($x) : '—' ist damit überflüssig; er verdoppelt nur, was der
Dienst ohnehin tut, und bringt die Gefahr mit, an einer Stelle ein anderes Zeichen zu
verwenden als an allen anderen.
Die Anzeige-Zeitzone setzt der Formatter selbst, nicht der Aufrufer. Schreiben Sie also
kein ->setTimezone(config('app.display_timezone')), bevor Sie einen Wert übergeben. Es
passiert innen ohnehin, und zweimal angewendet ist es ein Fehler, der erst auffällt, wenn
ein Kunde in einer anderen Zeitzone sitzt.
Helfer
In app/helpers.php liegen schmale Wrapper, die überall verfügbar sind — auch in den
Blade-Views Ihres Moduls:
format_date($moment) format_number($value, $decimals = 1)
format_time($moment, $withSeconds = false) format_money($cents)
format_datetime($moment, $withSeconds = false)
format_temperature($celsius) format_speed($kmh)
format_distance($metres) format_weight($kilograms)
format_snow_depth($centimetres) format_precipitation($millimetres)
{{-- In einer Modul-View --}}
<td>{{ format_datetime($delivery->created_at) }}</td>
<td>{{ format_weight($delivery->grit_kilograms) }}</td>
<td>{{ format_money($delivery->price_cents) }}</td>
Maßstabswechsel
distance() und weight() wählen ihren Maßstab nach der Größenordnung. Sie übergeben also
immer den kanonischen Wert und bekommen etwas Lesbares zurück, statt selbst entscheiden zu
müssen, ob 2.000 Meter als Meter oder als Kilometer erscheinen sollen:
| Aufruf | metrisch | imperial |
|---|---|---|
distance(850.0) | 850 m | 2,789 ft |
distance(2000.0) | 2,0 km | 1.2 mi |
weight(250.0) | 250 kg | 551 lb |
weight(1500.0) | 1,5 t | 1.7 tn |
snowDepth(12.0) | 12,0 cm | 4.7 in |
precipitation(1.2) | 1,2 mm | 0.05 in |
Imperiales Gewicht rechnet in short tons (2.000 lb), nicht in der britischen long ton.
Schneehöhe und Niederschlag folgen unit_distance — niemand misst die Straße in Meilen und
den Schnee darauf in Zentimetern. Der Niederschlag behält in Zoll zwei Nachkommastellen:
Ein Millimeter sind 0,04 in, mit nur einer Stelle würde ein echter Regen auf 0.0
abgerundet und damit zu „kein Niederschlag“.
Eingabe: die Gegenrichtung
Ein Formularfeld, das eine Zahl annimmt, muss sie im Format des Betreibers lesen. 1.234,56
und 1,234.56 sind derselbe Betrag, von zwei verschiedenen Menschen getippt — und
(float) '1.234,56' ergibt in PHP 1.0.
$cents = app(Formatter::class)->parseMoneyToCents($request->input('price'));
if ($cents === null) {
// nicht lesbar — ablehnen, niemals 0 speichern
}
parseDecimal() ist streng auf die eingestellten Trennzeichen und rät nicht. 12,34,56
liefert auf einer Installation mit Komma-Dezimaltrennung null statt 123456. Das ist
Absicht: Einen Tippfehler stillschweigend als hundertmal zu große Zahl zu lesen, setzt einen
falschen Preis in einen Vertrag.
Verwenden Sie auf einem frei getippten Zahlenfeld keine numeric-Validierung — Laravels
numeric weist 1.234,56 zurück. Erst parsen, dann den geparsten Wert prüfen.
Einheitenkürzel stehen in den Übersetzungen
°C, km/h, lb und Verwandte kommen aus lang/{locale}/format.php, nie als Literal im
Code. So kann ein Sprachpaket sie lokalisieren. Braucht Ihr Modul ein Kürzel, das der Core
nicht mitbringt, liefern Sie es in Ihrer eigenen, namensraumbehafteten Übersetzungsdatei
aus, statt die Zeichenkette einzubauen.
Der Core setzt das mit tests/Feature/FormatDriftGuardTest.php durch. Der Test lässt den
Build scheitern bei einem fest eingebauten €, km/h oder °C, bei einem mm oder cm,
das an einen ausgegebenen Wert geklebt oder in eine Übersetzung eingebacken wurde, sowie bei
format('d.m.Y…') oder einem nackten format('H:i…') irgendwo unter app/,
resources/views/ oder lang/.
Ein solcher Wächter lohnt sich auch für Ihr Modul. Die Klasse existiert nämlich nicht aus
Prinzipienreiterei: In lang/en/customer_object.php stand über mehrere Releases hinweg
Price (€), und es ist niemandem aufgefallen.
Was unangetastet bleibt
Maschinenformate sind keine Anzeige-Entscheidung. Diese hier bleiben genau so, wie sie sind:
Y-m-dim Wert von<input type="date">undY-m-d\TH:ibeidatetime-local— das liest der Browser, kein MenschY-m-d_Hisin erzeugten Dateinamen; ein Schrägstrich ausmdy_slashwäre dort ein Pfadtrenner- ISO 8601 in API-Antworten und im DSGVO-Datenexport (Art. 20 verlangt Maschinenlesbarkeit)
- Datenbankspalten, JSON-Nutzlasten,
toISOString()in JS-Datumsauswahlfeldern - Breiten- und Längengrad — technische Koordinaten, immer mit Punkt als Dezimaltrenner
Die Browser-Seite
Das Layout reicht an JavaScript genau drei Werte weiter, über Formatter::browserSettings():
window.schneespurFormat // { timeFormat, dateFormat, numberFormat }
window.schneespurFormatTime(date) // Date → "2:30 PM" / "14:30"
window.schneespurFormatDate(date) // Date → "08/06/2026" / "06.08.2026"
Beide Funktionen erwarten ein Date-Objekt und weisen alles andere zurück. Sie geben
dann denselben Platzhalter aus, den auch der Server schreibt, und warnen einmal pro Seite
statt einmal pro Aufruf. Sichern Sie Ihre eigene Aufrufstelle trotzdem ab: new Date(null)
ist nicht etwa ein ungültiges Datum, sondern der 1. Januar 1970. Übergeben Sie also
value ? new Date(value) : null.
Hierher gehört ohnehin fast nichts. Was der Server rendern kann, rendert der Server.
RegionRegistry
Ein Sprachpaket kann seine Regionsvorlage gleich mitbringen. Die Einzelheiten stehen bei den Registries.
Checkliste für ein Modul, das Zahlen anzeigt
- Speichert kanonisch — Cent, Celsius, km/h, Meter, Kilogramm, Zentimeter, Millimeter, UTC
- Jede Anzeige läuft über einen
format_*()-Helfer oder denFormatter - Keine Null-Dreisätze darum herum und kein
setTimezone()davor - Jede vom Betreiber getippte Zahl läuft durch
parseDecimal()/parseMoneyToCents() - Nirgends im Modul ein
€,km/h,°C,d.m.YoderH:ials Literal - Maschinenformate unangetastet