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": { 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 c8dacd6..e727b09 100644 --- a/src/TypeGuard.php +++ b/src/TypeGuard.php @@ -118,19 +118,20 @@ 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; } + $timeZone = $this->asDateTimeZone($timeZone) ?? $this->timeZone(); + if ($value instanceof DateTimeImmutable) { - if ($value->getTimezone()->getName() === $this->timeZone()->getName()) { + if ($value->getTimezone()->getName() === $timeZone->getName()) { return $value; } - return $value->setTimezone($this->timeZone()); + return $value->setTimezone($timeZone); } if ($value instanceof Stringable) { @@ -141,7 +142,29 @@ public function asDateTimeImmutable(mixed $value): DateTimeImmutable|null throw NotConvertable::toDateTime($value); } - return new DateTimeImmutable(asString($value), $this->timeZone()); + return new DateTimeImmutable(asString($value), $timeZone); + } + + /** @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) */ diff --git a/src/functions.php b/src/functions.php index e82f5fb..39ef983 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,20 @@ 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 + function asDateTimeImmutable( + mixed $value, + DateTimeZone|string|null $timeZone = null, + ): DateTimeImmutable|null { + return TypeGuard::instance()->asDateTimeImmutable($value, $timeZone); + } +} + +if (!function_exists('\Plook\TypeGuard\asDateTimeZone')) { // @codeCoverageIgnore + + /** @return ($value is null ? null : DateTimeZone) */ + function asDateTimeZone(mixed $value): DateTimeZone|null { - return TypeGuard::instance()->asDateTimeImmutable($value); + return TypeGuard::instance()->asDateTimeZone($value); } } diff --git a/tests/AsDateTimeImmutableTest.php b/tests/AsDateTimeImmutableTest.php index 531959e..4f1f251 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'); + + 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); 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); + } +}