Hi Tim,

On 03.07.26 19:14, Tim Düsterhus wrote:
Hi

On 7/3/26 18:29, Pierre Joye wrote:
I like that new Duration, good new APIs. I was following the
discussion and one thing keeps coming to my mind as an API mistake:

   public static function fromSeconds(int $seconds, int $nanoseconds = 0)

Let me break down my thinking:

fromSeconds implies I'm setting a duration in seconds. But there are
two arguments — the second is nanoseconds, and milliseconds isn't
representable at all. The name promises one unit and the signature
quietly provides another.

Please see https://news-web.php.net/php.internals/131419; and the fourth paragraph in the “design considerations”.

The $nanoseconds argument in the `fromSeconds()` constructor is intentionally limited to “the number of nanoseconds in a second” which means that the constructor can be reasoned about taking (fractional) seconds - the base unit of Durations - as a fixed point decimal. I do not consider this a break in the promise.

That's correct, but I don't think it applies here. The fromSeconds
issue is a naming defect regardless of language: the method name
promises "seconds" and the signature quietly also takes nanoseconds.

As mentioned above, the $nanoseconds parameter is intentionally limited, making the constructor consistent with the other constructors in that the “unit that is mentioned in the name” may overflow into the larger units to allow construction with the desired precision in a way that makes sense for the given use case.

Extending this pattern to e.g. `fromMinutes()` would result in `fromMinutes($minutes, $seconds = 0)` with $seconds being limited to 59.

However in the other direction, `fromMilliseconds($milliseconds, $microseconds = 0)`, with $microseconds <= 999 would feel weird, because one wouldn’t say 5 milliseconds and 500 microseconds, when they can just move the comma and say 5500 microseconds.

The seconds boundary is where the units shift from a “metric” factor of 1000 to a factor of 60. Thus the special handling.

PHP already supports named arguments, giving us the same clarity as
Temporal object literal without borrowing any JS syntax:

   Duration::from(seconds: -30, nanoseconds: -500);
   Duration::from(seconds: -30, nanoseconds: 500); // error
   Duration::from(seconds: 30, nanoseconds: 500).negate();

A single ::from() with named arguments would be unambiguous, easy to
remember, and remove the need for the whole family of from<Unit>

A design goal of the constructors was that they are injective functions, i.e. there must be at most one way of creating a Duration with a specific length from any given constructor. This is in order to make it easy to reason about the resulting value, without needing to perform (mental) calculations.

i.e. Duration::from(seconds: 1, milliseconds: 1500) would misleadingly look “between 1 and 2 seconds” at a glance. But when each of the “subunits” are restricted in magnitude, it would be very inconvenient to construct values with the appropriate precision, because it would require splitting inputs across the different parameters.

The `fromSeconds` constructor breaks the flow of all other constructors as this is not only about seconds. Also, this directly exposes how it's handled internally to the API which makes it hard to extend later on (maybe we want to support picoseconds on 10 years).

PHP should make things simpler - not more complex. There are plenty of different duration systems out there a such values could come from as input to a PHP application. The current Duration API forces you to manually correctly calculate the value before passing it the constructor.

E.g.:
* time measurement using microtime(true)
* using hrtime(true) returns float on 32bit
* using JS (even Temporal) exposing integer values as "number" often ends up as float in PHP

I totally understand that you want to keep it simple for now but the $nanoseconds second argument makes it impossible to change in the future.

I like the idea of a general "from" constructor - but not restricted to seconds + nanos but ALL supported units

public static function from(
    int $hours = 0,
    int $minutes = 0,
    int $seconds = 0,
    int $milliseconds = 0,
    int $microseconds = 0,
    int $nanoseconds = 0,
) : Duration

This makes it much more flexible, simpler to work with and extensible if we want to increase the precision any time in the future. It's intentional to start with the most uncommon duration unit of $hours. That forces you to use named arguments which reads nicely and handling floats or not can be discussed at a later time.

Duration::from(seconds: 100, microseconds: 500);
Duration::from(hours: 5, minutes: 30);

$seconds = 1;
$millis = 1_000;
$micros = 1_000_000;
$nanos = 1_000_000_000;
Duration::from(seconds: $seconds, milliseconds: $millis, microseconds: $micros, nanoseconds: $nanos); // 4 seconds - yes please

> ... there must be at most one way of creating a Duration with a specific length from any given constructor. This is in order to make it easy to reason about the resulting value, without needing to perform (mental) calculations.

It makes it simple to reason about for you as implementing it - it forces the burden to the one using the API.

Regards,
Marc

Reply via email to