Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 4 additions & 3 deletions src/MartinGeorgiev/Doctrine/DBAL/Types/BaseIntegerArray.php
Original file line number Diff line number Diff line change
Expand Up @@ -47,9 +47,10 @@ public function transformArrayItemForPHP($item): ?int
return null;
}

$isInvalidPHPInt = !(bool) \preg_match('/^-?\d+$/', (string) $item)
|| (string) $item < $this->getMinValue()
|| (string) $item > $this->getMaxValue();
$stringValue = (string) $item;
$isInvalidPHPInt = !(bool) \preg_match('/^-?\d+$/', $stringValue)
|| $stringValue < $this->getMinValue()
|| $stringValue > $this->getMaxValue();
if ($isInvalidPHPInt) {
throw new ConversionException(\sprintf('Given value of %s content cannot be transformed to valid PHP integer.', \var_export($item, true)));
}
Expand Down
6 changes: 3 additions & 3 deletions src/MartinGeorgiev/Doctrine/DBAL/Types/TextArray.php
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@
namespace MartinGeorgiev\Doctrine\DBAL\Types;

use Doctrine\DBAL\Platforms\AbstractPlatform;
use MartinGeorgiev\Utils\DataStructure;
use MartinGeorgiev\Utils\ArrayDataTransformer;

/**
* Implementation of PostgreSQL TEXT[] data type.
Expand Down Expand Up @@ -42,7 +42,7 @@ protected function transformToPostgresTextArray(array $phpTextArray): string
return '{}';
}

return DataStructure::transformPHPArrayToPostgresTextArray($phpTextArray);
return ArrayDataTransformer::transformPHPArrayToPostgresTextArray($phpTextArray);
}

/**
Expand All @@ -65,6 +65,6 @@ protected function transformFromPostgresTextArray(string $postgresValue): array
return [];
}

return DataStructure::transformPostgresTextArrayToPHPArray($postgresValue);
return ArrayDataTransformer::transformPostgresTextArrayToPHPArray($postgresValue);
}
}
170 changes: 170 additions & 0 deletions src/MartinGeorgiev/Utils/ArrayDataTransformer.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,170 @@
<?php

declare(strict_types=1);

namespace MartinGeorgiev\Utils;

use MartinGeorgiev\Utils\Exception\InvalidArrayFormatException;

/**
* @since 3.0
*
* @author Martin Georgiev <martin.georgiev@gmail.com>
*/
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<int, mixed>|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<int|string, string> */
$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);
}
}
90 changes: 0 additions & 90 deletions src/MartinGeorgiev/Utils/DataStructure.php

This file was deleted.

23 changes: 23 additions & 0 deletions src/MartinGeorgiev/Utils/Exception/InvalidArrayFormatException.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
<?php

declare(strict_types=1);

namespace MartinGeorgiev\Utils\Exception;

class InvalidArrayFormatException extends \InvalidArgumentException
{
public static function multiDimensionalArrayNotSupported(): self
{
return new self('Only single-dimensioned arrays are supported');
}

public static function invalidFormat(string $details = ''): self
{
$message = 'Invalid array format';
if ($details !== '') {
$message .= ': '.$details;
}

return new self($message);
}
}
6 changes: 4 additions & 2 deletions tests/MartinGeorgiev/Doctrine/DBAL/Types/TextArrayTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -74,17 +74,19 @@ public static function provideValidTransformations(): array
'phpValue' => [
1,
'2',
3.4,
'5.6',
'text',
'some text here',
'and some here',
<<<'END'
''"quotes"'' ain't no """worry""", '''right''' Alexander O'Vechkin?
END,
'back-slashing\double-slashing\\\hooking though',
'back-slashing\double-slashing\\\triple-slashing\\\\\hooking though',
'and "double-quotes"',
],
'postgresValue' => <<<'END'
{1,2,"text","some text here","and some here","''\"quotes\"'' ain't no \"\"\"worry\"\"\", '''right''' Alexander O'Vechkin?","back-slashing\\double-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?","back-slashing\\double-slashing\\\\triple-slashing\\\\\\hooking though","and \"double-quotes\""}
END,
],
];
Expand Down
Loading
Loading