PostgresArrayToPHPArrayTransformer   C
last analyzed

Complexity

Total Complexity 55

Size/Duplication

Total Lines 232
Duplicated Lines 0 %

Test Coverage

Coverage 94.5%

Importance

Changes 1
Bugs 1 Features 0
Metric Value
eloc 102
c 1
b 1
f 0
dl 0
loc 232
ccs 103
cts 109
cp 0.945
rs 6
wmc 55

11 Methods

Rating   Name   Duplication   Size   Complexity  
A processQuotedString() 0 6 1
A isNullValue() 0 3 2
A processBooleanValue() 0 3 2
A isNumericValue() 0 3 1
A isQuotedString() 0 3 3
B parsePostgresArrayManually() 0 49 10
A isBooleanValue() 0 3 1
D transformPostgresArrayToPHPArray() 0 63 18
A processPostgresValue() 0 22 5
A processNumericValue() 0 8 3
B unescapeString() 0 32 9

How to fix   Complexity   

Complex Class

Complex classes like PostgresArrayToPHPArrayTransformer often do a lot of different things. To break such a class down, we need to identify a cohesive component within that class. A common approach to find such a component is to look for fields/methods that share the same prefixes, or suffixes.

Once you have determined the fields that belong together, you can apply the Extract Class refactoring. If the component makes sense as a sub-class, Extract Subclass is also a candidate, and is often faster.

While breaking up the class, it is a good idea to analyze how other classes use PostgresArrayToPHPArrayTransformer, and based on these observations, apply Extract Interface, too.

1
<?php
2
3
declare(strict_types=1);
4
5
namespace MartinGeorgiev\Utils;
6
7
use MartinGeorgiev\Utils\Exception\InvalidArrayFormatException;
8
9
/**
10
 * Handles transformation from PostgreSQL text arrays to PHP values.
11
 *
12
 * @since 3.0
13
 *
14
 * @author Martin Georgiev <[email protected]>
15
 */
16
class PostgresArrayToPHPArrayTransformer
17
{
18
    private const POSTGRESQL_EMPTY_ARRAY = '{}';
19
20
    private const POSTGRESQL_NULL_VALUE = 'null';
21
22
    /**
23
     * Transforms a PostgreSQL text array to a PHP array.
24
     * This method supports only single-dimensioned text arrays and
25
     * relays on the default escaping strategy in PostgreSQL (double quotes).
26
     *
27
     * @throws InvalidArrayFormatException when the input is a multi-dimensional array or has invalid format
28
     */
29 42
    public static function transformPostgresArrayToPHPArray(string $postgresArray): array
30
    {
31 42
        $trimmed = \trim($postgresArray);
32
33 42
        if ($trimmed === '' || \strtolower($trimmed) === self::POSTGRESQL_NULL_VALUE) {
34 3
            return [];
35
        }
36
37 39
        if (\str_contains($trimmed, '},{') || \str_starts_with($trimmed, '{{')) {
38 1
            throw InvalidArrayFormatException::multiDimensionalArrayNotSupported();
39
        }
40
41 38
        if ($trimmed === self::POSTGRESQL_EMPTY_ARRAY) {
42 1
            return [];
43
        }
44
45
        // Check for malformed nesting - this is a more specific check than the one above
46
        // But we need to exclude cases where curly braces are part of quoted strings
47 37
        $content = \trim($trimmed, self::POSTGRESQL_EMPTY_ARRAY);
48 37
        $inQuotes = false;
49 37
        $escaping = false;
50
51 37
        for ($i = 0, $len = \strlen($content); $i < $len; $i++) {
52 37
            $char = $content[$i];
53
54 37
            if ($escaping) {
55 14
                $escaping = false;
56
57 14
                continue;
58
            }
59
60 37
            if ($char === '\\' && $inQuotes) {
61 14
                $escaping = true;
62
63 14
                continue;
64
            }
65
66 37
            if ($char === '"') {
67 26
                $inQuotes = !$inQuotes;
0 ignored issues
show
introduced by
The condition $inQuotes is always false.
Loading history...
68 36
            } elseif (($char === '{' || $char === '}') && !$inQuotes) {
69 1
                throw InvalidArrayFormatException::invalidFormat('Malformed array nesting detected');
70
            }
71
        }
72
73
        // Check for unclosed quotes
74 36
        if ($inQuotes) {
0 ignored issues
show
introduced by
The condition $inQuotes is always false.
Loading history...
75 2
            throw InvalidArrayFormatException::invalidFormat('Unclosed quotes in array');
76
        }
77
78
        // First try with json_decode for properly quoted values
79 34
        $jsonArray = '['.\trim($trimmed, self::POSTGRESQL_EMPTY_ARRAY).']';
80
81
        /** @var array<int, mixed>|null $decoded */
82 34
        $decoded = \json_decode($jsonArray, true, 512, JSON_BIGINT_AS_STRING);
83
84
        // If json_decode fails, try manual parsing for unquoted strings
85 34
        if ($decoded === null && \json_last_error() !== JSON_ERROR_NONE) {
86 13
            return self::parsePostgresArrayManually($content);
87
        }
88
89 21
        return \array_map(
90 21
            static fn (mixed $value): mixed => \is_string($value) ? self::unescapeString($value) : $value,
91 21
            (array) $decoded
92 21
        );
93
    }
94
95 13
    private static function parsePostgresArrayManually(string $content): array
96
    {
97 13
        if ($content === '') {
98
            return [];
99
        }
100
101
        // Parse the array manually, handling quoted and unquoted values
102 13
        $result = [];
103 13
        $inQuotes = false;
104 13
        $currentValue = '';
105 13
        $escaping = false;
106
107 13
        for ($i = 0, $len = \strlen($content); $i < $len; $i++) {
108 13
            $char = $content[$i];
109
110
            // Handle escaping within quotes
111 13
            if ($escaping) {
112 4
                $currentValue .= $char;
113 4
                $escaping = false;
114
115 4
                continue;
116
            }
117
118 13
            if ($char === '\\' && $inQuotes) {
119 4
                $escaping = true;
120 4
                $currentValue .= $char;
121
122 4
                continue;
123
            }
124
125 13
            if ($char === '"') {
126 6
                $inQuotes = !$inQuotes;
0 ignored issues
show
introduced by
The condition $inQuotes is always false.
Loading history...
127
                // For quoted values, we include the quotes for later processing
128 6
                $currentValue .= $char;
129 13
            } elseif ($char === ',' && !$inQuotes) {
130
                // End of value
131 9
                $result[] = self::processPostgresValue($currentValue);
132 9
                $currentValue = '';
133
            } else {
134 13
                $currentValue .= $char;
135
            }
136
        }
137
138
        // Add the last value
139 13
        if ($currentValue !== '') {
140 12
            $result[] = self::processPostgresValue($currentValue);
141
        }
142
143 13
        return $result;
144
    }
145
146
    /**
147
     * Process a single value from a PostgreSQL array.
148
     */
149 13
    private static function processPostgresValue(string $value): mixed
150
    {
151 13
        $value = \trim($value);
152
153 13
        if (self::isNullValue($value)) {
154 1
            return null;
155
        }
156
157 13
        if (self::isBooleanValue($value)) {
158 1
            return self::processBooleanValue($value);
159
        }
160
161 12
        if (self::isQuotedString($value)) {
162 6
            return self::processQuotedString($value);
163
        }
164
165 8
        if (self::isNumericValue($value)) {
166
            return self::processNumericValue($value);
167
        }
168
169
        // For unquoted strings, return as is
170 8
        return $value;
171
    }
172
173 13
    private static function isNullValue(string $value): bool
174
    {
175 13
        return $value === 'NULL' || $value === 'null';
176
    }
177
178 13
    private static function isBooleanValue(string $value): bool
179
    {
180 13
        return \in_array($value, ['true', 't', 'false', 'f'], true);
181
    }
182
183 1
    private static function processBooleanValue(string $value): bool
184
    {
185 1
        return $value === 'true' || $value === 't';
186
    }
187
188 12
    private static function isQuotedString(string $value): bool
189
    {
190 12
        return \strlen($value) >= 2 && $value[0] === '"' && $value[\strlen($value) - 1] === '"';
191
    }
192
193 6
    private static function processQuotedString(string $value): string
194
    {
195
        // Remove the quotes and unescape the string
196 6
        $unquoted = \substr($value, 1, -1);
197
198 6
        return self::unescapeString($unquoted);
199
    }
200
201 8
    private static function isNumericValue(string $value): bool
202
    {
203 8
        return \is_numeric($value);
204
    }
205
206
    private static function processNumericValue(string $value): float|int
207
    {
208
        // Convert to int or float as appropriate
209
        if (\str_contains($value, '.') || \stripos($value, 'e') !== false) {
210
            return (float) $value;
211
        }
212
213
        return (int) $value;
214
    }
215
216 24
    private static function unescapeString(string $value): string
217
    {
218 24
        $result = '';
219 24
        $len = \strlen($value);
220 24
        $i = 0;
221 24
        while ($i < $len) {
222 23
            if ($value[$i] === '\\') {
223 12
                $start = $i;
224 12
                while ($i < $len && $value[$i] === '\\') {
225 12
                    $i++;
226
                }
227
228 12
                $slashCount = $i - $start;
229 12
                $nextChar = $i < $len ? $value[$i] : '';
230 12
                if ($nextChar === '"' || $nextChar === '\\') {
231 3
                    $result .= \str_repeat('\\', \intdiv($slashCount, 2));
232 3
                    if ($slashCount % 2 === 1) {
233 2
                        $result .= $nextChar;
234 2
                        $i++;
235
                    }
236
                } else {
237 10
                    $result .= \str_repeat('\\', $slashCount);
238
                }
239
240 12
                continue;
241
            }
242
243 23
            $result .= $value[$i];
244 23
            $i++;
245
        }
246
247 24
        return $result;
248
    }
249
}
250