CarbonImmutable per Default: ein Drei-Zeilen-Patch, der eine Klasse Bugs erledigt
Carbon-Instanzen sind veränderlich. Das ist solange harmlos, bis du sie zwischen Methoden weiterreichst. Eine winzige Stelle in der AppServiceProvider, die das Problem grundsätzlich aus dem Weg räumt.
Ein kleiner Tipp aus dem Aufbau von Portchecks, den ich seit einer Weile in jedem neuen Projekt setze: Datums- und Zeitobjekte sind bei mir grundsätzlich unveränderlich.
Wo das Problem entsteht
Carbon ist standardmäßig mutable. Das heißt, jeder Aufruf wie $date->addMinutes(5) ändert das ursprüngliche Objekt – und nicht nur das, was du an die nächste Methode weiterreichst.
$nextRun = $check->next_run_at;
$soon = $nextRun->addMinutes(5);
// Beide zeigen jetzt auf dieselbe, um 5 Minuten verschobene Zeit.
// Nicht offensichtlich, aber genau das ist die Klasse Bugs,
// die du eine Stunde später debuggst.
Solange ein Carbon-Objekt nur lokal in einer Methode lebt, ist das egal. Sobald du es speicherst, an einen anderen Service übergibst oder in eine Eloquent-Property steckst, ist es ein Mienenfeld. Besonders fies: solche Bugs zeigen sich oft erst in der Queue, nie im Test.
Der Patch
Eine Zeile im boot() der AppServiceProvider:
use Illuminate\Support\Carbon;
public function boot(): void
{
Carbon::useFactory(\Illuminate\Support\Facades\Date::useClass(\Carbon\CarbonImmutable::class));
}
Oder noch knapper, weil Laravel das längst direkt unterstützt:
\Illuminate\Support\Facades\Date::use(\Carbon\CarbonImmutable::class);
Ab dem Moment liefert now(), jede Eloquent-Cast-Spalte ($cast = ['next_run_at' => 'datetime']) und alles, was über die Date-Facade läuft, eine CarbonImmutable-Instanz. Methoden wie addMinutes() geben jetzt ein neues Objekt zurück, statt das alte zu verändern. Das ursprüngliche Objekt bleibt, was es war.
Was sich ändert
In 99 % der Fälle: nichts. Du merkst es einfach nicht. Die meisten Carbon-Operationen werden ohnehin ergebnisorientiert genutzt und zugewiesen.
Was sich ändert, sind die Stellen, an denen du dich vorher auf Mutation verlassen hast. Dort musst du das Ergebnis explizit zuweisen:
// vorher (mutable, funktioniert):
$date->addMinutes(5);
echo $date;
// nachher (immutable):
$date = $date->addMinutes(5);
echo $date;
Die Stellen findet Pest oder PHPUnit dir innerhalb einer Test-Suite zuverlässig.
Warum sich das lohnt
Weil das einer dieser Wechsel ist, die nichts Sichtbares verbessern – aber eine ganze Kategorie an stillen Bugs ausschließen. Die Sorte, bei der du dich später fragst, warum ein next_run_at plötzlich fünf Minuten in der Vergangenheit liegt, obwohl niemand die Zeile geändert hat.
In Portchecks war das eine der ersten Code-Zeilen nach dem Setup. Seitdem ein Datums-Bug weniger, mit dem ich rechnen muss.