[PHP-DEV] [RFC] Time\Instant and Time\Clock

On 2026-10-01 06:40, Mirco Babin wrote:

=========
Comment 2

The RFC states:

An Time\Instant carries no timezone. It represents a unique point
on the timeline of the universe.

Why is there no explicit or implicit timezone attached? The RFC reads
to me like there is an implicit timezone attached, namely the UTC
timezone. Especially the `toIso8601DateTimeString()` function which
always returns the 'Z' timezone identifier seems to imply an implicit
UTC timezone?

I think I can answer that one. It's because for display purposes some consistent way of labelling Instants is required so that they can be displayed. Using an ISO8601 string just makes that label human-readable; the UTC timezone is not an intrinsic property of the Instant but of the labelling (nor are the labels themselves, for that matter). Instants would still be a thing and people would be able to say that two events occurred "at the same time" even if the International Meridian Conference had never happened.

(I mentioned earlier during the Duration discussion that time arithmetic is a one-dimensional real affine space. Such a space can be formalised as a set of points and a space of free vectors (and a way to add points and vectors together). Here, Durations provide the vectors and Instants provide the points.)

I think *precision* should somehow be an explicit part of \Time\Instant
class. How precise is the \Time\Instant?

Well, then it's not an _instant_, is it? It's a combination of an instant ("now") and a duration ("and the subsequent sixty seconds").

Hi

On 2026-09-30 19:40, Mirco Babin wrote:

=========
Prologue

Well, I do have 8 comments on the Request For Comments (RFC).
This is a lengthy post, be prepared for a long reading time.

Thank you! I'm fully prepared for lengthy emails and am happy to answer any clarifying questions to make sure we're all on the same page with regard to what is being proposed. Your email is also nicely structured, which makes it easy to work through it.

=========
Comment 1

Is \Time\Instant meant to be immutable? If so, please state that in
the RFC.

Yes. This was already implied since the stub specifies that it's a `readonly` class, but I've just adjusted the RFC text to explicitly include the word “immutable”.

=========
Comment 2

The RFC states:

An Time\Instant carries no timezone. It represents a unique point
on the timeline of the universe.

Why is there no explicit or implicit timezone attached? The RFC reads
to me like there is an implicit timezone attached, namely the UTC
timezone. Especially the `toIso8601DateTimeString()` function which
always returns the 'Z' timezone identifier seems to imply an implicit
UTC timezone?

Timezones are a concept to translate a point in time (“Instant”) into date and time as shown on a clock. Both 2026-09-30T20:13:30+02:00 and 2026-09-30T18:13:30+00:00 refer to the same point in time. The point in time itself does not have a timezone. The ISO-8601 getter chooses to represent the point in time with the UTC / Zulu timezone, because specifying an offset is required for the “date and time” representation to be unambiguous. It would be equally valid for it to select a random offset each time you use it. Using UTC / a zero offset is just a pragmatic choice for best interoperability.

=========
Comment 3

Why is it called \Time\Instant? I find \Time\UtcInstant a better name,
which embeds the timeline unit used.

\Time\UtcInstant leaves room for future different timeline Instants
like:
- \Time\TaiInstant (International Atomic Time)
- \Time\Ut1Instant (Universal Time)
- \Time\TtInstant (Terrestrial Time)

UTC, TAI and the others are just different projections of a “point on the timeline of the universe” and the `Instant` class is largely agnostic to them. It differs from a perfect model of the timeline of the universe in that leap seconds are ignored (see my reply to Andreas php.internals: Re: [RFC] Time\Instant and Time\Clock), since that is what the computing industry converged on. Nevertheless, this brings us back to comment (2): The differences between UTC, TAI and the others concern the *clock* time. The point on the timeline of the universe remains the same.

And of course `Time\Instant` is just the ergonomic name, matching the precedent set in Java (`java.time.Instant`) and JavaScript (`Temporal.Instant`).

=========
Comment 4

The \Time\Clock interface conflicts with PSR-20 Clock interface, see
PSR-20: Clock - PHP-FIG . A class can never implement both
interfaces at the same time. This will hinder adoption.

The RFC acknowledges that. Writing an adapter is easy, particularly since the addition of the `DateTimeImmutable::toInstant()` method to the proposal.

Consider renaming the `function now(): \Time\Instant` function to
`function nowAsInstant(): \Time\Instant`.

`now()` is the obvious name for the method. Using something else would mean that the proposed API would still suffer from a decision made for compatibility after everyone has migrated to it and PSR-20 is long forgotten. The horizon we are planning with is *at least* the next 15 years, but we're hoping that the API holds up for even longer.

For the same reason the compatibility layer with `DateTimeImmutable` is explicitly added to the *old API* so that it is possible to deprecate and remove the old API without affecting the new one.

=========
Comment 5

The RFC introduces a SystemClock class:

<?php

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 {}
}

?>

Why is SystemClock not a Singleton? Can there be multiple different
system clocks in practice? I believe a System Clock, with the emphasis
on "System" is a System Wide clock. This clock is not bound to PHP,
but to all processes running on the system, including Apache,
shell-scripts, NGINX and others.

Conceptually there is only one `SystemClock` and internally each `SystemClock` object accesses the same clock source provided by the operating system. But you are making a good point here: Replacing the constructor with a static `::get()` method or similar would make that explicit and would also allow for reducing memory usage when `SystemClock` objects are obtained at different places in the code (instead of being pulled from a dependency injection container). I'll put this on my list to discuss with Derick (and am happy to receive naming suggestions for such a static getter).

The RFC also states a FrozenClock for testing purposes. Well this

The `FrozenClock` is *not* part of the RFC. It is provided in the non-normative part of the RFC as an example to showcase how the `Clock` interface can be useful for testing purposes. Whether or not a `FrozenClock` will be added to PHP itself is to be decided as part of a future RFC (by different authors, as indicated in my email from yesterday).

pattern is not always going to work. Especially when an application
(the main program) uses Composer libraries and wants to freeze time
for each and everyone, including the `vendor` directory. Because not
every library in the `vendor` directory uses SystemClock. They all
use `time()`, because that is currently the standard way to obtain
the System Clock. Time freezing must involve the `time()` function!

It is debatable whether `time()` is the standard way to access the system clock. It lacks support for fractional seconds and has terrible ergonomics due to returning a bare integer. It is certainly still in common use, but I'd argue the current best practice is a `DateTimeImmutable` object, ideally one you obtained from a PSR-20 clock.

I think `SystemClock` should be removed from this RFC, it deserves
a seperate RFC. That RFC should also explore a frozen `time()`
function. Meaning the output of `time()` can be frozen recursively.
Because of this probably the \Time\Clock interface should also
be moved to that new RFC.

The point of the new date and time API is to completely supersede the existing date and time API with a proper API design matching best practices. Superseding the existing API includes the `time()` function - which I would say is already a legacy API as of *today*, since `DateTimeImmutable` does everything `time()` does and it does it better (except maybe for brevity).

=========
Comment 6

Why does \Time\Instant not have a named constructor
`public function fromSystemClock(): \Time\Instant` ?

Why all the complexity of constructing a \Time\SystemClock and
calling `now()`? The System Clock is a Singleton and is always
available, as the `time()` function shows.

See php.internals: Re: [RFC] Time\Instant and Time\Clock. “Injecting” a clock would be the correct solution. If `SystemClock` was made a singleton with a static getter as mentioned above (while still implementing the interface), this would allow for:

     SystemClock::get()->now()

for simple use cases and one-off scripts, which would be even shorter than your proposed:

     Instant::fromSystemClock()

=========
Comment 7

The RFC states:

PHP currently offers two ways to represent such a point, and both
are unsatisfying for the general case. A plain int as returned by
time() discards all sub-second precision and carries no type
information, leaving the question of “is this seconds, milliseconds
or something entirely different” to documentation instead of the
type system.

I want to bring something else to the table. What if one wants *less*
precision? What if one does not want seconds, milliseconds,
microseconds, nanoseconds? How is that going to fit into the
type system?

The `Instant` represents a point in time as precisely as possible. If for some reason you need less precision, you can adjust it using the `->add()` and `->sub()` methods as desired. Please also see php.internals: Re: [RFC] Time\Instant and Time\Clock for a possible future scope `->truncateTo()` method, but …

As a practical example, take a public transportation coach. The coach
has a schedule which is expressed in *minutes*, never in *seconds*. The
same argument, only *minutes* precision, goes for flight departures
and arrivals. Also ferries express their times in *minutes*.

… using an Instant to represent a public transportation schedule is the wrong abstraction: Public transportation works on “date and time” as shown on a calendar and a clock and only becomes meaningful once you involve timezones. This is particularly important when a DST changeover happens or your jurisdiction changes timezone rules: Now the point on the timeline of the universe where your bus departs is different, but the time shown on your wristwatch has remained the same.

=========
Comment 8

The RFC states:

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.

With regard to the `DateTimeInterface`, I want to share that this
interface is an internal interface and can't be implemented by
user-space classes. See also issue #23543 at
Fatal error: DateTimeInterface can't be implemented by user classes · Issue #23543 · php/php-src · GitHub

To clarify, this does not work:

<?php

class MyTime implements DateTimeInterface
{
}

// Fatal error: DateTimeInterface can't be implemented by user classes
?>

That is an accurate observation, but not relevant to the quoted part. I think there might be a misunderstanding: What the quote is saying is that if there is a function `foo(DateTimeInterface $when)` it should be adjusted to `foo(DateTimeInterface|Instant $when)` so that it is possible to use it with the new API without needing to first convert the `Instant` into a `DateTimeImmutable`.

Best regards
Tim Düsterhus

Hi

On 2026-09-30 21:10, Morgan wrote:

I think I can answer that one. […]

Yes, thank you! That matches what I just sent, just using different words.

I think *precision* should somehow be an explicit part of \Time\Instant
class. How precise is the \Time\Instant?

Well, then it's not an _instant_, is it? It's a combination of an instant ("now") and a duration ("and the subsequent sixty seconds").

Indeed. That's the uncertainty information that I mentioned in php.internals: Re: [RFC] Time\Instant and Time\Clock. As you say, it can be representing by an Instant + Duration indicating the amount of uncertainty.

Best regards
Tim Düsterhus

Hello Tim Düsterhus,

PHP: rfc:time_instant_class

Reply to Wed, 30 Sep 2026 19:48:51 +0000

=========
Comment 2

The RFC states:

An Time\Instant carries no timezone. It represents a unique point
on the timeline of the universe.

Timezones are a concept to translate a point in time (“Instant”) into
date and time as shown on a clock. Both 2026-09-30T20:13:30+02:00 and
2026-09-30T18:13:30+00:00 refer to the same point in time. The point in
time itself does not have a timezone. The ISO-8601 getter chooses to
represent the point in time with the UTC / Zulu timezone, because
specifying an offset is required for the “date and time” representation
to be unambiguous. It would be equally valid for it to select a random
offset each time you use it. Using UTC / a zero offset is just a
pragmatic choice for best interoperability.

Is this unique point on the timelime of the universe concept somewhere
defined? For example in an IETF-RFC, academic paper or somewhere else?
Because it seems to me that timeline of the universe must have some
definition somewhere? The RFC does not have any reference to a formal
definition?

=========
Comment 4

The \Time\Clock interface conflicts with PSR-20 Clock interface, see
PSR-20: Clock - PHP-FIG . A class can never implement both
interfaces at the same time. This will hinder adoption.

The RFC acknowledges that. Writing an adapter is easy, particularly
since the addition of the `DateTimeImmutable::toInstant()` method to the
proposal.

I have demonstrated it is impossible to write an adapter. Given the
constraint that only the PSR-20 MyClock class can be changed, call-sites
must not be adjusted. Also the purpose of a PSR-20 Clock is to never
call `new DateTimeImmutable()` or `time()` anymore, so the assumption there
are `new DateTimeImmutable()` calls that can be adapted is incorrect.

Consider renaming the `function now(): \Time\Instant` function to
`function nowAsInstant(): \Time\Instant`.

`now()` is the obvious name for the method. Using something else would
mean that the proposed API would still suffer from a decision made for
compatibility after everyone has migrated to it and PSR-20 is long
forgotten. The horizon we are planning with is *at least* the next 15
years, but we're hoping that the API holds up for even longer.

This is a strange point of view. Why would PSR-20 be forgotten? Is it
a goal to deprecate/forget PSR-20? And why is this not mentioned in
the RFC? And what would the duration be of the forget timeline?

`time()` is also an obvious name for the method. And it would not
conflict with PSR-20.


SystemClock::get()->time();

=========
Comment 5

The RFC introduces a SystemClock class:

The RFC also states a FrozenClock for testing purposes. Well this

The `FrozenClock` is *not* part of the RFC. It is provided in the
non-normative part of the RFC as an example to showcase how the `Clock`
interface can be useful for testing purposes. Whether or not a
`FrozenClock` will be added to PHP itself is to be decided as part of a
future RFC (by different authors, as indicated in my email from
yesterday).

That is surprising. How can a SystemClock be designed without
exploring the freezing of time? If the SystemClock is wrongly
designed in this RFC, and hinders the unexplored, unknown FrozenTime,
"*at least* the next 15 years" there will be an misdesign.

I really think Time\Clock interface, SystemClock, FrozenTime,
DateTimeImmutable freezing and `time()` freezing must be one RFC,
that designs them very well. And don't forget the very popular Carbon
library in this picture. This design should not be split between
2 RFC's and different authors.

=========
Comment 6

Why does \Time\Instant not have a named constructor
`public static function fromSystemClock(): \Time\Instant` ?

If `SystemClock` was made a singleton
with a static getter as mentioned above (while still implementing the
interface), this would allow for:

    SystemClock::get()->now()

for simple use cases and one-off scripts, which would be even shorter
than your proposed:

    Instant::fromSystemClock()

If it becomes `SystemClock::get()->time()` both variants have 26
characters to type. But IDE autocompletion has to parse 3 parts in
`SystemClock::get()->time()` and only 2 parts in
`Instant::fromSystemClock()`.

If it is not too much trouble I would add them both, because it
increases Developer Experience.

Kind regards,
Mirco Babin

Hi Tim,

Broadly in favor. The timezone-less design and keeping the compat layer
on the old API are the right calls. Three things I’d like pinned down
before a vote:

  1. toIso8601DateTimeString() output width. Omitting the fraction only
    when it is zero makes the output format depend on the value. A consumer
    that can’t parse fractions passes tests with whole-second fixtures, then
    fails on almost every SystemClock value in production, so the interop
    benefit doesn’t hold. It also breaks string ordering:
    “2026-09-29T16:42:45Z” sorts after “2026-09-29T16:42:45.500000000Z”, and
    people do store these in VARCHAR columns and ORDER BY them. Always
    emitting 9 digits gives fixed width, chronological string order for
    years 0000 to 9999, and the explicit precision you were after.

  2. The accepted grammar of fromIso8601DateTimeString(). “May not support
    all legal formats, extendable by a PR” means the accepted set gets
    decided in code review rather than by the vote. I’d like the RFC to
    guarantee RFC 3339 date-time (plus the expanded years the getter emits)
    and state the outcome for: more than 9 fractional digits, comma decimal
    sign, lowercase t/z, space separator, -00:00, 24:00:00 and 23:59:60. On
    the last one, java.time parses 23:59:60 as 23:59:59 rather than
    rejecting it, and RFC 3339 explicitly permits :60. Rejecting is
    defensible, but it should be written down.

  3. json_encode(). With no public properties, json_encode($instant)
    yields {}. Duration and DateTimeImmutable both encode to something
    meaningful today, so a silent {} is a trap for any API response carrying
    a timestamp. Either implement JsonSerializable returning the ISO-8601
    string, or state that {} is intentional.

On the exceptions open issue: splitting TimeException into subclasses
later is BC-safe, since catch (TimeException) keeps working, so I don’t
think it needs to block this RFC.

···

Ilia Alshanetsky
Technologist, CTO, Entrepreneur
E: ilia@ilia.ws
T: @iliaa
B: http://ilia.ws

Hey Mirco

On 1 Oct 2026, at 17:22, Mirco Babin wrote:

Hello Tim Düsterhus,

https://wiki.php.net/rfc/time_instant_class

Reply to Wed, 30 Sep 2026 19:48:51 +0000

=========

Comment 2

The RFC states:

An Time\Instant carries no timezone. It represents a unique point
on the timeline of the universe.

Timezones are a concept to translate a point in time (“Instant”) into
date and time as shown on a clock. Both 2026-09-30T20:13:30+02:00 and
2026-09-30T18:13:30+00:00 refer to the same point in time. The point in
time itself does not have a timezone. The ISO-8601 getter chooses to
represent the point in time with the UTC / Zulu timezone, because
specifying an offset is required for the “date and time” representation
to be unambiguous. It would be equally valid for it to select a random
offset each time you use it. Using UTC / a zero offset is just a
pragmatic choice for best interoperability.

Is this unique point on the timelime of the universe concept somewhere
defined? For example in an IETF-RFC, academic paper or somewhere else?
Because it seems to me that timeline of the universe must have some
definition somewhere? The RFC does not have any reference to a formal
definition?

I’d say that is defined as Barycentric Coordinated Time (TCB). But that is slightly off towards the Geocentric Coordinated Time (TCG) by about 490 ms per year. Geocentric Coordinated Time and Terrestrial Time (TT) again differ by a constant from one another. Though both are linear counts of SI-seconds.

But all that is obsoleted and irrelevant in the end as the Instant is based on the epoch which is based on UTC minus leap-seconds. At least as far as I understood Tim.

=========

Comment 4

The \Time\Clock interface conflicts with PSR-20 Clock interface, see
https://www.php-fig.org/psr/psr-20/ . A class can never implement both
interfaces at the same time. This will hinder adoption.

The RFC acknowledges that. Writing an adapter is easy, particularly
since the addition of the DateTimeImmutable::toInstant() method to the
proposal.

I have demonstrated it is impossible to write an adapter. Given the
constraint that only the PSR-20 MyClock class can be changed, call-sites
must not be adjusted. Also the purpose of a PSR-20 Clock is to never
call new DateTimeImmutable() or time() anymore, so the assumption there
are new DateTimeImmutable() calls that can be adapted is incorrect.

An adapter does never need to implement both interfaces. In essence an adapter only needs to implement “the other” interface. A ClockAdapter uses a ClockInterface to create an Instant whereas a ClockInterfaceAdapter uses a Clock to create a DateTime. 2 classes that seem rather straight forward.

Consider renaming the function now(): \Time\Instant function to
function nowAsInstant(): \Time\Instant.

now() is the obvious name for the method. Using something else would
mean that the proposed API would still suffer from a decision made for
compatibility after everyone has migrated to it and PSR-20 is long
forgotten. The horizon we are planning with is at least the next 15
years, but we’re hoping that the API holds up for even longer.

This is a strange point of view. Why would PSR-20 be forgotten? Is it
a goal to deprecate/forget PSR-20? And why is this not mentioned in
the RFC? And what would the duration be of the forget timeline?

time() is also an obvious name for the method. And it would not
conflict with PSR-20.

I can only second Tim here. The correct and only sensible method name is now() as is for any other ClockInterface.

Time() has a very special meaning in PHP and people will be very confused to realise that time() does not return a timestamp.

We had a lot of these discussions when preparing PSR20 and now() IS the sensible name.

And it will also make very clear that it is not possible to have ONE clock that serves BOTH purposes. Where we come back again to the adapters that make it possible to translate between the two


SystemClock::get()->time();

=========

Comment 5

The RFC introduces a SystemClock class:

The RFC also states a FrozenClock for testing purposes. Well this

The FrozenClock is not part of the RFC. It is provided in the
non-normative part of the RFC as an example to showcase how the Clock
interface can be useful for testing purposes. Whether or not a
FrozenClock will be added to PHP itself is to be decided as part of a
future RFC (by different authors, as indicated in my email from
yesterday).

That is surprising. How can a SystemClock be designed without
exploring the freezing of time? If the SystemClock is wrongly
designed in this RFC, and hinders the unexplored, unknown FrozenTime,
“at least the next 15 years” there will be an misdesign.

I really think Time\Clock interface, SystemClock, FrozenTime,
DateTimeImmutable freezing and time() freezing must be one RFC,
that designs them very well. And don’t forget the very popular Carbon
library in this picture. This design should not be split between
2 RFC’s and different authors.

Freezing time is always subject to the circumstances. And creating a frozenClock is just a few lines of code so that for the sake of testing it makes indeed more sense to leave that to the implementor of the test to create a FrozenClock matching exactly their needs.

=========

Comment 6

Why does \Time\Instant not have a named constructor
public static function fromSystemClock(): \Time\Instant ?

If SystemClock was made a singleton
with a static getter as mentioned above (while still implementing the
interface), this would allow for:

SystemClock::get()->now()

for simple use cases and one-off scripts, which would be even shorter
than your proposed:

Instant::fromSystemClock()

If it becomes SystemClock::get()->time() both variants have 26
characters to type. But IDE autocompletion has to parse 3 parts in
SystemClock::get()->time() and only 2 parts in
Instant::fromSystemClock().

If it is not too much trouble I would add them both, because it
increases Developer Experience.

When using SystemClock::get() or Instant::fromSystemClock() you can equally just call time() and be done.

The whole advantage of injecting an interface that can then be used with a “real” clock in production and a frozen clock for testing is then obsoleted.

If one requires these static dependencies in their code it is very easy to create the appropriate classes in Userland and be done.

IMO that is not something the language should provide out of the box.

My 0.02€

Cheers

Andreas
(PSR20 Workinggroup Member)

–
,
(o o)
±--------------------------------------------------------ooO-(_)-Ooo-+
| Andreas Heigl |
| mailto:andreas@heigl.org N 50°22’59.5" E 08°23’58" |
| https://andreas.heigl.org |
±--------------------------------------------------------------------+
| https://hei.gl/appointmentwithandreas |
±--------------------------------------------------------------------+
| GPG-Key: https://hei.gl/keyandreasheiglorg |
±--------------------------------------------------------------------+

Hi

thank you Andreas, that mostly matches what I would have answered myself. I'll add my own nuance in a separate reply to Mirco directly.

On 2026-10-01 17:57, Andreas Heigl wrote:

But all that is obsoleted and irrelevant in the end as the Instant is based on the epoch which is based on UTC minus leap-seconds. At least as far as I understood Tim.

Yes, or “POSIX time”, which avoids the overloaded term UTC that could also refer to the UTC timezone. Timezones reintroduce “date and time” as a concept, which is not relevant to a point in time.

I have demonstrated it is impossible to write an adapter. Given the
constraint that only the PSR-20 MyClock class can be changed, call-sites
must not be adjusted. Also the purpose of a PSR-20 Clock is to never
call `new DateTimeImmutable()` or `time()` anymore, so the assumption there
are `new DateTimeImmutable()` calls that can be adapted is incorrect.

An adapter does never need to implement both interfaces. In essence an adapter only needs to implement "the other" interface. A ClockAdapter uses a ClockInterface to create an Instant whereas a ClockInterfaceAdapter uses a Clock to create a DateTime. 2 classes that seem rather straight forward.

Indeed. Here's an example implementation for both directions. The `NativeToPsr20` adapter truncates to microseconds and needs to “invent” a timezone. The opinionated choices made there should be correct for the vast majority of users:

     final readonly class Psr20ToNative implements \Time\Clock
     {
         public function __construct(
             private \Psr\Clock\ClockInterface $clock,
         ) {}

         public function now(): \Time\Instant
         {
             return $this->clock->now()->toInstant();
         }
     }

     final readonly class NativeToPsr20 implements \Psr\Clock\ClockInterface
     {
         public function __construct(
             private \Time\Clock $clock,
             private \DateTimeZone $timezone = new \DateTimeZone('UTC'),
         ) {}

         public function now(): \DateTimeImmutable
         {
             $now = $this->clock->now();

             return \DateTimeImmutable::createFromTimestamp($now->getUnixTimestamp())
                 ->setMicrosecond(\intdiv($now->getNanoseconds(), 1_000))
                 ->setTimezone($this->timezone);
         }
     }

Best regards
Tim Düsterhus

Hi

On 2026-10-01 17:40, Ilia wrote:

1. toIso8601DateTimeString() output width. Omitting the fraction only
when it is zero makes the output format depend on the value. A consumer
that can't parse fractions passes tests with whole-second fixtures, then
fails on almost every SystemClock value in production, so the interop
benefit doesn't hold. It also breaks string ordering:

You are correct that SystemClock will emit fractional seconds in virtually all cases. However from my experience with dealing with API responses, they rarely contain sub-second information and fractional seconds are accordingly omitted from the ISO-8601 string. When using those as inputs, the “lack of fractional seconds” roundtrips and any math with `Duration`s that are integral multiples of a second will keep that property.

"2026-09-29T16:42:45Z" sorts after "2026-09-29T16:42:45.500000000Z", and
people do store these in VARCHAR columns and ORDER BY them. Always
emitting 9 digits gives fixed width, chronological string order for
years 0000 to 9999, and the explicit precision you were after.

As you say yourself, the VARCHAR ordering breaks down for years before 0000 and after 9999 since a mandatory sign will be added. While these are well outside our lifetimes, I don’t think “always emitting fractional seconds allows for naive string-based ordering” follows from that, since it still breaks down for points in time that are explicitly supported by the `Instant` class.

For proper ordering the `Instant::compare()` method is provided and users should use the date/time types provided by their database (e.g. `timestamptz` for PostgreSQL), which will also allow for in-database time arithmetic and default values (e.g. provided by `NOW()`).

2. The accepted grammar of fromIso8601DateTimeString(). "May not support
all legal formats, extendable by a PR" means the accepted set gets
decided in code review rather than by the vote. I'd like the RFC to
guarantee RFC 3339 date-time (plus the expanded years the getter emits)

The RFC already specifies that:

It naturally will support all possible return values of toIso8601DateTimeString().

And in practice the `toIso8601DateTimeString()` getter implements the RFC 3339 subset of ISO-8601. That would leave non-Z offsets, which are easy enough to include (and already supported by DateTimeImmutable, so we can just borrow from there).

and state the outcome for: more than 9 fractional digits, comma decimal
sign, lowercase t/z, space separator, -00:00, 24:00:00 and 23:59:60. On
the last one, java.time parses 23:59:60 as 23:59:59 rather than
rejecting it, and RFC 3339 explicitly permits :60. Rejecting is
defensible, but it should be written down.

The RFC notes that the parser is strict: All invalid inputs will be rejected (that includes the space separator). Other than that the inputs that can be parsed losslessly fall under the “extendable by PR” rule. As an example, if a comma decimal is not supported in the initial version, it can be added later without introducing a breaking change. The input is well-defined and representable by Instant. The same applies to lowercase t/z or the “basic format” 20261001T194330+0200.

The behavior for lossy inputs I’ll put on my list to discuss with Derick.

3. json_encode(). With no public properties, json_encode($instant)
yields {}. Duration and DateTimeImmutable both encode to something
meaningful today, so a silent {} is a trap for any API response carrying
a timestamp. Either implement JsonSerializable returning the ISO-8601
string, or state that {} is intentional.

I’ll put that on my Derick list, but my personal answer is: As the RFC notes there is no single representation that is correct in 100% of the cases. Some JSON payloads might want an ISO-8601 string, some might want a Unix timestamp with millisecond precision. Not implementing JsonSerializable forces the user to make a choice - it is unfortunate that PHP’s JSON serializer defaults to a useless object representation rather than requiring an opt-in. For the same reason there is no `__toString()`, but a `toIso8601DateTimeString()` (see also the design of ext/uri).

On the exceptions open issue: splitting TimeException into subclasses
later is BC-safe, since catch (TimeException) keeps working, so I don't
think it needs to block this RFC.

Yes. However we’ve already deferred adding the hierarchy in the Duration RFC, so I put it on the list for discussion. But it probably makes sense to define the remaining classes before committing to the final hierarchy.

Best regards
Tim Düsterhus

Hello Tim Düsterhus,

PHP: rfc:time_instant_class

=========
Comment 2

... snipped ...

(Tim Düsterhus)

Timezones are a concept to translate a point in time (“Instant”) into
date and time as shown on a clock. Both 2026-09-30T20:13:30+02:00 and
2026-09-30T18:13:30+00:00 refer to the same point in time. The point in
time itself does not have a timezone. The ISO-8601 getter chooses to
represent the point in time with the UTC / Zulu timezone, because
specifying an offset is required for the “date and time” representation
to be unambiguous. It would be equally valid for it to select a random
offset each time you use it. Using UTC / a zero offset is just a
pragmatic choice for best interoperability.

(Mirco Babin)

Is this unique point on the timelime of the universe concept somewhere
defined? For example in an IETF-RFC, academic paper or somewhere else?
Because it seems to me that timeline of the universe must have some
definition somewhere? The RFC does not have any reference to a formal
definition?

(Andreas Heigl)

I'd say that is defined as Barycentric Coordinated Time (TCB). But
that is slightly off towards the Geocentric Coordinated Time (TCG)
by about 490 ms per year. Geocentric Coordinated Time and Terrestrial
Time (TT) again differ by a constant from one another. Though both are
linear counts of SI-seconds.

But all that is obsoleted and irrelevant in the end as the Instant is
based on the epoch which is based on UTC minus leap-seconds. At least
as far as I understood Tim.

(Tim Düsterhus)

Yes, or “POSIX time”, which avoids the overloaded term UTC that could
also refer to the UTC timezone. Timezones reintroduce “date and time”
as a concept, which is not relevant to a point in time.

(Mirco Babin) Thank you to Andreas Heighl and to Tim Düsterhus for
this answer. This
clears a lot of my confusion.

The RFC introduction currently states:

This RFC proposes the addition of a Time\Instant class representing
an absolute point on the timeline of the universe together with a
Time\Clock interface and a Time\Clock\SystemClock implementation for
obtaining the current Time\Instant.

The term "timeline of the universe" made me believe it was something
completely different, a new time concept. It did not make me think of
the Unix-Epoch-int time concept.

Can an extra explanation be added in the introduction near the
aforementioned RFC paragraph? Something like:

A \Time\Instant can be viewed as a replacement for the Unix-Epoch-int.
With the difference that the base-date might not be 1970-01-01. And
with the difference that the \Time\Instant can have more precision in
the form of milliscondes, microseconds, nanoseconds. The base-date
and extra precision are an implementation detail and not described
further. This also means the full timerange of the \Time\Instant is at
least that of the Unix-Epoch-int, but can be extended in the future.

=========
Comment 4

The \Time\Clock interface conflicts with PSR-20 Clock interface, see
PSR-20: Clock - PHP-FIG . A
class can never implement both
interfaces at the same time. This will hinder adoption.

(Tim Düsterhus)

The RFC acknowledges that. Writing an adapter is easy, particularly
since the addition of the DateTimeImmutable::toInstant() method to
the proposal.

(Mirco Babin)

I have demonstrated it is impossible to write an adapter. Given the
constraint that only the PSR-20 MyClock class can be changed,
call-sites must not be adjusted. Also the purpose of a PSR-20 Clock
is to never call new DateTimeImmutable() or time() anymore, so the
assumption there are new DateTimeImmutable() calls that can be adapted
is incorrect.

(Andreas Heigl)

An adapter does never need to implement both interfaces. In essence an
adapter only needs to implement "the other" interface. A ClockAdapter
uses a ClockInterface to create an Instant whereas a
ClockInterfaceAdapter uses a Clock to create a DateTime.
2 classes that seem rather straight forward.

(Tim Düsterhus)

Indeed. Here's an example implementation for both directions.
The NativeToPsr20 adapter truncates to microseconds and needs to
“invent” a timezone. The opinionated choices made there should be
correct for the vast majority of users:

<?php
final readonly class Psr20ToNative implements \Time\Clock
{
    public function __construct(
        private \Psr\Clock\ClockInterface $clock,
    ) {}

    public function now(): \Time\Instant
    {
        return $this->clock->now()->toInstant();
    }
}
?>
<?php
final readonly class NativeToPsr20 implements
    \Psr\Clock\ClockInterface
{
    public function __construct(
        private \Time\Clock $clock,
        private \DateTimeZone $timezone =
            new \DateTimeZone('UTC'),
    ) {}

    public function now(): \DateTimeImmutable
    {
        $now = $this->clock->now();

        return \DateTimeImmutable
            ::createFromTimestamp($now->getUnixTimestamp())
            ->setMicrosecond(\intdiv($now->getNanoseconds(), 1_000))
            ->setTimezone($this->timezone);
    }
}
?>

(Mirco Babin) These adapters are not what I meant. My point concerns
all applications that currently use the PSR-20-Clock. To clarify more,
consider an application using the PSR-20 clock with a theoretical
function like this:

<?php
//
// The original theoretical PSR-20-application
//
class MyApp_Clock implements \Psr\Clock\ClockInterface
{
    public function now(): \DateTimeImmutable {
        return new \DateTimeImmutable();
    }
}

function AFunctionOfTheApplication()
{
    // $clock might also come from a Dependency Injection container.
    // Or be injected via parameters or via constructor injection.
    // It doesn't really matter.
    $clock = new MyApp_Clock();

    // The $clock is injected into external libraries. These
    // libraries are not under the control of the application.
    // Also the libraries do not use the Dependency Injection
    // container of the application. Otherwise the DI container
    // would have beeen provided as parameter instead of the
    // \Psr\Clock\ClockInterface.
    $result1 = Library_Alfa_Do_Something($clock);

    $result2 = Library_Beta_Do_Something($clock);

    $result3 = Library_Gamma_Do_Something($clock);

    $result4 = Library_Theta_Do_Something($clock);
}
?>

At some point in the future time, one of the 4 libraries switches
to \Time\SystemClock. Let's assume it is Beta that adapts. How is this
code then going to look?

<?php
//
// Modified theoretical PSR-20-application
//

function AFunctionOfTheApplication()
{
    // $clock might also come from a Dependency Injection container.
    // Or be injected via parameters or via constructor injection.
    // It doesn't really matter.
    $clock = new MyApp_Clock();

    // The $clock is injected into external libraries. These
    // libraries are not under the control of the application.
    // Also the libraries do not use the Dependency Injection
    // container of the application. Otherwise the DI container
    // would have beeen provided as parameter instead of the
    // \Psr\Clock\ClockInterface.
    $result1 = Library_Alfa_Do_Something($clock);

    // Or use the provided Psr20ToNative adapter.
    $clockInstant = new \Time\SystemClock();
    $result2 = Library_Beta_Do_Something($clockInstant);

    $result3 = Library_Gamma_Do_Something($clock);

    $result4 = Library_Theta_Do_Something($clock);
}
?>

I hope the pattern becomes clear. After the \Time\Clock fact
the PSR-20-application will be split in 2 clocks. Carefully selecting
the right clock for the right call. And the careful selection has to
take place on every update of any external library. Because the
external library might have switched to \Time\Clock at any moment.
This is a maintenance disaster.

The best solution I can think of, is to adjust one class, namely
the MyApp_Clock which the application controls. And make it a
*hybrid clock*, both a \Psr\Clock\ClockInterface and a \Time\Clock.
In that case there is an one-time adjustment in one class and all
else will work *without any modification*.

```php
<?php

// hybrid clock
class MyApp_Clock implements \Psr\Clock\ClockInterface,
    \Time\Clock
{
    public function now(): \DateTimeImmutable {
        return new \DateTimeImmutable();
    }

    public function nowAsInstant(): \Time\Instant {
        return \Time\SystemClock::get()->nowAsInstant();
    }
}

But because the \Time\Clock->now() conflicts, as I have
demonstrated, with the \Psr\Clock\ClockInterface->now(), this
one-time solution is not possible. Now assume this PSR-20-application
is a big one, thousands of classes, millions line of code. This will
become a PSR-20-application maintenance disaster.

So I ask once again, consider renaming \Time\Clock->now()
to something else. The 'now()' naming obsession is not worth this
interface conflict and maintenance disaster for PSR-20-applications.

This also means: Yes, I do consider all Psr's including PSR-20
to be part of Php, and that PSR-interfaces
can't be ignored nor conflicted.

Kind regards,
Mirco Babin

On Sun, Oct 4, 2026, at 9:04 AM, Mirco Babin wrote:

(Mirco Babin) These adapters are not what I meant. My point concerns
all applications that currently use the PSR-20-Clock. To clarify more,
consider an application using the PSR-20 clock with a theoretical
function like this:

<?php
//
// The original theoretical PSR-20-application
//
class MyApp_Clock implements \Psr\Clock\ClockInterface
{
    public function now(): \DateTimeImmutable {
        return new \DateTimeImmutable();
    }
}

function AFunctionOfTheApplication()
{
    // $clock might also come from a Dependency Injection container.
    // Or be injected via parameters or via constructor injection.
    // It doesn't really matter.
    $clock = new MyApp_Clock();

    // The $clock is injected into external libraries. These
    // libraries are not under the control of the application.
    // Also the libraries do not use the Dependency Injection
    // container of the application. Otherwise the DI container
    // would have beeen provided as parameter instead of the
    // \Psr\Clock\ClockInterface.
    $result1 = Library_Alfa_Do_Something($clock);

    $result2 = Library_Beta_Do_Something($clock);

    $result3 = Library_Gamma_Do_Something($clock);

    $result4 = Library_Theta_Do_Something($clock);
}
?>

At some point in the future time, one of the 4 libraries switches
to \Time\SystemClock. Let's assume it is Beta that adapts. How is this
code then going to look?

<?php
//
// Modified theoretical PSR-20-application
//

function AFunctionOfTheApplication()
{
    // $clock might also come from a Dependency Injection container.
    // Or be injected via parameters or via constructor injection.
    // It doesn't really matter.
    $clock = new MyApp_Clock();

    // The $clock is injected into external libraries. These
    // libraries are not under the control of the application.
    // Also the libraries do not use the Dependency Injection
    // container of the application. Otherwise the DI container
    // would have beeen provided as parameter instead of the
    // \Psr\Clock\ClockInterface.
    $result1 = Library_Alfa_Do_Something($clock);

    // Or use the provided Psr20ToNative adapter.
    $clockInstant = new \Time\SystemClock();
    $result2 = Library_Beta_Do_Something($clockInstant);

    $result3 = Library_Gamma_Do_Something($clock);

    $result4 = Library_Theta_Do_Something($clock);
}
?>

I hope the pattern becomes clear. After the \Time\Clock fact
the PSR-20-application will be split in 2 clocks. Carefully selecting
the right clock for the right call.

You're setting up a pretty pathological case here. Many services using a clock, many 3rd party services using a clock, but NOT using a DI container that has even rudimentary autowiring support.

Such an application is already stupidly written. A decent DI container makes this problem go away. And if you want to only have one source of time truth, you can, as Tim showed, wrap one clock around another to translate the value object.

And the careful selection has to
take place on every update of any external library. Because the
external library might have switched to \Time\Clock at any moment.
This is a maintenance disaster.

This is a maintainer disaster. If they switch external dependencies that you're supposed to handle without a major version bump, you can blame the maintainer, not PHP. No different than if they switch from one HTTP client to another, etc.

This also means: Yes, I do consider all Psr's including PSR-20
to be part of Php, and that PSR-interfaces
can't be ignored nor conflicted.

Kind regards,
Mirco Babin

As a long-time member of the PHP-FIG Core Committee, I disagree. The existing ecosystem should not be ignored, but nor should we be shackled by it. I've repeatedly said that any new http-message-object-in-core effort should NOT be bound by PSR-7, nor need it follow PSR-7; it should be good enough to *replace* PSR-7.

This proposal seems like a good step to replacing PSR-20 with something even more standardized and universally available. That seems like a win in my book.

--Larry Garfield

Hi

On 2026-10-04 16:04, Mirco Babin wrote:

Can an extra explanation be added in the introduction near the
aforementioned RFC paragraph? Something like:

A \Time\Instant can be viewed as a replacement for the Unix-Epoch-int.
With the difference that the base-date might not be 1970-01-01. And
with the difference that the \Time\Instant can have more precision in
the form of milliscondes, microseconds, nanoseconds. The base-date
and extra precision are an implementation detail and not described
further. This also means the full timerange of the \Time\Instant is at
least that of the Unix-Epoch-int, but can be extended in the future.

I'll put adding a clarification on my list for the next RFC update.

[…]
At some point in the future time, one of the 4 libraries switches
to \Time\SystemClock. Let's assume it is Beta that adapts. How is this
code then going to look?

<?php
//
// Modified theoretical PSR-20-application
//

function AFunctionOfTheApplication()
{
    // $clock might also come from a Dependency Injection container.
    // Or be injected via parameters or via constructor injection.
    // It doesn't really matter.
    $clock = new MyApp_Clock();

    // The $clock is injected into external libraries. These
    // libraries are not under the control of the application.
    // Also the libraries do not use the Dependency Injection
    // container of the application. Otherwise the DI container
    // would have beeen provided as parameter instead of the
    // \Psr\Clock\ClockInterface.
    $result1 = Library_Alfa_Do_Something($clock);

    // Or use the provided Psr20ToNative adapter.
    $clockInstant = new \Time\SystemClock();
    $result2 = Library_Beta_Do_Something($clockInstant);

    $result3 = Library_Gamma_Do_Something($clock);

    $result4 = Library_Theta_Do_Something($clock);
}
?>

I hope the pattern becomes clear. After the \Time\Clock fact
the PSR-20-application will be split in 2 clocks. Carefully selecting
the right clock for the right call. And the careful selection has to
take place on every update of any external library. Because the
external library might have switched to \Time\Clock at any moment.
This is a maintenance disaster.

The expectation is that you would use one of the two adapters to translate between the PSR-20 clock and PHP’s native clock, depending on which one is the source of truth in your application. The resulting code would look like this:

     function AFunctionOfTheApplication()
     {
         $clock = new MyApp_Clock();

         // The PSR-20 clock is the canonical one for this application,
         // create the native adapter.
         $nativeClock = new Psr20ToNative($clock);

         $result1 = Library_Alfa_Do_Something($clock);

         $result2 = Library_Beta_Do_Something($nativeClock);

         $result3 = Library_Gamma_Do_Something($clock);

         $result4 = Library_Theta_Do_Something($clock);
     }

This is one extra line to initialize the `$nativeClock` adapter and one changed line to pass the native clock to Beta instead of the PSR-20 clock.

But because the \Time\Clock->now() conflicts, as I have
demonstrated, with the \Psr\Clock\ClockInterface->now(), this
one-time solution is not possible. Now assume this PSR-20-application
is a big one, thousands of classes, millions line of code. This will
become a PSR-20-application maintenance disaster.

As Larry noted, changing from PSR-20 to the new date and time API would be a breaking change. This kind of migration would also not be limited to just the change in clocks. The `Time\Instant`, and the modeling of the new date and time API in general are not a drop-in replacement for the existing `DateTimeImmutable` API. A library that migrates from PSR-20 to the native clock will want to consistently use the date and time API to benefit from it. This will affect internal calculations and possibly returned values (e.g. returning an `Instant` where previously a `DateTimeImmutable` was returned). Just replacing the clock and keeping everything else the same brings little benefit. Changing the clock you pass in is the smallest part of the migration effort.

Best regards
Tim Düsterhus

Hey Tim,

thanks for the RFC.

On 22.09.26 22:20, Tim Düsterhus wrote:

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:

PHP: rfc:time_instant_class

Why would this not become an always bundled `ext/time`?

You called it 'greenfield design', and said 'deliberately don't want to “infect” the new API with ideas from the legacy API'. Yet it lives next to this old code. It was also mentioned that JavaScript's `Temporal` is roughly where the journey goes, which is quite some scope yet to come.

Since this proposed new date/time API will be the successor of the old date/time API, why not also physically & cleanly separate the new from the old stuff? `ext/standard` is such a kitchen sink already that I wonder why modern successors would not live in a more "sensible" structure. Imagine a (far) future where we could chose to compile PHP with or without legacy stuff -- clean separation would make this easier.

As far as my understanding of the policy goes, nothing would forbid making this an extension (see ext/uri)?

---

Cheers
Nick

Hi

On 2026-10-05 15:57, Nick Sdot wrote:

On 22.09.26 22:20, Tim Düsterhus wrote:

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:

PHP: rfc:time_instant_class

Why would this not become an always bundled `ext/time`?

For the “always enabled” extensions it doesn't really matter *where* the code is located and the (observable) end result for the user is the same (except for `ReflectionExtension`). Time\Duration went into ext/date for simplicity, since timelib is already properly wired into ext/date.

Since this proposed new date/time API will be the successor of the old date/time API, why not also physically & cleanly separate the new from the old stuff? `ext/standard` is such a kitchen sink already that I wonder why modern successors would not live in a more "sensible" structure. Imagine a (far) future where we could chose to compile PHP with or without legacy stuff -- clean separation would make this easier.

It's ext/date, not ext/standard where the API lives. For Time\Duration we already split off the implementation into a separate and effectively standalone `php_time.h` header with `php_time.c` and `time_duration.c` source files, instead of the `php_date.[ch]` kitchen sink.

As far as my understanding of the policy goes, nothing would forbid making this an extension (see ext/uri)?

That is accurate.

Best regards
Tim Düsterhus

Hey Tim,

On 05.10.26 22:05, Tim Düsterhus wrote:

Why would this not become an always bundled `ext/time`?

For the “always enabled” extensions it doesn't really matter *where* the code is located and the (observable) end result for the user is the same (except for `ReflectionExtension`). Time\Duration went into ext/date for simplicity, since timelib is already properly wired into ext/date.

It matters how code is organised. Patterns, assumptions, cognitive load, these, that. I think that's important. A "doesn't really matter *where*" approach is what brings code where core is.

I would find it more natural to look at the namespace `Time`, and find an `ext/time`. Me getting confused in my last mail shows it nicely. My mail was in draft since you opened the discussion, today I found time to answer and whoops mixed up the polling follow up commit with where things actually live. :slight_smile:

That is accurate.

If "doesn't really matter *where*" stands, then it cannot hurt to separate it. So: why _not_ doing it?

---

Cheers
Nick

Hi

On 2026-10-05 16:53, Nick Sdot wrote:

For the “always enabled” extensions it doesn't really matter *where* the code is located and the (observable) end result for the user is the same (except for `ReflectionExtension`). Time\Duration went into ext/date for simplicity, since timelib is already properly wired into ext/date.

It matters how code is organised. Patterns, assumptions, cognitive load, these, that. I think that's important. A "doesn't really matter *where*" approach is what brings code where core is.

I would find it more natural to look at the namespace `Time`, and find an `ext/time`. Me getting confused in my last mail shows it nicely. My mail was in draft since you opened the discussion, today I found time to answer and whoops mixed up the polling follow up commit with where things actually live. :slight_smile:

For the vast majority of PHP developers the exact extension where the code lives is not relevant - they likely don't even know that PHP’s always-enabled standard library is split into multiple extensions, since no action is required for it to be available. Accordingly the RFC also leaves this unspecified as an implementation detail.

If "doesn't really matter *where*" stands, then it cannot hurt to separate it. So: why _not_ doing it?

ext/date was the pragmatic choice to get things into PHP 8.6. Splitting it out would likely also require a separate ext/timelib extension (similar to ext/lexbor) that provides the underlying library for internal use. Otherwise we would just move some files - that are already entirely standalone - into a separate directory, but not actually do anything meaningful to separate ext/time from ext/date.

Best regards
Tim Düsterhus

On Tue, Sep 22, 2026, at 16:20, Tim Düsterhus 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

This time we have several months until the next feature freeze, so
there’s place to discuss additional functionality that we missed.
Nevertheless we would like to keep the RFC focused so that every
decision gets the attention it deserves and some bits are already listed
as “Future Scope” to that effect.

Best regards
Tim Düsterhus

Hi Tim,

I’m just now getting around to reading over the RFC, and I’ve read most messages but not all of them. So, I apologize if I’m reopening any old discussions here.

After spending much of the last 4-5 years working on a database that needs to deal with relativity, I’d recommend dropping the physics framing in the introduction. Time doesn’t move at the same speed for everyone, even on Earth, and you don’t specify a frame of reference for the instant. It would also make it easier to explain until as “computes the difference between two represented time values. It does not establish how much physical time elapsed between the events that produced those values.”

This also raises a question about uncertainty. I appreciate that the RFC distinguishes the nanosecond precision of the representation from the resolution of the underlying clock. However, Clock::now() returns only an Instant, with no way through that contract to convey the source resolution or any available uncertainty information. I’ve had servers lose access to their hardware clock due to hardware malfunctions, so this is not just a theoretical concern. Where that information is available, it should be preserved alongside the reading. Where it is unknown, it should remain explicitly unknown. Printing nine fractional digits cannot supply that information.

That being said, other than the introduction throwing me into a “things programmers wrongly believe about time” rage, this is really well written.

I think there are some things worth addressing in the current RFC:

  1. Instant should throw when serialized to JSON. I understand the argument is that applications should choose their own representation, but the choice should be enforced rather than silently doing the wrong thing. Or provide an opaque serialization that can restore the Instant.
  2. DateTimeInterface is missing the toInstant() method, is that by design?
  3. The backward-compatibility section still says this RFC only introduces three new namespaced symbols. It should also cover the new methods on DateTime and DateTimeImmutable, including potential conflicts with existing subclass methods named toInstant().

— Rob

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:

PHP: 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

On Tue, Oct 6, 2026, at 4:17 AM, marc@mabe.berlin wrote:

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.

I agree with this. $foo->toBar() is a very common "type conversion" pattern, and it makes sense to use here as well.

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.

I would agree here as well.

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 understand the tidiness/purity argument here, and I am generally sympathetic to it. However, that always needs to be balanced against usability. Any incoming date/time information to the PHP process is likely to be either an ISO-8601 string or a unix timestamp int. If we want people to convert those into Instants for doing manipulation and comparison on (which I presume is the case), then that needs to be ergonomically easy to do. Having to go through multiple parser/converter objects (which some purist is likely to insist be injected from a container rather than instantiated directly; I have been known to be that purist) to get there is not ergonomic. So if that logic is split off, we would need to be very careful to ensure the result is ergonomically simple and readily copy-pasteable.

14. until() vs Duration::between()

Was Duration::between(Instant $a, Instant $b) considered? Calculating a
duration feels like Duration's responsibility to me.

This also seems reasonable, as a named constructor of Duration.

--Larry Garfield

PHP: rfc:time_instant_class

=========
Comment 4

The \Time\Clock interface conflicts with PSR-20 Clock interface, see
PSR-20: Clock - PHP-FIG . A
class can never implement both
interfaces at the same time. This will hinder adoption.

... snipped ...

(Tim Düsterhus)

The expectation is that you would use one of the two adapters to
translate between the PSR-20 clock and PHP’s native clock, depending
on which one is the source of truth in your application.

(Mirco Babin) There is a misconception. The application does not
choose the abstracted clock truth for the Composer installed
libraries. It only chooses the clock for its internal parts. Each
piece of software, internal application and each different external
library, chooses its own source of abstracted clock truth, like:

- \time()
- new DateTimeImmutable()
- PSR-20 \Psr\Clock\ClockInterface->now()
- PSR-11 ContainerInterface->get(\Psr\Clock\ClockInterface::class)
- laravel/framework exposes a \now() function.
- MySql 'SELECT SYSDATE();'
- MySql 'SELECT NOW();' (can be frozen with SET TIMESTAMP).
- \Carbon\CarbonImmutable::now()
- \Time\SystemClock::get()->now()
- \Time\Clock->now()
- PSR-11 ContainerInterface->get(\Time\Clock::class)
etcetera.

But in the end, every "abstracted clock truth" uses one singleton, one
source of truth, namely the singleton system clock.

(Mirco Babin)

But because the \Time\Clock->now() conflicts, as I have demonstrated,
with the \Psr\Clock\ClockInterface->now(), this one-time solution is
not possible. Now assume this PSR-20-application is a big one,
thousands of classes, millions line of code. This will become a
PSR-20-application maintenance disaster.

(Tim Düsterhus)

As Larry noted, changing from PSR-20 to the new date and time API
would be a breaking change. This kind of migration would also not
be limited to just the change in clocks. The Time\Instant, and the
modeling of the new date and time API in general are not a drop-in
replacement for the existing DateTimeImmutable API. A library that
migrates from PSR-20 to the native clock will want to consistently
use the date and time API to benefit from it. This will affect
internal calculations and possibly returned values (e.g. returning
an Instant where previously a DateTimeImmutable was returned). Just
replacing the clock and keeping everything else the same brings
little benefit. Changing the clock you pass in is the smallest part
of the migration effort.

(Mirco Babin) Every effort, however small, however little, that eases
this migration path for applications, should be provided.

Also because Packagist psr/clock - Packagist.org shows
*439 458 545* (439 million) installations of the PSR-20 clock. This is
an *impure number*, because it also includes CI/test installations,
and it also includes nesbot/carbon and laravel/framework
installations.

*I agree to disagree.*

Can the RFC be updated? Under rejected features can something be added
like:

A hybrid clock, both implementing \Time\Clock from this RFC and the
PSR-20 \Psr\Clock\ClockInterface is rejected. Because ...(reasons)...

===============
End of comments

I have no further comments.

Thank you Tim Düsterhus for considering my comments.
And good luck with the RFC.

Kind regards,
Mirco Babin

Hi

On 2026-09-30 21:48, Tim Düsterhus wrote:

=========
Comment 5

The RFC introduces a SystemClock class:

<?php

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 {}
}

?>

Why is SystemClock not a Singleton? Can there be multiple different
system clocks in practice? I believe a System Clock, with the emphasis
on "System" is a System Wide clock. This clock is not bound to PHP,
but to all processes running on the system, including Apache,
shell-scripts, NGINX and others.

Conceptually there is only one `SystemClock` and internally each `SystemClock` object accesses the same clock source provided by the operating system. But you are making a good point here: Replacing the constructor with a static `::get()` method or similar would make that explicit and would also allow for reducing memory usage when `SystemClock` objects are obtained at different places in the code (instead of being pulled from a dependency injection container). I'll put this on my list to discuss with Derick (and am happy to receive naming suggestions for such a static getter).

We have now made the `SystemClock` a singleton with a `SystemClock::get()` factory / getter. The constructor is private now.

Best regards
Tim Düsterhus