PHP RFC: Time\Instant and Time\Clock
- Version: 1.0
- Date: 2026-09-21
- 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
The elevator pitch for the RFC. The first paragraph of this section will be rendered slightly larger to give it emphasis.
Please write an introduction that helps people by providing concise context and include a representative small code snippet.
<?php echo "Showcase your idea here"; ?>
Proposal
Add a new always-available class Time\Instant representing a “point in time” together with an interface Time\Clock and a Time\Clock\SystemClock implementation.
<?php namespace Time { /** * An absolute point on the wall-clock timeline with nanosecond precision. * It is independent of any timezone. * * Wall-clock time is not monotonic: it may jump forwards or backwards due to * NTP adjustments or manual changes. Instant is therefore unsuitable for * measuring elapsed time. * * Instants follow POSIX time and do not account for leap seconds. * * The representable range is bounded by an 64-bit number of seconds relative * to an unspecified origin, with nanosecond precision. Any operation that * leaves this range will throw a Time\TimeException. * * The internal representation and the origin it is relative to are not part * of the public API. * * @strict-properties */ final readonly class Instant { /** * Create an instant from a Unix timestamp. $nanoseconds must be between * 0 and 999_999_999 and always advances the instant, including for * negative values of $seconds. * * On 32-bit platforms PHP’s int range limits this constructor to the * years 1901 to 2038. * * @throws \ValueError If $nanoseconds is not between 0 and 999_999_999. */ public static function fromUnixTimestamp(int $seconds, int $nanoseconds = 0): self {} /** * Create an instant from a Unix timestamp in milliseconds. * * On 32-bit platforms PHP’s int range limits this constructor to roughly * 24 days around the Unix epoch. */ public static function fromUnixTimestampMilliseconds(int $milliseconds): self {} /** * Parse an ISO-8601 date and time string. A zone offset is required. * * @throws TimeException If $specification is not accepted by the parser, * or carries no zone offset. * @throws TimeException If the denoted instant is outside the * representable range. */ public static function fromIso8601DateTimeString(string $specification): self {} /** * The instant at which the Unix epoch began, equivalent to * `fromUnixTimestamp(0)`. */ public static function unixEpoch(): self {} /** * The whole seconds elapsed since the Unix epoch, rounded towards * negative infinity (RoundingMode::NegativeInfinity). The discarded * fractional part is available via ->getNanoseconds(). * * On 32-bit platforms this is representable only for the years 1901 to * 2038. * * @throws TimeException If the value is not representable as an int on * the current platform. */ public function getUnixTimestamp(): int {} /** * The whole milliseconds elapsed since the Unix epoch, rounded towards * negative infinity (RoundingMode::NegativeInfinity). * * On 32-bit platforms this is representable only within roughly 24 days * around the Unix epoch. * * @throws TimeException If the value is not representable as an int on * the current platform. */ public function getUnixTimestampMilliseconds(): int {} /** * The sub-second fraction of this instant, between 0 and 999_999_999. */ public function getNanoseconds(): int {} /** * Add the given duration to this instant. A negative duration moves the * instant backwards. * * @return self $this + $duration * * @throws TimeException If the result is outside the representable range. */ public function add(Duration $duration): self {} /** * Subtract the given duration from this instant. A negative duration * moves the instant forwards. * * @return self $this - $duration * * @throws TimeException If the result is outside the representable range. */ public function sub(Duration $duration): self {} /** * The duration elapsed from this instant until the given instant. The * result is negative if $instant precedes this instant. * * @return Duration $instant - $this * * @throws TimeException If the difference exceeds the range of * Time\Duration. */ public function until(self $instant): Duration {} /** * Returns -1, 0, 1 if $a is earlier than, equal to, or later than $b * respectively. */ public static function compare(self $a, self $b): int {} /** * Returns an ISO-8601 date and time string with the 'Z' zone designator. * * Years outside 0000 to 9999 use the expanded representation with a * mandatory sign. */ public function toIso8601DateTimeString(): string {} } interface Clock { /** * The current instant, at the resolution offered by the underlying clock. */ public function now(): Instant; } } namespace Time\Clock { /** * Reads the system's wall clock. * * @strict-properties */ final readonly class SystemClock implements \Time\Clock { public function __construct() {} /** * The current instant, at the resolution offered by the platform clock, * which is generally coarser than nanoseconds. * * Returned values are not guaranteed to be monotonically increasing * across calls. */ public function now(): \Time\Instant {} } } ?>
Just like Time\Duration, the Time\Instant class will implement internal “comparison handlers”, which means that direct comparisons with operators such as < or <=> will work. Other operators (such as + for addition) will not be overloaded.
Debug representations (e.g. using var_dump()) of Time\Instant will show the ISO-8601 representation, since it is both human readable and the only format capable of show every possible point in time the class can represent.
Serialization of Time\Instant will be supported, the exact format is considered an implementation detail.
Design Considerations
Time\Instant
An Time\Instant carries no timezone. It represents a unique point on the timeline of the universe. This point is the same for everyone, no matter where they are located on the Earth (or not even of the Earth), i.e. no matter their timezone.
This means that arbitrary formatting of an Time\Instant is unsupported, since that requires knowledge of a timezone. Combining an instant with a timezone to allow these “time as specified on calendar and clock” operations is left for a different class that is left to future scope.
For the same reason the internal representation of Time\Instant is intentionally left unspecified and there are deliberately no public properties. In particular the “Unix timestamp” is not the primary representation. Reasons for this are that across programming languages and computer science in general the representation of time differs. There are multiple equally valid epochs (e.g. the Unix epoch on 1970-01-01, but also 1900-01-01, 2000-01-01, 0000-01-01, and others) and resolution also differs, even where the Unix timestamp is the primary representation. Instead the Time\Instant should be capable of representing arbitrary points in time, without being limited by integer precision (particularly for 32-bit PHP). Constructors for these additional epochs or representations are left to future scope. The RFC ships with ISO-8601 as the universal representation and Unix timestamps, acknowledging its wide usage in practice.
Similarly, since the “timeline of the universe” does not know about days, or months, the only meaningful mathematical operations are moving backwards and forwards on the timeline by a number of (fractional) seconds - and calculating the distance between two points, which is exactly what the Time\Duration class represents. Moving by “a month” is an operation that will be supported by the aforementioned future scope class that knows about timezones.
Following the lead of virtually all programming languages and operating systems, leap seconds are ignored. This means that ISO-8601 strings representing a leap second (23:59:60) are rejected as unrepresentable and the duration returned by ->until() for two instants separated by a second second is one second short of the actual time elapsed.
Time\Clock\SystemClock
While Time\Instant supports nanosecond precision, matching the precision of Time\Duration to allow for all mathematical operations to have well-defined results, the precision provided by the operating system’s clock is generally much lower and dependent on the operating system.
Even though the timeline of the universe can only go forward and ticks at the same speed, the same is not true for the operating system clock: The internal clock in computers is not perfectly precise and might tick too fast or too slow, requiring regular adjustments to match reality (e.g. using NTP). During those adjustments the clock may make large jumps or may even move backwards. Users must be prepared that an instant B obtained after another instant A might be smaller than instant A with the Time\Duration returned by until() being negative, making it unsuitable to measure elapsed time. Measuring elapsed time using the monotonic clock (which only ticks forward) will be left to future scope.
Time\Clock
Obtaining the current time is likely the most common case for otherwise pure code to become impure and untestable. Following best practices in the ecosystem a Time\Clock interface is provided to allow injecting purpose-built clocks for testing purposes related use cases without needing to wrap the Time\Clock\SystemClock in custom class implementing a userland interface. The native interface will also enable future use cases of exchangeable clocks in internal APIs.
The RFC intentionally does not provide any clocks besides the system clock, since possible implementations for test clocks differ wildly (e.g. always return a fixed instant, moving the instant forward for every call, increasing or decreasing resolution) with purpose-built userland implementations providing more flexibility than internal implementations could.
Examples
Remember that the RFC contents should be easily reusable in the PHP Documentation. This means, if at all possible, they should be runnable as standalone, self-contained code with the proof-of-concept implementation.
Simple example:
<?php echo "First example"; ?>
Example showing an edge case:
<?php echo "Edge case"; ?>
Backward Incompatible Changes
This RFC is only proposing three new symbols, all within the Time\ namespace that was reserved for the new date/time API as part of the Duration class RFC for PHP 8.6. Generally adding new symbols is not considered a breaking change.
Proposed PHP Version(s)
Next minor (8.7).
RFC Impact
To the Ecosystem
The proposed Time\Clock interface is a direct replacement of PSR-20’s Psr\Clock\ClockInterface, except that it returns a timezone-less instant instead of a DateTimeImmutable. The ecosystem might want to build a Clock that translates between Time\Clock and the PSR-20 Psr\Clock\ClockInterface while the process of migrating to the new Time\Instant is in progress.
To Existing Extensions
Extensions may want to adjust their API to take Time\Instant in addition to an int representing a Unix timestamp or an object implementing DateTimeInterface.
To SAPIs
None.
Open Issues
None.
Future Scope
This section should outline areas that you are not planning to work on in the scope of this RFC, but that might be iterated upon in the future by yourself or another contributor.
This helps with long-term planning and ensuring this RFC does not prevent future work.
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
Rejected Features
None.
Changelog
- 2026-09-21: Initial version.