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

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

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

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

Глава 76 · После запросов и основ BLOB Firebird

Firebird: fbird_* для редакций заметок, файлов и безопасной отмены замены

Уточнение инструкции курьеру, бинарный журнал устройства, архив CSV и отклонение повреждённой замены: четыре последовательных сценария fbird BLOB.

Как обновлять содержимое, сохраняя понятный результат

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

Байты и открытый ресурс записи
  1. Исходный текст/файл → байты
  2. create → writer; add → следующие порции
  3. Проверка нового содержимого → одна из двух ветвей ниже
Исход проверкиЧто происходит дальше
Успехclose → строковый BLOB ID → INSERT/UPDATE с ID → commit → свежий SELECT колонки → open/get → исходное содержимое
Отказ до closecancel незавершённого writer → rollback операции → прежний подтверждённый файл остаётся без изменений

BLOB — содержимое в базе, размер которого может меняться. Writer можно представить открытой коробкой, куда последовательно кладут порции; close выдаёт служебную ссылку, а SQL связывает эту ссылку с заказом. Это модель вызовов, не описание физических страниц базы. Служебный ID не равен файлу и не заменяет его имя, номер документа или текст. После сохранения прочитывайте ID из строки: временная ссылка создания может измениться.

Функции fbird_blob_* — альтернативные имена соответствующих ibase_blob_* в выбранном драйвере. Прежде чем менять код, познакомьтесь с запросами fbird_*, базовой записью BLOB и commit/rollback. Текст и бинарные данные различаются: TEXT UTF8 подходит для заметки, BINARY — для точного архива файла. Base64 не нужен только ради записи бинарных байтов в BLOB.

Новая потребностьРабочий сценарий
Редакция инструкции курьеру: из NULL к двум последовательным текстамПерейти к примеру
Журнал устройства: сохранить 10000 бинарных записей без общего буфераПерейти к примеру
Архив исходного CSV: проверить заголовок и сохранить файл целикомПерейти к примеру
Неудачная замена вложения: сохранить прежний подтверждённый файлПерейти к примеру

Файлы примеров и размер контрольных данных

Каждый ZIP содержит example.php, lab.php и README.txt; в сценарии CSV добавлен настоящий incoming.csv. Программа создаёт одну свою таблицу в отдельной локальной базе, проверяет новые данные чтением после commit и удаляет таблицу. Существующая база и посторонние таблицы не заменяются. Запуск — CLI по README, а не PHP-код внутри страницы WordPress. Окружение: PHP 8.2.35, отдельный interbase 3.0.1 и Firebird 5.0.4.

Обёртки blobWriter162/blobAppend162/blobFinish162/blobCancel162 проверяют реальные fbird_blob_create/add/close/cancel. blobInspect162 читает сохранённый BLOB порциями, считает длину и SHA-256; целый текст собирается только для малых учебных заметок/CSV с лимитом 4096 байт. f162* обслуживают соединение, параметризованный SQL, очистку и отчёт. Небольшой фрагмент перед полной программой показывает один приём; полный файл содержит обработку ошибок и ресурсы.

Редакция инструкции курьеру: из NULL к двум последовательным текстам

Курьерская инструкция сначала не заполнена, затем оператор записывает время и просьбу позвонить, а позже уточняет вход со двора. Сохраним две последовательные редакции одной строки заказа. На странице результата будет обе прочитанные инструкции и номер текущей редакции, а не служебный BLOB ID.

Путь конкретных данных
  1. Заказ 162: revision=0, note=NULL
  2. create → два add → close возвращает ID
  3. UPDATE по order_id=162 и revision=0 → commit → редакция 1
  4. Новый create: прежний текст + «Вход со двора.»
  5. UPDATE по ожидаемой revision=1 → commit → редакция 2
  6. Свежий SELECT ID → чтение → полный текст без потери первой части

Сначала один шаг

$writer = fbird_blob_create($db);
fbird_blob_add($writer, "Доставка к 10:00.");
fbird_blob_add($writer, "\nПозвонить заранее.");
$id = fbird_blob_close($writer);
// $id передают параметром UPDATE, затем подтверждают транзакцию.

create открывает новый writer — ресурс записи. add получает его и очередную строку байтов; второй вызов добавляет к первому. close заканчивает запись и отдаёт ID, а UPDATE связывает новый BLOB со строкой заказа. Фрагмент показывает порядок на готовом соединении; полный файл проверяет ошибки каждого шага. В SQL стоит BLOB SUB_TYPE TEXT CHARACTER SET UTF8, соединение тоже UTF8. SQL NULL — отсутствие заметки, поэтому первый текст создаётся явно, без попытки «прибавить строку к NULL».

Результат запуска

НаблюдениеРезультат
До первой записиSQL NULL
Первая редакцияДоставка к 10:00. Позвонить заранее.
Текущая редакция2
Инструкция после обновленияДоставка к 10:00. Позвонить заранее. Вход со двора.
Байт UTF-8 во второй редакции87
Первая часть не потерянада
SHA-256 соответствует полному новому текстуда
Редакция инструкции курьеру: из NULL к двум последовательным текстамСамостоятельный пример
<?php
declare(strict_types=1);
require __DIR__.'/lab.php';
$db=null;$owned=$removed=$rows=$checks=$observations=[];
try {
    $config=f162Config();$db=f162Connect($config);$runtime=f162Runtime($db,$config);

    function blobWriter162($context) {
        $call=f162Capture(static fn()=>fbird_blob_create($context));
        if (!is_resource($call['value']) || $call['warning']) {f162Failure('Новый BLOB не открыт',$call);}
        return $call['value'];
    }
    function blobAppend162($writer,string $bytes): void {
        $call=f162Capture(static fn()=>fbird_blob_add($writer,$bytes));
        // Successful add returns null, not true; native refusal is false/diagnostic.
        if ($call['value']===false || $call['warning'] || $call['code']!==0) {f162Failure('Порция не записана',$call);}
    }
    function blobFinish162(&$writer): string {
        $call=f162Capture(static fn()=>fbird_blob_close($writer));
        if (!is_string($call['value']) || $call['warning'] || $call['code']!==0) {f162Failure('BLOB не закрыт',$call);}
        $writer=null;
        return $call['value'];
    }
    function blobCancel162(&$writer): bool {
        $call=f162Capture(static fn()=>fbird_blob_cancel($writer));
        if ($call['value']!==true || $call['warning'] || $call['code']!==0) {f162Failure('Черновик BLOB не отменён',$call);}
        $writer=null;
        return true;
    }
    function blobInspect162($context,string $savedId,bool $smallText=false): array {
        // Always use the ID selected from the saved row, not the transient creation ID.
        $call=f162Capture(static fn()=>fbird_blob_open($context,$savedId));
        if (!is_resource($call['value']) || $call['warning']) {f162Failure('Сохранённый BLOB не открыт',$call);}
        $reader=$call['value'];unset($call);
        $bytes=0;$value='';$hash=hash_init('sha256');
        try {
            while (true) {
                $call=f162Capture(static fn()=>fbird_blob_get($reader,4096));
                if (!is_string($call['value']) || $call['warning'] || $call['code']!==0) {f162Failure('BLOB не прочитан',$call);}
                $chunk=$call['value'];
                if ($chunk==='') {break;}
                $bytes+=strlen($chunk);hash_update($hash,$chunk);
                if ($smallText) {
                    if ($bytes>4096) {throw new DomainException('Текст учебного предпросмотра превышает 4096 байт.');}
                    $value.=$chunk;
                }
            }
        } finally {
            $call=f162Capture(static fn()=>fbird_blob_close($reader));$reader=null;
            if ($call['value']===false || $call['warning']) {f162Failure('Читающий BLOB не закрыт',$call);}
        }
        return ['bytes'=>$bytes,'sha256'=>hash_final($hash),'text'=>$value];
    }

    $name=f162OwnTable($db,$owned,'notes','(order_id INTEGER NOT NULL PRIMARY KEY,revision_no INTEGER NOT NULL,note_text BLOB SUB_TYPE TEXT CHARACTER SET UTF8)');
    $q=f162Qualified($name);
    f162Command($db,"INSERT INTO $q(order_id,revision_no,note_text) VALUES(?,?,?)",[162,0,null]);
    f162Commit($db);
    $initial=f162Read($db,"SELECT revision_no AS \"revision\",note_text AS \"blob_id\" FROM $q WHERE order_id=?",[162])[0];
    $firstParts=['Доставка к 10:00.',"\nПозвонить заранее."];
    $writer=blobWriter162($db);
    try {
        foreach ($firstParts as $part) {blobAppend162($writer,$part);}
        $createdId=blobFinish162($writer);
        $changed=f162Query($db,"UPDATE $q SET note_text=?,revision_no=1 WHERE order_id=? AND revision_no=?",[$createdId,162,0]);
        if ($changed!==1) {throw new DomainException('Редакция заказа уже изменилась.');}
        f162Commit($db);
    } finally {if (is_resource($writer)) {blobCancel162($writer);}}
    $row1=f162Read($db,"SELECT revision_no AS \"revision\",note_text AS \"blob_id\" FROM $q WHERE order_id=?",[162])[0];
    $first=blobInspect162($db,$row1['blob_id'],true);
    f162Commit($db);
    // A closed BLOB is not an appendable file. Build a new complete value.
    $writer=blobWriter162($db);
    try {
        blobAppend162($writer,$first['text']);
        blobAppend162($writer,"\nВход со двора.");
        $replacementId=blobFinish162($writer);
        $changed=f162Query($db,"UPDATE $q SET note_text=?,revision_no=2 WHERE order_id=? AND revision_no=?",[$replacementId,162,1]);
        if ($changed!==1) {throw new DomainException('Редакция заказа уже изменилась.');}
        f162Commit($db);
    } finally {if (is_resource($writer)) {blobCancel162($writer);}}
    $row2=f162Read($db,"SELECT revision_no AS \"revision\",note_text AS \"blob_id\" FROM $q WHERE order_id=?",[162])[0];
    $second=blobInspect162($db,$row2['blob_id'],true);
    $firstText=implode('',$firstParts);$secondText=$firstText."\nВход со двора.";
    $rows=[['До первой записи',($initial['blob_id']===null?'SQL NULL':'другое')],['Первая редакция',$first['text']],
        ['Текущая редакция',$row2['revision']],['Инструкция после обновления',$second['text']],
        ['Байт UTF-8 во второй редакции',$second['bytes']],['Первая часть не потеряна',str_starts_with($second['text'],$first['text'])],
        ['SHA-256 соответствует полному новому тексту',$second['sha256']===hash('sha256',$secondText)]];
    $checks=['starts_with_null'=>$initial===['revision'=>0,'blob_id'=>null],
        'first_revision_read'=>$row1['revision']===1 && $first['text']===$firstText,
        'second_revision_read'=>$row2['revision']===2 && $second['text']===$secondText,
        'new_text_bytes'=>$second['bytes']===strlen($secondText),'new_text_hash'=>$second['sha256']===hash('sha256',$secondText),
        'writers_closed'=>$writer===null];
    $observations=['order_id'=>162,'initial_null'=>$initial['blob_id']===null,
        'first'=>['revision'=>1,'text'=>$first['text'],'bytes'=>$first['bytes']],
        'second'=>['revision'=>2,'text'=>$second['text'],'bytes'=>$second['bytes'],'sha256'=>$second['sha256']],
        'updates_checked_expected_revision'=>true,'retained_history_table'=>false];

} finally {
    if (function_exists('f162HttpChildrenStopped') && !f162HttpChildrenStopped()) {
        throw new RuntimeException('Дочерний HTTP-процесс ещё работает: учебная таблица сохранена для точного восстановления.');
    }
    if (is_resource($db)) {try {$removed=f162Cleanup($db,$owned);} finally {ibase_close($db);}}
}

f162Report('fb162-note-revisions','Редакция инструкции курьеру: из NULL к двум последовательным текстам',$rows,$checks,$observations,$runtime,$owned,$removed);
Ожидаемый результат
До первой записи — SQL NULL
Первая редакция — Доставка к 10:00.
Позвонить заранее.
Текущая редакция — 2
Инструкция после обновления — Доставка к 10:00.
Позвонить заранее.
Вход со двора.
Байт UTF-8 во второй редакции — 87
Первая часть не потеряна — да
SHA-256 соответствует полному новому тексту — да

Самостоятельный CLI-пример: одна своя таблица Firebird, настоящая запись BLOB и чтение после сохранения; result.html/result.json; закрытие ресурсов и удаление таблицы. lab.php, README.txt и нужный CSV приложены.

Фактический HTML-результат · JSON со значениями и проверками.

Ошибки, границы и применение

Закрытый writer нельзя снова передать add: для новой редакции создаётся новое полное содержимое. Условие revision_no в UPDATE защищает от молчаливого затирания более новой редакции, но многопользовательские конфликты в этом одиночном примере не моделируются. Снимок первой редакции показан из уже прочитанных PHP-данных; в таблице истории нет. Для короткой инструкции достаточно VARCHAR, BLOB выбран как механизм растущего текста. Для больших заметок не собирайте весь прежний текст в PHP: используйте потоковый перенос или подходящую SQL-операцию с известной семантикой NULL.

Вопрос о старой Firebird: добавление текста к BLOB через конкатенацию не меняло поле, которое содержало NULL. Исходная ситуация на Stack Overflow. Разделяем отсутствие значения и пустой текст; первая редакция создаётся явно, следующая — как новое полное содержимое. Не переносим спорные ограничения старых ответов на современную Firebird и не называем add редактором сохранённого BLOB.

Продолжите пример

Нужно хранить историю для диспетчера. Достаточно ли оставить номер revision_no в этой таблице?

Ожидаемое решение

Нет: UPDATE заменяет ссылку на актуальную заметку. Для истории нужна отдельная модель редакций, например строки с составным ключом order_id/revision_no и временем автора. Не используйте временный BLOB ID как бизнес-номер редакции.

Журнал устройства: сохранить 10000 бинарных записей без общего буфера

Мини-ПК прислал двоичный журнал измерений. Его записи содержат номер пакета, нулевой байт и FF: преобразовывать их в текст нельзя. Создадим воспроизводимый файл из 10000 разных записей, перенесём в BLOB порциями и сверим весь поток после commit. Отчёт покажет 120000 байт и один и тот же SHA-256 источника и прочитанного содержимого.

Путь конкретных данных
  1. 10000 записей × 12 байт → временный файл 120000 байт
  2. fread максимум 4096 → add(writer, chunk)
  3. 30 порций, последняя короче → close → ID
  4. INSERT имени/размера/ID → commit
  5. SELECT одного вложения → get порциями → длина и SHA-256

Сначала один шаг

$chunk = fread($input, 4096);
if ($chunk === false) { throw new RuntimeException("Файл не читается"); }
if ($chunk !== "") {
    fbird_blob_add($writer, $chunk);
}
// Повторять до EOF; затем close, INSERT и commit.

fread возвращает строку байтов, даже если это не текст. Число 4096 — верхняя граница одного чтения приложения, не предел размера BLOB и не число символов. Каждая add продолжает текущий writer. Полный файл не создаёт $allBytes: вход формируется небольшими записями, а обратное чтение сразу обновляет хеш. pack("N", $index) задаёт четырёхбайтовый номер записи; это подготовка понятного контрольного файла, а не требование к формату ваших вложений.

Результат запуска

НаблюдениеРезультат
Файлdevice-sequence.bin
Записей устройства10000
Байт передано120000
Вызовов add30
Байт прочитано после commit120000
SHA-256345e92d9a9b697752abbc64fa4e2ff9eb450e0e1516f1689dc6f115e68477ca0
Длина и содержимое совпалида
Большой текстовый буфер не создавалсяда
Журнал устройства: сохранить 10000 бинарных записей без общего буфераСамостоятельный пример
<?php
declare(strict_types=1);
require __DIR__.'/lab.php';
$db=null;$owned=$removed=$rows=$checks=$observations=[];
try {
    $config=f162Config();$db=f162Connect($config);$runtime=f162Runtime($db,$config);

    function blobWriter162($context) {
        $call=f162Capture(static fn()=>fbird_blob_create($context));
        if (!is_resource($call['value']) || $call['warning']) {f162Failure('Новый BLOB не открыт',$call);}
        return $call['value'];
    }
    function blobAppend162($writer,string $bytes): void {
        $call=f162Capture(static fn()=>fbird_blob_add($writer,$bytes));
        // Successful add returns null, not true; native refusal is false/diagnostic.
        if ($call['value']===false || $call['warning'] || $call['code']!==0) {f162Failure('Порция не записана',$call);}
    }
    function blobFinish162(&$writer): string {
        $call=f162Capture(static fn()=>fbird_blob_close($writer));
        if (!is_string($call['value']) || $call['warning'] || $call['code']!==0) {f162Failure('BLOB не закрыт',$call);}
        $writer=null;
        return $call['value'];
    }
    function blobCancel162(&$writer): bool {
        $call=f162Capture(static fn()=>fbird_blob_cancel($writer));
        if ($call['value']!==true || $call['warning'] || $call['code']!==0) {f162Failure('Черновик BLOB не отменён',$call);}
        $writer=null;
        return true;
    }
    function blobInspect162($context,string $savedId,bool $smallText=false): array {
        // Always use the ID selected from the saved row, not the transient creation ID.
        $call=f162Capture(static fn()=>fbird_blob_open($context,$savedId));
        if (!is_resource($call['value']) || $call['warning']) {f162Failure('Сохранённый BLOB не открыт',$call);}
        $reader=$call['value'];unset($call);
        $bytes=0;$value='';$hash=hash_init('sha256');
        try {
            while (true) {
                $call=f162Capture(static fn()=>fbird_blob_get($reader,4096));
                if (!is_string($call['value']) || $call['warning'] || $call['code']!==0) {f162Failure('BLOB не прочитан',$call);}
                $chunk=$call['value'];
                if ($chunk==='') {break;}
                $bytes+=strlen($chunk);hash_update($hash,$chunk);
                if ($smallText) {
                    if ($bytes>4096) {throw new DomainException('Текст учебного предпросмотра превышает 4096 байт.');}
                    $value.=$chunk;
                }
            }
        } finally {
            $call=f162Capture(static fn()=>fbird_blob_close($reader));$reader=null;
            if ($call['value']===false || $call['warning']) {f162Failure('Читающий BLOB не закрыт',$call);}
        }
        return ['bytes'=>$bytes,'sha256'=>hash_final($hash),'text'=>$value];
    }

    $name=f162OwnTable($db,$owned,'packets','(attachment_id INTEGER NOT NULL PRIMARY KEY,file_name VARCHAR(80) NOT NULL,byte_size INTEGER NOT NULL,payload BLOB SUB_TYPE BINARY)');
    $q=f162Qualified($name);
    $input=tmpfile();
    if ($input===false) {throw new RuntimeException('Не создан учебный входной файл.');}
    $writer=null;$expectedHash=hash_init('sha256');$expectedBytes=0;$sentBytes=0;$calls=0;
    try {
        // Each synthetic record: four-byte unsigned sequence number + eight payload bytes.
        for ($index=0;$index<10000;$index++) {
            $record=pack('N',$index)."FB162\0\xFF\n";
            if (fwrite($input,$record)!==strlen($record)) {throw new RuntimeException('Входной файл записан не полностью.');}
            hash_update($expectedHash,$record);$expectedBytes+=strlen($record);
        }
        $expectedSha=hash_final($expectedHash);
        if (rewind($input)!==true) {throw new RuntimeException('Файл не перемотан.');}
        $writer=blobWriter162($db);
        while (!feof($input)) {
            $chunk=fread($input,4096);
            if ($chunk===false) {throw new RuntimeException('Не удалось прочитать порцию файла.');}
            if ($chunk==='') {
                if (!feof($input)) {throw new RuntimeException('Чтение остановилось до EOF.');}
                break;
            }
            blobAppend162($writer,$chunk);$sentBytes+=strlen($chunk);$calls++;
        }
        if ($sentBytes!==$expectedBytes) {throw new RuntimeException('Входной файл передан не полностью.');}
        $id=blobFinish162($writer);
        f162Command($db,"INSERT INTO $q(attachment_id,file_name,byte_size,payload) VALUES(?,?,?,?)",[162,'device-sequence.bin',$sentBytes,$id]);
        f162Commit($db);
    } finally {
        try {if (is_resource($writer)) {blobCancel162($writer);}}
        finally {fclose($input);$input=null;}
    }
    $row=f162Read($db,"SELECT file_name AS \"name\",byte_size AS \"declared_bytes\",payload AS \"blob_id\" FROM $q WHERE attachment_id=?",[162])[0];
    $saved=blobInspect162($db,$row['blob_id']);
    $rows=[['Файл',$row['name']],['Записей устройства',10000],['Байт передано',$sentBytes],['Вызовов add',$calls],
        ['Байт прочитано после commit',$saved['bytes']],['SHA-256',$saved['sha256']],
        ['Длина и содержимое совпали',$saved['bytes']===$expectedBytes && $saved['sha256']===$expectedSha],
        ['Большой текстовый буфер не создавался',$saved['text']==='']];
    $checks=['source_bytes'=>$expectedBytes===120000,'chunked_writes'=>$calls===30 && $sentBytes===120000,
        'stored_metadata'=>$row['declared_bytes']===120000,'roundtrip_length'=>$saved['bytes']===$sentBytes,
        'roundtrip_hash'=>$saved['sha256']===$expectedSha,'bounded_readback'=>$saved['text']==='',
        'file_and_blob_closed'=>$input===null && $writer===null];
    $observations=['source_records'=>10000,'record_bytes'=>12,'payload_hex'=>'464231363200ff0a','sequence_encoding'=>'unsigned32-big-endian',
        'chunk_bytes'=>4096,'write_calls'=>$calls,'expected_bytes'=>$expectedBytes,'actual_bytes'=>$saved['bytes'],
        'source_sha256'=>$expectedSha,'saved_sha256'=>$saved['sha256'],'readback_kept_whole_payload'=>false];

} finally {
    if (function_exists('f162HttpChildrenStopped') && !f162HttpChildrenStopped()) {
        throw new RuntimeException('Дочерний HTTP-процесс ещё работает: учебная таблица сохранена для точного восстановления.');
    }
    if (is_resource($db)) {try {$removed=f162Cleanup($db,$owned);} finally {ibase_close($db);}}
}

f162Report('fb162-chunked-file','Журнал устройства: сохранить 10000 бинарных записей без общего буфера',$rows,$checks,$observations,$runtime,$owned,$removed);
Ожидаемый результат
Файл — device-sequence.bin
Записей устройства — 10000
Байт передано — 120000
Вызовов add — 30
Байт прочитано после commit — 120000
SHA-256 — 345e92d9a9b697752abbc64fa4e2ff9eb450e0e1516f1689dc6f115e68477ca0
Длина и содержимое совпали — да
Большой текстовый буфер не создавался — да

Самостоятельный CLI-пример: одна своя таблица Firebird, настоящая запись BLOB и чтение после сохранения; result.html/result.json; закрытие ресурсов и удаление таблицы. lab.php, README.txt и нужный CSV приложены.

Фактический HTML-результат · JSON со значениями и проверками.

Ошибки, границы и применение

Для реального устройства входной tmpfile заменяется проверенным локальным потоком. Размер порции не равен размеру записи устройства: последовательные байты можно передавать через границы записей, если порядок сохранён. Счётчик add — число вызовов нашего кода, не обещанное число физических сегментов на диске. В 3.0.1 большие аргументы add дополнительно делятся внутри драйвера; это не повод сначала читать гигабайт в одну PHP-строку. Хеш обнаруживает несовпадение с ожидаемыми данными, но не удостоверяет отправителя. Выбор базы или файлового хранилища зависит от резервирования, доступа и нагрузки, а не только от возможности записать BLOB.

В реальной задаче с Firebird и .NET попытка предварительно прочитать большой файл целиком заканчивалась нехваткой памяти. Исходная ситуация на Stack Overflow. Проблема организации потока применима и к PHP; API .NET сюда не переносим. Наш PHP-код читает файл порциями, считает байты и хеш, а обратное чтение не собирает полный бинарный буфер. Не выдаём маленький контрольный файл за нагрузочное испытание гигабайтов.

Продолжите пример

Измените порцию на 8192. Какие результаты должны остаться прежними?

Ожидаемое решение

Размер 120000 и SHA-256 останутся прежними, число вызовов станет 15. Порции — способ передачи; они не должны менять последовательность байтов. Проверка количества порций нужна только для этого заранее известного учебного файла.

Архив исходного CSV: проверить заголовок и сохранить файл целиком

Импорт остатков нужно уметь повторить и объяснить поставщику. Сохраним исходный CSV без изменений, включая заголовок и название «Кабель; USB» с разделителем внутри кавычек. Сначала проверим заголовок, перемотаем файл, импортируем все байты, а затем разберём сохранённый CSV: три товара, общий остаток 15, нулевое количество не потеряно.

Путь конкретных данных
  1. incoming.csv → fgetcsv заголовка id/title/qty
  2. rewind → снова первый байт, а не данные после заголовка
  3. import(db, input) → готовый ID; PHP-файл ещё открыт
  4. INSERT архива → commit → свежий SELECT ID
  5. Чтение BLOB → полный CSV → fgetcsv → 3 строки, сумма 15

Сначала один шаг

$input = fopen(__DIR__."/incoming.csv", "rb");
if ($input === false) { throw new RuntimeException("Файл не открыт"); }
try {
    $header = fgetcsv($input, 0, ";", '"', "");
    if ($header !== ["id","title","qty"]) { throw new DomainException("Не тот формат"); }
    rewind($input);
    $id = fbird_blob_import($db, $input);
} finally { fclose($input); }
// Проверить ID, связать с записью архива и сделать commit.

import принимает ресурс открытого файла, а не его путь и не массив CSV. Она создаёт BLOB, переносит байты с текущей позиции и закрывает BLOB; файл PHP остаётся вашему коду. Поэтому после проверки первой строки нужен rewind. BINARY выбран для архива: он сохраняет точные байты входа; мы заранее знаем кодировку нашего fixture UTF-8. fgetcsv понимает кавычки, в отличие от explode(";", $line). Архив файла и запись товаров в рабочую таблицу — разные операции: здесь товары только прочитаны для понятного результата.

Результат запуска

НаблюдениеРезультат
Файл в архивеincoming.csv
Заголовок сохранёнid;title;qty
Товарных строк3
Название с разделителемКабель; USB
Нулевой остаток0
Сумма количеств15
Байт полного файла76
Исходные байты совпалида
Файл закрыл вызывающий кодда
Архив исходного CSV: проверить заголовок и сохранить файл целикомСамостоятельный пример
<?php
declare(strict_types=1);
require __DIR__.'/lab.php';
$db=null;$owned=$removed=$rows=$checks=$observations=[];
try {
    $config=f162Config();$db=f162Connect($config);$runtime=f162Runtime($db,$config);

    function blobWriter162($context) {
        $call=f162Capture(static fn()=>fbird_blob_create($context));
        if (!is_resource($call['value']) || $call['warning']) {f162Failure('Новый BLOB не открыт',$call);}
        return $call['value'];
    }
    function blobAppend162($writer,string $bytes): void {
        $call=f162Capture(static fn()=>fbird_blob_add($writer,$bytes));
        // Successful add returns null, not true; native refusal is false/diagnostic.
        if ($call['value']===false || $call['warning'] || $call['code']!==0) {f162Failure('Порция не записана',$call);}
    }
    function blobFinish162(&$writer): string {
        $call=f162Capture(static fn()=>fbird_blob_close($writer));
        if (!is_string($call['value']) || $call['warning'] || $call['code']!==0) {f162Failure('BLOB не закрыт',$call);}
        $writer=null;
        return $call['value'];
    }
    function blobCancel162(&$writer): bool {
        $call=f162Capture(static fn()=>fbird_blob_cancel($writer));
        if ($call['value']!==true || $call['warning'] || $call['code']!==0) {f162Failure('Черновик BLOB не отменён',$call);}
        $writer=null;
        return true;
    }
    function blobInspect162($context,string $savedId,bool $smallText=false): array {
        // Always use the ID selected from the saved row, not the transient creation ID.
        $call=f162Capture(static fn()=>fbird_blob_open($context,$savedId));
        if (!is_resource($call['value']) || $call['warning']) {f162Failure('Сохранённый BLOB не открыт',$call);}
        $reader=$call['value'];unset($call);
        $bytes=0;$value='';$hash=hash_init('sha256');
        try {
            while (true) {
                $call=f162Capture(static fn()=>fbird_blob_get($reader,4096));
                if (!is_string($call['value']) || $call['warning'] || $call['code']!==0) {f162Failure('BLOB не прочитан',$call);}
                $chunk=$call['value'];
                if ($chunk==='') {break;}
                $bytes+=strlen($chunk);hash_update($hash,$chunk);
                if ($smallText) {
                    if ($bytes>4096) {throw new DomainException('Текст учебного предпросмотра превышает 4096 байт.');}
                    $value.=$chunk;
                }
            }
        } finally {
            $call=f162Capture(static fn()=>fbird_blob_close($reader));$reader=null;
            if ($call['value']===false || $call['warning']) {f162Failure('Читающий BLOB не закрыт',$call);}
        }
        return ['bytes'=>$bytes,'sha256'=>hash_final($hash),'text'=>$value];
    }

    $name=f162OwnTable($db,$owned,'csv','(import_id INTEGER NOT NULL PRIMARY KEY,file_name VARCHAR(80) NOT NULL,payload BLOB SUB_TYPE BINARY)');
    $q=f162Qualified($name);
    $path=__DIR__.'/incoming.csv';$input=fopen($path,'rb');
    if ($input===false) {throw new RuntimeException('incoming.csv не открыт.');}
    try {
        $header=fgetcsv($input,0,';','"','');
        if ($header!==['id','title','qty']) {throw new DomainException('Неизвестные колонки CSV.');}
        $afterHeader=ftell($input);
        if (rewind($input)!==true) {throw new RuntimeException('Нельзя вернуться к началу CSV.');}
        $call=f162Capture(static fn()=>fbird_blob_import($db,$input));
        if (!is_string($call['value']) || $call['warning'] || $call['code']!==0) {f162Failure('CSV не импортирован',$call);}
        $id=$call['value'];$endPosition=ftell($input);$callerStillOwnsFile=is_resource($input);
    } finally {fclose($input);$input=null;}
    f162Command($db,"INSERT INTO $q(import_id,file_name,payload) VALUES(?,?,?)",[162,'incoming.csv',$id]);
    f162Commit($db);
    $row=f162Read($db,"SELECT file_name AS \"name\",payload AS \"blob_id\" FROM $q WHERE import_id=?",[162])[0];
    $saved=blobInspect162($db,$row['blob_id'],true);
    $source=file_get_contents($path);
    if ($source===false) {throw new RuntimeException('Контрольный CSV недоступен.');}
    // BLOB archive storage is distinct from parsing/accepting product rows.
    $csvStream=tmpfile();
    if ($csvStream===false) {throw new RuntimeException('Не создан поток для разбора CSV.');}
    try {
        if (fwrite($csvStream,$saved['text'])!==strlen($saved['text']) || rewind($csvStream)!==true) {throw new RuntimeException('CSV не подготовлен к разбору.');}
        $savedHeader=fgetcsv($csvStream,0,';','"','');$products=[];$total=0;
        while (($fields=fgetcsv($csvStream,0,';','"',''))!==false) {
            if (count($fields)!==3 || !ctype_digit($fields[0]) || !ctype_digit($fields[2])) {throw new DomainException('Некорректная строка CSV.');}
            $products[]=['id'=>(int)$fields[0],'title'=>$fields[1],'qty'=>(int)$fields[2]];$total+=(int)$fields[2];
        }
    } finally {fclose($csvStream);$csvStream=null;}
    $rows=[['Файл в архиве',$row['name']],['Заголовок сохранён',implode(';',$savedHeader)],['Товарных строк',count($products)],
        ['Название с разделителем',$products[0]['title']],['Нулевой остаток',$products[1]['qty']],['Сумма количеств',$total],
        ['Байт полного файла',$saved['bytes']],['Исходные байты совпали',$saved['text']===$source],['Файл закрыл вызывающий код',$callerStillOwnsFile && $input===null]];
    $checks=['header_checked_before_import'=>$header===['id','title','qty'] && $afterHeader>0,
        'full_import_after_rewind'=>$savedHeader===$header && $endPosition===strlen($source),
        'raw_file_roundtrip'=>$saved['text']===$source && $saved['sha256']===hash('sha256',$source),
        'parsed_rows'=>$products===[['id'=>701,'title'=>'Кабель; USB','qty'=>3],['id'=>702,'title'=>'Клемма','qty'=>0],['id'=>703,'title'=>'Лента','qty'=>12]],
        'quantity_total'=>$total===15,'streams_closed'=>$input===null && $csvStream===null];
    $observations=['file_name'=>'incoming.csv','header'=>$savedHeader,'products'=>$products,'quantity_total'=>$total,
        'source_bytes'=>strlen($source),'saved_bytes'=>$saved['bytes'],'sha256'=>$saved['sha256'],
        'position_after_header'=>$afterHeader,'position_after_import'=>$endPosition,'raw_archive_is_not_product_insert'=>true];

} finally {
    if (function_exists('f162HttpChildrenStopped') && !f162HttpChildrenStopped()) {
        throw new RuntimeException('Дочерний HTTP-процесс ещё работает: учебная таблица сохранена для точного восстановления.');
    }
    if (is_resource($db)) {try {$removed=f162Cleanup($db,$owned);} finally {ibase_close($db);}}
}

f162Report('fb162-import-csv','Архив исходного CSV: проверить заголовок и сохранить файл целиком',$rows,$checks,$observations,$runtime,$owned,$removed);
Ожидаемый результат
Файл в архиве — incoming.csv
Заголовок сохранён — id;title;qty
Товарных строк — 3
Название с разделителем — Кабель; USB
Нулевой остаток — 0
Сумма количеств — 15
Байт полного файла — 76
Исходные байты совпали — да
Файл закрыл вызывающий код — да

Самостоятельный CLI-пример: одна своя таблица Firebird, настоящая запись BLOB и чтение после сохранения; result.html/result.json; закрытие ресурсов и удаление таблицы. lab.php, README.txt и нужный CSV приложены.

Фактический HTML-результат · JSON со значениями и проверками.

Ошибки, границы и применение

Проверка заголовка ещё не подтверждает корректность каждой записи. Дальнейший импорт должен проверить допустимые ID, длины, диапазоны, дубликаты и политику принятия пакета. Маленький CSV читается обратно целиком с лимитом 4096 байт; большой архив проверяйте потоково. Для не перематываемого источника нельзя молча продолжить import после уже прочитанного заголовка: сохраните первые байты или организуйте другой поток. Ошибка ввода/вывода не должна приниматься за полный файл, поэтому дополнительно сравниваются размер и хеш.

В PHP-примере BLOB создавался и закрывался, но в SQL передавали исходное содержимое вместо полученного BLOB ID. Исходная ситуация на Stack Overflow. У потока, writer, ID и строки таблицы разные переменные и роли. Передаём ID параметром INSERT, делаем commit и проверяем байты через ID из нового SELECT; Base64 для хранения исходного бинарного файла не нужен.

Продолжите пример

Уберите rewind после проверки заголовка. Какой конкретный признак ошибки появится?

Ожидаемое решение

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

Неудачная замена вложения: сохранить прежний подтверждённый файл

У заказа уже есть подтверждённый файл approved.txt. Во время замены пришло повреждённое содержимое: его SHA-256 отличается от ожидаемого. К этому моменту новый writer уже получил две порции. Отменим незавершённый BLOB до close и UPDATE, завершим неудачную транзакцию и покажем, что прежний файл и редакция 1 остались доступны.

Путь конкретных данных
  1. approved.txt → сохранён и подтверждён, revision=1
  2. Новый create → add двух порций: 23 байта
  3. SHA-256 не совпал → DomainException приложения
  4. finally: cancel открытого writer → rollback неудачной операции
  5. Свежий SELECT прежней строки → approved.txt и APPROVED:162

Сначала один шаг

try {
    // Добавить порции и сравнить полный хеш.
    if (!hash_equals($expectedSha, $actualSha)) {
        throw new DomainException("Файл повреждён");
    }
    // Только после проверки close → UPDATE → commit.
} finally {
    if (is_resource($writer)) {
        fbird_blob_cancel($writer);
        $writer = null;
    }
}

cancel получает открытый writer нового содержимого. Мы не передаём ему ID прежнего файла. До успешной проверки не выполняется UPDATE, поэтому строка продолжает указывать на approved.txt. Полный код различает ожидаемый отказ проверки и ошибки базы, проверяет cancel и отдельно откатывает транзакцию. DomainException здесь выбрасывает приложение после сравнения хеша: это не выдуманный ответ Firebird.

Результат запуска

НаблюдениеРезультат
Причина отказаSHA-256 нового файла не совпал с ожидаемым
Получено байт нового файла23
Незавершённый BLOB отменёнда
UPDATE замены выполненнет
Оставшееся вложениеapproved.txt
Редакция вложения1
Прежнее содержимоеAPPROVED:162
Прежний файл не повреждёнда
Неудачная замена вложения: сохранить прежний подтверждённый файлСамостоятельный пример
<?php
declare(strict_types=1);
require __DIR__.'/lab.php';
$db=null;$owned=$removed=$rows=$checks=$observations=[];
try {
    $config=f162Config();$db=f162Connect($config);$runtime=f162Runtime($db,$config);

    function blobWriter162($context) {
        $call=f162Capture(static fn()=>fbird_blob_create($context));
        if (!is_resource($call['value']) || $call['warning']) {f162Failure('Новый BLOB не открыт',$call);}
        return $call['value'];
    }
    function blobAppend162($writer,string $bytes): void {
        $call=f162Capture(static fn()=>fbird_blob_add($writer,$bytes));
        // Successful add returns null, not true; native refusal is false/diagnostic.
        if ($call['value']===false || $call['warning'] || $call['code']!==0) {f162Failure('Порция не записана',$call);}
    }
    function blobFinish162(&$writer): string {
        $call=f162Capture(static fn()=>fbird_blob_close($writer));
        if (!is_string($call['value']) || $call['warning'] || $call['code']!==0) {f162Failure('BLOB не закрыт',$call);}
        $writer=null;
        return $call['value'];
    }
    function blobCancel162(&$writer): bool {
        $call=f162Capture(static fn()=>fbird_blob_cancel($writer));
        if ($call['value']!==true || $call['warning'] || $call['code']!==0) {f162Failure('Черновик BLOB не отменён',$call);}
        $writer=null;
        return true;
    }
    function blobInspect162($context,string $savedId,bool $smallText=false): array {
        // Always use the ID selected from the saved row, not the transient creation ID.
        $call=f162Capture(static fn()=>fbird_blob_open($context,$savedId));
        if (!is_resource($call['value']) || $call['warning']) {f162Failure('Сохранённый BLOB не открыт',$call);}
        $reader=$call['value'];unset($call);
        $bytes=0;$value='';$hash=hash_init('sha256');
        try {
            while (true) {
                $call=f162Capture(static fn()=>fbird_blob_get($reader,4096));
                if (!is_string($call['value']) || $call['warning'] || $call['code']!==0) {f162Failure('BLOB не прочитан',$call);}
                $chunk=$call['value'];
                if ($chunk==='') {break;}
                $bytes+=strlen($chunk);hash_update($hash,$chunk);
                if ($smallText) {
                    if ($bytes>4096) {throw new DomainException('Текст учебного предпросмотра превышает 4096 байт.');}
                    $value.=$chunk;
                }
            }
        } finally {
            $call=f162Capture(static fn()=>fbird_blob_close($reader));$reader=null;
            if ($call['value']===false || $call['warning']) {f162Failure('Читающий BLOB не закрыт',$call);}
        }
        return ['bytes'=>$bytes,'sha256'=>hash_final($hash),'text'=>$value];
    }

    $name=f162OwnTable($db,$owned,'replace','(attachment_id INTEGER NOT NULL PRIMARY KEY,file_name VARCHAR(80) NOT NULL,revision_no INTEGER NOT NULL,payload BLOB SUB_TYPE BINARY)');
    $q=f162Qualified($name);$approved="APPROVED:162\n";
    $writer=blobWriter162($db);
    try {
        blobAppend162($writer,$approved);$id=blobFinish162($writer);
        f162Command($db,"INSERT INTO $q(attachment_id,file_name,revision_no,payload) VALUES(?,?,?,?)",[162,'approved.txt',1,$id]);
        f162Commit($db);
    } finally {if (is_resource($writer)) {blobCancel162($writer);}}
    $expectedNew="NEW-DOCUMENT\nAPPROVED\n";
    $incomingParts=["NEW-DOCUMENT\n","CORRUPTED\n"];
    $writer=blobWriter162($db);$received=0;$hash=hash_init('sha256');$cancelled=false;$replacementApplied=false;$reason='';
    try {
        foreach ($incomingParts as $part) {
            blobAppend162($writer,$part);hash_update($hash,$part);$received+=strlen($part);
        }
        $receivedSha=hash_final($hash);$expectedSha=hash('sha256',$expectedNew);
        if (!hash_equals($expectedSha,$receivedSha)) {throw new DomainException('SHA-256 нового файла не совпал с ожидаемым');}
        $newId=blobFinish162($writer);
        $changed=f162Query($db,"UPDATE $q SET file_name=?,revision_no=2,payload=? WHERE attachment_id=? AND revision_no=?",['replacement.txt',$newId,162,1]);
        if ($changed!==1) {throw new DomainException('Вложение уже изменилось.');}
        f162Commit($db);$replacementApplied=true;
    } catch (DomainException $error) {
        $reason=$error->getMessage();
    } finally {
        if (is_resource($writer)) {$cancelled=blobCancel162($writer);}
        if (!$replacementApplied && ibase_rollback($db)!==true) {f162Failure('Транзакция замены не отменена');}
    }
    $row=f162Read($db,"SELECT file_name AS \"name\",revision_no AS \"revision\",payload AS \"blob_id\" FROM $q WHERE attachment_id=?",[162])[0];
    $saved=blobInspect162($db,$row['blob_id'],true);
    $rows=[['Причина отказа',$reason],['Получено байт нового файла',$received],['Незавершённый BLOB отменён',$cancelled],
        ['UPDATE замены выполнен',$replacementApplied],['Оставшееся вложение',$row['name']],['Редакция вложения',$row['revision']],
        ['Прежнее содержимое',$saved['text']],['Прежний файл не повреждён',$saved['text']===$approved]];
    $checks=['integrity_failure_detected'=>$receivedSha!==$expectedSha && $reason==='SHA-256 нового файла не совпал с ожидаемым',
        'new_bytes_written_before_reject'=>$received===23,'unfinished_writer_cancelled'=>$cancelled && $writer===null,
        'no_replacement_update'=>!$replacementApplied,'old_metadata_preserved'=>$row['name']==='approved.txt' && $row['revision']===1,
        'old_content_preserved'=>$saved['text']===$approved && $saved['sha256']===hash('sha256',$approved)];
    $observations=['failure'=>'checksum_mismatch','incoming_bytes'=>$received,'new_hash'=>$receivedSha,'expected_new_hash'=>$expectedSha,
        'cancelled'=>$cancelled,'replacement_applied'=>$replacementApplied,'remaining_name'=>$row['name'],'remaining_revision'=>$row['revision'],
        'remaining_text'=>$saved['text'],'remaining_sha256'=>$saved['sha256']];

} finally {
    if (function_exists('f162HttpChildrenStopped') && !f162HttpChildrenStopped()) {
        throw new RuntimeException('Дочерний HTTP-процесс ещё работает: учебная таблица сохранена для точного восстановления.');
    }
    if (is_resource($db)) {try {$removed=f162Cleanup($db,$owned);} finally {ibase_close($db);}}
}

f162Report('fb162-cancel-replacement','Неудачная замена вложения: сохранить прежний подтверждённый файл',$rows,$checks,$observations,$runtime,$owned,$removed);
Ожидаемый результат
Причина отказа — SHA-256 нового файла не совпал с ожидаемым
Получено байт нового файла — 23
Незавершённый BLOB отменён — да
UPDATE замены выполнен — нет
Оставшееся вложение — approved.txt
Редакция вложения — 1
Прежнее содержимое — APPROVED:162

Прежний файл не повреждён — да

Самостоятельный CLI-пример: одна своя таблица Firebird, настоящая запись BLOB и чтение после сохранения; result.html/result.json; закрытие ресурсов и удаление таблицы. lab.php, README.txt и нужный CSV приложены.

Фактический HTML-результат · JSON со значениями и проверками.

Ошибки, границы и применение

cancel не удаляет ранее сохранённые BLOB и не заменяет rollback SQL-изменений. После close ресурс уже завершён; отменить закрытый BLOB этой функцией нельзя. Старый файл подтверждён до попытки замены, поэтому rollback относится только к новой операции. Ожидаемый хеш должен приходить из известного вам источника; этот пример показывает целостность, а не цифровую подпись. Нельзя автоматически повторять UPDATE при конфликте редакций или скрывать native-ошибку как «файл повреждён». Для production добавьте права, допустимые форматы, лимиты и обработку незавершённых загрузок.

Продолжите пример

Если хеш совпал, но UPDATE по ожидаемой редакции затронул 0 строк, можно ли всё равно подтвердить операцию?

Ожидаемое решение

Нет. Другая редакция уже стала актуальной, и привязать новый файл к ней без решения пользователя нельзя. Откатите свою транзакцию и загрузите актуальное состояние. Здесь cancel нужен лишь пока writer открыт; после close оставшуюся операцию отменяет rollback.

Пять карточек записи и отмены

Нужно сделатьФункция
Открыть новый BLOB для последовательной записи.fbird_blob_create
Передать очередные байты в открытый writer.fbird_blob_add
Закончить запись/чтение BLOB и освободить ресурс.fbird_blob_close
Перенести открытый файл в новый BLOB и получить ID.fbird_blob_import
Отбросить незавершённое новое содержимое до close.fbird_blob_cancel

От сохранённого файла к его выдаче

Следующий шаг — метаданные, порционное чтение и вывод BLOB через fbird_*. Для обновления нескольких связанных записей используйте транзакции, для подготовки входного CSV — контракты параметров. Базовый маршрут ibase BLOB остаётся тем же API; смена префикса не делает файл безопаснее и не заменяет проверки приложения.

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

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

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

fbird_blob_createОткрыть новый BLOB для последовательной записи.

PHP 5 / PHP 7 < 7.4 исторически; отдельный interbase 3.0.1 · interbase

fbird_blob_create([resource $link_identifier]): resource|false

Чтобы принять новую заметку или вложение, сначала открывают запись нового BLOB. В этот момент ещё нет ни готового файла, ни связи со строкой заказа. ibase_blob_create — каноническое имя той же операции. В FirebirdSQL interbase 3.0.1 имя fbird_blob_create зарегистрировано как alias того же обработчика, поэтому данные и ресурсы совместимы; другое написание не меняет транзакцию или формат хранилища.

Параметр / результатКонтракт
Необязательный link_identifierОфициальная страница разрешает передать соединение; без аргумента используется последнее открытое. Реализация 3.0.1 через общий выбор контекста принимает также ресурс транзакции. Передавайте явный ресурс, если в программе несколько баз или транзакций.
Сигнатура с nullВ PHP Manual необязательный аргумент отображается как = null. C-парсер 3.0.1 ожидает необязательный ресурс: опустить аргумент и явно передать null — не одно и то же. В примерах всегда передан $db.
resource / falseУспех — writer, ресурс для последовательных add; отказ — false с диагностикой. Это не строковый BLOB ID, не ID заказа и не содержимое. Не передавайте writer в INSERT вместо результата close.
Контекст и завершениеСоздание относится к транзакции. После записи нужен close, связывание ID с колонкой и commit. При отказе до close — cancel. Перед завершением соединения закрывайте все BLOB-ресурсы.

Исторические страницы PHP Manual помечены PHP 5 / PHP 7 до 7.4: прежнее встроенное расширение удалено из поставки PHP 7.4. Здесь используется отдельный interbase 3.0.1 на PHP 8.2.35 с Firebird 5.0.4. Отличия исторического описания от выбранного драйвера указаны прямо; это не обещание работы без установленного расширения и не API PDO.

Начать первую инструкцию

$writer = fbird_blob_create($db);
if ($writer === false) { throw new RuntimeException("Нельзя создать заметку"); }
fbird_blob_add($writer, "Доставка к 10:00.");

В редакциях заметки это переход от SQL NULL к первой инструкции. Следующие части добавляются в тот же writer; полный файл снимает диагностику немедленно и отменяет незавершённое создание в finally.

Подготовить замену отдельно от принятого файла

$newWriter = fbird_blob_create($db);
if ($newWriter === false) { throw new RuntimeException("Нельзя принять новый файл"); }
// Добавить новые байты и проверить их.
// Пока не делать UPDATE старой строки.

В отмене замены старый approved.txt уже подтверждён. Новый writer не редактирует его на месте; повреждение новой загрузки не должно менять прежнюю ссылку. Ошибка доступа, невозможность записи и недействительный контекст требуют обработки, а не попытки использовать false как ресурс.

Типы колонок TEXT и BINARY выбирают по смыслу: текст с известной кодировкой или точные байты вложения. create не принимает filename, MIME или charset отдельными аргументами. Для готового файлового потока fbird_blob_import выполняет связку создания, копирования и закрытия внутри одного вызова.

fbird_blob_addПередать очередные байты в открытый writer.

PHP 5 / PHP 7 < 7.4 исторически; отдельный interbase 3.0.1 · interbase

fbird_blob_add(resource $blob_handle, string $data): void; в 3.0.1 native-отказ может вернуть false

Добавить очередную порцию можно только в BLOB, который ещё открыт для записи. Это помогает собрать текст из частей или передавать файл без общего большого буфера. ibase_blob_add — каноническое имя той же операции. В FirebirdSQL interbase 3.0.1 имя fbird_blob_add зарегистрировано как alias того же обработчика, поэтому данные и ресурсы совместимы; другое написание не меняет транзакцию или формат хранилища.

Параметр / результатКонтракт
blob_handleWriter из fbird_blob_create/ibase_blob_create. Не строковый ID и не ресурс, открытый для чтения. После close/cancel повторный add недопустим.
data: stringОчередная строка байтов; следующий add продолжает предыдущий. Нулевые байты и FF допустимы в бинарном содержимом. Base64 не требуется для BINARY-колонки.
ВозвратPHP Manual описывает void: полезного значения нет. В 3.0.1 успешный вызов возвращает null, а ветви native-отказа возвращают false и устанавливают диагностику. Проверка === true ошибочно отвергла бы успех.
Ошибки и памятьНеверный режим ресурса, отказ native-записи и ошибка аргумента не означают «записано 0 байт». Сохраните предупреждение/код немедленно. Внутренний C-цикл делит большую строку по USHRT_MAX, но PHP всё равно уже хранит переданную строку в памяти.

Исторические страницы PHP Manual помечены PHP 5 / PHP 7 до 7.4: прежнее встроенное расширение удалено из поставки PHP 7.4. Здесь используется отдельный interbase 3.0.1 на PHP 8.2.35 с Firebird 5.0.4. Отличия исторического описания от выбранного драйвера указаны прямо; это не обещание работы без установленного расширения и не API PDO.

Две строки одной инструкции

fbird_blob_add($writer, "Доставка к 10:00.");
fbird_blob_add($writer, "\nПозвонить заранее.");
// Далее close → UPDATE с ID → commit.

В первой редакции результат содержит обе строки в этом порядке. strlen измеряет UTF-8-байты, не число русских букв. Короткие фрагменты показывают действие; полноценный файл проверяет каждый add через blobAppend162.

Порция двоичного журнала

$chunk = fread($input, 4096);
if ($chunk === false) { throw new RuntimeException("Ошибка входного файла"); }
if ($chunk !== "") { fbird_blob_add($writer, $chunk); }

Это один проход записи журнала устройства. 30 вызовов передают 120000 байт. Пустое чтение — повод проверить EOF, а не отправить несуществующие данные. Предел одной порции не устанавливает общий предел BLOB: размер и допустимость вложения проверяет приложение.

add не добавляет текст к уже сохранённой колонке по её ID. Для изменения создайте новое содержимое и обновите ссылку в строке либо используйте подходящую SQL-операцию. Разделение потока может проходить между байтами текстового символа; его нельзя декодировать как самостоятельную строку в каждой порции без учёта границ. Наш бинарный сценарий вообще не выполняет текстовых преобразований.

fbird_blob_closeЗакончить запись/чтение BLOB и освободить ресурс.

PHP 5 / PHP 7 < 7.4 исторически; отдельный interbase 3.0.1 · interbase

fbird_blob_close(resource $blob_handle): mixed — строковый ID записи; исторически true чтения; false при отказе

После записи нужно получить значение, которое можно связать со строкой таблицы. После чтения нужно освободить ресурс чтения. Оба действия выполняет close, но это ещё не commit. ibase_blob_close — каноническое имя той же операции. В FirebirdSQL interbase 3.0.1 имя fbird_blob_close зарегистрировано как alias того же обработчика, поэтому данные и ресурсы совместимы; другое написание не меняет транзакцию или формат хранилища.

Параметр / результатКонтракт
blob_handleДействующий BLOB-ресурс из create для записи либо open для чтения. Функция не принимает файловый поток fopen или строковый ID.
Исторический mixedPHP Manual описывает строковый ID после записи, true после чтения, false при ошибке. Эти варианты сохранены как исторический контракт.
Выбранный драйвер 3.0.1Один C-обработчик после успешного закрытия возвращает строковый ID и для writer, и для reader. Поэтому завершение чтения нельзя проверять только через === true. Для writer проверяйте строку, для reader — отсутствие false/диагностики.
Ресурс и IDПосле успеха ставьте переменной ресурса null и не используйте её снова. Временный ID после create/close может отличаться от ID, выбранного после сохранения; для обратного чтения получите значение колонки новым SELECT.
Ошибкаfalse и native-диагностика означают незавершённое действие; неверный/закрытый ресурс может вызвать ошибку PHP. Сохраните причину до следующих вызовов.

Исторические страницы PHP Manual помечены PHP 5 / PHP 7 до 7.4: прежнее встроенное расширение удалено из поставки PHP 7.4. Здесь используется отдельный interbase 3.0.1 на PHP 8.2.35 с Firebird 5.0.4. Отличия исторического описания от выбранного драйвера указаны прямо; это не обещание работы без установленного расширения и не API PDO.

Закончить новую редакцию

$id = fbird_blob_close($writer);
if (!is_string($id)) { throw new RuntimeException("Заметка не закрыта"); }
$writer = null;
// Передать $id параметром UPDATE и проверить затронутую строку.

В редакции 2 UPDATE меняет ссылку только при ожидаемой revision_no=1. ID не выводится вместо инструкции и не используется как публичный ключ заказа. После commit текст снова прочитан из базы.

Завершить проверку бинарного содержимого

try {
    // get(reader, 4096) в цикле: длина и hash_update.
} finally {
    $closed = fbird_blob_close($reader);
    $reader = null;
    if ($closed === false) { throw new RuntimeException("Чтение не закрыто"); }
}

Так проверка журнала заканчивает обратное чтение. Полный файл также проверяет предупреждения. Строка, полученная от close reader, не является содержимым файла: полезные байты были получены get раньше.

close завершает конкретный BLOB-ресурс, а commit — транзакцию, в которой BLOB закреплён за записью. Закрыть writer и не выполнить INSERT/UPDATE недостаточно для появления вложения в каталоге. Отмена незавершённого writer разобрана в fbird_blob_cancel, чтение по сохранённому ID — в парной главе.

fbird_blob_importПеренести открытый файл в новый BLOB и получить ID.

PHP 5 / PHP 7 < 7.4 исторически; отдельный interbase 3.0.1 · interbase

fbird_blob_import(resource $link_identifier, resource $file_handle): string|false; fbird_blob_import(resource $file_handle): string|false

Если данные уже лежат в открытом файле, не обязательно вручную повторять create/add/close. import копирует файловый поток в новый BLOB и возвращает его ID. ibase_blob_import — каноническое имя той же операции. В FirebirdSQL interbase 3.0.1 имя fbird_blob_import зарегистрировано как alias того же обработчика, поэтому данные и ресурсы совместимы; другое написание не меняет транзакцию или формат хранилища.

Параметр / результатКонтракт
Две формыfbird_blob_import($link_identifier,$file_handle) и fbird_blob_import($file_handle). В однопараметрической форме используется последнее соединение; явный контекст понятнее. Общий выбор контекста 3.0.1 допускает ресурс транзакции.
file_handleОткрытый PHP-поток, обычно из fopen(...,"rb"). Это не путь, не массив строк и не writer BLOB. Чтение начинается с текущей позиции до конца доступного потока.
string / falseУспех — строковый ID закрытого BLOB; native-отказ — false с диагностикой. Сокращённая историческая сигнатура string не отменяет false из раздела Return Values.
Чем владеет вызывающий кодimport сама создаёт и закрывает BLOB, но оставляет PHP-файл открытым. fclose в finally, проверка полного переноса, INSERT/UPDATE и commit остаются приложению.
ГраницыФункция не разбирает CSV, не проверяет MIME и не возвращает продуктовые строки. Для непрерывного входа с промежуточными проверками может быть удобнее явный цикл add.

Исторические страницы PHP Manual помечены PHP 5 / PHP 7 до 7.4: прежнее встроенное расширение удалено из поставки PHP 7.4. Здесь используется отдельный interbase 3.0.1 на PHP 8.2.35 с Firebird 5.0.4. Отличия исторического описания от выбранного драйвера указаны прямо; это не обещание работы без установленного расширения и не API PDO.

Полный CSV после проверки заголовка

$header = fgetcsv($input, 0, ";", '"', "");
if ($header !== ["id","title","qty"]) { throw new DomainException("Неверный заголовок"); }
if (!rewind($input)) { throw new RuntimeException("Файл не перематывается"); }
$id = fbird_blob_import($db, $input);
if ($id === false) { throw new RuntimeException("Файл не импортирован"); }

Полный сценарий затем вставляет ID в запись архива, закрывает PHP-файл, делает commit и проверяет, что заголовок не пропал. Нулевой остаток и разделитель внутри кавычек становятся понятны уже при отдельном разборе сохранённого CSV.

Готовый отчёт без предварительного чтения

$input = fopen($validatedReportPath, "rb");
if ($input === false) { throw new RuntimeException("Отчёт недоступен"); }
try {
    $id = fbird_blob_import($db, $input);
    if ($id === false) { throw new RuntimeException("Отчёт не сохранён"); }
} finally { fclose($input); }
// Связать ID с записью отчёта; после commit сравнить размер и SHA-256.

Это контекстный вариант для уже созданного файла отчёта: позиция нового fopen равна началу и перемотка пока не нужна. Самостоятельный запускаемый архив CSV показывает полный цикл хранения и проверки. Путь $validatedReportPath должен быть выбран приложением, а не произвольным вводом HTTP.

Строковый ID сам по себе не доказывает, что источник был прочитан целиком: проверяйте состояние источника и ожидаемую длину/хеш. Если предварительное чтение уже сдвинуло неперематываемый поток, отдельно сохраните прочитанную часть или используйте create/add; нельзя молча считать остаток полным файлом. Для побайтового архива используется BINARY, даже если конкретный файл — известный UTF-8 CSV.

fbird_blob_cancelОтбросить незавершённое новое содержимое до close.

PHP 5 / PHP 7 < 7.4 исторически; отдельный interbase 3.0.1 · interbase

fbird_blob_cancel(resource $blob_handle): bool

При отказе проверки или чтения входа незавершённое новое содержимое не должно стать вложением. cancel отбрасывает именно открытый writer. ibase_blob_cancel — каноническое имя той же операции. В FirebirdSQL interbase 3.0.1 имя fbird_blob_cancel зарегистрировано как alias того же обработчика, поэтому данные и ресурсы совместимы; другое написание не меняет транзакцию или формат хранилища.

Параметр / результатКонтракт
blob_handleРесурс записи из create, ещё не закрытый close. Не ID сохранённого BLOB, не ID заказа и не файловый поток.
booltrue при успешной отмене; false и диагностика при отказе. После успеха переменная получает null, чтобы finally не повторил операцию. Ошибка типа/закрытого ресурса обрабатывается отдельно.
Что отменяетсяСоздание этого BLOB. Сохранённые ранее файлы и остальные SQL-изменения функция не удаляет и не откатывает. Для общей операции нужен rollback её транзакции.
ПорядокПроверка данных → при успехе close и привязка ID; при отказе до close — cancel. После close применять cancel поздно: выполняйте откат незавершённой транзакции или отдельную разрешённую UPDATE/DELETE-операцию для уже подтверждённых данных.

Исторические страницы PHP Manual помечены PHP 5 / PHP 7 до 7.4: прежнее встроенное расширение удалено из поставки PHP 7.4. Здесь используется отдельный interbase 3.0.1 на PHP 8.2.35 с Firebird 5.0.4. Отличия исторического описания от выбранного драйвера указаны прямо; это не обещание работы без установленного расширения и не API PDO.

Не совпала контрольная сумма

if (!hash_equals($expectedSha, $actualSha)) {
    if (fbird_blob_cancel($writer) !== true) {
        throw new RuntimeException("Черновик не отменён");
    }
    $writer = null;
    // Не выполнять UPDATE действующего вложения.
}

В неудачной замене до отказа добавлено 23 байта. После cancel и rollback свежий SELECT всё ещё видит approved.txt и прежнее содержимое. Это ожидаемый отказ проверки приложения, а не искусственно вызванная поломка драйвера.

Входной файл перестал читаться

try {
    // fread/add в цикле; при ошибке — исключение.
    // Успех: close, writer=null, INSERT/UPDATE, commit.
} finally {
    if (is_resource($writer)) {
        fbird_blob_cancel($writer);
        $writer = null;
    }
    fclose($input);
}

Так устроено владение ресурсами в порционной записи: writer существует только до успешного close. Полная программа дополнительно проверяет cancel и использует вложенный finally для закрытия входного файла даже при отказе очистки. Здесь показана ветка обработки, а не заявление о принудительно испытанном отказе диска.

Не считайте cancel освобождением места файла базы до определённого размера: внутреннее размещение и сборка старых версий зависят от сервера и транзакций. Бизнес-проверка в этом уроке конкретнее: новый ID не установлен, прежний файл читается без изменений. Если замена должна менять ещё и другие строки, объединяйте их в явную транзакционную операцию.

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

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

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

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

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