From 0957fca2c140519c9292bc4367dc41c0e35b1258 Mon Sep 17 00:00:00 2001 From: Stephan Wentz Date: Fri, 13 Mar 2026 13:34:50 +0100 Subject: [PATCH 1/4] fix: Add possibility to override default timezone in asDateTimeImmutable() --- src/TypeGuard.php | 13 ++++--- src/functions.php | 9 +++-- tests/AsDateTimeImmutableTest.php | 57 +++++++++++++++++++++++++++++++ 3 files changed, 71 insertions(+), 8 deletions(-) diff --git a/src/TypeGuard.php b/src/TypeGuard.php index c8dacd6..f582114 100644 --- a/src/TypeGuard.php +++ b/src/TypeGuard.php @@ -118,19 +118,22 @@ public function asString(mixed $value): string|null return (string) $value; } - /** @return DateTimeImmutable */ - public function asDateTimeImmutable(mixed $value): DateTimeImmutable|null + public function asDateTimeImmutable(mixed $value, DateTimeZone|string|null $timeZone = null): DateTimeImmutable|null { if ($value === null) { return null; } + $tz = $timeZone instanceof DateTimeZone + ? $timeZone + : ($timeZone !== null ? new DateTimeZone($timeZone) : $this->timeZone()); + if ($value instanceof DateTimeImmutable) { - if ($value->getTimezone()->getName() === $this->timeZone()->getName()) { + if ($value->getTimezone()->getName() === $tz->getName()) { return $value; } - return $value->setTimezone($this->timeZone()); + return $value->setTimezone($tz); } if ($value instanceof Stringable) { @@ -141,7 +144,7 @@ public function asDateTimeImmutable(mixed $value): DateTimeImmutable|null throw NotConvertable::toDateTime($value); } - return new DateTimeImmutable(asString($value), $this->timeZone()); + return new DateTimeImmutable(asString($value), $tz); } /** @return ($value is null ? null : string) */ diff --git a/src/functions.php b/src/functions.php index e82f5fb..847c049 100644 --- a/src/functions.php +++ b/src/functions.php @@ -5,6 +5,7 @@ namespace Plook\TypeGuard; use DateTimeImmutable; +use DateTimeZone; use function function_exists; @@ -49,9 +50,11 @@ function asString(mixed $value): string|null if (!function_exists('\Plook\TypeGuard\asDateTimeImmutable')) { // @codeCoverageIgnore /** @return ($value is null ? null : DateTimeImmutable) */ - function asDateTimeImmutable(mixed $value): DateTimeImmutable|null - { - return TypeGuard::instance()->asDateTimeImmutable($value); + function asDateTimeImmutable( + mixed $value, + DateTimeZone|string|null $timeZone = null, + ): DateTimeImmutable|null { + return TypeGuard::instance()->asDateTimeImmutable($value, $timeZone); } } diff --git a/tests/AsDateTimeImmutableTest.php b/tests/AsDateTimeImmutableTest.php index 531959e..65dfacf 100644 --- a/tests/AsDateTimeImmutableTest.php +++ b/tests/AsDateTimeImmutableTest.php @@ -94,6 +94,63 @@ public function testDoesNotTouchNull(): void self::assertNull(asDateTimeImmutable(null)); } + public function testTimezoneParameterOverridesDefaultWithDateTimeZone(): void + { + $result = asDateTimeImmutable('2010-09-08 07:06:05', new DateTimeZone('Australia/Adelaide')); + + self::assertInstanceOf(DateTimeImmutable::class, $result); + self::assertSame('2010-09-08T07:06:05+09:30', $result->format('c')); + } + + public function testTimezoneParameterOverridesDefaultWithString(): void + { + $result = asDateTimeImmutable('2010-09-08 07:06:05', 'Australia/Adelaide'); + + self::assertInstanceOf(DateTimeImmutable::class, $result); + self::assertSame('2010-09-08T07:06:05+09:30', $result->format('c')); + } + + public function testTimezoneParameterOverridesChangedDefault(): void + { + TypeGuard::instance()->timeZone('Europe/Berlin'); + + $result = asDateTimeImmutable('2010-09-08 07:06:05', 'Australia/Adelaide'); + + self::assertInstanceOf(DateTimeImmutable::class, $result); + self::assertSame('2010-09-08T07:06:05+09:30', $result->format('c')); + } + + public function testTimezoneParameterConvertsSameDateTimeImmutable(): void + { + $input = new DateTimeImmutable('2010-09-08T07:06:05', new DateTimeZone('Australia/Adelaide')); + + $result = asDateTimeImmutable($input, 'Australia/Adelaide'); + + self::assertInstanceOf(DateTimeImmutable::class, $result); + self::assertSame('Australia/Adelaide', $result->getTimezone()->getName()); + self::assertSame('2010-09-08T07:06:05+09:30', $result->format('c')); + } + + public function testTimezoneParameterConvertsDifferentDateTimeImmutable(): void + { + $input = new DateTimeImmutable('2010-09-08T07:06:05+00:00'); + + $result = asDateTimeImmutable($input, new DateTimeZone('Australia/Adelaide')); + + self::assertInstanceOf(DateTimeImmutable::class, $result); + self::assertSame('2010-09-08T16:36:05+09:30', $result->format('c')); + } + + public function testNullTimezoneParameterUsesDefault(): void + { + TypeGuard::instance()->timeZone('Australia/Adelaide'); + + $result = asDateTimeImmutable('2010-09-08 07:06:05', null); + + self::assertInstanceOf(DateTimeImmutable::class, $result); + self::assertSame('2010-09-08T07:06:05+09:30', $result->format('c')); + } + public function testOnlyScalarsAreConvertable(): void { $this->expectException(NotConvertable::class); From 164017e6cbb70bd04f0d72dbffd55a0111fcc075 Mon Sep 17 00:00:00 2001 From: Phillip Look Date: Fri, 13 Mar 2026 17:01:28 +0100 Subject: [PATCH 2/4] chore: Update dev dependencies --- composer.json | 18 +++++++++--------- 1 file changed, 9 insertions(+), 9 deletions(-) diff --git a/composer.json b/composer.json index f8f6cc6..6182971 100644 --- a/composer.json +++ b/composer.json @@ -15,15 +15,15 @@ "require-dev": { "brainbits/phpcs-standard": "^8.0.0", "brainbits/phpstan-rules": "^4.0.0", - "ergebnis/composer-normalize": "^2.47", - "ergebnis/phpstan-rules": "^2.11.0", - "phpstan/phpstan": "^2.1.22", - "phpstan/phpstan-phpunit": "^2.0.7", - "phpstan/phpstan-strict-rules": "^2.0.6", - "phpunit/phpunit": "^12.3.6", - "rector/rector": "^2.1.4", - "squizlabs/php_codesniffer": "^3.13.2", - "thecodingmachine/phpstan-safe-rule": "^1.4.1" + "ergebnis/composer-normalize": "^2.50", + "ergebnis/phpstan-rules": "^2.13.1", + "phpstan/phpstan": "^2.1.40", + "phpstan/phpstan-phpunit": "^2.0.16", + "phpstan/phpstan-strict-rules": "^2.0.10", + "phpunit/phpunit": "^12.5.14", + "rector/rector": "^2.3.8", + "squizlabs/php_codesniffer": "^3.13.5", + "thecodingmachine/phpstan-safe-rule": "^1.4.3" }, "autoload": { "psr-4": { From 1b176b4551e9dda44fbc8f89e966d9c1fd3f6ed6 Mon Sep 17 00:00:00 2001 From: Phillip Look Date: Fri, 13 Mar 2026 17:02:53 +0100 Subject: [PATCH 3/4] feat: Provide time zone functions --- src/NotConvertable.php | 5 +++ src/TypeGuard.php | 22 ++++++++++++ src/functions.php | 9 +++++ tests/AsDateTimeZoneTest.php | 67 ++++++++++++++++++++++++++++++++++++ 4 files changed, 103 insertions(+) create mode 100644 tests/AsDateTimeZoneTest.php diff --git a/src/NotConvertable.php b/src/NotConvertable.php index b3ab85f..d3dd696 100644 --- a/src/NotConvertable.php +++ b/src/NotConvertable.php @@ -38,6 +38,11 @@ public static function toDateTime(mixed $value): self return self::withMessage(sprintf('%s is not convertable to date time object', get_debug_type($value))); } + public static function toDateTimeZone(mixed $value): self + { + return self::withMessage(sprintf('%s is not convertable to date time zone', get_debug_type($value))); + } + private static function withMessage(string $message): self { $me = new self($message); diff --git a/src/TypeGuard.php b/src/TypeGuard.php index f582114..da71c36 100644 --- a/src/TypeGuard.php +++ b/src/TypeGuard.php @@ -147,6 +147,28 @@ public function asDateTimeImmutable(mixed $value, DateTimeZone|string|null $time return new DateTimeImmutable(asString($value), $tz); } + /** @return ($value is null ? null : DateTimeZone) */ + public function asDateTimeZone(mixed $value): DateTimeZone|null + { + if ($value === null) { + return null; + } + + if ($value instanceof DateTimeZone) { + return $value; + } + + if ($value instanceof Stringable) { + $value = (string) $value; + } + + if (!is_string($value)) { + throw NotConvertable::toDateTimeZone($value); + } + + return new DateTimeZone($value); + } + /** @return ($value is null ? null : string) */ public function asDateTimeString(mixed $value): string|null { diff --git a/src/functions.php b/src/functions.php index 847c049..39ef983 100644 --- a/src/functions.php +++ b/src/functions.php @@ -58,6 +58,15 @@ function asDateTimeImmutable( } } +if (!function_exists('\Plook\TypeGuard\asDateTimeZone')) { // @codeCoverageIgnore + + /** @return ($value is null ? null : DateTimeZone) */ + function asDateTimeZone(mixed $value): DateTimeZone|null + { + return TypeGuard::instance()->asDateTimeZone($value); + } +} + if (!function_exists('\Plook\TypeGuard\asDateTimeString')) { // @codeCoverageIgnore /** @return ($value is null ? null : string) */ diff --git a/tests/AsDateTimeZoneTest.php b/tests/AsDateTimeZoneTest.php new file mode 100644 index 0000000..100bd44 --- /dev/null +++ b/tests/AsDateTimeZoneTest.php @@ -0,0 +1,67 @@ +getName()); + } + + public function testConvertsStringables(): void + { + $dateTimeZone = asDateTimeZone(new StringableString('Australia/Adelaide')); + + self::assertInstanceOf(DateTimeZone::class, $dateTimeZone); + self::assertSame('Australia/Adelaide', $dateTimeZone->getName()); + } + + public function testReturnsSameDateTimeZone(): void + { + $dateTimeZone = new DateTimeZone('Europe/Berlin'); + + $result = asDateTimeZone($dateTimeZone); + + self::assertSame($dateTimeZone, $result); + } + + public function testDoesNotTouchNull(): void + { + self::assertNull(asDateTimeZone(null)); + } + + public function testOnlyStringsAreConvertable(): void + { + $this->expectException(NotConvertable::class); + $this->expectExceptionMessageMatches( + sprintf( + '/Closure is not convertable to date time zone in %s:%s/', + basename(__FILE__), + __LINE__ + 4, + ), + ); + + asDateTimeZone(static fn (): null => null); + } +} From 1ebea0d457fc6d9236af57fcda3d111f6ef24113 Mon Sep 17 00:00:00 2001 From: Phillip Look Date: Fri, 13 Mar 2026 17:03:41 +0100 Subject: [PATCH 4/4] fix: Convert to timezone with own utility function --- src/TypeGuard.php | 10 ++++------ tests/AsDateTimeImmutableTest.php | 2 +- 2 files changed, 5 insertions(+), 7 deletions(-) diff --git a/src/TypeGuard.php b/src/TypeGuard.php index da71c36..e727b09 100644 --- a/src/TypeGuard.php +++ b/src/TypeGuard.php @@ -124,16 +124,14 @@ public function asDateTimeImmutable(mixed $value, DateTimeZone|string|null $time return null; } - $tz = $timeZone instanceof DateTimeZone - ? $timeZone - : ($timeZone !== null ? new DateTimeZone($timeZone) : $this->timeZone()); + $timeZone = $this->asDateTimeZone($timeZone) ?? $this->timeZone(); if ($value instanceof DateTimeImmutable) { - if ($value->getTimezone()->getName() === $tz->getName()) { + if ($value->getTimezone()->getName() === $timeZone->getName()) { return $value; } - return $value->setTimezone($tz); + return $value->setTimezone($timeZone); } if ($value instanceof Stringable) { @@ -144,7 +142,7 @@ public function asDateTimeImmutable(mixed $value, DateTimeZone|string|null $time throw NotConvertable::toDateTime($value); } - return new DateTimeImmutable(asString($value), $tz); + return new DateTimeImmutable(asString($value), $timeZone); } /** @return ($value is null ? null : DateTimeZone) */ diff --git a/tests/AsDateTimeImmutableTest.php b/tests/AsDateTimeImmutableTest.php index 65dfacf..4f1f251 100644 --- a/tests/AsDateTimeImmutableTest.php +++ b/tests/AsDateTimeImmutableTest.php @@ -145,7 +145,7 @@ public function testNullTimezoneParameterUsesDefault(): void { TypeGuard::instance()->timeZone('Australia/Adelaide'); - $result = asDateTimeImmutable('2010-09-08 07:06:05', null); + $result = asDateTimeImmutable('2010-09-08 07:06:05'); self::assertInstanceOf(DateTimeImmutable::class, $result); self::assertSame('2010-09-08T07:06:05+09:30', $result->format('c'));