Re: [RFC] Time\Instant and Time\Clock

From: Date: Tue, 06 Oct 2026 09:17:49 +0000
Subject: Re: [RFC] Time\Instant and Time\Clock
References: 1  Groups: php.internals 
Request: Send a blank email to internals+get-132805@lists.php.net to get a copy of this message
> On 22.9.2026 16:20 CEST Tim Düsterhus <tim@bastelstu.be> wrote: > > > Hi > > following Time\Duration in PHP 8.6, Derick and I created an RFC for > Time\Instant and Time\Clock as the next part of the new date and time > API: > > https://wiki.php.net/rfc/time_instant_class > Hi Tim, Derick, thanks for the RFC and the work you have put into this. I like the direction. I think the following points still need some discussion. Some of them may overlap with things others have already raised in this thread. Rather than replying to each of those messages separately, I've collected everything in this one mail and mention who brought a point up where it applies. 1. Naming of Clock::now() The method returns an Instant, but the RFC already plans future classes that combine an instant with a timezone (a ZonedDateTime-like class). Callers will want to get those from a clock directly as well, without going the long way around: $clock->now()->toZonedDateTime($zone). Once a clock can return more than one type, now() no longer says what you get. Naming the method after the process of reading a clock and after what is read, e.g. $clock->takeInstant(), which leaves room for $clock->takeZonedDateTime($zone) later. java.time.Clock and Temporal.Now also name their methods after the result type. As a side effect, this also resolves the conflict with PSR-20's ClockInterface::now() that Mirco raised, a single class could implement both interfaces. One constraint thought, PHP interfaces can't have default implementations. Adding methods to Time\Clock later would break every userland clock. So either the interface should get its full shape now, or further result types should come in through factories on the target types (see 2). 2. Instant::fromClock(Clock $clock) You rejected Instant::now() because it would tie Instant to the SystemClock.. With a required Clock parameter that objection goes away. It is exactly what Java does. Later classes can then follow the same pattern (ZonedDateTime::fromClock($clock, $zone)) without the Clock interface ever growing. 3. Clock resolution The docblock says "at the resolution offered by the underlying clock", but nothing in the API exposes that resolution (Rob raised the same point). I measured what the platforms report via clock_getres() and what they actually deliver: clock_getres() smallest observed step macOS arm64, REALTIME 1 us 1 us Linux arm64, REALTIME 1 ns 42 ns (24 MHz counter) Linux, REALTIME_COARSE 1 ms - So clock_getres() is honest on some systems and only nominal on others. I'd still like SystemClock to expose it, documented as the nominal resolution of the source. That is better than no information at all. 4. Configurable clock resolution I'd like a way to configure the resolution, e.g. a SystemClock that only ever returns whole seconds. Many consumers (JWT iat/exp, HTTP dates, database columns) never want fractions. A clock configured that way guarantees whole seconds to every consumer it is injected into, instead of each consumer having to truncate on its own. It also makes test expectations simpler, because the values never carry fractions. 5. Taking a Unix timestamp directly from a clock Unix timestamps are used everywhere, not only in PHP, so "current time as a Unix timestamp" is a very common operation. Today that's time(), which can't be mocked. With this RFC the mockable replacement is $clock->now()->getUnixTimestamp(), which always goes through an Instant object. I'd like a way to take the Unix timestamp directly from the clock. 6. "Follows POSIX time" vs. "unspecified representation" The RFC states that Instants "follow POSIX time". At the same time it says that "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." I think an unspecified internal representation is the right way forward. But it doesn't match the class being specified as POSIX time. POSIX time is not just one of many ways to look at an instant. It is a specific time scale: seconds counted since 1970-01-01T00:00:00Z. If Instant follows POSIX time, then the Unix timestamp (plus a fraction of a second) is effectively the model of the class. So I'd suggest not defining Instant itself in terms of POSIX time. POSIX time is relevant where Unix timestamps come in or go out, so it belongs in the documentation of the Unix timestamp constructor and getter, not in the definition of the class. 7. get vs to The RFC explicitly states that the Unix timestamp is not the internal representation. getUnixTimestamp() is therefore a conversion, and by the RFC's own convention (toIso8601DateTimeString(), DateTime::toInstant()) it should be toUnixTimestamp(). Duration exposes its components as properties, so with get*() we'd end up with a third style within the same family. 8. getUnixTimestamp() / getUnixTimestampMilliseconds() Only the milliseconds variant names its unit, and microseconds (used all over PHP: microtime(), DateTime) and nanoseconds are missing entirely. A single pair taking the unit as an argument would cover all of them and stay symmetric: public static function fromUnixTimestamp(int $timestamp, TimeUnit $unit = TimeUnit::Second): self; public function toUnixTimestamp(TimeUnit $unit = TimeUnit::Second): int; TimeUnit would be an enum of Second, Millisecond, Microsecond and Nanosecond, rounding towards negative infinity as already specified. The current fromUnixTimestampSeconds($seconds, $nanoseconds) could remain alongside. 9. getNanoseconds() It isn't obvious whether this returns the nanoseconds since epoch or the nanoseconds within the current second. getNanoOfSecond() (Java's NANO_OF_SECOND) would remove that ambiguity. 10. Concepts the ISO-8601 pair brings into Instant An Instant is described as a timezone-less point in time. The ISO-8601 constructor and getter bring in several concepts that Instant otherwise deliberately avoids: a) parsing and formatting of date-time strings; b) UTC as the reference ("Z" means a zero offset from UTC; since RFC 9557 it formally means "UTC known, local offset unknown"); c) a calendar: ISO 8601 uses the proleptic Gregorian calendar with year 0000; d) an opinionated input and output format 11. The accepted input grammar The RFC says the parser is strict but "may not support all legal formats", and that accepted formats can be extended "by a simple pull request". That means the vote doesn't decide the accepted grammar. Widening it later is also not BC-neutral for code that uses the parser as a validator. I'd like the RFC to specify the grammar explicitly and to state the outcome for the cases Ilia listed: comma decimal sign, lowercase t/z, space separator, -00:00, 24:00:00, 23:59:60, more than 9 fractional digits. A related point: the docblock requires "a zone offset" while the prose requires "a timezone".. Is an RFC 9557 suffix such as [Europe/Berlin] accepted, rejected or ignored? 12. The output format The getter always emits Z, has a fractional part whose width depends on the value (omitted when zero, otherwise 9 digits), and uses signed expanded years outside 0000-9999. You mentioned that the getter implements the RFC 3339 subset of ISO-8601. That is not quite true: RFC 3339 only allows 4-digit years 0000-9999, so the output isn't RFC 3339 for expanded years. I also tested how well the possible outputs work as input for the existing DateTime API (PHP 8.5): - new DateTimeImmutable($s) accepts all of them, but truncates the 9 fractional digits to 6 without notice. - With createFromFormat() no single format string works. DATE_ATOM only matches whole seconds with 4-digit years. DATE_RFC3339_EXTENDED (.v, 3 digits) never matches. 'Y-m-d\TH:i:s.uP' fails on 9 digits because 'u' takes at most 6, so you need a hack like 'Y-m-d\TH:i:s.u???P'. Expanded years need 'X' instead of 'Y'. Because the fractional part is only present for some values, any strict consumer needs at least two format strings and has to choose between them, which is the trap Ilia described. An output that always includes the fractional part would at least make 'X-m-d\TH:i:s.u???P' work for every value. 13. A dedicated parser/formatter instead I'd prefer parsing and formatting to live in a dedicated parser/formatter API (future scope) rather than on Instant, where it would sit next to the other ISO-8601 profiles and custom patterns. If the pair stays on Instant, the name should say what it really is (a fixed RFC 3339-like profile), and the grammar should be part of what we vote on. 14. until() vs Duration::between() Was Duration::between(Instant $a, Instant $b) considered? Calculating a duration feels like Duration's responsibility to me. 15. Smaller things - On 32-bit, Duration only covers about 68 years, so until() is more limited there than the examples suggest. That's worth a sentence. - Duration uses sign-magnitude for its (seconds, nanoseconds) pair, while fromUnixTimestamp(-1, 500_000_000) uses floor semantics (-0.5s). Both are defensible, but the RFC should say why they differ. Best regards, Marc

« previous php.internals (#132805) next »