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

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

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

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

Глава 95 · После параметризованных запросов и сохранения BLOB/CLOB

CUBRID: выгрузка вложений, чтение порциями и позиции BLOB/CLOB

Настоящие файлы из LOB: полный экспорт, порционное чтение с SHA256, предпросмотр по байтовым позициям и CSV вложений.

Вложение, два курсора и файл результата

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

От записи базы к полезному файлу
  1. SELECT id, title, body, note WHERE id = ?
  2. fetch с CUBRID_LOB → доступ к body и note выбранной строки
  3. export целиком или read порциями; seek выбирает место
  4. Файл на машине PHP → проверенные байты и понятный результат

Эти уроки продолжают подключение и первые запросы и подготовленные запросы с параметрами. BLOB хранит двоичные данные, например содержимое файла измерений; CLOB предназначен для большого текста. Строка таблицы связывает такое содержимое с товаром, инструкцией или поставкой. Здесь из базы получаются настоящие файлы, которые можно скачать и сравнить.

Дескриптор LOB — средство доступа к данным, а не их строковое содержимое. Создать LOB, записать в него данные и привязать его к строке SQL — отдельные действия. В коротких фрагментах соединение $db и запрос $request уже подготовлены; полный проект показывает весь путь от создания таблицы до результата и очистки.

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

Что этоПримерЧто оно позволяет
Строка таблицыid=1, title=КабельСвязать вложение с товаром и проверить доступ
LOB в базеbody BLOB, note CLOBХранить бинарное содержимое и текст
LOB-дескриптор PHP$row['body']Читать или позиционировать конкретное содержимое
Поток файла PHPfopen(..., 'xb')Записывать новый локальный файл через fwrite

Дескриптор похож на выданный доступ к книге, а курсор — на закладку между байтами. Это только модель: дескриптор не является независимой копией и перестаёт быть пригоден после изменения контекста запроса. Начало имеет позицию 0. SQL fetch переключает строки, lob2_read передвигает курсор внутри одного BLOB/CLOB; эти действия нельзя заменять друг другом.

ЗадачаВыборЧто проверить
Сохранить всё без преобразованийcubrid_lob2_exportНовое безопасное имя; true; размер и содержимое файла
Обрабатывать ограниченными порциямиcubrid_lob2_readСтрогий EOF; положительная длина; фактическая запись
Взять заголовок или хвостcubrid_lob2_seek / cubrid_lob2_seek64origin; единицы; границы; UTF-8
Узнать/сохранить местоcubrid_lob2_tell / cubrid_lob2_tell640 допустим; число или строка; конкретная версия LOB

Выгрузить вложение целиком, сохранив уже существующий файл

Технику нужен исходный файл измерения, сохранённый в карточке. Создадим attachment.bin с теми же байтами, включая нулевой байт и 0xFF. Перед экспортом курсор специально сдвинут: это докажет, что выгружается весь LOB, а не остаток. Повторный экспорт на тот же путь должен отказать, сохранив первый файл.

Путь данных и полученный результат
  1. В строке 1: файл измерения с NUL и 0xFF
  2. Курсор чтения сдвинут на байт 4
  3. export → attachment.bin содержит все байты; курсор остался 4
  4. Повтор на тот же путь → отказ; первый файл неизменён

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

$ok = cubrid_lob2_export($lob, __DIR__ . '/attachment.bin');
if ($ok === false) {
    throw new RuntimeException('Файл не создан');
}

Первый аргумент — полученный LOB-дескриптор, второй — путь на компьютере, где выполняется PHP. Функция создаёт файл, не строку для echo. __DIR__ обозначает каталог example.php. В полном проекте подготовка записывает вложение, SELECT возвращает его с флагом CUBRID_LOB, а размер и SHA проверяют фактическую выгрузку.

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

НаблюдениеРезультат
ВложениеДанные измерения
Созданный файлattachment.bin
Размер файла, байт41
Позиция LOB до и после4 → 4
Строковая позиция tell644
Существующий файл не перезаписанда
SHA-2564b4e1163637b8d5fe4aad9f0dff9a587192e50840ac339a1fe68e419f7fb0936
Самостоятельный проект, исходник и архив
Выгрузить вложение целиком, сохранив уже существующий файлСамостоятельный пример
<?php
declare(strict_types=1);
require __DIR__.'/lab.php';
$db=null;$owned=$removed=$rows=$checks=$observations=[];
try {
    $config=c171Config();$db=c171Connect();$runtime=c171Runtime($db);

    // Подготовка данных продолжает главу о сохранении LOB.
    function attachmentTable171($db,array &$owned,string $label): string {
        $name=c171OwnName($db,$owned,'TABLE',$label);$table=c171Qualified($name);
        c171Ddl($db,'CREATE TABLE '.$table.' (id INT PRIMARY KEY,title VARCHAR(80),body BLOB,note CLOB)');
        c171Commit($db);return $table;
    }
    function closeOwnedLob171(&$lob): void {
        if(is_resource($lob)) {
            if(cubrid_lob2_close($lob)!==true){throw new RuntimeException('Созданный LOB не закрыт');}
            $lob=null;
        }
    }
    function storeAttachment171($db,string $table,int $id,string $title,string $body,string $note): void {
        $query=$binary=$text=null;
        try {
            $binary=cubrid_lob2_new($db,'BLOB');$text=cubrid_lob2_new($db,'CLOB');
            if(!is_resource($binary)||!is_resource($text)){throw new RuntimeException('LOB не создан');}
            if(cubrid_lob2_write($binary,$body)!==true || cubrid_lob2_write($text,$note)!==true){throw new RuntimeException('LOB не заполнен');}
            $query=cubrid_prepare($db,'INSERT INTO '.$table.' (id,title,body,note) VALUES (?,?,?,?)');
            if($query===false || !cubrid_bind($query,1,$id,'int') || !cubrid_bind($query,2,$title,'string')
                || !cubrid_lob2_bind($query,3,$binary,'BLOB') || !cubrid_lob2_bind($query,4,$text,'CLOB')
                || !cubrid_execute($query)){throw new RuntimeException('Вложение не сохранено');}
        } finally {
            c171Release($query);closeOwnedLob171($binary);closeOwnedLob171($text);
        }
    }
    function selectedAttachment171($db,string $table,int $id) {
        $query=cubrid_prepare($db,'SELECT id,title,body,note FROM '.$table.' WHERE id=?');
        if($query===false){throw new RuntimeException('Запрос вложения не подготовлен');}
        if(!cubrid_bind($query,1,$id,'int') || !cubrid_execute($query)){
            c171Release($query);throw new RuntimeException('Запрос вложения не выполнен');
        }
        return $query;
    }
    function fetchedAttachment171($query): array {
        $row=cubrid_fetch($query,CUBRID_ASSOC|CUBRID_LOB);
        if(!is_array($row)||!is_resource($row['body']??null)||!is_resource($row['note']??null)){
            throw new RuntimeException('Нужна строка с двумя заполненными LOB');
        }
        return $row;
    }
    function readPart171($lob,int $length) {
        if($length<1 || $length>8192){throw new InvalidArgumentException('Допустима порция от 1 до 8192 байт');}
        $call=c171Capture(static function()use($lob,$length){return cubrid_lob2_read($lob,$length);});
        if($call['warning'] || $call['code']!==0 || $call['value']===null){c171Failure('Чтение LOB',$call);}
        if($call['value']!==false && (!is_string($call['value'])||$call['value']==='')){
            throw new RuntimeException('Чтение не продвигается');
        }
        return $call['value'];
    }
    function writeAll171($file,string $data): void {
        for($done=0;$done<strlen($data);){
            $written=fwrite($file,substr($data,$done));
            if($written===false || $written===0){throw new RuntimeException('Файл записан не полностью');}
            $done+=$written;
        }
    }

    $table=attachmentTable171($db,$owned,'export');
    $payload="AEN-ATTACHMENT\0v1\nitem=socket\nqty=3\n\xffEND\n";
    storeAttachment171($db,$table,1,'Данные измерения',$payload,'Паспорт измерения');c171Commit($db);
    $query=selectedAttachment171($db,$table,1);$row=null;
    try {
        $row=fetchedAttachment171($query);$lob=$row['body'];
        if(!cubrid_lob2_seek($lob,4,CUBRID_CURSOR_FIRST)){throw new RuntimeException('Не задана позиция');}
        $before=cubrid_lob2_tell($lob);$before64=cubrid_lob2_tell64($lob);
        $path=__DIR__.'/attachment.bin';
        $export=c171Capture(static function()use($lob,$path){return cubrid_lob2_export($lob,$path);});
        if($export['value']!==true || $export['warning']){c171Failure('Экспорт вложения',$export);}
        $after=cubrid_lob2_tell($lob);$after64=cubrid_lob2_tell64($lob);
        $size=filesize($path);$digest=hash_file('sha256',$path);
        $duplicate=c171Capture(static function()use($lob,$path){return cubrid_lob2_export($lob,$path);});
        clearstatcache(true,$path);
        $unchanged=filesize($path)===$size && hash_file('sha256',$path)===$digest;
        $rows=[['Вложение',$row['title']],['Созданный файл','attachment.bin'],['Размер файла, байт',$size],
            ['Позиция LOB до и после',$before.' → '.$after],['Строковая позиция tell64',$after64],
            ['Существующий файл не перезаписан',$duplicate['value']===false && $unchanged],['SHA-256',$digest]];
        $checks=['export_ok'=>$export['value']===true,'entire_lob'=>$size===strlen($payload) && $digest===hash('sha256',$payload),
            'cursor_unchanged'=>$before===4 && $after===4,'position_string'=>$before64==='4' && $after64==='4',
            'existing_refused'=>$duplicate['value']===false && $duplicate['warning'],'existing_preserved'=>$unchanged];
        $observations=['bytes'=>$size,'sha256'=>$digest,'prefix_hex'=>bin2hex(substr($payload,0,18)),
            'position_before'=>$before,'position_after'=>$after,'position_string'=>$after64,'export'=>$export,'existing_file'=>$duplicate];
    } finally {
        // Для fetched LOB в закреплённом драйвере сначала завершаем запрос.
        c171Release($query);$row=null;$lob=null;
    }

} finally {
    if (is_resource($db)) {try {$removed=c171Cleanup($db,$owned);} finally {c171Close($db);}}
}

c171Report('cub171-file-export','Выгрузить вложение целиком, сохранив уже существующий файл',$rows,$checks,$observations,$runtime,$owned,$removed);
Ожидаемый результат
Вложение — Данные измерения
Созданный файл — attachment.bin
Размер файла, байт — 41
Позиция LOB до и после — 4 → 4
Строковая позиция tell64 — 4
Существующий файл не перезаписан — да
SHA-256 — 4b4e1163637b8d5fe4aad9f0dff9a587192e50840ac339a1fe68e419f7fb0936

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

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

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

Экспорт идёт в файловую систему PHP-процесса, не в загрузки браузера и не на сервер базы по его пути. Для выдачи пользователю нужен отдельный HTTP-обработчик с проверкой доступа и заголовками; данный пример — CLI. Уже существующий файл не удаляется ради повторной попытки. При публикации реального файла сначала выберите безопасный собственный путь, проверьте результат и лишь затем передавайте его дальше. SHA доказывает совпадение с ожидаемыми байтами, а не безопасность содержимого.

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

Нужны только последние восемь байтов. Поможет ли seek перед export?

Разбор

Нет. export игнорирует положение курсора и обрабатывает весь LOB. Для части используйте seek с CUBRID_CURSOR_LAST и read, как в проекте предпросмотра.

Перенести вложение порциями и проверить каждый записанный байт

Журнал поставки нужно сохранить на диск, не собирая всё содержимое в одной PHP-строке. Даже когда интерфейс называется потоковым, конкатенация каждой порции в $all вернёт прежнюю проблему памяти. Здесь каждая порция сразу записывается, участвует в SHA и затем заменяется следующей; отдельный файл можно скачать.

Путь данных и полученный результат
  1. Небольшой блок исходника записан в BLOB несколько раз
  2. read(1024) → записать эту порцию в streamed.bin → обновить SHA
  3. Повторять до false; последняя порция короче 1024
  4. Сверить байты, позицию tell64 и SHA файла

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

$chunk = cubrid_lob2_read($lob, 1024);
if ($chunk === null) {
    throw new RuntimeException('Чтение завершилось ошибкой');
}
if ($chunk !== false) {
    // Это данные, в том числе допустимая строка "0".
    echo strlen($chunk);
}

Число 1024 ограничивает одну порцию, а не весь файл. false обозначает конец данных, null — документированный отказ. Полный проект дополнительно проверяет предупреждение и немедленную диагностику, не допускает пустую порцию без прогресса и обрабатывает частичный fwrite. В исходном журнале есть двоичные байты; строку не преобразуют в текст и не экранируют перед записью.

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

НаблюдениеРезультат
Полученный файлstreamed.bin
Перенесено байт43680
Порций43
Максимальная порция, байт1024
Последняя порция, байт672
Позиция после EOF43680
SHA-25603ce209e1b6f0dfe21ae78dd9d4462c6c51dad0f5ed1630ea80125a886a4d811
Самостоятельный проект, исходник и архив
Перенести вложение порциями и проверить каждый записанный байтСамостоятельный пример
<?php
declare(strict_types=1);
require __DIR__.'/lab.php';
$db=null;$owned=$removed=$rows=$checks=$observations=[];
try {
    $config=c171Config();$db=c171Connect();$runtime=c171Runtime($db);

    // Подготовка данных продолжает главу о сохранении LOB.
    function attachmentTable171($db,array &$owned,string $label): string {
        $name=c171OwnName($db,$owned,'TABLE',$label);$table=c171Qualified($name);
        c171Ddl($db,'CREATE TABLE '.$table.' (id INT PRIMARY KEY,title VARCHAR(80),body BLOB,note CLOB)');
        c171Commit($db);return $table;
    }
    function closeOwnedLob171(&$lob): void {
        if(is_resource($lob)) {
            if(cubrid_lob2_close($lob)!==true){throw new RuntimeException('Созданный LOB не закрыт');}
            $lob=null;
        }
    }
    function storeAttachment171($db,string $table,int $id,string $title,string $body,string $note): void {
        $query=$binary=$text=null;
        try {
            $binary=cubrid_lob2_new($db,'BLOB');$text=cubrid_lob2_new($db,'CLOB');
            if(!is_resource($binary)||!is_resource($text)){throw new RuntimeException('LOB не создан');}
            if(cubrid_lob2_write($binary,$body)!==true || cubrid_lob2_write($text,$note)!==true){throw new RuntimeException('LOB не заполнен');}
            $query=cubrid_prepare($db,'INSERT INTO '.$table.' (id,title,body,note) VALUES (?,?,?,?)');
            if($query===false || !cubrid_bind($query,1,$id,'int') || !cubrid_bind($query,2,$title,'string')
                || !cubrid_lob2_bind($query,3,$binary,'BLOB') || !cubrid_lob2_bind($query,4,$text,'CLOB')
                || !cubrid_execute($query)){throw new RuntimeException('Вложение не сохранено');}
        } finally {
            c171Release($query);closeOwnedLob171($binary);closeOwnedLob171($text);
        }
    }
    function selectedAttachment171($db,string $table,int $id) {
        $query=cubrid_prepare($db,'SELECT id,title,body,note FROM '.$table.' WHERE id=?');
        if($query===false){throw new RuntimeException('Запрос вложения не подготовлен');}
        if(!cubrid_bind($query,1,$id,'int') || !cubrid_execute($query)){
            c171Release($query);throw new RuntimeException('Запрос вложения не выполнен');
        }
        return $query;
    }
    function fetchedAttachment171($query): array {
        $row=cubrid_fetch($query,CUBRID_ASSOC|CUBRID_LOB);
        if(!is_array($row)||!is_resource($row['body']??null)||!is_resource($row['note']??null)){
            throw new RuntimeException('Нужна строка с двумя заполненными LOB');
        }
        return $row;
    }
    function readPart171($lob,int $length) {
        if($length<1 || $length>8192){throw new InvalidArgumentException('Допустима порция от 1 до 8192 байт');}
        $call=c171Capture(static function()use($lob,$length){return cubrid_lob2_read($lob,$length);});
        if($call['warning'] || $call['code']!==0 || $call['value']===null){c171Failure('Чтение LOB',$call);}
        if($call['value']!==false && (!is_string($call['value'])||$call['value']==='')){
            throw new RuntimeException('Чтение не продвигается');
        }
        return $call['value'];
    }
    function writeAll171($file,string $data): void {
        for($done=0;$done<strlen($data);){
            $written=fwrite($file,substr($data,$done));
            if($written===false || $written===0){throw new RuntimeException('Файл записан не полностью');}
            $done+=$written;
        }
    }

    $table=attachmentTable171($db,$owned,'chunks');$created=$insert=null;
    $unit="ITEM=socket;QTY=3;\0\xff\n";$block=str_repeat($unit,32);$expectedHash=hash_init('sha256');$expectedBytes=0;
    try {
        $created=cubrid_lob2_new($db,'BLOB');
        if(!is_resource($created)){throw new RuntimeException('BLOB не создан');}
        for($i=0;$i<65;$i++){
            if(cubrid_lob2_write($created,$block)!==true){throw new RuntimeException('Блок исходника не записан');}
            hash_update($expectedHash,$block);$expectedBytes+=strlen($block);
        }
        $insert=cubrid_prepare($db,'INSERT INTO '.$table.' (id,title,body,note) VALUES (1,?,?,?)');
        if($insert===false || !cubrid_bind($insert,1,'Журнал поставки','string')
            || !cubrid_lob2_bind($insert,2,$created,'BLOB') || !cubrid_lob2_bind($insert,3,'Порционная копия','CLOB')
            || !cubrid_execute($insert)){throw new RuntimeException('Журнал не сохранён');}
    } finally {c171Release($insert);closeOwnedLob171($created);}
    c171Commit($db);$expectedDigest=hash_final($expectedHash);
    $query=selectedAttachment171($db,$table,1);$row=null;$output=null;
    try {
        $row=fetchedAttachment171($query);$lob=$row['body'];
        $output=fopen(__DIR__.'/streamed.bin','xb');
        if($output===false){throw new RuntimeException('Нужен новый выходной файл');}
        $context=hash_init('sha256');$bytes=0;$chunks=0;$largest=0;$lastLength=0;
        while(true){
            $chunk=readPart171($lob,1024);
            if($chunk===false){break;}
            writeAll171($output,$chunk);hash_update($context,$chunk);
            $lastLength=strlen($chunk);$bytes+=$lastLength;$chunks++;$largest=max($largest,$lastLength);
            if($bytes>$expectedBytes){throw new RuntimeException('Прочитано больше ожидаемого');}
        }
        fclose($output);$output=null;
        $digest=hash_final($context);$position=cubrid_lob2_tell64($lob);
        $rows=[['Полученный файл','streamed.bin'],['Перенесено байт',$bytes],['Порций',$chunks],
            ['Максимальная порция, байт',$largest],['Последняя порция, байт',$lastLength],['Позиция после EOF',$position],['SHA-256',$digest]];
        $checks=['all_bytes'=>$bytes===$expectedBytes,'chunk_bound'=>$largest<=1024 && $chunks>1,
            'partial_last'=>$lastLength>0 && $lastLength<1024,'hash_matches'=>$digest===$expectedDigest,
            'file_matches'=>filesize(__DIR__.'/streamed.bin')===$bytes && hash_file('sha256',__DIR__.'/streamed.bin')===$digest,
            'end_position'=>$position===(string)$bytes];
        $observations=['bytes'=>$bytes,'chunks'=>$chunks,'max_chunk'=>$largest,'last_chunk'=>$lastLength,
            'sha256'=>$digest,'tell64'=>$position,'whole_lob_accumulated_in_php'=>false,'fixture_block_bytes'=>strlen($block)];
    } finally {
        if(is_resource($output)){fclose($output);}
        c171Release($query);$row=null;$lob=null;
    }

} finally {
    if (is_resource($db)) {try {$removed=c171Cleanup($db,$owned);} finally {c171Close($db);}}
}

c171Report('cub171-chunked-read','Перенести вложение порциями и проверить каждый записанный байт',$rows,$checks,$observations,$runtime,$owned,$removed);
Ожидаемый результат
Полученный файл — streamed.bin
Перенесено байт — 43680
Порций — 43
Максимальная порция, байт — 1024
Последняя порция, байт — 672
Позиция после EOF — 43680
SHA-256 — 03ce209e1b6f0dfe21ae78dd9d4462c6c51dad0f5ed1630ea80125a886a4d811

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

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

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

Пример демонстрирует ограниченный буфер прикладного кода на небольшом учебном файле, а не измеряет максимальный размер или всю память CCI. У закреплённого native-драйвера свои расходы; название streaming само по себе не гарантирует постоянную память всего процесса. Строка "0" — данные, поэтому while ($chunk = read(...)) здесь неверен. Ошибка записи и EOF — разные события; после ошибки частичный файл не следует выдавать за готовый.

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

Можно ли для проверки написать $all .= $chunk, а в конце сравнить hash($all)?

Разбор

Это снова удержит весь LOB в PHP-памяти. Для проверки используйте hash_init/hash_update/hash_final, а фактический файл — hash_file. Сам журнал в этом проекте тоже создаётся небольшими повторяемыми блоками.

Прочитать заголовок, выбранный фрагмент и хвост без выгрузки всего LOB

Для проверки файла оператору нужны заголовок, одно поле и завершающая отметка. Нет смысла выгружать всё содержимое ради нескольких фрагментов. Здесь формат маленького файла заранее известен, поэтому можно безопасно задать байтовые границы и сохранить понятный preview.txt.

Путь данных и полученный результат
  1. FIRST 0 → прочитать HEADER| → позиция 7
  2. Прочитать полный UTF-8 заголовок; CURRENT +1 пропускает LF
  3. tell64 сохраняет начало BODY строкой → seek64 возвращается туда
  4. LAST 8 → TAIL|OK и LF → preview.txt с тремя видимыми строками

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

if (!cubrid_lob2_seek($lob, 0, CUBRID_CURSOR_FIRST)) {
    throw new RuntimeException('Позиция не задана');
}
$header = cubrid_lob2_read($lob, 7); // HEADER|
$nextPosition = cubrid_lob2_tell($lob); // 7

Нулевая позиция — начало данных; tell возвращает позицию следующего чтения, а не номер последнего символа. CURRENT прибавляет смещение к текущей позиции. LAST с положительным 8 отступает на восемь байтов от конца. seek64 получает десятичную строку, tell64 возвращает строку; здесь этот интерфейс проверяется на небольшом файле, без создания гигабайтного объекта.

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

НаблюдениеРезультат
МаркерHEADER|
Позиция после маркера7
ЗаголовокЗаказ 17
Закладка BODY как строка21
Выбранный текстПроверить O'Reilly
ХвостTAIL|OK
Позиция в конце62
Выход перед началом отклонёнда
Самостоятельный проект, исходник и архив
Прочитать заголовок, выбранный фрагмент и хвост без выгрузки всего LOBСамостоятельный пример
<?php
declare(strict_types=1);
require __DIR__.'/lab.php';
$db=null;$owned=$removed=$rows=$checks=$observations=[];
try {
    $config=c171Config();$db=c171Connect();$runtime=c171Runtime($db);

    // Подготовка данных продолжает главу о сохранении LOB.
    function attachmentTable171($db,array &$owned,string $label): string {
        $name=c171OwnName($db,$owned,'TABLE',$label);$table=c171Qualified($name);
        c171Ddl($db,'CREATE TABLE '.$table.' (id INT PRIMARY KEY,title VARCHAR(80),body BLOB,note CLOB)');
        c171Commit($db);return $table;
    }
    function closeOwnedLob171(&$lob): void {
        if(is_resource($lob)) {
            if(cubrid_lob2_close($lob)!==true){throw new RuntimeException('Созданный LOB не закрыт');}
            $lob=null;
        }
    }
    function storeAttachment171($db,string $table,int $id,string $title,string $body,string $note): void {
        $query=$binary=$text=null;
        try {
            $binary=cubrid_lob2_new($db,'BLOB');$text=cubrid_lob2_new($db,'CLOB');
            if(!is_resource($binary)||!is_resource($text)){throw new RuntimeException('LOB не создан');}
            if(cubrid_lob2_write($binary,$body)!==true || cubrid_lob2_write($text,$note)!==true){throw new RuntimeException('LOB не заполнен');}
            $query=cubrid_prepare($db,'INSERT INTO '.$table.' (id,title,body,note) VALUES (?,?,?,?)');
            if($query===false || !cubrid_bind($query,1,$id,'int') || !cubrid_bind($query,2,$title,'string')
                || !cubrid_lob2_bind($query,3,$binary,'BLOB') || !cubrid_lob2_bind($query,4,$text,'CLOB')
                || !cubrid_execute($query)){throw new RuntimeException('Вложение не сохранено');}
        } finally {
            c171Release($query);closeOwnedLob171($binary);closeOwnedLob171($text);
        }
    }
    function selectedAttachment171($db,string $table,int $id) {
        $query=cubrid_prepare($db,'SELECT id,title,body,note FROM '.$table.' WHERE id=?');
        if($query===false){throw new RuntimeException('Запрос вложения не подготовлен');}
        if(!cubrid_bind($query,1,$id,'int') || !cubrid_execute($query)){
            c171Release($query);throw new RuntimeException('Запрос вложения не выполнен');
        }
        return $query;
    }
    function fetchedAttachment171($query): array {
        $row=cubrid_fetch($query,CUBRID_ASSOC|CUBRID_LOB);
        if(!is_array($row)||!is_resource($row['body']??null)||!is_resource($row['note']??null)){
            throw new RuntimeException('Нужна строка с двумя заполненными LOB');
        }
        return $row;
    }
    function readPart171($lob,int $length) {
        if($length<1 || $length>8192){throw new InvalidArgumentException('Допустима порция от 1 до 8192 байт');}
        $call=c171Capture(static function()use($lob,$length){return cubrid_lob2_read($lob,$length);});
        if($call['warning'] || $call['code']!==0 || $call['value']===null){c171Failure('Чтение LOB',$call);}
        if($call['value']!==false && (!is_string($call['value'])||$call['value']==='')){
            throw new RuntimeException('Чтение не продвигается');
        }
        return $call['value'];
    }
    function writeAll171($file,string $data): void {
        for($done=0;$done<strlen($data);){
            $written=fwrite($file,substr($data,$done));
            if($written===false || $written===0){throw new RuntimeException('Файл записан не полностью');}
            $done+=$written;
        }
    }

    $table=attachmentTable171($db,$owned,'preview');
    $source="HEADER|Заказ 17\nBODY|Проверить O'Reilly\nTAIL|OK\n";
    storeAttachment171($db,$table,1,'Служебная карточка',$source,'Предпросмотр');c171Commit($db);
    $query=selectedAttachment171($db,$table,1);$row=null;
    try {
        $row=fetchedAttachment171($query);$lob=$row['body'];
        if(!cubrid_lob2_seek($lob,0,CUBRID_CURSOR_FIRST)){throw new RuntimeException('Нет начала LOB');}
        $magic=readPart171($lob,7);$p7=cubrid_lob2_tell($lob);
        $header=readPart171($lob,strlen('Заказ 17'));$headerEnd=cubrid_lob2_tell64($lob);
        if(!cubrid_lob2_seek($lob,1,CUBRID_CURSOR_CURRENT)){throw new RuntimeException('Не пропущен перевод строки');}
        $bodyOffset=cubrid_lob2_tell64($lob);
        if(!is_string($bodyOffset) || !preg_match('/^[0-9]+$/D',$bodyOffset)){throw new RuntimeException('Нет строковой позиции');}
        $bodyMarker=readPart171($lob,5);
        $bodyText=readPart171($lob,strlen("Проверить O'Reilly"));
        if(!cubrid_lob2_seek64($lob,$bodyOffset,CUBRID_CURSOR_FIRST)){throw new RuntimeException('Не восстановлена закладка');}
        $again=readPart171($lob,5);
        if(!cubrid_lob2_seek64($lob,'8',CUBRID_CURSOR_LAST)){throw new RuntimeException('Нет хвоста');}
        $tail=readPart171($lob,8);$end=cubrid_lob2_tell64($lob);
        $bad=c171Capture(static function()use($lob){return cubrid_lob2_seek($lob,-1,CUBRID_CURSOR_FIRST);});
        $afterBad=cubrid_lob2_tell64($lob);
        $preview='Заголовок: '.$header."\nФрагмент: ".$bodyText."\nКонец: ".rtrim($tail,"\n")."\n";
        $output=fopen(__DIR__.'/preview.txt','xb');
        if($output===false){throw new RuntimeException('Предпросмотр уже существует');}
        try{writeAll171($output,$preview);}finally{fclose($output);}
        $rows=[['Маркер',$magic],['Позиция после маркера',$p7],['Заголовок',$header],
            ['Закладка BODY как строка',$bodyOffset],['Выбранный текст',$bodyText],['Хвост',rtrim($tail,"\n")],
            ['Позиция в конце',$end],['Выход перед началом отклонён',$bad['value']===false && $bad['warning']]];
        $checks=['header'=>$magic==='HEADER|' && $p7===7 && $header==='Заказ 17',
            'utf8_byte_position'=>$headerEnd===(string)strlen('HEADER|Заказ 17'),
            'bookmark_string'=>is_string($bodyOffset) && $bodyOffset===(string)strlen("HEADER|Заказ 17\n"),
            'same_bookmark'=>$bodyMarker==='BODY|' && $again===$bodyMarker,
            'utf8_apostrophe'=>$bodyText==="Проверить O'Reilly",
            'tail'=>$tail==="TAIL|OK\n" && $end===(string)strlen($source),
            'before_start_refused'=>$bad['value']===false && $bad['warning'] && $afterBad===$end,
            'preview_exact'=>$preview==="Заголовок: Заказ 17\nФрагмент: Проверить O'Reilly\nКонец: TAIL|OK\n"];
        $observations=['header'=>$header,'header_end'=>$headerEnd,'body_offset'=>$bodyOffset,'body'=>$bodyText,
            'tail'=>$tail,'end'=>$end,'invalid_seek'=>$bad,'after_invalid'=>$afterBad,'preview'=>$preview];
    } finally {c171Release($query);$row=null;$lob=null;}

} finally {
    if (is_resource($db)) {try {$removed=c171Cleanup($db,$owned);} finally {c171Close($db);}}
}

c171Report('cub171-seek-preview','Прочитать заголовок, выбранный фрагмент и хвост без выгрузки всего LOB',$rows,$checks,$observations,$runtime,$owned,$removed);
Ожидаемый результат
Маркер — HEADER|
Позиция после маркера — 7
Заголовок — Заказ 17
Закладка BODY как строка — 21
Выбранный текст — Проверить O'Reilly
Хвост — TAIL|OK
Позиция в конце — 62
Выход перед началом отклонён — да

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

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

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

Байтовые границы здесь определены форматом и strlen UTF-8-строк, а не числом букв. Произвольный срез может разрезать многобайтный символ; выводите только целый декодированный фрагмент или обрабатывайте границы декодером. seek64 не даёт бесконечной точности: внутри драйвера signed64. Не переводите большие строковые позиции в float. Начало/конец файла не является границей SQL-курсора строк.

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

Что передать, чтобы перечитать предыдущие пять байтов относительно текущего места?

Разбор

Используйте cubrid_lob2_seek($lob, -5, CUBRID_CURSOR_CURRENT), предварительно убедившись, что текущая позиция не меньше 5. Для LAST тот же знак имеет другое правило: чтобы отступить на 5 от конца, передают положительное 5.

Составить CSV двух вложений, потребляя LOB в пределах текущей строки

Нужен небольшой реестр вложений: товар, размер файла и текстовая подпись. SELECT сразу выбирает два нужных товара и порядок. В каждой строке есть BLOB файла и CLOB подписи; оба читаются до перехода к следующей строке. Получится настоящий CSV с русскими названиями и непотерянным содержимым «0».

Путь данных и полученный результат
  1. SQL: id 1 и 2, нужные четыре столбца, ORDER BY id LIMIT 2
  2. Строка 1 → прочитать BLOB и CLOB → CSV Кабель;5;подпись
  3. Строка 2 → обработать её собственные LOB → CSV Хомут;1;подпись
  4. Завершить SQL-запрос, освободить ссылки на дескрипторы, закрыть CSV

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

$row = cubrid_fetch($request, CUBRID_ASSOC | CUBRID_LOB);
if (!is_array($row)) { throw new RuntimeException('Строка недоступна'); }
$chunk = cubrid_lob2_read($row['body'], 32);
// Полностью обработайте body и note текущей строки до следующего fetch.

CUBRID_ASSOC задаёт имена ключей, CUBRID_LOB просит вернуть LOB-дескрипторы вместо содержимого. Это два разных курсора: fetch движет строки результата, read — байтовую позицию одного LOB. Полный проект сохраняет ограниченную подпись целиком, чтобы не разрезать UTF-8 при выводе, а бинарное содержимое учитывает порциями.

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

НаблюдениеРезультат
Кабель5 байт; Кабель O'Reilly
Хомут1 байт; Хомут
Строк в каталоге2
Файлattachment-catalog.csv
Строка 0 не потерянада
Самостоятельный проект, исходник и архив
Составить CSV двух вложений, потребляя LOB в пределах текущей строкиСамостоятельный пример
<?php
declare(strict_types=1);
require __DIR__.'/lab.php';
$db=null;$owned=$removed=$rows=$checks=$observations=[];
try {
    $config=c171Config();$db=c171Connect();$runtime=c171Runtime($db);

    // Подготовка данных продолжает главу о сохранении LOB.
    function attachmentTable171($db,array &$owned,string $label): string {
        $name=c171OwnName($db,$owned,'TABLE',$label);$table=c171Qualified($name);
        c171Ddl($db,'CREATE TABLE '.$table.' (id INT PRIMARY KEY,title VARCHAR(80),body BLOB,note CLOB)');
        c171Commit($db);return $table;
    }
    function closeOwnedLob171(&$lob): void {
        if(is_resource($lob)) {
            if(cubrid_lob2_close($lob)!==true){throw new RuntimeException('Созданный LOB не закрыт');}
            $lob=null;
        }
    }
    function storeAttachment171($db,string $table,int $id,string $title,string $body,string $note): void {
        $query=$binary=$text=null;
        try {
            $binary=cubrid_lob2_new($db,'BLOB');$text=cubrid_lob2_new($db,'CLOB');
            if(!is_resource($binary)||!is_resource($text)){throw new RuntimeException('LOB не создан');}
            if(cubrid_lob2_write($binary,$body)!==true || cubrid_lob2_write($text,$note)!==true){throw new RuntimeException('LOB не заполнен');}
            $query=cubrid_prepare($db,'INSERT INTO '.$table.' (id,title,body,note) VALUES (?,?,?,?)');
            if($query===false || !cubrid_bind($query,1,$id,'int') || !cubrid_bind($query,2,$title,'string')
                || !cubrid_lob2_bind($query,3,$binary,'BLOB') || !cubrid_lob2_bind($query,4,$text,'CLOB')
                || !cubrid_execute($query)){throw new RuntimeException('Вложение не сохранено');}
        } finally {
            c171Release($query);closeOwnedLob171($binary);closeOwnedLob171($text);
        }
    }
    function selectedAttachment171($db,string $table,int $id) {
        $query=cubrid_prepare($db,'SELECT id,title,body,note FROM '.$table.' WHERE id=?');
        if($query===false){throw new RuntimeException('Запрос вложения не подготовлен');}
        if(!cubrid_bind($query,1,$id,'int') || !cubrid_execute($query)){
            c171Release($query);throw new RuntimeException('Запрос вложения не выполнен');
        }
        return $query;
    }
    function fetchedAttachment171($query): array {
        $row=cubrid_fetch($query,CUBRID_ASSOC|CUBRID_LOB);
        if(!is_array($row)||!is_resource($row['body']??null)||!is_resource($row['note']??null)){
            throw new RuntimeException('Нужна строка с двумя заполненными LOB');
        }
        return $row;
    }
    function readPart171($lob,int $length) {
        if($length<1 || $length>8192){throw new InvalidArgumentException('Допустима порция от 1 до 8192 байт');}
        $call=c171Capture(static function()use($lob,$length){return cubrid_lob2_read($lob,$length);});
        if($call['warning'] || $call['code']!==0 || $call['value']===null){c171Failure('Чтение LOB',$call);}
        if($call['value']!==false && (!is_string($call['value'])||$call['value']==='')){
            throw new RuntimeException('Чтение не продвигается');
        }
        return $call['value'];
    }
    function writeAll171($file,string $data): void {
        for($done=0;$done<strlen($data);){
            $written=fwrite($file,substr($data,$done));
            if($written===false || $written===0){throw new RuntimeException('Файл записан не полностью');}
            $done+=$written;
        }
    }

    $table=attachmentTable171($db,$owned,'catalog');
    storeAttachment171($db,$table,1,'Кабель',"BIN\0A","Кабель O'Reilly");
    storeAttachment171($db,$table,2,'Хомут','0','Хомут');c171Commit($db);
    $query=cubrid_prepare($db,'SELECT id,title,body,note FROM '.$table.' WHERE id IN (1,2) ORDER BY id LIMIT 2');
    if($query===false || !cubrid_execute($query)){throw new RuntimeException('Каталог не выбран');}
    $output=null;$row=null;$held=[];$catalog=[];
    try {
        $output=fopen(__DIR__.'/attachment-catalog.csv','xb');
        if($output===false){throw new RuntimeException('Нужен новый CSV');}
        if(fputcsv($output,['id','title','bytes','note'],';','"','\\')===false){throw new RuntimeException('Не записан заголовок');}
        while(true){
            $call=c171Capture(static function()use($query){return cubrid_fetch($query,CUBRID_ASSOC|CUBRID_LOB);});
            if($call['warning'] || $call['code']!==0 || $call['value']===null){c171Failure('Чтение строки',$call);}
            if($call['value']===false){break;}
            $row=$call['value'];unset($call);
            if(!is_resource($row['body']) || !is_resource($row['note'])){throw new RuntimeException('Нет двух LOB');}
            // Держим PHP-ссылки до завершения запроса; старые LOB больше не читаем.
            $held[]=$row['body'];$held[]=$row['note'];
            $bytes=0;$digest=hash_init('sha256');
            while(($part=readPart171($row['body'],32))!==false){$bytes+=strlen($part);hash_update($digest,$part);}
            $note='';
            while(($part=readPart171($row['note'],32))!==false){
                if(strlen($note)+strlen($part)>128){throw new RuntimeException('Подпись слишком длинная');}
                $note.=$part;
            }
            $record=[(int)$row['id'],$row['title'],$bytes,$note];
            if(fputcsv($output,$record,';','"','\\')===false){throw new RuntimeException('Не записана строка CSV');}
            $catalog[]=['id'=>$record[0],'title'=>$record[1],'bytes'=>$record[2],'note'=>$record[3],'sha256'=>hash_final($digest)];
        }
        fclose($output);$output=null;
    } finally {
        if(is_resource($output)){fclose($output);}
        c171Release($query);$row=null;$call=null;$held=[];
    }
    $expected="id;title;bytes;note\n1;Кабель;5;\"Кабель O'Reilly\"\n2;Хомут;1;Хомут\n";
    $rows=[['Кабель',$catalog[0]['bytes'].' байт; '.$catalog[0]['note']],['Хомут',$catalog[1]['bytes'].' байт; '.$catalog[1]['note']],
        ['Строк в каталоге',count($catalog)],['Файл','attachment-catalog.csv'],['Строка 0 не потеряна',$catalog[1]['sha256']===hash('sha256','0')]];
    $checks=['two_rows'=>count($catalog)===2,'first_payload'=>$catalog[0]['bytes']===5 && $catalog[0]['sha256']===hash('sha256',"BIN\0A"),
        'utf8_apostrophe'=>$catalog[0]['note']==="Кабель O'Reilly",'second_text'=>$catalog[1]['note']==='Хомут',
        'zero_is_data'=>$catalog[1]['bytes']===1 && $catalog[1]['sha256']===hash('sha256','0'),
        'exact_csv'=>filesize(__DIR__.'/attachment-catalog.csv')===strlen($expected) && hash_file('sha256',__DIR__.'/attachment-catalog.csv')===hash('sha256',$expected),
        'request_released'=>$query===null && $held===[]];
    $observations=['catalog'=>$catalog,'csv'=>$expected,'selected_rows'=>2,'lob_handles_per_row'=>2,
        'lifecycle'=>'consume current row; retain resource references; finish request; release references'];

} finally {
    if (is_resource($db)) {try {$removed=c171Cleanup($db,$owned);} finally {c171Close($db);}}
}

c171Report('cub171-attachment-catalog','Составить CSV двух вложений, потребляя LOB в пределах текущей строки',$rows,$checks,$observations,$runtime,$owned,$removed);
Ожидаемый результат
Кабель — 5 байт; Кабель O'Reilly
Хомут — 1 байт; Хомут
Строк в каталоге — 2
Файл — attachment-catalog.csv
Строка 0 не потеряна — да

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

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

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

В закреплённом драйвере 11.2.0.0002 запрос хранит внутренние указатели на fetched LOB. Поэтому здесь не вызывают lob2_close для fetched-ресурса перед следующим fetch/закрытием запроса: сначала полностью потребляют данные, удерживают небольшое число PHP-ссылок, завершают запрос и освобождают ссылки. Для большого каталога разумно получать LOB отдельным коротким запросом на выбранную запись, а в списке показывать сохранённые метаданные. Не сохраняйте такие дескрипторы между HTTP-запросами. Это версия-зависимый порядок, не универсальное обещание времени жизни ресурса.

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

Почему нельзя сперва собрать все дескрипторы из fetch, закрыть запрос, а потом прочитать каждый?

Разбор

Дескриптор не содержит самостоятельную копию файла. Продвижение и завершение запроса меняют его связь с соединением. Потребляйте LOB, пока текущая строка и запрос действительны; для отложенной обработки сохраните файл или бизнес-идентификатор и выполните новый авторизованный SELECT.

Память, текстовые границы и время жизни ресурсов

SQL NULL, пустой LOB и LOB со строкой "0" — три разных состояния. NULL нужно обработать до вызовов LOB API; пустое содержимое имеет размер 0; строка "0" содержит один байт. Глава сохранения вложений показывает эти состояния отдельно. В этой главе реестр намеренно включает "0", чтобы обычная проверка истинности не скрыла данные.

Для fetched LOB применён проверенный порядок конкретного драйвера: прочитать данные текущей строки, удержать ресурсы, закрыть SQL-запрос, затем освободить ссылки. Причина ограничения видна в закреплённом native-исходнике: запрос хранит внутренние указатели на ресурсы. Небольшой реестр удерживает четыре ресурса; он не предлагает накопить все вложения большой таблицы. В большом списке выбирайте только метаданные и открывайте содержимое отдельным параметризованным запросом для разрешённой записи.

Стриминг имеет смысл, когда приложение не собирает все порции обратно в огромную строку: реальная проблема исчерпания памяти после stream_get_contents показывает эту ловушку. Путаница длины текста и байтового смещения объясняет необходимость сохранять tell, а не число букв. Историческая проблема позиционирования за диапазоном int помогает понять строковые варианты API. Эти вопросы задают проблемы; технические решения здесь основаны на CUBRID, а не на переносе чужих FTP или fseek-обходов.

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

Шесть операций чтения и позиционирования

ФункцияКогда использовать
cubrid_lob2_readПолучить ограниченную порцию байтов BLOB/CLOB для обработки, передачи или записи в файл.
cubrid_lob2_exportВыгрузить целое вложение из BLOB/CLOB в новый файл на машине PHP.
cubrid_lob2_seekПереставить байтовый курсор LOB перед чтением заголовка, середины или хвоста.
cubrid_lob2_seek64Переставить курсор LOB, передав смещение десятичной строкой вместо ограниченного PHP int.
cubrid_lob2_tellУзнать текущую байтовую позицию LOB как целое число, например после чтения заголовка.
cubrid_lob2_tell64Получить позицию LOB десятичной строкой для закладки и сопоставления с большим размером.

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

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

cubrid_lob2_readПолучить ограниченную порцию байтов BLOB/CLOB для обработки, передачи или записи в файл.

PECL CUBRID >= 8.4.1 · cubrid

cubrid_lob2_read(resource $lob_identifier, int $len): string

Получить ограниченную порцию байтов BLOB/CLOB для обработки, передачи или записи в файл.

ПараметрЧто передать
lob_identifierДействующий ресурс BLOB или CLOB, созданный cubrid_lob2_new либо полученный из результата с флагом CUBRID_LOB. Это не путь к файлу, не текст содержимого и не обычный PHP-поток.
lenОбязательное целое число: сколько байтов запросить с текущей позиции. В проекте порция положительна и ограничена 1024 байтами; меньший остаток возвращается без дополнения.

Результат: Строка реально прочитанных байтов. Официальное описание отдельно указывает false при конце данных и NULL при отказе. Сводная сигнатура string не перечисляет эти альтернативы; их нужно проверять явно.

При отказе сразу сохраните cubrid_error_code(), cubrid_error_code_facility() и cubrid_error_msg(), пока следующий вызов не изменил диагностику. Полный проект использует c171Capture для перехвата предупреждения и сохранения признаков отказа. Проверяйте строгий тип результата: нулевой размер и пустое содержимое не обязательно означают ошибку.

Доступно с PECL CUBRID 8.4.1. Примеры этой главы используют native cubrid 11.2.0.0002, CCI 11.2.0 и сервер 11.3.5.1274. В коротком фрагменте $lob уже получен из выбранной записи; полный проект показывает создание данных и освобождение ресурсов. Вызов функции записан как имя(аргументы), результат присваивается переменной слева от =, а === проверяет значение вместе с типом.

Ресурс связан с соединением и временем жизни запроса. В закреплённом драйвере LOB из fetch нужно прочитать до следующей строки, сохранить PHP-ссылку до завершения запроса и затем освободить её. Не вызывайте lob2_close для такого ресурса перед следующим fetch или close_request: запрос хранит внутренний указатель на него. Для самостоятельно созданного LOB правила явного закрытия разобраны отдельно. Право читать выбранную запись приложение проверяет до получения ресурса.

Порция и продвижение курсора
  1. LOB: abcdefghij, позиция 2
  2. read(..., 4) → строка cdef
  3. Курсор 6 → следующая порция ghij
  4. Следующее положительное чтение на конце → false

Применение 1: последовательно перенести журнал в файл

$part = cubrid_lob2_read($lob, 1024);
if ($part === null) { throw new RuntimeException('LOB не прочитан'); }
if ($part !== false) {
    $bytes = strlen($part);
    // Передайте именно эти байты в файл или обработчик.
}

Это одна итерация порционного экспорта с контрольной суммой. Полный цикл сразу сохраняет диагностику, корректно дописывает неполную запись fwrite и не складывает все порции в одну строку. Строка "0" — один байт данных, а не конец. Сравнение while ($part = read(...)) такую строку потеряет.

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

$note = '';
while (true) {
    $part = cubrid_lob2_read($noteLob, 32);
    if ($part === null) { throw new RuntimeException('Подпись не прочитана'); }
    if ($part === false) { break; }
    if ($part === '' || strlen($note) + strlen($part) > 128) {
        throw new RuntimeException('Подпись не соответствует лимиту');
    }
    $note .= $part;
}

CSV-реестр товаров показывает два LOB в каждой SQL-строке. Собирать ограниченную подпись целиком допустимо: после чтения получается законченный UTF-8 для экранирования/CSV. Размер порции выражен в байтах; граница порции может попасть внутрь многобайтовой буквы. Для большого текста нужен обработчик, сохраняющий неполный символ между порциями.

Если требуется целый файл без собственной обработки порций, используйте cubrid_lob2_export. Для начала с определённого места нужен cubrid_lob2_seek, для получения позиции после чтения — cubrid_lob2_tell.

cubrid_lob2_exportВыгрузить целое вложение из BLOB/CLOB в новый файл на машине PHP.

PECL CUBRID >= 8.4.1 · cubrid

cubrid_lob2_export(resource $lob_identifier, string $file_name): bool

Выгрузить целое вложение из BLOB/CLOB в новый файл на машине PHP.

ПараметрЧто передать
lob_identifierДействующий ресурс BLOB или CLOB, созданный cubrid_lob2_new либо полученный из результата с флагом CUBRID_LOB. Это не путь к файлу, не текст содержимого и не обычный PHP-поток.
file_nameОбязательная строка с именем нового файла на стороне процесса PHP. Каталог должен существовать и быть доступным для записи.

Результат: true при завершённой выгрузке, false при отказе. Функция записывает файл; она не возвращает его содержимое, количество байтов или HTTP-ответ.

При отказе сразу сохраните cubrid_error_code(), cubrid_error_code_facility() и cubrid_error_msg(), пока следующий вызов не изменил диагностику. Полный проект использует c171Capture для перехвата предупреждения и сохранения признаков отказа. Проверяйте строгий тип результата: нулевой размер и пустое содержимое не обязательно означают ошибку.

Доступно с PECL CUBRID 8.4.1. Примеры этой главы используют native cubrid 11.2.0.0002, CCI 11.2.0 и сервер 11.3.5.1274. В коротком фрагменте $lob уже получен из выбранной записи; полный проект показывает создание данных и освобождение ресурсов. Вызов функции записан как имя(аргументы), результат присваивается переменной слева от =, а === проверяет значение вместе с типом.

Ресурс связан с соединением и временем жизни запроса. В закреплённом драйвере LOB из fetch нужно прочитать до следующей строки, сохранить PHP-ссылку до завершения запроса и затем освободить её. Не вызывайте lob2_close для такого ресурса перед следующим fetch или close_request: запрос хранит внутренний указатель на него. Для самостоятельно созданного LOB правила явного закрытия разобраны отдельно. Право читать выбранную запись приложение проверяет до получения ресурса.

Вывод целого файла
  1. Строка товара → действующий BLOB
  2. export(lob, attachment.bin)
  3. Новый файл со всеми байтами, включая NUL и FF
  4. Текущая позиция LOB остаётся прежней в проверенном драйвере

Применение 1: передать файл измерений внешнему инструменту

$path = __DIR__ . '/attachment.bin';
if (cubrid_lob2_export($lob, $path) !== true) {
    throw new RuntimeException('Файл не выгружен');
}
$bytes = filesize($path);
$sha256 = hash_file('sha256', $path);

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

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

$path = __DIR__ . '/instruction.txt';
if (cubrid_lob2_export($instructionClob, $path) !== true) {
    throw new RuntimeException('Инструкция не выгружена');
}

После сохранения текстовой инструкции CLOB можно выгрузить тем же способом. Функция не выбирает кодировку за приложение; экспортированные байты должны соответствовать ожидаемому формату. В отличие от CSV-реестра, здесь не формируется новая таблица: сохраняется одно готовое содержимое целиком.

Это файловая операция на машине PHP, а не на машине браузера и не путь сервера CUBRID. Не обещайте атомарную готовность файла при любой ошибке: сбой ввода-вывода может оставить неполный новый файл. Для обработки порций и собственного контроля записи нужен cubrid_lob2_read. Проверка локального экспорта не является проверкой HTTP-загрузки.

cubrid_lob2_seekПереставить байтовый курсор LOB перед чтением заголовка, середины или хвоста.

PECL CUBRID >= 8.4.1 · cubrid

cubrid_lob2_seek(resource $lob_identifier, int $offset, int $origin = CUBRID_CURSOR_CURRENT): bool

Переставить байтовый курсор LOB перед чтением заголовка, середины или хвоста.

ПараметрЧто передать
lob_identifierДействующий ресурс BLOB или CLOB, созданный cubrid_lob2_new либо полученный из результата с флагом CUBRID_LOB. Это не путь к файлу, не текст содержимого и не обычный PHP-поток.
offsetОбязательное целое смещение в байтах. Знак зависит от origin; диапазон будущей позиции ограничен размером LOB.
originНеобязательная константа; по умолчанию CUBRID_CURSOR_CURRENT. Выбирайте только FIRST, CURRENT или LAST из таблицы ниже.

Результат: true при успешном позиционировании, false при отказе. Возвращается результат действия, а не новая позиция; её читает tell.

При отказе сразу сохраните cubrid_error_code(), cubrid_error_code_facility() и cubrid_error_msg(), пока следующий вызов не изменил диагностику. Полный проект использует c171Capture для перехвата предупреждения и сохранения признаков отказа. Проверяйте строгий тип результата: нулевой размер и пустое содержимое не обязательно означают ошибку.

Доступно с PECL CUBRID 8.4.1. Примеры этой главы используют native cubrid 11.2.0.0002, CCI 11.2.0 и сервер 11.3.5.1274. В коротком фрагменте $lob уже получен из выбранной записи; полный проект показывает создание данных и освобождение ресурсов. Вызов функции записан как имя(аргументы), результат присваивается переменной слева от =, а === проверяет значение вместе с типом.

Ресурс связан с соединением и временем жизни запроса. В закреплённом драйвере LOB из fetch нужно прочитать до следующей строки, сохранить PHP-ссылку до завершения запроса и затем освободить её. Не вызывайте lob2_close для такого ресурса перед следующим fetch или close_request: запрос хранит внутренний указатель на него. Для самостоятельно созданного LOB правила явного закрытия разобраны отдельно. Право читать выбранную запись приложение проверяет до получения ресурса.

originКак вычисляется позицияПример при размере 10
CUBRID_CURSOR_FIRSTОт начала: позиция = offset; используйте неотрицательное смещениеoffset 0 → начало; 3 → позиция 3
CUBRID_CURSOR_CURRENTОт текущего места: позиция = текущая + offset; допустимы оба знакаИз позиции 3 смещение −1 → 2
CUBRID_CURSOR_LASTОт конца назад: позиция = размер − offset; смещение неотрицательноеoffset 2 → позиция 8; 0 → конец

Курсор указывает на границу перед следующим байтом: начало имеет номер 0, конец — размер LOB. Seek не читает, не вставляет и не меняет содержимое. Официальное описание использует слово positive для FIRST/LAST, но пример и проверенный драйвер принимают 0; отрицательное смещение допустимо для CURRENT, если итоговая позиция остаётся в пределах.

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

if (!cubrid_lob2_seek($lob, 7, CUBRID_CURSOR_FIRST)) {
    throw new RuntimeException('Нет позиции после HEADER|');
}
$header = cubrid_lob2_read($lob, strlen('Заказ 17'));

В предпросмотре документа семь ASCII-байтов HEADER| отделены от русского заголовка. strlen здесь считает байты известного UTF-8-фрагмента, а не число отображаемых букв. Служебные границы известны формату; произвольный пользовательский offset не гарантирует целую букву.

Применение 2: продолжить чтение после полного экспорта

if (!cubrid_lob2_seek($lob, 4, CUBRID_CURSOR_FIRST)) {
    throw new RuntimeException('Позиция недоступна');
}
$before = cubrid_lob2_tell($lob);
if (!cubrid_lob2_export($lob, __DIR__ . '/copy.bin')) {
    throw new RuntimeException('Копия не создана');
}
$after = cubrid_lob2_tell($lob);

Проект экспорта показывает before=4 и after=4: выгрузка целого содержимого не означает чтение от текущей позиции. Если требуется вернуть начало для последующего порционного чтения, это отдельный seek(..., 0, FIRST).

cubrid_lob2_seek64Переставить курсор LOB, передав смещение десятичной строкой вместо ограниченного PHP int.

PECL CUBRID >= 8.4.1 · cubrid

cubrid_lob2_seek64(resource $lob_identifier, string $offset, int $origin = CUBRID_CURSOR_CURRENT): bool

Переставить курсор LOB, передав смещение десятичной строкой вместо ограниченного PHP int.

ПараметрЧто передать
lob_identifierДействующий ресурс BLOB или CLOB, созданный cubrid_lob2_new либо полученный из результата с флагом CUBRID_LOB. Это не путь к файлу, не текст содержимого и не обычный PHP-поток.
offsetОбязательная десятичная строка смещения, например "0", "19" или "-1" для CURRENT. Это строковое представление целого, не float и не число в научной нотации.
originНеобязательная константа FIRST, CURRENT или LAST; значение по умолчанию CURRENT.

Результат: true при успешном перемещении, false при отказе. Содержимое LOB не возвращается.

При отказе сразу сохраните cubrid_error_code(), cubrid_error_code_facility() и cubrid_error_msg(), пока следующий вызов не изменил диагностику. Полный проект использует c171Capture для перехвата предупреждения и сохранения признаков отказа. Проверяйте строгий тип результата: нулевой размер и пустое содержимое не обязательно означают ошибку.

Доступно с PECL CUBRID 8.4.1. Примеры этой главы используют native cubrid 11.2.0.0002, CCI 11.2.0 и сервер 11.3.5.1274. В коротком фрагменте $lob уже получен из выбранной записи; полный проект показывает создание данных и освобождение ресурсов. Вызов функции записан как имя(аргументы), результат присваивается переменной слева от =, а === проверяет значение вместе с типом.

Ресурс связан с соединением и временем жизни запроса. В закреплённом драйвере LOB из fetch нужно прочитать до следующей строки, сохранить PHP-ссылку до завершения запроса и затем освободить её. Не вызывайте lob2_close для такого ресурса перед следующим fetch или close_request: запрос хранит внутренний указатель на него. Для самостоятельно созданного LOB правила явного закрытия разобраны отдельно. Право читать выбранную запись приложение проверяет до получения ресурса.

originКак вычисляется позицияПример при размере 10
CUBRID_CURSOR_FIRSTОт начала: позиция = offset; используйте неотрицательное смещениеoffset 0 → начало; 3 → позиция 3
CUBRID_CURSOR_CURRENTОт текущего места: позиция = текущая + offset; допустимы оба знакаИз позиции 3 смещение −1 → 2
CUBRID_CURSOR_LASTОт конца назад: позиция = размер − offset; смещение неотрицательноеoffset 2 → позиция 8; 0 → конец

Функция нужна, когда смещение не помещается в обычный integer платформы PHP. Строка избегает преждевременного преобразования в int или float, но не создаёт неограниченную точность: закреплённый драйвер преобразует значение в signed 64-bit. Сохраняйте каноническую десятичную запись и проверяйте диапазон до вызова. Одной проверки «начинается с цифры» недостаточно.

Применение 1: вернуться к сохранённой границе раздела

$bookmark = cubrid_lob2_tell64($lob);
if ($bookmark === false) { throw new RuntimeException('Позиция не получена'); }
// После другого чтения вернуть именно сохранённую байтовую границу.
if (!cubrid_lob2_seek64($lob, $bookmark, CUBRID_CURSOR_FIRST)) {
    throw new RuntimeException('Закладка недоступна');
}

Предпросмотр с закладкой BODY| сохраняет позицию строкой и повторно читает тот же маркер. Закладка относится к конкретной версии содержимого; если вложение заменили, прежнее число больше не гарантирует правильный фрагмент.

Применение 2: проверить небольшой хвост документа

if (!cubrid_lob2_seek64($lob, '8', CUBRID_CURSOR_LAST)) {
    throw new RuntimeException('Документ короче хвоста');
}
$tail = cubrid_lob2_read($lob, 8);

Этот независимый приём из проекта чтения заголовка и окончания даёт TAIL|OK и перевод строки без чтения всей середины. LAST отсчитывает положительное расстояние назад: "8", а не "-8". Для подготовки порционной копии с начала используйте строку "0" и FIRST.

cubrid_lob2_tellУзнать текущую байтовую позицию LOB как целое число, например после чтения заголовка.

PECL CUBRID >= 8.4.1 · cubrid

cubrid_lob2_tell(resource $lob_identifier): int

Узнать текущую байтовую позицию LOB как целое число, например после чтения заголовка.

ПараметрЧто передать
lob_identifierДействующий ресурс BLOB или CLOB, созданный cubrid_lob2_new либо полученный из результата с флагом CUBRID_LOB. Это не путь к файлу, не текст содержимого и не обычный PHP-поток.

Результат: Текущая позиция int, false при отказе. Официальная сводная сигнатура int опускает false, перечисленный в разделе результатов. Значение 0 — корректное начало.

При отказе сразу сохраните cubrid_error_code(), cubrid_error_code_facility() и cubrid_error_msg(), пока следующий вызов не изменил диагностику. Полный проект использует c171Capture для перехвата предупреждения и сохранения признаков отказа. Проверяйте строгий тип результата: нулевой размер и пустое содержимое не обязательно означают ошибку.

Доступно с PECL CUBRID 8.4.1. Примеры этой главы используют native cubrid 11.2.0.0002, CCI 11.2.0 и сервер 11.3.5.1274. В коротком фрагменте $lob уже получен из выбранной записи; полный проект показывает создание данных и освобождение ресурсов. Вызов функции записан как имя(аргументы), результат присваивается переменной слева от =, а === проверяет значение вместе с типом.

Ресурс связан с соединением и временем жизни запроса. В закреплённом драйвере LOB из fetch нужно прочитать до следующей строки, сохранить PHP-ссылку до завершения запроса и затем освободить её. Не вызывайте lob2_close для такого ресурса перед следующим fetch или close_request: запрос хранит внутренний указатель на него. Для самостоятельно созданного LOB правила явного закрытия разобраны отдельно. Право читать выбранную запись приложение проверяет до получения ресурса.

Позиция после чтения
  1. Начало BLOB → tell() = 0
  2. read(..., 7) читает HEADER|
  3. tell() = 7: следующий байт принадлежит заголовку

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

$marker = cubrid_lob2_read($lob, 7);
$position = cubrid_lob2_tell($lob);
if ($position === false) { throw new RuntimeException('Позиция недоступна'); }
echo $position; // 7 после успешного чтения HEADER| с начала

Так предпросмотр подтверждает не число букв, а фактически прочитанные байты. Это полезно при разборе формата с полями разной длины. Строка UTF-8 может занимать больше байтов, чем видимых символов.

Применение 2: проверить побочный эффект операции выгрузки

$before = cubrid_lob2_tell($lob);
if ($before === false) { throw new RuntimeException('Нет исходной позиции'); }
if (!cubrid_lob2_export($lob, __DIR__ . '/copy.bin')) {
    throw new RuntimeException('Экспорт не завершён');
}
$after = cubrid_lob2_tell($lob);

В проекте передачи вложения before и after равны 4. Позиция не равна общему размеру, а export сохраняет полный файл. Для размера используйте cubrid_lob2_size; tell не заменяет проверку длины полученного файла.

cubrid_lob2_tell64Получить позицию LOB десятичной строкой для закладки и сопоставления с большим размером.

PECL CUBRID >= 8.4.1 · cubrid

cubrid_lob2_tell64(resource $lob_identifier): string

Получить позицию LOB десятичной строкой для закладки и сопоставления с большим размером.

ПараметрЧто передать
lob_identifierДействующий ресурс BLOB или CLOB, созданный cubrid_lob2_new либо полученный из результата с флагом CUBRID_LOB. Это не путь к файлу, не текст содержимого и не обычный PHP-поток.

Результат: Десятичная строка текущей позиции, false при отказе. Начало представлено строкой "0", поэтому проверка только истинности неверна.

При отказе сразу сохраните cubrid_error_code(), cubrid_error_code_facility() и cubrid_error_msg(), пока следующий вызов не изменил диагностику. Полный проект использует c171Capture для перехвата предупреждения и сохранения признаков отказа. Проверяйте строгий тип результата: нулевой размер и пустое содержимое не обязательно означают ошибку.

Доступно с PECL CUBRID 8.4.1. Примеры этой главы используют native cubrid 11.2.0.0002, CCI 11.2.0 и сервер 11.3.5.1274. В коротком фрагменте $lob уже получен из выбранной записи; полный проект показывает создание данных и освобождение ресурсов. Вызов функции записан как имя(аргументы), результат присваивается переменной слева от =, а === проверяет значение вместе с типом.

Ресурс связан с соединением и временем жизни запроса. В закреплённом драйвере LOB из fetch нужно прочитать до следующей строки, сохранить PHP-ссылку до завершения запроса и затем освободить её. Не вызывайте lob2_close для такого ресурса перед следующим fetch или close_request: запрос хранит внутренний указатель на него. Для самостоятельно созданного LOB правила явного закрытия разобраны отдельно. Право читать выбранную запись приложение проверяет до получения ресурса.

Это вариант tell для позиций, которые могут выйти за диапазон PHP int. Строку можно сохранить без потери цифр и передать в seek64. Она не означает количество символов текста или число SQL-строк; в проверенном профиле BLOB и UTF-8 CLOB перемещаются в байтах.

Применение 1: закончить порционную копию с понятной позицией

$position = cubrid_lob2_tell64($lob);
if ($position === false) { throw new RuntimeException('Позиция недоступна'); }
// $copied — число байтов, которое сам цикл успешно записал.
if ($position !== (string) $copied) {
    throw new RuntimeException('Чтение и запись имеют разную длину');
}

Сопоставление корректно, когда копирование журнала началось с позиции 0. При чтении с середины сравнивают с начальным смещением плюс обработанные байты; контрольная сумма отдельно проверяет содержимое. Учебный счётчик помещается в int — это не готовый счётчик произвольного размера.

Применение 2: сохранить и восстановить место короткого предпросмотра

$bookmark = cubrid_lob2_tell64($lob);
if ($bookmark === false) { throw new RuntimeException('Закладка недоступна'); }
if (!cubrid_lob2_seek64($lob, '8', CUBRID_CURSOR_LAST)) {
    throw new RuntimeException('Хвост недоступен');
}
$tail = cubrid_lob2_read($lob, 8);
if (!cubrid_lob2_seek64($lob, $bookmark, CUBRID_CURSOR_FIRST)) {
    throw new RuntimeException('Исходное место не восстановлено');
}

Так можно временно осмотреть хвост, сохранив текущую границу раздела. Полный предпросмотр показывает обе составляющие: строковую закладку BODY| и окончание TAIL|OK. Экспорт целого файла дополнительно сравнивает int- и строковый интерфейсы для позиции 4.

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

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

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

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

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