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

Дерево руководства
Глава 77 · После SQL, BLOB и базовой работы с файлами
Firebird fbird_blob_*: список вложений, ограниченный текст, CSV и скачивание
Четыре задачи чтения BLOB в существующем fbird-коде: список вложений, ограниченный текст, точный CSV и настоящий HTTP-ответ.
От записи документа до понятного вложения
В старом PHP-сервисе названия fbird_blob_info и fbird_blob_get встречаются так же, как ibase_blob_info и ibase_blob_get. В этой главе разберём их на задачах интерфейса и передачи файлов: получить список вложений, прочитать допустимую заметку, вернуть исходный CSV другой системе и скачать акт заказа. Результат должен быть полезен человеку, а не ограничиваться строкой BLOB ID или числом успешно выполненных вызовов.
После запуска будут видны четыре вложения с разными состояниями, текст заявки 42, товары CAB-01 и CLP-02 в сохранённом CSV и акт «Кабель: 3 шт.» в HTTP-ответе. По этим данным можно проверить, что произошло с содержимым. Полные программы создают свою таблицу внутри отдельной лабораторной базы, а затем удаляют только её; файлы результата остаются.
- Заказ 42 → нужная строка SQL
- FILE_DATA содержит BLOB ID, а не имя или тело файла
- info → выбрать действие по размеру
- open + get → переменная либо локальный файл
- fbird_blob_echo → тело отдельного ответа для скачивания
Представьте библиотечный каталог: в карточке есть ссылка на отдельный большой документ. BLOB ID похож на такую ссылку, но это лишь модель. ID принадлежит базе и транзакционному контексту, не заменяет URL и не даёт посетителю права доступа. Ресурс open — уже открытое чтение; строка от get — конкретные байты. Эти три значения нельзя взаимозаменять.
Перед этой главой нужны создание и сохранение BLOB через fbird_*, параметры SQL и получение строк и базовое чтение BLOB через ibase_*. Текущий механизм объяснён здесь самостоятельно; ссылки помогают вернуться к подготовке. Установка расширения и различие CLI/web описаны в окружении Firebird.
| Потребность | Получаемый результат | Раздел |
|---|---|---|
| Список вложений | 4 имени, 3 существующих файла, 156 байт; пустой файл отличается от NULL | Список без чтения тела |
| Предпросмотр заметки | Заявка 42 показана; большой текст заявки 43 пропущен по лимиту | Ограниченный UTF-8 |
| Передача исходного CSV | Файл 67 байт, 2 товара и сохранённое поле «Кабель; 2 м» | Побайтовая выгрузка |
| Скачать акт | Отдельный HTTP-ответ с именем receipt-42.txt и текстом акта | Настоящее скачивание |
Составить список вложений: имя, размер и доступное действие
Менеджеру нужен список файлов заказа с понятными действиями. Файл draft.txt существует, хотя он пуст; pending.pdf пока не приложен. Вместо загрузки всех файлов в память запросим нужные поля и метаданные только существующих BLOB. Результат — четыре конкретные строки списка, три доступных вложения и их общий размер 156 байт.
- SELECT четырёх строк → имя + BLOB ID либо SQL NULL
- receipt-42.txt → info.length = 28 → Скачать
- draft.txt → length = 0, isnull = false → Скачать пустой файл
- pending.pdf → SQL NULL → Файл ещё не приложен
- report.bin → 128 байт; тело для списка не читается
Самый короткий вызов
$info = fbird_blob_info($db, $blobId);
if ($info === false) { throw new RuntimeException('Размер неизвестен'); }
echo $info['length'], ' байт'; // Для receipt-42.txt: 28 байт.$blobId — строковый идентификатор из SELECT, а $db — соединение, которому он принадлежит. Результат info — массив: квадратные скобки выбирают значение по имени length. Оно измеряется в байтах. Перед вызовом отдельно обработайте SQL NULL; нулевая длина у существующего BLOB обозначает пустой файл. Полная программа также строит текст действия и итоговый размер.
Что получится
| Наблюдение | Результат |
|---|---|
| receipt-42.txt | 28 байт; Скачать |
| draft.txt | 0 байт; Скачать пустой файл |
| pending.pdf | нет BLOB; Файл ещё не приложен |
| report.bin | 128 байт; Скачать |
| Доступных вложений | 3 |
| Общий размер, байт | 156 |
| Вызовов чтения тела при построении списка | 0 |
Полная программа, файлы и архив для запуска
<?php
declare(strict_types=1);
require __DIR__.'/lab.php';
$db=null;$owned=$removed=$rows=$checks=$observations=[];
try {
$config=f162Config();$db=f162Connect($config);$runtime=f162Runtime($db,$config);
// The preceding writing chapter explains fixture creation; the lesson below teaches reading.
function f162FixtureBlob($db,array $pieces): string {
$handle=fbird_blob_create($db);
if ($handle===false) {f162Failure('Учебный BLOB не создан');}
try {
foreach($pieces as $piece) {
$call=f162Capture(static fn()=>fbird_blob_add($handle,$piece));
if($call['value']===false || $call['warning'])f162Failure('Учебные байты не записаны',$call);
}
$id=fbird_blob_close($handle);
if(!is_string($id))f162Failure('Запись BLOB не закрыта');
$handle=null;return $id;
}finally{if(is_resource($handle)){fbird_blob_cancel($handle);$handle=null;}}
}
$name=f162OwnTable($db,$owned,'listing','(ID INTEGER NOT NULL PRIMARY KEY, FILE_NAME VARCHAR(40) NOT NULL, FILE_DATA BLOB SUB_TYPE BINARY)');
$q=f162Qualified($name);
$receipt=f162FixtureBlob($db,["Заказ 42: принят\n"]);
$empty=f162FixtureBlob($db,[]);
$binary=f162FixtureBlob($db,[str_repeat("\x00\xFF",64)]);
foreach([[1,'receipt-42.txt',$receipt],[2,'draft.txt',$empty],[3,'pending.pdf',null],[4,'report.bin',$binary]] as $entry) {
f162Command($db,"INSERT INTO $q (ID,FILE_NAME,FILE_DATA) VALUES (?,?,?)",$entry);
}
f162Commit($db);
$records=f162Read($db,"SELECT ID,FILE_NAME,FILE_DATA FROM $q ORDER BY ID");
$listing=[];$total=0;$downloadable=0;$infoCalls=0;$keysAgree=true;
foreach($records as $record) {
if($record['FILE_DATA']===null) {
$listing[]=['name'=>$record['FILE_NAME'],'bytes'=>null,'action'=>'Файл ещё не приложен'];
continue;
}
$info=fbird_blob_info($db,$record['FILE_DATA']);++$infoCalls;
if($info===false)f162Failure('Метаданные вложения не прочитаны');
$keysAgree=$keysAgree && $info[0]===$info['length'] && $info[1]===$info['numseg']
&& $info[2]===$info['maxseg'] && $info[3]===$info['stream'] && $info[4]===$info['isnull'];
if($info['isnull'])throw new RuntimeException('Ожидался сохранённый BLOB');
$listing[]=['name'=>$record['FILE_NAME'],'bytes'=>$info['length'],'action'=>$info['length']===0?'Скачать пустой файл':'Скачать'];
$total+=$info['length'];++$downloadable;
}
$expected=[['name'=>'receipt-42.txt','bytes'=>28,'action'=>'Скачать'],['name'=>'draft.txt','bytes'=>0,'action'=>'Скачать пустой файл'],
['name'=>'pending.pdf','bytes'=>null,'action'=>'Файл ещё не приложен'],['name'=>'report.bin','bytes'=>128,'action'=>'Скачать']];
$rows=[];
foreach($listing as $item)$rows[]=[$item['name'],($item['bytes']===null?'нет BLOB':$item['bytes'].' байт').'; '.$item['action']];
$rows[]=['Доступных вложений',$downloadable];$rows[]=['Общий размер, байт',$total];
$rows[]=['Вызовов чтения тела при построении списка',0];
$checks=['visible_listing'=>$listing===$expected,'null_not_empty'=>$listing[1]['bytes']===0&&$listing[2]['bytes']===null,
'three_existing_blobs'=>$infoCalls===3&&$downloadable===3,'aggregate_size'=>$total===156,
'metadata_alias_keys'=>$keysAgree,'blob_identifier_not_body'=>is_string($records[0]['FILE_DATA'])&&$records[0]['FILE_DATA']!=="Заказ 42: принят\n"];
$observations=['listing'=>$listing,'existing_blobs'=>$downloadable,'total_bytes'=>$total,'metadata_calls'=>$infoCalls,
'body_read_calls_during_listing'=>0,'raw_blob_ids_published'=>false,'counting_scope'=>'This count excludes preparation writes and concerns only the listing loop.'];
} finally {
if (function_exists('f162HttpChildrenStopped') && !f162HttpChildrenStopped()) {
throw new RuntimeException('Дочерний HTTP-процесс ещё работает: учебная таблица сохранена для точного восстановления.');
}
if (is_resource($db)) {try {$removed=f162Cleanup($db,$owned);} finally {ibase_close($db);}}
}
f162Report('fb162-attachment-list','Составить список вложений: имя, размер и доступное действие',$rows,$checks,$observations,$runtime,$owned,$removed);
Ожидаемый результат
receipt-42.txt — 28 байт; Скачать
draft.txt — 0 байт; Скачать пустой файл
pending.pdf — нет BLOB; Файл ещё не приложен
report.bin — 128 байт; Скачать
Доступных вложений — 3
Общий размер, байт — 156
Вызовов чтения тела при построении списка — 0
Распакуйте все файлы и запустите example.php по README.txt. Исходник ниже включает подготовку искусственного содержимого, чтение, проверки и finally. lab.php ограничивает работу отдельной базой и собственной FB162-таблицей. result.html показывает фактические строки, result.json хранит наблюдения, metrics.json — проверки и очистку. Все открытые чтения завершаются до удаления таблицы. Для HTTP-проекта README дополнительно описывает единственный временный локальный PHP-сервер; он запускается и останавливается автоматически.
Открыть фактический результат · Открыть JSON с данными.
Ограничения и частые ошибки
Число 0 в отчёте относится к вызовам чтения содержимого в цикле списка, а не к отсутствию сетевых обращений вообще: info тоже обращается к базе и открывает BLOB для метаданных. Для тысяч файлов используйте ограниченную SQL-страницу; проверенный размер можно сохранять рядом с вложением при записи. Metadata не угадывает MIME, кодировку и права посетителя. Имя файла хранится в отдельном столбце; не пытайтесь получить его из BLOB ID.
В результате LIST разработчик увидел шестнадцатеричные значения вместо ожидаемых имён и сначала заподозрил кодировку. Разобранное обсуждение. Проверяем тип: это BLOB ID, а не уже прочитанное содержимое. Список запрашивает metadata, предпросмотр — ограниченные байты. CAST в маленький VARCHAR не используется как универсальное решение для любого объёма.
Проверьте понимание
Можно ли скрыть кнопку скачивания при length = 0, считая такой файл отсутствующим?
Разбор
Технически это существующее вложение. В примере оно скачивается как пустой файл. Если продукт запрещает пустые вложения, запрет относится к правилу загрузки, а отсутствие по-прежнему определяется через SQL NULL.
Показать маленькую заметку целиком, а большой текст направить в скачивание
Карточка заказа должна показать короткую заметку без потери русских букв и не забрать в память большой документ. Для заявки 42 получим «Проверить <кабель> & разъём.». Для заявки 43 примем другое решение до чтения тела: отдельное скачивание. Лимит 96 байт нарочно мал, чтобы обе ветви были видны в одном запуске.
- Заявка 42 → metadata 65 байт ≤ 96 → открыть чтение
- get(7) → байты следующих частей; граница может попасть внутрь буквы
- Собрать не более 96 байт; пустая строка завершает цикл
- Проверить целый UTF-8 → экранировать текст для HTML
- Заявка 43 → metadata больше 96 → не открывать тело
Самый короткий вызов
$handle = fbird_blob_open($db, $blobId);
if ($handle === false) { throw new RuntimeException('Заметка не открыта'); }
try {
$part = fbird_blob_get($handle, 7);
if ($part === false) { throw new RuntimeException('Чтение не удалось'); }
// $part содержит максимум 7 байт, а не обязательно 7 целых букв.
} finally {
if (fbird_blob_close($handle) === false) { throw new RuntimeException('Чтение не закрыто'); }
$handle = null;
}open превращает ID в ресурс с текущей позицией чтения. get возвращает очередную строку байтов и сдвигает эту позицию. В полном цикле false проверяется отдельно от пустой строки: это ошибка, а не конец. Оператор .= дописывает порцию к тексту. preg_match с //u здесь только проверяет корректность собранного UTF-8; htmlspecialchars превращает угловые скобки и амперсанд в безопасное текстовое представление для HTML.
Что получится
| Наблюдение | Результат |
|---|---|
| Режим заявки 42 | Предпросмотр |
| Текст заявки 42 | Заказ #42 Проверить <кабель> & разъём. |
| Текст для HTML | Заказ #42 Проверить <кабель> & разъём. |
| Прочитано байт | 65 |
| Предел предпросмотра, байт | 96 |
| Запрошено байт за один шаг | 7 |
| Непустых порций | 10 |
| Режим заявки 43 | Отдельное скачивание |
| BLOB большой заметки открывался для чтения | нет |
Полная программа, файлы и архив для запуска
<?php
declare(strict_types=1);
require __DIR__.'/lab.php';
$db=null;$owned=$removed=$rows=$checks=$observations=[];
try {
$config=f162Config();$db=f162Connect($config);$runtime=f162Runtime($db,$config);
// The preceding writing chapter explains fixture creation; the lesson below teaches reading.
function f162FixtureBlob($db,array $pieces): string {
$handle=fbird_blob_create($db);
if ($handle===false) {f162Failure('Учебный BLOB не создан');}
try {
foreach($pieces as $piece) {
$call=f162Capture(static fn()=>fbird_blob_add($handle,$piece));
if($call['value']===false || $call['warning'])f162Failure('Учебные байты не записаны',$call);
}
$id=fbird_blob_close($handle);
if(!is_string($id))f162Failure('Запись BLOB не закрыта');
$handle=null;return $id;
}finally{if(is_resource($handle)){fbird_blob_cancel($handle);$handle=null;}}
}
$name=f162OwnTable($db,$owned,'preview','(ID INTEGER NOT NULL PRIMARY KEY, NOTE_TEXT BLOB SUB_TYPE TEXT CHARACTER SET UTF8)');
$q=f162Qualified($name);
$expected="Заказ #42\nПроверить <кабель> & разъём.\n";
$smallId=f162FixtureBlob($db,[$expected]);
$largeId=f162FixtureBlob($db,[str_repeat('Подробности заказа. ',20)]);
f162Command($db,"INSERT INTO $q (ID,NOTE_TEXT) VALUES (?,?)",[42,$smallId]);
f162Command($db,"INSERT INTO $q (ID,NOTE_TEXT) VALUES (?,?)",[43,$largeId]);f162Commit($db);
$records=f162Read($db,"SELECT ID,NOTE_TEXT FROM $q ORDER BY ID");
$limit=96;$chunkBytes=7;$previews=[];$opened=0;$chunks=0;$largeSkipped=false;
foreach($records as $record) {
if($record['NOTE_TEXT']===null)throw new RuntimeException('В учебной заявке ожидается заметка');
$info=fbird_blob_info($db,$record['NOTE_TEXT']);
if($info===false)f162Failure('Размер заметки неизвестен');
if($info['length']>$limit) {
$previews[]=['id'=>$record['ID'],'mode'=>'Отдельное скачивание','text'=>null];$largeSkipped=true;continue;
}
$handle=fbird_blob_open($db,$record['NOTE_TEXT']);
if($handle===false)f162Failure('Заметка не открыта');
++$opened;$value='';
try {
while(true) {
$part=fbird_blob_get($handle,$chunkBytes);
if($part===false)f162Failure('Порция заметки не прочитана');
if($part==='')break;
if(strlen($value)+strlen($part)>$limit)throw new RuntimeException('Превышен лимит собранного текста');
$value.=$part;++$chunks;
}
}finally{if(fbird_blob_close($handle)===false)f162Failure('Чтение не закрыто');$handle=null;}
if(preg_match('//u',$value)!==1)throw new RuntimeException('Заметка не является корректным UTF-8');
$previews[]=['id'=>$record['ID'],'mode'=>'Предпросмотр','text'=>$value];
}
$note=$previews[0]['text'];$escaped=htmlspecialchars($note,ENT_QUOTES|ENT_SUBSTITUTE,'UTF-8');
$rows=[['Режим заявки 42',$previews[0]['mode']],['Текст заявки 42',$note],['Текст для HTML',$escaped],
['Прочитано байт',strlen($note)],['Предел предпросмотра, байт',$limit],['Запрошено байт за один шаг',$chunkBytes],
['Непустых порций',$chunks],['Режим заявки 43',$previews[1]['mode']],['BLOB большой заметки открывался для чтения',false]];
$checks=['small_exact_text'=>$note===$expected,'small_bytes'=>strlen($note)===65,'bounded_memory_policy'=>strlen($note)<=$limit,
'chunked_alias_read'=>$chunks===10,'large_not_opened'=>$largeSkipped&&$opened===1&&$previews[1]['text']===null,
'utf8_reassembled'=>preg_match('//u',$note)===1,'escaped_only_after_reassembly'=>str_contains($escaped,'<кабель> & разъём.'),
'reader_released'=>$handle===null];
$observations=['previews'=>$previews,'text_bytes'=>strlen($note),'limit_bytes'=>$limit,'chunk_bytes'=>$chunkBytes,'nonempty_chunks'=>$chunks,
'opened_blob_count'=>$opened,'large_blob_body_read'=>false,'html_text'=>$escaped,'encoding'=>'UTF8','eof_value'=>''];
} finally {
if (function_exists('f162HttpChildrenStopped') && !f162HttpChildrenStopped()) {
throw new RuntimeException('Дочерний HTTP-процесс ещё работает: учебная таблица сохранена для точного восстановления.');
}
if (is_resource($db)) {try {$removed=f162Cleanup($db,$owned);} finally {ibase_close($db);}}
}
f162Report('fb162-bounded-text','Показать маленькую заметку целиком, а большой текст направить в скачивание',$rows,$checks,$observations,$runtime,$owned,$removed);
Ожидаемый результат
Режим заявки 42 — Предпросмотр
Текст заявки 42 — Заказ #42
Проверить <кабель> & разъём.
Текст для HTML — Заказ #42
Проверить <кабель> & разъём.
Прочитано байт — 65
Предел предпросмотра, байт — 96
Запрошено байт за один шаг — 7
Непустых порций — 10
Режим заявки 43 — Отдельное скачивание
BLOB большой заметки открывался для чтения — нет
Распакуйте все файлы и запустите example.php по README.txt. Исходник ниже включает подготовку искусственного содержимого, чтение, проверки и finally. lab.php ограничивает работу отдельной базой и собственной FB162-таблицей. result.html показывает фактические строки, result.json хранит наблюдения, metrics.json — проверки и очистку. Все открытые чтения завершаются до удаления таблицы. Для HTTP-проекта README дополнительно описывает единственный временный локальный PHP-сервер; он запускается и останавливается автоматически.
Открыть фактический результат · Открыть JSON с данными.
Ограничения и частые ошибки
В памяти всё равно хранится целая маленькая заметка: порции не делают бесконечное .= безопасным. Ограничены и предварительный размер, и фактически накопленные байты. Не экранируйте каждую семибайтовую порцию отдельно: разрезанная буква может превратиться в символ замены. Здесь кодировка заранее известна как UTF8; неизвестные старые байты требуют отдельной диагностики. IBASE_TEXT при fetch удобен для небольших известных BLOB, но загрузит содержимое сразу и не выполнит эту политику выбора.
Проверьте понимание
Как показать первые 40 букв большого BLOB: достаточно ли get($handle, 40)?
Разбор
Нет: аргумент измеряется в байтах. Такой вызов может обрезать UTF-8 посередине символа. Нужен отдельный алгоритм ограниченного чтения с сохранением незавершённой последовательности и подсчётом символов; этот пример вместо рискованного обрезания предлагает скачивание.
Выгрузить сохранённый CSV и проверить, что его поля не потерялись
В базе лежит исходный CSV, который нужно передать другой системе без изменения разделителей и переводов строк. Выгрузим его в export.csv, затем прочитаем стандартным CSV-парсером. Человек увидит заголовок и два товара, включая название «Кабель; 2 м» и нулевой остаток хомута, а не только сообщение «файл записан».
Самый короткий вызов
$part = fbird_blob_get($handle, 13);
if ($part === false) { throw new RuntimeException('Выгрузка прервалась'); }
// Передайте эти байты в fwrite, не разбирая CSV на границах порций.
// get вернёт следующую часть при следующем вызове.Содержимое читается в $part. fbird_blob_echo здесь не подходит: он выводит байты в ответ, а возвращает только признак успеха. fopen(..., "wb") открывает локальный файл для точной побайтовой записи. Внутренний цикл учитывает короткую запись fwrite. После выгрузки fgetcsv с разделителем ;, кавычкой " и явным пустым escape разбирает поля; explode(";", ...) ошибочно разрежет название товара.
Что получится
| Наблюдение | Результат |
|---|---|
| Файл | export.csv |
| Заголовок | sku | title | qty |
| Товар 1 | CAB-01 | Кабель; 2 м | 3 |
| Товар 2 | CLP-02 | Хомут | 0 |
| Байт в выгрузке | 67 |
| Непустых порций | 6 |
| SHA256 файла | a0e0108b39fabbd340e0d020acfa25feabfbd6071f89256a181eaa2bd73a9e75 |
| Кавычки сохранили точку с запятой внутри названия | да |
Полная программа, файлы и архив для запуска
<?php
declare(strict_types=1);
require __DIR__.'/lab.php';
$db=null;$owned=$removed=$rows=$checks=$observations=[];
try {
$config=f162Config();$db=f162Connect($config);$runtime=f162Runtime($db,$config);
// The preceding writing chapter explains fixture creation; the lesson below teaches reading.
function f162FixtureBlob($db,array $pieces): string {
$handle=fbird_blob_create($db);
if ($handle===false) {f162Failure('Учебный BLOB не создан');}
try {
foreach($pieces as $piece) {
$call=f162Capture(static fn()=>fbird_blob_add($handle,$piece));
if($call['value']===false || $call['warning'])f162Failure('Учебные байты не записаны',$call);
}
$id=fbird_blob_close($handle);
if(!is_string($id))f162Failure('Запись BLOB не закрыта');
$handle=null;return $id;
}finally{if(is_resource($handle)){fbird_blob_cancel($handle);$handle=null;}}
}
$name=f162OwnTable($db,$owned,'csv_export','(ID INTEGER NOT NULL PRIMARY KEY, FILE_DATA BLOB SUB_TYPE BINARY)');
$q=f162Qualified($name);
$expected="sku;title;qty\r\nCAB-01;\"Кабель; 2 м\";3\r\nCLP-02;Хомут;0\r\n";
$expectedHash=hash('sha256',$expected);
$blobId=f162FixtureBlob($db,str_split($expected,11));
f162Command($db,"INSERT INTO $q (ID,FILE_DATA) VALUES (?,?)",[1,$blobId]);f162Commit($db);
$record=f162Read($db,"SELECT FILE_DATA FROM $q WHERE ID=?",[1])[0];
$info=fbird_blob_info($db,$record['FILE_DATA']);if($info===false)f162Failure('Размер CSV неизвестен');
if($info['length']>1048576)throw new RuntimeException('Учебный CSV слишком велик');
$handle=fbird_blob_open($db,$record['FILE_DATA']);if($handle===false)f162Failure('CSV не открыт');
$path=__DIR__.'/export.csv';$out=null;$written=0;$chunks=0;$complete=false;
try {
$out=fopen($path,'wb');if($out===false)throw new RuntimeException('Файл CSV не создан');
while(true) {
$part=fbird_blob_get($handle,13);
if($part===false)f162Failure('Порция CSV не прочитана');
if($part==='')break;
++$chunks;$offset=0;$length=strlen($part);
if($written+$length>1048576)throw new RuntimeException('Превышен предел выгрузки');
while($offset<$length) {
$n=fwrite($out,substr($part,$offset));
if($n===false||$n===0)throw new RuntimeException('CSV записан не полностью');
$offset+=$n;$written+=$n;
}
}
$complete=$written===$info['length'];
if(!$complete)throw new RuntimeException('Число выгруженных байт не совпало');
}finally{
if(is_resource($out)){fclose($out);$out=null;}
if(fbird_blob_close($handle)===false)f162Failure('Ресурс CSV не закрыт');$handle=null;
if(!$complete&&is_file($path))unlink($path);
}
$actualHash=hash_file('sha256',$path);
if($actualHash!==$expectedHash){unlink($path);throw new RuntimeException('Контрольная сумма CSV не совпала');}
$input=fopen($path,'rb');if($input===false)throw new RuntimeException('Выгрузка не открыта для разбора');
$parsed=[];
try{while(($row=fgetcsv($input,0,';','"',''))!==false)$parsed[]=$row;}
finally{fclose($input);$input=null;}
$expectedRows=[['sku','title','qty'],['CAB-01','Кабель; 2 м','3'],['CLP-02','Хомут','0']];
$rows=[['Файл','export.csv'],['Заголовок',implode(' | ',$parsed[0])],['Товар 1',implode(' | ',$parsed[1])],
['Товар 2',implode(' | ',$parsed[2])],['Байт в выгрузке',$written],['Непустых порций',$chunks],
['SHA256 файла',$actualHash],['Кавычки сохранили точку с запятой внутри названия',$parsed[1][1]==='Кабель; 2 м']];
$checks=['exact_export_hash'=>$actualHash===$expectedHash,'export_size'=>$written===67,
'multiple_bounded_chunks'=>$chunks===6,'csv_rows'=>$parsed===$expectedRows,
'quoted_field_preserved'=>$parsed[1][1]==='Кабель; 2 м','zero_is_data'=>$parsed[2][2]==='0',
'resources_closed'=>$handle===null&&$out===null&&$input===null];
$observations=['export_file'=>'export.csv','bytes'=>$written,'sha256'=>$actualHash,'expected_sha256'=>$expectedHash,
'chunk_bytes'=>13,'chunks'=>$chunks,'parsed_csv'=>$parsed,'encoding'=>'UTF-8','bom'=>false,'newline'=>'CRLF',
'partial_writes_handled'=>true,'body_assembled_during_export'=>false,'fixture_preparation_in_memory'=>true];
} finally {
if (function_exists('f162HttpChildrenStopped') && !f162HttpChildrenStopped()) {
throw new RuntimeException('Дочерний HTTP-процесс ещё работает: учебная таблица сохранена для точного восстановления.');
}
if (is_resource($db)) {try {$removed=f162Cleanup($db,$owned);} finally {ibase_close($db);}}
}
f162Report('fb162-export-csv','Выгрузить сохранённый CSV и проверить, что его поля не потерялись',$rows,$checks,$observations,$runtime,$owned,$removed);
Ожидаемый результат
Файл — export.csv
Заголовок — sku | title | qty
Товар 1 — CAB-01 | Кабель; 2 м | 3
Товар 2 — CLP-02 | Хомут | 0
Байт в выгрузке — 67
Непустых порций — 6
SHA256 файла — a0e0108b39fabbd340e0d020acfa25feabfbd6071f89256a181eaa2bd73a9e75
Кавычки сохранили точку с запятой внутри названия — да
Распакуйте все файлы и запустите example.php по README.txt. Исходник ниже включает подготовку искусственного содержимого, чтение, проверки и finally. lab.php ограничивает работу отдельной базой и собственной FB162-таблицей. result.html показывает фактические строки, result.json хранит наблюдения, metrics.json — проверки и очистку. Все открытые чтения завершаются до удаления таблицы. Для HTTP-проекта README дополнительно описывает единственный временный локальный PHP-сервер; он запускается и останавливается автоматически.
Открыть фактический результат · Открыть JSON с данными.
Скачать готовый CSV с двумя товарами. Файл в UTF-8 без BOM; разделитель — точка с запятой, переводы строк CRLF. Это фактическая выгрузка программы.
| sku | title | qty |
|---|---|---|
| CAB-01 | Кабель; 2 м | 3 |
| CLP-02 | Хомут | 0 |
Содержимое CSV как текст
sku;title;qty
CAB-01;"Кабель; 2 м";3
CLP-02;Хомут;0
Ограничения и частые ошибки
Часть BLOB не равна строке CSV: граница может пройти внутри буквы, перевода строки или поля в кавычках. Мы не перекодируем и не разбираем порции. Во время собственно выгрузки вся строка не накапливается; маленький исходник заранее создан в памяти только для самостоятельной демонстрации. Файл назначения фиксирован внутри каталога примера и перезаписывается при повторном запуске. Для реальной доставки большого экспорта используйте временный файл и выдавайте его только после успешного завершения; этот пример удаляет незавершённую запись.
Файл после выгрузки не открывался; среди попыток было сохранение return функции blob_echo как содержимого файла. Разобранное обсуждение. Чётко отделяем вывод от return: get возвращает байты для fwrite, echo выводит их в ответ. В CSV-проекте сохраняется исходный BINARY BLOB и проверяются байты, а не подбирается кодировка наугад.
Проверьте понимание
Почему нельзя заменить проверку CSV-парсером сравнением числа строк после explode(" ", $text)?
Разбор
CSV разрешает разделители и переводы строк внутри поля в кавычках. Физическая строка и запись не всегда совпадают. Совпадение SHA подтверждает сохранение байтов, а CSV-парсер дополнительно показывает, что означают поля.
Получить акт по HTTP с именем файла и исходными русскими буквами
Кнопка «Скачать акт» должна отдавать понятный файл receipt-42.txt с текстом «Кабель: 3 шт.», а не страницу с BLOB ID или случайно добавленной единицей. Полная программа делает настоящий локальный HTTP-запрос к download.php, сохраняет ответ клиента и проверяет тело вместе с заголовками.
- Записать акт заявки 42 → commit → закрыть родительское соединение
- Один локальный GET /download.php с внутренним учебным токеном
- SELECT ID = 42 → проверка вложения → metadata
- Content-Type + Content-Disposition + Content-Length
- fbird_blob_echo выводит 77 байт; возврат true не печатается
- Клиент получает русский текст и имя receipt-42.txt; сервер остановлен
Самый короткий вызов
header('Content-Type: text/plain; charset=UTF-8');
header('Content-Disposition: attachment; filename="receipt-42.txt"');
$ok = fbird_blob_echo($db, $blobId);
if ($ok === false) { /* Зафиксировать отказ без дописывания текста к файлу. */ }
// Не echo $ok: байты уже отправлены, а true превратится в лишнюю "1".header задаёт свойства HTTP-ответа до начала тела. fbird_blob_echo принимает ID, сам читает BLOB и пишет его в стандартный вывод — в HTTP-обработчике это тело ответа. Значение $ok описывает успех операции и не содержит файл. Content-Type описывает известный формат; attachment предлагает браузеру скачать файл с указанным именем. В полном endpoint отдельно проверены размер и запись, а диагностика не попадает в файл.
Что получится
| Наблюдение | Результат |
|---|---|
| HTTP-статус | 200 |
| Имя скачиваемого файла | receipt-42.txt |
| Content-Type | text/plain; charset=UTF-8 |
| Content-Disposition | attachment; filename="receipt-42.txt" |
| Полученное содержимое акта | Акт приёмки №42 Кабель: 3 шт. Статус: принят |
| Получено байт | 77 |
| SHA256 полученного файла | 3dd5720e3950207d628873762554cb18809e73ca5f7a89d007554ef3b1c5b7bd |
| Ответ совпал с исходным актом | да |
| Дочерний сервер остановлен | да |
Полная программа, файлы и архив для запуска
<?php
declare(strict_types=1);
require __DIR__.'/lab.php';
$db=null;$owned=$removed=$rows=$checks=$observations=[];
try {
$config=f162Config();$db=f162Connect($config);$runtime=f162Runtime($db,$config);
// The preceding writing chapter explains fixture creation; the lesson below teaches reading.
function f162FixtureBlob($db,array $pieces): string {
$handle=fbird_blob_create($db);
if ($handle===false) {f162Failure('Учебный BLOB не создан');}
try {
foreach($pieces as $piece) {
$call=f162Capture(static fn()=>fbird_blob_add($handle,$piece));
if($call['value']===false || $call['warning'])f162Failure('Учебные байты не записаны',$call);
}
$id=fbird_blob_close($handle);
if(!is_string($id))f162Failure('Запись BLOB не закрыта');
$handle=null;return $id;
}finally{if(is_resource($handle)){fbird_blob_cancel($handle);$handle=null;}}
}
require __DIR__.'/http-lab.php';
$name=f162OwnTable($db,$owned,'download','(ID INTEGER NOT NULL PRIMARY KEY, FILE_DATA BLOB SUB_TYPE BINARY)');
$q=f162Qualified($name);
$payload="Акт приёмки №42\nКабель: 3 шт.\nСтатус: принят\n";
$expectedHash=hash('sha256',$payload);
$blobId=f162FixtureBlob($db,str_split($payload,9));
f162Command($db,"INSERT INTO $q (ID,FILE_DATA) VALUES (?,?)",[42,$blobId]);f162Commit($db);
if(fbird_close($db)!==true)f162Failure('Родительское подключение не закрыто');$db=null;
try{$response=f162HttpDownload($name);}
finally{if(f162HttpChildrenStopped())$db=f162Connect($config);}
$body=$response['body'];$headers=$response['headers'];$bytes=strlen($body);$sha=hash('sha256',$body);
if(file_put_contents(__DIR__.'/downloaded.bin',$body)!==$bytes)throw new RuntimeException('Полученный файл не сохранён');
$rows=[['HTTP-статус',$response['status']],['Имя скачиваемого файла','receipt-42.txt'],
['Content-Type',$headers['content-type']??''],['Content-Disposition',$headers['content-disposition']??''],
['Полученное содержимое акта',$body],['Получено байт',$bytes],['SHA256 полученного файла',$sha],
['Ответ совпал с исходным актом',$body===$payload],['Дочерний сервер остановлен',$response['server_stopped']]];
$checks=['http_200'=>$response['status']===200,'text_content_type'=>($headers['content-type']??'')==='text/plain; charset=UTF-8',
'download_filename'=>($headers['content-disposition']??'')==='attachment; filename="receipt-42.txt"',
'content_length'=>($headers['content-length']??'')==='77'&&$bytes===77,
'exact_received_text'=>$body===$payload,'received_hash'=>$sha===$expectedHash,
'no_sniff'=>($headers['x-content-type-options']??'')==='nosniff',
'same_runtime'=>($headers['x-phpman-php']??'')===PHP_VERSION&&($headers['x-phpman-interbase']??'')===phpversion('interbase'),
'local_child_stopped'=>$response['server_started']&&$response['server_stopped']&&f162HttpChildrenStopped(),
'downloaded_file_exact'=>hash_file('sha256',__DIR__.'/downloaded.bin')===$expectedHash];
$observations=['http'=>['status'=>$response['status'],'bytes'=>$bytes,'sha256'=>$sha,'headers'=>$headers,
'server_started'=>$response['server_started'],'server_stopped'=>$response['server_stopped'],'http_requests'=>1],
'body_text'=>$body,'client_saved_file'=>'downloaded.bin','browser_filename'=>'receipt-42.txt','expected_sha256'=>$expectedHash,
'transport'=>'real HTTP over 127.0.0.1','private_token_published'=>false,'cli_output_buffer_used_as_http_test'=>false];
} finally {
if (function_exists('f162HttpChildrenStopped') && !f162HttpChildrenStopped()) {
throw new RuntimeException('Дочерний HTTP-процесс ещё работает: учебная таблица сохранена для точного восстановления.');
}
if (is_resource($db)) {try {$removed=f162Cleanup($db,$owned);} finally {ibase_close($db);}}
}
f162Report('fb162-http-download','Получить акт по HTTP с именем файла и исходными русскими буквами',$rows,$checks,$observations,$runtime,$owned,$removed);
Ожидаемый результат
HTTP-статус — 200
Имя скачиваемого файла — receipt-42.txt
Content-Type — text/plain; charset=UTF-8
Content-Disposition — attachment; filename="receipt-42.txt"
Полученное содержимое акта — Акт приёмки №42
Кабель: 3 шт.
Статус: принят
Получено байт — 77
SHA256 полученного файла — 3dd5720e3950207d628873762554cb18809e73ca5f7a89d007554ef3b1c5b7bd
Ответ совпал с исходным актом — да
Дочерний сервер остановлен — да
Распакуйте все файлы и запустите example.php по README.txt. Исходник ниже включает подготовку искусственного содержимого, чтение, проверки и finally. lab.php ограничивает работу отдельной базой и собственной FB162-таблицей. result.html показывает фактические строки, result.json хранит наблюдения, metrics.json — проверки и очистку. Все открытые чтения завершаются до удаления таблицы. Для HTTP-проекта README дополнительно описывает единственный временный локальный PHP-сервер; он запускается и останавливается автоматически.
Открыть фактический результат · Открыть JSON с данными.
Обработчик download.php: что делает сервер
<?php
declare(strict_types=1);
require __DIR__.'/lab.php';
$db=null;$bodyStarted=false;
try {
$config=f162Config();$db=f162Connect($config);f162AssertDatabase($db);
$table=getenv('PHPMAN_FIREBIRD_HTTP_TABLE');
if(!is_string($table))throw new RuntimeException('Нет собственной таблицы вложений');
$q=f162Qualified($table);
// The private router authorizes only this test. A real site must authorize its user and document.
$records=f162Read($db,"SELECT FILE_DATA FROM $q WHERE ID=?",[42]);
if(count($records)!==1||$records[0]['FILE_DATA']===null)throw new RuntimeException('Акт не приложен');
$blobId=$records[0]['FILE_DATA'];
$info=fbird_blob_info($db,$blobId);
if($info===false)f162Failure('Размер акта неизвестен');
if($info['length']>1048576)throw new RuntimeException('Учебное вложение слишком велико');
header('Content-Type: text/plain; charset=UTF-8');
header('Content-Disposition: attachment; filename="receipt-42.txt"');
header('Content-Length: '.(string)$info['length']);
header('X-Content-Type-Options: nosniff');header('Cache-Control: no-store');
$bodyStarted=true;
$sent=f162Capture(static fn()=>fbird_blob_echo($db,$blobId));
if($sent['value']!==true||$sent['warning'])f162Failure('Вложение не передано',$sent);
f162Commit($db);
}catch(Throwable $error){
// Do not append warnings, HTML or a boolean result to an attachment, even with output buffering.
if(!$bodyStarted&&!headers_sent()){
header_remove('Content-Length');header_remove('Content-Disposition');
http_response_code(500);header('Content-Type: text/plain; charset=UTF-8');
echo 'Не удалось подготовить учебное вложение.';
}
error_log('Учебное скачивание Firebird не завершено.');
}finally{if(is_resource($db)){ibase_close($db);$db=null;}}
Ограничения и частые ошибки
Заголовки не превращают произвольные байты в UTF-8, PDF или картинку. В примере исходный формат известен, имя фиксировано, размер ограничен. Отказ посреди передачи может оставить неполный ответ, поэтому после начала тела нельзя дописывать HTML ошибки; сервер пишет служебное сообщение отдельно. Функция не реализует Range, возобновление скачивания и права пользователя. Это учебный закрытый endpoint: не устанавливайте его на публичный сайт без авторизации и выбора только доступных документов. Буферизация web-сервера может менять расход памяти; здесь проверяется точность одного ответа, а не производительность больших скачиваний.
Разработчик менял Content-Type, пытаясь показать BLOB как изображение; данные содержали собственный префикс приложения. Разобранное обсуждение. Заголовок описывает известный формат и не преобразует байты. Учебный акт имеет заранее известные UTF-8 байты; удаление чужого префикса и угадывание формата не выдаются за универсальную обработку BLOB.
Проверьте понимание
Можно ли записать файл вызовом file_put_contents("act.txt", fbird_blob_echo($blobId))?
Разбор
Нет. В файл передастся булев результат, а сами байты уже уйдут в стандартный вывод. Для локального файла нужен open/get и запись порций, как в CSV-проекте. Для HTTP вызывайте echo-функцию внутри отдельного ответа и не выводите её return.
Справочник чтения BLOB
| Функция | Для чего | Каноническая карточка |
|---|---|---|
| fbird_blob_info | Получить размер и устройство BLOB без загрузки его тела в PHP. | ibase_blob_info |
| fbird_blob_open | Открыть существующий BLOB для последовательного чтения. | ibase_blob_open |
| fbird_blob_get | Получить следующую ограниченную порцию байтов в переменную. | ibase_blob_get |
| fbird_blob_echo | Передать содержимое BLOB в стандартный вывод или HTTP-ответ. | ibase_blob_echo |
Выбирать чтение по результату, который нужен человеку
Выбор начинается с того, что требуется экрану или следующей системе. Для списка достаточно нужных SQL-полей и размера. Для маленькой заметки нужен ограниченный текст и HTML-экранирование. Для файла на диске — порции get и точная запись. Для отдельного скачивания — ответ с корректными заголовками и выводом BLOB, без дополнительного HTML. Не загружайте все тела заранее только потому, что они есть в таблице.
Если файл пришёл в неожиданном формате, сначала проверьте его происхождение и контракт хранения. Ни смена префикса fbird_/ibase_, ни Content-Type, ни произвольный CAST не восстанавливают неизвестные данные. Дальше полезны сохранение новой версии вложения, транзакции и диагностика и разрешения на данные.
Функции и методы этой темы
Откройте карточку, чтобы увидеть договор функции, примеры, ограничения и связанные инструменты.
fbird_blob_infoПолучить размер и устройство BLOB без загрузки его тела в PHP.
PHP 5 / PHP 7 до 7.4; далее отдельное расширение. Целевой профиль PHP 8.2.35 / interbase 3.0.1. · interbase
fbird_blob_info(resource $linkIdentifier, string $blobId): array|falseДля списка вложений достаточно знать размер, а для предпросмотра — решить, допустимо ли читать тело в память. Эта функция получает сведения о сохранённом BLOB, не возвращая его содержимое.
fbird_blob_info — альтернативное имя ibase_blob_info. В расширении interbase 3.0.1 это тот же обработчик, поэтому выбор префикса не меняет формат BLOB, стоимость SQL или правила чтения. Такой справочник нужен для сопровождения кода, в котором уже используются fbird_*. Историческая пометка PHP Manual — PHP 5 и PHP 7 до 7.4; в современном PHP требуется отдельно установленное расширение. Здесь целевой профиль — PHP 8.2.35 и interbase 3.0.1, описанный в README. Это не API PDO.
- SQL даёт FILE_NAME и FILE_DATA = BLOB ID
- info($db, $blobId) → массив metadata
- length = 28 → надпись «28 байт» рядом с receipt-42.txt
- length = 0 при существующем BLOB → пустой файл
- SQL NULL проверен отдельно → вложение отсутствует
fbird_blob_info(string $blobId): array|false
fbird_blob_info(resource $linkIdentifier, string $blobId): array|false| Аргумент / результат | Назначение |
|---|---|
| $linkIdentifier | Открытое соединение с нужной базой; обработчик 3.0.1 также принимает ресурс явной транзакции и выбирает её контекст. В одноаргументной форме используется последнее открытое соединение. Для нескольких баз передавайте контекст явно. |
| $blobId | Строковый ID, полученный из SELECT. Это не имя файла, не массив метаданных и не ресурс open. |
| array | Сведения с числовыми и именованными ключами. Размер и сегменты не являются содержимым. |
| false | ID не распознан, BLOB недоступен либо произошёл отказ API. Сохраните errcode/errmsg сразу; не подменяйте ошибку нулевым размером. |
| Индекс / имя | Тип | Что означает |
|---|---|---|
| 0 / length | int | Общий размер BLOB в байтах. Это не число букв. |
| 1 / numseg | int | Число сохранённых сегментов. Не число строк CSV и не число HTTP-пакетов. |
| 2 / maxseg | int | Размер самого большого сегмента в байтах. Не обязательная порция для get. |
| 3 / stream | bool | true для stream BLOB, false для segmented BLOB. Это устройство Firebird, а не PHP stream или режим ответа. |
| 4 / isnull | bool | Признак нулевого BLOB ID в этом драйвере; для сохранённого пустого BLOB — false. Обычный SQL NULL обработайте до вызова. |
Базовая официальная страница перечисляет общие сведения, а точные имена ключей, isnull и отказ false дополнены по закреплённому исходнику. Одни и те же значения доступны двумя способами: $info[0] и $info["length"]. Предпочитайте именованные ключи, чтобы назначение было видно из кода. У функции нет параметров формата, кодировки, лимита памяти или MIME-типа.
Применение 1: не перепутать пустой файл и отсутствие
if ($row['FILE_DATA'] === null) {
$label = 'Файл ещё не приложен';
} else {
$info = fbird_blob_info($db, $row['FILE_DATA']);
if ($info === false) { throw new RuntimeException('Размер не получен'); }
$label = $info['length'] === 0 ? 'Пустой файл' : $info['length'] . ' байт';
}Полная программа списка показывает draft.txt как пустое сохранённое вложение, pending.pdf как отсутствие, а два заполненных файла — с размерами. Ни один BLOB ID не используется как текст для пользователя.
Применение 2: выбрать способ показа до чтения
$info = fbird_blob_info($db, $blobId);
if ($info === false) { throw new RuntimeException('Размер неизвестен'); }
$mode = $info['length'] <= 96 ? 'Предпросмотр' : 'Отдельное скачивание';В предпросмотре двух заявок маленькая заметка читается через fbird_blob_open и fbird_blob_get, а большая пропускается. Дополнительно ограничивается фактически собранный объём. Это продуктовая политика, а не встроенная защита info. Для HTTP-выдачи перейдите к fbird_blob_echo.
info не извлекает тело в PHP, но обращается к базе; список из миллионов строк всё равно нельзя бездумно обходить целиком. Запрашивайте нужную страницу и поля. Много гигабайт и предельные размеры старого драйвера в этих проектах не проверяются: нельзя переносить учебный результат на любой размер BLOB. Не считайте stream доказательством низкого расхода памяти приложения.
fbird_blob_openОткрыть существующий BLOB для последовательного чтения.
PHP 5 / PHP 7 до 7.4; далее отдельное расширение. Целевой профиль PHP 8.2.35 / interbase 3.0.1. · interbase
fbird_blob_open(resource $linkIdentifier, string $blobId): resource|falseЧтобы получить байты заметки или перенести файл постепенно, сначала открывают существующий BLOB для чтения. open возвращает ресурс с позицией чтения; само содержимое придёт при последующих get.
fbird_blob_open — альтернативное имя ibase_blob_open. В расширении interbase 3.0.1 это тот же обработчик, поэтому выбор префикса не меняет формат BLOB, стоимость SQL или правила чтения. Такой справочник нужен для сопровождения кода, в котором уже используются fbird_*. Историческая пометка PHP Manual — PHP 5 и PHP 7 до 7.4; в современном PHP требуется отдельно установленное расширение. Здесь целевой профиль — PHP 8.2.35 и interbase 3.0.1, описанный в README. Это не API PDO.
- SELECT → строковый $blobId
- open($db, $blobId) → $handle
- get($handle, 7) → строка очередных байтов
- Следующий get продолжает чтение
- close($handle) → освободить чтение; $handle = null
fbird_blob_open(string $blobId): resource|false
fbird_blob_open(resource $linkIdentifier, string $blobId): resource|false| Аргумент / возврат | Правило |
|---|---|
| $linkIdentifier | Соединение с базой этого BLOB; обработчик 3.0.1 также принимает ресурс явной транзакции. При отсутствии используется последнее открытое соединение. Не подставляйте вместо контекста ресурс результата SELECT. |
| $blobId | ID существующего содержимого из SQL-результата. SQL NULL, путь на диске и содержимое файла не подходят. |
| resource | Ресурс чтения для fbird_blob_get; он не является потоком, открытым fopen. |
| false | BLOB не удалось открыть. Возможны неверный ID, другой контекст/база, завершённая транзакция или ошибка сервера; сохраните диагностику немедленно. |
Это режим чтения. Для создания нового содержимого нужен fbird_blob_create, а не open. Открытый ресурс зависит от его соединения и транзакции: закончите чтение до commit/rollback и закрытия подключения. ID не становится самостоятельным файлом и не заменяет проверку права пользователя на документ. Повторное открытие создаёт отдельную позицию в начале; функции перемотки уже открытого BLOB здесь нет.
Применение 1: получить первую порцию заметки
$reader = fbird_blob_open($db, $row['NOTE_TEXT']);
if ($reader === false) { throw new RuntimeException('Заметка не открыта'); }
try {
$part = fbird_blob_get($reader, 7);
if ($part === false) { throw new RuntimeException('Заметка не прочитана'); }
} finally {
if (fbird_blob_close($reader) === false) { throw new RuntimeException('Чтение не закрыто'); }
$reader = null;
}Фрагмент показывает жизненный цикл одного reader. Полный предпросмотр сначала выбирает допустимый размер, затем повторяет чтение до конца и получает понятную заметку заявки 42. Содержимое $part нельзя считать целой UTF-8-строкой: буква может быть разделена.
Применение 2: разделить ресурс BLOB и ресурс файла
$reader = fbird_blob_open($db, $blobId);
if ($reader === false) { throw new RuntimeException('Вложение не открыто'); }
$out = fopen(__DIR__ . '/export.csv', 'wb');
// reader передаётся в fbird_blob_get; out передаётся в fwrite.
// Полная программа проверяет fopen и закрывает оба ресурса в finally.В выгрузке CSV это два разных канала: первый читает базу, второй пишет локальный файл. Нельзя передать reader в fread или out в fbird_blob_get. После работы вызывается fbird_blob_close и ссылка обнуляется. По исходнику 3.0.1 и прежним наблюдениям close читателя возвращает строковый ID; проверяйте отказ === false, не требуйте исключительно true. Проверка is_resource после освобождения сама по себе не гарантирует пригодность старого handle.
Не передавайте произвольный BLOB ID из URL как разрешение на скачивание: сначала найдите доступную этому пользователю строку документа, затем берите её BLOB ID. При полном прямом выводе в ответ можно обойтись fbird_blob_echo — он сам открывает и закрывает внутреннее чтение.
fbird_blob_getПолучить следующую ограниченную порцию байтов в переменную.
PHP 5 / PHP 7 до 7.4; далее отдельное расширение. Целевой профиль PHP 8.2.35 / interbase 3.0.1. · interbase
fbird_blob_get(resource $blobHandle, int $length): string|falseЭта функция нужна, когда содержимое должно оказаться в PHP-переменной: например, чтобы показать ограниченную заметку, передать порцию в файл или разобрать уже выгруженный CSV. Она возвращает следующую часть содержимого, а не печатает её.
fbird_blob_get — альтернативное имя ibase_blob_get. В расширении interbase 3.0.1 это тот же обработчик, поэтому выбор префикса не меняет формат BLOB, стоимость SQL или правила чтения. Такой справочник нужен для сопровождения кода, в котором уже используются fbird_*. Историческая пометка PHP Manual — PHP 5 и PHP 7 до 7.4; в современном PHP требуется отдельно установленное расширение. Здесь целевой профиль — PHP 8.2.35 и interbase 3.0.1, описанный в README. Это не API PDO.
- Ресурс уже открыт через open
- get(handle, 13) → не более 13 байт
- Следующий вызов → следующие байты
- Порция направляется в ограниченный текст или fwrite
- Пустая строка — конец в данном драйвере; false — ошибка
fbird_blob_get(resource $blobHandle, int $length): string|false| Аргумент / результат | Правило |
|---|---|
| $blobHandle | Открытый ресурс fbird_blob_open. Ресурс создания для записи через fbird_blob_create читать нельзя. |
| $length | Максимум байт в результате одного вызова. Используйте положительное ограниченное число из настроек приложения. |
| string | Не более запрошенного числа байт. Следующий вызов продолжает с текущего места; последнее чтение может быть короче. |
| '' | После исчерпания BLOB в закреплённом драйвере возвращается пустая строка. Не путайте её с текстом "0". |
| false | Чтение не выполнено. Неверные PHP-типы могут приводить и к ошибкам проверки аргументов; не скрывайте их как конец файла. |
Размер не означает количество букв, CSV-полей, строк или сохранённых Firebird-сегментов. Драйвер 3.0.1 собирает запрошенную порцию из сегментов, при необходимости несколькими внутренними чтениями; 65535 не следует выдавать за универсальный предел одного вызова PHP. Однако выделение очень большого результата увеличивает память. Отрицательную длину не передавайте, нулевая не продвигает чтение и для цикла не подходит. Параметров кодировки, смещения или обратной перемотки нет.
Применение 1: маленькое известное содержимое в переменную
$info = fbird_blob_info($db, $blobId);
if ($info === false || $info['length'] > 96) { throw new RuntimeException('Нужен другой способ выдачи'); }
$reader = fbird_blob_open($db, $blobId);
if ($reader === false) { throw new RuntimeException('BLOB не открыт'); }
try {
$value = $info['length'] === 0 ? '' : fbird_blob_get($reader, $info['length']);
if ($value === false) { throw new RuntimeException('Чтение не удалось'); }
} finally {
if (fbird_blob_close($reader) === false) { throw new RuntimeException('BLOB не закрыт'); }
$reader = null;
}Это базовый вариант официального приёма «получить содержимое в переменную», дополненный лимитом, обработкой пустого файла и закрытием. Полная программа дополнительно показывает порции, контроль фактически собранных байт и UTF-8. Только после сборки полного ограниченного текста выполняется HTML-экранирование; для больших файлов целиком накапливать $value не нужно.
Применение 2: передать порции в файл
while (true) {
$part = fbird_blob_get($reader, 8192);
if ($part === false) { throw new RuntimeException('Чтение прервалось'); }
if ($part === '') { break; }
$offset = 0;
while ($offset < strlen($part)) {
$n = fwrite($out, substr($part, $offset));
if ($n === false || $n === 0) { throw new RuntimeException('Запись прервалась'); }
$offset += $n;
}
}Это контекстный цикл для уже открытых reader и out. В самостоятельном CSV-проекте размер порции уменьшен до 13 байт, чтобы увидеть несколько шагов на маленьком файле; открытие, закрытие, предел и удаление неполного файла включены. Результат — сохранённые строки CAB-01 и CLP-02, а не распечатка булевого успеха. Изменение размера порции не должно менять содержимое файла.
Для большого файла hash_file после записи проверяет содержимое без чтения всего файла в одну PHP-строку. SHA не доказывает безопасность файла или личность отправителя; это сравнение с известным исходником. Если нужен прямой HTTP-ответ без обработки данных в PHP, рассмотрите fbird_blob_echo. Если нужен быстрый известный небольшой текст при fetch, возможен IBASE_TEXT, но политика лимита при этом должна быть решена заранее.
fbird_blob_echoПередать содержимое BLOB в стандартный вывод или HTTP-ответ.
PHP 5 / PHP 7 до 7.4; далее отдельное расширение. Целевой профиль PHP 8.2.35 / interbase 3.0.1. · interbase
fbird_blob_echo(resource $linkIdentifier, string $blobId): boolКогда посетитель нажимает «Скачать акт», содержимое должно уйти в тело отдельного HTTP-ответа. Функция сама читает BLOB и выводит его байты. Она удобна, когда не нужно изменять содержимое в PHP перед отправкой.
fbird_blob_echo — альтернативное имя ibase_blob_echo. В расширении interbase 3.0.1 это тот же обработчик, поэтому выбор префикса не меняет формат BLOB, стоимость SQL или правила чтения. Такой справочник нужен для сопровождения кода, в котором уже используются fbird_*. Историческая пометка PHP Manual — PHP 5 и PHP 7 до 7.4; в современном PHP требуется отдельно установленное расширение. Здесь целевой профиль — PHP 8.2.35 и interbase 3.0.1, описанный в README. Это не API PDO.
- Из выбранной доступной записи взят $blobId
- Заголовки описывают файл receipt-42.txt
- fbird_blob_echo($db, $blobId) → байты в тело ответа
- В переменную $ok попадает true/false
- Клиент сохраняет акт; $ok не дописывается к файлу
fbird_blob_echo(string $blobId): bool
fbird_blob_echo(resource $linkIdentifier, string $blobId): bool| Аргумент / результат | Правило |
|---|---|
| $linkIdentifier | Соединение с базой BLOB либо ресурс явной транзакции, поддерживаемый обработчиком 3.0.1; при одноаргументном вызове — последнее открытое соединение. |
| $blobId | Строковый BLOB ID из нужной строки SELECT. Это не reader от open и не содержимое. |
| Побочный эффект | Байты попадают в стандартный вывод PHP: в HTTP это тело ответа, в CLI — stdout. |
| true | Внутреннее чтение и вывод завершились успешно по API. Файл в эту переменную не возвращается. |
| false | Ошибка открытия, чтения или закрытия внутреннего BLOB. Часть тела могла быть уже выведена; это не обещание атомарного HTTP-ответа. |
Функция сама открывает BLOB и освобождает своё внутреннее чтение; передавать handle от open нельзя. Она не устанавливает HTTP-заголовки, не делает HTML-экранирование, не кодирует base64, не превращает текст в PDF и не проверяет права посетителя. Входные SQL NULL и отсутствие строки обрабатываются до вызова.
Применение 1: предложить скачивание известного текстового акта
header('Content-Type: text/plain; charset=UTF-8');
header('Content-Disposition: attachment; filename="receipt-42.txt"');
header('X-Content-Type-Options: nosniff');
$sent = fbird_blob_echo($db, $blobId);
if ($sent === false) {
error_log('Передача акта не завершена.');
}
// Не выводим $sent и не добавляем HTML после файла.Это часть отдельного endpoint после выбора разрешённого документа. Полный HTTP-проект добавляет проверку размера, Content-Length, защиту учебного маршрута, обработку отказов до начала тела и настоящий запрос отдельного процесса. На стороне клиента проверены заголовки, 77 байт и понятный текст акта. Локальная техническая копия downloaded.bin содержит тот же текст; браузерное имя задаётся filename="receipt-42.txt".
Применение 2: открыть известный журнал как обычный текст
header('Content-Type: text/plain; charset=UTF-8');
header('Content-Disposition: inline');
header('X-Content-Type-Options: nosniff');
if (fbird_blob_echo($db, $logBlobId) === false) {
error_log('Журнал не передан полностью.');
}В этом контекстном варианте пользователь просматривает заранее известный UTF-8 журнал вместо сохранения файла. Это по-прежнему отдельный text/plain-ответ, а не вставка неэкранированных строк в HTML-страницу. Для текстовой заметки внутри карточки используйте fbird_blob_get и htmlspecialchars; для локального файла — побайтовую выгрузку.
header вызывается до любого тела, включая пробелы, BOM, предупреждения и шаблон страницы. display_errors=0 не отменяет проверку ошибок — диагностику сохраняют отдельно. После начала передачи нельзя дописывать понятное сообщение внутрь бинарного или текстового файла. Буферизация PHP и web-сервера остаётся отдельным фактором; функция не гарантирует, что клиент действительно сохранил файл, и не реализует Range, повторную отправку или защиту от обрыва соединения.
MIME и имя берите из доверенного правила либо предварительно проверенных метаданных. Заголовок image/jpeg не исправляет собственный формат старого приложения, где перед картинкой есть служебный префикс. Нельзя автоматически отрезать первые байты у всех BLOB: сначала установите формат. Права на запись и документ рассматриваются в главе о разграничении доступа.
Источники и обсуждения
- PHP Manual: fbird_blob_info
- PHP Manual: fbird_blob_open
- PHP Manual: fbird_blob_get
- PHP Manual: fbird_blob_echo
- PHP Manual: ibase_blob_info
- PHP Manual: ibase_blob_open
- PHP Manual: ibase_blob_get
- PHP Manual: ibase_blob_echo
- FirebirdSQL: BLOB, драйвер 3.0.1
- FirebirdSQL: регистрация aliases 3.0.1
- Firebird 5: BLOB и подтипы
- PHP: fgetcsv
- PHP: fwrite
- PHP: header
Код и руководство с собой
Скачайте самостоятельные примеры с исходными данными, инструкциями и ожидаемыми результатами. Примеры HTTP и фрагменты для проекта помечены отдельно: им нужно указанное окружение.
Код не исполняется на сайте. Скачайте нужный пример и запускайте его в своём локальном окружении.
Материалы сверены 2026-10-12. Примеры и объяснения написаны для этого руководства; источники и обсуждения доступны в каждой главе.


