declare(strict_types=1): warum es in jeder neuen Datei stehen sollte
AI generated
<?php
8.4
PHP 8.4 · strict_types · Typsicherheit · TypeError
declare(strict_types=1): warum es in jeder neuen Datei stehen sollte
von stiller Typkonvertierung zu sofortigem TypeError

strict_types schaltet die automatische, stille Typkonvertierung von PHP für die aktuelle Datei ab: Ein String an einen int-Parameter, ein Float an einen int-Parameter oder ein falscher Rückgabetyp lösen sofort einen TypeError aus, statt unbemerkt in einen anderen Typ konvertiert zu werden, was Bugs Jahre früher sichtbar macht, direkt am Aufruf statt irgendwo tiefer im Code.

11 Min. Lesezeit strict_types · TypeError · Typsicherheit · Migration PHP 7.0 · 8.x

1. Coercive Typing vs. Strict Typing in PHP

PHP kennt seit jeher zwei grundlegend verschiedene Modi für den Umgang mit Typdeklarationen: coercive Typing, der Standardmodus, konvertiert übergebene Werte automatisch in den deklarierten Typ, wenn eine sinnvolle Konvertierung möglich erscheint. Ein String "42" wird an einen int-Parameter stillschweigend zu 42, ein Float 3.9 wird zu 3 abgeschnitten. strict_types schaltet genau dieses Verhalten für die aktuelle Datei ab.

Ohne strict_types verschleiert PHP damit potenzielle Fehler an der Schnittstelle zwischen Funktionen: Ein Tippfehler, der eine Zeichenkette statt einer Zahl übergibt, fällt nicht auf, solange die Zeichenkette numerisch aussieht. Erst wenn ein tatsächlich nicht-numerischer String übergeben wird, etwa aus einem defekten Formularfeld, wird der Fehler sichtbar, oft an einer völlig anderen Stelle im Code als dort, wo der eigentliche Fehler entstanden ist.

strict_types verändert dieses Verhalten grundlegend: Sobald diese Deklaration aktiv ist, akzeptiert eine Funktion nur noch exakt den deklarierten Typ, mit einer einzigen Ausnahme für die Erweiterung von int zu float. Jede andere Typabweichung wirft sofort eine TypeError-Exception, direkt am Aufrufpunkt, nicht erst irgendwo tiefer in der Verarbeitungskette.

2. declare(strict_types=1) im Detail: Syntax und Geltungsbereich

Die Deklaration declare(strict_types=1); muss als erste Anweisung der Datei stehen, direkt nach dem öffnenden <?php-Tag, vor jeder anderen Anweisung einschließlich Namespace-Deklarationen und use-Importen. Ein Versuch, sie später in der Datei zu platzieren, führt zu einem Compile-Fehler, denn PHP muss den Modus kennen, bevor irgendein anderer Code der Datei ausgewertet wird.

Wichtig zu verstehen: strict_types ist eine dateibezogene Einstellung, kein globaler Schalter für das gesamte Projekt und keine Klasseneigenschaft. Jede .php-Datei entscheidet unabhängig, ob sie diesen Modus aktiviert. Das erlaubt eine graduelle Einführung in bestehenden Projekten, bringt aber auch mit sich, dass unterschiedliche Dateien im selben Projekt unterschiedlich strikt sein können, was in Abschnitt 7 genauer betrachtet wird.


<?php

// Must be the very first statement in the file, before namespace and use
declare(strict_types=1);

namespace App\Billing;

use App\Billing\Exception\InvalidAmountException;

final class Invoice
{
    public function __construct(
        private readonly int $amountInCents,
    ) {
    }
}

3. Was sich bei Funktionsargumenten konkret ändert

Der sichtbarste Effekt von strict_types zeigt sich bei Funktionsargumenten. Ohne strict_types akzeptiert eine Funktion mit Parameter int $amount auch den String "100", der String wird automatisch zu 100 konvertiert. Mit aktiviertem strict_types löst genau dieser Aufruf eine TypeError aus, denn ein String ist kein Integer, unabhängig davon, ob er numerisch aussieht.

Diese Strenge gilt für alle Skalartypen gleichermaßen: string, int, float und bool akzeptieren im Strict-Modus jeweils nur den exakt passenden Typ. Ein bool-Parameter akzeptiert also weder 1 noch "true" noch 0, sondern ausschließlich echte Boolean-Werte. Diese Konsequenz ist beabsichtigt: strict_types soll implizite Annahmen über kompatible Typen beseitigen, nicht nur reduzieren.


<?php

declare(strict_types=1);

function applyDiscount(int $amountInCents, float $percentage): int
{
    return (int) round($amountInCents * (1 - $percentage / 100));
}

// Works: exact types match the declaration
$result = applyDiscount(1999, 10.0);

// Throws TypeError: strict_types rejects a string for an int parameter
// $result = applyDiscount('1999', 10.0);

// Works: int widens to float automatically, the one allowed exception
$result = applyDiscount(1999, 10);

4. Rückgabewerte und strict_types

strict_types beschränkt sich nicht auf Parameter, sondern gilt genauso für deklarierte Rückgabetypen. Eine Funktion mit : int als Rückgabetyp muss im Strict-Modus tatsächlich einen Integer zurückgeben. Liefert der Funktionskörper stattdessen einen numerischen String zurück, wirft PHP eine TypeError, selbst wenn die Funktion selbst über keine expliziten Parameter-Typprüfungen verfügt.

Diese Symmetrie zwischen Parametern und Rückgabewerten ist wichtig, denn sie stellt sicher, dass eine Funktion ihren eigenen deklarierten Vertrag konsequent einhält, nicht nur bei der Entgegennahme von Werten, sondern auch bei deren Herausgabe. Wer eine Funktion mit strengem Rückgabetyp schreibt, kann sich beim Aufrufer darauf verlassen, dass der zurückgegebene Wert exakt dem deklarierten Typ entspricht, ohne den Rückgabewert vorsorglich selbst nochmals zu validieren.


<?php

declare(strict_types=1);

final class PriceCalculator
{
    // Declared return type ": int" is enforced strictly on every path
    public function finalPriceInCents(int $basePrice, float $taxRate): int
    {
        $withTax = $basePrice * (1 + $taxRate);

        // Explicit cast required: without it, a float would violate
        // the declared ": int" return type under strict_types
        return (int) round($withTax);
    }
}

$calculator = new PriceCalculator();
echo $calculator->finalPriceInCents(1999, 0.19);

5. Warum int-zu-float-Erweiterung trotzdem erlaubt bleibt

Die einzige Ausnahme innerhalb von strict_types betrifft die Übergabe eines int-Werts an einen float-Parameter oder Rückgabetyp. Diese Erweiterung bleibt bewusst erlaubt, weil jeder ganzzahlige Wert verlustfrei als Fließkommazahl darstellbar ist, die Konvertierung also keine Information verliert, anders als etwa die Kürzung eines Floats zu einem Integer.

Diese Entscheidung folgt einem allgemeinen Prinzip numerischer Typsysteme: Eine Erweiterung, sogenanntes Widening, von einem präziseren zu einem weniger präzisen, aber umfassenderen Typ ist verlustfrei und daher unproblematisch. Die umgekehrte Richtung, ein Float an einen int-Parameter, ist dagegen nicht sicher, weil dabei Nachkommastellen verloren gehen würden, weshalb strict_types genau diese Richtung konsequent blockiert und stattdessen eine explizite Konvertierung mit (int) oder intval() verlangt.

Szenario Ohne strict_types (coercive) Mit strict_types=1 Konsequenz
String "42" an int-Parameter Stillschweigend zu int 42 konvertiert TypeError wird geworfen Falsche Typen fallen sofort auf
Float an int-Parameter Abgeschnitten zu int, Präzisionsverlust unbemerkt TypeError wird geworfen Kein stiller Präzisionsverlust
int an float-Parameter Funktioniert, wird erweitert Funktioniert weiterhin (erlaubt) Keine Regression bei numerischer Erweiterung
Deklarierter Rückgabewert Wird bei Bedarf konvertiert Muss exakt passen (außer int zu float) Vertragssicherheit der Funktion garantiert
Bug-Erkennung im Review Fehler zeigt sich erst zur Laufzeit an anderer Stelle Fehler direkt am Aufruf sichtbar Kürzere Debugging-Zyklen

6. strict_types und interne PHP-Funktionen

Ein häufiges Missverständnis betrifft interne PHP-Funktionen wie strlen() oder array_map(). strict_types gilt für Aufrufe interner Funktionen ebenso wie für benutzerdefinierte Funktionen, sofern die Datei, in der der Aufruf steht, strict_types aktiviert hat. Ein Aufruf strlen(42) in einer Strict-Datei löst also eine TypeError aus, weil strlen() einen string-Parameter erwartet.

Entscheidend ist dabei die Datei des Aufrufers, nicht die Datei, in der die aufgerufene Funktion definiert ist. Interne Funktionen haben keine eigene Datei im herkömmlichen Sinn, ihr Verhalten richtet sich vollständig danach, ob der aufrufende Code unter strict_types steht. Das bedeutet: Derselbe Aufruf von strlen(42) verhält sich in einer Datei mit strict_types anders als in einer Datei ohne diese Deklaration, obwohl strlen() selbst unverändert bleibt.


<?php

declare(strict_types=1);

// Internal functions are checked just as strictly as user-defined ones,
// because strict_types depends on the CALLER's file, not the callee's
function describeLength(int|string $value): string
{
    // Passing an int here would throw a TypeError: strlen() requires a string
    return sprintf('Length: %d characters', strlen((string) $value));
}

echo describeLength('hyva');

// Throws TypeError under strict_types, since strlen() expects a string:
// strlen(42);

7. Warum die Wirkung dateibezogen ist, nicht projektweit

Ein zentraler und oft überraschender Aspekt von strict_types: Die Deklaration betrifft ausschließlich Funktionsaufrufe, die aus der jeweiligen Datei heraus erfolgen, nicht die Funktionen, die dort definiert sind. Eine Funktion, die in einer Strict-Datei definiert ist, prüft ihre Argumente trotzdem streng, wenn sie aus einer Datei ohne strict_types aufgerufen wird, denn die Prüfung richtet sich nach der Definition der Funktion, nicht nach dem Aufrufer.

Umgekehrt gilt: Wenn eine Datei mit strict_types eine Funktion aus einer anderen Datei ohne diese Deklaration aufruft, gilt für diesen Aufruf trotzdem der Strict-Modus, weil der Aufrufer die maßgebliche Datei ist. Diese Kombinationsregel führt in gemischten Codebasen gelegentlich zu Verwirrung, ist aber konsistent: Für die Frage, ob ein konkreter Aufruf streng geprüft wird, zählt immer die Datei, aus der heraus der Aufruf erfolgt, niemals die Datei der aufgerufenen Funktion.

Diese Dateibezogenheit macht strict_types gut migrierbar, aber auch inkonsistent innerhalb eines Projekts, solange nicht jede Datei umgestellt ist. Ein Legacy-Modul ohne strict_types, das eine neue, strikte Bibliotheksfunktion aufruft, unterliegt bei diesem Aufruf trotzdem der Strenge der Bibliothek, was gelegentlich bestehende, bisher unbemerkte Typfehler im Legacy-Code aufdeckt.

8. Fehler früh abfangen: TypeError statt stiller Konvertierung

Der eigentliche Wert von strict_types liegt in der Verschiebung des Fehlerzeitpunkts. Ohne diese Deklaration entsteht ein Bug oft an einer Stelle, an der ein falscher Typ still konvertiert wird, zeigt sich aber erst viele Aufrufe später, wenn der stillschweigend konvertierte Wert an anderer Stelle zu einem sichtbaren Problem führt. Die eigentliche Ursache und der sichtbare Effekt liegen dann weit auseinander im Code.

Mit strict_types fällt derselbe Fehler exakt an der Stelle auf, an der der falsche Typ zuerst übergeben wird, mit einer klaren TypeError-Meldung, die den erwarteten und den tatsächlichen Typ nennt. Für die Fehlersuche bedeutet das einen erheblichen Zeitgewinn, weil der Stack Trace direkt auf die fehlerhafte Aufrufstelle zeigt, statt auf eine spätere Symptomstelle, die mit der eigentlichen Ursache nur noch lose zusammenhängt.

9. Bestehende Codebasen schrittweise zu strict_types migrieren

Eine Migration bestehender, gewachsener Codebasen zu strict_types sollte niemals in einem einzigen großen Schritt erfolgen. Da die Deklaration dateibezogen ist, lässt sich sie datei- oder modulweise einführen, beginnend mit neu geschriebenem Code und gut getesteten, isolierten Komponenten, bevor älterer, weniger gut abgesicherter Code folgt. Jede umgestellte Datei sollte von einer vollständigen Testsuite begleitet sein, weil zuvor stillschweigend funktionierende Aufrufe plötzlich Fehler werfen können.

Statische Analysewerkzeuge wie PHPStan helfen, Typinkonsistenzen bereits vor der Aktivierung von strict_types aufzudecken, indem sie Aufrufstellen mit potenziell falschen Typen markieren, ohne dass der Code tatsächlich ausgeführt werden muss. In neuen Projekten und neuen Dateien gibt es dagegen keinen Grund, auf strict_types zu verzichten: Die Deklaration gehört heute an den Anfang jeder neuen PHP-Datei, direkt nach dem öffnenden Tag, als fester Bestandteil des Datei-Boilerplates.


<?php

declare(strict_types=1);

// Migrated file: strict from the top, calling a not-yet-migrated helper
final class LegacyPriceAdapter
{
    public function normalize(mixed $legacyValue): int
    {
        // Guard clause instead of relying on implicit coercion,
        // since strict_types would reject a raw string or float here
        if (!is_int($legacyValue)) {
            throw new InvalidArgumentException(
                sprintf('Expected int from legacy source, got %s', get_debug_type($legacyValue))
            );
        }

        return $legacyValue;
    }
}

10. Zusammenfassung

strict_types schaltet die automatische, stille Typkonvertierung von PHP für die jeweilige Datei ab und verlangt stattdessen exakt passende Typen bei Funktionsargumenten und Rückgabewerten, mit der einzigen Ausnahme der verlustfreien Erweiterung von int zu float. Statt eines still konvertierten, potenziell falschen Werts wirft PHP eine TypeError-Exception, direkt am Ort der fehlerhaften Übergabe, nicht irgendwo später im Programmablauf.

Da die Deklaration dateibezogen ist, richtet sich die Strenge eines Funktionsaufrufs immer nach der Datei des Aufrufers, unabhängig davon, wo die aufgerufene Funktion definiert ist. Für neue PHP-Dateien gibt es praktisch keinen Grund, auf declare(strict_types=1); zu verzichten, für bestehende Codebasen empfiehlt sich eine schrittweise, testabgesicherte Migration statt einer einmaligen, projektweiten Umstellung.

declare(strict_types=1) - Das Wichtigste auf einen Blick

Wirkung

Deaktiviert stille Typkonvertierung. TypeError statt automatischer Umwandlung bei Parametern und Rückgabewerten.

Ausnahme

int an float-Parameter bleibt erlaubt, da diese Erweiterung verlustfrei ist. Alle anderen Skalarkonvertierungen werden abgelehnt.

Geltungsbereich

Muss als erste Anweisung der Datei stehen. Gilt für Aufrufe aus dieser Datei, nicht projektweit und nicht für definierte Funktionen selbst.

Migration

Datei für Datei mit Testabsicherung. PHPStan hilft, Typinkonsistenzen vor der Aktivierung aufzudecken.

11. FAQ: declare(strict_types=1)

1Wo muss die Deklaration in der Datei stehen?
Als allererste Anweisung, direkt nach dem öffnenden Tag, vor Namespace und use-Importen.
2Projektweit oder nur pro Datei?
Nur pro Datei. Kein globaler Schalter, jede Datei entscheidet unabhängig.
3String an int-Parameter übergeben?
Wirft eine TypeError, unabhängig davon, ob der String numerisch aussieht.
4int an float-Parameter weiterhin erlaubt?
Ja, einzige Ausnahme, da diese Erweiterung verlustfrei ist.
5Gilt es auch für strlen() und Co.?
Ja, entscheidend ist die Datei des Aufrufers, nicht die interne Funktion selbst.
6Zählt die Datei der Funktion oder des Aufrufers?
Immer die Datei des Aufrufers, unabhängig von der Datei der Funktionsdefinition.
7Betrifft es auch Rückgabewerte?
Ja, ein deklarierter Rückgabetyp muss exakt passen, mit derselben int-zu-float-Ausnahme.
8Immer in neuen Dateien verwenden?
Ja, praktisch immer, ohne echte Nachteile gegenüber coercive Typing.
9Große Codebasis sicher migrieren?
Datei für Datei mit Testsuite, PHPStan zur Vorabprüfung, niemals in einem großen Schritt.
10Macht es den Code langsamer?
Nein, der Performance-Unterschied ist vernachlässigbar.

Mironsoft

PHP-Architektur, Codequalität und Magento-Entwicklung

Bestehendes PHP-Projekt schrittweise auf strict_types umstellen?

Wir analysieren bestehenden Code auf Typinkonsistenzen, planen eine sichere, testabgesicherte Migration zu strict_types und richten PHPStan als kontinuierliche Absicherung für neue und bestehende Dateien ein.

Typanalyse

Aufdecken von Typinkonsistenzen mit PHPStan vor der Migration

Schrittweise Migration

Datei- und modulweise Einführung von strict_types mit Testabsicherung

PHPStan-Absicherung

Kontinuierliche statische Analyse auf Level 5 und höher für neuen Code