Passed
Push — float-arrays ( d487a4...cd1ee0 )
by Martin
10:59
created

transformPostgresTextArrayToPHPArray()   B

Complexity

Conditions 9
Paths 5

Size

Total Lines 27
Code Lines 14

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 15
CRAP Score 9

Importance

Changes 0
Metric Value
cc 9
eloc 14
nc 5
nop 1
dl 0
loc 27
ccs 15
cts 15
cp 1
crap 9
rs 8.0555
c 0
b 0
f 0
1
<?php
2
3
declare(strict_types=1);
4
5
namespace MartinGeorgiev\Utils;
6
7
use MartinGeorgiev\Utils\Exception\InvalidArrayFormatException;
8
9
/**
10
 * @since 3.0
11
 *
12
 * @author Martin Georgiev <[email protected]>
13
 */
14
class ArrayDataTransformer
15
{
16
    private const POSTGRESQL_EMPTY_ARRAY = '{}';
17
18
    private const POSTGRESQL_NULL_VALUE = 'null';
19
20
    /**
21
     * This method supports only single-dimensioned text arrays and
22
     * relays on the default escaping strategy in PostgreSQL (double quotes).
23
     *
24
     * @throws InvalidArrayFormatException when the input is a multi-dimensional array or has invalid format
25
     */
26 25
    public static function transformPostgresTextArrayToPHPArray(string $postgresArray): array
27
    {
28 25
        $trimmed = \trim($postgresArray);
29
30 25
        if ($trimmed === '' || \strtolower($trimmed) === self::POSTGRESQL_NULL_VALUE) {
31 2
            return [];
32
        }
33
34 23
        if (\str_contains($trimmed, '},{') || \str_starts_with($trimmed, '{{')) {
35 1
            throw InvalidArrayFormatException::multiDimensionalArrayNotSupported();
36
        }
37
38 22
        if ($trimmed === self::POSTGRESQL_EMPTY_ARRAY) {
39 1
            return [];
40
        }
41
42 21
        $jsonArray = '['.\trim($trimmed, '{}').']';
43
44
        /** @var array<int, mixed>|null $decoded */
45 21
        $decoded = \json_decode($jsonArray, true, 512, JSON_BIGINT_AS_STRING);
46 21
        if ($decoded === null && \json_last_error() !== JSON_ERROR_NONE) {
47 3
            throw InvalidArrayFormatException::invalidFormat(\json_last_error_msg());
48
        }
49
50 18
        return \array_map(
51 18
            static fn (mixed $value): mixed => \is_string($value) ? self::unescapeString($value) : $value,
52 18
            (array) $decoded
53 18
        );
54
    }
55
56
    /**
57
     * This method supports only single-dimensioned PHP arrays.
58
     * This method relays on the default escaping strategy in PostgreSQL (double quotes).
59
     *
60
     * @throws InvalidArrayFormatException when the input is a multi-dimensional array or has invalid format
61
     */
62 25
    public static function transformPHPArrayToPostgresTextArray(array $phpArray): string
63
    {
64 25
        if ($phpArray === []) {
65 1
            return self::POSTGRESQL_EMPTY_ARRAY;
66
        }
67
68 24
        if (\array_filter($phpArray, 'is_array')) {
69 5
            throw InvalidArrayFormatException::multiDimensionalArrayNotSupported();
70
        }
71
72
        /** @var array<int|string, string> */
73 19
        $processed = \array_map(
74 19
            static fn (mixed $value): string => self::formatValue($value),
75 19
            $phpArray
76 19
        );
77
78 19
        return '{'.\implode(',', $processed).'}';
79
    }
80
81
    /**
82
     * Formats a single value for PostgreSQL array.
83
     */
84 19
    private static function formatValue(mixed $value): string
85
    {
86
        // Handle null
87 19
        if ($value === null) {
88
            return 'NULL';
89
        }
90
91
        // Handle actual numbers
92 19
        if (\is_int($value) || \is_float($value)) {
93 6
            return (string) $value;
94
        }
95
96
        // Handle booleans
97 17
        if (\is_bool($value)) {
98 1
            return $value ? 'true' : 'false';
99
        }
100
101
        // Handle objects that implement __toString()
102 16
        if (\is_object($value)) {
103 1
            if (\method_exists($value, '__toString')) {
104 1
                $stringValue = $value->__toString();
105
            } else {
106
                // For objects without __toString, use a default representation
107
                $stringValue = $value::class;
108
            }
109
        } else {
110
            // For all other types, force string conversion
111
            // This covers strings, resources, and other types
112 15
            $stringValue = match (true) {
113 15
                \is_resource($value) => '(resource)',
114 14
                default => (string) $value // @phpstan-ignore-line
115 15
            };
116
        }
117
118 16
        \assert(\is_string($stringValue));
119
120
        // Handle empty string
121 16
        if ($stringValue === '') {
122 1
            return '""';
123
        }
124
125 15
        if (self::isNumericSimple($stringValue)) {
126 9
            return '"'.$stringValue.'"';
127
        }
128
129
        // Double the backslashes and escape quotes
130 7
        $escaped = \str_replace(
131 7
            ['\\', '"'],
132 7
            ['\\\\', '\"'],
133 7
            $stringValue
134 7
        );
135
136 7
        return '"'.$escaped.'"';
137
    }
138
139 15
    private static function isNumericSimple(string $value): bool
140
    {
141
        // Fast path for obvious numeric strings
142 15
        if ($value === '' || $value[0] === '"') {
143 1
            return false;
144
        }
145
146
        // Handle scientific notation
147 15
        $lower = \strtolower($value);
148 15
        if (\str_contains($lower, 'e')) {
149 8
            $value = \str_replace('e', '', $lower);
150
        }
151
152
        // Use built-in numeric check
153 15
        return \is_numeric($value);
154
    }
155
156 15
    private static function unescapeString(string $value): string
157
    {
158
        // First handle escaped quotes
159 15
        $value = \str_replace('\"', '___QUOTE___', $value);
160
161
        // Handle double backslashes
162 15
        $value = \str_replace('\\\\', '___DBLBACK___', $value);
163
164
        // Restore double backslashes
165 15
        $value = \str_replace('___DBLBACK___', '\\\\', $value);
166
167
        // Finally restore quotes
168 15
        return \str_replace('___QUOTE___', '"', $value);
169
    }
170
}
171