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

Дерево руководства
Глава 62 · После первых запросов и транзакций Firebird
Firebird BLOB: сохранить примечание, загрузить файл и отменить незавершённую запись
Примечание с read-back, двоичное вложение по частям, импорт накладной и отмена черновика: четыре реальных сценария и пять полных карточек.
Зачем хранить вложение и чем ID отличается от содержимого
У заказа есть примечание, у накладной — исходный файл, у оборудования — двоичный пакет. Их удобно связать с конкретной строкой базы, чтобы получать вложение по документу. BLOB хранит последовательность байтов, а запись содержит ссылку на эти данные. В этой главе вы сохраните и прочитаете русское примечание, перенесёте 90000 двоичных байт, импортируете готовую накладную и отклоните частичную загрузку.
- Содержимое: текст или байты файла
- writer: ресурс для дописывания нового BLOB
- close(writer): строковый BLOB ID
- INSERT документа с ID + commit
- 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 необязателен.
- PHP: «Оставить коробки у стойки выдачи.»
- create(db) → writer: открыта запись нового содержимого
- add(writer, note) → байты внутри BLOB
- close(writer) → id; INSERT(155, id) → строка заказа
- 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-8 | 61 |
| В строке хранится BLOB ID | да |
| Прочитанный текст совпал | да |
<?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 вызовов добавления, а не нечитаемую бинарную строку.
- Временный учебный файл: 9000 записей по 10 байт
- fread(input, 8192) → очередная порция PHP-строки
- add(writer, chunk) повторяется 11 раз
- close → ID; INSERT имени и ID; commit
- 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 |
| Байт прочитано из BLOB | 90000 |
| SHA-256 | fe792f8cbc151d3279a058045af4862f8b74f0ba8d5a31d01f311b0b89b7e789 |
| Байты и SHA-256 совпали | да |
<?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. В отчёте будет текст накладной, извлечённый из базы, включая русские буквы и переносы строк.
- invoice.txt рядом с example.php → fopen(path, rb)
- import(db, input) читает от текущей позиции до EOF
- Возвращается строковый BLOB ID; input всё ещё принадлежит PHP
- INSERT(155, invoice.txt, ID) → commit
- 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 байт укладываются в тот же лимит. В таблице остаётся ровно одна принятая запись, а черновика там нет.
- Черновик: первая порция 12 байт → add
- Следующие 12 превысят лимит 16 → не добавлять
- cancel(writer) → незавершённый BLOB отброшен; INSERT не выполняется
- Отдельное принятое примечание → create/add/close → INSERT id=2
- 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 не подходит. |
| data | PHP-строка с очередными байтами; это может быть 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): mixedclose завершает работу с открытым 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): boolcancel нужен, когда часть данных уже записана в новый BLOB, но принимать вложение нельзя. Он отбрасывает ещё незавершённый BLOB, открытый через create. Это локальная отмена создания содержимого, а не откат произвольного SQL.
| Элемент | Контракт |
|---|---|
| blob_handle | Ещё не закрытый BLOB handle из blob_create. После blob_close обычный маршрут отмены через cancel уже недопустим. |
| bool | true при успешной отмене, 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. Примеры и объяснения написаны для этого руководства; источники и обсуждения доступны в каждой главе.


