Was declare(strict_types=1) wirklich absichert und wo Type Juggling trotzdem zuschlägt
Viele Entwickler setzen declare(strict_types=1) an den Anfang jeder Datei und glauben damit, ihr Code sei vollständig gegen die berüchtigten Type-Juggling-Überraschungen von PHP abgesichert. Tatsächlich deckt strict_types nur einen Teil des Problems ab, nämlich Funktionsargumente und Rückgabewerte. Interne Vergleiche mit == bleiben davon vollkommen unberührt und lose typisiert. Dieser Artikel zeigt die Grenzen von strict_types genau auf und beschreibt konkrete Fallstricke, denen erfahrene PHP-Entwickler trotzdem regelmässig begegnen.
Inhaltsverzeichnis
- 1. Was declare(strict_types=1) tatsächlich abdeckt
- 2. Was strict_types NICHT abdeckt: interne Vergleiche
- 3. Type Juggling bei == im Detail
- 4. Die PHP-8-Änderung bei String-Zahl-Vergleichen
- 5. Die Falle numerischer Strings
- 6. Die Zero-vs-Empty-Falle in Bedingungen
- 7. Fallstricke in switch und in_array ohne strikten Modus
- 8. Wie PHPStan und Psalm die Lücke schließen
- 9. Praxisregeln für den täglichen Gebrauch
- 10. Zusammenfassung
- 11. FAQ
1. Was declare(strict_types=1) tatsächlich abdeckt
declare(strict_types=1) verändert ausschließlich das Verhalten bei der Übergabe von Argumenten an Funktionen und Methoden sowie bei deren Rückgabewerten. Ohne strict_types würde PHP versuchen, einen übergebenen String wie '42' automatisch in einen erwarteten int-Parameter zu konvertieren. Mit aktiviertem strict_types schlägt dieser Aufruf stattdessen mit einem TypeError fehl, außer bei der einzigen erlaubten Ausnahme: int-Werte dürfen weiterhin verlustfrei an einen float-Parameter übergeben werden.
Wichtig ist, dass diese Direktive dateibasiert wirkt und sich auf den Aufrufkontext bezieht, nicht auf die Definition der Funktion selbst. Ruft eine Datei ohne strict_types eine streng typisierte Funktion in einer anderen Datei auf, gilt für diesen Aufruf trotzdem die lose Typisierung der aufrufenden Datei. Wer eine Bibliothek schreibt, hat also keine Garantie, dass strict_types beim Konsumenten überhaupt aktiv ist, das Verhalten hängt einzig von der Datei ab, in der der Aufruf steht.
2. Was strict_types NICHT abdeckt: interne Vergleiche
Der häufigste Irrtum ist die Annahme, strict_types würde auch den Vergleichsoperator == strenger machen. Das ist falsch: strict_types hat keinerlei Einfluss auf ==, in, switch-Fälle ohne strikten Modus oder implizite Boolean-Auswertungen in if-Bedingungen. Der Ausdruck '0' == false liefert weiterhin true, vollkommen unabhängig davon, ob strict_types in der Datei gesetzt ist oder nicht.
Das bedeutet konkret: strict_types löst genau ein Klasse von Bugs, nämlich falsch typisierte Funktionsaufrufe, lässt aber die zweite große Fehlerklasse, unerwartete Ergebnisse bei losen Vergleichen, vollständig unberührt. Erfahrene Entwickler, die sich allein auf strict_types verlassen, wiegen sich hier in falscher Sicherheit und übersehen genau die Stellen, an denen == mit gemischten Typen im Code auftaucht.
3. Type Juggling bei == im Detail
Der lose Vergleichsoperator == konvertiert bei unterschiedlichen Operanden-Typen einen der beiden Werte, bevor er sie vergleicht. Welcher Wert konvertiert wird und in welche Richtung, folgt einer Reihe von Regeln, die PHP in der offiziellen Vergleichstabelle dokumentiert, und die selbst erfahrene Entwickler regelmässig überraschen. Ein numerischer String wird beim Vergleich mit einem int in eine Zahl umgewandelt, ein bool-Wert erzwingt eine boolesche Auswertung des anderen Operanden.
Besonders gefährlich wird es bei Arrays von Vergleichen, etwa in in_array oder array_search ohne den dritten Parameter strict. Sucht man mit in_array('admin', $roles) und $roles enthält zufällig den Wert 0, liefert der Vergleich '0' == 'admin' zwar false, aber 0 == 'admin' lieferte in PHP-Versionen vor 8.0 tatsächlich true, weil der String in eine Zahl umgewandelt wurde. Seit PHP 8 wurde dieses spezielle Verhalten zwar entschärft, das grundsätzliche Risiko bei == bleibt aber bei anderen Typkombinationen bestehen.
<?php
declare(strict_types=1);
// strict_types schützt hier überhaupt nicht, == bleibt lose
var_dump('0' == false); // true
var_dump('0.0' == '0'); // true
var_dump(' 1' == '1'); // true, führender Leerraum wird ignoriert
var_dump('1e2' == '100'); // true, beide numerisch gleichwertig
var_dump(null == false); // true
// Sichere Alternative: identischer Typ und Wert erzwingen
var_dump('0' === false); // false
var_dump('1e2' === '100'); // false
4. Die PHP-8-Änderung bei String-Zahl-Vergleichen
Vor PHP 8.0 wurde beim Vergleich eines int mit einem nicht numerischen String der String zürst in eine Zahl konvertiert, was zu dem berühmten Ergebnis 0 == 'foo' als true führte, weil 'foo' zu 0 konvertiert wurde. Seit PHP 8.0 gilt eine deutlich intuitivere Regel: Ist der String nicht numerisch, wird stattdessen die Zahl in einen String konvertiert und beide Werte als Strings verglichen, wodurch 0 == 'foo' inzwischen korrekt false liefert.
Diese Änderung entschärft einen der berüchtigtsten historischen Fallstricke von PHP erheblich, sie löst das grundsätzliche Problem loser Vergleiche aber nicht vollständig auf. Für zwei numerische Strings oder für einen int gegen einen numerischen String gilt weiterhin die numerische Konvertierung, und genau dort lauern die im vorherigen Abschnitt gezeigten Überraschungen bei führenden Nullen oder wissenschaftlicher Notation unverändert fort.
5. Die Falle numerischer Strings
PHP unterscheidet intern zwischen numerischen und nicht numerischen Strings, und diese Unterscheidung beeinflusst das Verhalten von ==. Ein String wie '123' gilt als vollständig numerisch, ein String wie '123abc' dagegen nicht. Werden zwei Strings verglichen, die beide numerisch sind, wandelt PHP sie für den Vergleich in Zahlen um, unabhängig von == oder deklarierten Typen. Das führt zu Überraschungen wie '010' == '10', obwohl beide Strings als Zeichenketten offensichtlich unterschiedlich sind.
Besonders relevant wird das bei Eingabedaten aus Formularen oder CSV-Dateien, die grundsätzlich als Strings vorliegen. Eine Postleitzahl wie '00500' wird beim Vergleich mit '500' als numerisch gleich behandelt, obwohl fachlich zwei unterschiedliche Werte gemeint sein könnten. Wer solche Daten vergleicht, sollte explizit mit === und identischer String-Normalisierung arbeiten, statt sich auf die automatische numerische Interpretation zu verlassen.
6. Die Zero-vs-Empty-Falle in Bedingungen
Eine weitere klassische Falle betrifft implizite Boolean-Auswertungen in if-Bedingungen, die ebenfalls von strict_types unberührt bleiben. Die Werte 0, 0.0, '0', '', null, false und ein leeres Array gelten in PHP allesamt als falsy und werden in einer if-Bedingung identisch behandelt. Ein Code wie if (!$quantity) für eine Bestellmenge behandelt eine tatsächliche Menge von 0 genauso wie einen fehlenden Wert null, obwohl beides fachlich sehr unterschiedliche Situationen sind.
Die zuverlässige Alternative ist eine explizite Prüfung mit === null oder eine strikte Typprüfung mit is_int in Kombination mit einer expliziten Wertebereichsprüfung. Gerade bei Mengenangaben, Preisen oder IDs, bei denen 0 ein gültiger, fachlich bedeutsamer Wert sein kann, führt eine implizite Boolean-Prüfung regelmässig zu Bugs, die erst in der Produktion mit realen Randfall-Daten sichtbar werden.
<?php
declare(strict_types=1);
function applyDiscount(?int $quantity): string
{
// Fehleranfällig: 0 und null werden identisch behandelt
if (!$quantity) {
return 'Keine Menge angegeben';
}
// Korrekt: unterscheidet fehlenden Wert von tatsächlicher Null
if ($quantity === null) {
return 'Keine Menge angegeben';
}
if ($quantity === 0) {
return 'Menge ist explizit null';
}
return sprintf('Rabatt für %d Stück', $quantity);
}
7. Fallstricke in switch und in_array ohne strikten Modus
Auch switch-Anweisungen vergleichen ihre case-Werte standardmässig mit ==, nicht mit ===. Ein switch über einen Wert, der zufällig 0 oder ein leerer String sein kann, kann so unbeabsichtigt in einem case landen, der eigentlich für einen ganz anderen Wert gedacht war, etwa case false, sofern dieser vorkommt. Da PHP die case-Reihenfolge von oben nach unten prüft und beim ersten losen Treffer stoppt, ist das Ergebnis oft schwer vorherzusagen.
Bei in_array und array_search gilt dasselbe Prinzip: Ohne den dritten Parameter true wird jeder Vergleich mit == durchgeführt. Bei Arrays mit gemischten Typen, etwa IDs, die teils als int und teils als String aus unterschiedlichen Datenquellen stammen, führt das zu inkonsistenten Ergebnissen. Die einfache Regel lautet: in_array und array_search sollten in modernem Code praktisch immer mit dem dritten Parameter true aufgerufen werden.
8. Wie PHPStan und Psalm die Lücke schließen
Weil strict_types den Vergleichsoperator == nicht absichert, übernehmen statische Analysewerkzeuge diese Aufgabe. PHPStan meldet ab einem ausreichend hohen Level, in Kombination mit der Erweiterung phpstan-strict-rules, verdächtige lose Vergleiche zwischen inkompatiblen Typen, etwa den Vergleich eines int mit einem string, der offensichtlich nicht numerisch ist. Psalm bietet mit seinem RiskyTruthyFalsyComparison-Level eine ähnliche Absicherung für implizite Boolean-Auswertungen.
In der Praxis empfiehlt es sich, beide Werkzeuge in der CI-Pipeline so scharf wie möglich zu konfigurieren und explizit auf lose Vergleiche zu prüfen, statt sich auf Code-Review-Aufmerksamkeit zu verlassen. Die Kombination aus strict_types für Funktionsgrenzen und einer strengen statischen Analyse für interne Vergleiche schließt die beiden grössten Lücken des PHP-Typsystems zuverlässig.
9. Praxisregeln für den täglichen Gebrauch
Eine einfache Faustregel hilft im Alltag: === ist der Standard, == ist die bewusste, dokumentierte Ausnahme. Ausnahmen sollten sich auf wenige, klar begründete Fälle beschränken, etwa den absichtlichen Vergleich zwischen int und float in einer numerischen Berechnung, bei der eine automatische Konvertierung tatsächlich gewünscht ist. Jeder andere Einsatz von == verdient im Code-Review eine kritische Nachfrage.
Zusammengefasst gilt: declare(strict_types=1) ist eine notwendige, aber keine hinreichende Maßnahme für typsicheren PHP-Code. Erst die Kombination aus strict_types an Funktionsgrenzen, konsequentem === bei internen Vergleichen und einer scharf konfigurierten statischen Analyse deckt beide Hälften des PHP-Typsystems ab und verhindert die typischen Überraschungen des Type Juggling zuverlässig.
| Situation | strict_types wirkt? | Risiko ohne Vorsicht | Empfehlung |
|---|---|---|---|
| Funktionsargument | Ja | TypeError statt Konvertierung | strict_types aktiv lassen |
| Rückgabewert | Ja | TypeError statt Konvertierung | strict_types aktiv lassen |
| == Vergleich | Nein | Type Juggling, falsche Gleichheit | === verwenden |
| switch/case | Nein | Unerwarteter case-Treffer | match-Ausdruck statt switch |
| in_array/array_search | Nein | Falsche Treffer bei gemischten Typen | Dritten Parameter true setzen |
| if-Bedingung mit Skalar | Nein | Zero-vs-Empty-Verwechslung | Explizit === null oder is_int prüfen |
Mironsoft
PHP-Modernisierung, Code-Qualität und Legacy-Refactoring
Gewachsener PHP-Code, der niemand mehr gern anfasst?
Wir modernisieren PHP-Codebasen auf aktuelle Sprachstandards, führen statische Analyse und Coding Standards ein und refactorn Legacy-Code Schritt für Schritt, ohne den laufenden Betrieb zu gefährden.
Legacy-Refactoring
Gewachsenen PHP-Code strukturiert und risikoarm modernisieren.
Code-Qualität etablieren
PHPStan, Coding Standards und CI-Checks nachhaltig im Team verankern.
Versions-Upgrade
PHP-Major-Version-Upgrades sicher planen und ohne Ausfallzeit umsetzen.
10. Zusammenfassung
Weak vs. Strict Typing
Geltungsbereich
strict_types wirkt nur auf Funktionsargumente und Rückgabewerte, nicht auf ==.
Type Juggling
== konvertiert Operanden nach festen Regeln, numerische Strings sind ein Hauptrisiko.
Zero vs Empty
0, '0', '', null und false gelten alle als falsy in if-Bedingungen.
Absicherung
PHPStan mit phpstan-strict-rules und Psalm decken lose Vergleiche statisch auf.