Re: [RFC] Time\Instant and Time\Clock
| From: | marc at mabe dot berlin | 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