فهرست منبع

исправлена ошибка базы

ilg2005 3 ماه پیش
والد
کامیت
bce7c535bd
1فایلهای تغییر یافته به همراه85 افزوده شده و 2 حذف شده
  1. 85 2
      app/Casts/BinaryTextCast.php

+ 85 - 2
app/Casts/BinaryTextCast.php

@@ -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;
+    }
 }