Es beginnt mit einem Sentry-Alert an einem Dienstagabend. Eine Query ist fehlgeschlagen, die Exception-Message steht direkt da, und mittendrin, in voller Länge, die E-Mail-Adresse einer Kundin und eine halbe Telefonnummer. Niemand hat das absichtlich geloggt. QueryException hat die gebundenen Werte schon immer in die Message interpoliert, und diese Message wird in deine Logfiles kopiert, in die failed_jobs-Tabelle, in jedes APM, an das du deine Exceptions weiterreichst. Laravel 13.27 spendiert dafür endlich einen Schalter: mask_bindings_in_exception_messages, pro Connection gesetzt, lässt die ?-Platzhalter im Message-Text stehen, während getBindings() weiterhin die echten Werte liefert, wenn du gezielt danach fragst.
<?php
// config/database.php (excerpt)
return [
'connections' => [
'mysql' => [
'driver' => 'mysql',
'mask_bindings_in_exception_messages' => env('DB_MASK_BINDINGS', false),
],
],
];Dieser Schalter ist mein Favorit aus einem ungewöhnlich dichten Abschnitt: elf Minor-Releases zwischen 13.17 und 13.27, ausgeliefert in unter drei Monaten. Der Headliner ist natürlich die neue First-Party-Image-Komponente, und dazu komme ich noch. Aber meine These lautet: Die Features, die deinen Upgrade-Nachmittag wert sind, sind die defensiven, also die, deren Wert erst unter parallelem Traffic oder mitten in einem Incident sichtbar wird, und die meisten davon kommen abgeschaltet daher oder verstecken sich in einer Changelog-Zeile, die niemand retweetet. Wenn du diese Releases nur nach glänzenden Dingen abscannst, läufst du genau an den Neuerungen vorbei, die dir ein Postmortem erspart hätten.
Der Maskierungs-Schalter hat Gesellschaft. 13.23 hat den Vergleich des Maintenance-Mode-Bypass-Secrets still und leise auf durchgängiges hash_equals() umgestellt, wo er vorher auf manchen Codepfaden timing-sicher war und auf anderen ein schlichtes ===. Kleinkram, klar. Aber Log-Hygiene und timing-sichere Vergleiche sind genau die Sorte Arbeit, die es nie in eine Keynote schafft und trotzdem darüber entscheidet, ob dein nächstes Security-Audit langweilig verläuft. Ich würde DB_MASK_BINDINGS in jedem neuen Projekt auf true setzen und in den alten mal die failed_jobs-Tabelle nach @-Zeichen greppen. Was dabei auftaucht, wird dir womöglich nicht gefallen.
Dann ist da die Familie der Race Conditions. 13.27 bringt refreshForUpdate() auf Eloquent-Models. Stell dir einen Coupon mit drei verbleibenden Einlösungen vor, direkt nachdem die Marketing-Mail rausging: Zwei Requests treffen den Endpoint innerhalb derselben hundert Millisekunden, beide lesen den Zähler, beide sehen noch Luft, beide schreiben, und schon hast du mehr Rabatte verteilt, als der Coupon hergab. Pessimistisches Locking behebt das, und Eloquent unterstützt es seit Jahren, aber nur zur Query-Zeit. Sobald ein Model bereits in deiner Hand lag, war der Workaround umständlich: innerhalb der Transaction eine zweite, gelockte Query abfeuern und dann mit dieser frischen Kopie weiterarbeiten, während die ursprüngliche Variable als veralteter Köder herumlag und nur darauf wartete, dir ein Bein zu stellen. refreshForUpdate() dampft diesen Tanz auf einen Aufruf ein, der das Model in deiner Hand mit angewandtem lockForUpdate() neu einliest, innerhalb der Transaction, in der ein Lock überhaupt etwas bedeutet. Solche Races reproduzieren sich auf einem Laptop mit einem einzelnen User praktisch nie, und genau deshalb wiegt so ein Einzeiler-Primitiv schwerer, als es aussieht. Gleich nebenan liefert whereBinary() MySQL- und MariaDB-Nutzern case-sensitive Vergleiche, ohne dass sie auf rohes SQL ausweichen müssen.
Auch die Deploy-Story ist besser geworden. queue:pause --all und queue:resume --all halten mit einem einzigen Artisan-Aufruf sämtliche Queues an, statt dass du Queue-Namen hinterherjagst, die sich verschoben haben, seit sie zuletzt jemand aufgeschrieben hat. schedule:work behandelt SIGINT, SIGTERM und SIGQUIT jetzt sauber, was genau in dem Moment zählt, in dem Kubernetes deinen Pod umplant, während ein Task halb durch ist. Das WorkerStopping-Event trägt nun jobsProcessed und lastJobProcessedAt, der Durchsatz pro Worker wird also sichtbar, ohne dass du Zähler an den Job-Loop schrauben musst. Zwei Infrastruktur-Stücke passen ins selbe defensive Muster: 13.17 bringt der pgsql-Config den Unterschied zwischen einem gepoolten Endpoint (PgBouncer, RDS Proxy, Neon) und einem direkten bei und schickt Migrations und schema:dump von selbst über die direkte Verbindung. Und der Read-Through-Filesystem-Driver aus 13.26 liefert von einer primären Disk aus, fällt bei einem Miss auf die alte Disk zurück und kopiert Dateien herüber, sobald sie angefragt werden, was eine Storage-Provider-Migration von einem rsync-Marathon übers Wochenende in einen Hintergrundprozess verwandelt, an den du bald kaum noch denkst.
Jetzt das ehrliche Zugeständnis: Die glänzende Seite dieser Releases ist gut. Illuminate\Image, eingeführt in 13.20, wrappt Intervention Image v4 (composer require intervention/image machst du weiterhin selbst, darunter GD oder Imagick) und ist schnell gereift. HEIC- und HEIF-Uploads von iPhones werden seit 13.24 akzeptiert, statt abzuprallen, dominantColor() reicht dir eine Platzhalter-Hintergrundfarbe für lazy geladene Fotos, und weil Image das Responsable-Interface implementiert, kannst du ein transformiertes Bild direkt aus einer Route zurückgeben. Instanzen sind immutable, ein Quellfoto kann also hier einen kleinen Avatar und dort einen geblurrten Hero liefern, ohne dass die Transformationen einander verunreinigen. Das ist das Feature, das Teams noch diese Woche adoptieren werden, und Adoption hält ein Framework am Leben. Ich tue nicht so, als wäre das anders.
Und ich räume auch das Gegenargument gegen meinen eigenen Favoriten ein. Wer um 3 Uhr nachts Bereitschaft hat, will die gebundenen Werte in der Exception gedruckt sehen, weil der problematische Wert oft der kürzeste Weg zum Bug ist, und die Maskierung schiebt einen Schritt zwischen dich und die Antwort. Das stimmt, und vermutlich ist der Schalter genau deshalb per Default aus. Ich lande trotzdem beim Aktivieren: In Code, den du kontrollierst, gibt dir getBindings() die Werte ohnehin, während der Message-Text weiterzieht zu Drittanbietern, Retention-Policies und Backup-Archiven, die du überhaupt nicht kontrollierst. Debugging-Reibung lässt sich verschmerzen. Die Telefonnummer einer Kundin im Log-Storage von jemand anderem nicht.
In diesem Abschnitt steckt noch vieles, worüber ich gar nicht gestritten habe: Validator::fakeDnsLookups(), damit email:dns-Tests wacklige Netze überleben, das #[RouteKey('slug')]-Attribut, eine base64-Validierungsregel, #[DebounceFor] auf gequeueten Event-Listenern, HTTP-QUERY-Support via Http::query(), sogar ein monatlicher Log-Driver. Scroll selbst durch den Changelog. Meine Frage an dich ist enger gefasst: Wenn eine Query in Production hochgeht, willst du die gebundenen Werte direkt in der Message sitzen haben oder lieber hinter einem bewussten getBindings()-Aufruf? Ich habe meine Wahl getroffen, und ich vermute, die On-Call-Fraktion hat Geschichten, die sie auf die Probe stellen würden. Erzähl mir, wo dich dieser Trade schon gebissen hat.




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.