Zum Hauptinhalt springen
Schneespur
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ößeGespeichert alsSpaltentyp
Geldganzzahlige Centinteger (z. B. price_amount_cents)
TemperaturCelsiusdecimal / float
Geschwindigkeitkm/hdecimal / float
EntfernungMeterdecimal / float
GewichtKilogrammdecimal / float
SchneehöheZentimeterdecimal / float
NiederschlagMillimeterdecimal / float
ZeitstempelUTCtimestamp / 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üsselErlaubte Werte
format_regionde at ch us ca gb (dazu, was Module registrieren)
currency_codeISO 4217, z. B. EUR USD CAD GBP CHF
currency_positionbefore after
number_formatdot_comma (1.234,56) · comma_dot (1,234.56) · apostrophe_dot (1’234.56)
date_formatdmy_dot (06.08.2026) · dmy_slash (06/08/2026) · mdy_slash (08/06/2026) · iso (2026-08-06)
time_format24h 12h
unit_distancemetric imperial
unit_temperaturecelsius fahrenheit
unit_weightmetric 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:

Aufrufmetrischimperial
distance(850.0)850 m2,789 ft
distance(2000.0)2,0 km1.2 mi
weight(250.0)250 kg551 lb
weight(1500.0)1,5 t1.7 tn
snowDepth(12.0)12,0 cm4.7 in
precipitation(1.2)1,2 mm0.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-d im Wert von <input type="date"> und Y-m-d\TH:i bei datetime-local — das liest der Browser, kein Mensch
  • Y-m-d_His in erzeugten Dateinamen; ein Schrägstrich aus mdy_slash wä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 den Formatter
  • 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.Y oder H:i als Literal
  • Maschinenformate unangetastet