PHP 8.6 Duration: Eine kleine Klasse gegen einen wiederkehrenden Designfehler

· Oskar Stark · Expertise · 4 Minuten zum Lesen
Abstract editorial concept of a Duration class, showing elapsed time and precise time intervals in modern PHP coding

PHP 8.6 soll eine dedizierte Duration-Klasse erhalten. Das wirkt klein, gibt PHP-Projekten aber endlich einen klaren Typ für verstrichene Zeit statt der Vermischung von DateInterval, Integers und Zeitrechnungen.

Der angenommene PHP-RFC für eine neue Duration-Klasse ist eine jener Änderungen, die auf den ersten Blick unscheinbar wirken. Sie führt weder ein neues Programmierparadigma ein, noch wird sie vermutlich Release-Notes so prägen wie Property Hooks oder Fibers.

Im Backend-Alltag löst sie jedoch ein Problem, das wir in Symfony- und PHP-Projekten regelmäßig sehen: Teams verwenden dieselben Konstrukte für zwei verschiedene Konzepte.

  • Kalenderintervalle: „in einem Monat“, „nächster Werktag“, „das Abo verlängert sich jährlich“.

  • Verstrichene Dauern: „nach 500 Millisekunden erneut versuchen“, „Sperre läuft nach 30 Sekunden ab“, „SLA-Budget beträgt 2 Minuten“.

PHP kennt DateInterval schon lange, aber DateInterval ist kein präziser, kontextfreier Typ für Dauern. P1M ist keine feste Anzahl von Sekunden. Einen Monat zu einem 31. Januar zu addieren ist eine Kalenderoperation, keine Rechenoperation mit verstrichener Zeit. Die neue Duration-Klasse ist wichtig, weil sie uns einen Kernsprachtyp für die zweite Kategorie gibt: messbare Zeitspannen.

Warum das in echten Projekten wichtig ist

In Symfony-Anwendungen tauchen Zeitwerte überall auf:

  • Messenger-Retry-Delays werden in Millisekunden angegeben.

  • Cache-TTLs werden meist in Sekunden angegeben.

  • HTTP-Client-Timeouts sind oft Floats in Sekunden.

  • Lock-TTLs, Rate Limits und Job-Deadlines können wieder ganz anderen Konventionen folgen.

  • Datenbankspalten mit den Namen timeout, ttl oder delay sind häufig einfache Integer ohne Einheit im Namen.

So schleichen sich Fehler in reife Systeme ein. Nicht, weil Entwickler Zeit nicht verstehen, sondern weil der Code die Unterscheidung nicht erzwingt.

Ein typisches Beispiel:

use Symfony\Component\Cache\CacheItem;
use Symfony\Component\Messenger\Envelope;
use Symfony\Component\Messenger\Stamp\DelayStamp;

$retryDelay = 15 * 60;

$cache->get('import-status-'.$importId, function (CacheItem $item) use ($retryDelay): ImportStatus {
    $item->expiresAfter($retryDelay);

    return ImportStatus::pending();
});

$bus->dispatch(new Envelope(
    new ImportMessage($importId),
    [new DelayStamp($retryDelay)]
));

Der Code ist lesbar genug für ein Review, aber er ist falsch: expiresAfter() erwartet Sekunden, DelayStamp hingegen Millisekunden. Derselbe Integer wandert durch zwei APIs mit unterschiedlichen Einheitenkonventionen.

Ein dedizierter Dauer-Typ behebt nicht magisch jede API-Grenze. Aber er verändert den Design-Druck: Man übergibt nicht mehr „irgendeinen Integer“, sondern „eine Dauer, die an der Grenze explizit konvertiert werden muss“.

DateInterval sollte nicht verschwinden — sondern bewusster eingesetzt werden

Die falsche Schlussfolgerung wäre: „Sobald PHP Duration hat, sollten wir DateInterval nicht mehr verwenden.“

DateInterval bleibt das richtige Werkzeug für kalenderabhängige Operationen:

$renewalDate = $currentPeriodStart->add(new DateInterval('P1M'));

Das bedeutet „einen Kalendermonat später“. Das Ergebnis hängt vom Datum ab. Genau das will man bei Abrechnungszeiträumen, Abo-Verlängerungen oder Auswertungsfenstern.

Für einen Timeout ist DateInterval('P1M') jedoch meist ein Warnsignal. Ein Timeout sollte normalerweise messbar sein, ohne den Startzeitpunkt, die Zeitzone oder den Kalendermonat zu kennen. Genau in diesen konzeptionellen Raum gehört PHP 8.6s Duration.

Eine hilfreiche Regel für Code Reviews:

Wenn der Wert beantwortet „wie lange hat das gedauert?“ oder „wie lange sollen wir warten?“, modellieren Sie ihn als Dauer. Wenn er beantwortet „welches Kalenderdatum kommt als Nächstes?“, modellieren Sie ihn als Kalenderintervall.

Bereiten Sie Ihre Codebasis schon vor PHP 8.6 vor

Sie müssen nicht auf PHP 8.6 warten, um Ihr Design zu verbessern. Der sicherste Migrationspfad ist sogar, schon jetzt explizite Dauer-Grenzen einzuführen und ihre Implementierung später an die Core-Klasse anzupassen.

Ein kleines Value Object auf Projektebene beseitigt bereits den Großteil der Mehrdeutigkeit:

namespace App\Time;

use InvalidArgumentException;

final readonly class TimeSpan
{
    private function __construct(private int $milliseconds)
    {
        if ($milliseconds < 0) {
            throw new InvalidArgumentException('A time span cannot be negative.');
        }
    }

    public static function milliseconds(int $milliseconds): self
    {
        return new self($milliseconds);
    }

    public static function seconds(int $seconds): self
    {
        return new self($seconds * 1_000);
    }

    public static function minutes(int $minutes): self
    {
        return self::seconds($minutes * 60);
    }

    public function toMilliseconds(): int
    {
        return $this->milliseconds;
    }

    public function toWholeSeconds(): int
    {
        if ($this->milliseconds % 1_000 !== 0) {
            throw new InvalidArgumentException('The time span cannot be represented as whole seconds.');
        }

        return intdiv($this->milliseconds, 1_000);
    }
}

Machen Sie dann die Einheitenkonvertierung an Framework-Grenzen sichtbar:

use App\Time\TimeSpan;
use Symfony\Component\Cache\CacheItem;
use Symfony\Component\Messenger\Envelope;
use Symfony\Component\Messenger\Stamp\DelayStamp;

$retryDelay = TimeSpan::minutes(15);

$cache->get('import-status-'.$importId, function (CacheItem $item) use ($retryDelay): ImportStatus {
    $item->expiresAfter($retryDelay->toWholeSeconds());

    return ImportStatus::pending();
});

$bus->dispatch(new Envelope(
    new ImportMessage($importId),
    [new DelayStamp($retryDelay->toMilliseconds())]
));

Das ist absichtlich nicht clever. Der Vorteil liegt darin, dass die Konvertierung jetzt explizit und überprüfbar ist. Wenn jemand das Delay auf TimeSpan::milliseconds(500) ändert, schlägt die Cache-Grenze sofort fehl, statt stillschweigend auf null Sekunden zu runden.

Sobald Ihre Mindestversion PHP 8.6 ist, kann dieses Value Object entweder die native Duration-Klasse wrappen oder ganz entfallen, sofern Framework-APIs den nativen Typ direkt unterstützen. Entscheidend ist, dass Ihre Domain- und Application-Services nicht mehr von unbeschrifteten Integers abhängen.

Speichern Sie Dauern im Modell mit der Einheit, nicht nur im Datenbankschema

Ein weiteres häufiges Anti-Pattern ist eine Datenbankspalte namens duration oder ttl mit INT-Typ und einer Einheit, die nur in einem Migrationskommentar dokumentiert ist. Sechs Monate später liest jemand die Entity-Property und muss raten.

Benennen Sie die Einheit lieber dort, wo der Wert in die Persistenz gelangt:

namespace App\Entity;

use Doctrine\ORM\Mapping as ORM;

#[ORM\Entity]
final class ImportJob
{
    #[ORM\Column(type: 'integer')]
    private int $processingBudgetMilliseconds;

    public function setProcessingBudget(TimeSpan $budget): void
    {
        $this->processingBudgetMilliseconds = $budget->toMilliseconds();
    }

    public function processingBudget(): TimeSpan
    {
        return TimeSpan::milliseconds($this->processingBudgetMilliseconds);
    }
}

Das hält das Doctrine-Mapping einfach und portabel, verhindert aber, dass der Rest der Anwendung den Wert als beliebigen Integer behandelt. Außerdem erhalten Sie so einen klaren Migrationspunkt, falls Sie später Mikrosekunden, Nanosekunden oder einen nativen Datenbank-Intervalltyp speichern möchten.

Verwenden Sie zur Messung von Dauern keine Wall-Clock-Zeit

Die neue Duration-Klasse macht auch eine andere Unterscheidung sichtbarer: Verstrichene Zeit zu messen ist nicht dasselbe wie das aktuelle Datum auszulesen.

Für Performance-Messungen sollten Sie eine monotone Zeitquelle wie hrtime() verwenden, statt zwei DateTimeImmutable-Instanzen aus der Wall-Clock-Zeit voneinander zu subtrahieren. Wall-Clock-Zeit kann sich durch NTP-Anpassungen, manuelle Uhränderungen oder zeitzonenbezogene Annahmen ändern. Ein monotimer Zähler ist für verstrichene Messungen gemacht.

$startedAt = hrtime(true);

$processor->process($message);

$elapsedNanoseconds = hrtime(true) - $startedAt;
$elapsed = TimeSpan::milliseconds(intdiv($elapsedNanoseconds, 1_000_000));

In Symfony-Anwendungen lohnt sich auch die Clock-Komponente für Services, die testbaren Zeitzugriff benötigen. Sie hilft dabei, fest codierte Aufrufe von new DateTimeImmutable() in der Business-Logik zu vermeiden und zeitabhängige Tests deterministisch zu machen.

Was sich im Stil von Symfony- und PHP-Code voraussichtlich ändern wird

Die nützlichste Folge der Duration-Klasse in PHP 8.6 wird nicht kürzerer Code sein, sondern bessere Schnittstellen.

Heute sehen viele Application Services so aus:

final readonly class ImportScheduler
{
    public function schedule(string $importId, int $delay): void
    {
        // Ist $delay in Sekunden, Millisekunden oder Minuten?
    }
}

Eine bessere Schnittstelle sagt, was sie meint:

use App\Time\TimeSpan;

final readonly class ImportScheduler
{
    public function schedule(string $importId, TimeSpan $delay): void
    {
        // Die Einheit ist nicht mehr Teil des Rätselratens beim Aufrufer.
    }
}

Mit PHP 8.6 erhalten Bibliotheks- und Framework-Autoren durch die native Klasse ein gemeinsames Ziel. Genau darin liegt die strategische Bedeutung dieses RFCs: Sobald das Ökosystem gegen ein gemeinsames Dauer-Konzept typisieren kann, müssen weniger APIs ihre eigenen Integer-Konventionen erfinden.

Unsere Empfehlung für bestehende Projekte ist daher pragmatisch:

  1. Verwenden Sie DateInterval weiterhin für Kalenderlogik.

  2. Übergeben Sie innerhalb Ihrer Anwendungsschicht keine rohen Integers mehr für Timeouts, TTLs und Delays.

  3. Konvertieren Sie erst an der Grenze in framework-spezifische Einheiten.

  4. Verwenden Sie monotone Zeit für Messungen.

  5. Planen Sie eine kleine Kompatibilitätsschicht, damit PHP 8.6s Duration ohne Neuschreiben der Business-Logik eingeführt werden kann.

Das ist keine kosmetische Refaktorierung. Sie beseitigt eine Klasse von Produktionsfehlern, die sich sonst meist erst unter Last, bei Retries, in Queues oder bei zeitkritischen Integrationen zeigt.

Quellen

Planen Sie Ihre PHP-8.6-Migration

SensioLabs Deutschland unterstützt Teams mit Architektur-Reviews, Migrationsplanung und praxisnaher Begleitung bei der Modernisierung von Symfony- und PHP-Codebasen.

Das könnte dich auch interessieren

Abstract editorial illustration of PHP 8.6 JSON parsing with a highlighted error position and subtle purple accents
Oskar Stark

PHP 8.6: JSON-Fehlerposition beim Decoding

PHP 8.6 bringt eine kleine, aber nützliche Verbesserung beim JSON-Decoding: Bei einem Parsing-Fehler lässt sich jetzt die exakte Fehlerposition ermitteln. Das beschleunigt das Debugging fehlerhafter Payloads deutlich.

Mehr erfahren : PHP 8.6: JSON-Fehlerposition beim Decoding
Modern abstract editorial scene with cleaner, safer sorting API concepts and subtle purple accents
Oskar Stark

PHP 8.6 SortDirection: sauberere, sicherere Sorting-APIs für modernes PHP

PHP 8.6 führt SortDirection ein – ein kleiner, aber nützlicher Schritt hin zu klareren Sorting-APIs. Hier erfahren Sie, was sich ändert, warum das wichtig ist und wie Sie es in der Praxis einsetzen.

Mehr erfahren : PHP 8.6 SortDirection: sauberere, sicherere Sorting-APIs für modernes PHP
Large tree under the sunlight
Mathieu Santostefano

Vervielfache deine KI-Entwicklungsgeschwindigkeit mit Git Worktrees

Verabschiede dich vom lästigen „Stash-and-Switch“-Dilemma und vom Kontextwechsel. Durch die Kombination von Git Worktrees mit modernen KI-Agenten kannst du jetzt Umgebungen isolieren und Bugfixes parallel beheben, während deine KI im Hintergrund neue Funktionen entwickelt. Das ist ein absoluter Game-Changer für die Developer Experience (DX) und verwandelt deinen Git-Workflow in ein Multithreading-Kraftpaket.

Mehr erfahren : Vervielfache deine KI-Entwicklungsgeschwindigkeit mit Git Worktrees
A man sculpting a rock with PDF written on it
Steven Renaux

Create a Custom Builder – Eine GotenbergBundle Story

In einem vorherigen Artikel haben wir uns angeschaut, wie du mit nur wenigen Zeilen Code dein erstes PDF generierst. Dabei kamen Gotenberg und das GotenbergBundle zum Einsatz – ein Symfony Bundle, das die HTTP API von Gotenberg kapselt, um HTML- oder Office-Dateien in PDFs umzuwandeln oder Screenshots zu erstellen. Das war ein super Anfang. Aber was passiert, wenn deine Applikation viele verschiedene PDFs erzeugen muss, jedes mit eigenem Layout, Style und Daten?

Mehr erfahren : Create a Custom Builder – Eine GotenbergBundle Story
Nicolas Grekas standing on stage at SymfonyLive Paris 2026
Jules Daunay

SymfonyLive Paris 2026: KI-Revolution und Gipfeltreffen für das SensioLabs-Team

Der Vorhang für die SymfonyLive Paris 2026 ist gefallen, und wir haben immer noch Sterne ✨ (und Codezeilen) in den Augen. Als Entwickler von Symfony und langjähriger Hauptsponsor hätte sich SensioLabs keinen besseren Moment wünschen können, um Open Source, Innovation und vor allem die unglaubliche Community um uns herum zu feiern.

Mehr erfahren : SymfonyLive Paris 2026: KI-Revolution und Gipfeltreffen für das SensioLabs-Team
Paper notes on a wall
Imen Ezzine

Ein Blick hinter die Kulissen: 3 kollaborative Zeremonien

Nach einem Post auf LinkedIn dachte ich mir, ich schreibe diesen Artikel, um 3 Zeremonien zu beschreiben, die mich während eines meiner letzten Projekte besonders beeindruckt haben: Event Storming, Example Mapping und Domain Storytelling.

Mehr erfahren : Ein Blick hinter die Kulissen: 3 kollaborative Zeremonien
Illustration of Developer
Silas Joisten

Die Developer-Experience-Revolution 2026

Entdecke, warum Developer Experience heute wichtiger ist denn je und wie bessere Tools, smartere Workflows und eine starke Lernkultur die Art und Weise verändern, wie Teams Software entwickeln.

Mehr erfahren : Die Developer-Experience-Revolution 2026
Nicolas Grekas with a mic in his right hand raising his left hand on stage at SymfonyCon Amsterdam 2025
Jules Daunay

Symfony 8: Stabilität, Sicherheit und Innovation für Entwickler

Zum Launch von Symfony 8 haben wir Nicolas Grekas getroffen, eine Ikone der Open-Source-Welt und Haupt-Contributor des Frameworks. Zwischen neuen JSON-Komponenten, Security-Hardening und der nativen Integration von PHP 8.4 erklärt uns Nicolas, warum die Version 8 nahtlos an die Vorgängerversionen anknüpft, ohne Unternehmen vor den Kopf zu stoßen. Hier erfährst du alles Wichtige über die Neuerungen, damit du dein Upgrade ganz entspannt angehen kannst.

Mehr erfahren : Symfony 8: Stabilität, Sicherheit und Innovation für Entwickler