Skip to content

Bug: Time loses its timezone and locale after unserialize() since 4.7.5 #10681

Closed

Description

@LeMyst

PHP Version

8.4

CodeIgniter4 Version

4.7.5 (also present on develop and 4.8)

CodeIgniter4 Installation Method

Manual (zip or tar.gz)

Which operating systems have you tested for this bug?

Linux, Windows

Which server did you use?

cli

Environment

production

Database

N/A

What happened?

Since 4.7.5, a Time instance that goes through serialize() / unserialize() (for example, any Time stored with cache()->save() using a serializing handler such as Memcached) comes back with its $timezone and $locale properties set to null.

Any method that relies on $this->timezone then fails. difference() throws:

Error: Call to a member function getOffset() on null
in SYSTEMPATH/I18n/TimeTrait.php on line 1126.

getTimezoneName() fails the same way (Call to a member function getName() on null), and toLocalizedString() silently falls back to the default locale.

This looks like a regression from #9957, which replaced __wakeup() with __unserialize():

public function __unserialize(array $data): void
{
    $date     = $data['date'] ?? null;
    $timezone = $data['timezone'] ?? null;
    assert(is_string($date) && is_string($timezone));

    parent::__construct($date, new DateTimeZone($timezone));
}

Before #9957, the inherited DateTimeImmutable::__unserialize() restored the subclass's own properties ($timezone, $locale, $toStringFormat) from the serialized data. The new override replaces it without restoring them. It only calls the DateTimeImmutable constructor, which never sets the trait's properties, so they stay at their defaults (null, null, and the default format).

Steps to Reproduce

use CodeIgniter\I18n\Time;

$time     = Time::parse('2024-03-01 10:00:00', 'Europe/Paris', 'fr_FR');
$restored = unserialize(serialize($time));

$restored->difference(Time::now()); // Error: Call to a member function getOffset() on null

Same result through the cache:

cache()->save('t', Time::now(), 60);
cache('t')->difference(Time::now()); // with a serializing handler such as Memcached

The same code works on 4.7.0.

Expected Output

The unserialized instance should be equivalent to the original: same timezone, locale and __toString() format, and difference(), getTimezoneName() and toLocalizedString() should work as before.

Anything else?

DateTimeImmutable::__serialize() already exports the trait's protected properties under their mangled names. For the example above, the serialized data is:

"date"                    => '2024-03-01 10:00:00.000000'
"timezone_type"           => 3
"timezone"                => 'Europe/Paris'
"\0*\0timezone"           => DateTimeZone
"\0*\0locale"             => 'fr_FR'
"\0*\0toStringFormat"     => 'yyyy-MM-dd HH:mm:ss'

So __unserialize() can restore them. A possible fix:

public function __unserialize(array $data): void
{
    $date     = $data['date'] ?? null;
    $timezone = $data['timezone'] ?? null;
    assert(is_string($date) && is_string($timezone));

    $this->timezone       = new DateTimeZone($timezone);
    $this->locale         = $data["\0*\0locale"] ?? Locale::getDefault();
    $this->toStringFormat = $data["\0*\0toStringFormat"] ?? $this->toStringFormat;

    parent::__construct($date, $this->timezone);
}

The fallbacks keep payloads serialized without those keys working. A regression test could round-trip a Time through serialize() / unserialize(), then assert on getTimezoneName(), difference() and toLocalizedString().

I haven't opened a PR, so feel free to take this as a starting point.

Activity

  1. added
    bugVerified issues on the current code behavior or pull requests that will fix them
    on Oct 29, 2026
  2. LeMyst commented on Oct 30, 2026

    @LeMyst
    ContributorAuthor

    Thanks

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    bugVerified issues on the current code behavior or pull requests that will fix them

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions