diff --git a/phpstan-baseline.php b/phpstan-baseline.php index 38a4bc211d44..311fb150235a 100644 --- a/phpstan-baseline.php +++ b/phpstan-baseline.php @@ -7483,18 +7483,6 @@ 'count' => 1, 'path' => __DIR__ . '/system/I18n/Time.php', ]; -$ignoreErrors[] = [ - // identifier: method.childReturnType - 'message' => '#^Return type \\(CodeIgniter\\\\I18n\\\\Time\\) of method CodeIgniter\\\\I18n\\\\Time\\:\\:setTimestamp\\(\\) should be covariant with return type \\(static\\(DateTimeImmutable\\)\\) of method DateTimeImmutable\\:\\:setTimestamp\\(\\)$#', - 'count' => 1, - 'path' => __DIR__ . '/system/I18n/Time.php', -]; -$ignoreErrors[] = [ - // identifier: method.childReturnType - 'message' => '#^Return type \\(CodeIgniter\\\\I18n\\\\Time\\) of method CodeIgniter\\\\I18n\\\\Time\\:\\:setTimezone\\(\\) should be covariant with return type \\(static\\(DateTimeImmutable\\)\\) of method DateTimeImmutable\\:\\:setTimezone\\(\\)$#', - 'count' => 1, - 'path' => __DIR__ . '/system/I18n/Time.php', -]; $ignoreErrors[] = [ // identifier: ternary.shortNotAllowed 'message' => '#^Short ternary operator is not allowed\\. Use null coalesce operator if applicable or consider using long ternary\\.$#', @@ -7507,18 +7495,6 @@ 'count' => 1, 'path' => __DIR__ . '/system/I18n/TimeLegacy.php', ]; -$ignoreErrors[] = [ - // identifier: method.childReturnType - 'message' => '#^Return type \\(CodeIgniter\\\\I18n\\\\TimeLegacy\\) of method CodeIgniter\\\\I18n\\\\TimeLegacy\\:\\:setTimestamp\\(\\) should be covariant with return type \\(static\\(DateTime\\)\\) of method DateTime\\:\\:setTimestamp\\(\\)$#', - 'count' => 1, - 'path' => __DIR__ . '/system/I18n/TimeLegacy.php', -]; -$ignoreErrors[] = [ - // identifier: method.childReturnType - 'message' => '#^Return type \\(CodeIgniter\\\\I18n\\\\TimeLegacy\\) of method CodeIgniter\\\\I18n\\\\TimeLegacy\\:\\:setTimezone\\(\\) should be covariant with return type \\(static\\(DateTime\\)\\) of method DateTime\\:\\:setTimezone\\(\\)$#', - 'count' => 1, - 'path' => __DIR__ . '/system/I18n/TimeLegacy.php', -]; $ignoreErrors[] = [ // identifier: ternary.shortNotAllowed 'message' => '#^Short ternary operator is not allowed\\. Use null coalesce operator if applicable or consider using long ternary\\.$#', diff --git a/system/DataCaster/Cast/TimestampCast.php b/system/DataCaster/Cast/TimestampCast.php index 52a4d88f9e46..f19d1a78810f 100644 --- a/system/DataCaster/Cast/TimestampCast.php +++ b/system/DataCaster/Cast/TimestampCast.php @@ -32,7 +32,7 @@ public static function get( self::invalidTypeValueError($value); } - return Time::createFromTimestamp((int) $value); + return Time::createFromTimestamp((int) $value, date_default_timezone_get()); } public static function set( diff --git a/system/Entity/Cast/DatetimeCast.php b/system/Entity/Cast/DatetimeCast.php index 2d01ad79b0ae..88b7b29267e0 100644 --- a/system/Entity/Cast/DatetimeCast.php +++ b/system/Entity/Cast/DatetimeCast.php @@ -40,7 +40,7 @@ public static function get($value, array $params = []) } if (is_numeric($value)) { - return Time::createFromTimestamp((int) $value); + return Time::createFromTimestamp((int) $value, date_default_timezone_get()); } if (is_string($value)) { diff --git a/system/I18n/Time.php b/system/I18n/Time.php index 906479470b17..255f879a90ec 100644 --- a/system/I18n/Time.php +++ b/system/I18n/Time.php @@ -39,6 +39,8 @@ * @property-read string $weekOfYear * @property-read string $year * + * @phpstan-consistent-constructor + * * @see \CodeIgniter\I18n\TimeTest */ class Time extends DateTimeImmutable implements Stringable diff --git a/system/I18n/TimeLegacy.php b/system/I18n/TimeLegacy.php index 403fced2108d..b62877ec40d6 100644 --- a/system/I18n/TimeLegacy.php +++ b/system/I18n/TimeLegacy.php @@ -39,6 +39,8 @@ * @property string $weekOfYear read-only * @property string $year read-only * + * @phpstan-consistent-constructor + * * @deprecated Use Time instead. * @see \CodeIgniter\I18n\TimeLegacyTest */ diff --git a/system/I18n/TimeTrait.php b/system/I18n/TimeTrait.php index 3257ad06189f..397d9a5a870d 100644 --- a/system/I18n/TimeTrait.php +++ b/system/I18n/TimeTrait.php @@ -78,7 +78,7 @@ public function __construct(?string $time = null, $timezone = null, ?string $loc $time ??= ''; // If a test instance has been provided, use it instead. - if ($time === '' && static::$testNow instanceof self) { + if ($time === '' && static::$testNow instanceof static) { if ($timezone !== null) { $testNow = static::$testNow->setTimezone($timezone); $time = $testNow->format('Y-m-d H:i:s.u'); @@ -108,13 +108,13 @@ public function __construct(?string $time = null, $timezone = null, ?string $loc * * @param DateTimeZone|string|null $timezone * - * @return self + * @return static * * @throws Exception */ public static function now($timezone = null, ?string $locale = null) { - return new self(null, $timezone, $locale); + return new static(null, $timezone, $locale); } /** @@ -125,13 +125,13 @@ public static function now($timezone = null, ?string $locale = null) * * @param DateTimeZone|string|null $timezone * - * @return self + * @return static * * @throws Exception */ public static function parse(string $datetime, $timezone = null, ?string $locale = null) { - return new self($datetime, $timezone, $locale); + return new static($datetime, $timezone, $locale); } /** @@ -139,13 +139,13 @@ public static function parse(string $datetime, $timezone = null, ?string $locale * * @param DateTimeZone|string|null $timezone * - * @return self + * @return static * * @throws Exception */ public static function today($timezone = null, ?string $locale = null) { - return new self(date('Y-m-d 00:00:00'), $timezone, $locale); + return new static(date('Y-m-d 00:00:00'), $timezone, $locale); } /** @@ -153,13 +153,13 @@ public static function today($timezone = null, ?string $locale = null) * * @param DateTimeZone|string|null $timezone * - * @return self + * @return static * * @throws Exception */ public static function yesterday($timezone = null, ?string $locale = null) { - return new self(date('Y-m-d 00:00:00', strtotime('-1 day')), $timezone, $locale); + return new static(date('Y-m-d 00:00:00', strtotime('-1 day')), $timezone, $locale); } /** @@ -167,13 +167,13 @@ public static function yesterday($timezone = null, ?string $locale = null) * * @param DateTimeZone|string|null $timezone * - * @return self + * @return static * * @throws Exception */ public static function tomorrow($timezone = null, ?string $locale = null) { - return new self(date('Y-m-d 00:00:00', strtotime('+1 day')), $timezone, $locale); + return new static(date('Y-m-d 00:00:00', strtotime('+1 day')), $timezone, $locale); } /** @@ -182,7 +182,7 @@ public static function tomorrow($timezone = null, ?string $locale = null) * * @param DateTimeZone|string|null $timezone * - * @return self + * @return static * * @throws Exception */ @@ -196,7 +196,7 @@ public static function createFromDate(?int $year = null, ?int $month = null, ?in * * @param DateTimeZone|string|null $timezone * - * @return self + * @return static * * @throws Exception */ @@ -210,7 +210,7 @@ public static function createFromTime(?int $hour = null, ?int $minutes = null, ? * * @param DateTimeZone|string|null $timezone * - * @return self + * @return static * * @throws Exception */ @@ -231,7 +231,7 @@ public static function create( $minutes ??= 0; $seconds ??= 0; - return new self(date('Y-m-d H:i:s', strtotime("{$year}-{$month}-{$day} {$hour}:{$minutes}:{$seconds}")), $timezone, $locale); + return new static(date('Y-m-d H:i:s', strtotime("{$year}-{$month}-{$day} {$hour}:{$minutes}:{$seconds}")), $timezone, $locale); } /** @@ -242,7 +242,7 @@ public static function create( * @param string $datetime * @param DateTimeZone|string|null $timezone * - * @return self + * @return static * * @throws Exception */ @@ -253,7 +253,7 @@ public static function createFromFormat($format, $datetime, $timezone = null) throw I18nException::forInvalidFormat($format); } - return new self($date->format('Y-m-d H:i:s.u'), $timezone); + return new static($date->format('Y-m-d H:i:s.u'), $timezone); } /** @@ -261,15 +261,13 @@ public static function createFromFormat($format, $datetime, $timezone = null) * * @param DateTimeZone|string|null $timezone * - * @return self - * * @throws Exception */ - public static function createFromTimestamp(int $timestamp, $timezone = null, ?string $locale = null) + public static function createFromTimestamp(float|int $timestamp, $timezone = null, ?string $locale = null): static { - $time = new self(gmdate('Y-m-d H:i:s', $timestamp), 'UTC', $locale); + $time = new static(sprintf('@%.6f', $timestamp), 'UTC', $locale); - $timezone ??= date_default_timezone_get(); + $timezone ??= 'UTC'; return $time->setTimezone($timezone); } @@ -277,7 +275,7 @@ public static function createFromTimestamp(int $timestamp, $timezone = null, ?st /** * Takes an instance of DateTimeInterface and returns an instance of Time with it's same values. * - * @return self + * @return static * * @throws Exception */ @@ -286,13 +284,13 @@ public static function createFromInstance(DateTimeInterface $dateTime, ?string $ $date = $dateTime->format('Y-m-d H:i:s.u'); $timezone = $dateTime->getTimezone(); - return new self($date, $timezone, $locale); + return new static($date, $timezone, $locale); } /** * Takes an instance of DateTime and returns an instance of Time with it's same values. * - * @return self + * @return static * * @throws Exception * @@ -302,7 +300,7 @@ public static function createFromInstance(DateTimeInterface $dateTime, ?string $ */ public static function instance(DateTime $dateTime, ?string $locale = null) { - return self::createFromInstance($dateTime, $locale); + return static::createFromInstance($dateTime, $locale); } /** @@ -347,9 +345,9 @@ public static function setTestNow($datetime = null, $timezone = null, ?string $l // Convert to a Time instance if (is_string($datetime)) { - $datetime = new self($datetime, $timezone, $locale); - } elseif ($datetime instanceof DateTimeInterface && ! $datetime instanceof self) { - $datetime = new self($datetime->format('Y-m-d H:i:s.u'), $timezone); + $datetime = new static($datetime, $timezone, $locale); + } elseif ($datetime instanceof DateTimeInterface && ! $datetime instanceof static) { + $datetime = new static($datetime->format('Y-m-d H:i:s.u'), $timezone); } static::$testNow = $datetime; @@ -477,7 +475,7 @@ public function getWeekOfYear(): string public function getAge() { // future dates have no age - return max(0, $this->difference(self::now())->getYears()); + return max(0, $this->difference(static::now())->getYears()); } /** @@ -534,7 +532,7 @@ public function getTimezoneName(): string * * @param int|string $value * - * @return self + * @return static * * @throws Exception */ @@ -548,7 +546,7 @@ public function setYear($value) * * @param int|string $value * - * @return self + * @return static * * @throws Exception */ @@ -570,7 +568,7 @@ public function setMonth($value) * * @param int|string $value * - * @return self + * @return static * * @throws Exception */ @@ -594,7 +592,7 @@ public function setDay($value) * * @param int|string $value * - * @return self + * @return static * * @throws Exception */ @@ -612,7 +610,7 @@ public function setHour($value) * * @param int|string $value * - * @return self + * @return static * * @throws Exception */ @@ -630,7 +628,7 @@ public function setMinute($value) * * @param int|string $value * - * @return self + * @return static * * @throws Exception */ @@ -648,7 +646,7 @@ public function setSecond($value) * * @param int $value * - * @return self + * @return static * * @throws Exception */ @@ -658,7 +656,7 @@ protected function setValue(string $name, $value) ${$name} = $value; - return self::create( + return static::create( (int) $year, (int) $month, (int) $day, @@ -675,7 +673,7 @@ protected function setValue(string $name, $value) * * @param DateTimeZone|string $timezone * - * @return self + * @return static * * @throws Exception */ @@ -684,7 +682,7 @@ public function setTimezone($timezone) { $timezone = $timezone instanceof DateTimeZone ? $timezone : new DateTimeZone($timezone); - return self::createFromInstance($this->toDateTime()->setTimezone($timezone), $this->locale); + return static::createFromInstance($this->toDateTime()->setTimezone($timezone), $this->locale); } /** @@ -692,7 +690,7 @@ public function setTimezone($timezone) * * @param int $timestamp * - * @return self + * @return static * * @throws Exception */ @@ -701,7 +699,7 @@ public function setTimestamp($timestamp) { $time = date('Y-m-d H:i:s', $timestamp); - return self::parse($time, $this->timezone, $this->locale); + return static::parse($time, $this->timezone, $this->locale); } // -------------------------------------------------------------------- @@ -1087,7 +1085,7 @@ public function difference($testTime, ?string $timezone = null) if (is_string($testTime)) { $timezone = ($timezone !== null) ? new DateTimeZone($timezone) : $this->timezone; $testTime = new DateTime($testTime, $timezone); - } elseif ($testTime instanceof self) { + } elseif ($testTime instanceof static) { $testTime = $testTime->toDateTime(); } @@ -1118,7 +1116,7 @@ public function difference($testTime, ?string $timezone = null) */ public function getUTCObject($time, ?string $timezone = null) { - if ($time instanceof self) { + if ($time instanceof static) { $time = $time->toDateTime(); } elseif (is_string($time)) { $timezone = $timezone ?: $this->timezone; diff --git a/tests/system/DataConverter/DataConverterTest.php b/tests/system/DataConverter/DataConverterTest.php index 53a6361bb0de..3383bbfdc102 100644 --- a/tests/system/DataConverter/DataConverterTest.php +++ b/tests/system/DataConverter/DataConverterTest.php @@ -398,6 +398,12 @@ public function testDateTimeConvertDataToDBWithFormat(): void public function testTimestampConvertDataFromDB(): void { + // Save the current timezone. + $tz = date_default_timezone_get(); + + // Change the timezone other than UTC. + date_default_timezone_set('Asia/Tokyo'); // +09:00 + $types = [ 'id' => 'int', 'date' => 'timestamp', @@ -412,10 +418,20 @@ public function testTimestampConvertDataFromDB(): void $this->assertInstanceOf(Time::class, $data['date']); $this->assertSame(1_700_285_831, $data['date']->getTimestamp()); + $this->assertSame('Asia/Tokyo', $data['date']->getTimezoneName()); + + // Restore timezone. + date_default_timezone_set($tz); } public function testTimestampConvertDataToDB(): void { + // Save the current timezone. + $tz = date_default_timezone_get(); + + // Change the timezone other than UTC. + date_default_timezone_set('Asia/Tokyo'); // +09:00 + $types = [ 'id' => 'int', 'date' => 'timestamp', @@ -429,6 +445,9 @@ public function testTimestampConvertDataToDB(): void $data = $converter->toDataSource($phpData); $this->assertSame(1_700_285_831, $data['date']); + + // Restore timezone. + date_default_timezone_set($tz); } public function testURIConvertDataFromDB(): void diff --git a/tests/system/Entity/EntityTest.php b/tests/system/Entity/EntityTest.php index 8dd7ceac31fa..b0ec5ebbda92 100644 --- a/tests/system/Entity/EntityTest.php +++ b/tests/system/Entity/EntityTest.php @@ -460,6 +460,27 @@ public function testCastDateTime(): void $this->assertSame('2017-03-12', $entity->eighth->format('Y-m-d')); } + public function testCastDateTimeWithTimestampTimezone(): void + { + // Save the current timezone. + $tz = date_default_timezone_get(); + + // Change the timezone other than UTC. + date_default_timezone_set('Asia/Tokyo'); // +09:00 + + $entity = $this->getCastEntity(); + + $entity->eighth = 1722988800; // 2024-08-07 00:00:00 UTC + + $this->assertInstanceOf(DateTimeInterface::class, $entity->eighth); + // The timezone is the default timezone, not UTC. + $this->assertSame('2024-08-07 09:00:00', $entity->eighth->format('Y-m-d H:i:s')); + $this->assertSame('Asia/Tokyo', $entity->eighth->getTimezoneName()); + + // Restore timezone. + date_default_timezone_set($tz); + } + public function testCastTimestamp(): void { $entity = $this->getCastEntity(); diff --git a/tests/system/I18n/TimeTest.php b/tests/system/I18n/TimeTest.php index 0e5bc733407c..10e38e2110a2 100644 --- a/tests/system/I18n/TimeTest.php +++ b/tests/system/I18n/TimeTest.php @@ -277,15 +277,26 @@ public function testCreateFromTimestamp(): void $timestamp = strtotime('2017-03-18 midnight'); + // The timezone will be UTC if you don't specify. $time = Time::createFromTimestamp($timestamp); - $this->assertSame('Asia/Tokyo', $time->getTimezone()->getName()); - $this->assertSame('2017-03-18 00:00:00', $time->format('Y-m-d H:i:s')); + $this->assertSame('UTC', $time->getTimezone()->getName()); + $this->assertSame('2017-03-17 15:00:00', $time->format('Y-m-d H:i:s')); // Restore timezone. date_default_timezone_set($tz); } + public function testCreateFromTimestampWithMicroseconds(): void + { + $timestamp = 1489762800.654321; + + // The timezone will be UTC if you don't specify. + $time = Time::createFromTimestamp($timestamp); + + $this->assertSame('2017-03-17 15:00:00.654321', $time->format('Y-m-d H:i:s.u')); + } + public function testCreateFromTimestampWithTimezone(): void { // Set the timezone temporarily to UTC to make sure the test timestamp is correct diff --git a/user_guide_src/source/changelogs/v4.6.0.rst b/user_guide_src/source/changelogs/v4.6.0.rst index 1cd78a3d51e2..51c43456ae28 100644 --- a/user_guide_src/source/changelogs/v4.6.0.rst +++ b/user_guide_src/source/changelogs/v4.6.0.rst @@ -57,7 +57,13 @@ Added check to prevent Auto-Discovery of Registrars from running twice. If it is executed twice, an exception will be thrown. See :ref:`upgrade-460-registrars-with-dirty-hack`. -.. _v460-interface-changes: +Time::createFromTimestamp() +--------------------------- + +``Time::createFromTimestamp()`` handles timezones differently. If ``$timezone`` +is not explicitly passed then the instance has timezone set to UTC unlike earlier +where the currently set default timezone was used. +See :ref:`Upgrading Guide ` for details. Time with Microseconds ---------------------- @@ -65,6 +71,8 @@ Time with Microseconds Fixed bugs that some methods in ``Time`` to lose microseconds have been fixed. See :ref:`Upgrading Guide ` for details. +.. _v460-interface-changes: + Interface Changes ================= @@ -89,6 +97,9 @@ Method Signature Changes changed. The ``RouteCollection`` typehint has been changed to ``RouteCollectionInterface``. - **View:** The return type of the ``renderSection()`` method has been changed to ``string``, and now the method does not call ``echo``. +- **Time:** The first parameter type of the ``createFromTimestamp()`` has been + changed from ``int`` to ``int|float``, and the return type ``static``` has been + added. Removed Type Definitions ------------------------ diff --git a/user_guide_src/source/installation/upgrade_460.rst b/user_guide_src/source/installation/upgrade_460.rst index 7078110b7068..5cbfd664bf92 100644 --- a/user_guide_src/source/installation/upgrade_460.rst +++ b/user_guide_src/source/installation/upgrade_460.rst @@ -29,6 +29,26 @@ See :ref:`ChangeLog ` for details. If you have code that catches these exceptions, change the exception classes. +.. _upgrade-460-time-create-from-timestamp: + +Time::createFromTimestamp() Timezone Change +=========================================== + +When you do not explicitly pass a timezone, now +:ref:`Time::createFromTimestamp() ` returns a Time +instance with **UTC**. In v4.4.6 to prior to v4.6.0, a Time instance with the +currently set default timezone was returned. + +This behavior change normalizes behavior with changes in PHP 8.4 which adds a +new ``DateTimeInterface::createFromTimestamp()`` method. + +If you want to keep the default timezone, you need to pass the timezone as the +second parameter:: + + use CodeIgniter\I18n\Time; + + $time = Time::createFromTimestamp(1501821586, date_default_timezone_get()); + .. _upgrade-460-time-keeps-microseconds: Time keeps Microseconds diff --git a/user_guide_src/source/libraries/time.rst b/user_guide_src/source/libraries/time.rst index b18e56350403..9fd986d31421 100644 --- a/user_guide_src/source/libraries/time.rst +++ b/user_guide_src/source/libraries/time.rst @@ -122,12 +122,18 @@ and returns a ``Time`` instance, instead of DateTimeImmutable: createFromTimestamp() ===================== -This method takes a UNIX timestamp and, optionally, the timezone and locale, to create a new Time instance: +This method takes a UNIX timestamp and, optionally, the timezone and locale, to +create a new Time instance: .. literalinclude:: time/012.php -.. note:: Due to a bug, prior to v4.4.6, this method returned a Time instance - in timezone UTC when you do not specify a timezone. +If you do not explicitly pass a timezone, it returns a Time instance with **UTC**. + +.. note:: We recommend to always call ``createFromTimestamp()`` with 2 parameters + (i.e. explicitly pass a timezone) unless using UTC as the default timezone. + +.. note:: In v4.4.6 to prior to v4.6.0, this method returned a Time instance + with the default timezone when you do not specify a timezone. createFromInstance() ==================== diff --git a/user_guide_src/source/models/model.rst b/user_guide_src/source/models/model.rst index c1cbf21a4dbd..9057001ae785 100644 --- a/user_guide_src/source/models/model.rst +++ b/user_guide_src/source/models/model.rst @@ -407,6 +407,12 @@ The datetime format is set in the ``dateFormat`` array of the .. note:: Prior to v4.6.0, you cannot use ``ms`` or ``us`` as a parameter. Because the second's fractional part of Time was lost due to bugs. +timestamp +--------- + +The timezone of the ``Time`` instance created will be the default timezone +(app's timezone), not UTC. + Custom Casting ==============