Passed
Push — float-arrays ( f8b99a )
by Martin
10:40
created

DataStructure   A

Complexity

Total Complexity 23

Size/Duplication

Total Lines 128
Duplicated Lines 0 %

Test Coverage

Coverage 94.23%

Importance

Changes 2
Bugs 1 Features 0
Metric Value
eloc 48
dl 0
loc 128
ccs 49
cts 52
cp 0.9423
rs 10
c 2
b 1
f 0
wmc 23

5 Methods

Rating   Name   Duplication   Size   Complexity  
B transformPostgresTextArrayToPHPArray() 0 26 9
A formatValue() 0 32 6
A unescapeString() 0 16 1
A isNumericSimple() 0 15 4
A transformPHPArrayToPostgresTextArray() 0 13 3
1
<?php
2
3
declare(strict_types=1);
4
5
namespace MartinGeorgiev\Utils;
6
7
/**
8
 * Util class with helpers for working with PostgreSQL data structures.
9
 *
10
 * @since 0.9
11
 *
12
 * @author Martin Georgiev <[email protected]>
13
 */
14
class DataStructure
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 14
    public static function transformPostgresTextArrayToPHPArray(string $postgresArray): array
25
    {
26 14
        $trimmed = \trim($postgresArray);
27
28 14
        if ($trimmed === '' || \strtolower($trimmed) === self::POSTGRESQL_NULL_VALUE) {
29
            return [];
30
        }
31
32 14
        if (\str_contains($trimmed, '},{') || \str_starts_with($trimmed, '{{')) {
33 1
            throw new \InvalidArgumentException('Only single-dimensioned arrays are supported');
34
        }
35
36 13
        if ($trimmed === self::POSTGRESQL_EMPTY_ARRAY) {
37 1
            return [];
38
        }
39
40 12
        $jsonArray = '['.\trim($trimmed, '{}').']';
41
42 12
        $decoded = \json_decode($jsonArray, true, 512, JSON_BIGINT_AS_STRING);
43 12
        if ($decoded === null && \json_last_error() !== JSON_ERROR_NONE) {
44
            throw new \InvalidArgumentException('Invalid array format: '.\json_last_error_msg());
45
        }
46
47 12
        return \array_map(
48 12
            static fn ($value): mixed => \is_string($value) ? self::unescapeString($value) : $value,
49 12
            $decoded
50 12
        );
51
    }
52
53
    /**
54
     * This method supports only single-dimensioned PHP arrays.
55
     * This method relays on the default escaping strategy in PostgreSQL (double quotes).
56
     */
57 14
    public static function transformPHPArrayToPostgresTextArray(array $phpArray): string
58
    {
59 14
        if ($phpArray === []) {
60 1
            return self::POSTGRESQL_EMPTY_ARRAY;
61
        }
62
63 13
        if (\array_filter($phpArray, 'is_array')) {
64 1
            throw new \InvalidArgumentException('Only single-dimensioned arrays are supported');
65
        }
66
67 12
        $processed = \array_map(static fn ($value): string => self::formatValue($value), $phpArray);
68
69 12
        return '{'.\implode(',', $processed).'}';
70
    }
71
72
    /**
73
     * Formats a single value for PostgreSQL array.
74
     */
75 12
    private static function formatValue(mixed $value): string
76
    {
77
        // Handle null
78 12
        if ($value === null) {
79
            return 'NULL';
80
        }
81
82
        // Handle actual numbers
83 12
        if (\is_int($value) || \is_float($value)) {
84 5
            return (string) $value;
85
        }
86
87
        // Convert to string if not already
88 10
        $stringValue = (string) $value;
89
90
        // Handle empty string
91 10
        if ($stringValue === '') {
92 1
            return '""';
93
        }
94
95 9
        if (self::isNumericSimple($stringValue)) {
96 7
            return '"'.$stringValue.'"';
97
        }
98
99
        // Double the backslashes and escape quotes
100 3
        $escaped = \str_replace(
101 3
            ['\\', '"'],
102 3
            ['\\\\', '\"'],
103 3
            $stringValue
104 3
        );
105
106 3
        return '"'.$escaped.'"';
107
    }
108
109 9
    private static function isNumericSimple(string $value): bool
110
    {
111
        // Fast path for obvious numeric strings
112 9
        if ($value === '' || $value[0] === '"') {
113 1
            return false;
114
        }
115
116
        // Handle scientific notation
117 9
        $lower = \strtolower($value);
118 9
        if (\str_contains($lower, 'e')) {
119 5
            $value = \str_replace('e', '', $lower);
120
        }
121
122
        // Use built-in numeric check
123 9
        return \is_numeric($value);
124
    }
125
126 10
    private static function unescapeString(string $value): string
127
    {
128
        // First handle escaped quotes
129 10
        $value = \str_replace('\"', '___QUOTE___', $value);
130
131
        // Handle double backslashes
132 10
        $value = \str_replace('\\\\', '___DBLBACK___', $value);
133
134
        // Handle remaining single backslashes
135 10
        $value = \str_replace('\\', '\\', $value);
136
137
        // Restore double backslashes
138 10
        $value = \str_replace('___DBLBACK___', '\\\\', $value);
139
140
        // Finally restore quotes
141 10
        return \str_replace('___QUOTE___', '"', $value);
142
    }
143
}
144