Hi
Due to the security releases it's been a while since I last looked into
this RFC and I probably lost some of my working memory. Please indicate
if there is something obvious that I missed somewhere or similar.
On 2026-10-06 11:17, [email protected] wrote:
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.
A “point in time” is the canonical representation of “now” for the
reasons that are also given in the RFC and this is also the type I
expect to be returned, even without including it in the name. As you
note yourself, a `ZonedDateTime` requires supplementary information (the
timezone) to be provided at call time. For that I would also not call it
“going the long way around”. As a direct comparison:
$clock->now()->toZonedDateTime($zone)
// vs your suggested
$clock->takeZonedDateTime($zone)
saves a grand total of 5 characters, at the expense of making the “point
in time” retrieval much more verbose. The `ZonedDateTime` will very
likely be a thin wrapper over `Instant` + `Timezone`, thus the `Instant`
object can just be reused as `ZonedDateTime::$instant`.
I would also say that using a `ZonedDateTime` as the default type for
handling time is doing it wrong. The timezone is a presentation-layer
concern and stuff you “present” to the user is generally expected to be
coming from the database, rather than coming from the clock. Allowing
comparisons between `ZonedDateTime` and “now” can be enabled by just
making `ZonedDateTime::compare()` accept `Instant|ZonedDateTime` as the
parameters. The nice part of the `compare()` method is that it's
specifically designed for ordering rather than equality, thus a return
value of `0` can mean “same point in time” even if the timezones differ.
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).
Yes, adding methods to interfaces after the fact is a breaking change
and “huge interfaces” are an indicator of wrong design. Java solves this
by making the Clock an abstract class, but this also is a heavy-weight
solution to something that is not really a problem as per the above, so
…
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.
… that would be a reasonable middle-ground of providing convenience
constructors. With the `from*()` prefix the naming is consistent and it
doesn't magically invent information out of thin air. Given that it's
pretty much redundant with `$clock->now()` with the current scope of the
RFC, I would punt this to when `ZonedDateTime` is introduced so that it
can be designed consistently across all classes.
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:
As I noted in my reply to Rob, I don't believe this is something that
belongs on the interface, since this information is unlikely to be
actionable by a majority of applications …
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.
… but we can see how exposing the (nominal) resolution of specifically
the SystemClock can be useful. I assume that a
`SystemClock::getNominalResolution(): Duration` with the `Duration`
being the greatest common divisor of possible returned values is what
you would expect? I have just added a future scope note to the RFC,
since I would need to do additional research into all the operating
systems supported by PHP. It's probably also something that can just go
in by “simple PR” without going through a full RFC, since this is a
minor feature on a `final` class.
4. Configurable clock resolution
Handled previously.
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.
Our goal with this RFC series is to provide a well-designed and
well-typed new API that nudges the user to do the right thing. Making
“raw integers” a first class citizen would go against that goal to save
7 characters (and one object creation). The previous arguments against
adding a `takeZonedDateTime()` or similar also apply here.
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.
It's not quite seconds counted since 1970-01-01T00:00:00Z, the relevant
part is that leap seconds are dropped (which makes the POSIX timescale
different from UTC).
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.
I'll look into cleaning up the wording when handling the points that Rob
mentioned.
7. get vs to
Handled previously.
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:
The `toUnixTimestamp()` + `getNanoseconds()` pair is the canonical
representation of a Unix timestamp - with “seconds” also being arguably
by far the most common interpretation when hearing “Unix timestamp”. The
“milliseconds” variant is provided for interoperability convenience, in
particular with JavaScript.
PHP’s existing `DateTime` does not provide functionality to obtain the
timestamp as an integral number of microseconds, and `microtime()`
either returns a `float` or a weird two-component pair as a string. I am
not aware of anything commonly used that returns “integral microseconds
since the Unix epoch”, do you know of anything?
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
I do not find it obvious at all that “seconds” would be the largest
supported unit of a `TimeUnit`.
9. getNanoseconds()
Handled previously.
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
Yes. I don't understand what you are getting at, though. There clearly
has to be a way to represent a point in time in some way - and an
ISO-8601 datetime string is one of the possible options, probably the
most widely used one. The Unix timestamp is another one.
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
Since the RFC guarantees that only legal formats will be accepted, you
are able to use it as a validator. If your use case requires narrower
validation, then you will need to perform custom validation according to
whatever specification you are dealing with. This is not unique to the
Instant class.
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.
See my reply to Ilia (https://news-web.php.net/php.internals/132764).
The RFC changes are still open.
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?
Thanks, I adjusted the prose to also require a “zone offset”.
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.
That is a bit of a distinction without difference: RFC 3339 is only
capable of representing years 0000 to 9999, so years outside of that
range are by definition not representable as RFC 3339. The ISO-8601
getter specifically only switches to the expanded format outside that
range. Adding a separate RFC 3339 getter that differs only by throwing
an Exception for unrepresentable instants does not sound useful, since
this Exception is not actionable in any way.
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.
I do not believe that the existing `DateTime` API is a model of good
design in that regard (and there are better ways for interoperability
with it, as noted in https://news-web.php.net/php.internals/132761).
Only taking a fixed format is already a problem as of now, because
“conditionally omitting the fractional part” is common.
The specified format is interoperable with JavaScript's
`Temporal.Instant.from()`:
> Temporal.Instant.from('1969-07-20T20:17:40Z')
Temporal.Instant 1969-07-20T20:17:40Z
> Temporal.Instant.from('1969-07-20T20:17:40.123456789Z')
Temporal.Instant 1969-07-20T20:17:40.123456789Z
> Temporal.Instant.from('1969-07-20T20:17:40.123000000Z')
Temporal.Instant 1969-07-20T20:17:40.123Z
With Go’s time.RFC3339Nano:
t, err := time.Parse(time.RFC3339Nano, "1969-07-20T20:17:40Z")
fmt.Printf("%+v\n", t) // 1969-07-20 20:17:40 +0000 UTC
fmt.Printf("%+v\n", err) // <nil>
t, err = time.Parse(time.RFC3339Nano,
"1969-07-20T20:17:40.123456789Z")
fmt.Printf("%+v\n", t) // 1969-07-20 20:17:40.123456789 +0000 UTC
fmt.Printf("%+v\n", err) // <nil>
t, err = time.Parse(time.RFC3339Nano,
"1969-07-20T20:17:40.123000000Z")
fmt.Printf("%+v\n", t) // 1969-07-20 20:17:40.123 +0000 UTC
fmt.Printf("%+v\n", err) // <nil>
With Java’s `java.time.Instant.parse()`:
System.out.println(Instant.parse("1969-07-20T20:17:40Z")); //
1969-07-20T20:17:40Z
System.out.println(Instant.parse("1969-07-20T20:17:40.123456789Z"));
// 1969-07-20T20:17:40.123456789Z
System.out.println(Instant.parse("1969-07-20T20:17:40.123000000Z"));
// 1969-07-20T20:17:40.123Z
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.
I agree with Larry’s DX argument here. I'd also like to note that the
*output* format is fixed by the RFC. The input format is less
constrained, but the RFC guarantees that it stays true to its name by
only accepting what ISO-8601 specifies.
14. until() vs Duration::between()
Was Duration::between(Instant $a, Instant $b) considered? Calculating a
duration feels like Duration's responsibility to me.
That suggestion breaks down when measuring elapsed time with a monotonic
clock. PHP does not support method overloading, so it would need to
become `Duration::between(Instant|MonoInstant, Instant|MonoInstant)`
which then also allows non-sense combinations.
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.
Added a note to `until()`’s docblock.
- 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.
Yes, the Unix timestamp needs “floor semantics” due to external
constraints: The second part only ticks once the full second has
elapsed. Negative Unix timestamps are also comparatively rare and not
always well-defined (e.g. POSIX does not specify a negative `time_t`).
This is different for Duration where negative values can regularly
appear and where direct interaction with the properties is the expected
use case.
Best regards
Tim Düsterhus