Endlich hat jemand die Hausaufgabe gemacht, die wir alle immer aufschieben: Er hat die VLD-Extension gegen PHP 8.3.6 gebaut und die kompilierten Opcodes derselben Funktion zweimal ausgegeben, einmal mit declare(strict_types=1) am Dateianfang und einmal ohne. Die beiden Listings sind identisch. Dasselbe RECV, das das Argument entgegennimmt, dasselbe VERIFY_RETURN_TYPE auf dem Rückweg, identische Struktur vom ersten bis zum letzten Opcode. Die Striktheit schafft es nie in den Bytecode. Sie existiert als Flag an der kompilierten Datei, und die Runtime schaut auf dieses Flag innerhalb einer Typprüfung, die die Engine sowieso durchführt.
Function greet:
line #* E I O op fetch ext return operands
2 0 E > RECV !0
3 1 ROPE_INIT 3 ~2 'You+are+'
2 ROPE_ADD 1 ~2 ~2, !0
3 ROPE_END 2 ~1 ~2, '+years+old'
4 VERIFY_RETURN_TYPE ~1
5 > RETURN ~1Diese eine Messung erledigt den Performance-Einwand ganz leise. Der Vergleich zwischen deklariertem Typ und tatsächlichem Wert passiert bei jedem typisierten Aufruf, strikt oder nicht; das Flag entscheidet nur, ob ein Mismatch einen TypeError wirft oder einen Konvertierungsversuch auslöst. Werfen ist, wenn überhaupt, der billigere Zweig. Kosten waren also nie die eigentliche Frage. Die eigentliche Frage ist die Topologie, denn das Flag gehört zu der Datei, aus der der Aufruf kommt, und genau an diesem Detail verletzen sich Teams. Und hier stehe ich: Entweder vollständig oder gar nicht. Eine halb-strikte Codebasis ist schlimmer als eine komplett lose, denn die lose ist wenigstens vorhersehbar falsch.
Geh einmal durch, was pro Datei tatsächlich bedeutet. Bei Argumenten entscheidet die Datei des Aufrufers: Wenn Datei A strict_types deklariert und ein takesInt() aufruft, das in einer losen Datei B definiert ist, dann ist dieser Aufruf strikt. Bei Rückgabewerten dreht es sich um, die Prüfung läuft dort, wo das return-Statement steht, also darf eine Funktion in einer losen Datei den String "5" fröhlich in den versprochenen int umwandeln, egal wie strikt ihr Aufrufer ist. Intern ist das konsistent, die Prüfung gehorcht der Datei, in der sie ausgeführt wird. Von außen heißt es: Dieselbe Funktion akzeptiert "25", wenn sie aus admin/report.php aufgerufen wird, und wirft, wenn der Aufruf aus cron/rebuild.php kommt. Erklär das mal der Person im Bereitschaftsdienst, wenn diese beiden Codepfade um 2 Uhr nachts unterschiedlicher Meinung sind.
Und der Fehlermodus des losen Modus ist nicht laut, er ist Arithmetik. Gib einen Datums-String wie "2024-01-01" an einen Parameter, der als int deklariert ist, und PHP behält die führenden Ziffern und wirft den Rest weg, deine Funktion rechnet jetzt also mit 2024. Zwei Daten im selben Jahr kollabieren zur selben Zahl, eine Tageszählung ergibt null, ein Report bleibt leer, und nichts warnt dich. Der Artikel mit dem VLD-Experiment hat genau dieses Muster durch einen Finanz-Reporting-Service verfolgt, und die Debugging-Rechnung lag bei zwei Tagen. Mit gesetztem Flag ist derselbe Fehler ein TypeError mit einem Stacktrace, der auf den Aufrufer zeigt, behoben in unter einer Minute.
Das ehrliche Gegenargument verdient Sendezeit. Legacy-Code kann sich absichtlich auf Coercion verlassen: Formulareingaben kommen als Strings an, alte Config-Loader liefern dir "1", wo ein bool gemeint ist, und ein Big-Bang-Umstieg bringt jeden bisher tolerierten Mismatch auf einmal an die Oberfläche, in Produktion, wenn du unvorsichtig bist. Es gibt auch echte Grenzen dessen, was dir das Flag bringt. Interne Funktionen ignorieren es komplett, strlen(123) konvertiert auch in deiner striktesten Datei. Klassentypen waren schon immer strikt, dasselbe gilt für Arrays und Callables. Und als Library-Autor kannst du Konsumenten keine Striktheit aufzwingen, weil die Datei des Aufrufers gewinnt. Also nein, strict_types ist kein Schutzschild. Es regelt skalare Deklarationen an deinen eigenen Funktionen, mehr nicht.
Ich plädiere trotzdem für die flächendeckende Einführung, gerade weil der Geltungsbereich so eng und die Kosten so nah an null sind. Deklarierte Typen, die tatsächlich durchgesetzt werden, sind der Boden, auf dem PHPStan und Psalm stehen; ohne Durchsetzung sind deine Signaturen optimistische Kommentare. Die Migration ist langweilig, aber machbar. Neue Dateien bekommen die declare-Zeile ohne Diskussion. Alte Dateien bekommen sie eine nach der anderen, bei laufender Testsuite. Die TypeErrors, die dabei auftauchen, werden an der echten Grenze behoben, sprich: ein expliziter (int)-Cast dort, wo Request-Daten ins System kommen, keine Casts, die über jede Aufrufstelle verstreut sind. Wenn du dich dabei erwischst, an jedem Aufrufer zu casten, lügt die Signatur ohnehin und will vermutlich einen Union-Type sein.
Eine Sache aus dem Opcode-Dump lässt mich nicht los: Wenn Striktheit nur ein anderer Branch zur Laufzeit ist, dann war das Opt-in pro Datei eine Sprachdesign-Entscheidung, keine technische Notwendigkeit. PHP hätte es engine-weit machen können und hat sich dagegen entschieden, vermutlich damit zwanzig Jahre bestehender Code weiterläuft, was fair ist. Aber diese Entscheidung schiebt die Konsistenz-Last auf uns ab. Also erzähl mir, wie du sie trägst. Ist deine Codebasis bei 100 Prozent strict_types, und falls nicht, welche Datei wird die declare-Zeile nie bekommen, und was ist der ehrliche Grund dafür? Ich vermute, die Antworten sagen mehr über unsere Systeme als jeder Bytecode-Dump.
Kommentare
Noch keine Kommentare — schreib den ersten.
Starte die Diskussion
Kein Konto, kein Passwort nötig — gib einfach deine E-Mail-Adresse ein, wir senden dir einen einmaligen Anmelde-Link. Beim ersten Mal bist du damit automatisch angemeldet.
Deine Bewertung wird nach der Anmeldung automatisch übernommen.
Schau in dein Postfach
Wir haben einen Anmelde-Link an … gesendet. Öffne ihn auf diesem Gerät — dieser Tab meldet dich automatisch an.
Nichts angekommen? Wirf einen Blick in den Spam-Ordner — und markiere die Mail dort als „Kein Spam“, dann landet sie künftig direkt im Postfach.