fix: emit full seconds in Instant and ZonedDateTime toISOString (RFC 3339)
Instant::toISOString() and ZonedDateTime::toISOString() emitted a compact time that dropped zero seconds (e.g. 2023-11-14T22:14Z), which is valid ISO 8601 but not valid RFC 3339. Section 5.6 of RFC 3339 requires the full HH:MM:SS time even when the seconds are zero. These types are meant for interoperability, so they must always emit RFC 3339 compliant output. Build the time part of ZonedDateTime::toISOString() explicitly with a mandatory seconds component instead of delegating to LocalTime's compact formatter. Instant delegates to ZonedDateTime, so it is fixed transitively; jsonSerialize() and __toString() route through toISOString(). LocalTime's documented compact format is intentional and left unchanged. This changes the serialized string representation and is therefore a BC break; fixtures that pinned the compact output are updated accordingly.
This commit is contained in:
1 parent
d37085b46a
commit
491e7ada29
5 files changed
+67
-36
No files matched your search
+23
-1
@@ -17,6 +17,10 @@ use Stringable;
|
||||
|
||||
use function assert;
|
||||
use function intdiv;
|
||||
use function rtrim;
|
||||
use function str_pad;
|
||||
|
||||
use const STR_PAD_LEFT;
|
||||
|
||||
/**
|
||||
* A date-time with a time-zone in the ISO-8601 calendar system.
|
||||
@@ -735,11 +739,29 @@ final readonly class ZonedDateTime implements JsonSerializable, Stringable
|
||||
/**
|
||||
* Returns the ISO 8601 representation of this zoned date time.
|
||||
*
|
||||
* Unlike {@see LocalTime::toISOString()}, the time part always includes the
|
||||
* seconds, as RFC 3339 section 5.6 requires, even when they are zero.
|
||||
*
|
||||
* @return non-empty-string
|
||||
*/
|
||||
public function toISOString(): string
|
||||
{
|
||||
$string = $this->localDateTime . $this->timeZoneOffset;
|
||||
$time = $this->localDateTime->getTime();
|
||||
|
||||
$hour = $time->getHour();
|
||||
$minute = $time->getMinute();
|
||||
$second = $time->getSecond();
|
||||
$nano = $time->getNano();
|
||||
|
||||
$string = $this->localDateTime->getDate()
|
||||
. 'T'
|
||||
. ($hour < 10 ? '0' . $hour : $hour)
|
||||
. ':'
|
||||
. ($minute < 10 ? '0' . $minute : $minute)
|
||||
. ':'
|
||||
. ($second < 10 ? '0' . $second : $second)
|
||||
. ($nano !== 0 ? '.' . rtrim(str_pad((string) $nano, 9, '0', STR_PAD_LEFT), '0') : '')
|
||||
. $this->timeZoneOffset;
|
||||
|
||||
if ($this->timeZone instanceof TimeZoneRegion) {
|
||||
$string .= '[' . $this->timeZone . ']';
|
||||
|
||||
Reference in new issue
Block a user