From 20bd93c4d416e6952e363a2160525762a6c74cbb Mon Sep 17 00:00:00 2001 From: David Grudl Date: Mon, 24 Oct 2022 21:52:55 +0200 Subject: [PATCH] added Cast --- src/Utils/Cast.php | 194 ++++++++++++++++++++++++++++ src/Utils/Helpers.php | 4 +- tests/Utils/Cast.falseToNull().phpt | 17 +++ tests/Utils/Cast.methods.phpt | 135 +++++++++++++++++++ tests/Utils/Cast.to.phpt | 30 +++++ 5 files changed, 377 insertions(+), 3 deletions(-) create mode 100644 src/Utils/Cast.php create mode 100644 tests/Utils/Cast.falseToNull().phpt create mode 100644 tests/Utils/Cast.methods.phpt create mode 100644 tests/Utils/Cast.to.phpt diff --git a/src/Utils/Cast.php b/src/Utils/Cast.php new file mode 100644 index 000000000..b55d8275a --- /dev/null +++ b/src/Utils/Cast.php @@ -0,0 +1,194 @@ + self::toBool($value), + 'int' => self::toInt($value), + 'float' => self::toFloat($value), + 'string' => self::toString($value), + 'array' => self::toArray($value), + default => throw new TypeError("Unsupported type '$type'."), + }; + } + + + /** + * Converts a value to a specified type or returns null if the value is null. + * Supported types: bool, int, float, string, array. + * @throws TypeError if the value cannot be converted + */ + public static function toOrNull(mixed $value, string $type): mixed + { + return $value === null ? null : self::to($value, $type); + } + + + /** + * Converts a value to a boolean. + * @throws TypeError if the value cannot be converted + */ + public static function toBool(mixed $value): bool + { + return match (true) { + is_bool($value) => $value, + is_int($value) => $value !== 0, + is_float($value) => $value !== 0.0, + is_string($value) => $value !== '' && $value !== '0', + $value === null => false, + default => throw new TypeError('Cannot cast ' . get_debug_type($value) . ' to bool.'), + }; + } + + + /** + * Converts a value to an integer. + * @throws TypeError if the value cannot be converted + */ + public static function toInt(mixed $value): int + { + return match (true) { + is_bool($value) => (int) $value, + is_int($value) => $value, + is_float($value) => $value === (float) ($tmp = (int) $value) + ? $tmp + : throw new TypeError('Cannot cast ' . self::toString($value) . ' to int.'), + is_string($value) => preg_match('~^-?\d+(\.0*)?$~D', $value) + ? (int) $value + : throw new TypeError("Cannot cast '$value' to int."), + $value === null => 0, + default => throw new TypeError('Cannot cast ' . get_debug_type($value) . ' to int.'), + }; + } + + + /** + * Converts a value to a float. + * @throws TypeError if the value cannot be converted + */ + public static function toFloat(mixed $value): float + { + return match (true) { + is_bool($value) => $value ? 1.0 : 0.0, + is_int($value) => (float) $value, + is_float($value) => $value, + is_string($value) => preg_match('~^-?\d+(\.\d*)?$~D', $value) + ? (float) $value + : throw new TypeError("Cannot cast '$value' to float."), + $value === null => 0.0, + default => throw new TypeError('Cannot cast ' . get_debug_type($value) . ' to float.'), + }; + } + + + /** + * Converts a value to a string. + * @throws TypeError if the value cannot be converted + */ + public static function toString(mixed $value): string + { + return match (true) { + is_bool($value) => $value ? '1' : '0', + is_int($value) => (string) $value, + is_float($value) => str_contains($tmp = (string) $value, '.') ? $tmp : $tmp . '.0', + is_string($value) => $value, + $value === null => '', + default => throw new TypeError('Cannot cast ' . get_debug_type($value) . ' to string.'), + }; + } + + + /** + * Ensures the value is an array. Wraps the value in an array if it is not already one. + */ + public static function toArray(mixed $value): array + { + return match (true) { + is_array($value) => $value, + $value === null => [], + default => [$value], + }; + } + + + /** + * Converts a value to a boolean or returns null if the value is null. + * @throws TypeError if the value cannot be converted + */ + public static function toBoolOrNull(mixed $value): ?bool + { + return $value === null ? null : self::toBool($value); + } + + + /** + * Converts a value to an integer or returns null if the value is null. + * @throws TypeError if the value cannot be converted + */ + public static function toIntOrNull(mixed $value): ?int + { + return $value === null ? null : self::toInt($value); + } + + + /** + * Converts a value to a float or returns null if the value is null. + * @throws TypeError if the value cannot be converted + */ + public static function toFloatOrNull(mixed $value): ?float + { + return $value === null ? null : self::toFloat($value); + } + + + /** + * Converts a value to a string or returns null if the value is null. + * @throws TypeError if the value cannot be converted + */ + public static function toStringOrNull(mixed $value): ?string + { + return $value === null ? null : self::toString($value); + } + + + /** + * Converts a value to an array or returns null if the value is null. + */ + public static function toArrayOrNull(mixed $value): ?array + { + return $value === null ? null : self::toArray($value); + } + + + /** + * Converts false to null, does not change other values. + */ + public static function falseToNull(mixed $value): mixed + { + return $value === false ? null : $value; + } +} diff --git a/src/Utils/Helpers.php b/src/Utils/Helpers.php index b3586c16c..abfe76378 100644 --- a/src/Utils/Helpers.php +++ b/src/Utils/Helpers.php @@ -43,9 +43,7 @@ public static function getLastError(): string } - /** - * Converts false to null, does not change other values. - */ + /** @deprecated use Cast::falseToNull() */ public static function falseToNull(mixed $value): mixed { return $value === false ? null : $value; diff --git a/tests/Utils/Cast.falseToNull().phpt b/tests/Utils/Cast.falseToNull().phpt new file mode 100644 index 000000000..9a8e6f9c4 --- /dev/null +++ b/tests/Utils/Cast.falseToNull().phpt @@ -0,0 +1,17 @@ + Cast::toBool([]), + TypeError::class, + 'Cannot cast array to bool.', +); + + +// int +Assert::same(0, Cast::toInt(null)); +Assert::same(0, Cast::toInt(false)); +Assert::same(1, Cast::toInt(true)); +Assert::same(0, Cast::toInt(0)); +Assert::same(1, Cast::toInt(1)); +Assert::exception( + fn() => Cast::toInt(PHP_INT_MAX + 1), + TypeError::class, + 'Cannot cast 9.2233720368548E+18 to int.', +); +Assert::same(0, Cast::toInt(0.0)); +Assert::same(1, Cast::toInt(1.0)); +Assert::exception( + fn() => Cast::toInt(0.1), + TypeError::class, + 'Cannot cast 0.1 to int.', +); +Assert::exception( + fn() => Cast::toInt(''), + TypeError::class, + "Cannot cast '' to int.", +); +Assert::same(0, Cast::toInt('0')); +Assert::same(1, Cast::toInt('1')); +Assert::same(-1, Cast::toInt('-1.')); +Assert::same(1, Cast::toInt('1.0000')); +Assert::exception( + fn() => Cast::toInt('0.1'), + TypeError::class, + "Cannot cast '0.1' to int.", +); +Assert::exception( + fn() => Cast::toInt([]), + TypeError::class, + 'Cannot cast array to int.', +); + + +// float +Assert::same(0.0, Cast::toFloat(null)); +Assert::same(0.0, Cast::toFloat(false)); +Assert::same(1.0, Cast::toFloat(true)); +Assert::same(0.0, Cast::toFloat(0)); +Assert::same(1.0, Cast::toFloat(1)); +Assert::same(0.0, Cast::toFloat(0.0)); +Assert::same(1.0, Cast::toFloat(1.0)); +Assert::same(0.1, Cast::toFloat(0.1)); +Assert::exception( + fn() => Cast::toFloat(''), + TypeError::class, + "Cannot cast '' to float.", +); +Assert::same(0.0, Cast::toFloat('0')); +Assert::same(1.0, Cast::toFloat('1')); +Assert::same(-1.0, Cast::toFloat('-1.')); +Assert::same(1.0, Cast::toFloat('1.0')); +Assert::same(0.1, Cast::toFloat('0.1')); +Assert::exception( + fn() => Cast::toFloat([]), + TypeError::class, + 'Cannot cast array to float.', +); + + +// string +Assert::same('', Cast::toString(null)); +Assert::same('0', Cast::toString(false)); // differs from PHP strict casting +Assert::same('1', Cast::toString(true)); +Assert::same('0', Cast::toString(0)); +Assert::same('1', Cast::toString(1)); +Assert::same('0.0', Cast::toString(0.0)); // differs from PHP strict casting +Assert::same('1.0', Cast::toString(1.0)); // differs from PHP strict casting +Assert::same('-0.1', Cast::toString(-0.1)); +Assert::same('9.2233720368548E+18', Cast::toString(PHP_INT_MAX + 1)); +Assert::same('', Cast::toString('')); +Assert::same('x', Cast::toString('x')); +Assert::exception( + fn() => Cast::toString([]), + TypeError::class, + 'Cannot cast array to string.', +); + + +// array +Assert::same([], Cast::toArray(null)); +Assert::same([false], Cast::toArray(false)); +Assert::same([true], Cast::toArray(true)); +Assert::same([0], Cast::toArray(0)); +Assert::same([0.0], Cast::toArray(0.0)); +Assert::same([1], Cast::toArray([1])); +Assert::equal([new stdClass], Cast::toArray(new stdClass)); // differs from PHP strict casting + + +// OrNull +Assert::true(Cast::toBoolOrNull(true)); +Assert::null(Cast::toBoolOrNull(null)); +Assert::same(0, Cast::toIntOrNull(0)); +Assert::null(Cast::toIntOrNull(null)); +Assert::same(0.0, Cast::toFloatOrNull(0)); +Assert::null(Cast::toFloatOrNull(null)); +Assert::same('0', Cast::toStringOrNull(0)); +Assert::null(Cast::toStringOrNull(null)); +Assert::same([], Cast::toArrayOrNull([])); +Assert::null(Cast::toArrayOrNull(null)); diff --git a/tests/Utils/Cast.to.phpt b/tests/Utils/Cast.to.phpt new file mode 100644 index 000000000..d9a23f1c3 --- /dev/null +++ b/tests/Utils/Cast.to.phpt @@ -0,0 +1,30 @@ + Cast::to(null, 'unknown'), + TypeError::class, + "Unsupported type 'unknown'.", +); + + +// toOrNull +Assert::null(Cast::toOrNull(null, 'bool')); +Assert::null(Cast::toOrNull(null, 'int')); +Assert::null(Cast::toOrNull(null, 'float')); +Assert::null(Cast::toOrNull(null, 'string')); +Assert::null(Cast::toOrNull(null, 'array')); +Assert::null(Cast::toOrNull(null, 'unknown')); // implementation imperfection