Test Failed
Push — renovate/major-composer-qa-too... ( 6faed2...5f6191 )
by
unknown
11:41
created

transformPostgresArrayToPHPArray()   D

Complexity

Conditions 18
Paths 19

Size

Total Lines 63
Code Lines 31

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 0
CRAP Score 342

Importance

Changes 1
Bugs 1 Features 0
Metric Value
cc 18
eloc 31
c 1
b 1
f 0
nc 19
nop 1
dl 0
loc 63
ccs 0
cts 32
cp 0
crap 342
rs 4.8666

How to fix   Long Method    Complexity   

Long Method

Small methods make your code easier to understand, in particular if combined with a good name. Besides, if your method is small, finding a good name is usually much easier.

For example, if you find yourself adding comments to a method's body, this is usually a good sign to extract the commented part to a new method, and use the comment as a starting point when coming up with a good name for this new method.

Commonly applied refactorings include:

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
    public static function transformPostgresArrayToPHPArray(string $postgresArray): array
30
    {
31
        $trimmed = \trim($postgresArray);
32
33
        if ($trimmed === '' || \strtolower($trimmed) === self::POSTGRESQL_NULL_VALUE) {
34
            return [];
35
        }
36
37
        if (\str_contains($trimmed, '},{') || \str_starts_with($trimmed, '{{')) {
38
            throw InvalidArrayFormatException::multiDimensionalArrayNotSupported();
39
        }
40
41
        if ($trimmed === self::POSTGRESQL_EMPTY_ARRAY) {
42
            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
        $content = \trim($trimmed, self::POSTGRESQL_EMPTY_ARRAY);
48
        $inQuotes = false;
49
        $escaping = false;
50
51
        for ($i = 0, $len = \strlen($content); $i < $len; $i++) {
52
            $char = $content[$i];
53
54
            if ($escaping) {
55
                $escaping = false;
56
57
                continue;
58
            }
59
60
            if ($char === '\\' && $inQuotes) {
61
                $escaping = true;
62
63
                continue;
64
            }
65
66
            if ($char === '"') {
67
                $inQuotes = !$inQuotes;
0 ignored issues
show
introduced by
The condition $inQuotes is always false.
Loading history...
68
            } elseif (($char === '{' || $char === '}') && !$inQuotes) {
69
                throw InvalidArrayFormatException::invalidFormat('Malformed array nesting detected');
70
            }
71
        }
72
73
        // Check for unclosed quotes
74
        if ($inQuotes) {
0 ignored issues
show
introduced by
The condition $inQuotes is always false.
Loading history...
75
            throw InvalidArrayFormatException::invalidFormat('Unclosed quotes in array');
76
        }
77
78
        // First try with json_decode for properly quoted values
79
        $jsonArray = '['.\trim($trimmed, self::POSTGRESQL_EMPTY_ARRAY).']';
80
81
        /** @var array<int, mixed>|null $decoded */
82
        $decoded = \json_decode($jsonArray, true, 512, JSON_BIGINT_AS_STRING);
83
84
        // If json_decode fails, try manual parsing for unquoted strings
85
        if ($decoded === null && \json_last_error() !== JSON_ERROR_NONE) {
86
            return self::parsePostgresArrayManually($content);
87
        }
88
89
        return \array_map(
90
            static fn (mixed $value): mixed => \is_string($value) ? self::unescapeString($value) : $value,
91
            (array) $decoded
92
        );
93
    }
94
95
    private static function parsePostgresArrayManually(string $content): array
96
    {
97
        if ($content === '') {
98
            return [];
99
        }
100
101
        // Parse the array manually, handling quoted and unquoted values
102
        $result = [];
103
        $inQuotes = false;
104
        $currentValue = '';
105
        $escaping = false;
106
107
        for ($i = 0, $len = \strlen($content); $i < $len; $i++) {
108
            $char = $content[$i];
109
110
            // Handle escaping within quotes
111
            if ($escaping) {
112
                $currentValue .= $char;
113
                $escaping = false;
114
115
                continue;
116
            }
117
118
            if ($char === '\\' && $inQuotes) {
119
                $escaping = true;
120
                $currentValue .= $char;
121
122
                continue;
123
            }
124
125
            if ($char === '"') {
126
                $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
                $currentValue .= $char;
129
            } elseif ($char === ',' && !$inQuotes) {
130
                // End of value
131
                $result[] = self::processPostgresValue($currentValue);
132
                $currentValue = '';
133
            } else {
134
                $currentValue .= $char;
135
            }
136
        }
137
138
        // Add the last value
139
        if ($currentValue !== '') {
140
            $result[] = self::processPostgresValue($currentValue);
141
        }
142
143
        return $result;
144
    }
145
146
    /**
147
     * Process a single value from a PostgreSQL array.
148
     */
149
    private static function processPostgresValue(string $value): mixed
150
    {
151
        $value = \trim($value);
152
153
        if (self::isNullValue($value)) {
154
            return null;
155
        }
156
157
        if (self::isBooleanValue($value)) {
158
            return self::processBooleanValue($value);
159
        }
160
161
        if (self::isQuotedString($value)) {
162
            return self::processQuotedString($value);
163
        }
164
165
        if (self::isNumericValue($value)) {
166
            return self::processNumericValue($value);
167
        }
168
169
        // For unquoted strings, return as is
170
        return $value;
171
    }
172
173
    private static function isNullValue(string $value): bool
174
    {
175
        return $value === 'NULL' || $value === 'null';
176
    }
177
178
    private static function isBooleanValue(string $value): bool
179
    {
180
        return \in_array($value, ['true', 't', 'false', 'f'], true);
181
    }
182
183
    private static function processBooleanValue(string $value): bool
184
    {
185
        return $value === 'true' || $value === 't';
186
    }
187
188
    private static function isQuotedString(string $value): bool
189
    {
190
        return \strlen($value) >= 2 && $value[0] === '"' && $value[\strlen($value) - 1] === '"';
191
    }
192
193
    private static function processQuotedString(string $value): string
194
    {
195
        // Remove the quotes and unescape the string
196
        $unquoted = \substr($value, 1, -1);
197
198
        return self::unescapeString($unquoted);
199
    }
200
201
    private static function isNumericValue(string $value): bool
202
    {
203
        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
    private static function unescapeString(string $value): string
217
    {
218
        $result = '';
219
        $len = \strlen($value);
220
        $i = 0;
221
        $backslashCount = 0;
222
223
        while ($i < $len) {
224
            if ($value[$i] === '\\') {
225
                $backslashCount++;
226
                $i++;
227
228
                continue;
229
            }
230
231
            if ($backslashCount > 0) {
232
                if ($value[$i] === '"') {
233
                    // This is an escaped quote
234
                    $result .= \str_repeat('\\', (int) ($backslashCount / 2));
235
                    if ($backslashCount % 2 === 1) {
236
                        $result .= '"';
237
                    } else {
238
                        $result .= '\"';
239
                    }
240
                } else {
241
                    // These are literal backslashes
242
                    $result .= \str_repeat('\\', $backslashCount);
243
                    $result .= $value[$i];
244
                }
245
246
                $backslashCount = 0;
247
            } else {
248
                $result .= $value[$i];
249
            }
250
251
            $i++;
252
        }
253
254
        // Handle any trailing backslashes
255
        if ($backslashCount > 0) {
256
            $result .= \str_repeat('\\', $backslashCount);
257
        }
258
259
        return $result;
260
    }
261
}
262