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
=== falseprüfen, nicht auf falsy. (int) 'abc'ist0, ohne jede Meldung.'abc' + 1ist seit PHP 8 einTypeError, kein Warning mehr.(float) '4,99'ist4— deutsche Eingaben vorher umschreiben.- Große Zahlen werden beim Cast lautlos auf
PHP_INT_MAXgekappt. - 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, ...).