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

Reply via email to