Перейти к содержимому
Поиск
Php Разработка

PHP: полный мануал с примерами, функциями и ООП

Практический мануал PHP: от переменных и читаемого кода до безопасных форм, интеграций, базы данных и ООП на примере магазина. Учебный маршрут, поиск функций, подробные примеры, разбор ошибок и исходники для скачивания.

Александр НекрасовИзменено:
Практический мануал PHP: от переменных и функций к объектной модели интернет-магазина
PHP 8.5.11468 глав6256 карточек справочника
Дерево руководства

Глава 97 · После LOB2, параметризованных запросов и транзакций

CUBRID: разобраться в старом GLO-коде и перенести файлы в LOB2

Исторический контракт удалённых GLO-функций и четыре настоящих проекта LOB2: импорт по ведомости, UTF-8, замена SQL-ссылки и сверка.

Сначала понять старую связь, затем переносить содержимое

В старом PHP-проекте остались вызовы GLO, а документы нужны людям и после обновления. Здесь сначала разберём, что делал прежний код, затем перенесём учебный архив на работающий LOB2. Получатся ведомость импорта, файл инструкции, журнал замены и CSV сверки с понятными решениями для каждого документа.

Старый код может обращаться к прежним LOB-функциям или удалённому GLO. Сначала нужно различить исторический контракт, наличие имени в установленном расширении и фактически работающий путь. Эти главы сохраняют объяснение прежнего API и показывают перенос полезной задачи на проверенный механизм.

Перед началом прочитайте подключение и первые запросы и BLOB, CLOB и привязку вложения к записи. В коротких фрагментах соединение $db уже открыто; полные проекты показывают собственную учебную таблицу, конкретный файл результата и очистку. Синтетический архив миграции не выдаётся за содержимое извлечённой исторической GLO-базы.

Профиль проверки: PHP 7.4.33, native cubrid 11.2.0.0002, CCI 11.2.0, сервер CUBRID 11.3.5.1274. Это отдельная среда совместимости. Статус прежних функций указан явно; успешный современный сценарий не служит доказательством запуска удалённой функции.

Соседняя глава совместимости прежнего LOB API разбирает имена cubrid_lob_get/size/export/send/close. Это отдельное семейство: его наличие не возвращает удалённый GLO. Для новых проектов основа — LOB2 и явная связь содержимого с SQL-строкой.

ПонятиеВ нашем примереЧем не является
Исторический OIDsynthetic-glo-701 — явно учебная меткаНе реальный locator и не разрешение на скачивание
Бизнес-ключbusiness_id=701 — документ заказаНе адрес памяти и не порядковый номер файла в папке
Исходный файлreceipt-701.txt и полный SHA256Не один только путь без проверки содержимого
Новая записьbusiness_id, old_oid, revision, body BLOBНе результат переименования GLO-функции
LOB2-дескрипторРесурс new/import либо fetch текущего запросаНе старый OID строкой
Путь переноса и контроль результата
  1. В совместимой старой среде: согласованная выгрузка файлов и связей
  2. В учебном ZIP: вместо неё честно подготовленные синтетические файлы
  3. Ведомость → проверка пути/размера/SHA → новый LOB → SQL-строка
  4. Новый SELECT → проверка содержимого и бизнес-ключа → отчёт

Самая маленькая проверка до выбора пути

$legacyAvailable = function_exists('cubrid_load_from_glo');
if (!$legacyAvailable) {
    // Не вызываем отсутствующую функцию и не называем это экспортом GLO.
    // Получаем согласованный архив из совместимой среды; дальше читаем ведомость.
}

Проверка имени — только первый шаг. Даже если функция есть, нужно отдельное подтверждение её работоспособности на нужной старой версии, доступ к файлам, права и согласованность данных. В современной подготовке все четыре GLO-имени отсутствуют. Отсутствующую функцию нельзя «исправить» заменой префикса: новое API принимает ресурсы и привязывает их к параметрам SQL другим способом.

Перенести два файла по ведомости и сохранить связь со старым ключом

У старого архива есть файлы и ведомость соответствий. Нужно перенести два документа так, чтобы они остались связаны с заказами 701 и 702. Учебный ZIP содержит маленькие синтетические файлы и manifest.csv; он не является выгрузкой настоящей GLO-базы. После запуска получится CSV с фактическими размерами и полными SHA256 прочитанных из новой базы документов.

Путь данных и полученный результат
  1. synthetic-glo-701 → business_id 701 → receipt-701.txt и SHA256
  2. Проверка имени/каталога/размера/хеша → новый BLOB
  3. INSERT бизнес-ключа и содержимого → commit
  4. SELECT по каждому ключу → сверка → migration-import.csv

Минимальный вызов

$lob = cubrid_lob2_new($db, 'BLOB');
if ($lob === false || !cubrid_lob2_import($lob, $checkedPath)) {
    throw new RuntimeException('Исходный файл не перенесён');
}
// Далее lob2_bind связывает LOB с ? INSERT, execute сохраняет SQL-строку.

checkedPath — проверенный путь из ведомости, не HTTP-параметр. New создаёт новый ресурс, import переносит файл, но ещё не сохраняет строку заказа. Полный проект выполняет INSERT с параметрами, commit и новый SELECT. old_oid хранится только как текст происхождения; он не передаётся в API современной базы.

Что получится после запуска

НаблюдениеРезультат
Документ 701Заказ 701 Кабель: 3
Документ 702Кабель O'Reilly Проверить маркировку.
Импортировано2
Связь старого ключаsynthetic-glo-701 → 701; synthetic-glo-702 → 702
Выход из папки отвергнутда
Квитанцияmigration-import.csv
Самостоятельный проект, исходник и архив
Перенести два файла по ведомости и сохранить связь со старым ключомСамостоятельный пример
<?php
declare(strict_types=1);
require __DIR__.'/lab.php';
$db=null;$owned=$removed=$rows=$checks=$observations=[];

try {
    $config=c172Config();$db=c172Connect();$runtime=c172Runtime($db);

    // These fixtures are a synthetic export, not data read from a real GLO server.
    function step172(callable $action) {
        $call=c172Capture($action);
        if($call['value']===false || $call['value']===null || $call['warning']) {c172Failure('Действие миграции не выполнено',$call);}
        return $call['value'];
    }
    function migrationTable172($db,array &$owned,string $label): string {
        $name=c172OwnName($db,$owned,'TABLE',$label);$table=c172Qualified($name);
        c172Ddl($db,'CREATE TABLE '.$table.' (business_id INT PRIMARY KEY,old_oid VARCHAR(40) UNIQUE,title VARCHAR(80),revision INT NOT NULL,body BLOB)');
        c172Commit($db);return $table;
    }
    function sourcePath172(string $file): string {
        if(!preg_match('/^[a-z0-9][a-z0-9-]*\.txt$/D',$file)){throw new InvalidArgumentException('Недопустимое имя исходного файла');}
        $base=realpath(__DIR__);$path=realpath(__DIR__.'/'.$file);
        if($base===false || $path===false || dirname($path)!==$base || !is_file($path) || !is_readable($path)) {
            throw new RuntimeException('Нет допустимого исходного файла');
        }
        if(filesize($path)>4096){throw new RuntimeException('Учебный файл превышает лимит');}
        return $path;
    }
    function manifest172(string $filename): array {
        $file=fopen(__DIR__.'/'.$filename,'rb');if($file===false){throw new RuntimeException('Нет ведомости');}
        try {
            if(fgetcsv($file,0,';')!==['old_oid','business_id','file','bytes','sha256']){throw new RuntimeException('Неверный заголовок ведомости');}
            $items=[];$ids=[];$oldIds=[];
            while(($cells=fgetcsv($file,0,';'))!==false){
                if(count($cells)!==5 || !preg_match('/^synthetic-glo-[0-9]+$/D',$cells[0])
                    || !preg_match('/^[1-9][0-9]{0,5}$/D',$cells[1]) || !preg_match('/^[0-9]+$/D',$cells[3])
                    || !preg_match('/^[0-9a-f]{64}$/D',$cells[4])){throw new RuntimeException('Неверная строка ведомости');}
                $id=(int)$cells[1];
                if(isset($ids[$id]) || isset($oldIds[$cells[0]])){throw new RuntimeException('Повтор ключа в ведомости');}
                $ids[$id]=true;$oldIds[$cells[0]]=true;
                $items[]=['old_oid'=>$cells[0],'business_id'=>$id,'file'=>$cells[2],'bytes'=>(int)$cells[3],'sha256'=>$cells[4]];
                if(count($items)>4){throw new RuntimeException('Учебная ведомость ограничена четырьмя файлами');}
            }
            return $items;
        } finally {fclose($file);}
    }
    function checkedSource172(array $item): string {
        $path=sourcePath172($item['file']);
        if(filesize($path)!==$item['bytes'] || hash_file('sha256',$path)!==$item['sha256']){
            throw new RuntimeException('Размер или полный SHA256 исходника не совпадает');
        }
        return $path;
    }
    function storeFile172($db,string $table,array $item,string $title,int $revision,bool $replace=false): void {
        $path=checkedSource172($item);$lob=$request=null;
        try {
            $lob=step172(static function()use($db){return cubrid_lob2_new($db,'BLOB');});
            step172(static function()use($lob,$path){return cubrid_lob2_import($lob,$path);});
            $sql=$replace?'UPDATE '.$table.' SET body=?,revision=? WHERE business_id=?'
                :'INSERT INTO '.$table.' (body,revision,business_id,old_oid,title) VALUES (?,?,?,?,?)';
            $request=step172(static function()use($db,$sql){return cubrid_prepare($db,$sql);});
            step172(static function()use($request,$lob){return cubrid_lob2_bind($request,1,$lob,'BLOB');});
            step172(static function()use($request,$revision){return cubrid_bind($request,2,$revision,'int');});
            $id=$item['business_id'];
            step172(static function()use($request,$id){return cubrid_bind($request,3,$id,'int');});
            if(!$replace){
                $old=$item['old_oid'];
                step172(static function()use($request,$old){return cubrid_bind($request,4,$old,'string');});
                step172(static function()use($request,$title){return cubrid_bind($request,5,$title,'string');});
            }
            step172(static function()use($request){return cubrid_execute($request);});
            $affected=cubrid_affected_rows($request);
            if($affected!==1){throw new RuntimeException('Изменена не одна запись документа');}
        } finally {
            c172Release($request);
            if(is_resource($lob)){
                $closed=cubrid_lob2_close($lob);$lob=null;
                if($closed!==true){throw new RuntimeException('Созданный LOB не закрыт');}
            }
        }
    }
    function storedDocument172($db,string $table,int $id): array {
        $request=$row=$lob=$call=null;
        try {
            $request=c172Request($db,'SELECT business_id,old_oid,title,revision,body FROM '.$table.' WHERE business_id=?',[$id]);
            $call=c172Capture(static function()use($request){return cubrid_fetch_array($request,CUBRID_ASSOC|CUBRID_LOB);});
            if($call['warning'] || $call['code']!==0 || $call['value']===null){c172Failure('Документ не прочитан',$call);}
            if($call['value']===false){return ['found'=>false];}
            $row=$call['value'];$value=['found'=>true,'business_id'=>(int)$row['business_id'],'old_oid'=>$row['old_oid'],
                'title'=>$row['title'],'revision'=>(int)$row['revision'],'sql_null'=>$row['body']===null,'data'=>null];
            if($row['body']===null){return $value;}
            $lob=$row['body'];
            $size=step172(static function()use($lob){return cubrid_lob2_size($lob);});
            if(!is_int($size)||$size<0||$size>4096){throw new RuntimeException('Недопустимый размер учебного документа');}
            $data='';
            while(strlen($data)<$size){
                $amount=min(512,$size-strlen($data));
                $part=step172(static function()use($lob,$amount){return cubrid_lob2_read($lob,$amount);});
                if(!is_string($part)||$part===''){throw new RuntimeException('Неполное чтение документа');}
                $data.=$part;
            }
            $value['data']=$data;$value['bytes']=strlen($data);$value['sha256']=hash('sha256',$data);return $value;
        } finally {
            // Keep all fetched references until the request drops its native locator list.
            c172Release($request);$row=$lob=$call=null;
        }
    }
    function writeCsv172(string $filename,array $rows): void {
        $file=fopen(__DIR__.'/'.$filename,'xb');if($file===false){throw new RuntimeException('Новый CSV не открыт');}
        try {foreach($rows as $row){if(fputcsv($file,$row,';','"','\\')===false){throw new RuntimeException('CSV не записан');}}}
        finally{fclose($file);}
    }

    $table=migrationTable172($db,$owned,'manifest');$items=manifest172('manifest.csv');
    $rejected=false;
    try {sourcePath172('../receipt-701.txt');}catch(InvalidArgumentException $e){$rejected=true;}
    $receipt=[['business_id','old_oid','bytes','sha256','status']];$documents=[];
    foreach($items as $item){storeFile172($db,$table,$item,'Документ '.$item['business_id'],1);}
    c172Commit($db);
    foreach($items as $item){
        $saved=storedDocument172($db,$table,$item['business_id']);
        if(!$saved['found']||$saved['sql_null']||$saved['sha256']!==$item['sha256']||$saved['bytes']!==$item['bytes']){
            throw new RuntimeException('Импортированный документ не прошёл сверку');
        }
        $receipt[]=[$saved['business_id'],$saved['old_oid'],$saved['bytes'],$saved['sha256'],'imported'];
        $documents[]=['business_id'=>$saved['business_id'],'old_oid'=>$saved['old_oid'],'text'=>$saved['data'],'sha256'=>$saved['sha256']];
    }
    writeCsv172('migration-import.csv',$receipt);
    $rows=[['Документ 701',$documents[0]['text']],['Документ 702',$documents[1]['text']],['Импортировано',count($documents)],
        ['Связь старого ключа','synthetic-glo-701 → 701; synthetic-glo-702 → 702'],['Выход из папки отвергнут',$rejected],['Квитанция','migration-import.csv']];
    $checks=['two_documents'=>count($documents)===2,'business_mapping'=>$documents[0]['business_id']===701&&$documents[1]['business_id']===702,
        'old_mapping'=>$documents[0]['old_oid']==='synthetic-glo-701'&&$documents[1]['old_oid']==='synthetic-glo-702',
        'first_text'=>$documents[0]['text']==="Заказ 701\nКабель: 3\n",'second_text'=>$documents[1]['text']==="Кабель O'Reilly\nПроверить маркировку.\n",
        'safe_path'=>$rejected,'all_hashes'=>array_column($documents,'sha256')===array_column($items,'sha256')];
    $observations=['fixture_origin'=>'synthetic export, not extracted from GLO','documents'=>$documents,'manifest_rows'=>count($items),'receipt'=>$receipt];

} finally {

    if (is_resource($db)) {try {$removed=c172Cleanup($db,$owned);} finally {c172Close($db);}}
}

c172Report('cub172-glo-manifest-import','Перенести два файла по ведомости и сохранить связь со старым ключом',$rows,$checks,$observations,$runtime,$owned,$removed);
Ожидаемый результат
Документ 701 — Заказ 701
Кабель: 3

Документ 702 — Кабель O'Reilly
Проверить маркировку.

Импортировано — 2
Связь старого ключа — synthetic-glo-701 → 701; synthetic-glo-702 → 702
Выход из папки отвергнут — да
Квитанция — migration-import.csv

Распакуйте весь ZIP и прочитайте README.txt. example.php содержит действия урока, lab.php — подключение к отдельной учебной базе, создание только собственной таблицы и очистку. Запускается CLI-проект с native-расширением cubrid, не публичный обработчик сайта. После запуска result.html показывает результат, result.json — наблюдения, а отдельный файл ниже содержит полученные данные.

Открыть результат выполнения · Посмотреть данные JSON · Скачать квитанцию импорта

Границы решения

Лимит этой самостоятельной программы — четыре файла по 4096 байт. Реальная миграция требует согласованной выгрузки старой системы, доверенной ведомости и плана повторного запуска. Наличие совпавшего SHA256 не проверяет авторство ведомости. При нескольких исходных системах добавьте source_system к ключу соответствия. Путь проверяется для существующих локальных файлов; это не обработчик загрузки из браузера.

Попробуйте изменить условие

Что делать, если manifest содержит два разных файла для одного business_id?

Разбор

Не выбирать последний молча. В проекте повтор ключа отклоняется до импорта. Для реального конфликта нужны правила версий документов и запись решения в ведомости; старый OID сам по себе не определяет актуальный файл.

Перенести текстовую инструкцию без незаметной смены кодировки

Название O'Reilly и русская инструкция должны остаться читаемыми после переноса. Здесь источник уже объявлен UTF-8: программа проверяет это условие, сохраняет исходные байты в BLOB, читает их обратно и создаёт текстовый файл. Результат показывает сам текст и отдельно его байтовый размер.

Путь данных и полученный результат
  1. instructions-702.txt: заголовок и одна инструкция в UTF-8
  2. Проверка полного SHA и корректности UTF-8
  3. Новый BLOB → SQL-строка 702 → commit
  4. Новый SELECT → тот же текст → instruction-migrated.txt

Минимальный вызов

$text = file_get_contents($checkedPath);
if ($text === false || preg_match('//u', $text) !== 1) {
    throw new RuntimeException('Ожидался файл UTF-8');
}
// Проверка не перекодирует файл. Неизвестную кодировку нужно установить отдельно.

file_get_contents подходит именно для заранее ограниченного маленького файла. Модификатор u проверяет согласованность UTF-8, не определяет язык и не доказывает правильность уже испорченного текста. BLOB выбран для точной архивной копии; тип CLOB не нужен для исправления кодировки.

Что получится после запуска

НаблюдениеРезультат
ТоварКабель O'Reilly
ИнструкцияПроверить маркировку.
Байт UTF-863
Кодовых точек38
Текст совпалда
Файлinstruction-migrated.txt
Самостоятельный проект, исходник и архив
Перенести текстовую инструкцию без незаметной смены кодировкиСамостоятельный пример
<?php
declare(strict_types=1);
require __DIR__.'/lab.php';
$db=null;$owned=$removed=$rows=$checks=$observations=[];

try {
    $config=c172Config();$db=c172Connect();$runtime=c172Runtime($db);

    // These fixtures are a synthetic export, not data read from a real GLO server.
    function step172(callable $action) {
        $call=c172Capture($action);
        if($call['value']===false || $call['value']===null || $call['warning']) {c172Failure('Действие миграции не выполнено',$call);}
        return $call['value'];
    }
    function migrationTable172($db,array &$owned,string $label): string {
        $name=c172OwnName($db,$owned,'TABLE',$label);$table=c172Qualified($name);
        c172Ddl($db,'CREATE TABLE '.$table.' (business_id INT PRIMARY KEY,old_oid VARCHAR(40) UNIQUE,title VARCHAR(80),revision INT NOT NULL,body BLOB)');
        c172Commit($db);return $table;
    }
    function sourcePath172(string $file): string {
        if(!preg_match('/^[a-z0-9][a-z0-9-]*\.txt$/D',$file)){throw new InvalidArgumentException('Недопустимое имя исходного файла');}
        $base=realpath(__DIR__);$path=realpath(__DIR__.'/'.$file);
        if($base===false || $path===false || dirname($path)!==$base || !is_file($path) || !is_readable($path)) {
            throw new RuntimeException('Нет допустимого исходного файла');
        }
        if(filesize($path)>4096){throw new RuntimeException('Учебный файл превышает лимит');}
        return $path;
    }
    function manifest172(string $filename): array {
        $file=fopen(__DIR__.'/'.$filename,'rb');if($file===false){throw new RuntimeException('Нет ведомости');}
        try {
            if(fgetcsv($file,0,';')!==['old_oid','business_id','file','bytes','sha256']){throw new RuntimeException('Неверный заголовок ведомости');}
            $items=[];$ids=[];$oldIds=[];
            while(($cells=fgetcsv($file,0,';'))!==false){
                if(count($cells)!==5 || !preg_match('/^synthetic-glo-[0-9]+$/D',$cells[0])
                    || !preg_match('/^[1-9][0-9]{0,5}$/D',$cells[1]) || !preg_match('/^[0-9]+$/D',$cells[3])
                    || !preg_match('/^[0-9a-f]{64}$/D',$cells[4])){throw new RuntimeException('Неверная строка ведомости');}
                $id=(int)$cells[1];
                if(isset($ids[$id]) || isset($oldIds[$cells[0]])){throw new RuntimeException('Повтор ключа в ведомости');}
                $ids[$id]=true;$oldIds[$cells[0]]=true;
                $items[]=['old_oid'=>$cells[0],'business_id'=>$id,'file'=>$cells[2],'bytes'=>(int)$cells[3],'sha256'=>$cells[4]];
                if(count($items)>4){throw new RuntimeException('Учебная ведомость ограничена четырьмя файлами');}
            }
            return $items;
        } finally {fclose($file);}
    }
    function checkedSource172(array $item): string {
        $path=sourcePath172($item['file']);
        if(filesize($path)!==$item['bytes'] || hash_file('sha256',$path)!==$item['sha256']){
            throw new RuntimeException('Размер или полный SHA256 исходника не совпадает');
        }
        return $path;
    }
    function storeFile172($db,string $table,array $item,string $title,int $revision,bool $replace=false): void {
        $path=checkedSource172($item);$lob=$request=null;
        try {
            $lob=step172(static function()use($db){return cubrid_lob2_new($db,'BLOB');});
            step172(static function()use($lob,$path){return cubrid_lob2_import($lob,$path);});
            $sql=$replace?'UPDATE '.$table.' SET body=?,revision=? WHERE business_id=?'
                :'INSERT INTO '.$table.' (body,revision,business_id,old_oid,title) VALUES (?,?,?,?,?)';
            $request=step172(static function()use($db,$sql){return cubrid_prepare($db,$sql);});
            step172(static function()use($request,$lob){return cubrid_lob2_bind($request,1,$lob,'BLOB');});
            step172(static function()use($request,$revision){return cubrid_bind($request,2,$revision,'int');});
            $id=$item['business_id'];
            step172(static function()use($request,$id){return cubrid_bind($request,3,$id,'int');});
            if(!$replace){
                $old=$item['old_oid'];
                step172(static function()use($request,$old){return cubrid_bind($request,4,$old,'string');});
                step172(static function()use($request,$title){return cubrid_bind($request,5,$title,'string');});
            }
            step172(static function()use($request){return cubrid_execute($request);});
            $affected=cubrid_affected_rows($request);
            if($affected!==1){throw new RuntimeException('Изменена не одна запись документа');}
        } finally {
            c172Release($request);
            if(is_resource($lob)){
                $closed=cubrid_lob2_close($lob);$lob=null;
                if($closed!==true){throw new RuntimeException('Созданный LOB не закрыт');}
            }
        }
    }
    function storedDocument172($db,string $table,int $id): array {
        $request=$row=$lob=$call=null;
        try {
            $request=c172Request($db,'SELECT business_id,old_oid,title,revision,body FROM '.$table.' WHERE business_id=?',[$id]);
            $call=c172Capture(static function()use($request){return cubrid_fetch_array($request,CUBRID_ASSOC|CUBRID_LOB);});
            if($call['warning'] || $call['code']!==0 || $call['value']===null){c172Failure('Документ не прочитан',$call);}
            if($call['value']===false){return ['found'=>false];}
            $row=$call['value'];$value=['found'=>true,'business_id'=>(int)$row['business_id'],'old_oid'=>$row['old_oid'],
                'title'=>$row['title'],'revision'=>(int)$row['revision'],'sql_null'=>$row['body']===null,'data'=>null];
            if($row['body']===null){return $value;}
            $lob=$row['body'];
            $size=step172(static function()use($lob){return cubrid_lob2_size($lob);});
            if(!is_int($size)||$size<0||$size>4096){throw new RuntimeException('Недопустимый размер учебного документа');}
            $data='';
            while(strlen($data)<$size){
                $amount=min(512,$size-strlen($data));
                $part=step172(static function()use($lob,$amount){return cubrid_lob2_read($lob,$amount);});
                if(!is_string($part)||$part===''){throw new RuntimeException('Неполное чтение документа');}
                $data.=$part;
            }
            $value['data']=$data;$value['bytes']=strlen($data);$value['sha256']=hash('sha256',$data);return $value;
        } finally {
            // Keep all fetched references until the request drops its native locator list.
            c172Release($request);$row=$lob=$call=null;
        }
    }
    function writeCsv172(string $filename,array $rows): void {
        $file=fopen(__DIR__.'/'.$filename,'xb');if($file===false){throw new RuntimeException('Новый CSV не открыт');}
        try {foreach($rows as $row){if(fputcsv($file,$row,';','"','\\')===false){throw new RuntimeException('CSV не записан');}}}
        finally{fclose($file);}
    }

    $table=migrationTable172($db,$owned,'text_migrate');$items=manifest172('manifest.csv');$item=$items[0];
    $path=checkedSource172($item);$original=file_get_contents($path);
    if(!is_string($original)||preg_match('//u',$original)!==1){throw new RuntimeException('Исходник не является согласованным UTF-8');}
    storeFile172($db,$table,$item,"Кабель O'Reilly",1);c172Commit($db);
    $saved=storedDocument172($db,$table,702);
    if(!$saved['found']||$saved['sql_null']||preg_match('//u',$saved['data'])!==1){throw new RuntimeException('Текст не получен');}
    $target=__DIR__.'/instruction-migrated.txt';
    if(file_put_contents($target,$saved['data'])!==strlen($saved['data'])){throw new RuntimeException('Текст результата не записан');}
    $lines=explode("\n",rtrim($saved['data'],"\n"));
    $points=preg_split('//u',$saved['data'],-1,PREG_SPLIT_NO_EMPTY);
    $rows=[['Товар',$lines[0]],['Инструкция',$lines[1]],['Байт UTF-8',$saved['bytes']],['Кодовых точек',count($points)],
        ['Текст совпал',$saved['data']===$original],['Файл','instruction-migrated.txt']];
    $checks=['selected_business'=>$saved['business_id']===702,'valid_utf8'=>preg_match('//u',$saved['data'])===1,
        'apostrophe'=>strpos($saved['data'],"O'Reilly")!==false,'exact_text'=>$saved['data']===$original,
        'two_lines'=>count($lines)===2,'bytes_not_characters'=>$saved['bytes']>count($points),
        'output_hash'=>hash_file('sha256',$target)===$item['sha256']];
    $observations=['business_id'=>702,'text'=>$saved['data'],'encoding'=>'UTF-8 verified, not guessed','sha256'=>$saved['sha256'],
        'source_file'=>$item['file'],'characters'=>count($points),'bytes'=>$saved['bytes']];

} finally {

    if (is_resource($db)) {try {$removed=c172Cleanup($db,$owned);} finally {c172Close($db);}}
}

c172Report('cub172-glo-text-migration','Перенести текстовую инструкцию без незаметной смены кодировки',$rows,$checks,$observations,$runtime,$owned,$removed);
Ожидаемый результат
Товар — Кабель O'Reilly
Инструкция — Проверить маркировку.
Байт UTF-8 — 63
Кодовых точек — 38
Текст совпал — да
Файл — instruction-migrated.txt

Распакуйте весь ZIP и прочитайте README.txt. example.php содержит действия урока, lab.php — подключение к отдельной учебной базе, создание только собственной таблицы и очистку. Запускается CLI-проект с native-расширением cubrid, не публичный обработчик сайта. После запуска result.html показывает результат, result.json — наблюдения, а отдельный файл ниже содержит полученные данные.

Открыть результат выполнения · Посмотреть данные JSON · Скачать перенесённую инструкцию

Границы решения

Если старый файл в Windows-1251, сначала сохраните неизменённый оригинал и его хеш, а отдельное преобразование выполните с явно установленной кодировкой источника. После преобразования изменится SHA256: его нельзя сравнивать как побайтную копию оригинала. Перед HTML-выводом текст экранируют. Этот CLI-проект не проверяет HTTP-выдачу.

Попробуйте изменить условие

Почему совпадение числа букв не заменяет сверку исходного текста?

Разбор

Разные строки могут иметь одинаковую длину. Здесь дополнительно сравниваются все байты и полный SHA256. Число кодовых точек объясняет UTF-8 и не выдаётся за число графем любого языка.

Заменить ссылку на документ целиком: отмена и подтверждение

В старом приложении файл могли заменять внутри того же GLO. При переносе безопаснее явно создать новое содержимое и изменить ссылку в строке документа. Проект сначала отменяет замену версии 1 на 2, затем повторяет её новым LOB и подтверждает. В журнал попадают реальные версии и хеши заново прочитанных документов.

Путь данных и полученный результат
  1. Версия 1 → строка 701 → commit
  2. Новый LOB версии 2 → UPDATE → виден внутри операции
  3. rollback → новый SELECT снова возвращает версию 1
  4. Ещё один новый LOB → UPDATE → commit → версия 2 и CSV

Минимальный вызов

// $newLob создан заново и заполнен до UPDATE.
// UPDATE documents SET body=?, revision=? WHERE business_id=?
if (!cubrid_lob2_bind($request, 1, $newLob, 'BLOB')) {
    throw new RuntimeException('Новый файл не привязан');
}
// После execute: commit целой операции либо rollback SQL-изменения.

Меняется не байт внутри старого файла, а значение body в строке с business_id=701 вместе с revision. Откат SQL возвращает прежнюю ссылку и номер версии. Полный код закрывает запросы до созданных LOB, а прочитанные из результата LOB освобождает после закрытия запроса.

Что получится после запуска

НаблюдениеРезультат
До заменыЗаказ 701 Версия: 1
Неподтверждённая версия2
После rollbackЗаказ 701 Версия: 1
После commitЗаказ 701 Версия: 2
Исходный файл сохранёнда
Журналreplacement-audit.csv
Самостоятельный проект, исходник и архив
Заменить ссылку на документ целиком: отмена и подтверждениеСамостоятельный пример
<?php
declare(strict_types=1);
require __DIR__.'/lab.php';
$db=null;$owned=$removed=$rows=$checks=$observations=[];

try {
    $config=c172Config();$db=c172Connect();$runtime=c172Runtime($db);

    // These fixtures are a synthetic export, not data read from a real GLO server.
    function step172(callable $action) {
        $call=c172Capture($action);
        if($call['value']===false || $call['value']===null || $call['warning']) {c172Failure('Действие миграции не выполнено',$call);}
        return $call['value'];
    }
    function migrationTable172($db,array &$owned,string $label): string {
        $name=c172OwnName($db,$owned,'TABLE',$label);$table=c172Qualified($name);
        c172Ddl($db,'CREATE TABLE '.$table.' (business_id INT PRIMARY KEY,old_oid VARCHAR(40) UNIQUE,title VARCHAR(80),revision INT NOT NULL,body BLOB)');
        c172Commit($db);return $table;
    }
    function sourcePath172(string $file): string {
        if(!preg_match('/^[a-z0-9][a-z0-9-]*\.txt$/D',$file)){throw new InvalidArgumentException('Недопустимое имя исходного файла');}
        $base=realpath(__DIR__);$path=realpath(__DIR__.'/'.$file);
        if($base===false || $path===false || dirname($path)!==$base || !is_file($path) || !is_readable($path)) {
            throw new RuntimeException('Нет допустимого исходного файла');
        }
        if(filesize($path)>4096){throw new RuntimeException('Учебный файл превышает лимит');}
        return $path;
    }
    function manifest172(string $filename): array {
        $file=fopen(__DIR__.'/'.$filename,'rb');if($file===false){throw new RuntimeException('Нет ведомости');}
        try {
            if(fgetcsv($file,0,';')!==['old_oid','business_id','file','bytes','sha256']){throw new RuntimeException('Неверный заголовок ведомости');}
            $items=[];$ids=[];$oldIds=[];
            while(($cells=fgetcsv($file,0,';'))!==false){
                if(count($cells)!==5 || !preg_match('/^synthetic-glo-[0-9]+$/D',$cells[0])
                    || !preg_match('/^[1-9][0-9]{0,5}$/D',$cells[1]) || !preg_match('/^[0-9]+$/D',$cells[3])
                    || !preg_match('/^[0-9a-f]{64}$/D',$cells[4])){throw new RuntimeException('Неверная строка ведомости');}
                $id=(int)$cells[1];
                if(isset($ids[$id]) || isset($oldIds[$cells[0]])){throw new RuntimeException('Повтор ключа в ведомости');}
                $ids[$id]=true;$oldIds[$cells[0]]=true;
                $items[]=['old_oid'=>$cells[0],'business_id'=>$id,'file'=>$cells[2],'bytes'=>(int)$cells[3],'sha256'=>$cells[4]];
                if(count($items)>4){throw new RuntimeException('Учебная ведомость ограничена четырьмя файлами');}
            }
            return $items;
        } finally {fclose($file);}
    }
    function checkedSource172(array $item): string {
        $path=sourcePath172($item['file']);
        if(filesize($path)!==$item['bytes'] || hash_file('sha256',$path)!==$item['sha256']){
            throw new RuntimeException('Размер или полный SHA256 исходника не совпадает');
        }
        return $path;
    }
    function storeFile172($db,string $table,array $item,string $title,int $revision,bool $replace=false): void {
        $path=checkedSource172($item);$lob=$request=null;
        try {
            $lob=step172(static function()use($db){return cubrid_lob2_new($db,'BLOB');});
            step172(static function()use($lob,$path){return cubrid_lob2_import($lob,$path);});
            $sql=$replace?'UPDATE '.$table.' SET body=?,revision=? WHERE business_id=?'
                :'INSERT INTO '.$table.' (body,revision,business_id,old_oid,title) VALUES (?,?,?,?,?)';
            $request=step172(static function()use($db,$sql){return cubrid_prepare($db,$sql);});
            step172(static function()use($request,$lob){return cubrid_lob2_bind($request,1,$lob,'BLOB');});
            step172(static function()use($request,$revision){return cubrid_bind($request,2,$revision,'int');});
            $id=$item['business_id'];
            step172(static function()use($request,$id){return cubrid_bind($request,3,$id,'int');});
            if(!$replace){
                $old=$item['old_oid'];
                step172(static function()use($request,$old){return cubrid_bind($request,4,$old,'string');});
                step172(static function()use($request,$title){return cubrid_bind($request,5,$title,'string');});
            }
            step172(static function()use($request){return cubrid_execute($request);});
            $affected=cubrid_affected_rows($request);
            if($affected!==1){throw new RuntimeException('Изменена не одна запись документа');}
        } finally {
            c172Release($request);
            if(is_resource($lob)){
                $closed=cubrid_lob2_close($lob);$lob=null;
                if($closed!==true){throw new RuntimeException('Созданный LOB не закрыт');}
            }
        }
    }
    function storedDocument172($db,string $table,int $id): array {
        $request=$row=$lob=$call=null;
        try {
            $request=c172Request($db,'SELECT business_id,old_oid,title,revision,body FROM '.$table.' WHERE business_id=?',[$id]);
            $call=c172Capture(static function()use($request){return cubrid_fetch_array($request,CUBRID_ASSOC|CUBRID_LOB);});
            if($call['warning'] || $call['code']!==0 || $call['value']===null){c172Failure('Документ не прочитан',$call);}
            if($call['value']===false){return ['found'=>false];}
            $row=$call['value'];$value=['found'=>true,'business_id'=>(int)$row['business_id'],'old_oid'=>$row['old_oid'],
                'title'=>$row['title'],'revision'=>(int)$row['revision'],'sql_null'=>$row['body']===null,'data'=>null];
            if($row['body']===null){return $value;}
            $lob=$row['body'];
            $size=step172(static function()use($lob){return cubrid_lob2_size($lob);});
            if(!is_int($size)||$size<0||$size>4096){throw new RuntimeException('Недопустимый размер учебного документа');}
            $data='';
            while(strlen($data)<$size){
                $amount=min(512,$size-strlen($data));
                $part=step172(static function()use($lob,$amount){return cubrid_lob2_read($lob,$amount);});
                if(!is_string($part)||$part===''){throw new RuntimeException('Неполное чтение документа');}
                $data.=$part;
            }
            $value['data']=$data;$value['bytes']=strlen($data);$value['sha256']=hash('sha256',$data);return $value;
        } finally {
            // Keep all fetched references until the request drops its native locator list.
            c172Release($request);$row=$lob=$call=null;
        }
    }
    function writeCsv172(string $filename,array $rows): void {
        $file=fopen(__DIR__.'/'.$filename,'xb');if($file===false){throw new RuntimeException('Новый CSV не открыт');}
        try {foreach($rows as $row){if(fputcsv($file,$row,';','"','\\')===false){throw new RuntimeException('CSV не записан');}}}
        finally{fclose($file);}
    }

    $table=migrationTable172($db,$owned,'replacement');
    $old=manifest172('version-one.csv')[0];$new=manifest172('version-two.csv')[0];
    storeFile172($db,$table,$old,'Заказ 701',1);c172Commit($db);
    $before=storedDocument172($db,$table,701);c172Commit($db);
    storeFile172($db,$table,$new,'Заказ 701',2,true);
    $pending=storedDocument172($db,$table,701);
    c172Rollback($db);
    $afterRollback=storedDocument172($db,$table,701);c172Commit($db);
    // A different fresh LOB is imported for the confirmed attempt.
    storeFile172($db,$table,$new,'Заказ 701',2,true);c172Commit($db);
    $afterCommit=storedDocument172($db,$table,701);
    $audit=[['stage','business_id','revision','sha256'],['before',701,$before['revision'],$before['sha256']],
        ['after_rollback',701,$afterRollback['revision'],$afterRollback['sha256']],['after_commit',701,$afterCommit['revision'],$afterCommit['sha256']]];
    writeCsv172('replacement-audit.csv',$audit);
    $rows=[['До замены',$before['data']],['Неподтверждённая версия',$pending['revision']],['После rollback',$afterRollback['data']],
        ['После commit',$afterCommit['data']],['Исходный файл сохранён',hash_file('sha256',sourcePath172('version-one.txt'))===$old['sha256']],['Журнал','replacement-audit.csv']];
    $checks=['before_v1'=>$before['revision']===1&&$before['data']==="Заказ 701\nВерсия: 1\n",
        'pending_v2'=>$pending['revision']===2&&$pending['sha256']===$new['sha256'],
        'rollback_reference'=>$afterRollback['revision']===1&&$afterRollback['sha256']===$old['sha256'],
        'commit_reference'=>$afterCommit['revision']===2&&$afterCommit['sha256']===$new['sha256'],
        'old_source_untouched'=>hash_file('sha256',sourcePath172('version-one.txt'))===$old['sha256'],
        'same_business'=>$before['business_id']===$afterCommit['business_id']&&$afterCommit['business_id']===701];
    $observations=['audit'=>$audit,'before'=>$before['data'],'after_rollback'=>$afterRollback['data'],'after_commit'=>$afterCommit['data'],
        'rollback_scope'=>'SQL reference and revision; no claim that external LOB bytes are undone'];

} finally {

    if (is_resource($db)) {try {$removed=c172Cleanup($db,$owned);} finally {c172Close($db);}}
}

c172Report('cub172-glo-replacement','Заменить ссылку на документ целиком: отмена и подтверждение',$rows,$checks,$observations,$runtime,$owned,$removed);
Ожидаемый результат
До замены — Заказ 701
Версия: 1

Неподтверждённая версия — 2
После rollback — Заказ 701
Версия: 1

После commit — Заказ 701
Версия: 2

Исходный файл сохранён — да
Журнал — replacement-audit.csv

Распакуйте весь ZIP и прочитайте README.txt. example.php содержит действия урока, lab.php — подключение к отдельной учебной базе, создание только собственной таблицы и очистку. Запускается CLI-проект с native-расширением cubrid, не публичный обработчик сайта. После запуска result.html показывает результат, result.json — наблюдения, а отдельный файл ниже содержит полученные данные.

Открыть результат выполнения · Посмотреть данные JSON · Скачать журнал замены

Границы решения

Не делается утверждение, что rollback отменяет произвольные записи во внешнее LOB-хранилище или удаляет все его неиспользуемые байты. Нужна политика очистки хранилища и резервного копирования. Пример однопроцессный: для параллельных редакторов добавьте проверку ожидаемой revision в WHERE и обработку affected_rows=0; не держите транзакцию открытой, пока человек читает форму.

Попробуйте изменить условие

Как предотвратить перезапись чужой версии, если форма была открыта на revision=1?

Разбор

Обновляйте WHERE business_id=? AND revision=1; подтверждайте только если изменена одна строка. Ноль строк означает устаревшую форму и требует перечитать документ. Новый LOB и исходные файлы не являются разрешением обходить эту проверку.

Сверить перенесённые документы и отделить четыре разных результата

Счётчик «импорт завершён» не доказывает, что все документы дошли правильно. В этом проекте намеренно подготовлены совпавший документ, другой файл под верным ключом, пропущенная строка и строка без вложения. Сверка выдаёт CSV с четырьмя разными действиями и не исправляет исходные данные автоматически.

Путь данных и полученный результат
  1. Ведомость: ключи 701, 702, 703, 704 и ожидаемые SHA256
  2. Четыре параметризованных SELECT по бизнес-ключу
  3. Сравнение состояния, длины и полного хеша
  4. reconciliation.csv: keep / review / missing / SQL NULL; без изменений

Минимальный вызов

$actualHash = hash('sha256', $readBackBytes);
if ($actualHash !== $manifestHash) {
    $status = 'hash_mismatch';
}
// Сравнивается полный хеш фактически прочитанного содержимого, не только его размер.

Сначала различаются отсутствие строки и SQL NULL. Только для существующего содержимого вычисляется хеш. Проверка полного SHA256 удобна для ведомости; для маленьких файлов можно дополнительно сравнить все байты. Одинаковое число строк или одинаковый размер файла не заменяют эту проверку.

Что получится после запуска

НаблюдениеРезультат
701Совпадает: оставить
702Другие байты: сверить исходник
703Нет строки: проверить перед импортом
704SQL NULL: выяснить отсутствие документа
Проверено по ведомости4
Строк в базе после сверки3
Файлreconciliation.csv
Самостоятельный проект, исходник и архив
Сверить перенесённые документы и отделить четыре разных результатаСамостоятельный пример
<?php
declare(strict_types=1);
require __DIR__.'/lab.php';
$db=null;$owned=$removed=$rows=$checks=$observations=[];

try {
    $config=c172Config();$db=c172Connect();$runtime=c172Runtime($db);

    // These fixtures are a synthetic export, not data read from a real GLO server.
    function step172(callable $action) {
        $call=c172Capture($action);
        if($call['value']===false || $call['value']===null || $call['warning']) {c172Failure('Действие миграции не выполнено',$call);}
        return $call['value'];
    }
    function migrationTable172($db,array &$owned,string $label): string {
        $name=c172OwnName($db,$owned,'TABLE',$label);$table=c172Qualified($name);
        c172Ddl($db,'CREATE TABLE '.$table.' (business_id INT PRIMARY KEY,old_oid VARCHAR(40) UNIQUE,title VARCHAR(80),revision INT NOT NULL,body BLOB)');
        c172Commit($db);return $table;
    }
    function sourcePath172(string $file): string {
        if(!preg_match('/^[a-z0-9][a-z0-9-]*\.txt$/D',$file)){throw new InvalidArgumentException('Недопустимое имя исходного файла');}
        $base=realpath(__DIR__);$path=realpath(__DIR__.'/'.$file);
        if($base===false || $path===false || dirname($path)!==$base || !is_file($path) || !is_readable($path)) {
            throw new RuntimeException('Нет допустимого исходного файла');
        }
        if(filesize($path)>4096){throw new RuntimeException('Учебный файл превышает лимит');}
        return $path;
    }
    function manifest172(string $filename): array {
        $file=fopen(__DIR__.'/'.$filename,'rb');if($file===false){throw new RuntimeException('Нет ведомости');}
        try {
            if(fgetcsv($file,0,';')!==['old_oid','business_id','file','bytes','sha256']){throw new RuntimeException('Неверный заголовок ведомости');}
            $items=[];$ids=[];$oldIds=[];
            while(($cells=fgetcsv($file,0,';'))!==false){
                if(count($cells)!==5 || !preg_match('/^synthetic-glo-[0-9]+$/D',$cells[0])
                    || !preg_match('/^[1-9][0-9]{0,5}$/D',$cells[1]) || !preg_match('/^[0-9]+$/D',$cells[3])
                    || !preg_match('/^[0-9a-f]{64}$/D',$cells[4])){throw new RuntimeException('Неверная строка ведомости');}
                $id=(int)$cells[1];
                if(isset($ids[$id]) || isset($oldIds[$cells[0]])){throw new RuntimeException('Повтор ключа в ведомости');}
                $ids[$id]=true;$oldIds[$cells[0]]=true;
                $items[]=['old_oid'=>$cells[0],'business_id'=>$id,'file'=>$cells[2],'bytes'=>(int)$cells[3],'sha256'=>$cells[4]];
                if(count($items)>4){throw new RuntimeException('Учебная ведомость ограничена четырьмя файлами');}
            }
            return $items;
        } finally {fclose($file);}
    }
    function checkedSource172(array $item): string {
        $path=sourcePath172($item['file']);
        if(filesize($path)!==$item['bytes'] || hash_file('sha256',$path)!==$item['sha256']){
            throw new RuntimeException('Размер или полный SHA256 исходника не совпадает');
        }
        return $path;
    }
    function storeFile172($db,string $table,array $item,string $title,int $revision,bool $replace=false): void {
        $path=checkedSource172($item);$lob=$request=null;
        try {
            $lob=step172(static function()use($db){return cubrid_lob2_new($db,'BLOB');});
            step172(static function()use($lob,$path){return cubrid_lob2_import($lob,$path);});
            $sql=$replace?'UPDATE '.$table.' SET body=?,revision=? WHERE business_id=?'
                :'INSERT INTO '.$table.' (body,revision,business_id,old_oid,title) VALUES (?,?,?,?,?)';
            $request=step172(static function()use($db,$sql){return cubrid_prepare($db,$sql);});
            step172(static function()use($request,$lob){return cubrid_lob2_bind($request,1,$lob,'BLOB');});
            step172(static function()use($request,$revision){return cubrid_bind($request,2,$revision,'int');});
            $id=$item['business_id'];
            step172(static function()use($request,$id){return cubrid_bind($request,3,$id,'int');});
            if(!$replace){
                $old=$item['old_oid'];
                step172(static function()use($request,$old){return cubrid_bind($request,4,$old,'string');});
                step172(static function()use($request,$title){return cubrid_bind($request,5,$title,'string');});
            }
            step172(static function()use($request){return cubrid_execute($request);});
            $affected=cubrid_affected_rows($request);
            if($affected!==1){throw new RuntimeException('Изменена не одна запись документа');}
        } finally {
            c172Release($request);
            if(is_resource($lob)){
                $closed=cubrid_lob2_close($lob);$lob=null;
                if($closed!==true){throw new RuntimeException('Созданный LOB не закрыт');}
            }
        }
    }
    function storedDocument172($db,string $table,int $id): array {
        $request=$row=$lob=$call=null;
        try {
            $request=c172Request($db,'SELECT business_id,old_oid,title,revision,body FROM '.$table.' WHERE business_id=?',[$id]);
            $call=c172Capture(static function()use($request){return cubrid_fetch_array($request,CUBRID_ASSOC|CUBRID_LOB);});
            if($call['warning'] || $call['code']!==0 || $call['value']===null){c172Failure('Документ не прочитан',$call);}
            if($call['value']===false){return ['found'=>false];}
            $row=$call['value'];$value=['found'=>true,'business_id'=>(int)$row['business_id'],'old_oid'=>$row['old_oid'],
                'title'=>$row['title'],'revision'=>(int)$row['revision'],'sql_null'=>$row['body']===null,'data'=>null];
            if($row['body']===null){return $value;}
            $lob=$row['body'];
            $size=step172(static function()use($lob){return cubrid_lob2_size($lob);});
            if(!is_int($size)||$size<0||$size>4096){throw new RuntimeException('Недопустимый размер учебного документа');}
            $data='';
            while(strlen($data)<$size){
                $amount=min(512,$size-strlen($data));
                $part=step172(static function()use($lob,$amount){return cubrid_lob2_read($lob,$amount);});
                if(!is_string($part)||$part===''){throw new RuntimeException('Неполное чтение документа');}
                $data.=$part;
            }
            $value['data']=$data;$value['bytes']=strlen($data);$value['sha256']=hash('sha256',$data);return $value;
        } finally {
            // Keep all fetched references until the request drops its native locator list.
            c172Release($request);$row=$lob=$call=null;
        }
    }
    function writeCsv172(string $filename,array $rows): void {
        $file=fopen(__DIR__.'/'.$filename,'xb');if($file===false){throw new RuntimeException('Новый CSV не открыт');}
        try {foreach($rows as $row){if(fputcsv($file,$row,';','"','\\')===false){throw new RuntimeException('CSV не записан');}}}
        finally{fclose($file);}
    }

    $table=migrationTable172($db,$owned,'reconcile');$items=manifest172('manifest.csv');
    storeFile172($db,$table,$items[0],'Документ 701',1);
    $wrong=['old_oid'=>'synthetic-glo-702','business_id'=>702,'file'=>'wrong-702.txt','bytes'=>filesize(sourcePath172('wrong-702.txt')),
        'sha256'=>hash_file('sha256',sourcePath172('wrong-702.txt'))];
    storeFile172($db,$table,$wrong,'Документ 702',1);
    c172Command($db,'INSERT INTO '.$table.' (business_id,old_oid,title,revision,body) VALUES (?,?,?,?,NULL)',[704,'synthetic-glo-704','Документ 704',1]);
    c172Commit($db);$findings=[];$csv=[['business_id','status','action']];
    foreach($items as $item){
        $saved=storedDocument172($db,$table,$item['business_id']);
        if(!$saved['found']){$status='missing_row';$action='import_after_review';}
        elseif($saved['sql_null']){$status='null_body';$action='review_missing_document';}
        elseif($saved['sha256']!==$item['sha256']||$saved['bytes']!==$item['bytes']){$status='hash_mismatch';$action='review_source';}
        else{$status='matched';$action='keep';}
        $findings[]=['business_id'=>$item['business_id'],'status'=>$status,'action'=>$action,'expected_sha256'=>$item['sha256'],
            'actual_sha256'=>isset($saved['sha256'])?$saved['sha256']:null];
        $csv[]=[$item['business_id'],$status,$action];
    }
    writeCsv172('reconciliation.csv',$csv);
    $count=c172Read($db,'SELECT COUNT(*) AS n FROM '.$table);
    $rows=[['701','Совпадает: оставить'],['702','Другие байты: сверить исходник'],['703','Нет строки: проверить перед импортом'],
        ['704','SQL NULL: выяснить отсутствие документа'],['Проверено по ведомости',count($findings)],['Строк в базе после сверки',(int)$count[0]['n']],['Файл','reconciliation.csv']];
    $checks=['four_statuses'=>array_column($findings,'status')===['matched','hash_mismatch','missing_row','null_body'],
        'full_hash_match'=>$findings[0]['actual_sha256']===$findings[0]['expected_sha256'],
        'mismatch_detected'=>$findings[1]['actual_sha256']!==$findings[1]['expected_sha256'],
        'missing_not_null'=>$findings[2]['status']!==$findings[3]['status'],
        'no_automatic_repair'=>(int)$count[0]['n']===3,'all_business_keys'=>array_column($findings,'business_id')===[701,702,703,704]];
    $observations=['findings'=>$findings,'policy'=>'report only, no repair or deletion','expected_count'=>4,'actual_count'=>3];

} finally {

    if (is_resource($db)) {try {$removed=c172Cleanup($db,$owned);} finally {c172Close($db);}}
}

c172Report('cub172-glo-reconciliation','Сверить перенесённые документы и отделить четыре разных результата',$rows,$checks,$observations,$runtime,$owned,$removed);
Ожидаемый результат
701 — Совпадает: оставить
702 — Другие байты: сверить исходник
703 — Нет строки: проверить перед импортом
704 — SQL NULL: выяснить отсутствие документа
Проверено по ведомости — 4
Строк в базе после сверки — 3
Файл — reconciliation.csv

Распакуйте весь ZIP и прочитайте README.txt. example.php содержит действия урока, lab.php — подключение к отдельной учебной базе, создание только собственной таблицы и очистку. Запускается CLI-проект с native-расширением cubrid, не публичный обработчик сайта. После запуска result.html показывает результат, result.json — наблюдения, а отдельный файл ниже содержит полученные данные.

Открыть результат выполнения · Посмотреть данные JSON · Скачать ведомость сверки

Границы решения

Это сверка выбранной партии, а не аудит всех данных организации. В реальной миграции учитывайте согласованный момент выгрузки, новые версии после него, лишние записи назначения и повторные запуски. Хеш должен приходить из доверенной сохранённой ведомости; сравнивать новый файл с хешем, вычисленным только из него самого, бессмысленно.

Попробуйте изменить условие

Почему нельзя при missing_row сразу повторить весь импорт?

Разбор

Потому что часть документов уже перенесена, а причина отсутствия может быть в неверном соответствии бизнес-ключей. CSV сначала даёт список конкретных решений; повторный запуск должен ограничивать выбранную партию и не создавать дубли или затирать подтверждённые версии.

Что доказывает сверка и как продолжать перенос

Задача сопоставления идентификаторов двух баз показывает, почему нельзя переносить только числовой ключ или OID, забыв его источник. Для нескольких систем ведомость должна включать source_system; сопоставление с заказом проверяют независимо от совпадения байтов.

Вопрос о сравнении части SHA256 напоминает: квитанцию сверяет программа по полному значению. Размер полезен для ранней проверки, но два разных документа могут иметь одинаковое число байтов. Сам хеш не доказывает, что ведомость получена от доверенного владельца архива.

Не смешивайте три границы: транзакцию SQL, внешнее LOB-хранилище и исходные файлы миграции. Проект замены доказывает возврат SQL-ссылки после rollback. Он не утверждает, что любой сбой автоматически удалит созданные внешние данные. Исходные файлы и ведомость сохраняют до принятой сверки и согласованного решения об их сроке хранения.

Для большой миграции нужны партии по бизнес-ключу, журнал выполненных решений и безопасное возобновление. Сначала выбирайте только нужную партию SQL; не выгружайте все вложения ради последующей фильтрации в PHP. Читатель может расширить пример повторным запуском: совпавшую запись пропустить, конфликт отправить на проверку, недостающую добавить только после принятия соответствия.

Четыре исторических действия и путь замены

Имя в старом кодеЧто требовалось приложению
cubrid_new_gloИсторически создать экземпляр GLO указанного класса и получить OID для связи с документом.
cubrid_save_to_gloИсторически сохранить указанный файл в уже существующем экземпляре GLO.
cubrid_load_from_gloИсторически получить содержимое GLO в назначенный файл для архива или переноса.
cubrid_send_gloИсторически направить содержимое GLO в стандартный вывод PHP, не возвращая его строкой.

Функции и методы этой темы

Откройте карточку, чтобы увидеть договор функции, примеры, ограничения и связанные инструменты.

cubrid_new_gloИсторически создать экземпляр GLO указанного класса и получить OID для связи с документом.

Исторический PECL CUBRID >= 8.3.0; PHP Manual: removed from CUBRID 3.1; отсутствует в проверенном 11.2.0.0002. · cubrid

cubrid_new_glo(resource $conn_identifier, string $class_name, string $file_name): string

Исторически создать экземпляр GLO указанного класса базы и получить OID для связи с документом.

ПараметрЗначение
conn_identifierСоединение со старой совместимой CUBRID-средой. Не соединение PDO и не PHP-сессия пользователя.
class_nameИмя класса базы, в котором создавался GLO; исторический пример использует "glo". Это не имя PHP-класса.
file_nameИмя файла с содержимым для нового GLO; его доступность должна проверяться в старой среде. Это не содержимое файла и не ссылка на браузерную загрузку.

Результат старого контракта: OID созданного экземпляра строкой, false при неуспехе. Описание PHP Manual называет созданный GLO типом LO. Один успешный вызов ещё не подтверждает, что ссылка сохранена в нужной бизнес-записи.

Все параметры обязательны. Проверка function_exists помогает обнаружить отсутствие API, но не проверяет здоровье старого сервера, права на документ или доступность файлов. При наличии функции её false — отказ, который надо обработать и сверить с диагностикой конкретной исторической версии. Из современной подготовки нельзя вывести точные коды ошибок удалённой функции.

Значение OID обозначало экземпляр базы, а не PHP-объект, публичный URL или разрешение доступа. При миграции его сохраняют вместе с источником и бизнес-ключом только как соответствие. В новом LOB2 ресурс получается другим способом; подставлять старый OID вместо него нельзя. Примечание PHP Manual об устаревшем alias повторяет то же имя и не задаёт поддерживаемой современной замены.

Смысл исторического создания
  1. Файл изображения документа → new_glo(conn, "glo", file)
  2. Возвращённый OID → ссылка в строке документа старой базы
  3. Ведомость переноса: источник + old_oid → business_id → файл и SHA256

Применение 1: понять старое прикрепление скана к заказу

// Исторический фрагмент: только для совместимого старого окружения.
if (!function_exists('cubrid_new_glo')) {
    throw new RuntimeException('GLO API отсутствует; нужен план переноса');
}
$oldOid = cubrid_new_glo($oldConnection, 'glo', $checkedScanFile);
if ($oldOid === false) { throw new RuntimeException('Старый экземпляр не создан'); }
// Сохранить его связь с заказом средствами старого приложения.

При разборе старого кода важно найти не только файл, но и строку, которая ссылается на возвращённый OID. Импорт по ведомости переносит эту связь явно: synthetic-glo-701 становится документом business_id=701. Это учебный ключ, не настоящий результат new_glo.

Применение 2: создавать новые инструкции уже после миграции

// Современная альтернатива; это не вызов cubrid_new_glo.
$lob = cubrid_lob2_new($db, 'BLOB');
if ($lob === false || !cubrid_lob2_import($lob, $checkedInstructionFile)) {
    throw new RuntimeException('Новое содержимое не подготовлено');
}
// Отдельно: подготовленный INSERT, lob2_bind, execute, commit.

Перенос инструкции UTF-8 показывает сохранённый текст, число байтов и файл для скачивания. У нового пути явно разделены содержимое и SQL-строка. Для дальнейшего развития изучите cubrid_lob2_new и cubrid_lob2_bind.

Не создавайте заново все старые GLO только ради нового OID: это не переносит автоматически отношения между документами. Сначала составьте доверенную ведомость и перечень файлов через работающую старую систему, затем проверяйте каждую бизнес-связь в новой. Историческое создание и замена существующего содержимого — разные операции.

cubrid_save_to_gloИсторически сохранить указанный файл в уже существующем экземпляре GLO.

Исторический PECL CUBRID >= 8.3.0; PHP Manual: removed from CUBRID 3.1; отсутствует в проверенном 11.2.0.0002. · cubrid

cubrid_save_to_glo(resource $conn_identifier, string $oid, string $file_name): int

Исторически сохранить указанный файл в уже существующем экземпляре GLO.

ПараметрЗначение
conn_identifierСоединение со старой совместимой CUBRID-средой. Не соединение PDO и не PHP-сессия пользователя.
oidOID уже существующего GLO, содержимое которого требуется изменить. Проверка права на документ выполняется отдельно.
file_nameФайл, который нужно сохранить в выбранном GLO; заранее проверенный путь в совместимом окружении.

Результат старого контракта: Раздел Return Values PHP Manual перечисляет true при успехе и false при отказе. Его краткая сигнатура показывает int: эту несогласованность нельзя превращать в обещание числа записанных байтов.

Все параметры обязательны. Проверка function_exists помогает обнаружить отсутствие API, но не проверяет здоровье старого сервера, права на документ или доступность файлов. При наличии функции её false — отказ, который надо обработать и сверить с диагностикой конкретной исторической версии. Из современной подготовки нельзя вывести точные коды ошибок удалённой функции.

Значение OID обозначало экземпляр базы, а не PHP-объект, публичный URL или разрешение доступа. При миграции его сохраняют вместе с источником и бизнес-ключом только как соответствие. В новом LOB2 ресурс получается другим способом; подставлять старый OID вместо него нельзя. Примечание PHP Manual об устаревшем alias повторяет то же имя и не задаёт поддерживаемой современной замены.

Применение 1: найти в старом коде замену скана

// Исторический фрагмент; в проверенном модуле функция отсутствует.
if (!function_exists('cubrid_save_to_glo')) {
    throw new RuntimeException('Этот путь требует старого GLO API');
}
$ok = cubrid_save_to_glo($oldConnection, $documentOid, $checkedReplacement);
if ($ok === false) { throw new RuntimeException('Старый документ не обновлён'); }
// Успех ещё сверяют повторным извлечением и связью с нужным заказом.

Такой код менял содержимое выбранного старого экземпляра. При сопровождении нужно выяснить, кто ещё ссылается на тот же OID: замена общей сущности может затронуть несколько документов. Эта карточка не обещает поведение отката внешних файлов в неизвестной старой версии.

Применение 2: заменить исправленную квитанцию современным способом

// Для нового содержимого создаётся новый LOB.
// UPDATE documents SET body=?, revision=? WHERE business_id=?
if (!cubrid_lob2_bind($request, 1, $newLob, 'BLOB')) {
    throw new RuntimeException('Замена не привязана');
}
if (!cubrid_execute($request)) {
    cubrid_rollback($db);
    throw new RuntimeException('SQL-замена отменена');
}

Проект замены версии документа сначала демонстрирует rollback ссылки и revision, затем commit новой версии. Старый исходный файл остаётся неизменным. Для реального приложения дополните UPDATE ожидаемой revision и проверьте affected_rows: это предотвращает перезапись уже изменённого документа.

Вместо неявной перезаписи
  1. Версия 1 связана с заказом 701
  2. Новый LOB версии 2 проверен отдельно
  3. UPDATE ссылки + revision в одной транзакции
  4. rollback → версия 1; commit → версия 2

Не вызывайте cubrid_lob2_import на заполненном LOB, рассчитывая получить безопасную замену: закреплённая реализация имеет ограничение этой ветки. Создавайте новый ресурс. Последующая сверка отличает успешно заменённый документ от совпавшего только размера. cubrid_load_from_glo исторически выполняла обратное извлечение в файл.

cubrid_load_from_gloИсторически получить содержимое GLO в назначенный файл для архива или переноса.

Исторический PECL CUBRID >= 8.3.0; PHP Manual: removed from CUBRID 3.1; отсутствует в проверенном 11.2.0.0002. · cubrid

cubrid_load_from_glo(resource $conn_identifier, string $oid, string $file_name): int

Исторически получить содержимое GLO в назначенный файл для архива или переноса.

ПараметрЗначение
conn_identifierСоединение со старой совместимой CUBRID-средой. Не соединение PDO и не PHP-сессия пользователя.
oidOID GLO, из которого требуется извлечь данные.
file_nameНазначенный файл для сохранения извлечённого содержимого; не переменная для возвращённой строки.

Результат старого контракта: По Return Values — true при выполнении, false при отказе. Краткая сигнатура страницы int не означает длину файла; содержимое направляется в file_name.

Все параметры обязательны. Проверка function_exists помогает обнаружить отсутствие API, но не проверяет здоровье старого сервера, права на документ или доступность файлов. При наличии функции её false — отказ, который надо обработать и сверить с диагностикой конкретной исторической версии. Из современной подготовки нельзя вывести точные коды ошибок удалённой функции.

Значение OID обозначало экземпляр базы, а не PHP-объект, публичный URL или разрешение доступа. При миграции его сохраняют вместе с источником и бизнес-ключом только как соответствие. В новом LOB2 ресурс получается другим способом; подставлять старый OID вместо него нельзя. Примечание PHP Manual об устаревшем alias повторяет то же имя и не задаёт поддерживаемой современной замены.

Применение 1: подготовить согласованную выгрузку старого архива

// Исторический фрагмент, не выполненный в современной лаборатории.
if (!function_exists('cubrid_load_from_glo')) {
    throw new RuntimeException('Экспорт нужно выполнять в совместимой старой среде');
}
$ok = cubrid_load_from_glo($oldConnection, $oldOid, $assignedOutputFile);
if ($ok === false) { throw new RuntimeException('Старое вложение не извлечено'); }
// Отдельно записать источник, old_oid, бизнес-ключ, размер и полный хеш файла.

Эта стадия должна происходить там, где старый GLO действительно доступен. Наш импорт начинается уже с файла и ведомости, а не выдаёт LOB2 за исполнение load_from_glo. Согласуйте момент выгрузки: пока пользователи меняют документы, ведомость и содержимое могут относиться к разным версиям.

Применение 2: извлечь одну подозрительную запись для сверки

// Историческая операция в изолированном каталоге проверки.
if (cubrid_load_from_glo($oldConnection, $suspectOid, $reviewFile) === false) {
    throw new RuntimeException('Нет файла для сравнения');
}
$actualHash = hash_file('sha256', $reviewFile);
if ($actualHash === false) { throw new RuntimeException('Хеш не вычислен'); }
$matches = $actualHash === $trustedManifestHash;

Ведомость сверки четырёх исходов переносит саму задачу на современную базу: совпадение, другие байты, пропущенная строка и SQL NULL. Нужно сравнить полный хеш с ранее сохранённым доверенным значением, а не только первые символы и не два раза хеш одного и того же файла.

Для LOB2 современный эквивалент задачи сохранения файла — cubrid_lob2_export. Это другая функция с другим ресурсом и проверенными собственными ограничениями, включая отказ перезаписать существующий файл. Не приписывайте это поведение удалённому load_from_glo без отдельной проверки старой версии. cubrid_send_glo направляла данные в вывод, а не в файл.

cubrid_send_gloИсторически направить содержимое GLO в стандартный вывод PHP, не возвращая его строкой.

Исторический PECL CUBRID >= 8.3.0; PHP Manual: removed from CUBRID 3.1; отсутствует в проверенном 11.2.0.0002. · cubrid

cubrid_send_glo(resource $conn_identifier, string $oid): int

Исторически направить содержимое GLO в стандартный вывод PHP, не возвращая его строкой.

ПараметрЗначение
conn_identifierСоединение со старой совместимой CUBRID-средой. Не соединение PDO и не PHP-сессия пользователя.
oidOID GLO, содержимое которого должно попасть в стандартный вывод PHP.

Результат старого контракта: Return Values описывает true/false, хотя сводная сигнатура показывает int. Это статус отправки в вывод, не строка содержимого. Его нельзя печатать вслед за файлом.

Все параметры обязательны. Проверка function_exists помогает обнаружить отсутствие API, но не проверяет здоровье старого сервера, права на документ или доступность файлов. При наличии функции её false — отказ, который надо обработать и сверить с диагностикой конкретной исторической версии. Из современной подготовки нельзя вывести точные коды ошибок удалённой функции.

Значение OID обозначало экземпляр базы, а не PHP-объект, публичный URL или разрешение доступа. При миграции его сохраняют вместе с источником и бизнес-ключом только как соответствие. В новом LOB2 ресурс получается другим способом; подставлять старый OID вместо него нельзя. Примечание PHP Manual об устаревшем alias повторяет то же имя и не задаёт поддерживаемой современной замены.

Два разных результата вызова
  1. GLO → байты стандартного вывода PHP
  2. return → статус действия в переменной $ok
  3. echo $ok добавил бы лишние байты к документу

Применение 1: разобраться в старой выдаче изображения

// Исторический обработчик; этот фрагмент не является HTTP-тестом.
if (!function_exists('cubrid_send_glo')) {
    throw new RuntimeException('Старый обработчик необходимо заменить');
}
// До этого проверены права, найден OID и подтверждён формат JPEG.
header('Content-Type: image/jpeg');
$ok = cubrid_send_glo($oldConnection, $imageOid);
// Не echo $ok: статус не является частью JPEG.
if ($ok === false) { error_log('Старая выдача GLO не завершена'); }
exit;

MIME-тип определяется реальным форматом, а не только расширением имени. После начала бинарного тела нельзя добавлять HTML с сообщением об ошибке. Успех серверной функции также не доказывает, что браузер пользователя сохранил файл. Проверенные современные варианты чтения рассматривает раздел файлов и потоков LOB2.

Применение 2: отделить старый текстовый вывод от новой инструкции

// Историческая CLI-утилита: байты уходили в stdout.
if (cubrid_send_glo($oldConnection, $instructionOid) === false) {
    // Диагностика отдельно, без примеси к самому тексту документа.
    fwrite(STDERR, "Инструкция не выведена
");
    exit(1);
}

При переносе такой задачи современный проект инструкции сначала получает реальные байты из новой базы и сохраняет отдельный TXT. Результат HTML/JSON и сам документ — разные файлы, поэтому служебное сообщение не портит текст. Это проверяемый следующий шаг, а не заявление о запуске исторической функции.

Нужны одновременно контроль доступа и чистое тело ответа. Не принимайте OID из URL как разрешение прочитать любой документ. Вопрос о печати return после readfile показывает ту же ошибку смешения статуса и данных; значение return у readfile и старого send_glo различается, поэтому код чужой функции не копируется как контракт CUBRID.

Источники и обсуждения

Код и руководство с собой

Скачайте самостоятельные примеры с исходными данными, инструкциями и ожидаемыми результатами. Примеры HTTP и фрагменты для проекта помечены отдельно: им нужно указанное окружение.

Код не исполняется на сайте. Скачайте нужный пример и запускайте его в своём локальном окружении.

Материалы сверены 2026-10-12. Примеры и объяснения написаны для этого руководства; источники и обсуждения доступны в каждой главе.