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

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

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

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

Глава 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-ответе. По этим данным можно проверить, что произошло с содержимым. Полные программы создают свою таблицу внутри отдельной лабораторной базы, а затем удаляют только её; файлы результата остаются.

Как найти содержимое по записи документа
  1. Заказ 42 → нужная строка SQL
  2. FILE_DATA содержит BLOB ID, а не имя или тело файла
  3. info → выбрать действие по размеру
  4. open + get → переменная либо локальный файл
  5. 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 байт.

Путь данных
  1. SELECT четырёх строк → имя + BLOB ID либо SQL NULL
  2. receipt-42.txt → info.length = 28 → Скачать
  3. draft.txt → length = 0, isnull = false → Скачать пустой файл
  4. pending.pdf → SQL NULL → Файл ещё не приложен
  5. 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.txt28 байт; Скачать
draft.txt0 байт; Скачать пустой файл
pending.pdfнет BLOB; Файл ещё не приложен
report.bin128 байт; Скачать
Доступных вложений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 байт нарочно мал, чтобы обе ветви были видны в одном запуске.

Путь данных
  1. Заявка 42 → metadata 65 байт ≤ 96 → открыть чтение
  2. get(7) → байты следующих частей; граница может попасть внутрь буквы
  3. Собрать не более 96 байт; пустая строка завершает цикл
  4. Проверить целый UTF-8 → экранировать текст для HTML
  5. Заявка 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 Проверить &lt;кабель&gt; &amp; разъём.
Прочитано байт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,'&lt;кабель&gt; &amp; разъём.'),
        '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
Проверить &lt;кабель&gt; &amp; разъём.

Прочитано байт — 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 м» и нулевой остаток хомута, а не только сообщение «файл записан».

Путь данных
  1. BLOB хранит точные UTF-8 байты CSV с CRLF
  2. open → get по 13 байт → fwrite до записи всей порции
  3. Проверить 67 байт и SHA256 исходника
  4. fgetcsv читает целые CSV-записи из готового файла
  5. Кавычки удерживают ; внутри поля «Кабель; 2 м»
  6. Выгрузка и видимые строки доступны отдельно

Самый короткий вызов

$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
Товар 1CAB-01 | Кабель; 2 м | 3
Товар 2CLP-02 | Хомут | 0
Байт в выгрузке67
Непустых порций6
SHA256 файлаa0e0108b39fabbd340e0d020acfa25feabfbd6071f89256a181eaa2bd73a9e75
Кавычки сохранили точку с запятой внутри названияда
Полная программа, файлы и архив для запуска
Выгрузить сохранённый CSV и проверить, что его поля не потерялисьСамостоятельный пример
<?php
declare(strict_types=1);
require __DIR__.'/lab.php';
$db=null;$owned=$removed=$rows=$checks=$observations=[];
try {
    $config=f162Config();$db=f162Connect($config);$runtime=f162Runtime($db,$config);

    // 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. Это фактическая выгрузка программы.

skutitleqty
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, сохраняет ответ клиента и проверяет тело вместе с заголовками.

Путь данных
  1. Записать акт заявки 42 → commit → закрыть родительское соединение
  2. Один локальный GET /download.php с внутренним учебным токеном
  3. SELECT ID = 42 → проверка вложения → metadata
  4. Content-Type + Content-Disposition + Content-Length
  5. fbird_blob_echo выводит 77 байт; возврат true не печатается
  6. Клиент получает русский текст и имя 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-Typetext/plain; charset=UTF-8
Content-Dispositionattachment; filename="receipt-42.txt"
Полученное содержимое актаАкт приёмки №42 Кабель: 3 шт. Статус: принят
Получено байт77
SHA256 полученного файла3dd5720e3950207d628873762554cb18809e73ca5f7a89d007554ef3b1c5b7bd
Ответ совпал с исходным актомда
Дочерний сервер остановленда
Полная программа, файлы и архив для запуска
Получить акт по HTTP с именем файла и исходными русскими буквамиСамостоятельный пример
<?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.

Что получает список заказа
  1. SQL даёт FILE_NAME и FILE_DATA = BLOB ID
  2. info($db, $blobId) → массив metadata
  3. length = 28 → надпись «28 байт» рядом с receipt-42.txt
  4. length = 0 при существующем BLOB → пустой файл
  5. 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Сведения с числовыми и именованными ключами. Размер и сегменты не являются содержимым.
falseID не распознан, BLOB недоступен либо произошёл отказ API. Сохраните errcode/errmsg сразу; не подменяйте ошибку нулевым размером.
Индекс / имяТипЧто означает
0 / lengthintОбщий размер BLOB в байтах. Это не число букв.
1 / numsegintЧисло сохранённых сегментов. Не число строк CSV и не число HTTP-пакетов.
2 / maxsegintРазмер самого большого сегмента в байтах. Не обязательная порция для get.
3 / streambooltrue для stream BLOB, false для segmented BLOB. Это устройство Firebird, а не PHP stream или режим ответа.
4 / isnullboolПризнак нулевого 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.

ID, ресурс и текст — разные значения
  1. SELECT → строковый $blobId
  2. open($db, $blobId) → $handle
  3. get($handle, 7) → строка очередных байтов
  4. Следующий get продолжает чтение
  5. 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.
$blobIdID существующего содержимого из SQL-результата. SQL NULL, путь на диске и содержимое файла не подходят.
resourceРесурс чтения для fbird_blob_get; он не является потоком, открытым fopen.
falseBLOB не удалось открыть. Возможны неверный 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.

Последовательные порции
  1. Ресурс уже открыт через open
  2. get(handle, 13) → не более 13 байт
  3. Следующий вызов → следующие байты
  4. Порция направляется в ограниченный текст или fwrite
  5. Пустая строка — конец в данном драйвере; 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.

Вывод и возврат расходятся
  1. Из выбранной доступной записи взят $blobId
  2. Заголовки описывают файл receipt-42.txt
  3. fbird_blob_echo($db, $blobId) → байты в тело ответа
  4. В переменную $ok попадает true/false
  5. Клиент сохраняет акт; $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: сначала установите формат. Права на запись и документ рассматриваются в главе о разграничении доступа.

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

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

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

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

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