Repository navigation
Description
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 nullSame result through the cache:
cache()->save('t', Time::now(), 60);
cache('t')->difference(Time::now()); // with a serializing handler such as MemcachedThe 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
LeMyst commentedon Oct 30, 2026
Thanks