diff --git a/docs/UPGRADE.md b/docs/UPGRADE.md index 755d671c..677a7c99 100644 --- a/docs/UPGRADE.md +++ b/docs/UPGRADE.md @@ -1,6 +1,6 @@ # Upgrade Instructions -## How to Upgrade to Version 3.0.0 +## How to Upgrade to Version 3.0 ### 1. Review type handling in your code If your application relies on automatic type conversion between PostgreSQL and PHP (e.g., expecting string numbers to be converted to actual numbers or vice versa), you'll need to update your code to explicitly handle type conversion where needed. @@ -16,7 +16,7 @@ $numericValue = (float)$tags[0] + 2; // Explicit conversion needed ``` ### 2. Update your code to handle exceptions -If you're catching specific exception types when working with `JsonbArray`, update your exception handling to catch the new `InvalidJsonbArrayItemForPHPException`. +If you're catching specific exception types when working with `JsonbArray`, update your exception handling to catch the new `InvalidJsonItemForPHPException` and `InvalidJsonArrayItemForPHPException`. ```php // Before @@ -29,7 +29,7 @@ try { // After try { $jsonArray = $jsonbArrayType->convertToPHPValue($postgresValue, $platform); -} catch (\MartinGeorgiev\Doctrine\DBAL\Types\Exceptions\InvalidJsonbArrayItemForPHPException $e) { +} catch (\MartinGeorgiev\Doctrine\DBAL\Types\Exceptions\InvalidJsonArrayItemForPHPException $e) { // Handle exception } ``` diff --git a/src/MartinGeorgiev/Doctrine/DBAL/Types/Exceptions/InvalidJsonbArrayItemForPHPException.php b/src/MartinGeorgiev/Doctrine/DBAL/Types/Exceptions/InvalidJsonArrayItemForPHPException.php similarity index 78% rename from src/MartinGeorgiev/Doctrine/DBAL/Types/Exceptions/InvalidJsonbArrayItemForPHPException.php rename to src/MartinGeorgiev/Doctrine/DBAL/Types/Exceptions/InvalidJsonArrayItemForPHPException.php index 9fd2633e..261407d6 100644 --- a/src/MartinGeorgiev/Doctrine/DBAL/Types/Exceptions/InvalidJsonbArrayItemForPHPException.php +++ b/src/MartinGeorgiev/Doctrine/DBAL/Types/Exceptions/InvalidJsonArrayItemForPHPException.php @@ -6,7 +6,7 @@ use Doctrine\DBAL\Types\ConversionException; -class InvalidJsonbArrayItemForPHPException extends ConversionException +class InvalidJsonArrayItemForPHPException extends ConversionException { private static function create(string $message, mixed $value): self { @@ -20,6 +20,6 @@ public static function forInvalidType(mixed $value): self public static function forInvalidFormat(mixed $value): self { - return self::create('Invalid JSONB format in array: %s', $value); + return self::create('Invalid JSON format in array: %s', $value); } } diff --git a/src/MartinGeorgiev/Doctrine/DBAL/Types/Exceptions/InvalidJsonItemForPHPException.php b/src/MartinGeorgiev/Doctrine/DBAL/Types/Exceptions/InvalidJsonItemForPHPException.php new file mode 100644 index 00000000..eab0c146 --- /dev/null +++ b/src/MartinGeorgiev/Doctrine/DBAL/Types/Exceptions/InvalidJsonItemForPHPException.php @@ -0,0 +1,25 @@ +transformFromPostgresJson($item); - if (!\is_array($transformedValue)) { - throw InvalidJsonbArrayItemForPHPException::forInvalidType($item); - } - - return $transformedValue; - } catch (\JsonException) { - throw InvalidJsonbArrayItemForPHPException::forInvalidFormat($item); - } + return PostgresJsonToPHPArrayTransformer::transformPostgresJsonEncodedValueToPHPArray($item); } } diff --git a/src/MartinGeorgiev/Doctrine/DBAL/Types/TextArray.php b/src/MartinGeorgiev/Doctrine/DBAL/Types/TextArray.php index 67ab54b3..8c091558 100644 --- a/src/MartinGeorgiev/Doctrine/DBAL/Types/TextArray.php +++ b/src/MartinGeorgiev/Doctrine/DBAL/Types/TextArray.php @@ -5,7 +5,8 @@ namespace MartinGeorgiev\Doctrine\DBAL\Types; use Doctrine\DBAL\Platforms\AbstractPlatform; -use MartinGeorgiev\Utils\ArrayDataTransformer; +use MartinGeorgiev\Utils\PHPArrayToPostgresValueTransformer; +use MartinGeorgiev\Utils\PostgresArrayToPHPArrayTransformer; /** * Implementation of PostgreSQL TEXT[] data type. @@ -42,7 +43,7 @@ protected function transformToPostgresTextArray(array $phpTextArray): string return '{}'; } - return ArrayDataTransformer::transformPHPArrayToPostgresTextArray($phpTextArray); + return PHPArrayToPostgresValueTransformer::transformToPostgresTextArray($phpTextArray); } /** @@ -65,6 +66,6 @@ protected function transformFromPostgresTextArray(string $postgresValue): array return []; } - return ArrayDataTransformer::transformPostgresTextArrayToPHPArray($postgresValue); + return PostgresArrayToPHPArrayTransformer::transformPostgresArrayToPHPArray($postgresValue); } } diff --git a/src/MartinGeorgiev/Utils/ArrayDataTransformer.php b/src/MartinGeorgiev/Utils/ArrayDataTransformer.php deleted file mode 100644 index 505db92d..00000000 --- a/src/MartinGeorgiev/Utils/ArrayDataTransformer.php +++ /dev/null @@ -1,170 +0,0 @@ - - */ -class ArrayDataTransformer -{ - private const POSTGRESQL_EMPTY_ARRAY = '{}'; - - private const POSTGRESQL_NULL_VALUE = 'null'; - - /** - * This method supports only single-dimensioned text arrays and - * relays on the default escaping strategy in PostgreSQL (double quotes). - * - * @throws InvalidArrayFormatException when the input is a multi-dimensional array or has invalid format - */ - public static function transformPostgresTextArrayToPHPArray(string $postgresArray): array - { - $trimmed = \trim($postgresArray); - - if ($trimmed === '' || \strtolower($trimmed) === self::POSTGRESQL_NULL_VALUE) { - return []; - } - - if (\str_contains($trimmed, '},{') || \str_starts_with($trimmed, '{{')) { - throw InvalidArrayFormatException::multiDimensionalArrayNotSupported(); - } - - if ($trimmed === self::POSTGRESQL_EMPTY_ARRAY) { - return []; - } - - $jsonArray = '['.\trim($trimmed, '{}').']'; - - /** @var array|null $decoded */ - $decoded = \json_decode($jsonArray, true, 512, JSON_BIGINT_AS_STRING); - if ($decoded === null && \json_last_error() !== JSON_ERROR_NONE) { - throw InvalidArrayFormatException::invalidFormat(\json_last_error_msg()); - } - - return \array_map( - static fn (mixed $value): mixed => \is_string($value) ? self::unescapeString($value) : $value, - (array) $decoded - ); - } - - /** - * This method supports only single-dimensioned PHP arrays. - * This method relays on the default escaping strategy in PostgreSQL (double quotes). - * - * @throws InvalidArrayFormatException when the input is a multi-dimensional array or has invalid format - */ - public static function transformPHPArrayToPostgresTextArray(array $phpArray): string - { - if ($phpArray === []) { - return self::POSTGRESQL_EMPTY_ARRAY; - } - - if (\array_filter($phpArray, 'is_array')) { - throw InvalidArrayFormatException::multiDimensionalArrayNotSupported(); - } - - /** @var array */ - $processed = \array_map( - static fn (mixed $value): string => self::formatValue($value), - $phpArray - ); - - return '{'.\implode(',', $processed).'}'; - } - - /** - * Formats a single value for PostgreSQL array. - */ - private static function formatValue(mixed $value): string - { - // Handle null - if ($value === null) { - return 'NULL'; - } - - // Handle actual numbers - if (\is_int($value) || \is_float($value)) { - return (string) $value; - } - - // Handle booleans - if (\is_bool($value)) { - return $value ? 'true' : 'false'; - } - - // Handle objects that implement __toString() - if (\is_object($value)) { - if (\method_exists($value, '__toString')) { - $stringValue = $value->__toString(); - } else { - // For objects without __toString, use a default representation - $stringValue = $value::class; - } - } else { - // For all other types, force string conversion - // This covers strings, resources, and other types - $stringValue = match (true) { - \is_resource($value) => '(resource)', - default => (string) $value // @phpstan-ignore-line - }; - } - - \assert(\is_string($stringValue)); - - // Handle empty string - if ($stringValue === '') { - return '""'; - } - - if (self::isNumericSimple($stringValue)) { - return '"'.$stringValue.'"'; - } - - // Double the backslashes and escape quotes - $escaped = \str_replace( - ['\\', '"'], - ['\\\\', '\"'], - $stringValue - ); - - return '"'.$escaped.'"'; - } - - private static function isNumericSimple(string $value): bool - { - // Fast path for obvious numeric strings - if ($value === '' || $value[0] === '"') { - return false; - } - - // Handle scientific notation - $lower = \strtolower($value); - if (\str_contains($lower, 'e')) { - $value = \str_replace('e', '', $lower); - } - - // Use built-in numeric check - return \is_numeric($value); - } - - private static function unescapeString(string $value): string - { - // First handle escaped quotes - $value = \str_replace('\"', '___QUOTE___', $value); - - // Handle double backslashes - $value = \str_replace('\\\\', '___DBLBACK___', $value); - - // Restore double backslashes - $value = \str_replace('___DBLBACK___', '\\\\', $value); - - // Finally restore quotes - return \str_replace('___QUOTE___', '"', $value); - } -} diff --git a/src/MartinGeorgiev/Utils/PHPArrayToPostgresValueTransformer.php b/src/MartinGeorgiev/Utils/PHPArrayToPostgresValueTransformer.php new file mode 100644 index 00000000..705e8dd3 --- /dev/null +++ b/src/MartinGeorgiev/Utils/PHPArrayToPostgresValueTransformer.php @@ -0,0 +1,101 @@ + + */ +class PHPArrayToPostgresValueTransformer +{ + private const POSTGRESQL_EMPTY_ARRAY = '{}'; + + /** + * Transforms a PHP array to a PostgreSQL text array. + * This method supports only single-dimensioned PHP arrays. + * This method relays on the default escaping strategy in PostgreSQL (double quotes). + * + * @throws InvalidArrayFormatException when the input is a multi-dimensional array or has invalid format + */ + public static function transformToPostgresTextArray(array $phpArray): string + { + if ($phpArray === []) { + return self::POSTGRESQL_EMPTY_ARRAY; + } + + if (\array_filter($phpArray, 'is_array')) { + throw InvalidArrayFormatException::multiDimensionalArrayNotSupported(); + } + + /** @var array */ + $processed = \array_map( + static fn (mixed $value): string => self::formatValue($value), + $phpArray + ); + + return '{'.\implode(',', $processed).'}'; + } + + /** + * Formats a single value for PostgreSQL array. + */ + private static function formatValue(mixed $value): string + { + if ($value === null) { + return 'NULL'; + } + + if (\is_int($value) || \is_float($value)) { + return (string) $value; + } + + if (\is_bool($value)) { + return $value ? 'true' : 'false'; + } + + if (\is_object($value)) { + if (\method_exists($value, '__toString')) { + $stringValue = $value->__toString(); + } else { + // For objects without __toString, use a default representation + $stringValue = $value::class; + } + } elseif (\is_resource($value)) { + $stringValue = '(resource)'; + } else { + $valueType = \get_debug_type($value); + + if ($valueType === 'string') { + $stringValue = $value; + } elseif (\in_array($valueType, ['int', 'float', 'bool'], true)) { + /** @var bool|float|int $value */ + $stringValue = (string) $value; + } else { + $stringValue = $valueType; + } + } + + \assert(\is_string($stringValue)); + + if ($stringValue === '') { + return '""'; + } + + // Make sure strings are quoted, PostgreSQL will handle this gracefully + // Double the backslashes and escape quotes + $escaped = \str_replace( + ['\\', '"'], + ['\\\\', '\"'], + $stringValue + ); + + return '"'.$escaped.'"'; + } +} diff --git a/src/MartinGeorgiev/Utils/PostgresArrayToPHPArrayTransformer.php b/src/MartinGeorgiev/Utils/PostgresArrayToPHPArrayTransformer.php new file mode 100644 index 00000000..b2071bfa --- /dev/null +++ b/src/MartinGeorgiev/Utils/PostgresArrayToPHPArrayTransformer.php @@ -0,0 +1,261 @@ + + */ +class PostgresArrayToPHPArrayTransformer +{ + private const POSTGRESQL_EMPTY_ARRAY = '{}'; + + private const POSTGRESQL_NULL_VALUE = 'null'; + + /** + * Transforms a PostgreSQL text array to a PHP array. + * This method supports only single-dimensioned text arrays and + * relays on the default escaping strategy in PostgreSQL (double quotes). + * + * @throws InvalidArrayFormatException when the input is a multi-dimensional array or has invalid format + */ + public static function transformPostgresArrayToPHPArray(string $postgresArray): array + { + $trimmed = \trim($postgresArray); + + if ($trimmed === '' || \strtolower($trimmed) === self::POSTGRESQL_NULL_VALUE) { + return []; + } + + if (\str_contains($trimmed, '},{') || \str_starts_with($trimmed, '{{')) { + throw InvalidArrayFormatException::multiDimensionalArrayNotSupported(); + } + + if ($trimmed === self::POSTGRESQL_EMPTY_ARRAY) { + return []; + } + + // Check for malformed nesting - this is a more specific check than the one above + // But we need to exclude cases where curly braces are part of quoted strings + $content = \trim($trimmed, self::POSTGRESQL_EMPTY_ARRAY); + $inQuotes = false; + $escaping = false; + + for ($i = 0, $len = \strlen($content); $i < $len; $i++) { + $char = $content[$i]; + + if ($escaping) { + $escaping = false; + + continue; + } + + if ($char === '\\' && $inQuotes) { + $escaping = true; + + continue; + } + + if ($char === '"') { + $inQuotes = !$inQuotes; + } elseif (($char === '{' || $char === '}') && !$inQuotes) { + throw InvalidArrayFormatException::invalidFormat('Malformed array nesting detected'); + } + } + + // Check for unclosed quotes + if ($inQuotes) { + throw InvalidArrayFormatException::invalidFormat('Unclosed quotes in array'); + } + + // First try with json_decode for properly quoted values + $jsonArray = '['.\trim($trimmed, self::POSTGRESQL_EMPTY_ARRAY).']'; + + /** @var array|null $decoded */ + $decoded = \json_decode($jsonArray, true, 512, JSON_BIGINT_AS_STRING); + + // If json_decode fails, try manual parsing for unquoted strings + if ($decoded === null && \json_last_error() !== JSON_ERROR_NONE) { + return self::parsePostgresArrayManually($content); + } + + return \array_map( + static fn (mixed $value): mixed => \is_string($value) ? self::unescapeString($value) : $value, + (array) $decoded + ); + } + + private static function parsePostgresArrayManually(string $content): array + { + if ($content === '') { + return []; + } + + // Parse the array manually, handling quoted and unquoted values + $result = []; + $inQuotes = false; + $currentValue = ''; + $escaping = false; + + for ($i = 0, $len = \strlen($content); $i < $len; $i++) { + $char = $content[$i]; + + // Handle escaping within quotes + if ($escaping) { + $currentValue .= $char; + $escaping = false; + + continue; + } + + if ($char === '\\' && $inQuotes) { + $escaping = true; + $currentValue .= $char; + + continue; + } + + if ($char === '"') { + $inQuotes = !$inQuotes; + // For quoted values, we include the quotes for later processing + $currentValue .= $char; + } elseif ($char === ',' && !$inQuotes) { + // End of value + $result[] = self::processPostgresValue($currentValue); + $currentValue = ''; + } else { + $currentValue .= $char; + } + } + + // Add the last value + if ($currentValue !== '') { + $result[] = self::processPostgresValue($currentValue); + } + + return $result; + } + + /** + * Process a single value from a PostgreSQL array. + */ + private static function processPostgresValue(string $value): mixed + { + $value = \trim($value); + + if (self::isNullValue($value)) { + return null; + } + + if (self::isBooleanValue($value)) { + return self::processBooleanValue($value); + } + + if (self::isQuotedString($value)) { + return self::processQuotedString($value); + } + + if (self::isNumericValue($value)) { + return self::processNumericValue($value); + } + + // For unquoted strings, return as is + return $value; + } + + private static function isNullValue(string $value): bool + { + return $value === 'NULL' || $value === 'null'; + } + + private static function isBooleanValue(string $value): bool + { + return \in_array($value, ['true', 't', 'false', 'f'], true); + } + + private static function processBooleanValue(string $value): bool + { + return $value === 'true' || $value === 't'; + } + + private static function isQuotedString(string $value): bool + { + return \strlen($value) >= 2 && $value[0] === '"' && $value[\strlen($value) - 1] === '"'; + } + + private static function processQuotedString(string $value): string + { + // Remove the quotes and unescape the string + $unquoted = \substr($value, 1, -1); + + return self::unescapeString($unquoted); + } + + private static function isNumericValue(string $value): bool + { + return \is_numeric($value); + } + + private static function processNumericValue(string $value): float|int + { + // Convert to int or float as appropriate + if (\str_contains($value, '.') || \stripos($value, 'e') !== false) { + return (float) $value; + } + + return (int) $value; + } + + private static function unescapeString(string $value): string + { + $result = ''; + $len = \strlen($value); + $i = 0; + $backslashCount = 0; + + while ($i < $len) { + if ($value[$i] === '\\') { + $backslashCount++; + $i++; + + continue; + } + + if ($backslashCount > 0) { + if ($value[$i] === '"') { + // This is an escaped quote + $result .= \str_repeat('\\', (int) ($backslashCount / 2)); + if ($backslashCount % 2 === 1) { + $result .= '"'; + } else { + $result .= '\"'; + } + } else { + // These are literal backslashes + $result .= \str_repeat('\\', $backslashCount); + $result .= $value[$i]; + } + + $backslashCount = 0; + } else { + $result .= $value[$i]; + } + + $i++; + } + + // Handle any trailing backslashes + if ($backslashCount > 0) { + $result .= \str_repeat('\\', $backslashCount); + } + + return $result; + } +} diff --git a/src/MartinGeorgiev/Utils/PostgresJsonToPHPArrayTransformer.php b/src/MartinGeorgiev/Utils/PostgresJsonToPHPArrayTransformer.php new file mode 100644 index 00000000..da55e048 --- /dev/null +++ b/src/MartinGeorgiev/Utils/PostgresJsonToPHPArrayTransformer.php @@ -0,0 +1,65 @@ + + */ +class PostgresJsonToPHPArrayTransformer +{ + private const POSTGRESQL_EMPTY_ARRAY = '{}'; + + public static function transformPostgresArrayToPHPArray(string $postgresValue): array + { + if ($postgresValue === self::POSTGRESQL_EMPTY_ARRAY) { + return []; + } + + $trimmedPostgresArray = \mb_substr($postgresValue, 2, -2); + $phpArray = \explode('},{', $trimmedPostgresArray); + foreach ($phpArray as &$item) { + $item = '{'.$item.'}'; + } + + return $phpArray; + } + + /** + * @throws InvalidJsonArrayItemForPHPException When the PostgreSQL value is not a JSON + */ + public static function transformPostgresJsonEncodedValueToPHPArray(string $postgresValue): array + { + try { + $transformedValue = \json_decode($postgresValue, true, 512, JSON_THROW_ON_ERROR); + if (!\is_array($transformedValue)) { + throw InvalidJsonArrayItemForPHPException::forInvalidType($postgresValue); + } + + return $transformedValue; + } catch (\JsonException) { + throw InvalidJsonArrayItemForPHPException::forInvalidFormat($postgresValue); + } + } + + /** + * @throws InvalidJsonItemForPHPException When the PostgreSQL value is not JSON-decodable + */ + public static function transformPostgresJsonEncodedValueToPHPValue(string $postgresValue): null|array|bool|float|int|string + { + try { + // @phpstan-ignore-next-line + return \json_decode($postgresValue, true, 512, JSON_THROW_ON_ERROR); + } catch (\JsonException) { + throw InvalidJsonItemForPHPException::forInvalidType($postgresValue); + } + } +} diff --git a/tests/MartinGeorgiev/Doctrine/DBAL/Types/JsonbArrayTest.php b/tests/MartinGeorgiev/Doctrine/DBAL/Types/JsonbArrayTest.php index 0d2f80bb..368ec42d 100644 --- a/tests/MartinGeorgiev/Doctrine/DBAL/Types/JsonbArrayTest.php +++ b/tests/MartinGeorgiev/Doctrine/DBAL/Types/JsonbArrayTest.php @@ -5,7 +5,7 @@ namespace Tests\MartinGeorgiev\Doctrine\DBAL\Types; use Doctrine\DBAL\Platforms\AbstractPlatform; -use MartinGeorgiev\Doctrine\DBAL\Types\Exceptions\InvalidJsonbArrayItemForPHPException; +use MartinGeorgiev\Doctrine\DBAL\Types\Exceptions\InvalidJsonArrayItemForPHPException; use MartinGeorgiev\Doctrine\DBAL\Types\JsonbArray; use PHPUnit\Framework\MockObject\MockObject; use PHPUnit\Framework\TestCase; @@ -97,8 +97,8 @@ public static function provideValidTransformations(): array */ public function throws_exception_when_invalid_data_provided_to_convert_to_php_value(string $postgresValue): void { - $this->expectException(InvalidJsonbArrayItemForPHPException::class); - $this->expectExceptionMessage('Invalid JSONB format in array'); + $this->expectException(InvalidJsonArrayItemForPHPException::class); + $this->expectExceptionMessage('Invalid JSON format in array'); $this->fixture->convertToPHPValue($postgresValue, $this->platform); } diff --git a/tests/MartinGeorgiev/Doctrine/DBAL/Types/TextArrayTest.php b/tests/MartinGeorgiev/Doctrine/DBAL/Types/TextArrayTest.php index ca6199ea..58f34d84 100644 --- a/tests/MartinGeorgiev/Doctrine/DBAL/Types/TextArrayTest.php +++ b/tests/MartinGeorgiev/Doctrine/DBAL/Types/TextArrayTest.php @@ -70,6 +70,22 @@ public static function provideValidTransformations(): array 'phpValue' => [], 'postgresValue' => '{}', ], + [ + 'phpValue' => ['\single-back-slash-at-the-start-and-end\\'], + 'postgresValue' => '{"\\\single-back-slash-at-the-start-and-end\\\"}', + ], + [ + 'phpValue' => ['double-back-slash-at-the-end\\\\'], + 'postgresValue' => '{"double-back-slash-at-the-end\\\\\\\"}', + ], + [ + 'phpValue' => ['triple-\\\\\-back-slash-in-the-middle'], + 'postgresValue' => '{"triple-\\\\\\\\\\\-back-slash-in-the-middle"}', + ], + [ + 'phpValue' => ['quadruple-back-slash\\\\\\\\'], + 'postgresValue' => '{"quadruple-back-slash\\\\\\\\\\\\\\\"}', + ], [ 'phpValue' => [ 1, @@ -82,13 +98,38 @@ public static function provideValidTransformations(): array <<<'END' ''"quotes"'' ain't no """worry""", '''right''' Alexander O'Vechkin? END, - 'back-slashing\double-slashing\\\triple-slashing\\\\\hooking though', 'and "double-quotes"', ], 'postgresValue' => <<<'END' -{1,"2",3.4,"5.6","text","some text here","and some here","''\"quotes\"'' ain't no \"\"\"worry\"\"\", '''right''' Alexander O'Vechkin?","back-slashing\\double-slashing\\\\triple-slashing\\\\\\hooking though","and \"double-quotes\""} +{1,"2",3.4,"5.6","text","some text here","and some here","''\"quotes\"'' ain't no \"\"\"worry\"\"\", '''right''' Alexander O'Vechkin?","and \"double-quotes\""} END, ], + [ + 'phpValue' => ['STRING_A', 'STRING_B', 'STRING_C', 'STRING_D'], + 'postgresValue' => '{"STRING_A","STRING_B","STRING_C","STRING_D"}', + ], ]; } + + /** + * @test + */ + public function can_transform_unquoted_postgres_array_to_php(): void + { + $postgresValue = '{STRING_A,STRING_B,STRING_C,STRING_D}'; + $expectedPhpValue = ['STRING_A', 'STRING_B', 'STRING_C', 'STRING_D']; + + self::assertEquals($expectedPhpValue, $this->fixture->convertToPHPValue($postgresValue, $this->platform)); + } + + /** + * @test + */ + public function can_handle_backslashes_correctly(): void + { + $postgresValue = '{"simple\\\backslash"}'; + $expectedPhpValue = ['simple\backslash']; + + self::assertEquals($expectedPhpValue, $this->fixture->convertToPHPValue($postgresValue, $this->platform)); + } } diff --git a/tests/MartinGeorgiev/Utils/ArrayDataTransformerTest.php b/tests/MartinGeorgiev/Utils/ArrayDataTransformerTest.php deleted file mode 100644 index c8c97209..00000000 --- a/tests/MartinGeorgiev/Utils/ArrayDataTransformerTest.php +++ /dev/null @@ -1,351 +0,0 @@ -> $phpValue - */ - public function can_transform_from_php_value(array $phpValue, string $postgresValue): void - { - self::assertEquals($postgresValue, ArrayDataTransformer::transformPHPArrayToPostgresTextArray($phpValue)); - } - - /** - * @test - * - * @dataProvider provideValidTransformations - * - * @param array> $phpValue - */ - public function can_transform_to_php_value(array $phpValue, string $postgresValue): void - { - self::assertEquals($phpValue, ArrayDataTransformer::transformPostgresTextArrayToPHPArray($postgresValue)); - } - - /** - * @return array - */ - public static function provideValidTransformations(): array - { - return [ - 'simple integer strings as strings are preserved as strings' => [ - 'phpValue' => [ - 0 => '1', - 1 => '2', - 2 => '3', - 3 => '4', - ], - 'postgresValue' => '{"1","2","3","4"}', - ], - 'simple integer strings' => [ - 'phpValue' => [ - 0 => 1, - 1 => 2, - 2 => 3, - 3 => 4, - ], - 'postgresValue' => '{1,2,3,4}', - ], - 'decimal numbers represented as strings are preserved as strings' => [ - 'phpValue' => [ - 0 => '1.23', - 1 => '2.34', - 2 => '3.45', - 3 => '4.56', - ], - 'postgresValue' => '{"1.23","2.34","3.45","4.56"}', - ], - 'decimal numbers' => [ - 'phpValue' => [ - 0 => 1.23, - 1 => 2.34, - 2 => 3.45, - 3 => 4.56, - ], - 'postgresValue' => '{1.23,2.34,3.45,4.56}', - ], - 'mixed content with special characters' => [ - 'phpValue' => [ - 0 => 'dfasdf', - 1 => 'qw,,e{q"we', - 2 => "'qrer'", - 3 => 604, - 4 => '"aaa","b""bb","ccc"', - ], - 'postgresValue' => '{"dfasdf","qw,,e{q\"we","\'qrer\'",604,"\"aaa\",\"b\"\"bb\",\"ccc\""}', - ], - 'empty strings' => [ - 'phpValue' => [ - 0 => '', - 1 => '', - ], - 'postgresValue' => '{"",""}', - ], - 'empty array' => [ - 'phpValue' => [], - 'postgresValue' => '{}', - ], - 'scientific notation as strings' => [ - 'phpValue' => ['1.23e4', '2.34e5', '3.45e6'], - 'postgresValue' => '{"1.23e4","2.34e5","3.45e6"}', - ], - 'scientific notation with negative exponents' => [ - 'phpValue' => ['1.23e-4', '2.34e-5', '3.45e-6'], - 'postgresValue' => '{"1.23e-4","2.34e-5","3.45e-6"}', - ], - 'whole floats that look like integers' => [ - 'phpValue' => ['1.0', '2.00', '3.000', '4.0000'], - 'postgresValue' => '{"1.0","2.00","3.000","4.0000"}', - ], - 'large integers beyond PHP_INT_MAX' => [ - 'phpValue' => [ - '9223372036854775808', // PHP_INT_MAX + 1 - '9999999999999999999', - '-9223372036854775809', // PHP_INT_MIN - 1 - ], - 'postgresValue' => '{"9223372036854775808","9999999999999999999","-9223372036854775809"}', - ], - 'mixed numeric formats' => [ - 'phpValue' => [ - '1.23', // regular float string - 1.23, // regular float - '1.230', // float with trailing zeros - '1.23e4', // scientific notation - '1.0', // whole float as string - 1.0, // whole float - '9999999999999999999', // large integer - ], - 'postgresValue' => '{"1.23",1.23,"1.230","1.23e4","1.0",1,"9999999999999999999"}', - ], - 'boolean values' => [ - 'phpValue' => [true, false], - 'postgresValue' => '{true,false}', - ], - 'objects with __toString' => [ - 'phpValue' => [new class { - public function __toString(): string - { - return 'custom string'; - } - }], - 'postgresValue' => '{"custom string"}', - ], - 'strings with backslashes' => [ - 'phpValue' => ['path\to\file', 'C:\Windows\System32'], - 'postgresValue' => '{"path\\\to\\\file","C:\\\Windows\\\System32"}', - ], - 'strings with unicode characters' => [ - 'phpValue' => ['Hello δΈ–η•Œ', '🌍 Earth'], - 'postgresValue' => '{"Hello δΈ–η•Œ","🌍 Earth"}', - ], - ]; - } - - /** - * @test - * - * @dataProvider provideInvalidTransformations - * - * @param array $phpValue - */ - public function throws_invalid_argument_exception_when_tries_to_non_single_dimensioned_array_from_php_value(array $phpValue, string $postgresValue): void - { - $this->expectException(\InvalidArgumentException::class); - ArrayDataTransformer::transformPHPArrayToPostgresTextArray($phpValue); - } - - /** - * @test - * - * @dataProvider provideInvalidTransformations - * - * @param array $phpValue - */ - public function throws_invalid_argument_exception_when_tries_to_non_single_dimensioned_array_to_php_value(array $phpValue, string $postgresValue): void - { - $this->expectException(\InvalidArgumentException::class); - ArrayDataTransformer::transformPostgresTextArrayToPHPArray($postgresValue); - } - - /** - * @return list - */ - public static function provideInvalidTransformations(): array - { - return [ - [ - 'phpValue' => [ - [ - 0 => '1-1', - 1 => '1-2', - 2 => '1-3', - ], - [ - 0 => '2-1', - 1 => '2-2', - 2 => '2-3', - ], - ], - 'postgresValue' => '{{"1-1","1-2","1-3"},{"2-1","2-2","2-3"}}', - ], - ]; - } - - /** - * @test - */ - public function preserves_numeric_string_types(): void - { - $input = ['1', '1.0', '1.00', 1, 1.01]; - $postgres = ArrayDataTransformer::transformPHPArrayToPostgresTextArray($input); - $output = ArrayDataTransformer::transformPostgresTextArrayToPHPArray($postgres); - - self::assertSame([ - '1', - '1.0', - '1.00', - 1, - 1.01, - ], $output); - } - - /** - * @test - */ - public function handles_resource_cleanup(): void - { - $resource = \fopen('php://memory', 'r'); - \assert(\is_resource($resource)); - $input = [$resource]; - $result = ArrayDataTransformer::transformPHPArrayToPostgresTextArray($input); - \fclose($resource); - - self::assertSame('{"(resource)"}', $result); - } - - /** - * @test - */ - public function handles_empty_input(): void - { - $input = ''; - $result = ArrayDataTransformer::transformPostgresTextArrayToPHPArray($input); - self::assertSame([], $result); - } - - /** - * @test - */ - public function handles_null_string_input(): void - { - $input = 'null'; - $result = ArrayDataTransformer::transformPostgresTextArrayToPHPArray($input); - self::assertSame([], $result); - } - - /** - * @test - */ - public function preserves_numeric_precision(): void - { - $input = ['9223372036854775808', '1.23456789012345']; - $postgres = ArrayDataTransformer::transformPHPArrayToPostgresTextArray($input); - $output = ArrayDataTransformer::transformPostgresTextArrayToPHPArray($postgres); - - self::assertSame($input, $output); - } - - /** - * @test - * - * @dataProvider provideMultiDimensionalArrays - * - * @param array $phpValue - */ - public function throws_exception_for_multi_dimensional_arrays(array $phpValue): void - { - $this->expectException(InvalidArrayFormatException::class); - $this->expectExceptionMessage('Only single-dimensioned arrays are supported'); - ArrayDataTransformer::transformPHPArrayToPostgresTextArray($phpValue); - } - - /** - * @return array - */ - public static function provideMultiDimensionalArrays(): array - { - return [ - 'nested arrays' => [ - 'phpValue' => [ - [1, 2, 3], - [4, 5, 6], - ], - ], - 'deeply nested arrays' => [ - 'phpValue' => [ - 1, - [2, [3, 4]], - 5, - ], - ], - 'associative nested arrays' => [ - 'phpValue' => [ - 'first' => [1, 2, 3], - 'second' => [4, 5, 6], - ], - ], - 'mixed nesting' => [ - 'phpValue' => [ - 1, - 'string', - ['nested' => ['deep' => 'value']], - ], - ], - ]; - } - - /** - * @test - * - * @dataProvider provideInvalidPostgresArrays - */ - public function throws_exception_for_invalid_postgres_arrays(string $postgresValue): void - { - $this->expectException(InvalidArrayFormatException::class); - $this->expectExceptionMessage('Invalid array format'); - ArrayDataTransformer::transformPostgresTextArrayToPHPArray($postgresValue); - } - - /** - * @return array - */ - public static function provideInvalidPostgresArrays(): array - { - return [ - 'unclosed string' => [ - 'postgresValue' => '{1,2,"unclosed string}', - ], - 'invalid format' => [ - 'postgresValue' => '{invalid"format}', - ], - 'malformed nesting' => [ - 'postgresValue' => '{1,{2,3},4}', - ], - ]; - } -} diff --git a/tests/MartinGeorgiev/Utils/PHPArrayToPostgresValueTransformerTest.php b/tests/MartinGeorgiev/Utils/PHPArrayToPostgresValueTransformerTest.php new file mode 100644 index 00000000..3ad91054 --- /dev/null +++ b/tests/MartinGeorgiev/Utils/PHPArrayToPostgresValueTransformerTest.php @@ -0,0 +1,255 @@ +> $phpValue + */ + public function can_transform_from_php_value(array $phpValue, string $postgresValue): void + { + self::assertEquals($postgresValue, PHPArrayToPostgresValueTransformer::transformToPostgresTextArray($phpValue)); + } + + /** + * @return array + */ + public static function provideValidTransformations(): array + { + return [ + 'empty array' => [ + 'phpValue' => [], + 'postgresValue' => '{}', + ], + 'simple integer strings as strings are preserved as strings' => [ + 'phpValue' => [ + 0 => '1', + 1 => '2', + 2 => '3', + 3 => '4', + ], + 'postgresValue' => '{"1","2","3","4"}', + ], + 'simple integer strings as integers are preserved as integers' => [ + 'phpValue' => [ + 0 => 1, + 1 => 2, + 2 => 3, + 3 => 4, + ], + 'postgresValue' => '{1,2,3,4}', + ], + 'float values' => [ + 'phpValue' => [ + 0 => 1.5, + 1 => 2.75, + 2 => -3.25, + ], + 'postgresValue' => '{1.5,2.75,-3.25}', + ], + 'boolean values' => [ + 'phpValue' => [ + 0 => true, + 1 => false, + ], + 'postgresValue' => '{true,false}', + ], + 'null values' => [ + 'phpValue' => [ + 0 => null, + 1 => 'not null', + 2 => null, + ], + 'postgresValue' => '{NULL,"not null",NULL}', + ], + 'empty string' => [ + 'phpValue' => [ + 0 => '', + 1 => 'not empty', + ], + 'postgresValue' => '{"","not empty"}', + ], + 'simple strings' => [ + 'phpValue' => [ + 0 => 'this', + 1 => 'is', + 2 => 'a', + 3 => 'test', + ], + 'postgresValue' => '{"this","is","a","test"}', + ], + 'strings with special characters' => [ + 'phpValue' => [ + 0 => 'this has "quotes"', + 1 => 'this has \backslashes\\', + ], + 'postgresValue' => '{"this has \"quotes\"","this has \\\backslashes\\\"}', + ], + 'strings with backslashes' => [ + 'phpValue' => ['path\to\file', 'C:\Windows\System32'], + 'postgresValue' => '{"path\\\to\\\file","C:\\\Windows\\\System32"}', + ], + 'strings with unicode characters' => [ + 'phpValue' => ['Hello δΈ–η•Œ', '🌍 Earth'], + 'postgresValue' => '{"Hello δΈ–η•Œ","🌍 Earth"}', + ], + ]; + } + + /** + * @test + * + * @dataProvider provideInvalidTransformations + * + * @param array $phpValue + */ + public function throws_invalid_argument_exception_when_tries_to_non_single_dimensioned_array_from_php_value(array $phpValue): void + { + $this->expectException(InvalidArrayFormatException::class); + PHPArrayToPostgresValueTransformer::transformToPostgresTextArray($phpValue); + } + + /** + * @return array + */ + public static function provideInvalidTransformations(): array + { + return [ + 'multi-dimensioned array' => [ + 'phpValue' => [ + 0 => [ + 'this', + 'is', + 'a', + 'test', + ], + ], + 'postgresValue' => '', + ], + ]; + } + + /** + * @test + */ + public function can_transform_object_with_to_string_method(): void + { + $object = new class { + public function __toString(): string + { + return 'object string representation'; + } + }; + + self::assertSame('{"object string representation"}', PHPArrayToPostgresValueTransformer::transformToPostgresTextArray([$object])); + } + + /** + * @test + */ + public function can_transform_object_without_to_string_method(): void + { + $object = new class {}; + + // Should contain the class name + self::assertStringContainsString('class@anonymous', PHPArrayToPostgresValueTransformer::transformToPostgresTextArray([$object])); + } + + /** + * @test + */ + public function can_transform_closed_resource(): void + { + $resource = \fopen('php://temp', 'r'); + \assert(\is_resource($resource)); + \fclose($resource); + + self::assertSame('{"resource (closed)"}', PHPArrayToPostgresValueTransformer::transformToPostgresTextArray([$resource])); + } + + /** + * @test + */ + public function can_transform_open_resource(): void + { + $resource = \fopen('php://temp', 'r'); + \assert(\is_resource($resource)); + + self::assertSame('{"(resource)"}', PHPArrayToPostgresValueTransformer::transformToPostgresTextArray([$resource])); + } + + /** + * @test + */ + public function can_transform_mixed_types_in_array(): void + { + $input = [ + 'string', + 123, + 1.5, + true, + null, + new class { + public function __toString(): string + { + return 'object'; + } + }, + '', + ]; + + self::assertEquals('{"string",123,1.5,true,NULL,"object",""}', PHPArrayToPostgresValueTransformer::transformToPostgresTextArray($input)); + } + + /** + * @test + * + * @dataProvider provideMultiDimensionalArrays + * + * @param array $phpValue + */ + public function throws_exception_for_multi_dimensional_arrays(array $phpValue): void + { + $this->expectException(InvalidArrayFormatException::class); + $this->expectExceptionMessage('Only single-dimensioned arrays are supported'); + PHPArrayToPostgresValueTransformer::transformToPostgresTextArray($phpValue); + } + + /** + * @return array + */ + public static function provideMultiDimensionalArrays(): array + { + return [ + 'array with nested array' => [ + 'phpValue' => [ + 'nested' => ['array'], + ], + ], + 'array with multiple nested arrays' => [ + 'phpValue' => [ + ['array1'], + ['array2'], + ], + ], + 'deeply nested array' => [ + 'phpValue' => [ + 'deeply' => [ + 'nested' => ['array'], + ], + ], + ], + ]; + } +} diff --git a/tests/MartinGeorgiev/Utils/PostgresArrayToPHPArrayTransformerTest.php b/tests/MartinGeorgiev/Utils/PostgresArrayToPHPArrayTransformerTest.php new file mode 100644 index 00000000..66c2fc48 --- /dev/null +++ b/tests/MartinGeorgiev/Utils/PostgresArrayToPHPArrayTransformerTest.php @@ -0,0 +1,230 @@ +> $phpValue + */ + public function can_transform_to_php_value(array $phpValue, string $postgresValue): void + { + self::assertEquals($phpValue, PostgresArrayToPHPArrayTransformer::transformPostgresArrayToPHPArray($postgresValue)); + } + + /** + * @return array + */ + public static function provideValidTransformations(): array + { + return [ + 'null value' => [ + 'phpValue' => [], + 'postgresValue' => 'null', + ], + 'empty value' => [ + 'phpValue' => [], + 'postgresValue' => '', + ], + 'empty array' => [ + 'phpValue' => [], + 'postgresValue' => '{}', + ], + 'simple integer strings as strings are preserved as strings' => [ + 'phpValue' => [ + 0 => '1', + 1 => '2', + 2 => '3', + 3 => '4', + ], + 'postgresValue' => '{"1","2","3","4"}', + ], + 'simple integer strings as integers are preserved as integers' => [ + 'phpValue' => [ + 0 => 1, + 1 => 2, + 2 => 3, + 3 => 4, + ], + 'postgresValue' => '{1,2,3,4}', + ], + 'float values' => [ + 'phpValue' => [ + 0 => 1.5, + 1 => 2.75, + 2 => -3.25, + ], + 'postgresValue' => '{1.5,2.75,-3.25}', + ], + 'scientific notation' => [ + 'phpValue' => [ + 0 => 1.5e3, + 1 => 2.75e-2, + ], + 'postgresValue' => '{1.5e3,2.75e-2}', + ], + 'boolean values' => [ + 'phpValue' => [ + 0 => true, + 1 => false, + 2 => true, + 3 => false, + ], + 'postgresValue' => '{true,false,t,f}', + ], + 'null values' => [ + 'phpValue' => [ + 0 => null, + 1 => 'not null', + 2 => null, + ], + 'postgresValue' => '{NULL,"not null",null}', + ], + 'simple strings' => [ + 'phpValue' => [ + 0 => 'this', + 1 => 'is', + 2 => 'a', + 3 => 'test', + ], + 'postgresValue' => '{"this","is","a","test"}', + ], + 'strings with special characters' => [ + 'phpValue' => [ + 0 => 'this has "quotes"', + 1 => 'this has \\\backslashes\\\\', + ], + 'postgresValue' => '{"this has \"quotes\"","this has \\\\\\\backslashes\\\\\\\"}', + ], + 'strings with backslashes' => [ + 'phpValue' => ['path\to\file', 'C:\Windows\System32'], + 'postgresValue' => '{"path\\\to\\\file","C:\\\Windows\\\System32"}', + ], + 'strings with unicode characters' => [ + 'phpValue' => ['Hello δΈ–η•Œ', '🌍 Earth'], + 'postgresValue' => '{"Hello δΈ–η•Œ","🌍 Earth"}', + ], + 'unquoted strings' => [ + 'phpValue' => ['unquoted', 'strings'], + 'postgresValue' => '{unquoted,strings}', + ], + 'mixed quoted and unquoted strings' => [ + 'phpValue' => ['quoted', 'unquoted'], + 'postgresValue' => '{"quoted",unquoted}', + ], + ]; + } + + /** + * @test + * + * @dataProvider provideMultiDimensionalArrays + */ + public function throws_exception_for_multi_dimensional_arrays(string $postgresValue): void + { + $this->expectException(InvalidArrayFormatException::class); + $this->expectExceptionMessage('Only single-dimensioned arrays are supported'); + PostgresArrayToPHPArrayTransformer::transformPostgresArrayToPHPArray($postgresValue); + } + + /** + * @return array + */ + public static function provideMultiDimensionalArrays(): array + { + return [ + 'multi-dimensioned array' => [ + 'postgresValue' => '{{1,2,3},{4,5,6}}', + ], + ]; + } + + /** + * @test + * + * @dataProvider provideManualParsingArrays + */ + public function can_recover_from_json_decode_failure_and_transform_value_through_manual_parsing(array $phpValue, string $postgresValue): void + { + self::assertEquals($phpValue, PostgresArrayToPHPArrayTransformer::transformPostgresArrayToPHPArray($postgresValue)); + } + + /** + * @return array + */ + public static function provideManualParsingArrays(): array + { + return [ + 'manual parsing of unquoted only text' => [ + 'phpValue' => ['unquoted string', 'another unquoted string'], + 'postgresValue' => '{unquoted string,another unquoted string}', + ], + 'manual parsing with escaping' => [ + 'phpValue' => ['escaped " quote', 'unescaped'], + 'postgresValue' => '{"escaped \" quote",unescaped}', + ], + 'manual parsing with trailing backslash' => [ + 'phpValue' => ['backslash\\', 'another\one'], + 'postgresValue' => '{backslash\,another\one}', + ], + ]; + } + + /** + * @test + */ + public function can_transform_escaped_quotes_with_backslashes(): void + { + $postgresArray = '{"\\\"quoted\\\""}'; + self::assertSame(['\\\"quoted\\\"'], PostgresArrayToPHPArrayTransformer::transformPostgresArrayToPHPArray($postgresArray)); + } + + /** + * @test + */ + public function can_preserves_numeric_precision(): void + { + $postgresArray = '{"9223372036854775808","1.23456789012345"}'; + self::assertSame(['9223372036854775808', '1.23456789012345'], PostgresArrayToPHPArrayTransformer::transformPostgresArrayToPHPArray($postgresArray)); + } + + /** + * @test + * + * @dataProvider provideInvalidPostgresArrays + */ + public function throws_exception_for_invalid_postgres_arrays(string $postgresValue): void + { + $this->expectException(InvalidArrayFormatException::class); + $this->expectExceptionMessage('Invalid array format'); + PostgresArrayToPHPArrayTransformer::transformPostgresArrayToPHPArray($postgresValue); + } + + /** + * @return array + */ + public static function provideInvalidPostgresArrays(): array + { + return [ + 'unclosed string' => [ + 'postgresValue' => '{1,2,"unclosed string}', + ], + 'invalid format' => [ + 'postgresValue' => '{invalid"format}', + ], + 'malformed nesting' => [ + 'postgresValue' => '{1,{2,3},4}', + ], + ]; + } +} diff --git a/tests/MartinGeorgiev/Utils/PostgresJsonToPHPArrayTransformerTest.php b/tests/MartinGeorgiev/Utils/PostgresJsonToPHPArrayTransformerTest.php new file mode 100644 index 00000000..1c71893d --- /dev/null +++ b/tests/MartinGeorgiev/Utils/PostgresJsonToPHPArrayTransformerTest.php @@ -0,0 +1,148 @@ + + */ + public static function provideValidJsonTransformations(): array + { + return [ + 'simple object' => [ + 'phpValue' => ['key' => 'value'], + 'postgresValue' => '{"key":"value"}', + ], + 'nested object' => [ + 'phpValue' => ['key' => ['nested' => 'value']], + 'postgresValue' => '{"key":{"nested":"value"}}', + ], + 'array' => [ + 'phpValue' => [1, 2, 3], + 'postgresValue' => '[1,2,3]', + ], + 'string' => [ + 'phpValue' => 'string', + 'postgresValue' => '"string"', + ], + 'number' => [ + 'phpValue' => 123, + 'postgresValue' => '123', + ], + 'boolean' => [ + 'phpValue' => true, + 'postgresValue' => 'true', + ], + 'null' => [ + 'phpValue' => null, + 'postgresValue' => 'null', + ], + ]; + } + + /** + * @test + * + * @dataProvider provideValidJsonbArrayTransformations + */ + public function can_transform_json_array_to_php_array(array $phpArray, string $postgresArray): void + { + self::assertEquals($phpArray, PostgresJsonToPHPArrayTransformer::transformPostgresArrayToPHPArray($postgresArray)); + } + + /** + * @return array + */ + public static function provideValidJsonbArrayTransformations(): array + { + return [ + 'empty array' => [ + 'phpArray' => [], + 'postgresArray' => '{}', + ], + 'array with one object' => [ + 'phpArray' => ['{key:value}'], + 'postgresArray' => '{{key:value}}', + ], + 'array with multiple objects' => [ + 'phpArray' => ['{key1:value1}', '{key2:value2}'], + 'postgresArray' => '{{key1:value1},{key2:value2}}', + ], + ]; + } + + /** + * @test + * + * @dataProvider provideValidJsonbArrayItemTransformations + */ + public function can_transform_json_array_item_to_php_array(array $phpArray, string $item): void + { + self::assertEquals($phpArray, PostgresJsonToPHPArrayTransformer::transformPostgresJsonEncodedValueToPHPArray($item)); + } + + /** + * @return array + */ + public static function provideValidJsonbArrayItemTransformations(): array + { + return [ + 'simple object' => [ + 'phpArray' => ['key' => 'value'], + 'item' => '{"key":"value"}', + ], + 'nested object' => [ + 'phpArray' => ['key' => ['nested' => 'value']], + 'item' => '{"key":{"nested":"value"}}', + ], + ]; + } + + /** + * @test + */ + public function throws_exception_for_invalid_json(): void + { + $this->expectException(InvalidJsonItemForPHPException::class); + $this->expectExceptionMessage("Postgres value must be single, valid JSON object, '{invalid json}' given"); + PostgresJsonToPHPArrayTransformer::transformPostgresJsonEncodedValueToPHPValue('{invalid json}'); + } + + /** + * @test + */ + public function throws_exception_for_invalid_json_array_item(): void + { + $this->expectException(InvalidJsonArrayItemForPHPException::class); + $this->expectExceptionMessage("Invalid JSON format in array: '{invalid json}'"); + PostgresJsonToPHPArrayTransformer::transformPostgresJsonEncodedValueToPHPArray('{invalid json}'); + } + + /** + * @test + */ + public function throws_exception_for_non_array_json_array_item(): void + { + $this->expectException(InvalidJsonArrayItemForPHPException::class); + $this->expectExceptionMessage('Array values must be valid JSON objects, \'"string"\' given'); + PostgresJsonToPHPArrayTransformer::transformPostgresJsonEncodedValueToPHPArray('"string"'); + } +}