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

Дерево руководства
Глава 54 · После файлов, параметризованного SQLSRV и чтения полей результата
SQLSRV: вложения потоком, проверка целостности и повторная отправка
Акт и журнал сохраняются как точные байты: автоматическая и пошаговая отправка, повторное использование потока, SHA-256 и различие NULL/пустого файла.
Прикрепить файл и понять, что именно передаётся
Нужно приложить к заявке акт, сохранить журнал устройства или выдать сохранённый документ. Важно получить тот же файл целиком, различать отсутствие и пустой файл, а при повторной отправке понимать позицию источника. Поток позволяет читать следующие байты порциями; это средство работы с данными, а не особый формат файла.
- Файл на диске → fopen(rb) → ресурс с текущей позицией
- Параметр SQLSRV STREAM(BINARY) → столбец VARBINARY(MAX)
- SELECT конкретного id → get_field(STREAM(BINARY))
- Чтение порциями → счётчик байтов и SHA-256 → карточка документа
Предварительно пройдите подключение SQLSRV, параметризованный запрос и работу с файлами. В коде db — соединение, stmt — ресурс команды, input — файловый поток; это три разных ресурса. Таблица q создаётся только для учебной программы. Примеры работают с уже существующими локальными файлами из архива, а не принимают пользовательский путь из запроса.
Увидеть поток до знакомства с базой
$input = fopen(__DIR__ . "/attachment.txt", "rb");
if ($input === false) { throw new RuntimeException("Файл не открыт"); }
try {
$firstBytes = fread($input, 4);
// Это первые четыре байта, а не обязательно четыре буквы UTF-8.
rewind($input); // Вернуть позицию в начало перед отправкой целиком.
} finally {
fclose($input);
}fopen открывает доступ, fread читает порцию и двигает позицию, rewind возвращает её в начало, fclose закрывает доступ. Поток напоминает рулон с отметкой места чтения, но аналогия не обещает возможность перемотать любой источник: для сети она может отсутствовать. Байт — единица хранимых данных; русская буква UTF-8 обычно занимает несколько байтов. Бинарный режим означает сохранение байтов; он подходит и текстовому файлу, если нужно сохранить его как вложение без перекодировки.
| Что требуется | Подход |
|---|---|
| Обычная запись локального файла | Передать открытый поток параметром; автоматическая отправка по умолчанию. |
| Управлять шагами отправки | SendStreamParamsAtExec=false и цикл sqlsrv_send_stream_data с проверкой окончания. |
| Повторить или заменить вложение | Задать позицию источника, сохранить ссылки параметров, открыть новый поток при замене. |
| Показать «файл не приложен» отдельно от пустого | Хранить и читать NULL как отдельное состояние, не подменять пустой строкой. |
VARBINARY(MAX) хранит байты; это не обещание неограниченного размера и не автоматический выбор лучшего хранилища для любого сервиса. Маленькие учебные файлы позволяют увидеть весь путь; большой файл читают порциями без полного накопления в строку PHP. На сервере выбирается только нужное вложение по id, а списку карточек обычно нужны имя и размер без payload.
Сохранить акт осмотра и прочитать те же байты из базы
К карточке осмотра нужно прикрепить акт и позже получить его без потери символов или обрезки. Файл уже лежит на диске: откроем его, передадим поток в VARBINARY(MAX), прочитаем обратно и покажем текст, длину и контрольную сумму. В этой задаче база хранит точные байты файла; она не разбирает документ на строки SQL.
- attachment.txt → fopen(rb): открытый поток, не текст целиком
- query с параметром STREAM(BINARY) → VARBINARY(MAX)
- SELECT только filename,признак NULL,размер,payload WHERE id=1
- get_field: метаданные0–2, затем поток3 → fread по4096байт → SHA-256
- Квитанция: акт виден, размер и SHA-256 совпадают; ресурсы закрыты
Сначала небольшой вызов
$input = fopen(__DIR__ . "/attachment.txt", "rb");
// Проверить input !== false.
$params = [[$input, SQLSRV_PARAM_IN,
SQLSRV_PHPTYPE_STREAM(SQLSRV_ENC_BINARY), SQLSRV_SQLTYPE_VARBINARY("max")]];
$stmt = sqlsrv_query($db, "UPDATE $q SET payload = ? WHERE id = 1", $params);
// По умолчанию поток отправляет сам query. Проверить stmt, затем закрыть ресурсы.Поток — открытый источник, из которого читают следующие байты порциями. Переменная input содержит ресурс доступа, а не весь файл. rb означает чтение в бинарном режиме: на Windows это важно для сохранения байтов без текстового преобразования переводов строк. В параметре указаны направление IN, PHP-поток с бинарной кодировкой и SQL-тип VARBINARY(MAX). Вопросительный знак принимает значение параметра, не имя таблицы. Автоматическая отправка не означает, что PHP сначала сделал file_get_contents всего файла. Для readback функция readAttachment151 читает поля по порядку, считает длину и SHA-256 и накапливает текст только для маленького учебного акта.
Что получится
| Наблюдение | Результат |
|---|---|
| Файл в карточке | attachment.txt |
| Байт сохранено и прочитано | 87 |
| Содержимое акта | Акт осмотра №151 | Объект: склад | Проверено: 3 линии |
| SHA-256 прочитанного файла | 8f604ecbd03a06be01e1a8b38edfb5ee4316a6758d87ec9a8eccadaebe72377e |
| Размер и SHA-256 совпали с исходником | да |
<?php
declare(strict_types=1);
require __DIR__.'/lab.php';
$db=$config=null;$owned=$removed=$rows=$checks=$observations=[];
$runtime=s151Runtime();
try {
$config=s151Config();$db=s151Connect($config);$runtime=s151Runtime($db,$config);
function readAttachment151($db,string $q,int $id,bool $showKnownUtf8Text=false): array {
$stmt=s151Query($db,"SELECT filename,CASE WHEN payload IS NULL THEN 1 ELSE 0 END AS missing,CAST(DATALENGTH(payload) AS int) AS byte_count,payload FROM $q WHERE id = ?",[$id]);
$blob=null;
try {
$state=sqlsrv_fetch($stmt);
if ($state===false) {s151Failure('Чтение вложения');}
if ($state===null) {throw new RuntimeException('Вложение с заданным id отсутствует.');}
$filename=sqlsrv_get_field($stmt,0);
if ($filename===false) {s151Failure('Имя вложения');}
$missing=sqlsrv_get_field($stmt,1);
if ($missing===false) {s151Failure('Признак NULL');}
$sqlBytes=sqlsrv_get_field($stmt,2);
if ($sqlBytes===false) {s151Failure('Размер вложения');}
// A missing attachment is represented explicitly, not turned into an empty stream.
if ($missing===1) {
if ($sqlBytes!==null) {throw new RuntimeException('У SQL NULL ожидался NULL-размер.');}
return ['filename'=>$filename,'is_null'=>true,'bytes'=>null,'sha256'=>null,'preview'=>null];
}
$blob=sqlsrv_get_field($stmt,3,SQLSRV_PHPTYPE_STREAM(SQLSRV_ENC_BINARY));
if ($blob===false) {s151Failure('Получение бинарного потока');}
if (!is_resource($blob)) {throw new RuntimeException('Для существующего SQL-значения ожидался поток.');}
$hash=hash_init('sha256');$bytes=0;$preview='';
while (!feof($blob)) {
$chunk=fread($blob,4096);
if ($chunk===false) {throw new RuntimeException('Не удалось прочитать поток вложения.');}
if ($chunk==='') {
if (feof($blob)) {break;}
throw new RuntimeException('Чтение потока не продвинулось.');
}
$bytes+=strlen($chunk);hash_update($hash,$chunk);
// Enabled only for known UTF-8 text fixtures, never guessed from binary size.
if ($showKnownUtf8Text && $sqlBytes<=256) {$preview.=$chunk;}
}
$readErrors=sqlsrv_errors(SQLSRV_ERR_ERRORS);
if ($readErrors!==null) {throw new RuntimeException('SQLSRV сообщил об ошибке чтения потока.');}
if ($bytes!==$sqlBytes) {throw new RuntimeException('Прочитанный размер отличается от DATALENGTH.');}
return ['filename'=>$filename,'is_null'=>false,'bytes'=>$bytes,'sha256'=>hash_final($hash),'preview'=>$preview];
} finally {
if (is_resource($blob)) {fclose($blob);}
sqlsrv_free_stmt($stmt);
}
}
$name=s151OwnTable($db,$owned,'auto','(id int NOT NULL PRIMARY KEY, filename nvarchar(80) NOT NULL, payload varbinary(max) NULL)');
$q=s151Qualified($name);
$path=__DIR__.'/attachment.txt';
$expectedBytes=filesize($path);$expectedHash=hash_file('sha256',$path);
if ($expectedBytes===false || $expectedHash===false) {throw new RuntimeException('Не удалось прочитать учебный файл.');}
$input=fopen($path,'rb');
if ($input===false) {throw new RuntimeException('Не удалось открыть attachment.txt.');}
try {
$params=[1,'attachment.txt',[$input,SQLSRV_PARAM_IN,SQLSRV_PHPTYPE_STREAM(SQLSRV_ENC_BINARY),SQLSRV_SQLTYPE_VARBINARY('max')]];
// Default SendStreamParamsAtExec=true: query sends the stream itself.
$insert=s151Query($db,"INSERT INTO $q (id,filename,payload) VALUES (?,?,?)",$params);
sqlsrv_free_stmt($insert);
} finally {fclose($input);}
$saved=readAttachment151($db,$q,1,true);
$rows=[['Файл в карточке',$saved['filename']],['Байт сохранено и прочитано',$saved['bytes']],
['Содержимое акта',str_replace("\n",' | ',trim($saved['preview']))],['SHA-256 прочитанного файла',$saved['sha256']],
['Размер и SHA-256 совпали с исходником',$saved['bytes']===$expectedBytes&&$saved['sha256']===$expectedHash]];
$checks=['stored_filename'=>$saved['filename']==='attachment.txt','not_null'=>$saved['is_null']===false,
'byte_length'=>$saved['bytes']===$expectedBytes,'hash_equal'=>$saved['sha256']===$expectedHash,
'visible_text'=>$saved['preview']==="Акт осмотра №151\nОбъект: склад\nПроверено: 3 линии\n",'input_closed'=>!is_resource($input)];
$observations=['attachment'=>$saved,'input_bytes'=>$expectedBytes,'input_sha256'=>$expectedHash,'send_mode'=>'automatic'];
} finally {
if (is_resource($db)) {try {$removed=s151Cleanup($db,$owned);} finally {sqlsrv_close($db);}}
}
s151Report('sqlsrv151-stream-automatic','Сохранить акт осмотра и прочитать те же байты из базы',$rows,$checks,$observations,$runtime,$owned,$removed);
Ожидаемый результат
Файл в карточке — attachment.txt
Байт сохранено и прочитано — 87
Содержимое акта — Акт осмотра №151 | Объект: склад | Проверено: 3 линии
SHA-256 прочитанного файла — 8f604ecbd03a06be01e1a8b38edfb5ee4316a6758d87ec9a8eccadaebe72377e
Размер и SHA-256 совпали с исходником — да
Локальная программа включает исходные текстовые файлы, lab.php и README.txt. Она создаёт одну собственную таблицу SQL Server, сохраняет result.html/result.json/metrics.json и удаляет свою таблицу. Исходные файлы остаются рядом для сравнения.
Фактический результат запуска · Данные результата JSON.
Ограничения и ошибки
Не передавайте путь к файлу вместо открытого ресурса и не закрывайте input до окончания отправки. На чтении не возвращайтесь к столбцу имени после получения бинарного потока; сначала метаданные, затем поток. SHA-256 здесь подтверждает одинаковые байты, а не доверие к отправителю. Для больших вложений сравните хранение в базе и объектном/файловом хранилище: важны резервное копирование, размер базы, права и выдача. Это локальный сценарий передачи SQLSRV; загрузка файла из браузера, проверка MIME и авторизация рассматриваются отдельно.
Поток документа перестал быть пригодным для чтения после попытки вернуться к более раннему столбцу результата. Исходная ситуация на Stack Overflow. Сначала читать имя и метаданные по возрастанию индексов, затем бинарный столбец. Полностью прочитать и закрыть его поток до следующей строки или освобождения statement.
Попробуйте изменить
Замените в attachment.txt «3 линии» на «4 линии». Сначала предположите, что изменится в отчёте.
Как проверить решение
Текст и SHA-256 изменятся, длина останется прежней для замены одной цифры другой. Код сравнивает с новым исходником; статическое ожидание учебного текста тоже следует обновить. Совпадение размера без совпадения SHA-256 недостаточно для проверки содержимого.
Отправить журнал порциями и подтвердить полный файл
У устройства накопился журнал из 600 событий. При явной отправке приложение само вызывает очередной шаг, а после завершения проверяет, что в базе весь журнал. В учебном файле больше 8 КБ: одного шага недостаточно. Отчёт показывает фактическую длину и SHA-256; число вызовов сохранено отдельно как наблюдение, а не как обещанный процент загрузки.
- dispatch-log.txt:600записей,24600байт
- query с SendStreamParamsAtExec=false → statement ожидает отправки параметра
- send_stream_data → передать до8КБ; true → есть ещё данные
- SQLSRV5.13.3: null → закончено; false → ошибка
- SELECT payload потоком → размер и SHA-256 совпали с файлом
Сначала небольшой вызов
while (true) {
$more = sqlsrv_send_stream_data($stmt);
if ($more === false) {
$errors = sqlsrv_errors(SQLSRV_ERR_ERRORS);
throw new RuntimeException("Ошибка передачи");
}
if ($more === null) { break; }
}SendStreamParamsAtExec=false выключает автоматическую отправку параметров-потоков при выполнении. Ресурс stmt уже существует, но файл ещё нужно передать. В проверенном SQLSRV 5.13.3 один вызов отправляет до 8 КБ; true означает продолжение, null — нормальное завершение, false — ошибку. Короткое описание PHP.net/Microsoft говорит о bool и не отражает этот null; полный контракт подтверждён исходниками версии и запуском. Поэтому цикл явно разделяет три состояния. Перед сообщением «готово» проверяются диагностика и фактически сохранённое содержимое; значение more не является числом байтов.
Что получится
| Наблюдение | Результат |
|---|---|
| Файл журнала | dispatch-log.txt |
| Байт в журнале | 24600 |
| Байт прочитано из базы | 24600 |
| Для отправки потребовалось несколько вызовов | да |
| Завершающий null без ошибки | да |
| SHA-256 прочитанного журнала | 2c500d864816bedf0130a090f3e2611886cbcb8be7f127e60bb5258bde06fa8b |
| Журнал сохранился полностью | да |
<?php
declare(strict_types=1);
require __DIR__.'/lab.php';
$db=$config=null;$owned=$removed=$rows=$checks=$observations=[];
$runtime=s151Runtime();
try {
$config=s151Config();$db=s151Connect($config);$runtime=s151Runtime($db,$config);
function readAttachment151($db,string $q,int $id,bool $showKnownUtf8Text=false): array {
$stmt=s151Query($db,"SELECT filename,CASE WHEN payload IS NULL THEN 1 ELSE 0 END AS missing,CAST(DATALENGTH(payload) AS int) AS byte_count,payload FROM $q WHERE id = ?",[$id]);
$blob=null;
try {
$state=sqlsrv_fetch($stmt);
if ($state===false) {s151Failure('Чтение вложения');}
if ($state===null) {throw new RuntimeException('Вложение с заданным id отсутствует.');}
$filename=sqlsrv_get_field($stmt,0);
if ($filename===false) {s151Failure('Имя вложения');}
$missing=sqlsrv_get_field($stmt,1);
if ($missing===false) {s151Failure('Признак NULL');}
$sqlBytes=sqlsrv_get_field($stmt,2);
if ($sqlBytes===false) {s151Failure('Размер вложения');}
// A missing attachment is represented explicitly, not turned into an empty stream.
if ($missing===1) {
if ($sqlBytes!==null) {throw new RuntimeException('У SQL NULL ожидался NULL-размер.');}
return ['filename'=>$filename,'is_null'=>true,'bytes'=>null,'sha256'=>null,'preview'=>null];
}
$blob=sqlsrv_get_field($stmt,3,SQLSRV_PHPTYPE_STREAM(SQLSRV_ENC_BINARY));
if ($blob===false) {s151Failure('Получение бинарного потока');}
if (!is_resource($blob)) {throw new RuntimeException('Для существующего SQL-значения ожидался поток.');}
$hash=hash_init('sha256');$bytes=0;$preview='';
while (!feof($blob)) {
$chunk=fread($blob,4096);
if ($chunk===false) {throw new RuntimeException('Не удалось прочитать поток вложения.');}
if ($chunk==='') {
if (feof($blob)) {break;}
throw new RuntimeException('Чтение потока не продвинулось.');
}
$bytes+=strlen($chunk);hash_update($hash,$chunk);
// Enabled only for known UTF-8 text fixtures, never guessed from binary size.
if ($showKnownUtf8Text && $sqlBytes<=256) {$preview.=$chunk;}
}
$readErrors=sqlsrv_errors(SQLSRV_ERR_ERRORS);
if ($readErrors!==null) {throw new RuntimeException('SQLSRV сообщил об ошибке чтения потока.');}
if ($bytes!==$sqlBytes) {throw new RuntimeException('Прочитанный размер отличается от DATALENGTH.');}
return ['filename'=>$filename,'is_null'=>false,'bytes'=>$bytes,'sha256'=>hash_final($hash),'preview'=>$preview];
} finally {
if (is_resource($blob)) {fclose($blob);}
sqlsrv_free_stmt($stmt);
}
}
$name=s151OwnTable($db,$owned,'chunks','(id int NOT NULL PRIMARY KEY, filename nvarchar(80) NOT NULL, payload varbinary(max) NULL)');
$q=s151Qualified($name);$path=__DIR__.'/dispatch-log.txt';
$expectedBytes=filesize($path);$expectedHash=hash_file('sha256',$path);
if ($expectedBytes===false || $expectedHash===false) {throw new RuntimeException('Не удалось прочитать журнал.');}
$input=fopen($path,'rb');
if ($input===false) {throw new RuntimeException('Не удалось открыть журнал.');}
$stmt=null;$calls=0;$moreCalls=0;$finalErrors=null;
try {
$params=[1,'dispatch-log.txt',[$input,SQLSRV_PARAM_IN,SQLSRV_PHPTYPE_STREAM(SQLSRV_ENC_BINARY),SQLSRV_SQLTYPE_VARBINARY('max')]];
$stmt=s151Query($db,"INSERT INTO $q (id,filename,payload) VALUES (?,?,?)",$params,['SendStreamParamsAtExec'=>false]);
while (true) {
$more=sqlsrv_send_stream_data($stmt);$calls++;
if ($more===false) {
// SQLSRV 5.13.3: false is failure, null is normal completion.
$finalErrors=sqlsrv_errors(SQLSRV_ERR_ERRORS);
throw new RuntimeException('Поток не отправлен: '.json_encode(s151Diagnostics($finalErrors)));
}
if ($more===null) {$finalErrors=sqlsrv_errors(SQLSRV_ERR_ERRORS);break;}
$moreCalls++;
}
} finally {
if (is_resource($stmt)) {sqlsrv_free_stmt($stmt);}
fclose($input);
}
$saved=readAttachment151($db,$q,1);
$rows=[['Файл журнала',$saved['filename']],['Байт в журнале',$expectedBytes],['Байт прочитано из базы',$saved['bytes']],
['Для отправки потребовалось несколько вызовов',$calls>1&&$moreCalls>0],['Завершающий null без ошибки',$more===null&&$finalErrors===null],
['SHA-256 прочитанного журнала',$saved['sha256']],['Журнал сохранился полностью',$saved['bytes']===$expectedBytes&&$saved['sha256']===$expectedHash]];
$checks=['fixture_larger_than_chunk'=>$expectedBytes>8192,'multiple_send_calls'=>$calls>1&&$moreCalls>0,
'terminal_null_clean'=>$more===null&&$finalErrors===null,'byte_length'=>$saved['bytes']===$expectedBytes,
'hash_equal'=>$saved['sha256']===$expectedHash,'large_body_not_accumulated'=>$saved['preview']==='',
'input_closed'=>!is_resource($input)];
$observations=['attachment'=>$saved,'send_calls'=>$calls,'true_returns'=>$moreCalls,'final_errors'=>s151Diagnostics($finalErrors),
'input_bytes'=>$expectedBytes,'input_sha256'=>$expectedHash,'send_mode'=>'explicit_chunks'];
} finally {
if (is_resource($db)) {try {$removed=s151Cleanup($db,$owned);} finally {sqlsrv_close($db);}}
}
s151Report('sqlsrv151-stream-chunks','Отправить журнал порциями и подтвердить полный файл',$rows,$checks,$observations,$runtime,$owned,$removed);
Ожидаемый результат
Файл журнала — dispatch-log.txt
Байт в журнале — 24600
Байт прочитано из базы — 24600
Для отправки потребовалось несколько вызовов — да
Завершающий null без ошибки — да
SHA-256 прочитанного журнала — 2c500d864816bedf0130a090f3e2611886cbcb8be7f127e60bb5258bde06fa8b
Журнал сохранился полностью — да
Локальная программа включает исходные текстовые файлы, lab.php и README.txt. Она создаёт одну собственную таблицу SQL Server, сохраняет result.html/result.json/metrics.json и удаляет свою таблицу. Исходные файлы остаются рядом для сравнения.
Фактический результат запуска · Данные результата JSON.
Ограничения и ошибки
Не вычисляйте прогресс как calls×8192: порции могут быть меньше, а завершающий вызов тоже участвует в управлении протоколом. При нескольких потоковых параметрах функция обслуживает их через statement; это не отдельный запрос INSERT на каждую порцию. false — отказ, даже если первое выполнение команды было успешным. Не объявляйте переданным весь файл по одному query с отключённой автоматической отправкой. Читать большой payload через stream_get_contents целиком означало бы снова выделить память под весь файл; здесь хеш считается постепенно.
Разработчик опасается, что драйвер обрезает бинарное значение, и пробует уменьшить тип поля, теряя часть данных. Исходная ситуация на Stack Overflow. Не обрезать файл ради исчезновения ошибки. Здесь выбран VARBINARY(MAX), а сохранность подтверждается серверным DATALENGTH и SHA-256 прочитанного потока. Историческая ошибка старого драйвера не объявляется текущим ограничением в 4000 или 8000 байт.
Попробуйте изменить
Увеличьте журнал до 1200 строк, повторив генерацию строк с новыми номерами. Какие показатели должны измениться, а какие правила останутся теми же?
Как проверить решение
Увеличатся длина и обычно число шагов отправки, изменится SHA-256. Строгой формулы количества вызовов этот пример не обещает. Проверка должна подтвердить фактический новый размер и хеш; до окончания передачи нельзя закрывать поток или читать квитанцию.
Повторно отправить файл и заменить поток в подготовленной команде
Один акт нужно прикрепить к двум карточкам, затем той же подготовленной командой отправить исправленный файл. При чтении у потока меняется позиция: даже подсчёт хеша может дойти до конца. Пример явно возвращает указатель в начало, выполняет две отправки и затем заменяет ресурс в связанной переменной. В результате первые два вложения одинаковы, третье содержит исправленный акт.
- original.txt → hash_update_stream → позиция в конце
- rewind(input) → позиция0 → execute id1
- rewind(input) → execute id2: тот же полный документ
- input=новый fopen(replacement.txt); id3 → execute использует связанную переменную
- Прочитать три отдельных вложения: два одинаковых SHA-256, третий другой
Сначала небольшой вызов
if (rewind($input) === false) {
throw new RuntimeException("Нельзя вернуть поток в начало");
}
$id = 2;
// stmt был подготовлен с &$id и &$input.
if (sqlsrv_execute($stmt) === false) {
$errors = sqlsrv_errors();
}У файла есть содержимое, а у открытого потока ещё и текущая позиция. rewind ставит её в начало; ftell показывает смещение в байтах. hash_update_stream читает источник с текущего места, поэтому после хеширования нужен осознанный возврат к началу для повторного использования. Знак & в массиве параметров связывает statement с переменными id, filename и input. Присваивание нового fopen в input меняет ресурс для следующего execute. Подготовка выполняется один раз, но каждое execute создаёт отдельную строку в таблице.
Что получится
| Наблюдение | Результат |
|---|---|
| Позиция после подсчёта хеша | 46 |
| Позиция после rewind | 0 |
| Первая копия | Акт №151-A: принято 3 линии. |
| Повторная копия | Акт №151-A: принято 3 линии. |
| Заменённое вложение | Акт №151-B: принято 4 линии. |
| Обе отправки исходного файла совпали | да |
| Связанная переменная приняла новый поток | да |
<?php
declare(strict_types=1);
require __DIR__.'/lab.php';
$db=$config=null;$owned=$removed=$rows=$checks=$observations=[];
$runtime=s151Runtime();
try {
$config=s151Config();$db=s151Connect($config);$runtime=s151Runtime($db,$config);
function readAttachment151($db,string $q,int $id,bool $showKnownUtf8Text=false): array {
$stmt=s151Query($db,"SELECT filename,CASE WHEN payload IS NULL THEN 1 ELSE 0 END AS missing,CAST(DATALENGTH(payload) AS int) AS byte_count,payload FROM $q WHERE id = ?",[$id]);
$blob=null;
try {
$state=sqlsrv_fetch($stmt);
if ($state===false) {s151Failure('Чтение вложения');}
if ($state===null) {throw new RuntimeException('Вложение с заданным id отсутствует.');}
$filename=sqlsrv_get_field($stmt,0);
if ($filename===false) {s151Failure('Имя вложения');}
$missing=sqlsrv_get_field($stmt,1);
if ($missing===false) {s151Failure('Признак NULL');}
$sqlBytes=sqlsrv_get_field($stmt,2);
if ($sqlBytes===false) {s151Failure('Размер вложения');}
// A missing attachment is represented explicitly, not turned into an empty stream.
if ($missing===1) {
if ($sqlBytes!==null) {throw new RuntimeException('У SQL NULL ожидался NULL-размер.');}
return ['filename'=>$filename,'is_null'=>true,'bytes'=>null,'sha256'=>null,'preview'=>null];
}
$blob=sqlsrv_get_field($stmt,3,SQLSRV_PHPTYPE_STREAM(SQLSRV_ENC_BINARY));
if ($blob===false) {s151Failure('Получение бинарного потока');}
if (!is_resource($blob)) {throw new RuntimeException('Для существующего SQL-значения ожидался поток.');}
$hash=hash_init('sha256');$bytes=0;$preview='';
while (!feof($blob)) {
$chunk=fread($blob,4096);
if ($chunk===false) {throw new RuntimeException('Не удалось прочитать поток вложения.');}
if ($chunk==='') {
if (feof($blob)) {break;}
throw new RuntimeException('Чтение потока не продвинулось.');
}
$bytes+=strlen($chunk);hash_update($hash,$chunk);
// Enabled only for known UTF-8 text fixtures, never guessed from binary size.
if ($showKnownUtf8Text && $sqlBytes<=256) {$preview.=$chunk;}
}
$readErrors=sqlsrv_errors(SQLSRV_ERR_ERRORS);
if ($readErrors!==null) {throw new RuntimeException('SQLSRV сообщил об ошибке чтения потока.');}
if ($bytes!==$sqlBytes) {throw new RuntimeException('Прочитанный размер отличается от DATALENGTH.');}
return ['filename'=>$filename,'is_null'=>false,'bytes'=>$bytes,'sha256'=>hash_final($hash),'preview'=>$preview];
} finally {
if (is_resource($blob)) {fclose($blob);}
sqlsrv_free_stmt($stmt);
}
}
$name=s151OwnTable($db,$owned,'rewind','(id int NOT NULL PRIMARY KEY, filename nvarchar(80) NOT NULL, payload varbinary(max) NULL)');
$q=s151Qualified($name);$input=fopen(__DIR__.'/original.txt','rb');
if ($input===false) {throw new RuntimeException('Не удалось открыть исходный акт.');}
$stmt=null;
try {
// Hashing consumes this same handle. Rewind before binding/sending its full contents.
$hash=hash_init('sha256');$hashedBytes=hash_update_stream($hash,$input);$originalHash=hash_final($hash);
$afterHash=ftell($input);
if (rewind($input)===false) {throw new RuntimeException('Исходный поток нельзя перемотать.');}
$beforeFirst=ftell($input);$id=1;$filename='original.txt';
$params=[&$id,&$filename,[&$input,SQLSRV_PARAM_IN,SQLSRV_PHPTYPE_STREAM(SQLSRV_ENC_BINARY),SQLSRV_SQLTYPE_VARBINARY('max')]];
$stmt=sqlsrv_prepare($db,"INSERT INTO $q (id,filename,payload) VALUES (?,?,?)",$params,['QueryTimeout'=>10]);
if ($stmt===false) {s151Failure('Подготовка вложения');}
if (sqlsrv_execute($stmt)===false) {s151Failure('Первая отправка');}
if (rewind($input)===false) {throw new RuntimeException('Не удалось повторно перемотать акт.');}
$id=2;$filename='copy.txt';
if (sqlsrv_execute($stmt)===false) {s151Failure('Повторная отправка');}
fclose($input);
$input=fopen(__DIR__.'/replacement.txt','rb');
if ($input===false) {throw new RuntimeException('Не удалось открыть замену.');}
$id=3;$filename='replacement.txt';
if (sqlsrv_execute($stmt)===false) {s151Failure('Отправка замены');}
} finally {
if (is_resource($stmt)) {sqlsrv_free_stmt($stmt);}
if (is_resource($input)) {fclose($input);}
}
$first=readAttachment151($db,$q,1,true);$copy=readAttachment151($db,$q,2,true);$replacement=readAttachment151($db,$q,3,true);
$replacementHash=hash_file('sha256',__DIR__.'/replacement.txt');
$rows=[['Позиция после подсчёта хеша',$afterHash],['Позиция после rewind',$beforeFirst],
['Первая копия',trim($first['preview'])],['Повторная копия',trim($copy['preview'])],['Заменённое вложение',trim($replacement['preview'])],
['Обе отправки исходного файла совпали',$first['sha256']===$originalHash&&$copy['sha256']===$originalHash],
['Связанная переменная приняла новый поток',$replacement['sha256']===$replacementHash&&$replacement['sha256']!==$originalHash]];
$checks=['hash_consumed_handle'=>$afterHash===$hashedBytes&&$hashedBytes>0,'rewind_position_zero'=>$beforeFirst===0,
'first_complete'=>$first['bytes']===$hashedBytes&&$first['sha256']===$originalHash,
'repeat_complete'=>$copy['bytes']===$hashedBytes&&$copy['sha256']===$originalHash,
'replacement_complete'=>$replacement['sha256']===$replacementHash,'replacement_distinct'=>$replacement['sha256']!==$originalHash,
'input_closed'=>!is_resource($input)];
$observations=['first'=>$first,'copy'=>$copy,'replacement'=>$replacement,'after_hash_position'=>$afterHash,'rewound_position'=>$beforeFirst];
} finally {
if (is_resource($db)) {try {$removed=s151Cleanup($db,$owned);} finally {sqlsrv_close($db);}}
}
s151Report('sqlsrv151-stream-rewind','Повторно отправить файл и заменить поток в подготовленной команде',$rows,$checks,$observations,$runtime,$owned,$removed);
Ожидаемый результат
Позиция после подсчёта хеша — 46
Позиция после rewind — 0
Первая копия — Акт №151-A: принято 3 линии.
Повторная копия — Акт №151-A: принято 3 линии.
Заменённое вложение — Акт №151-B: принято 4 линии.
Обе отправки исходного файла совпали — да
Связанная переменная приняла новый поток — да
Локальная программа включает исходные текстовые файлы, lab.php и README.txt. Она создаёт одну собственную таблицу SQL Server, сохраняет result.html/result.json/metrics.json и удаляет свою таблицу. Исходные файлы остаются рядом для сравнения.
Фактический результат запуска · Данные результата JSON.
Ограничения и ошибки
Проверяйте результат rewind: сетевой поток или другое нестандартное хранилище может не поддерживать возврат. Тогда заново откройте воспроизводимый источник либо сохраните его во временный файл с ограничением размера. Не используйте закрытый ресурс, не меняйте связанный поток посреди незавершённой отправки. Повтор операции после сетевого сбоя — отдельная задача идемпотентности: копия файла в примере создаётся намеренно с новым id, а не маскируется как безопасный автоматический retry. Механизм SQLSRV может самостоятельно возвращать seekable-потоки в начало; этот пример задаёт позицию явно и проверяет конечные байты вместо зависимости от скрытой перемотки драйвера.
Попробуйте изменить
Перед третьим execute откройте original.txt вместо replacement.txt. Предскажите содержимое и сравнение хешей.
Как проверить решение
Все три вложения будут иметь одинаковые байты, хотя имя третьей записи пока останется replacement.txt. Имя — отдельное поле, оно не доказывает содержимое. Чтобы сценарий соответствовал новой задаче, измените также имя и ожидание distinct.
Различить отсутствующее вложение, пустой файл и документ
В карточке заявки три разных состояния: файл ещё не приложили; пользователь приложил существующий файл длиной 0; файл содержит документ. От них зависит сообщение человеку. Сохраним эти состояния раздельно и прочитаем их, не превращая NULL в пустую строку. Для NULL не существует контрольной суммы файла, у пустого файла есть SHA-256 нулевого содержимого.
- id1: payload=NULL → missing1, DATALENGTH=NULL → «не прикреплено»
- id2: fopen(empty.txt) → 0байт в VARBINARY → missing0, DATALENGTH0
- id3: small-note.txt → реальные байты → «Вложение принято.»
- Сначала читать SQL-признак отсутствия, затем поток только для существующего файла
- Отчёт сохраняет null и0 как разные значения
Сначала небольшой вызов
$sql = "SELECT CASE WHEN payload IS NULL THEN 1 ELSE 0 END AS missing,
CAST(DATALENGTH(payload) AS int) AS byte_count FROM $q WHERE id = ?";
$stmt = sqlsrv_query($db, $sql, [1]);
// Для отсутствующего вложения: missing=1, byte_count=null.
// Для пустого существующего файла: missing=0, byte_count=0.SQL NULL означает отсутствие значения, а пустое VARBINARY содержит значение длиной 0 байт. CASE WHEN создаёт явный числовой признак для интерфейса. DATALENGTH считает байты, возвращая NULL для NULL. Для VARBINARY(MAX) это SQL bigint, который SQLSRV обычно возвращает строкой: в полном примере ограниченный размер приведён в SQL к int. readAttachment151 сначала читает признак и размер, затем получает бинарный поток только у существующего файла. Это существенно: в проверенном SQLSRV 5.13.3 явный запрос PHPTYPE_STREAM для SQL NULL возвращает пустой ресурс потока, поэтому по нулю прочитанных байтов нельзя восстановить отсутствие значения. SQL-признак сохраняет различие заранее.
Что получится
| Наблюдение | Результат |
|---|---|
| id1: состояние | не прикреплено |
| id1: размер SQL | null |
| id2: состояние | есть пустой файл |
| id2: байт | 0 |
| id2: SHA-256 пустого файла | e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855 |
| id3: содержимое | Вложение принято. |
| id3: байт | 33 |
<?php
declare(strict_types=1);
require __DIR__.'/lab.php';
$db=$config=null;$owned=$removed=$rows=$checks=$observations=[];
$runtime=s151Runtime();
try {
$config=s151Config();$db=s151Connect($config);$runtime=s151Runtime($db,$config);
function readAttachment151($db,string $q,int $id,bool $showKnownUtf8Text=false): array {
$stmt=s151Query($db,"SELECT filename,CASE WHEN payload IS NULL THEN 1 ELSE 0 END AS missing,CAST(DATALENGTH(payload) AS int) AS byte_count,payload FROM $q WHERE id = ?",[$id]);
$blob=null;
try {
$state=sqlsrv_fetch($stmt);
if ($state===false) {s151Failure('Чтение вложения');}
if ($state===null) {throw new RuntimeException('Вложение с заданным id отсутствует.');}
$filename=sqlsrv_get_field($stmt,0);
if ($filename===false) {s151Failure('Имя вложения');}
$missing=sqlsrv_get_field($stmt,1);
if ($missing===false) {s151Failure('Признак NULL');}
$sqlBytes=sqlsrv_get_field($stmt,2);
if ($sqlBytes===false) {s151Failure('Размер вложения');}
// A missing attachment is represented explicitly, not turned into an empty stream.
if ($missing===1) {
if ($sqlBytes!==null) {throw new RuntimeException('У SQL NULL ожидался NULL-размер.');}
return ['filename'=>$filename,'is_null'=>true,'bytes'=>null,'sha256'=>null,'preview'=>null];
}
$blob=sqlsrv_get_field($stmt,3,SQLSRV_PHPTYPE_STREAM(SQLSRV_ENC_BINARY));
if ($blob===false) {s151Failure('Получение бинарного потока');}
if (!is_resource($blob)) {throw new RuntimeException('Для существующего SQL-значения ожидался поток.');}
$hash=hash_init('sha256');$bytes=0;$preview='';
while (!feof($blob)) {
$chunk=fread($blob,4096);
if ($chunk===false) {throw new RuntimeException('Не удалось прочитать поток вложения.');}
if ($chunk==='') {
if (feof($blob)) {break;}
throw new RuntimeException('Чтение потока не продвинулось.');
}
$bytes+=strlen($chunk);hash_update($hash,$chunk);
// Enabled only for known UTF-8 text fixtures, never guessed from binary size.
if ($showKnownUtf8Text && $sqlBytes<=256) {$preview.=$chunk;}
}
$readErrors=sqlsrv_errors(SQLSRV_ERR_ERRORS);
if ($readErrors!==null) {throw new RuntimeException('SQLSRV сообщил об ошибке чтения потока.');}
if ($bytes!==$sqlBytes) {throw new RuntimeException('Прочитанный размер отличается от DATALENGTH.');}
return ['filename'=>$filename,'is_null'=>false,'bytes'=>$bytes,'sha256'=>hash_final($hash),'preview'=>$preview];
} finally {
if (is_resource($blob)) {fclose($blob);}
sqlsrv_free_stmt($stmt);
}
}
$name=s151OwnTable($db,$owned,'empty','(id int NOT NULL PRIMARY KEY, filename nvarchar(80) NOT NULL, payload varbinary(max) NULL)');
$q=s151Qualified($name);
// Missing file is a business state. It is deliberately SQL NULL, not a fake stream.
s151Command($db,"INSERT INTO $q (id,filename,payload) VALUES (1,N'not-attached',NULL)");
foreach ([[2,'empty.txt'],[3,'small-note.txt']] as [$id,$filename]) {
$input=fopen(__DIR__.'/'.$filename,'rb');
if ($input===false) {throw new RuntimeException('Не удалось открыть учебный файл '.$filename);}
try {
$params=[$id,$filename,[$input,SQLSRV_PARAM_IN,SQLSRV_PHPTYPE_STREAM(SQLSRV_ENC_BINARY),SQLSRV_SQLTYPE_VARBINARY('max')]];
$insert=s151Query($db,"INSERT INTO $q (id,filename,payload) VALUES (?,?,?)",$params);
sqlsrv_free_stmt($insert);
} finally {fclose($input);}
}
$absent=readAttachment151($db,$q,1);$empty=readAttachment151($db,$q,2);$note=readAttachment151($db,$q,3,true);
$rows=[['id1: состояние',$absent['is_null']?'не прикреплено':'есть файл'],['id1: размер SQL',$absent['bytes']],
['id2: состояние',$empty['is_null']?'не прикреплено':'есть пустой файл'],['id2: байт',$empty['bytes']],
['id2: SHA-256 пустого файла',$empty['sha256']],['id3: содержимое',trim($note['preview'])],['id3: байт',$note['bytes']]];
$checks=['sql_null_preserved'=>$absent['is_null']===true&&$absent['bytes']===null&&$absent['sha256']===null,
'empty_is_not_null'=>$empty['is_null']===false,'empty_zero_bytes'=>$empty['bytes']===0,
'empty_has_hash'=>$empty['sha256']===hash('sha256',''),'note_not_null'=>$note['is_null']===false,
'note_bytes'=>$note['bytes']===filesize(__DIR__.'/small-note.txt'),'note_hash'=>$note['sha256']===hash_file('sha256',__DIR__.'/small-note.txt'),
'note_visible'=>$note['preview']==="Вложение принято.\n"];
$observations=['missing'=>$absent,'empty'=>$empty,'document'=>$note];
} finally {
if (is_resource($db)) {try {$removed=s151Cleanup($db,$owned);} finally {sqlsrv_close($db);}}
}
s151Report('sqlsrv151-stream-empty-null','Различить отсутствующее вложение, пустой файл и документ',$rows,$checks,$observations,$runtime,$owned,$removed);
Ожидаемый результат
id1: состояние — не прикреплено
id1: размер SQL — null
id2: состояние — есть пустой файл
id2: байт — 0
id2: SHA-256 пустого файла — e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855
id3: содержимое — Вложение принято.
id3: байт — 33
Локальная программа включает исходные текстовые файлы, lab.php и README.txt. Она создаёт одну собственную таблицу SQL Server, сохраняет result.html/result.json/metrics.json и удаляет свою таблицу. Исходные файлы остаются рядом для сравнения.
Фактический результат запуска · Данные результата JSON.
Ограничения и ошибки
Пустой файл не обязательно допустим по правилам продукта: можно отклонить его до записи понятным сообщением, но это бизнес-правило, не ошибка SQLSRV. Нельзя автоматически заменить NULL на пустую строку или считать любой ложный результат «не найдено». Этот пример проверяет SQL-значение, а не HTTP upload error: ошибки формы, размер загрузки и отсутствующий временный файл разбираются в главе загрузок.
В приложении после смены драйвера и слоя доступа SQL NULL стал восприниматься как пустая строка. Исходная ситуация на Stack Overflow. Проверять отдельный контракт отсутствия данных, не превращать любое значение в строку через поток. В примере SQL-признак IS NULL и DATALENGTH различают отсутствующее вложение, существующий пустой файл и содержимое.
Попробуйте изменить
Установите payload=NULL для id2 и выполните чтение. Что станет с размером и хешем?
Как проверить решение
Второй результат должен перейти в состояние «не прикреплено», размер и хеш станут null. SHA-256 пустого файла не подходит отсутствующему значению. Если интерфейс продолжает показывать «пустой файл», он потерял бизнес-состояние.
Функция отправки и соседние операции
| Действие | Куда перейти |
|---|---|
| Отправлять очередную порцию | sqlsrv_send_stream_data |
| Открыть и читать файл | Файлы и потоки PHP |
| Получить поток столбца | sqlsrv_get_field |
| Отменить незавершённую команду | sqlsrv_cancel |
От сохранённого файла к устойчивому сервису
Сохранность вложения определяется прочитанными байтами, а не именем файла или bool от первого вызова. Потоки должны оставаться открытыми до конца своей операции; порядок чтения столбцов и позиция файла — часть контракта. Для списка документов выбирайте метаданные без больших payload; содержимое получайте для конкретной карточки или скачивания.
Следующий шаг — отмена, тайм-аут и повторное использование statement. Если вложение и карточка должны сохраниться вместе, применяйте транзакцию. Веб-приём файла с формой, ограничениями и проверками начинается в главе загрузки файлов.
Функции и методы этой темы
Откройте карточку, чтобы увидеть договор функции, примеры, ограничения и связанные инструменты.
sqlsrv_send_stream_dataПередавать поток параметра порциями и отдельно проверять завершение.
Полный контракт true/null/false проверен на SQLSRV 5.13.3; первая версия не указана в PHP Manual · sqlsrv
sqlsrv_send_stream_data(resource $stmt): bool|nullЯвно отправить очередную порцию параметра-потока, когда автоматическая отправка выключена. Это нужно, например, при записи большого журнала устройства, когда приложение управляет шагами передачи и отдельно подтверждает сохранность файла.
- query/execute с SendStreamParamsAtExec=false
- statement остаётся ресурсом подготовленной/выполненной команды
- send_stream_data(stmt) отправляет до 8 КБ; true означает продолжение
- SQLSRV 5.13.3: null → готово; false → ошибка и немедленная диагностика
| Элемент | Смысл |
|---|---|
| $stmt | Ресурс statement от sqlsrv_prepare или sqlsrv_query после запуска команды. Это не файловый поток и не bool результата sqlsrv_execute. |
| Подготовка | Поток включён в параметры, а SendStreamParamsAtExec=false задан в options query/prepare. По умолчанию true, и query/execute отправляет данные сам. |
| true | У драйвера остаются данные для отправки; нужно продолжить. Это не количество байтов и не утверждение, что отправлено ровно 8192 байта. |
| null | Проверенный SQLSRV 5.13.3 закончил отправку. Затем нужно подтвердить результат команды и содержимое. |
| false | Ошибка. Сразу сохраните sqlsrv_errors, не выдавайте сообщение об успешной загрузке. |
Функция принимает один параметр — ресурс команды. В проверенной версии 5.13.3 результат имеет три состояния true/null/false. Краткие страницы PHP.net и Microsoft описывают bool; отличие подтверждают исходники Microsoft 5.13.3, sqlsrv_send_stream_data и реальный запуск. Сигнатура карточки показывает этот полный контракт. Кроме того, PHP Manual упоминает ресурс от execute, но sqlsrv_execute возвращает bool: ресурс остаётся переменной, полученной от prepare. Не присваивайте результат execute обратно в stmt.
Журнал устройства больше одной порции
$stmt = sqlsrv_query($db, $sql, $params, ["SendStreamParamsAtExec" => false]);
if ($stmt === false) { throw new RuntimeException("Команда не началась"); }
while (true) {
$more = sqlsrv_send_stream_data($stmt);
if ($more === false) {
$errors = sqlsrv_errors(SQLSRV_ERR_ERRORS);
throw new RuntimeException("Ошибка отправки");
}
if ($more === null) { break; }
}Здесь SQL, поток и параметры готовятся заранее. Полный пример отправляет 600 событий и сверяет размер/SHA-256 прочитанного журнала. Само окончание цикла не подменяет эту квитанцию.
Повторная отправка через подготовленную команду
// stmt создан sqlsrv_prepare с &$input и SendStreamParamsAtExec=false.
if (rewind($input) === false) { throw new RuntimeException("Источник нельзя перемотать"); }
if (sqlsrv_execute($stmt) === false) { throw new RuntimeException("Выполнение не началось"); }
do {
$more = sqlsrv_send_stream_data($stmt);
if ($more === false) {
$errors = sqlsrv_errors(SQLSRV_ERR_ERRORS);
throw new RuntimeException("Ошибка повторной отправки");
}
} while ($more === true);
// Здесь null: отправка закончена. Проверить сохранённый файл.Это составной фрагмент: он добавляет явные шаги к повторному выполнению. Самостоятельная программа показывает позицию потока, ссылки параметров и замену файла использует автоматическую отправку, чтобы отдельно объяснить повторное использование ресурса; цикл явной отправки полностью выполнен в соседнем сценарии журнала.
Границы и ошибки
Данные передаются через существующий statement, а не отдельными INSERT для каждого блока. Держите input открытым до завершения. Не выполняйте команду повторно и не освобождайте statement посреди передачи. Вызов с закрытым/неверным PHP-ресурсом может быть ошибкой PHP, а не обычным false с SQL-диагностикой. Число true не является процентом, максимум8КБ относится к одному вызову. Не нужен ручной цикл, если оставлен SendStreamParamsAtExec=true. Успешная передача параметра не заменяет commit транзакции, обработку последующих результатов и правила повторов при неоднозначном сетевом отказе.
Продолжение: prepare/execute и параметры, чтение бинарного поля, отмена и завершение команды, фиксация и откат.
Код и руководство с собой
Скачайте самостоятельные примеры с исходными данными, инструкциями и ожидаемыми результатами. Примеры HTTP и фрагменты для проекта помечены отдельно: им нужно указанное окружение.
Код не исполняется на сайте. Скачайте нужный пример и запускайте его в своём локальном окружении.
Материалы сверены 2026-10-12. Примеры и объяснения написаны для этого руководства; источники и обсуждения доступны в каждой главе.


