Bladeren bron

рефакторинг CharMail

ilg2005 3 maanden geleden
bovenliggende
commit
f4f5a67b16
2 gewijzigde bestanden met toevoegingen van 153 en 167 verwijderingen
  1. 148 0
      app/Casts/BinaryTextCast.php
  2. 5 167
      app/Models/Char/CharMail.php

+ 148 - 0
app/Casts/BinaryTextCast.php

@@ -0,0 +1,148 @@
+<?php
+
+namespace App\Casts;
+
+use Illuminate\Contracts\Database\Eloquent\CastsAttributes;
+use Illuminate\Support\Facades\DB;
+
+/**
+ * Каст для двоичных полей MSSQL (VARBINARY).
+ *
+ * Чтение: удаляет нулевые байты в конце, пытается дважды декодировать base64,
+ * затем приводит к UTF-8 и убирает управляющие символы.
+ * Запись: принимает человекочитаемую строку, при необходимости конвертирует из UTF-8 в CP1251,
+ * выполняет двойной base64 и сохраняет как HEX-литерал 0x... для совместимости с VARBINARY на MSSQL.
+ * Пустая строка сохраняется как 0x, NULL — как NULL.
+ */
+class BinaryTextCast implements CastsAttributes
+{
+    /**
+     * Декодирует значение из двойного base64 и приводит к UTF-8.
+     */
+    public function get($model, string $key, $value, array $attributes)
+    {
+        $value = $this->stripNulls($value);
+        if ($value === null || $value === '') {
+            return $value;
+        }
+
+        $decoded = $value;
+        for ($i = 0; $i < 2; $i++) {
+            $tmp = base64_decode($decoded, true);
+            if ($tmp === false) {
+                break;
+            }
+            $decoded = $this->stripNulls($tmp);
+        }
+
+        return $decoded === null ? null : $this->ensureUtf8($decoded);
+    }
+
+    /**
+     * Подготавливает строку к сохранению в VARBINARY: CP1251 → двойной base64 → HEX 0x...
+     */
+    public function set($model, string $key, $value, array $attributes)
+    {
+        $value = $this->stripNulls($value);
+        if ($value === null) {
+            return $value; // сохраняем NULL как есть
+        }
+        if ($value === '') {
+            // Пустая бинарная строка для VARBINARY в MSSQL
+            return DB::raw('0x');
+        }
+
+        if ($this->isBase64($value)) {
+            $inner = base64_decode($value, true);
+            if ($inner !== false && $this->isBase64($inner)) {
+                $encoded = $value; // Уже двойной base64
+            } else {
+                $encoded = base64_encode($value);
+            }
+        } else {
+            $prepared = function_exists('iconv') ? @iconv('UTF-8', 'CP1251//IGNORE', $value) : $value;
+            if ($prepared === false) {
+                $prepared = $value;
+            }
+            $encoded = base64_encode(base64_encode($prepared));
+        }
+
+        // MSSQL VARBINARY требует явного бинарного литерала
+        return DB::raw('0x' . bin2hex($encoded));
+    }
+
+    /**
+     * Удаляет завершающие нулевые байты (часто встречаются в char(n)).
+     */
+    private function stripNulls(?string $value): ?string
+    {
+        return $value === null ? null : rtrim($value, "\0");
+    }
+
+    /**
+     * Лояльная проверка, что строка похожа на base64.
+     */
+    private function isBase64(string $value): bool
+    {
+        if ($value === '') {
+            return false;
+        }
+        if (preg_match('/^[A-Za-z0-9+\/\r\n]+=*$/', $value) !== 1) {
+            return false;
+        }
+        return (strlen($value) % 4 === 0);
+    }
+
+    /**
+     * Гарантирует корректный UTF-8, пытаясь конвертацию из CP1251/ISO-8859-1
+     * и удаляя управляющие символы.
+     */
+    private function ensureUtf8(string $value): string
+    {
+        if ($value === '') {
+            return '';
+        }
+
+        if (mb_check_encoding($value, 'UTF-8')) {
+            return $this->removeControlChars($value);
+        }
+
+        $candidates = [];
+
+        if (function_exists('iconv')) {
+            $candidates[] = fn () => @iconv('CP1251', 'UTF-8//IGNORE', $value);
+        }
+
+        if (function_exists('mb_convert_encoding')) {
+            $candidates[] = fn () => @mb_convert_encoding($value, 'UTF-8', 'CP1251');
+            $candidates[] = fn () => @mb_convert_encoding($value, 'UTF-8', 'ISO-8859-1');
+        }
+
+        $candidates[] = fn () => utf8_encode($value);
+
+        foreach ($candidates as $candidate) {
+            $converted = $candidate();
+            if (is_string($converted) && mb_check_encoding($converted, 'UTF-8')) {
+                return $this->removeControlChars($converted);
+            }
+        }
+
+        if (function_exists('iconv')) {
+            $sanitized = @iconv('UTF-8', 'UTF-8//IGNORE', $value);
+            if (is_string($sanitized) && mb_check_encoding($sanitized, 'UTF-8')) {
+                return $this->removeControlChars($sanitized);
+            }
+        }
+
+        return '';
+    }
+
+    /**
+     * Удаляет управляющие ASCII-символы (0x00–0x1F, 0x7F).
+     */
+    private function removeControlChars(string $value): string
+    {
+        $clean = preg_replace('/[\x00-\x08\x0B\x0C\x0E-\x1F\x7F]/u', '', $value);
+        return is_string($clean) ? $clean : '';
+    }
+}

+ 5 - 167
app/Models/Char/CharMail.php

@@ -3,7 +3,7 @@
 namespace App\Models\Char;
 
 use Illuminate\Database\Eloquent\Model;
-use Illuminate\Support\Facades\DB;
+use App\Casts\BinaryTextCast;
 
 class CharMail extends Model
 {
@@ -53,170 +53,8 @@ class CharMail extends Model
 
     public $timestamps = false;
 
-    public function getBinLetterAttribute($value)
-    {
-        return $this->decodeBinaryValue($value);
-    }
-
-    public function setBinLetterAttribute($value)
-    {
-        // В Table_CharMail колонка binLetter хранится как VARBINARY,
-        // поэтому передаём в UPDATE двоичные данные через HEX-литерал (0x...)
-        // чтобы избежать имплицитного преобразования NVARCHAR -> VARBINARY.
-        $encoded = $this->encodeBinaryValue($value); // ожидается двойной base64-ASCII
-
-        // Сохраняем ASCII base64 как набор байт (varbinary) — это позволит
-        // корректно прочитать значение и декодировать его в accessor'е.
-        $hex = '0x' . bin2hex($encoded);
-        $this->attributes['binLetter'] = DB::raw($hex);
-    }
-
-    public function getBinTitleAttribute($value)
-    {
-        return $this->decodeBinaryValue($value);
-    }
-
-    public function setBinTitleAttribute($value)
-    {
-        $this->attributes['binTitle'] = $this->encodeBinaryValue($value);
-    }
-
-    private function decodeBinaryValue(?string $value): ?string
-    {
-        // Удаляем завершающие нули – они часто встречаются в char( n ) столбцах
-        $value = $this->stripNulls($value);
-        if ($value === null || $value === '') {
-            return $value;
-        }
-
-        // Пробуем декодировать максимум два раза (схема хранения – двойной base64)
-        $decoded = $value;
-        for ($i = 0; $i < 2; $i++) {
-            $tmp = base64_decode($decoded, true);
-            if ($tmp === false) {
-                // Строка не похожа на base64 – прекращаем попытки
-                break;
-            }
-            $decoded = $this->stripNulls($tmp);
-        }
-
-        return $decoded === null ? null : $this->ensureUtf8($decoded);
-    }
-
-    private function encodeBinaryValue(?string $value): ?string
-    {
-        $value = $this->stripNulls($value);
-        if ($value === null || $value === '') {
-            return $value;
-        }
-
-        // Если строка уже дважды закодирована – ничего не делаем
-        if ($this->isBase64($value)) {
-            $inner = base64_decode($value, true);
-            if ($inner !== false && $this->isBase64($inner)) {
-                return $value; // уже двойной base64
-            }
-            // Если только один слой – добавляем ещё один
-            return base64_encode($value);
-        }
-
-        // Конвертируем в CP1251 (игровой клиент ожидает именно эту кодировку)
-        $prepared = @iconv('UTF-8', 'CP1251//IGNORE', $value);
-        if ($prepared === false) {
-            $prepared = $value; // если iconv не смог – используем как есть
-        }
-
-        // Двойное кодирование base64
-        return base64_encode(base64_encode($prepared));
-    }
-
-    private function stripNulls(?string $value): ?string
-    {
-        return $value === null ? null : rtrim($value, "\0");
-    }
-
-    // Более лояльная проверка base64 (без строгого ===, учитываем отсутствие отступов и символы \r\n)
-    private function isBase64(string $value): bool
-    {
-        if ($value === '') {
-            return false;
-        }
-        // Проверяем набор допустимых символов и кратность 4
-        if (preg_match('/^[A-Za-z0-9+\/\r\n]+=*$/', $value) !== 1) {
-            return false;
-        }
-        return (strlen($value) % 4 === 0);
-    }
-
-    private function ensureUtf8(string $value): string
-    {
-        if ($value === '') {
-            return '';
-        }
-
-        if (mb_check_encoding($value, 'UTF-8')) {
-            return $this->removeControlChars($value);
-        }
-
-        $candidates = [];
-
-        if (function_exists('iconv')) {
-            $candidates[] = fn () => @iconv('CP1251', 'UTF-8//IGNORE', $value);
-        }
-
-        if (function_exists('mb_convert_encoding')) {
-            $candidates[] = fn () => @mb_convert_encoding($value, 'UTF-8', 'CP1251');
-            $candidates[] = fn () => @mb_convert_encoding($value, 'UTF-8', 'ISO-8859-1');
-        }
-
-        $candidates[] = fn () => utf8_encode($value);
-
-        foreach ($candidates as $candidate) {
-            $converted = $candidate();
-            if (is_string($converted) && mb_check_encoding($converted, 'UTF-8')) {
-                return $this->removeControlChars($converted);
-            }
-        }
-
-        if (function_exists('iconv')) {
-            $sanitized = @iconv('UTF-8', 'UTF-8//IGNORE', $value);
-            if (is_string($sanitized) && mb_check_encoding($sanitized, 'UTF-8')) {
-                return $this->removeControlChars($sanitized);
-            }
-        }
-
-        return '';
-    }
-
-    private function removeControlChars(string $value): string
-    {
-        $clean = preg_replace('/[\x00-\x08\x0B\x0C\x0E-\x1F\x7F]/u', '', $value);
-        return is_string($clean) ? $clean : '';
-    }
-
-    public function toArray()
-    {
-        $array = parent::toArray();
-
-        // Явно декодируем бинарные поля, на случай если accessor не сработал при сериализации
-        if (array_key_exists('binTitle', $this->attributes)) {
-            $array['binTitle'] = $this->decodeBinaryValue($this->attributes['binTitle']);
-        } elseif (array_key_exists('binTitle', $array)) {
-            $array['binTitle'] = $this->decodeBinaryValue($array['binTitle']);
-        }
-
-        if (array_key_exists('binLetter', $this->attributes)) {
-            $array['binLetter'] = $this->decodeBinaryValue($this->attributes['binLetter']);
-        } elseif (array_key_exists('binLetter', $array)) {
-            $array['binLetter'] = $this->decodeBinaryValue($array['binLetter']);
-        }
-
-        array_walk_recursive($array, function (&$v) {
-            if (is_string($v)) {
-                $v = $this->ensureUtf8($v);
-            }
-        });
-        return $array;
-    }
-
+    protected $casts = [
+        'binTitle' => BinaryTextCast::class,
+        'binLetter' => BinaryTextCast::class,
+    ];
 }