| Total Complexity | 53 | 
| Total Lines | 244 | 
| Duplicated Lines | 0 % | 
| Changes | 0 | ||
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 | ||
| 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, '{}'); | ||
| 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; | ||
|  | |||
| 68 |             } elseif (($char === '{' || $char === '}') && !$inQuotes) { | ||
| 69 |                 throw InvalidArrayFormatException::invalidFormat('Malformed array nesting detected'); | ||
| 70 | } | ||
| 71 | } | ||
| 72 | |||
| 73 | // Check for unclosed quotes | ||
| 74 |         if ($inQuotes) { | ||
| 75 |             throw InvalidArrayFormatException::invalidFormat('Unclosed quotes in array'); | ||
| 76 | } | ||
| 77 | |||
| 78 | // First try with json_decode for properly quoted values | ||
| 79 |         $jsonArray = '['.\trim($trimmed, '{}').']'; | ||
| 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; | ||
| 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 | ||
| 199 | } | ||
| 200 | |||
| 201 | private static function isNumericValue(string $value): bool | ||
| 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) { | ||
| 214 | } | ||
| 215 | |||
| 216 | private static function unescapeString(string $value): string | ||
| 262 |