Inhalt
- Warum das in echten Projekten wichtig ist
- DateInterval sollte nicht verschwinden — sondern bewusster eingesetzt werden
- Bereiten Sie Ihre Codebasis schon vor PHP 8.6 vor
- Speichern Sie Dauern im Modell mit der Einheit, nicht nur im Datenbankschema
- Verwenden Sie zur Messung von Dauern keine Wall-Clock-Zeit
- Was sich im Stil von Symfony- und PHP-Code voraussichtlich ändern wird
- Quellen
PHP 8.6 Duration: Eine kleine Klasse gegen einen wiederkehrenden Designfehler
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,ttloderdelaysind 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:
Verwenden Sie
DateIntervalweiterhin für Kalenderlogik.Übergeben Sie innerhalb Ihrer Anwendungsschicht keine rohen Integers mehr für Timeouts, TTLs und Delays.
Konvertieren Sie erst an der Grenze in framework-spezifische Einheiten.
Verwenden Sie monotone Zeit für Messungen.
Planen Sie eine kleine Kompatibilitätsschicht, damit PHP 8.6s
Durationohne 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.