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

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

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

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

Глава 62 · После первых запросов и транзакций Firebird

Firebird BLOB: сохранить примечание, загрузить файл и отменить незавершённую запись

Примечание с read-back, двоичное вложение по частям, импорт накладной и отмена черновика: четыре реальных сценария и пять полных карточек.

Зачем хранить вложение и чем ID отличается от содержимого

У заказа есть примечание, у накладной — исходный файл, у оборудования — двоичный пакет. Их удобно связать с конкретной строкой базы, чтобы получать вложение по документу. BLOB хранит последовательность байтов, а запись содержит ссылку на эти данные. В этой главе вы сохраните и прочитаете русское примечание, перенесёте 90000 двоичных байт, импортируете готовую накладную и отклоните частичную загрузку.

Три разных значения в одном сохранении
  1. Содержимое: текст или байты файла
  2. writer: ресурс для дописывания нового BLOB
  3. close(writer): строковый BLOB ID
  4. INSERT документа с ID + commit
  5. SELECT документа → ID → отдельное чтение содержимого

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

Предварительные шаги — соединение и первый SELECT, SQL и значения отдельно и commit/rollback всей операции. Здесь используются interbase 3.0.1 от FirebirdSQL, PHP 8.2.35 и Firebird 5.0.4. Старый встроенный модуль PHP был удалён из поставки PHP 7.4; наличие страниц с именами ibase не означает, что современный PHP содержит его автоматически. Настройка отдельного драйвера приведена в предыдущих главах и README каждого примера.

ЗадачаМаршрут
Описание формируется в кодеcreate → add → close → ID в строке
Нужно ограничить память и обрабатывать порциицикл чтения файла и add
Локальный файл уже готовimport → ID → INSERT
Новый файл нельзя принятьcancel до закрытия и вставки строки

Текстовая колонка, двоичное вложение и граница транзакции

BLOB SUB_TYPE TEXT CHARACTER SET UTF8 объявляет текстовую колонку с известной кодировкой. BLOB SUB_TYPE BINARY объявляет двоичные данные: PDF, изображение или пакет устройства. Если SUB_TYPE и CHARACTER SET отсутствуют, подразумевается BINARY. Для двоичных файлов строковые преобразования и base64 обычно не нужны. Имя файла, тип содержимого и привязка к заказу — отдельные поля вашей модели, а не свойства PHP-ресурса writer.

CREATE TABLE attachments (
    attachment_id INTEGER NOT NULL PRIMARY KEY,
    file_name VARCHAR(80) NOT NULL,
    payload BLOB SUB_TYPE BINARY
);

Это форма структуры, не команда для запуска в рабочей базе. Каждый самостоятельный пример ниже создаёт свою таблицу со случайным именем, выполняет INSERT и SELECT и удаляет только её. Для примечания используется другая колонка — note_text BLOB SUB_TYPE TEXT CHARACTER SET UTF8. Параметр SEGMENT SIZE в старых объявлениях не задаёт полезный предел размера вложения; фактические порции передачи выбирает клиент.

В лабе f155Query/f155Command передают значения отдельными параметрами, f155Capture сразу сохраняет признак предупреждения и код ошибки без раскрытия пути к частной базе, а f155Commit подтверждает default-транзакцию. Создание BLOB и INSERT идут через один и тот же $db. finally закрывает/отменяет принадлежащие программе ресурсы перед общей очисткой. В производственном сценарии с другими изменениями используйте согласованный явный $tr и завершайте весь документ одной границей.

Примечание к заказу: сохранить текст и прочитать его после commit

В заказе есть подробная инструкция курьеру. Короткий заголовок поместится в VARCHAR, но многострочное описание может расти. Сохраним примечание в текстовом BLOB, закрепим его за заказом №155 и покажем именно восстановленный текст после подтверждения записи. Маленькая фраза нужна для понятного первого шага; для такого короткого поля самого по себе BLOB необязателен.

Путь данных в этом примере
  1. PHP: «Оставить коробки у стойки выдачи.»
  2. create(db) → writer: открыта запись нового содержимого
  3. add(writer, note) → байты внутри BLOB
  4. close(writer) → id; INSERT(155, id) → строка заказа
  5. commit(db) → новое чтение строки → open/get → исходная фраза

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

$writer = ibase_blob_create($db);
ibase_blob_add($writer, "Оставить коробки у стойки выдачи.");
$id = ibase_blob_close($writer);
// В параметр BLOB-колонки передаётся $id.
// INSERT и commit следуют ниже в самостоятельном примере.

В первой строке $writer получает ресурс: открытый канал для добавления байтов. Во второй строке функция получает этот ресурс и текст. Третья строка завершает запись и возвращает строковый идентификатор $id. Это не текст и не имя файла. Короткий фрагмент показывает порядок вызовов при уже открытом $db; полный пример проверяет каждый результат. Поле note_text объявлено как BLOB SUB_TYPE TEXT CHARACTER SET UTF8: назначение — текст, кодировка — UTF-8. Соединение лаборатории тоже использует UTF8. INSERT с вопросительными знаками получает отдельно номер заказа и BLOB ID.

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

НаблюдениеРезультат
Заказ155
Примечание после commitОставить коробки у стойки выдачи.
Байт UTF-861
В строке хранится BLOB IDда
Прочитанный текст совпалда
Примечание к заказу: сохранить текст и прочитать его после commitСамостоятельный пример
<?php
declare(strict_types=1);
require __DIR__.'/lab.php';
$db=null;$owned=$removed=$rows=$checks=$observations=[];
try {
    $config=f155Config();$db=f155Connect($config);$runtime=f155Runtime($db,$config);

    function createBlob155($context) {
        $call=f155Capture(static fn()=>ibase_blob_create($context));
        if (!is_resource($call['value']) || $call['warning']) {f155Failure('BLOB не создан',$call);}
        return $call['value'];
    }
    function appendBlob155($handle,string $bytes): void {
        $call=f155Capture(static fn()=>ibase_blob_add($handle,$bytes));
        // Successful add has no value (null), not true. Check refusal and diagnostics.
        if ($call['value']===false || $call['warning'] || $call['code']!==0) {f155Failure('Порция BLOB не записана',$call);}
    }
    function finishBlob155(&$handle): string {
        $call=f155Capture(static fn()=>ibase_blob_close($handle));
        if (!is_string($call['value']) || $call['warning']) {f155Failure('BLOB не закрыт',$call);}
        $handle=null;
        return $call['value'];
    }
    function discardBlob155(&$handle): bool {
        $call=f155Capture(static fn()=>ibase_blob_cancel($handle));
        if ($call['value']!==true || $call['warning']) {f155Failure('Незавершённый BLOB не отменён',$call);}
        $handle=null;
        return true;
    }
    function inspectBlob155($context,string $id,bool $keepSmallText=false): array {
        $call=f155Capture(static fn()=>ibase_blob_open($context,$id));
        if (!is_resource($call['value']) || $call['warning']) {f155Failure('Сохранённый BLOB не открыт',$call);}
        $reader=$call['value'];unset($call);
        $bytes=0;$value='';$hash=hash_init('sha256');
        try {
            while (true) {
                $call=f155Capture(static fn()=>ibase_blob_get($reader,8192));
                if (!is_string($call['value']) || $call['warning']) {f155Failure('BLOB не прочитан',$call);}
                $chunk=$call['value'];
                if ($chunk==='') {break;}
                $bytes+=strlen($chunk);hash_update($hash,$chunk);
                if ($keepSmallText) {
                    if ($bytes>4096) {throw new RuntimeException('Учебный текст превышает лимит 4096 байт.');}
                    $value.=$chunk;
                }
            }
        } finally {
            $closed=f155Capture(static fn()=>ibase_blob_close($reader));$reader=null;
            if ($closed['value']===false || $closed['warning']) {f155Failure('Читающий BLOB не закрыт',$closed);}
        }
        return ['bytes'=>$bytes,'sha256'=>hash_final($hash),'text'=>$value];
    }

    $name=f155OwnTable($db,$owned,'note','(order_id INTEGER NOT NULL PRIMARY KEY,note_text BLOB SUB_TYPE TEXT CHARACTER SET UTF8)');
    $q=f155Qualified($name);

    $note='Оставить коробки у стойки выдачи.';
    $writer=createBlob155($db);
    try {
        appendBlob155($writer,$note);
        $id=finishBlob155($writer);
        f155Command($db,"INSERT INTO $q(order_id,note_text) VALUES(?,?)",[155,$id]);
        f155Commit($db);
    } finally {
        if (is_resource($writer)) {discardBlob155($writer);}
    }
    $record=f155Read($db,"SELECT order_id AS \"order_id\",note_text AS \"blob_id\" FROM $q WHERE order_id=?",[155])[0];
    $saved=inspectBlob155($db,$record['blob_id'],true);
    $rows=[['Заказ',$record['order_id']],['Примечание после commit',$saved['text']],['Байт UTF-8',$saved['bytes']],
        ['В строке хранится BLOB ID',is_string($record['blob_id'])],['Прочитанный текст совпал',$saved['text']===$note]];
    $checks=['order_found'=>$record['order_id']===155,'writer_closed'=>$writer===null,
        'blob_id_saved'=>is_string($record['blob_id'])&&$record['blob_id']!=='',
        'text_roundtrip'=>$saved['text']===$note,'byte_count'=>$saved['bytes']===strlen($note),
        'content_hash'=>$saved['sha256']===hash('sha256',$note)];
    $observations=['order_id'=>$record['order_id'],'source_text'=>$note,'saved_text'=>$saved['text'],'source_bytes'=>strlen($note),
        'saved_bytes'=>$saved['bytes'],'sha256'=>$saved['sha256'],'id_is_content'=>$record['blob_id']===$note];

} finally {
    if (is_resource($db)) {try {$removed=f155Cleanup($db,$owned);} finally {ibase_close($db);}}
}

f155Report('fb155-text-note','Примечание к заказу: сохранить текст и прочитать его после commit',$rows,$checks,$observations,$runtime,$owned,$removed);
Ожидаемый результат
Заказ — 155
Примечание после commit — Оставить коробки у стойки выдачи.
Байт UTF-8 — 61
В строке хранится BLOB ID — да
Прочитанный текст совпал — да

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

Фактический HTML-результат · Данные результата JSON.

Ошибки, ограничения и применение

Закрытие BLOB ещё не является commit. В нашей задаче BLOB и строка заказа принадлежат одной default-транзакции соединения; общая очистка откатывает её при незавершённой операции. Не передавайте случайный BLOB ID от другого соединения/базы и не принимайте его как публичный идентификатор файла. Проверка читает только заказ №155. inspectBlob155 удерживает целиком лишь небольшой учебный текст с лимитом 4096 байт; большие вложения в следующем сценарии проверяются потоково. Замена кодировки наугад не исправляет байты неизвестного происхождения.

В реальном вопросе описание платежа хранилось в binary BLOB, а попытки прочитать его через произвольный CAST и разные кодировки портили кириллицу. Разбор исходной ситуации на Stack Overflow. В нашем примере назначение поля задано как TEXT UTF8, кодировка соединения определена заранее, а исходная фраза сравнивается с восстановленной. Для чужого binary BLOB сначала нужен известный формат исходных байтов; перебор кодировок наугад не является исправлением.

Измените условие задачи

Добавьте к примечанию новую строку «Позвонить за 10 минут». Что должно измениться в отчёте?

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

Изменятся восстановленный текст, число байтов и SHA-256, но сравнение исходного текста с прочитанным останется истинным. Перенос строки сохраняется как часть содержимого. Не сравнивайте размер в байтах с количеством русских букв: эти величины различаются.

Двоичное вложение: записать порциями и сверить все 90000 байт

Сервис получает файл сырых пакетов сканера. В нём встречаются нулевой байт и байт FF, поэтому обращаться с ним как с обычным текстом нельзя. Сохраним файл в BLOB SUB_TYPE BINARY, передавая не более 8192 байт за один вызов нашего цикла. После commit ещё раз прочитаем содержимое порциями: длина и SHA-256 должны совпасть. Отчёт содержит имя файла, 90000 байт и 11 вызовов добавления, а не нечитаемую бинарную строку.

Путь данных в этом примере
  1. Временный учебный файл: 9000 записей по 10 байт
  2. fread(input, 8192) → очередная порция PHP-строки
  3. add(writer, chunk) повторяется 11 раз
  4. close → ID; INSERT имени и ID; commit
  5. open/get → счётчик байтов + hash_update → 90000 и тот же SHA-256

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

$chunk = fread($input, 8192);
// $input открыт в режиме rb; $writer создан через ibase_blob_create.
if ($chunk === false) { throw new RuntimeException("Файл не прочитан"); }
ibase_blob_add($writer, $chunk);
// Повторять до конца файла, затем close → INSERT → commit.

fread возвращает порцию байтов, а 8192 — верхний размер этой порции, не размер всего вложения. PHP-строка может содержать нулевые и нетекстовые байты. Каждая следующая add дописывает данные в тот же BLOB в том же порядке. hash_update постепенно считает контрольную сумму; целиком загружать файл для неё не требуется. Временный файл создаётся только для самостоятельности урока и автоматически удаляется после fclose. В настоящем обработчике вместо генератора нужно открыть уже проверенный временный файл загрузки.

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

НаблюдениеРезультат
Вложениеscanner-packets.bin
Байт исходного файла90000
Вызовов добавления11
Байт прочитано из BLOB90000
SHA-256fe792f8cbc151d3279a058045af4862f8b74f0ba8d5a31d01f311b0b89b7e789
Байты и SHA-256 совпалида
Двоичное вложение: записать порциями и сверить все 90000 байтСамостоятельный пример
<?php
declare(strict_types=1);
require __DIR__.'/lab.php';
$db=null;$owned=$removed=$rows=$checks=$observations=[];
try {
    $config=f155Config();$db=f155Connect($config);$runtime=f155Runtime($db,$config);

    function createBlob155($context) {
        $call=f155Capture(static fn()=>ibase_blob_create($context));
        if (!is_resource($call['value']) || $call['warning']) {f155Failure('BLOB не создан',$call);}
        return $call['value'];
    }
    function appendBlob155($handle,string $bytes): void {
        $call=f155Capture(static fn()=>ibase_blob_add($handle,$bytes));
        // Successful add has no value (null), not true. Check refusal and diagnostics.
        if ($call['value']===false || $call['warning'] || $call['code']!==0) {f155Failure('Порция BLOB не записана',$call);}
    }
    function finishBlob155(&$handle): string {
        $call=f155Capture(static fn()=>ibase_blob_close($handle));
        if (!is_string($call['value']) || $call['warning']) {f155Failure('BLOB не закрыт',$call);}
        $handle=null;
        return $call['value'];
    }
    function discardBlob155(&$handle): bool {
        $call=f155Capture(static fn()=>ibase_blob_cancel($handle));
        if ($call['value']!==true || $call['warning']) {f155Failure('Незавершённый BLOB не отменён',$call);}
        $handle=null;
        return true;
    }
    function inspectBlob155($context,string $id,bool $keepSmallText=false): array {
        $call=f155Capture(static fn()=>ibase_blob_open($context,$id));
        if (!is_resource($call['value']) || $call['warning']) {f155Failure('Сохранённый BLOB не открыт',$call);}
        $reader=$call['value'];unset($call);
        $bytes=0;$value='';$hash=hash_init('sha256');
        try {
            while (true) {
                $call=f155Capture(static fn()=>ibase_blob_get($reader,8192));
                if (!is_string($call['value']) || $call['warning']) {f155Failure('BLOB не прочитан',$call);}
                $chunk=$call['value'];
                if ($chunk==='') {break;}
                $bytes+=strlen($chunk);hash_update($hash,$chunk);
                if ($keepSmallText) {
                    if ($bytes>4096) {throw new RuntimeException('Учебный текст превышает лимит 4096 байт.');}
                    $value.=$chunk;
                }
            }
        } finally {
            $closed=f155Capture(static fn()=>ibase_blob_close($reader));$reader=null;
            if ($closed['value']===false || $closed['warning']) {f155Failure('Читающий BLOB не закрыт',$closed);}
        }
        return ['bytes'=>$bytes,'sha256'=>hash_final($hash),'text'=>$value];
    }

    $name=f155OwnTable($db,$owned,'binary','(attachment_id INTEGER NOT NULL PRIMARY KEY,file_name VARCHAR(80) NOT NULL,payload BLOB SUB_TYPE BINARY)');
    $q=f155Qualified($name);

    // Only fixture preparation: a tiny repeatable device record, including NUL and 0xFF.
    // In the application this is fopen($validatedTemporaryUpload,'rb').
    $input=tmpfile();
    if ($input===false) {throw new RuntimeException('Не создан временный учебный файл.');}
    $writer=null;$sourceHash=hash_init('sha256');$sourceBytes=0;$writes=0;
    try {
        $block=str_repeat("PACKET\0\xFF\x10\n",300);
        for ($part=0;$part<30;$part++) {
            if (fwrite($input,$block)!==strlen($block)) {throw new RuntimeException('Учебный файл записан не полностью.');}
        }
        if (rewind($input)!==true) {throw new RuntimeException('Не удалось вернуться к началу файла.');}
        $writer=createBlob155($db);
        while (!feof($input)) {
            $chunk=fread($input,8192);
            if ($chunk===false) {throw new RuntimeException('Ошибка чтения входного файла.');}
            if ($chunk==='') {break;}
            appendBlob155($writer,$chunk);
            $sourceBytes+=strlen($chunk);hash_update($sourceHash,$chunk);$writes++;
        }
        $id=finishBlob155($writer);
        f155Command($db,"INSERT INTO $q(attachment_id,file_name,payload) VALUES(?,?,?)",[201,'scanner-packets.bin',$id]);
        f155Commit($db);
    } finally {
        if (is_resource($writer)) {discardBlob155($writer);}
        fclose($input);$input=null;
    }
    $sourceSha=hash_final($sourceHash);
    $record=f155Read($db,"SELECT file_name AS \"file_name\",payload AS \"blob_id\" FROM $q WHERE attachment_id=?",[201])[0];
    $saved=inspectBlob155($db,$record['blob_id']);
    $rows=[['Вложение',$record['file_name']],['Байт исходного файла',$sourceBytes],['Вызовов добавления',$writes],
        ['Байт прочитано из BLOB',$saved['bytes']],['SHA-256',$saved['sha256']],['Байты и SHA-256 совпали',$saved['bytes']===$sourceBytes&&$saved['sha256']===$sourceSha]];
    $checks=['source_fixture_size'=>$sourceBytes===90000,'bounded_chunk_writes'=>$writes===11,'writer_closed'=>$writer===null,
        'binary_byte_count'=>$saved['bytes']===$sourceBytes,'binary_hash'=>$saved['sha256']===$sourceSha,
        'no_full_readback_buffer'=>$saved['text']===''];
    $observations=['fixture_record_hex'=>'5041434b455400ff100a','fixture_records'=>9000,'chunk_bytes'=>8192,
        'source_bytes'=>$sourceBytes,'source_sha256'=>$sourceSha,'saved_bytes'=>$saved['bytes'],'saved_sha256'=>$saved['sha256'],
        'write_calls'=>$writes,'text_buffer_used'=>false];

} finally {
    if (is_resource($db)) {try {$removed=f155Cleanup($db,$owned);} finally {ibase_close($db);}}
}

f155Report('fb155-chunked-attachment','Двоичное вложение: записать порциями и сверить все 90000 байт',$rows,$checks,$observations,$runtime,$owned,$removed);
Ожидаемый результат
Вложение — scanner-packets.bin
Байт исходного файла — 90000
Вызовов добавления — 11
Байт прочитано из BLOB — 90000
SHA-256 — fe792f8cbc151d3279a058045af4862f8b74f0ba8d5a31d01f311b0b89b7e789
Байты и SHA-256 совпали — да

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

Фактический HTML-результат · Данные результата JSON.

Ошибки, ограничения и применение

8192 выбрано для примера, это не универсальный максимум BLOB. В драйвере FirebirdSQL 3.0.1 большой аргумент add дополнительно делится на допустимые клиентские сегменты; исторический рассказ о падении Apache не означает, что текущий драйвер обязательно падает на 65536 байтах. Наш цикл нужен прежде всего для ограниченного потребления памяти и проверяемого продвижения. base64 не нужен для двоичной колонки: он меняет содержимое и увеличивает размер. Совпадение SHA-256 здесь доказывает целостность передачи между двумя контролируемыми чтениями, а не доверенность загруженного файла. В реальном веб-сервисе до записи нужны лимит размера, проверка разрешённого типа и прав на заказ.

В историческом вопросе небольшая запись BLOB работала, а расширенный сценарий с большим файлом сопровождался падением Apache; ответ предположил проблему размера сегмента. Разбор исходной ситуации на Stack Overflow. Это сообщение о старом окружении, а не диагноз текущего драйвера. Здесь фиксируются версии, входной файл читается порциями, каждый шаг проверяется и сверяется SHA-256. Исходник выбранного драйвера уже делит большой аргумент add на клиентские сегменты; путать эту внутреннюю границу с предельным размером файла нельзя.

Измените условие задачи

Измените размер порции с 8192 на 4096. Как изменится отчёт?

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

Вызовов добавления станет 22: 21 полная порция и остаток. Размер 90000 байт и SHA-256 останутся прежними. Для своего изменённого учебного запуска также скорректируйте проверку write_calls; порядок байтов нельзя менять.

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

Накладная уже лежит в локальном файле. Нет необходимости вручную собирать её содержимое в одну строку и повторять цикл add: ibase_blob_import открывает новый BLOB, переносит туда данные потока и закрывает BLOB. После этого мы всё равно явно создаём строку с номером накладной, именем файла и полученным ID. В отчёте будет текст накладной, извлечённый из базы, включая русские буквы и переносы строк.

Путь данных в этом примере
  1. invoice.txt рядом с example.php → fopen(path, rb)
  2. import(db, input) читает от текущей позиции до EOF
  3. Возвращается строковый BLOB ID; input всё ещё принадлежит PHP
  4. INSERT(155, invoice.txt, ID) → commit
  5. SELECT только накладной №155 → read-back → все байты исходного файла

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

$input = fopen(__DIR__ . "/invoice.txt", "rb");
if ($input === false) { throw new RuntimeException("Файл не открыт"); }
try {
    $id = ibase_blob_import($db, $input);
    if ($id === false) { throw new RuntimeException("Импорт не выполнен"); }
} finally {
    fclose($input);
}
// Далее $id передают параметром INSERT и подтверждают транзакцию.

fopen получает путь и возвращает файловый ресурс. Режим rb означает чтение байтов без текстового преобразования перевода строк. import получает соединение и этот ресурс, а не путь. Она сама выполняет работу create/add/close для BLOB, но не закрывает PHP-файл и не вставляет строку в таблицу. Здесь BINARY хранит файл как точную последовательность байтов; для показа текста мы заранее знаем, что наш fixture — UTF-8. Смысл файла и его кодировка не угадываются по самому BLOB ID.

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

НаблюдениеРезультат
Накладная155
Имя файлаinvoice.txt
Прочитано байт97
Текст из базыНакладная №155 Кабель USB-C; 3 шт. Получатель: мастерская
После import файл ещё был открытда
Весь файл совпалда
Накладная из файла: импортировать содержимое и сохранить связь с заказомСамостоятельный пример
<?php
declare(strict_types=1);
require __DIR__.'/lab.php';
$db=null;$owned=$removed=$rows=$checks=$observations=[];
try {
    $config=f155Config();$db=f155Connect($config);$runtime=f155Runtime($db,$config);

    function createBlob155($context) {
        $call=f155Capture(static fn()=>ibase_blob_create($context));
        if (!is_resource($call['value']) || $call['warning']) {f155Failure('BLOB не создан',$call);}
        return $call['value'];
    }
    function appendBlob155($handle,string $bytes): void {
        $call=f155Capture(static fn()=>ibase_blob_add($handle,$bytes));
        // Successful add has no value (null), not true. Check refusal and diagnostics.
        if ($call['value']===false || $call['warning'] || $call['code']!==0) {f155Failure('Порция BLOB не записана',$call);}
    }
    function finishBlob155(&$handle): string {
        $call=f155Capture(static fn()=>ibase_blob_close($handle));
        if (!is_string($call['value']) || $call['warning']) {f155Failure('BLOB не закрыт',$call);}
        $handle=null;
        return $call['value'];
    }
    function discardBlob155(&$handle): bool {
        $call=f155Capture(static fn()=>ibase_blob_cancel($handle));
        if ($call['value']!==true || $call['warning']) {f155Failure('Незавершённый BLOB не отменён',$call);}
        $handle=null;
        return true;
    }
    function inspectBlob155($context,string $id,bool $keepSmallText=false): array {
        $call=f155Capture(static fn()=>ibase_blob_open($context,$id));
        if (!is_resource($call['value']) || $call['warning']) {f155Failure('Сохранённый BLOB не открыт',$call);}
        $reader=$call['value'];unset($call);
        $bytes=0;$value='';$hash=hash_init('sha256');
        try {
            while (true) {
                $call=f155Capture(static fn()=>ibase_blob_get($reader,8192));
                if (!is_string($call['value']) || $call['warning']) {f155Failure('BLOB не прочитан',$call);}
                $chunk=$call['value'];
                if ($chunk==='') {break;}
                $bytes+=strlen($chunk);hash_update($hash,$chunk);
                if ($keepSmallText) {
                    if ($bytes>4096) {throw new RuntimeException('Учебный текст превышает лимит 4096 байт.');}
                    $value.=$chunk;
                }
            }
        } finally {
            $closed=f155Capture(static fn()=>ibase_blob_close($reader));$reader=null;
            if ($closed['value']===false || $closed['warning']) {f155Failure('Читающий BLOB не закрыт',$closed);}
        }
        return ['bytes'=>$bytes,'sha256'=>hash_final($hash),'text'=>$value];
    }

    $name=f155OwnTable($db,$owned,'invoice','(invoice_id INTEGER NOT NULL PRIMARY KEY,file_name VARCHAR(80) NOT NULL,body BLOB SUB_TYPE BINARY)');
    $q=f155Qualified($name);

    $path=__DIR__.'/invoice.txt';
    $input=fopen($path,'rb');
    if ($input===false) {throw new RuntimeException('Учебная накладная не открыта.');}
    try {
        // Freshly opened stream is at byte 0. Import consumes from its current position.
        $call=f155Capture(static fn()=>ibase_blob_import($db,$input));
        if (!is_string($call['value']) || $call['warning'] || $call['code']!==0) {f155Failure('Накладная не импортирована',$call);}
        $id=$call['value'];
        $fileStillOpen=is_resource($input);$position=ftell($input);
    } finally {fclose($input);$input=null;}
    f155Command($db,"INSERT INTO $q(invoice_id,file_name,body) VALUES(?,?,?)",[155,'invoice.txt',$id]);
    f155Commit($db);
    $record=f155Read($db,"SELECT invoice_id AS \"invoice_id\",file_name AS \"file_name\",body AS \"blob_id\" FROM $q WHERE invoice_id=?",[155])[0];
    $saved=inspectBlob155($db,$record['blob_id'],true);
    $expected=file_get_contents($path);
    if ($expected===false) {throw new RuntimeException('Исходная накладная недоступна для сравнения.');}
    $rows=[['Накладная',$record['invoice_id']],['Имя файла',$record['file_name']],['Прочитано байт',$saved['bytes']],
        ['Текст из базы',$saved['text']],['После import файл ещё был открыт',$fileStillOpen],['Весь файл совпал',$saved['text']===$expected]];
    $checks=['invoice_found'=>$record['invoice_id']===155,'import_returned_id'=>is_string($id)&&$id!=='',
        'import_consumed_file'=>$position===strlen($expected),'caller_owns_file'=>$fileStillOpen&&$input===null,
        'file_roundtrip'=>$saved['text']===$expected,'file_hash'=>$saved['sha256']===hash('sha256',$expected)];
    $observations=['file_name'=>'invoice.txt','source_bytes'=>strlen($expected),'saved_bytes'=>$saved['bytes'],'text'=>$saved['text'],
        'sha256'=>$saved['sha256'],'stream_position_after_import'=>$position,'file_was_open_after_import'=>$fileStillOpen];

} finally {
    if (is_resource($db)) {try {$removed=f155Cleanup($db,$owned);} finally {ibase_close($db);}}
}

f155Report('fb155-import-file','Накладная из файла: импортировать содержимое и сохранить связь с заказом',$rows,$checks,$observations,$runtime,$owned,$removed);
Ожидаемый результат
Накладная — 155
Имя файла — invoice.txt
Прочитано байт — 97
Текст из базы — Накладная №155
Кабель USB-C; 3 шт.
Получатель: мастерская

После import файл ещё был открыт — да
Весь файл совпал — да

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

Фактический HTML-результат · Данные результата JSON.

Ошибки, ограничения и применение

Функция начинает с текущей позиции файлового потока. Если ранее прочитали заголовок для проверки, выполните rewind для seekable-файла до импорта целого вложения. Иначе сохранится остаток. Импорт не заменяет проверку результата INSERT и commit. Обработка повторной загрузки, прав доступа и имени файла остаётся задачей приложения; не склеивайте пользовательское имя с SQL и не открывайте произвольный путь из запроса. Строковый ID — ещё не проверка полноты передачи, поэтому в нашем сценарии есть повторное чтение и сравнение длины/хеша.

Автор создал и закрыл BLOB, но в INSERT передал переменную с base64-содержимым вместо полученного BLOB ID. Разбор исходной ситуации на Stack Overflow. В нашей программе данные файла, PHP-файловый ресурс и строковый BLOB ID имеют разные переменные. В параметр body попадает $id. Содержимое binary-вложения не преобразуется в base64; правильность проверяется повторным чтением всей накладной.

Измените условие задачи

Перед import прочитайте первую строку через fgets. Как сохранить всё содержимое, а не только оставшиеся строки?

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

После проверки заголовка вызовите rewind($input) и проверьте true. Затем import должен снова прочитать исходное число байтов, а read-back совпасть со всем invoice.txt. Для неперематываемого источника нужно заранее выбрать подходящую схему: например, проверять и записывать все порции самостоятельно.

Отклонить незавершённое вложение и сохранить исправленное

При приёме файла размер может выясняться по мере чтения. Первая порция уже записана, следующая превышает выбранный лимит: частичный файл не должен стать вложением заказа. Покажем отказ до close и INSERT. Затем принимаем исправленное краткое примечание «3 коробки»: его 16 байт укладываются в тот же лимит. В таблице остаётся ровно одна принятая запись, а черновика там нет.

Путь данных в этом примере
  1. Черновик: первая порция 12 байт → add
  2. Следующие 12 превысят лимит 16 → не добавлять
  3. cancel(writer) → незавершённый BLOB отброшен; INSERT не выполняется
  4. Отдельное принятое примечание → create/add/close → INSERT id=2
  5. commit → только accepted-note.txt с правильным текстом

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

if ($bytesSoFar + strlen($nextChunk) > $limit) {
    if (ibase_blob_cancel($writer) !== true) {
        throw new RuntimeException("Черновик не отменён");
    }
    $writer = null;
    // Не получать ID и не выполнять INSERT для этого файла.
}

cancel получает ещё открытый writer. Это отмена создания одного BLOB, а не кнопка «отменить последнюю строку в базе». После успешного вызова переменную обнуляем, чтобы finally не отменил тот же ресурс второй раз. Учебные 16 байт позволяют увидеть отказ после первой порции; это не рекомендованный производственный лимит. Исправленный текст проходит тот же лимит до создания нового BLOB. Здесь 16 байт UTF-8 — это девять символов «3 коробки», а не шестнадцать букв.

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

НаблюдениеРезультат
Причина отказаРазмер превышает учебный лимит 16 байт
До отказа записано байт12
Незавершённый BLOB отменёнда
Строк после отказа0
Принятый файлaccepted-note.txt
Текст принятой записи3 коробки
Всего принятых записей1
Отклонить незавершённое вложение и сохранить исправленноеСамостоятельный пример
<?php
declare(strict_types=1);
require __DIR__.'/lab.php';
$db=null;$owned=$removed=$rows=$checks=$observations=[];
try {
    $config=f155Config();$db=f155Connect($config);$runtime=f155Runtime($db,$config);

    function createBlob155($context) {
        $call=f155Capture(static fn()=>ibase_blob_create($context));
        if (!is_resource($call['value']) || $call['warning']) {f155Failure('BLOB не создан',$call);}
        return $call['value'];
    }
    function appendBlob155($handle,string $bytes): void {
        $call=f155Capture(static fn()=>ibase_blob_add($handle,$bytes));
        // Successful add has no value (null), not true. Check refusal and diagnostics.
        if ($call['value']===false || $call['warning'] || $call['code']!==0) {f155Failure('Порция BLOB не записана',$call);}
    }
    function finishBlob155(&$handle): string {
        $call=f155Capture(static fn()=>ibase_blob_close($handle));
        if (!is_string($call['value']) || $call['warning']) {f155Failure('BLOB не закрыт',$call);}
        $handle=null;
        return $call['value'];
    }
    function discardBlob155(&$handle): bool {
        $call=f155Capture(static fn()=>ibase_blob_cancel($handle));
        if ($call['value']!==true || $call['warning']) {f155Failure('Незавершённый BLOB не отменён',$call);}
        $handle=null;
        return true;
    }
    function inspectBlob155($context,string $id,bool $keepSmallText=false): array {
        $call=f155Capture(static fn()=>ibase_blob_open($context,$id));
        if (!is_resource($call['value']) || $call['warning']) {f155Failure('Сохранённый BLOB не открыт',$call);}
        $reader=$call['value'];unset($call);
        $bytes=0;$value='';$hash=hash_init('sha256');
        try {
            while (true) {
                $call=f155Capture(static fn()=>ibase_blob_get($reader,8192));
                if (!is_string($call['value']) || $call['warning']) {f155Failure('BLOB не прочитан',$call);}
                $chunk=$call['value'];
                if ($chunk==='') {break;}
                $bytes+=strlen($chunk);hash_update($hash,$chunk);
                if ($keepSmallText) {
                    if ($bytes>4096) {throw new RuntimeException('Учебный текст превышает лимит 4096 байт.');}
                    $value.=$chunk;
                }
            }
        } finally {
            $closed=f155Capture(static fn()=>ibase_blob_close($reader));$reader=null;
            if ($closed['value']===false || $closed['warning']) {f155Failure('Читающий BLOB не закрыт',$closed);}
        }
        return ['bytes'=>$bytes,'sha256'=>hash_final($hash),'text'=>$value];
    }

    $name=f155OwnTable($db,$owned,'accepted','(upload_id INTEGER NOT NULL PRIMARY KEY,file_name VARCHAR(80) NOT NULL,payload BLOB SUB_TYPE BINARY)');
    $q=f155Qualified($name);

    $limit=16;$acceptedBytes=0;$cancelled=false;$reason='';
    $writer=createBlob155($db);
    try {
        foreach (['DRAFT-PART-1','DRAFT-PART-2'] as $chunk) {
            if ($acceptedBytes+strlen($chunk)>$limit) {
                $reason='Размер превышает учебный лимит 16 байт';
                $cancelled=discardBlob155($writer);
                break;
            }
            appendBlob155($writer,$chunk);$acceptedBytes+=strlen($chunk);
        }
        if (!$cancelled) {throw new LogicException('Учебный черновик должен быть отклонён.');}
    } finally {if (is_resource($writer)) {discardBlob155($writer);}}
    // No close/ID/INSERT for the rejected file. Cancel does not roll back other SQL.
    $before=f155Read($db,"SELECT COUNT(*) AS \"found\" FROM $q")[0]['found'];
    $accepted='3 коробки';
    if (strlen($accepted)>$limit) {throw new DomainException('Исправленное примечание тоже превышает лимит.');}
    $writer=createBlob155($db);
    try {
        appendBlob155($writer,$accepted);
        $id=finishBlob155($writer);
        f155Command($db,"INSERT INTO $q(upload_id,file_name,payload) VALUES(?,?,?)",[2,'accepted-note.txt',$id]);
        f155Commit($db);
    } finally {if (is_resource($writer)) {discardBlob155($writer);}}
    $savedRows=f155Read($db,"SELECT upload_id AS \"upload_id\",file_name AS \"file_name\",payload AS \"blob_id\" FROM $q ORDER BY upload_id");
    $saved=inspectBlob155($db,$savedRows[0]['blob_id'],true);
    $rows=[['Причина отказа',$reason],['До отказа записано байт',$acceptedBytes],['Незавершённый BLOB отменён',$cancelled],
        ['Строк после отказа',$before],['Принятый файл',$savedRows[0]['file_name']],['Текст принятой записи',$saved['text']],['Всего принятых записей',count($savedRows)]];
    $checks=['limit_detected'=>$reason!=='','first_chunk_was_written'=>$acceptedBytes===12,'unfinished_blob_cancelled'=>$cancelled,
        'no_rejected_row'=>$before===0,'only_accepted_row'=>count($savedRows)===1&&$savedRows[0]['upload_id']===2,
        'accepted_payload'=>$saved['text']===$accepted,'accepted_within_limit'=>strlen($accepted)<=$limit,'writer_closed'=>$writer===null];
    $observations=['draft_limit_bytes'=>$limit,'draft_written_bytes'=>$acceptedBytes,'cancelled'=>$cancelled,'rows_after_rejection'=>$before,
        'accepted_ids'=>array_column($savedRows,'upload_id'),'accepted_name'=>$savedRows[0]['file_name'],'accepted_text'=>$saved['text']];

} finally {
    if (is_resource($db)) {try {$removed=f155Cleanup($db,$owned);} finally {ibase_close($db);}}
}

f155Report('fb155-cancel-upload','Отклонить незавершённое вложение и сохранить исправленное',$rows,$checks,$observations,$runtime,$owned,$removed);
Ожидаемый результат
Причина отказа — Размер превышает учебный лимит 16 байт
До отказа записано байт — 12
Незавершённый BLOB отменён — да
Строк после отказа — 0
Принятый файл — accepted-note.txt
Текст принятой записи — 3 коробки
Всего принятых записей — 1

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

Фактический HTML-результат · Данные результата JSON.

Ошибки, ограничения и применение

Нельзя сначала закрыть BLOB, сохранить его ID в строке, сделать commit, а затем пытаться удалить вложение через cancel: функция предназначена для незавершённого создания. Для отказа всей операции до commit используется rollback её транзакции. Для удаления уже принятого вложения нужна отдельная бизнес-операция UPDATE/DELETE с проверкой прав и связей. COUNT(*) здесь относится к одной собственной учебной таблице с нулём/одной строкой; в рабочем списке вложений выбирают необходимые строки запросом, а не загружают всю таблицу для PHP-фильтрации.

Измените условие задачи

Что делать, если до обнаружения ошибки программа уже поменяла статус заказа, но ещё не сделала commit?

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

Отменить открытый BLOB, затем rollback общей транзакции изменения статуса и вложения. Один cancel не возвращает статус заказа. Если статус и вложение обязаны сохраняться вместе, объедините их под одной явной транзакцией из предыдущей главы.

Пять операций записи: выбрать нужную карточку

Что требуетсяКарточка
Открыть новый BLOB для записи частями.ibase_blob_create
Дополнить открытый BLOB очередными байтами.ibase_blob_add
Закрыть BLOB; после записи получить ID для строки документа.ibase_blob_close
Перенести открытый файл в новый BLOB и получить ID.ibase_blob_import
Отбросить ещё незавершённый BLOB.ibase_blob_cancel

От сохранённого вложения к его получению

Теперь у сохранения есть проверяемый итог: из базы возвращается исходное примечание, все 90000 двоичных байт дают тот же хеш, импорт возвращает весь invoice.txt, а отменённый черновик не становится вложением. Следующая глава — размер BLOB, чтение порциями, выгрузка в файл и отдельный HTTP-ответ.

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

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

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

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

Исторический PHP 5 / PHP 7 < 7.4; отдельный поддерживаемый FirebirdSQL 3.0.1 · interbase

ibase_blob_create(resource $link_identifier = null): resource|false

Когда содержимое собирается в PHP или приходит порциями, сначала нужен новый BLOB для записи. Результат create — открытый ресурс; строки заказа в таблице пока нет. Полученный writer передаётся add, затем один раз close или cancel.

Аргумент / результатЗначение
link_identifierНеобязательное соединение. Без аргумента исторический API использует последнее открытое соединение. В примерах $db передаётся явно. В проверяемом FirebirdSQL 3.0.1 также распознаётся ресурс явной транзакции; его нужно использовать согласованно для BLOB и INSERT.
resource | falseОткрытый BLOB handle для записи либо отказ. Это ещё не BLOB ID. Ошибка требует немедленной диагностики.
= null в старой сигнатуреЭто документационное обозначение отсутствующего аргумента. Не полагайтесь на явный create(null): выбранная реализация разбирает необязательный ресурс.

Примечание к заказу

$writer = ibase_blob_create($db);
if ($writer === false) { throw new RuntimeException("BLOB не создан"); }
// Затем добавить текст и получить ID через close.

В полном сценарии примечания новый writer получает фразу курьера; после close и INSERT читатель увидит ту же фразу из базы. create не определяет номер заказа, имя файла или MIME-тип — это отдельные данные строки.

Вложение и изменение статуса в одной операции

$tr = ibase_trans(IBASE_WRITE | IBASE_CONCURRENCY | IBASE_NOWAIT, $db);
if ($tr === false) { throw new RuntimeException("Транзакция не начата"); }
$writer = ibase_blob_create($tr);
// add → close → ibase_query($tr, INSERT с ID) → UPDATE статуса через $tr.
// Успех: ibase_commit($tr). Отказ: cancel открытого writer и rollback($tr).

Контекстный вариант уже изученной общей транзакции: новый файл и статус документа должны подтверждаться вместе. Нельзя создать BLOB через default-транзакцию $db, а потом бездумно вставлять его ID через независимый $tr. Этот фрагмент поясняет выбор контекста; полный протокол завершения берётся из главы о транзакциях.

Не оставляйте открытый writer до закрытия соединения и не держите транзакцию между HTTP-запросами. Для готового файла удобнее ibase_blob_import. Для заранее небольшого обычного поля подходит VARCHAR; BLOB выбирают по назначению и ожидаемому объёму, а не ради самого API.

ibase_blob_addДополнить открытый BLOB очередными байтами.

Исторический PHP 5 / PHP 7 < 7.4; отдельный поддерживаемый FirebirdSQL 3.0.1 · interbase

ibase_blob_add(resource $blob_handle, string $data): void; выбранный драйвер также возвращает false при отказе

add дописывает байты в BLOB, открытый через create. Несколько вызовов продолжают один и тот же объект: порядок переданных порций становится порядком содержимого.

ЭлементКонтракт
blob_handleРесурс нового BLOB, открытого для записи. Ресурс чтения из blob_open не подходит.
dataPHP-строка с очередными байтами; это может быть UTF-8-текст или двоичная порция с NUL. Функция не преобразует файл в изображение и не угадывает кодировку.
Успешный результатPHP Manual описывает void: полезного значения нет. В выбранной реализации успешный вызов даёт null; нельзя писать if (!ibase_blob_add(...)) как проверку ошибки.
ОтказВ выбранном драйвере возвращается false и/или возникает диагностическое предупреждение. Снимите его сразу, до другого вызова ibase. Неправильный PHP-тип аргумента также может приводить к ошибке PHP.

Описание из двух частей

ibase_blob_add($writer, "Заказ №155
");
ibase_blob_add($writer, "Оставить коробки у стойки выдачи.");
// После close содержимое: заголовок, перевод строки, инструкция.

Это развитие примечания к заказу: add не вставляет разделитель сама, поэтому перевод строки включён явно. Короткий фрагмент опускает обработку ошибок; в самостоятельной программе appendBlob155 проверяет false, предупреждение и код ошибки.

Порция вложения из файла

$chunk = fread($input, 8192);
if ($chunk === false) { throw new RuntimeException("Не прочитана порция"); }
$call = f155Capture(static fn() => ibase_blob_add($writer, $chunk));
if ($call["value"] === false || $call["warning"] || $call["code"] !== 0) {
    f155Failure("Порция не сохранена", $call);
}

Полный цикл вложения делает 11 вызовов для 90000 байт. Размер порции регулирует работу приложения с памятью; он не ограничивает общий размер BLOB. Источник драйвера 3.0.1 дополнительно разбивает слишком большой аргумент на сегменты не более 65535 байт. Поэтому «у BLOB лимит 64 КБ» — неверный вывод.

После ошибки не закрывайте частичное содержимое как успешное вложение: отмените незавершённый BLOB и при необходимости откатите всю транзакцию. Одно успешное добавление не означает, что следующий INSERT или commit также завершатся успешно.

ibase_blob_closeЗакрыть BLOB; после записи получить ID для строки документа.

Исторический PHP 5 / PHP 7 < 7.4; отдельный поддерживаемый FirebirdSQL 3.0.1 · interbase

ibase_blob_close(resource $blob_handle): mixed

close завершает работу с открытым BLOB. После записи полученный ID нужен, чтобы связать содержимое со строкой документа. После чтения закрытие освобождает открытый канал; оно не удаляет файл из базы.

ЭлементКонтракт
blob_handleОткрытый ресурс из blob_create для записи либо blob_open для чтения. Он закрывается один раз.
Исторический PHP ManualСигнатура mixed: при записи строковый BLOB ID, при чтении true, при ошибке false. Этот вариант описания сохранён как исторический контракт.
FirebirdSQL 3.0.1Выбранная реализация возвращает строковый ID и после записи, и после чтения. Для читателя проверяйте отказ через === false/диагностику, не требуйте === true.
Что не происходитclose не вставляет строку в таблицу и не делает commit транзакции.

Связать новое вложение с записью

$id = ibase_blob_close($writer);
if (!is_string($id)) { throw new RuntimeException("BLOB не завершён"); }
$writer = null;
// Передать $id параметром INSERT в BLOB-колонку.
// После успешного INSERT подтвердить транзакцию.

В примере заказа только после этой границы появляется значение для параметра note_text. Ни сам ресурс writer, ни исходная фраза не являются идентификатором завершённого BLOB. Возвращаемый ID не выводится как полезное содержимое: результатом для читателя служит восстановленное примечание.

Закончить проверочное чтение

try {
    // ibase_blob_get($reader, 8192) и обработка порций.
} finally {
    $closed = ibase_blob_close($reader);
    $reader = null;
    if ($closed === false) { throw new RuntimeException("Чтение не закрыто"); }
}

Так устроена завершающая часть контрольного чтения; подробнее — чтение BLOB по частям. Не проверяйте только is_resource после close: PHP-ссылка на ресурс и фактически открытый дескриптор не одно и то же. Обнуляйте собственную переменную и не вызывайте close повторно.

ID, полученный при создании, может отличаться от постоянного ID, который возвращается после вставки в строку. После commit мы заново выбираем документ и открываем BLOB по значению из SELECT; сравниваем содержимое, а не эти два ID. Если INSERT не состоялся, ID сам по себе не создаёт приложение-вложение: нужна обработка отказа всей транзакции. Если BLOB уже закрыт, cancel больше не является корректным продолжением обычной цепочки создания. Отмена неподтверждённой бизнес-операции делается через rollback.

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

Исторический PHP 5 / PHP 7 < 7.4; отдельный поддерживаемый FirebirdSQL 3.0.1 · interbase

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

Когда данные уже доступны через файловый ресурс, import объединяет создание BLOB, чтение потока, добавление байтов и закрытие BLOB. На выходе получается ID; запись с именем файла и номером документа приложение создаёт отдельно.

Форма / элементКонтракт
ibase_blob_import($db, $file_handle)Явное соединение и открытый PHP-файл. В выбранном драйвере вместо соединения также распознаётся ресурс транзакции.
ibase_blob_import($file_handle)Сокращённая форма без соединения использует последнее открытое соединение. При нескольких подключениях надёжнее явная форма.
file_handleРесурс из fopen, не строка пути. Для точного сохранения байтов открывайте локальный файл в rb. Импорт читает от текущей позиции до конца; перемотку и fclose выполняет вызывающий код.
РезультатСтроковый BLOB ID при успехе, false при ошибке. На странице PHP Manual сигнатура сокращена до string, но её раздел Return Values отдельно указывает false.

Файл накладной рядом с программой

$input = fopen(__DIR__ . "/invoice.txt", "rb");
if ($input === false) { throw new RuntimeException("Накладная не найдена"); }
try { $id = ibase_blob_import($db, $input); }
finally { fclose($input); }
if ($id === false) { throw new RuntimeException("Импорт не выполнен"); }

Самостоятельный пример накладной содержит сам invoice.txt, SQL создания таблицы, INSERT и read-back. Вы увидите точный текст файла из базы. Список операций после import обязателен: ID нужно сохранить в нужной строке, а затем подтвердить транзакцию.

Импорт после проверки заголовка

$input = fopen($validatedLocalPath, "rb");
if ($input === false) { throw new RuntimeException("Файл недоступен"); }
try {
    $header = fread($input, 16); // Проверить заголовок по правилам своего формата.
    if ($header === false || rewind($input) !== true) {
        throw new RuntimeException("Файл нельзя повторно прочитать целиком");
    }
    $id = ibase_blob_import($db, $input);
} finally { fclose($input); }
// Проверить $id и продолжить INSERT/commit.

Контекстный вариант для уже проверенного локального пути: перемотка возвращает начало, поэтому первые 16 байт не теряются. Переменная header сама по себе ничего не валидирует — проверку формата нужно добавить до сохранения. Для загрузки из HTTP сначала проверяют upload error, размер, принадлежность временного файла и право прикрепить его к документу. Сетевой URL или произвольный путь посетителя не является разрешением открыть файл.

Функция закрывает BLOB, но не PHP-файл. Поэтому передавать её результат в blob_close ошибочно: это уже строка ID. Для преобразования/ограничения каждой порции используйте create + add вместо непрозрачного импорта. При важных вложениях сравнивайте ожидаемый размер/хеш с фактическим чтением; наш сценарий так и делает.

ibase_blob_cancelОтбросить ещё незавершённый BLOB.

Исторический PHP 5 / PHP 7 < 7.4; отдельный поддерживаемый FirebirdSQL 3.0.1 · interbase

ibase_blob_cancel(resource $blob_handle): bool

cancel нужен, когда часть данных уже записана в новый BLOB, но принимать вложение нельзя. Он отбрасывает ещё незавершённый BLOB, открытый через create. Это локальная отмена создания содержимого, а не откат произвольного SQL.

ЭлементКонтракт
blob_handleЕщё не закрытый BLOB handle из blob_create. После blob_close обычный маршрут отмены через cancel уже недопустим.
booltrue при успешной отмене, false при отказе; диагностику снимайте немедленно. Нельзя считать отказ успешной очисткой.
Связь с транзакциейОтмена одного BLOB не отменяет UPDATE статуса, не удаляет ранее подтверждённую строку и не заменяет rollback всей операции.

Превышение лимита при чтении очередной порции

if ($written + strlen($chunk) > $limit) {
    if (ibase_blob_cancel($writer) !== true) {
        throw new RuntimeException("Незавершённый BLOB не отменён");
    }
    $writer = null;
    // Не выполнять INSERT отклонённого вложения.
}

В полном примере отказа первые 12 байт уже записаны, вторые 12 превысят лимит 16. После cancel таблица пуста; отдельное принятое примечание затем даёт ровно одну правильную запись. Такой результат объясняет пользу отмены, а не только её логическое значение true.

Источник перестал читаться посреди загрузки

$writer = ibase_blob_create($db);
if ($writer === false) { throw new RuntimeException("BLOB не создан"); }
try {
    // Читать файл, проверять fread и добавлять порции.
    // Только после полного успеха: close, $writer = null, INSERT, commit.
} finally {
    if (is_resource($writer)) {
        $cancelled = ibase_blob_cancel($writer);
        $writer = null;
        if ($cancelled !== true) { throw new RuntimeException("Частичный BLOB не отменён"); }
    }
}

Это шаблон владения writer для записи файла порциями. Если чтение бросило исключение до close, finally завершает незавершённый BLOB. Если close успешно выполнился, переменная уже null и повторной отмены нет. SQL-изменения этой же операции при отказе всё равно должен откатить владелец транзакции.

Не используйте cancel для удаления принятого файла из истории документа. Для этого проектируют отдельный запрос изменения ссылки/строки, политику хранения и права доступа. Не оставляйте открытые BLOB до неявного завершения PHP в надежде, что все бизнес-границы будут восстановлены автоматически.

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

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

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

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

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