|
|
@@ -16,6 +16,11 @@ use Illuminate\Support\Facades\DB;
|
|
|
*/
|
|
|
class BinaryTextCast implements CastsAttributes
|
|
|
{
|
|
|
+ /**
|
|
|
+ * Кэш размеров колонок VARBINARY: ["connection.table.column" => int|null]
|
|
|
+ */
|
|
|
+ private static $maxLenCache = [];
|
|
|
+
|
|
|
/**
|
|
|
* Декодирует значение из двойного base64 и приводит к UTF-8.
|
|
|
*/
|
|
|
@@ -52,21 +57,42 @@ class BinaryTextCast implements CastsAttributes
|
|
|
return DB::raw('0x');
|
|
|
}
|
|
|
|
|
|
+ // Определяем максимально допустимую длину VARBINARY для данной колонки
|
|
|
+ $maxLen = $this->getVarbinaryMaxLength($model, $key);
|
|
|
+
|
|
|
+ // Готовим "сырой" CP1251 (или то, что пришло в base64) и итоговый двойной base64
|
|
|
+ $raw = null;
|
|
|
if ($this->isBase64($value)) {
|
|
|
$inner = base64_decode($value, true);
|
|
|
if ($inner !== false && $this->isBase64($inner)) {
|
|
|
- $encoded = $value; // Уже двойной base64
|
|
|
+ // Значение уже двойной base64: пробуем восстановить сырой текст
|
|
|
+ $raw = base64_decode($inner, true);
|
|
|
+ $encoded = $value; // пока используем исходный двойной base64
|
|
|
} else {
|
|
|
+ // Только один слой base64 → сырой текст после одного декодирования
|
|
|
+ $raw = $inner !== false ? $inner : null;
|
|
|
$encoded = base64_encode($value);
|
|
|
}
|
|
|
- } else {
|
|
|
+ }
|
|
|
+
|
|
|
+ if ($raw === null) {
|
|
|
+ // Готовим сырой CP1251 из UTF-8
|
|
|
$prepared = function_exists('iconv') ? @iconv('UTF-8', 'CP1251//IGNORE', $value) : $value;
|
|
|
if ($prepared === false) {
|
|
|
$prepared = $value;
|
|
|
}
|
|
|
+ $raw = $prepared;
|
|
|
$encoded = base64_encode(base64_encode($prepared));
|
|
|
}
|
|
|
|
|
|
+ // Если известна максимальная длина и результат не помещается,
|
|
|
+ // пересчитываем кодирование для усечения по сырой длине так, чтобы двойной base64 гарантированно влез
|
|
|
+ if (is_int($maxLen) && strlen($encoded) > $maxLen) {
|
|
|
+ $maxRaw = $this->maxRawLenForDoubleBase64($maxLen);
|
|
|
+ $raw = substr($raw, 0, max(0, $maxRaw));
|
|
|
+ $encoded = base64_encode(base64_encode($raw));
|
|
|
+ }
|
|
|
+
|
|
|
// MSSQL VARBINARY требует явного бинарного литерала
|
|
|
return DB::raw('0x' . bin2hex($encoded));
|
|
|
}
|
|
|
@@ -145,4 +171,61 @@ class BinaryTextCast implements CastsAttributes
|
|
|
$clean = preg_replace('/[\x00-\x08\x0B\x0C\x0E-\x1F\x7F]/u', '', $value);
|
|
|
return is_string($clean) ? $clean : '';
|
|
|
}
|
|
|
+
|
|
|
+ /**
|
|
|
+ * Возвращает максимально допустимую длину VARBINARY для указанной колонки.
|
|
|
+ * Для varbinary(max) возвращает null (без ограничения).
|
|
|
+ */
|
|
|
+ private function getVarbinaryMaxLength($model, string $key): ?int
|
|
|
+ {
|
|
|
+ try {
|
|
|
+ $connection = $model->getConnectionName();
|
|
|
+ $table = $model->getTable();
|
|
|
+ $cacheKey = $connection . '.' . $table . '.' . $key;
|
|
|
+ if (array_key_exists($cacheKey, self::$maxLenCache)) {
|
|
|
+ return self::$maxLenCache[$cacheKey];
|
|
|
+ }
|
|
|
+
|
|
|
+ $conn = DB::connection($connection);
|
|
|
+ // Запрашиваем max_length из системных таблиц
|
|
|
+ $sql = "SELECT c.max_length, t.name AS type_name
|
|
|
+ FROM sys.columns c
|
|
|
+ JOIN sys.types t ON c.user_type_id = t.user_type_id
|
|
|
+ JOIN sys.tables tb ON c.object_id = tb.object_id
|
|
|
+ WHERE tb.name = ? AND c.name = ?";
|
|
|
+ $rows = $conn->select($sql, [$table, $key]);
|
|
|
+ if (!empty($rows)) {
|
|
|
+ $row = (array) $rows[0];
|
|
|
+ $type = isset($row['type_name']) ? $row['type_name'] : ($row['type_name'] ?? null);
|
|
|
+ $max = isset($row['max_length']) ? (int) $row['max_length'] : null;
|
|
|
+ // Для varbinary(max) max_length = -1
|
|
|
+ if ($max === -1 && in_array(strtolower((string) $type), ['varbinary', 'binary'], true)) {
|
|
|
+ self::$maxLenCache[$cacheKey] = null;
|
|
|
+ return null;
|
|
|
+ }
|
|
|
+ self::$maxLenCache[$cacheKey] = $max;
|
|
|
+ return $max;
|
|
|
+ }
|
|
|
+ } catch (\Throwable $e) {
|
|
|
+ // Игнорируем ошибки определения схемы, работаем без ограничения
|
|
|
+ }
|
|
|
+ return null;
|
|
|
+ }
|
|
|
+
|
|
|
+ /**
|
|
|
+ * Вычисляет максимально допустимую длину "сырого" текста (в байтах),
|
|
|
+ * чтобы длина двойного base64 не превышала $maxLen.
|
|
|
+ */
|
|
|
+ private function maxRawLenForDoubleBase64(int $maxLen): int
|
|
|
+ {
|
|
|
+ // Итеративный безопасный расчёт
|
|
|
+ for ($n = $maxLen; $n >= 0; $n--) {
|
|
|
+ $b1 = 4 * (int) ceil($n / 3);
|
|
|
+ $b2 = 4 * (int) ceil($b1 / 3);
|
|
|
+ if ($b2 <= $maxLen) {
|
|
|
+ return $n;
|
|
|
+ }
|
|
|
+ }
|
|
|
+ return 0;
|
|
|
+ }
|
|
|
}
|