PHP: Wie wandelt man einen String in eine Zahl um?

Für die Umwandlung gibt es einen Cast, und der funktioniert. Die eigentliche Frage ist aber meistens eine andere: Was soll passieren, wenn der String gar keine Zahl ist? Der Cast beantwortet sie nämlich stillschweigend, und zwar selten so, wie man es gebrauchen kann.

Die Casts im Überblick

Eingabe    (int)    (float)   is_numeric
'42'       42       42        true
'42.7'     42       42.7      true
'  42  '   42       42        true
'42abc'    42       42        false
'abc'      0        0         false
''         0        0         false
'1e3'      1000     1000      true
'0x1A'     0        0         false
'007'      7        7         true
'4,99'     4        4         false
'-3.5'     -3       -3.5      true
'+7'       7        7         true

Zwei Zeilen davon sind es wert, sich sie zu merken. 'abc' wird zu 0 — nicht zu null, nicht zu false, sondern zu einer gültig aussehenden Zahl. Und '4,99' wird zu 4, was in einem deutschsprachigen Formular der häufigste Fall überhaupt sein dürfte.

Beide Male gibt es keine Meldung. Dein Code rechnet einfach mit einer Zahl weiter, die nichts mit der Eingabe zu tun hat.

Der Cast schweigt, die Rechnung meldet sich

Das ist ein Unterschied, den man kennen sollte:

(int) '42abc'   : 42
'42abc' + 1     : 43    Warning: A non-numeric value encountered
(int) 'abc'     : 0
'abc' + 1       : TypeError: Unsupported operand types: string + int

Der Cast sagt in keinem der Fälle etwas. Die Rechnung dagegen warnt bei einem teilweise numerischen String — und wirft bei einem komplett nicht-numerischen einen TypeError.

Der TypeError ist neu in PHP 8. In PHP 7 gab es dort nur eine Warning. Wer also $s + 0 als Umwandlung im Code stehen hat, hat seit PHP 8 einen möglichen Absturz an dieser Stelle. Das ist eine der Änderungen, die man beim Umstieg leicht übersieht, weil sie nur bei ungültigen Eingaben zuschlägt.

Was ich stattdessen empfehle

Wenn die Eingabe von außen kommt — und das tut sie fast immer, sonst hättest du ja schon eine Zahl — dann willst du nicht casten, sondern prüfen:

<?php
  $menge = filter_var($_POST['menge'] ?? '', FILTER_VALIDATE_INT);

  if ($menge === false) {
    // Eingabe war keine gültige ganze Zahl
  }
?>
'42'     INT: 42       FLOAT: 42.0
'42.7'   INT: false    FLOAT: 42.7
'42abc'  INT: false    FLOAT: false
' 42 '   INT: 42       FLOAT: 42.0
''       INT: false    FLOAT: false
'0'      INT: 0        FLOAT: 0.0
'-3'     INT: -3       FLOAT: -3.0

filter_var() gibt dir bei einer ungültigen Eingabe false zurück — also etwas, das sich von jeder gültigen Zahl unterscheiden lässt. Genau das kann der Cast nicht.

Beachte, dass FILTER_VALIDATE_INT den String '42.7' ablehnt. Das ist meist gewollt: Wer eine ganze Zahl erwartet, will keine stillschweigend abgerundete.

Wenn dir ein Standardwert lieber ist als false:

<?php
  $menge = filter_var($eingabe, FILTER_VALIDATE_INT,
      ['options' => ['default' => 1]]);
?>

Der Klassiker mit der Null

Aufpassen bei der Prüfung:

filter_var('0', FILTER_VALIDATE_INT) : 0
if ($r)           : false   <- 0 ist falsy!
if ($r !== false) : true

'0' ist eine gültige Eingabe und liefert 0. Und 0 ist falsy. Wer mit if (!$menge) prüft, behandelt die gültige Eingabe 0 wie einen Fehler.

Also === false beziehungsweise !== false. Dieselbe Falle wie bei array_search() und strpos().

Das Komma

In deutschsprachigen Formularen gibt jeder zweite Nutzer 4,99 ein. Was PHP daraus macht:

(float) '4,99'                   : 4       <- alles nach dem Komma weg
is_numeric('4,99')               : false
str_replace(',', '.', ...) davor : 4.99

Aus 4,99 € werden vier Euro. Der Fehler fällt niemandem auf, weil 4 eine völlig plausible Zahl ist — bis jemand die Summen nachrechnet.

Ein str_replace(',', '.', $eingabe) vor der Umwandlung räumt das auf. Bei Beträgen mit Tausendertrennzeichen (1.234,56) musst du zusätzlich die Punkte entfernen, und zwar in dieser Reihenfolge:

<?php
  $roh = str_replace(['.', ','], ['', '.'], $eingabe);
  $betrag = filter_var($roh, FILTER_VALIDATE_FLOAT);
?>
'1.234,56' -> '1234.56'  -> 1234.56
'4,99'     -> '4.99'     -> 4.99
'1234.56'  -> '123456'   -> 123456.0    <- falsch
'1,234.56' -> '1.23456'  -> 1.23456     <- falsch

Die beiden unteren Zeilen zeigen die Grenze dieses Verfahrens: Es setzt deutsche Schreibweise voraus. Gibt jemand englisch formatiert ein, wird lautlos etwas völlig anderes daraus — aus 1234.56 werden 123.456.

Wenn du das nicht ausschließen kannst, nimm NumberFormatter aus der Erweiterung intl. Der kennt die Konventionen der jeweiligen Sprache:

<?php
  $nf = new NumberFormatter('de_DE', NumberFormatter::DECIMAL);
  $betrag = $nf->parse('1.234,56');   // 1234.56
?>

Ganz aus der Welt ist das Problem damit nicht — du musst ja immer noch wissen, in welcher Sprache der Nutzer getippt hat. Zuverlässiger ist es, im Formular gar nicht erst beide Schreibweisen zuzulassen: ein <input type="number"> liefert dem Server immer einen Punkt als Dezimaltrennzeichen, unabhängig davon, was der Nutzer im Browser sieht.

intval() mit einer Basis

Das kann intval() und der Cast nicht:

intval('0x1A')      : 0
intval('0x1A', 16)  : 26
intval('1A', 16)    : 26
intval('012', 8)    : 10
intval('101', 2)    : 5
intval('0x1A', 0)   : 26

Die Basis 0 heißt “schau dir das Präfix an”: 0x hexadezimal, führende 0 oktal, sonst dezimal. Praktisch beim Einlesen von Konfigurationsdateien, in denen Dateirechte als 0644 stehen.

Wo die Zahlen aufhören

Zwei Grenzen, die man kennen sollte.

Die Größe:

PHP_INT_MAX                 : 9223372036854775807
(int) '9223372036854775808' : 9223372036854775807   <- still gekappt
'9223372036854775808' + 0   : float

Der Cast kappt lautlos auf PHP_INT_MAX. Bei einer ID aus einem Fremdsystem bekommst du also eine falsche, aber plausibel aussehende Zahl. Solche IDs lässt man besser als String stehen — man rechnet ja ohnehin nicht mit ihnen.

Die Genauigkeit:

0.1 + 0.2 == 0.3        : false
0.1 + 0.2               : 0.30000000000000004441
(int) ((0.1 + 0.7) * 10) : 7

Die letzte Zeile ist der Klassiker aus dem PHP-Handbuch. (0.1 + 0.7) * 10 ist intern minimal kleiner als 8, und (int) schneidet ab statt zu runden. Heraus kommt 7.

Für Geldbeträge heißt das: in Cent rechnen, also mit ganzen Zahlen. Erst bei der Ausgabe durch 100 teilen. Das erspart eine ganze Klasse von Fehlern, die sonst irgendwann als “ein Cent Differenz” in der Buchhaltung auftauchen.

is_numeric oder ctype_digit?

Zum Schluss noch der Unterschied, der oft übersehen wird:

'42'   is_numeric: true   ctype_digit: true
'4.2'  is_numeric: true   ctype_digit: false
'-4'   is_numeric: true   ctype_digit: false
' 42'  is_numeric: true   ctype_digit: false
'42 '  is_numeric: true   ctype_digit: false
'1e3'  is_numeric: true   ctype_digit: false
''     is_numeric: false  ctype_digit: false

is_numeric() akzeptiert Vorzeichen, Dezimalpunkt, Exponentialschreibweise und Leerzeichen davor wie dahinter. Wenn du “nur Ziffern” meinst — bei einer Postleitzahl oder einer Kundennummer — dann nimm ctype_digit().

Und beachte, dass ctype_digit('') ebenfalls false liefert, ein leerer String also nicht durchrutscht.

Zusammenfassung

  • Für Eingaben von außen filter_var() statt eines Casts — es kann “ungültig” von “gültige Null” unterscheiden.
  • Dabei auf === false prüfen, nicht auf falsy.
  • (int) 'abc' ist 0, ohne jede Meldung.
  • 'abc' + 1 ist seit PHP 8 ein TypeError, kein Warning mehr.
  • (float) '4,99' ist 4 — deutsche Eingaben vorher umschreiben.
  • Große Zahlen werden beim Cast lautlos auf PHP_INT_MAX gekappt.
  • Geldbeträge in Cent rechnen, nicht in Euro.
  • ctype_digit(), wenn du wirklich nur Ziffern meinst.

Hinweis zu Netcup (Werbung)

Der deutsche Hoster Netcup bietet unter anderem günstige und zugleich leistungsstarke Webhosting Pakete, KVM-basierte Root Server und dezidierte Server an. Mit unseren Gutscheincodes kannst du noch mehr Geld sparen (6€ bei deiner ersten Bestellung, 30% Rabatt auf alle KVM-basierten Root Server, ...).