PHP RFC: Duration class
- Version: 1.0
- Date: 2026-06-16
- Author: Tim Düsterhus, tim@tideways-gmbh.com, Derick Rethans, derick@php.net
- Status: Draft
- Implementation: https://github.com/...
- Discussion thread: https://news-web.php.net/php.internals/…
- Voting thread: tbd
Introduction
Proposal
Add a new class Time\Duration with the following stub:
<?php namespace Time; /** * @strict-properties */ final class Duration { public readonly int $seconds; public readonly int $nanoseconds; public readonly bool $negative; /** * Create a duration representing $seconds seconds and $nanoseconds nanoseconds. Neither parameter * may be negative. $nanoseconds must be less than 1_000_000_000 (the number of nanoseconds in a * second). * * This contructor creates a Duration from its “atomic” components. */ public static function fromSeconds(int $seconds, int $nanoseconds = 0): self { } /** * Create a duration representing $nanoseconds nano-seconds. $nanoseconds must not be negative. */ public static function fromNanoseconds(int $nanoseconds): self { } /** * Create a duration representing $microseconds micro-seconds. $microseconds must not be negative. */ public static function fromMicroseconds(int $microseconds): self { } /** * Create a duration representing $milliseconds milli-seconds. $milliseconds must not be negative. */ public static function fromMilliseconds(int $milliseconds): self { } /** * Create a duration representing $minutes minutes. $minutes must not be negative. */ public static function fromMinutes(int $minutes): self { } /** * Create a duration representing $hours hours. $hours must not be negative. */ public static function fromHours(int $hours): self { } /** * Parse a ISO-8601 period. ISO-8601 periods with a date component will be rejected. * The biggest allowed component is H. */ public static function fromIso8601String(string $specification): self { } /** * Negates the duration. * * @return self -$this */ public function negate(): self { } /** * Add the given duration to the duration. * * @return self $this + $duration */ public function add(self $duration): self { } /** * Subtract the given duration from the duration. * * @return self $this - $duration */ public function sub(self $duration): self { } /** * Multiply the length of the duration by the given factor: * * @return self $this * $factor */ public function multiplyBy(int $factor): self { } /** * Divide the length of the duration by the given divisor. * * Fractional nanoseconds will be truncated. * * @return self $this / $factor */ public function divideBy(int $divisor): self { } /** * Returns -1, 0, 1 if $a is less than, equal to, or greater than $b respectively. */ public static function compare(self $a, self $b): int { } }
If this RFC is accepted for PHP 8.6, the polling API introduced in PHP RFC: Polling API for PHP 8.6 will be adjusted to make use of the Time\Duration class instead of separate $timeout and $timeoutMicroseconds parameters:
namespace Io\Poll; final class Context { /** * […] * * @param ?Time\Duration $timeout Timeout. * null means wait indefinitely. * Time\Duration::fromNanoseconds(0) means return immediately (non-blocking poll). * Must be !$timeout->negative * * […] */ public function wait( ?Time\Duration $timeout = null, ?int $maxEvents = null ): array {} }
Examples
Note that these examples show possible hypothetical use-cases. The RFC does not propose a change to existing functions, except for the Io\Poll\Context as described previously.
<?php use Time\Duration; $oneSecond = Duration::fromSeconds(1); $halfSecond = $oneSecond->divideBy(2); $onePointFiveSeconds = $oneSecond->add($halfSecond); // Sleep for 1.5 seconds sleep($onePointFiveSeconds); $negativeHour = Duration::fromHours(1)->negate(); $durations = [ $oneSecond, $negativeHour, $onePointFiveSeconds, $halfSecond, ]; usort($durations, Duration::compare(...)); // Will sort to [-1h, 0.5s, 1s, 1.5s] ?>
A proof of concept user-land implementation is as follows. This implementation is non-authoritative and may contain bugs.
- proof_of_concept.php
<?php namespace Time; /** * @strict-properties */ final class Duration { private function __construct( public readonly int $seconds, public readonly int $nanoseconds, public readonly bool $negative, ) { \assert($seconds >= 0); \assert($nanoseconds >= 0); \assert($nanoseconds < 1_000_000_000); } /** * Create a duration representing $seconds seconds and $nanoseconds nanoseconds. Neither parameter * may be negative. $nanoseconds must be less than 1_000_000_000 (the number of nanoseconds in a * second). * * This contructor creates a Duration from its “atomic” components. */ public static function fromSeconds(int $seconds, int $nanoseconds = 0): self { if ($seconds < 0) { throw new \ValueError('$seconds must not be negative'); } if ($nanoseconds < 0 || $nanoseconds >= 1_000_000_000) { throw new \ValueError('$nanoseconds must be between 0 and 999_999_999'); } return new self($seconds, $nanoseconds, false); } /** * Create a duration representing $nanoseconds nano-seconds. $nanoseconds must not be negative. */ public static function fromNanoseconds(int $nanoseconds): self { $seconds = \intdiv($nanoseconds, 1_000_000_000); $nanoseconds = $nanoseconds % 1_000_000_000; return self::fromSeconds($seconds, $nanoseconds); } /** * Create a duration representing $microseconds micro-seconds. $microseconds must not be negative. */ public static function fromMicroseconds(int $microseconds): self { return self::fromNanoseconds($microseconds * 1_000); } /** * Create a duration representing $milliseconds milli-seconds. $milliseconds must not be negative. */ public static function fromMilliseconds(int $milliseconds): self { return self::fromMicroseconds($milliseconds * 1_000); } /** * Create a duration representing $minutes minutes. $minutes must not be negative. */ public static function fromMinutes(int $minutes): self { return self::fromSeconds($minutes * 60); } /** * Create a duration representing $hours hours. $hours must not be negative. */ public static function fromHours(int $hours): self { return self::fromMinutes($hours * 60); } /** * Parse a ISO-8601 period. ISO-8601 periods with a date component will be rejected. * The biggest allowed component is H. */ public static function fromIso8601String(string $specification): self { /* Omitted */ } /** * Negates the duration. * * @return self -$this */ public function negate(): self { return clone($this, ['negative' => !$this->negative]); } /** * Add the given duration to the duration. * * @return self $this + $duration */ public function add(self $duration): self { /* (+x) + (-y) == (+x) - (+y) */ if (!$this->negative && $duration->negative) { return $this->sub($duration->negate()); } /* (-x) + (+y) == (+y) - (+x) */ if ($this->negative && !$duration->negative) { return $duration->sub($this->negate()); } /* (-x) + (-y) = -((+x) + (+y)) */ if ($this->negative && $duration->negative) { return $this->negate()->add($duration->negate())->negate(); } \assert(!$this->negative); \assert(!$duration->negative); $seconds = $this->seconds + $duration->seconds; $nanoseconds = $this->nanoseconds + $duration->nanoseconds; $seconds += \intdiv($nanoseconds, 1_000_000_000); $nanoseconds = $nanoseconds % 1_000_000_000; return self::fromSeconds($seconds, $nanoseconds); } /** * Subtract the given duration from the duration. * * @return self $this - $duration */ public function sub(self $duration): self { /* (+x) - (-y) == (+x) + (+y) */ if (!$this->negative && $duration->negative) { return $this->add($duration->negate()); } /* (-x) - (+y) == -((+x) + (+y)) */ if ($this->negative && !$duration->negative) { return $this->negate()->add($duration)->negate(); } /* (-x) - (-y) == (-x) + (+y) */ if ($this->negative && $duration->negative) { return $this->add($duration->negate()); } \assert(!$this->negative); \assert(!$duration->negative); if (self::compare($this, $duration) >= 0) { $seconds = $this->seconds - $duration->seconds; $nanoseconds = $this->nanoseconds - $duration->nanoseconds; if ($nanoseconds < 0) { $nanoseconds += 1_000_000_000; $seconds--; } return self::fromSeconds($seconds, $nanoseconds); } else { /* (+x) - (+y) = -((+y) - (+x)) */ return $duration->sub($this)->negate(); } } /** * Multiply the length of the duration by the given factor: * * @return self $this * $factor */ public function multiplyBy(int $factor): self { $seconds = $this->seconds * $factor; $nanoseconds = $this->nanoseconds * $factor; $seconds += \intdiv($nanoseconds, 1_000_000_000); $nanoseconds = $nanoseconds % 1_000_000_000; return clone($this, ['seconds' => $seconds, 'nanoseconds' => $nanoseconds]); } /** * Divide the length of the duration by the given divisor. * * Fractional nanoseconds will be truncated. * * @return self $this / $factor */ public function divideBy(int $divisor): self { $seconds = \intdiv($this->seconds, $divisor); $nanoseconds = $this->nanoseconds + (($this->seconds % $divisor) * 1_000_000_000); $nanoseconds = \intdiv($nanoseconds, $divisor); return clone($this, ['seconds' => $seconds, 'nanoseconds' => $nanoseconds]); } /** * Returns -1, 0, 1 if $a is less than, equal to, or greater than $b respectively. */ public static function compare(self $a, self $b): int { if ($a->negative && !$b->negative) { return -1; } if (!$a->negative && $b->negative) { return 1; } if ($a->negative && $b->negative) { return - ($a->seconds <=> $b->seconds) ?: - ($a->nanoseconds <=> $b->nanoseconds); } if (!$a->negative && !$b->negative) { return ($a->seconds <=> $b->seconds) ?: ($a->nanoseconds <=> $b->nanoseconds); } throw new \Error('unreachable'); } }
Backward Incompatible Changes
This RFC is introducing a single class and starts using the Time for PHP’s standard library. Adding new symbols is not considered a breaking change as per our policy. The naming of the class and namespace is in line with PHP’s naming policy.
A GitHub search for language:php “namespace Time;” symbol:Duration reveals a total of 7 hits. There are two notable hits with:
which both try to solve the same problem in a very similar way and confirming there is an interest in having this API.
Proposed PHP Version(s)
Next minor (8.6).
RFC Impact
To the Ecosystem
None.
To Existing Extensions
Existing extensions may want to start using this new class where-ever a duration is required.
To SAPIs
None.
Open Issues
None.
Future Scope
The introduction of this class and the associated Time namespace is intended to lay the groundwork for a new date and time library in upcoming PHP versions. It is intentionally designed with a minimal, but useful, API that future additions will be able to build on.
Some ideas that have been taken into account when designing the Time\Duration class:
- Calculating the difference between two points in time (“Instant”) will return a
Time\Durationobject. - Units larger than “hour” are only meaningful when a full date and time (including timezone) is available, because the length of a day might change on daylight saving time changes and the length of a month might change depending on the current month. These “calendar-based” calculations will use a dedicated class that only works with a full DateTime carrying both a time and date and a timezone. It will be possible to construct such a “calendar interval” from a
Time\Duration, but not the other way around.
Voting Choices
Primary Vote requiring a 2/3 majority to accept the RFC:
Patches and Tests
Links to proof of concept PR.
If there is no patch, make it clear who will create a patch, or whether a volunteer to help with implementation is needed.
Implementation
After the RFC is implemented, this section should contain:
- the version(s) it was merged into
- a link to the git commit(s)
- a link to the PHP manual entry for the feature
References
- PHP Date/Time API Brainstorm document: https://docs.google.com/document/d/1pxPSRbfATKE4TFWw72K3p7ir-02YQbTf3S3SIxOKWsk/edit?usp=sharing
Rejected Features
- Adding a “unit” enum with a single constructor instead of multiple named constructors.
- Adding constructors with multiple (named) parameters, such as “hoursAndMinutes”. The
fromSeconds()constructor is an intentional exception, because it works with the two “base units”.
Changelog
- 2026-06-16: Initial version.