====== 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:
If this RFC is accepted for PHP 8.6, the polling API introduced in [[poll_api|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.
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.
= 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:
* https://github.com/marc-mabe/php-timelib/blob/5092160dfae9c3bf9484a6c86c6809688f7341bd/polyfill/Duration.php#L21
* https://github.com/jiripudil/php-ext-time-prototype/blob/06b97b9e0c8f4878d68aee3a7571fc079f3ebc17/polyfill/src/Duration.php#L16
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\Duration object.
* 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:
* Yes
* No
* Abstain
===== 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.