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

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

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

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

Глава 63 · После первой таблицы Firebird и записи BLOB

Firebird BLOB: размер вложения, чтение текста, экспорт и скачивание

BLOB ID и metadata, SQL NULL и пустое вложение, текст UTF8 по частям, двоичный экспорт с SHA256 и проверяемое отдельное HTTP-скачивание.

Идентификатор, открытое чтение и содержимое — разные значения

У вложения в таблице есть идентификатор, у открытого чтения — ресурс, а у самого файла — байты. Их легко спутать: вывести ID вместо заметки, загрузить все документы в массив или дописать HTML в бинарный ответ. Пройдём один маршрут от списка вложений до точного файла и отдельного скачивания.

Что лежит на каждом шаге
  1. Строка таблицы: ID записи + FILE_DATA
  2. SELECT без IBASE_TEXT → строковый BLOB ID либо null
  3. info → размер; open → ресурс чтения
  4. get → байты для текста/файла; echo → байты сразу в вывод
  5. close BLOB завершает чтение; commit/rollback завершает транзакцию

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

Перед чтением полезны создание, запись и отмена BLOB, первый SELECT и транзакции Firebird. Четыре программы используют отдельную учебную базу и отдельно установленный interbase 3.0.1 с PHP 8.2. Историческая пометка PHP Manual о PHP 7 до 7.4 относится к включению расширения в поставку PHP, а не к запрету современных внешних сборок.

Показать размер вложения и отличить пустой файл от отсутствующего

В списке вложений нужно написать размер, показать кнопку скачивания и отличить «файл пуст» от «файл не прикреплён». Для этого не нужно читать все байты каждого вложения в массив PHP. Создадим заполненный BLOB, пустой BLOB и SQL NULL, а затем примем решение о предпросмотре по длине.

Движение данных
  1. Три строки: файл счёта, пустой файл, отсутствие файла
  2. SELECT возвращает BLOB ID для первых двух строк и null для третьей
  3. info(BLOB ID) → размер и устройство хранения
  4. Файл 36 байт превышает учебный порог 32 байта → предложить скачивание
  5. Пустой BLOB: length = 0, isnull = false
  6. SQL NULL проверяется до обращения к BLOB API

Первый небольшой вызов

if ($row['FILE_DATA'] === null) {
    echo 'Вложение отсутствует';
} else {
    $info = ibase_blob_info($db, $row['FILE_DATA']);
    if ($info === false) { throw new RuntimeException('Размер не получен'); }
    echo $info['length'], ' байт';
}

Поле FILE_DATA без специального fetch-флага содержит идентификатор BLOB, а не байты файла. info принимает этот идентификатор вместе с соединением и возвращает массив. Ключ length измеряется в байтах. SQL NULL приходит как PHP null; его нельзя передавать вместо строкового BLOB ID. Числовые и именованные ключи metadata описывают одни и те же значения.

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

НаблюдениеРезультат
Размер заполненного вложения, байт36
Лимит предпросмотра, байт32
Предпросмотр разрешён без отдельного скачиваниянет
Размер пустого BLOB, байт0
Пустой BLOB имеет признак isnullнет
Третий SQL-столбец равен NULLда
Числовой ключ 0 совпадает с lengthда
Содержимое вложения читалось для списканет
Полная программа с собственными данными и архивом
Показать размер вложения и отличить пустой файл от отсутствующегоСамостоятельный пример
<?php
declare(strict_types=1);
require __DIR__.'/lab.php';
$db=null;$owned=$removed=$rows=$checks=$observations=[];
try {
    $config=f155Config();$db=f155Connect($config);$runtime=f155Runtime($db,$config);

    // Fixture creation is explained in the preceding chapter; this chapter teaches reading.
    function f155FixtureBlob($db,array $pieces): string {
        $handle=ibase_blob_create($db);
        if ($handle===false) {f155Failure('Учебный BLOB не создан');}
        try {
            foreach ($pieces as $piece) {
                $add=f155Capture(static fn()=>ibase_blob_add($handle,$piece));
                if ($add['value']===false || $add['warning']) {f155Failure('Порция BLOB не записана',$add);}
            }
            $id=ibase_blob_close($handle);
            if (!is_string($id)) {f155Failure('Записанный BLOB не закрыт');}
            $handle=null;return $id;
        } finally {
            if (is_resource($handle)) {ibase_blob_cancel($handle);$handle=null;}
        }
    }

    $attachments=f155OwnTable($db,$owned,'metadata','(ID INTEGER NOT NULL PRIMARY KEY, FILE_DATA BLOB SUB_TYPE BINARY)');
    $q=f155Qualified($attachments);
    $content="invoice=42\namount=1500\ncurrency=RUB\n";
    $fullId=f155FixtureBlob($db,["invoice=42\n","amount=1500\n","currency=RUB\n"]);
    $emptyId=f155FixtureBlob($db,[]);
    f155Command($db,"INSERT INTO $q (ID,FILE_DATA) VALUES (?,?)",[1,$fullId]);
    f155Command($db,"INSERT INTO $q (ID,FILE_DATA) VALUES (?,?)",[2,$emptyId]);
    f155Command($db,"INSERT INTO $q (ID,FILE_DATA) VALUES (?,?)",[3,null]);
    f155Commit($db);
    $stored=f155Read($db,"SELECT ID,FILE_DATA FROM $q ORDER BY ID");
    $full=ibase_blob_info($db,$stored[0]['FILE_DATA']);
    if ($full===false) {f155Failure('Нет метаданных вложения');}
    $empty=ibase_blob_info($db,$stored[1]['FILE_DATA']);
    if ($empty===false) {f155Failure('Нет метаданных пустого BLOB');}
    $missing=$stored[2]['FILE_DATA']===null;
    $previewLimit=32;
    $previewAllowed=$full['length']<=$previewLimit;
    $rows=[['Размер заполненного вложения, байт',$full['length']],['Лимит предпросмотра, байт',$previewLimit],
        ['Предпросмотр разрешён без отдельного скачивания',$previewAllowed],['Размер пустого BLOB, байт',$empty['length']],
        ['Пустой BLOB имеет признак isnull',$empty['isnull']],['Третий SQL-столбец равен NULL',$missing],
        ['Числовой ключ 0 совпадает с length',$full[0]===$full['length']],['Содержимое вложения читалось для списка',False]];
    $checks=['full_length'=>$full['length']===strlen($content),'preview_budget'=>$previewAllowed===false,
        'empty_is_not_null'=>$empty['length']===0&&$empty['isnull']===false,'sql_null_handled_before_api'=>$missing,
        'numeric_and_named_keys'=>$full[0]===$full['length']&&$full[1]===$full['numseg']&&$full[2]===$full['maxseg']&&$full[3]===$full['stream']&&$full[4]===$full['isnull'],
        'info_did_not_replace_identifier'=>is_string($stored[0]['FILE_DATA'])];
    $observations=['full_blob_metadata'=>$full,'empty_blob_metadata'=>$empty,'sql_null'=>$missing,
        'preview_limit_bytes'=>$previewLimit,'preview_allowed'=>$previewAllowed,'body_read_for_listing'=>false,
        'stored_blob_id_published'=>false,'subtype'=>'BINARY','fixture_bytes'=>strlen($content)];

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

f155Report('fb155-blob-metadata','Показать размер вложения и отличить пустой файл от отсутствующего',$rows,$checks,$observations,$runtime,$owned,$removed);
Ожидаемый результат
Размер заполненного вложения, байт — 36
Лимит предпросмотра, байт — 32
Предпросмотр разрешён без отдельного скачивания — нет
Размер пустого BLOB, байт — 0
Пустой BLOB имеет признак isnull — нет
Третий SQL-столбец равен NULL — да
Числовой ключ 0 совпадает с length — да
Содержимое вложения читалось для списка — нет

Запустите example.php по README.txt в отдельной учебной базе. Программа создаёт одну собственную таблицу, получает данные, сохраняет result.html/result.json/metrics.json и удаляет свою таблицу. Код вспомогательного lab.php открыт; операции записи тестового BLOB разобраны в предыдущей главе.

Открыть результат · Данные и наблюдения JSON.

Границы и возможные ошибки

Порог 32 байта специально мал для наглядного запуска; в приложении задайте предел по требованиям экрана. Metadata не содержит достоверного MIME-типа, имени файла, числа символов или разрешения изображения. stream означает устройство BLOB в Firebird, а не PHP-ресурс fopen и не режим HTTP. numseg/maxseg полезны для диагностики хранения, но не задают размер порции чтения. При большом списке это всё ещё отдельные обращения за metadata; для частого каталога можно хранить проверенную длину рядом при загрузке, согласованно с самим вложением.

После SELECT читатель видел BLOB ID вместо изображения и не понимал, как добраться до байтов. Исходный вопрос на Stack Overflow. Разделяем идентификатор, metadata, ресурс чтения и содержимое. Размер не определяет формат; для тела вызываются open/get или отдельный HTTP-обработчик.

Измените требование

Можно ли считать length = 0 доказательством, что пользователь ничего не прикрепил?

Разбор

Нет. Пустой файл имеет нулевую длину, но является сохранённым вложением. Отсутствие определяйте по SQL NULL. Решение, разрешены ли пустые вложения, принимает приложение при записи.

Прочитать заметку UTF8 порциями и показать её как текст

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

Движение данных
  1. SELECT заявки 42 → ID её NOTE_TEXT
  2. info → 67 байт; наш предел предпросмотра 4096 байт
  3. open → курсор чтения; get(7) последовательно выдаёт части
  4. Склеить байты без промежуточного преобразования UTF8
  5. get возвращает пустую строку → конец; close освобождает чтение
  6. htmlspecialchars целого текста → <кабель> виден как текст

Первый небольшой вызов

$handle = ibase_blob_open($db, $blobId);
if ($handle === false) { throw new RuntimeException('BLOB не открыт'); }
$firstBytes = ibase_blob_get($handle, 7);
// Это байты начала, а не обязательно семь целых символов.
// Полный пример читает до конца, проверяет false и закрывает handle.

open получает ID и возвращает ресурс чтения. get получает этот ресурс и максимальное число байт текущего шага; следующий вызов продолжает с прежнего места. false означает отказ, а пустая строка в проверенном профиле — конец содержимого. Конкатенация .= добавляет очередные байты к $note. Только после сборки всего ограниченного текста применяем htmlspecialchars: UTF8-символ мог быть разделён между двумя порциями.

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

НаблюдениеРезультат
Прочитано байт67
Размер одной запрошенной порции, байт7
Непустых порций10
Пустая строка обозначила конец BLOBда
Собранный текст совпал с исходнымда
Текст заметкиЗаявка #42 Проверить <кабель> & разъём.
Текст после HTML-экранированияЗаявка #42 Проверить &lt;кабель&gt; &amp; разъём.
Полная программа с собственными данными и архивом
Прочитать заметку UTF8 порциями и показать её как текстСамостоятельный пример
<?php
declare(strict_types=1);
require __DIR__.'/lab.php';
$db=null;$owned=$removed=$rows=$checks=$observations=[];
try {
    $config=f155Config();$db=f155Connect($config);$runtime=f155Runtime($db,$config);

    // Fixture creation is explained in the preceding chapter; this chapter teaches reading.
    function f155FixtureBlob($db,array $pieces): string {
        $handle=ibase_blob_create($db);
        if ($handle===false) {f155Failure('Учебный BLOB не создан');}
        try {
            foreach ($pieces as $piece) {
                $add=f155Capture(static fn()=>ibase_blob_add($handle,$piece));
                if ($add['value']===false || $add['warning']) {f155Failure('Порция BLOB не записана',$add);}
            }
            $id=ibase_blob_close($handle);
            if (!is_string($id)) {f155Failure('Записанный BLOB не закрыт');}
            $handle=null;return $id;
        } finally {
            if (is_resource($handle)) {ibase_blob_cancel($handle);$handle=null;}
        }
    }

    $notes=f155OwnTable($db,$owned,'note','(ID INTEGER NOT NULL PRIMARY KEY, NOTE_TEXT BLOB SUB_TYPE TEXT CHARACTER SET UTF8)');
    $q=f155Qualified($notes);
    $expected="Заявка #42\nПроверить <кабель> & разъём.\n";
    $blobId=f155FixtureBlob($db,[$expected]);
    f155Command($db,"INSERT INTO $q (ID,NOTE_TEXT) VALUES (?,?)",[42,$blobId]);
    f155Commit($db);
    $record=f155Read($db,"SELECT ID,NOTE_TEXT FROM $q WHERE ID=?",[42])[0];
    if ($record['NOTE_TEXT']===null) {throw new RuntimeException('Для этой заявки нужна заметка');}
    $info=ibase_blob_info($db,$record['NOTE_TEXT']);
    if ($info===false) {f155Failure('Размер заметки недоступен');}
    $limit=4096;
    if ($info['length']>$limit) {throw new RuntimeException('Заметка превышает предел предпросмотра');}
    $handle=ibase_blob_open($db,$record['NOTE_TEXT']);
    if ($handle===false) {f155Failure('Заметка не открыта');}
    $note='';$chunks=0;$eof=false;
    try {
        while (true) {
            $part=ibase_blob_get($handle,7);
            if ($part===false) {f155Failure('Порция заметки не прочитана');}
            if ($part==='') {$eof=true;break;}
            $note.=$part;++$chunks;
            if (strlen($note)>$limit) {throw new RuntimeException('Превышен предел собранного текста');}
        }
    } finally {
        if (ibase_blob_close($handle)===false) {f155Failure('Чтение заметки не закрыто');}
        $handle=null;
    }
    $escaped=htmlspecialchars($note,ENT_QUOTES|ENT_SUBSTITUTE,'UTF-8');
    $rows=[['Прочитано байт',strlen($note)],['Размер одной запрошенной порции, байт',7],['Непустых порций',$chunks],
        ['Пустая строка обозначила конец BLOB',$eof],['Собранный текст совпал с исходным',$note===$expected],
        ['Текст заметки',$note],['Текст после HTML-экранирования',$escaped]];
    $checks=['complete_note'=>$note===$expected,'bytes_match_metadata'=>strlen($note)===$info['length'],
        'multiple_reads'=>$chunks===10,'eof_is_empty_string'=>$eof,'preview_within_budget'=>strlen($note)<=$limit,
        'html_specials_escaped'=>str_contains($escaped,'&lt;кабель&gt;')&&str_contains($escaped,'&amp;'),
        'reader_handle_released'=>$handle===null];
    $observations=['note_text'=>$note,'escaped_for_html'=>$escaped,'bytes'=>strlen($note),'chunk_request_bytes'=>7,
        'nonempty_chunks'=>$chunks,'preview_limit_bytes'=>$limit,'eof_return'=>'','charset'=>'UTF8',
        'escape_applied_after_reassembly'=>true,'handle_closed'=>true];

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

f155Report('fb155-read-note','Прочитать заметку UTF8 порциями и показать её как текст',$rows,$checks,$observations,$runtime,$owned,$removed);
Ожидаемый результат
Прочитано байт — 67
Размер одной запрошенной порции, байт — 7
Непустых порций — 10
Пустая строка обозначила конец BLOB — да
Собранный текст совпал с исходным — да
Текст заметки — Заявка #42
Проверить <кабель> & разъём.

Текст после HTML-экранирования — Заявка #42
Проверить &lt;кабель&gt; &amp; разъём.

Запустите example.php по README.txt в отдельной учебной базе. Программа создаёт одну собственную таблицу, получает данные, сохраняет result.html/result.json/metrics.json и удаляет свою таблицу. Код вспомогательного lab.php открыт; операции записи тестового BLOB разобраны в предыдущей главе.

Открыть результат · Данные и наблюдения JSON.

Границы и возможные ошибки

Этот предпросмотр намеренно держит всю заметку в памяти и ограничен 4096 байтами. Чтение порциями само по себе не делает накопление всей строки экономным. Большой файл передавайте в поток или скачивание; не накапливайте его через .=. Мы сохраняем и читаем известный UTF8; неизвестную кодировку нельзя исправить сменой HTTP-заголовка. SUB_TYPE TEXT — SQL-назначение, но приложению всё равно нужно согласовать кодировку. IBASE_TEXT при fetch может сразу загрузить BLOB целиком; это удобная альтернатива для заведомо небольшого текста, а не замена контролю размера.

Измените требование

Можно ли вызвать htmlspecialchars отдельно для каждой семибайтовой порции, а потом склеить результат?

Разбор

Не в общем случае. Порция может разрезать UTF8-символ, и преобразование заменит его части. Для небольшого текста сначала соберите ограниченный объём, затем экранируйте; для большого нужен корректный потоковый обработчик кодировки.

Выгрузить бинарное вложение в файл без накопления всего BLOB

При переносе вложений в файловое хранилище нужно получить именно исходные байты, включая нулевой байт и значения, которые не являются текстом UTF8. Выгрузим вложение размером 81 000 байт в export.bin порциями до 8192 байт и сверим размер и SHA256. В цикле выгрузки весь BLOB не собирается в одну строку.

Движение данных
  1. BLOB размером 81 000 байт хранится в FILE_DATA
  2. open → ресурс чтения; fopen(export.bin, wb) → ресурс записи
  3. get(8192) → очередная порция; fwrite → файл
  4. Частичная запись fwrite повторяется до конца текущей порции
  5. Пустая строка от get → завершить и закрыть оба ресурса
  6. Размер 81 000 и одинаковый SHA256 подтверждают точность выгрузки

Первый небольшой вызов

$part = ibase_blob_get($handle, 8192);
if ($part === false) { throw new RuntimeException('Ошибка чтения'); }
if ($part !== '') {
    $written = fwrite($out, $part);
    // Полная программа обрабатывает false, ноль и частичную запись.
}

Режим wb сохраняет байты без текстового преобразования. BLOB-ресурс относится к Firebird, файловый ресурс — к PHP; их закрывают разными функциями. get возвращает не более запрошенной порции, fwrite может записать меньше, поэтому внутренний цикл учитывает смещение. SHA256 вычисляется у исходного учебного содержимого и у готового файла; совпадение проверяет все байты, а не только размер.

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

НаблюдениеРезультат
Созданный файлexport.bin
Записано байт81000
Непустых порций чтения10
Самая большая порция, байт8192
Размер совпал с metadataда
SHA256 файла совпал с исходникомда
Первые 9 байт в hex464231353500ff0d0a
SHA256 выгруженного файла8797cedecc25674df8d7c2204744a22c05338efb4c269311ef5194ea7d93ab9e
Полная программа с собственными данными и архивом
Выгрузить бинарное вложение в файл без накопления всего BLOBСамостоятельный пример
<?php
declare(strict_types=1);
require __DIR__.'/lab.php';
$db=null;$owned=$removed=$rows=$checks=$observations=[];
try {
    $config=f155Config();$db=f155Connect($config);$runtime=f155Runtime($db,$config);

    // Fixture creation is explained in the preceding chapter; this chapter teaches reading.
    function f155FixtureBlob($db,array $pieces): string {
        $handle=ibase_blob_create($db);
        if ($handle===false) {f155Failure('Учебный BLOB не создан');}
        try {
            foreach ($pieces as $piece) {
                $add=f155Capture(static fn()=>ibase_blob_add($handle,$piece));
                if ($add['value']===false || $add['warning']) {f155Failure('Порция BLOB не записана',$add);}
            }
            $id=ibase_blob_close($handle);
            if (!is_string($id)) {f155Failure('Записанный BLOB не закрыт');}
            $handle=null;return $id;
        } finally {
            if (is_resource($handle)) {ibase_blob_cancel($handle);$handle=null;}
        }
    }

    $files=f155OwnTable($db,$owned,'export','(ID INTEGER NOT NULL PRIMARY KEY, FILE_DATA BLOB SUB_TYPE BINARY)');
    $q=f155Qualified($files);
    $fixture=str_repeat("FB155\x00\xFF\r\n",9000);
    $expectedBytes=strlen($fixture);$expectedHash=hash('sha256',$fixture);
    $blobId=f155FixtureBlob($db,str_split($fixture,12000));
    f155Command($db,"INSERT INTO $q (ID,FILE_DATA) VALUES (?,?)",[1,$blobId]);
    f155Commit($db);unset($fixture);
    $record=f155Read($db,"SELECT FILE_DATA FROM $q WHERE ID=?",[1])[0];
    $info=ibase_blob_info($db,$record['FILE_DATA']);
    if ($info===false) {f155Failure('Размер вложения недоступен');}
    $handle=ibase_blob_open($db,$record['FILE_DATA']);
    if ($handle===false) {f155Failure('Вложение не открыто');}
    $out=null;$bytes=0;$chunks=0;$maximumChunk=0;
    $path=__DIR__.'/export.bin';
    try {
        // This generated name is fixed by the lesson, never taken from a request or BLOB ID.
        $out=fopen($path,'wb');
        if ($out===false) {throw new RuntimeException('Не удалось создать export.bin');}
        while (true) {
            $part=ibase_blob_get($handle,8192);
            if ($part===false) {f155Failure('Не удалось прочитать порцию вложения');}
            if ($part==='') {break;}
            $length=strlen($part);$offset=0;
            while ($offset<$length) {
                $written=fwrite($out,substr($part,$offset));
                if ($written===false || $written===0) {throw new RuntimeException('Файл записан не полностью');}
                $offset+=$written;
            }
            $bytes+=$length;++$chunks;$maximumChunk=max($maximumChunk,$length);
        }
        if (fflush($out)!==true) {throw new RuntimeException('Файл не сброшен в поток');}
    } finally {
        if (is_resource($out)) {fclose($out);$out=null;}
        if (ibase_blob_close($handle)===false) {f155Failure('Чтение вложения не закрыто');}
        $handle=null;
    }
    $actualHash=hash_file('sha256',$path);
    if (!is_string($actualHash)) {throw new RuntimeException('Не удалось вычислить хеш выгрузки');}
    $prefix=file_get_contents($path,false,null,0,9);
    $rows=[['Созданный файл','export.bin'],['Записано байт',$bytes],['Непустых порций чтения',$chunks],
        ['Самая большая порция, байт',$maximumChunk],['Размер совпал с metadata',$bytes===$info['length']],
        ['SHA256 файла совпал с исходником',$actualHash===$expectedHash],['Первые 9 байт в hex',bin2hex($prefix)],
        ['SHA256 выгруженного файла',$actualHash]];
    $checks=['expected_byte_count'=>$bytes===81000&&$bytes===$expectedBytes,'metadata_length_matches'=>$bytes===$info['length'],
        'bounded_chunks'=>$maximumChunk<=8192&&$chunks===10,'hash_matches'=>$actualHash===$expectedHash,
        'binary_bytes_preserved'=>$prefix==="FB155\x00\xFF\r\n",'file_size_matches'=>filesize($path)===$bytes,
        'both_handles_released'=>$handle===null&&$out===null];
    $observations=['file'=>'export.bin','bytes'=>$bytes,'metadata_bytes'=>$info['length'],'chunk_request_bytes'=>8192,
        'nonempty_chunks'=>$chunks,'maximum_chunk_bytes'=>$maximumChunk,'sha256'=>$actualHash,'expected_sha256'=>$expectedHash,
        'prefix_hex'=>bin2hex($prefix),'entire_blob_accumulated_during_export'=>false,'fixture_created_in_memory_before_export'=>true];

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

f155Report('fb155-export-file','Выгрузить бинарное вложение в файл без накопления всего BLOB',$rows,$checks,$observations,$runtime,$owned,$removed);
Ожидаемый результат
Созданный файл — export.bin
Записано байт — 81000
Непустых порций чтения — 10
Самая большая порция, байт — 8192
Размер совпал с metadata — да
SHA256 файла совпал с исходником — да
Первые 9 байт в hex — 464231353500ff0d0a
SHA256 выгруженного файла — 8797cedecc25674df8d7c2204744a22c05338efb4c269311ef5194ea7d93ab9e

Запустите example.php по README.txt в отдельной учебной базе. Программа создаёт одну собственную таблицу, получает данные, сохраняет result.html/result.json/metrics.json и удаляет свою таблицу. Код вспомогательного lab.php открыт; операции записи тестового BLOB разобраны в предыдущей главе.

Открыть результат · Данные и наблюдения JSON.

Границы и возможные ошибки

8192 — выбранный размер порции, а не обязательный предел функции. Драйвер 3.0.1 умеет внутреннее дробление и больших запросов; запрашивать весь огромный BLOB за раз всё равно означает выделять под него строку. Для самостоятельности тестовый исходник здесь заранее создаётся в памяти, затем удаляется перед выгрузкой: это демонстрация алгоритма чтения, не замер памяти всего процесса. В промышленной миграции задайте лимиты общего размера и пути, используйте временный файл и публикуйте его только после успешной сверки. При ошибке не выдавайте частичный файл за готовый. Фиксированное export.bin в учебном каталоге перезаписывается при повторном запуске.

Для миграции изображений из Firebird пытались применять средства другой СУБД, а требовалось читать каждое вложение клиентом и писать файл. Исходный вопрос на Stack Overflow. Показываем оригинальный PHP-путь: ограниченный SELECT, последовательное чтение порций, полная запись файла и сравнение байтов. Команды SQL Server не переносятся в Firebird.

Измените требование

Файл имеет ожидаемые 81 000 байт. Можно ли пропустить сравнение SHA256?

Разбор

Одинаковый размер не доказывает одинаковое содержимое. Для проверяемого переноса сравните контрольную сумму с достоверным исходником и решите, что делать при несовпадении. Хеш сам по себе не подтверждает автора или безопасность вложения.

Скачать BLOB отдельным HTTP-ответом и проверить полученные байты

В карточке документа есть кнопка скачивания. Она должна получить отдельный ответ с файлом, без шаблона сайта, предупреждений PHP и лишней единицы после успешного вызова. Создадим вложение, запустим отдельный локальный PHP-сервер, выполним настоящий GET и проверим те заголовки и байты, которые получил клиент.

Движение данных
  1. Записать собственное вложение 80 016 байт и явно подтвердить
  2. Закрыть родительское embedded-соединение
  3. Локальный GET /download.php → отдельный PHP HTTP-процесс
  4. Endpoint получает ID из собственной таблицы и задаёт заголовки
  5. ibase_blob_echo → только тело вложения; клиент сохраняет downloaded.bin
  6. Сравнить HTTP 200, Content-Length и SHA256; остановить сервер; удалить учебную таблицу

Первый небольшой вызов

header('Content-Type: application/octet-stream');
header('Content-Disposition: attachment; filename="firebird-attachment.bin"');
if (ibase_blob_echo($db, $blobId) !== true) {
    // Сбой сохраняется в закрытый журнал, а не дописывается в середину файла.
}

Это фрагмент отдельного download.php после подключения, выбора разрешённой записи и проверки размера. header задаёт свойства HTTP-ответа до начала тела. blob_echo сам выводит байты; результат функции — bool, его не выводят через echo. Полный пример включает endpoint, закрытый учебный router и http-lab.php, который запускает сервер только на 127.0.0.1, делает один GET и останавливает процесс в finally. Проверка готовности использует TCP и не добавляет скрытых HTTP-запросов.

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

НаблюдениеРезультат
HTTP-статус200
Content-Typeapplication/octet-stream
Content-Dispositionattachment; filename="firebird-attachment.bin"
Получено байт по HTTP80016
Content-Length совпал с теломда
Полученные байты совпали с исходникомда
SHA256 полученного вложения1fa505ea1e8fe687f0f4e56b0be50fd330cd2af624cf9e25b55a3e85a70115ac
Дочерний HTTP-сервер остановленда
Копия файла у клиентаdownloaded.bin
Полная программа с собственными данными и архивом
Скачать BLOB отдельным HTTP-ответом и проверить полученные байтыСамостоятельный пример
<?php
declare(strict_types=1);
require __DIR__.'/lab.php';
$db=null;$owned=$removed=$rows=$checks=$observations=[];
try {
    $config=f155Config();$db=f155Connect($config);$runtime=f155Runtime($db,$config);

    // Fixture creation is explained in the preceding chapter; this chapter teaches reading.
    function f155FixtureBlob($db,array $pieces): string {
        $handle=ibase_blob_create($db);
        if ($handle===false) {f155Failure('Учебный BLOB не создан');}
        try {
            foreach ($pieces as $piece) {
                $add=f155Capture(static fn()=>ibase_blob_add($handle,$piece));
                if ($add['value']===false || $add['warning']) {f155Failure('Порция BLOB не записана',$add);}
            }
            $id=ibase_blob_close($handle);
            if (!is_string($id)) {f155Failure('Записанный BLOB не закрыт');}
            $handle=null;return $id;
        } finally {
            if (is_resource($handle)) {ibase_blob_cancel($handle);$handle=null;}
        }
    }

    require __DIR__.'/http-lab.php';
    $attachments=f155OwnTable($db,$owned,'http','(ID INTEGER NOT NULL PRIMARY KEY, FILE_DATA BLOB SUB_TYPE BINARY)');
    $q=f155Qualified($attachments);
    $payload="attachment-155\r\n".str_repeat("\x00\xFFab",20000);
    $expectedHash=hash('sha256',$payload);
    $blobId=f155FixtureBlob($db,str_split($payload,10000));
    f155Command($db,"INSERT INTO $q (ID,FILE_DATA) VALUES (?,?)",[1,$blobId]);
    f155Commit($db);
    // The embedded fixture is released before the separate PHP HTTP process opens it.
    if (ibase_close($db)!==true) {f155Failure('Родительское соединение не закрыто');}
    $db=null;
    try {$response=f155HttpDownload($attachments);}
    finally {$db=f155Connect($config);}
    $headers=$response['headers'];$body=$response['body'];
    $bodyBytes=strlen($body);$actualHash=hash('sha256',$body);
    $contentType=$headers['content-type']??'';
    $disposition=$headers['content-disposition']??'';
    $declaredLength=$headers['content-length']??'';
    $sameBytes=$body===$payload;
    $saved=file_put_contents(__DIR__.'/downloaded.bin',$body);
    if ($saved!==$bodyBytes) {throw new RuntimeException('Локальная копия HTTP-ответа не сохранена');}
    $rows=[['HTTP-статус',$response['status']],['Content-Type',$contentType],['Content-Disposition',$disposition],
        ['Получено байт по HTTP',$bodyBytes],['Content-Length совпал с телом',$declaredLength===(string)$bodyBytes],
        ['Полученные байты совпали с исходником',$sameBytes],['SHA256 полученного вложения',$actualHash],
        ['Дочерний HTTP-сервер остановлен',$response['server_stopped']],['Копия файла у клиента','downloaded.bin']];
    $checks=['http_status'=>$response['status']===200,'binary_content_type'=>$contentType==='application/octet-stream',
        'attachment_name'=>$disposition==='attachment; filename="firebird-attachment.bin"',
        'content_length'=>$declaredLength==='80016'&&$bodyBytes===80016,'response_exact_bytes'=>$sameBytes,
        'response_sha256'=>$actualHash===$expectedHash,'no_sniff'=>($headers['x-content-type-options']??'')==='nosniff',
        'real_server_stopped'=>$response['server_started']===true&&$response['server_stopped']===true,
        'same_php_and_driver'=>($headers['x-phpman-php']??'')===PHP_VERSION&&($headers['x-phpman-interbase']??'')===phpversion('interbase'),
        'saved_client_copy'=>hash_file('sha256',__DIR__.'/downloaded.bin')===$expectedHash];
    $observations=['transport'=>'real HTTP over 127.0.0.1','http_status'=>$response['status'],
        'content_type'=>$contentType,'content_disposition'=>$disposition,'content_length'=>$declaredLength,
        'received_bytes'=>$bodyBytes,'received_sha256'=>$actualHash,'expected_sha256'=>$expectedHash,
        'exact_bytes_match'=>$sameBytes,'php_header'=>$headers['x-phpman-php']??null,
        'interbase_header'=>$headers['x-phpman-interbase']??null,'server_started'=>$response['server_started'],
        'server_stopped'=>$response['server_stopped'],'http_requests'=>1,'client_saved_file'=>'downloaded.bin',
        'private_token_published'=>false,'cli_output_buffer_used_as_http_test'=>false];

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

f155Report('fb155-http-download','Скачать BLOB отдельным HTTP-ответом и проверить полученные байты',$rows,$checks,$observations,$runtime,$owned,$removed);
Ожидаемый результат
HTTP-статус — 200
Content-Type — application/octet-stream
Content-Disposition — attachment; filename="firebird-attachment.bin"
Получено байт по HTTP — 80016
Content-Length совпал с телом — да
Полученные байты совпали с исходником — да
SHA256 полученного вложения — 1fa505ea1e8fe687f0f4e56b0be50fd330cd2af624cf9e25b55a3e85a70115ac
Дочерний HTTP-сервер остановлен — да
Копия файла у клиента — downloaded.bin

Запустите example.php по README.txt в отдельной учебной базе. Программа создаёт одну собственную таблицу, получает данные, сохраняет result.html/result.json/metrics.json и удаляет свою таблицу. Код вспомогательного lab.php открыт; операции записи тестового BLOB разобраны в предыдущей главе.

Открыть результат · Данные и наблюдения JSON.

Границы и возможные ошибки

Локальный router ограничивает учебный запрос внутренним временным токеном; это изоляция теста, не готовая авторизация пользователей сайта. В публичном endpoint нужно проверять доступ к конкретному документу, безопасно формировать имя, управлять кешированием и политикой размеров. MIME в примере известен заранее; заголовок не меняет содержимое. Серверная выдача не собирает BLOB в PHP-строку, но ограниченный тестовый клиент держит ответ для побайтовой сверки — это не измерение памяти больших загрузок. Повторный запуск перезаписывает downloaded.bin. Дочерний процесс использует точный локальный PHP/драйвер по README; его нельзя заменять CLI-буферизацией и называть HTTP-проверкой.

В одном ответе смешивались бинарные байты и HTML; кроме того, содержимое имело служебную обёртку перед BMP. Исходный вопрос на Stack Overflow. Отделяем endpoint вложения от HTML-страницы и сохраняем известный формат. Историческое удаление восьми байт помогло конкретному формату, но не является правилом для любого BLOB; MIME-заголовок не преобразует файл.

Измените требование

Почему запись echo ibase_blob_echo($db, $blobId) может испортить скачанный файл?

Разбор

Функция уже отправила содержимое. Внешний echo дополнительно напечатает true как 1, и это станет лишним байтом. Сохраняйте bool в переменную либо проверяйте его условием; не добавляйте результат вызова к телу файла.

Справочник чтения BLOB

ФункцияЗадача
ibase_blob_infoУзнать длину и сведения о BLOB до чтения содержимого.
ibase_blob_openОткрыть сохранённый BLOB для последовательного чтения.
ibase_blob_getПолучить следующую ограниченную порцию байтов.
ibase_blob_echoНаправить содержимое BLOB в отдельный ответ.

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

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

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

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

ibase_blob_infoУзнать длину и сведения о BLOB до чтения содержимого.

Встроено в PHP 5 и PHP 7 до 7.4; далее внешнее расширение. Проверенный профиль: interbase 3.0.1 / PHP 8.2. · interbase

ibase_blob_info(resource $connection, string $blobId): array|false

Список вложений должен показать размер и решить, стоит ли делать предпросмотр. ibase_blob_info получает сведения о сохранённом BLOB без передачи его полного тела в строку PHP. Это отдельный шаг после SELECT, который вернул идентификатор вложения.

Решение по размеру
  1. Строка вложения → BLOB ID
  2. info(ID) → length = 36 байт
  3. Лимит предпросмотра 32 байта → нужна отдельная выдача
  4. Тело файла не собирается в строку для этого решения
ibase_blob_info(string $blobId): array|false
ibase_blob_info(resource $connection, string $blobId): array|false
АргументСмысл
$connectionСоединение с базой, которой принадлежит ID. При опущенном аргументе используется последнее открытое соединение.
$blobIdСтроковый идентификатор, полученный из BLOB-поля, а не путь к файлу, не содержимое и не открытый BLOB-ресурс.
Индекс / имя в 3.0.1Что означает
0 / lengthПолная длина в байтах. Это не число символов и не ширина изображения.
1 / numsegЧисло сегментов хранения.
2 / maxsegРазмер наибольшего сегмента в байтах.
3 / streamtrue для stream BLOB, false для segmented BLOB. Не PHP stream и не признак HTTP-передачи.
4 / isnullПризнак нулевого BLOB ID в реализации драйвера. SQL NULL из строки проверяется отдельно до вызова.

Официальное описание перечисляет длину, сегменты и способ хранения; именованные ключи, isnull и false при отказе дополнительно сверены с драйвером 3.0.1. Неверный ID, недоступное вложение либо неподходящий контекст требуют немедленной диагностики. PHP-ошибка типа аргумента не обязана превратиться в обычный false. Флагов у info нет; SQL NULL нельзя подменять пустой строкой ID.

Размер и отсутствие вложения в списке

if ($row['FILE_DATA'] === null) {
    $label = 'Не прикреплено';
} else {
    $info = ibase_blob_info($db, $row['FILE_DATA']);
    if ($info === false) { throw new RuntimeException('Метаданные недоступны'); }
    $label = $info['length'] . ' байт';
}

Самостоятельная программа показывает заполненный файл, пустой BLOB и отсутствие вложения. Нулевая длина пустого файла сохраняет его отдельный смысл.

Ограничить предпросмотр до чтения тела

$info = ibase_blob_info($db, $blobId);
if ($info === false) { throw new RuntimeException('Размер неизвестен'); }
if ($info['length'] > 4096) {
    $mode = 'Скачивание отдельным запросом';
} else {
    $mode = 'Ограниченный текстовый предпросмотр';
}

Предел выбирается продуктом. Пример заметки дополнительно контролирует фактически собранные байты. Метаданные не определяют MIME, расширение, кодировку или безопасность файла; такие свойства проверяют при записи и хранят отдельно. Перейдите к ibase_blob_open и ibase_blob_echo.

ibase_blob_openОткрыть сохранённый BLOB для последовательного чтения.

Встроено в PHP 5 и PHP 7 до 7.4; далее внешнее расширение. Проверенный профиль: interbase 3.0.1 / PHP 8.2. · interbase

ibase_blob_open(resource $connection, string $blobId): resource|false

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

Три разных значения
  1. SELECT → строковый $blobId
  2. open($db, $blobId) → ресурс $handle
  3. get($handle, 8192) → строка очередных байтов
  4. close($handle), затем $handle = null
ibase_blob_open(string $blobId): resource|false
ibase_blob_open(resource $connection, string $blobId): resource|false
Аргумент/возвратНазначение
$connectionСоединение с нужной базой. Без него выбирается последнее открытое соединение.
$blobIdИдентификатор существующего BLOB из результата SELECT. SQL NULL обрабатывается раньше.
resourceОткрытый ресурс для ibase_blob_get; это не обычный файловый ресурс fopen.
falseОткрыть BLOB не удалось. Сохраните диагностику до других драйверных вызовов.

Открытие выполняется для чтения, не для дополнения содержимого. Для нового BLOB нужен ibase_blob_create. Ресурс связан с подключением и транзакционным контекстом: закончите чтение до commit/rollback/close. Не переносите ID между базами и не считайте его публичной ссылкой, которая сама проверяет права пользователя.

Прочитать ограниченную заметку

$handle = ibase_blob_open($db, $row['NOTE_TEXT']);
if ($handle === false) { throw new RuntimeException('Заметка не открыта'); }
try {
    $firstPart = ibase_blob_get($handle, 7);
    if ($firstPart === false) { throw new RuntimeException('Чтение не удалось'); }
} finally {
    if (ibase_blob_close($handle) === false) { throw new RuntimeException('BLOB не закрыт'); }
    $handle = null;
}

Это контекстный первый шаг: полная программа читает все порции, проверяет предел и собирает UTF8 до экранирования. Пустой BLOB даёт корректное чтение без данных; это не ошибка открытия.

Начать отдельное чтение для выгрузки

$handle = ibase_blob_open($db, $blobId);
if ($handle === false) { throw new RuntimeException('Вложение не открыто'); }
$out = fopen(__DIR__ . '/export.bin', 'wb');
// Два разных ресурса: get читает BLOB, fwrite пишет файл.
// Полный пример проверяет создание файла и закрывает оба ресурса в finally.

Запускаемая выгрузка создаёт файл 81 000 байт. Для повторного чтения с начала закройте прежний BLOB и откройте новый ресурс; API open не является функцией перемотки существующего handle. Не проверяйте пригодность уже закрытого handle только через is_resource; убирайте ссылку после close. В драйвере 3.0.1 close читателя возвращает строковый ID, поэтому отказ проверяется === false, а не ожиданием только true. Дальше: ibase_blob_get и ibase_blob_close.

ibase_blob_getПолучить следующую ограниченную порцию байтов.

Встроено в PHP 5 и PHP 7 до 7.4; далее внешнее расширение. Проверенный профиль: interbase 3.0.1 / PHP 8.2. · interbase

ibase_blob_get(resource $blobHandle, int $length): string|false

Заметку нужно получить в переменную для обработки, а большое вложение — перенести порциями в файл. ibase_blob_get читает следующую часть уже открытого BLOB. Вы выбираете максимальный размер этой части и решаете, куда направить полученные байты.

Последовательное чтение
  1. Открытый handle стоит в начале BLOB
  2. get(handle, 8192) → первые байты
  3. Следующий get → продолжение после уже прочитанного
  4. Порция уходит в файл или ограниченную заметку
  5. Пустая строка → конец; false → отказ чтения
ibase_blob_get(resource $blobHandle, int $length): string|false
Аргумент/результатПравило
$blobHandleРесурс, открытый ibase_blob_open для чтения. Ресурс ibase_blob_create для записи не подходит.
$lengthМаксимум возвращаемых байт. Используйте положительное ограниченное число, выбранное приложением; это не число символов.
stringОчередная порция, длина которой не больше запрошенной. В проверенном профиле пустая строка означает конец чтения.
falseОшибка чтения. Отличайте строго от пустой строки и от содержимого "0".

Следующий вызов продолжает чтение, а не начинает BLOB заново. Не передавайте отрицательную длину, произвольный размер из HTTP-запроса или весь огромный length без ограничения: функция выделяет память под результат. Значение 65535 относится к внутренним сегментам; реализация 3.0.1 сама дробит более крупный запрос. Наши 8192 байта — осознанный размер порции, не универсальный предел API. Флагов и параметра кодировки здесь нет.

Базовый случай: маленький BLOB целиком

$info = ibase_blob_info($db, $blobId);
if ($info === false || $info['length'] > 4096) { throw new RuntimeException('Нужен другой способ выдачи'); }
$handle = ibase_blob_open($db, $blobId);
if ($handle === false) { throw new RuntimeException('BLOB не открыт'); }
try {
    $value = $info['length'] === 0 ? '' : ibase_blob_get($handle, $info['length']);
    if ($value === false) { throw new RuntimeException('Чтение не удалось'); }
} finally {
    if (ibase_blob_close($handle) === false) { throw new RuntimeException('BLOB не закрыт'); }
    $handle = null;
}

Так сохраняется смысл базового примера PHP Manual: содержимое попадает в переменную, а не сразу в вывод. Добавлены ограничение размера, отдельный пустой BLOB и завершение ресурса. Для заметки UTF8 в полном сценарии показаны несколько вызовов и обработка целого текста после сборки.

Большой файл — без накопления всех частей

while (true) {
    $part = ibase_blob_get($handle, 8192);
    if ($part === false) { throw new RuntimeException('Ошибка чтения'); }
    if ($part === '') { break; }
    // Передать текущую part в файл; не добавлять её в общий массив или строку.
}

Полный экспорт обрабатывает частичную fwrite, закрывает оба ресурса и сверяет SHA256. Цикл get не исправляет кодировку, не декодирует base64 и не снимает служебный формат чужого приложения. Перед отправкой текста в HTML выполняйте экранирование; бинарные файлы так не преобразуют. Для прямой выдачи тела есть ibase_blob_echo, для характеристик — ibase_blob_info.

ibase_blob_echoНаправить содержимое BLOB в отдельный ответ.

Встроено в PHP 5 и PHP 7 до 7.4; далее внешнее расширение. Проверенный профиль: interbase 3.0.1 / PHP 8.2. · interbase

ibase_blob_echo(resource $connection, string $blobId): bool

Пользователь нажал «Скачать вложение». ibase_blob_echo открывает сохранённый BLOB и направляет его байты в стандартный вывод PHP. В отдельном HTTP-обработчике это тело ответа браузеру. Функция не создаёт HTML-страницу, не определяет MIME и не возвращает содержимое строкой.

Скачивание вложения
  1. Браузер → отдельный download.php
  2. Сервер проверяет доступ и выбирает BLOB ID
  3. Заголовки описывают известный формат и имя файла
  4. blob_echo передаёт только байты BLOB в тело ответа
  5. Возврат true подтверждает выполнение функции; клиент отдельно получает тело
ibase_blob_echo(string $blobId): bool
ibase_blob_echo(resource $connection, string $blobId): bool
Аргумент/возвратОбъяснение
$connectionСоединение нужной базы, иначе последнее открытое соединение.
$blobIdID сохранённого BLOB, не открытый handle и не содержимое.
true / falseУспех или отказ передачи со стороны вызова. Это не строка файла и не доказательство, что удалённый пользователь сохранил файл.
Побочный эффектТело уже уходит в стандартный вывод. В CLI это stdout; HTTP-проверку нельзя заменить одним ob_start в CLI.

Отдельный ответ для скачивания

// Контекст: права проверены, blobId выбран по разрешённой записи.
header('Content-Type: application/octet-stream');
header('Content-Disposition: attachment; filename="attachment.bin"');
header('X-Content-Type-Options: nosniff');
if (ibase_blob_echo($db, $blobId) !== true) {
    // Не дописывать HTML или сырой текст ошибки в уже начатое тело файла.
}

Самостоятельный HTTP-сценарий делает реальный локальный GET и сравнивает статус, заголовки, размер и SHA256 полученных байтов. Пример хранит собственную таблицу и не является готовой авторизацией публичного файлового сервиса.

Изображение как отдельный ресурс HTML-страницы

// image.php отдаёт только известный и проверенный PNG:
header('Content-Type: image/png');
header('X-Content-Type-Options: nosniff');
$ok = ibase_blob_echo($db, $pngBlobId);
// А на другой HTML-странице: <img src="image.php?id=42" alt="Фото товара">

Контекстный вариант требует проверки прав на запись и действительно сохранённых PNG-байтов. Смена Content-Type не превращает BMP, JPEG или служебный контейнер в PNG. Если нужны преобразования, сначала прочитайте и обработайте изображение соответствующей библиотекой. Данный пакет проверяет бинарное скачивание; отдельный PNG endpoint не заявлен как ещё один выполненный MAIN.

До заголовков не должно быть BOM, пробелов вне PHP, debug-echo или HTML; после байтов нельзя добавлять шаблон страницы. Не пишите echo ibase_blob_echo(...): true превратится в лишнюю единицу в конце файла. Content-Length должен соответствовать фактическим байтам, а имя в Content-Disposition формируется безопасно. Функция не поддерживает сама авторизацию, диапазоны Range, кеширование или ограничение размера; это обязанности HTTP-слоя. Если вывод уже начался и чтение оборвалось, не заменяйте середину файла красивой HTML-ошибкой. Сначала сохраняйте диагностику в закрытый журнал. Альтернативы: получить данные для обработки и сохранить файл в BLOB.

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

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

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

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

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