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

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

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

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

Глава 118 · После соединения, первого prepared SELECT и понятия транзакции

MySQLi: состояние подготовленной команды, буфер и двоичные вложения

NULL и конец чтения, заголовки пустого ответа, перемотка statement, отправка BLOB частями и различие reset, free_result и rollback.

Разделить команду, результат и транзакцию

Чтобы вывести заказ или сохранить вложение, недостаточно знать имя одной функции. Важно понимать, что уже произошло: SQL подготовлен, отправлен, ответ прочитан, строки сохранены в буфере или изменения подтверждены. Разберём эти состояния на шести самостоятельных программах и видимых результатах.

У каждой операции своя обязанность
  1. prepare сохраняет SQL-шаблон команды
  2. bind_param задаёт входные переменные; execute запускает команду
  3. bind_result задаёт выходные переменные; fetch читает очередную строку
  4. store_result сохраняет ответ; free_result освобождает его
  5. reset сбрасывает временное состояние команды; rollback откатывает транзакцию

Пример знакомого механизма: у аппарата выдачи билетов есть шаблон бланка, введённые данные и уже напечатанные билеты. Сброс незавершённого ввода не уничтожает напечатанный билет. Это аналогия для различия уровней, а не буквальное устройство памяти MySQL. В коде следует отслеживать конкретные $stmt, связанные переменные и соединение $db.

Что хотим получитьЧто выбираем
Поля одной следующей строкиbind_result + fetch
Описание колонок, даже при нуле строкresult_metadata
Счётчик и перемотку маленького ответаstore_result + num_rows + data_seek
Двоичное вложение из частейbind_param с b + send_long_data + execute
Отказаться от незаписанных частейreset
Отменить выполненные записи незавершённой транзакцииrollback

Базовое соединение и первый SELECT находятся в начале MySQL, обычные способы получения строк — в жизненном цикле результата. Здесь расширяем справочные контракты MySQLi, сохраняя ссылки на обе формы вызова API.

Запуск на своей учебной базе

Нужны PHP 8.2+ с mysqli/mysqlnd и MySQL 8.4; контрольный стенд — PHP 8.5.11 / MySQL 8.4.13. Это поддерживаемое окружение комплекта: рассматриваемые методы появились раньше. Сервер запускается отдельно от PHP. Подготовьте свою локальную учебную базу по инструкции подключения.

Задайте PHPMAN_MYSQL_HOST, PHPMAN_MYSQL_PORT, PHPMAN_MYSQL_DATABASE, PHPMAN_MYSQL_USER и PHPMAN_MYSQL_PASSWORD в терминале. Контрольный адрес 127.0.0.1:33070; у вас может быть другой порт. Пользователю нужны SELECT, INSERT, UPDATE и CREATE TEMPORARY TABLES. В одном распакованном ZIP уже находятся example.php, ms112-helper.php и README. Выполните php example.php, затем откройте result.html.

Все программы сами создают фиксированные данные только во временных таблицах mysqli112_stmt_*. Постоянная схема не меняется. Результаты сохраняются в HTML/JSON, версии и проверки — в metrics.json без реквизитов. BLOB-сценарий дополнительно создаёт два учебных бинарных файла рядом с программой. Эти примеры проверяют PHP CLI и настоящий MySQL, а не форму загрузки на сайте.

Две записи одного действия: методы и функции

Объектная записьПроцедурная записьВозвращаемое значение
$stmt->bind_result($id,$name)mysqli_stmt_bind_result($stmt,$id,$name)bool; переменные связываются по ссылке
$stmt->fetch()mysqli_stmt_fetch($stmt)true, false или null; результат данных — в связанных переменных
$stmt->result_metadata()mysqli_stmt_result_metadata($stmt)mysqli_result с описаниями либо false
$stmt->store_result()mysqli_stmt_store_result($stmt)bool
$stmt->num_rowsmysqli_stmt_num_rows($stmt)int|string
$stmt->data_seek($offset)mysqli_stmt_data_seek($stmt,$offset)void; offset с нуля внутри буфера
$stmt->free_result()mysqli_stmt_free_result($stmt)void
$stmt->send_long_data($index,$bytes)mysqli_stmt_send_long_data($stmt,$index,$bytes)bool; index с нуля среди SQL-параметров
$stmt->reset()mysqli_stmt_reset($stmt)bool

В процедурной форме объект команды передаётся первым аргументом; это не отдельный сервер и не другая логика SQL. В примерах основной стиль объектный, а отдельные процедурные вызовы показывают соответствие. Ошибки серверных операций могут стать mysqli_sql_exception при включённом строгом режиме. Полные сигнатуры, свойства и связанные операции доступны по ссылкам справочника в каждом разборе.

Отличить пустое поле заказа от конца чтения

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

Исходные данные

Заказcomment_text
101SQL NULL
102Пустая строка
103Позвонить у двери
Как меняется состояние
  1. execute отправляет SELECT с двумя колонками
  2. bind_result связывает id с $orderId, comment_text с $comment
  3. fetch возвращает true и заполняет обе переменные даже при $comment=null
  4. После трёх строк fetch возвращает null: нового заказа нет

Что получится

Статус fetchЗаказ IDЗначение поляЧто показать
true101NULLКомментарий не указан
true102Сохранена пустая строка
true103Позвонить у двериПозвонить у двери
nullNULLNULLСтроки закончились; это не новый заказ

Минимальная запись действия

$stmt->execute();
$stmt->bind_result($orderId, $comment);
while (($status = $stmt->fetch()) === true) {
    echo $orderId, ': ', $comment ?? 'Комментарий не указан', "\n";
}
if ($status === false) {
    throw new RuntimeException('Не удалось прочитать результат');
}

Это объясняющий фрагмент для уже подготовленной команды и объявленных переменных. Полная программа ниже самостоятельно создаёт таблицу, входные значения и итоговый отчёт.

Подготовленная команда — объект, который хранит SQL и состояние его выполнения. execute запускает запрос, но сам по себе не раскладывает строки по вашим переменным. bind_result указывает получателей столбцов в порядке SELECT. Связать нужно все выбранные колонки. Только fetch читает следующую строку и меняет значения этих переменных.

Здесь две разные величины могут оказаться null: $comment — данные конкретного заказа, $status — ответ операции чтения. Для заказа 101 fetch успешен и равен true, хотя комментарий равен null. Поэтому цикл проверяет именно статус вызова, а оформление поля выполняется внутри. Оператор ?? подставляет подпись только для null; пустую строку он не заменяет.

Контракт fetch: true — строка получена; false — ошибка; null — больше нет данных либо произошло усечение. В этом примере короткие поля читаются целиком, все три значения сверяются, а завершающий статус проверяется отдельно. Строгий режим MySQLi переводит серверные ошибки в исключения; явная проверка false дополнительно отделяет неудачу от ожидаемого завершения. Не считайте любое ложноподобное значение доказательством успешной выгрузки.

Границы и частые ошибки

Скопируйте нужные значения в выходную строку до следующего fetch: связанные переменные будут обновлены. Не помещайте в цикл bind_result вместо fetch — привязка не продвигает курсор. Для обычного массива строки удобен get_result с mysqlnd, но это другой способ получения результата и его уже показывает базовая глава. Большую выгрузку не нужно накапливать целиком в $rows; здесь всего три учебных заказа.

Полная самостоятельная программа и файлы
Отличить пустое поле заказа от конца чтенияСамостоятельный пример
<?php
declare(strict_types=1);
require __DIR__.'/ms112-helper.php';
$db=ms112Connect();
$runtime=['php'=>PHP_VERSION,'client'=>$db->client_info,'server'=>$db->server_info];
$details=[];$artifacts=[];
try {
$db->query('CREATE TEMPORARY TABLE mysqli112_stmt_delivery (id INT PRIMARY KEY,comment_text VARCHAR(80) NULL) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4');
$db->query("INSERT INTO mysqli112_stmt_delivery VALUES (101,NULL),(102,''),(103,'Позвонить у двери')");
$stmt=$db->prepare('SELECT id,comment_text FROM mysqli112_stmt_delivery ORDER BY id');
$stmt->execute();$stmt->bind_result($orderId,$comment);
$rows=[];$values=[];
while (($status=$stmt->fetch())===true) {
    $values[]=[$orderId,$comment];
    $meaning=$comment===null?'Комментарий не указан':($comment===''?'Сохранена пустая строка':$comment);
    $rows[]=['true',$orderId,$comment,$meaning];
}
if($status===false){throw new RuntimeException('Fetching failed');}
$rows[]=['null',null,null,'Строки закончились; это не новый заказ'];
$checks=['three_distinct_values'=>$values===[[101,null],[102,''],[103,'Позвонить у двери']],'end_status_is_null'=>$status===null,'no_statement_error'=>$stmt->errno===0];
$details=['successful_fetches'=>count($values),'last_fetch_status'=>$status,'statement_errno'=>$stmt->errno];
$stmt->free_result();$stmt->close();
} finally {$db->close();}
ms112Report('mysqli112-statement-null-and-end', ['Статус fetch', 'Заказ ID', 'Значение поля', 'Что показать'], $rows, $checks, $runtime, $details, $artifacts);
echo 'mysqli112-statement-null-and-end: verified result.html and result.json
';
Ожидаемый результат
mysqli112-statement-null-and-end: verified result.html and result.json

Откройте result.html и сравните фактические строки с таблицей выше. Подключение, временные данные и helper уже включены; предыдущие программы выполнять не нужно.

Открыть фактический HTML-результат · Скачать результат JSON

Попробуйте сами. Сделайте у заказа 102 комментарий NULL. Количество успешных fetch останется три, но две строки получат подпись «Комментарий не указан». Отдельный завершающий null не создаст четвёртый заказ.

Практический вопрос: После bind_result ничего не выводится. В вопросе столбцы привязаны к переменным, но строка не прочитана через fetch. Наш сценарий отделяет привязку, статус чтения и NULL внутри успешно полученного заказа. Вопрос задаёт ситуацию; технические правила сверены с официальной документацией, наш пример написан отдельно.

Сохранить заголовки пустого отчёта и распознать UPDATE

Отчёт «Товары с отрицательным остатком» сегодня пуст. Пользователь всё равно должен видеть заголовки product_id, product_title и qty. После UPDATE, напротив, таблицы строк вообще нет: результат операции — изменённое количество.

Исходные данные

Товар IDНазваниеОстаток
1Перчатки5
Как меняется состояние
  1. SELECT qty < 0 возвращает ноль строк товаров
  2. result_metadata всё равно описывает три выходных столбца
  3. fetch_fields читает их имена; метаданные освобождаются отдельно
  4. UPDATE qty+1 меняет остаток на 6 и не создаёт набор столбцов

Что получится

ОперацияЗаголовкиСтрок данных в набореИзменено строк
SELECT: нет товаров с qty < 0product_id, product_title, qty00
UPDATE: количество увеличеноНет набора столбцов01

Минимальная запись действия

$metadata = $stmt->result_metadata();
if ($metadata instanceof mysqli_result) {
    foreach ($metadata->fetch_fields() as $field) {
        echo $field->name, "\n";
    }
    $metadata->free();
}
// Описания колонок не содержат строки товаров.

Это объясняющий фрагмент для уже подготовленной команды и объявленных переменных. Полная программа ниже самостоятельно создаёт таблицу, входные значения и итоговый отчёт.

result_metadata возвращает объект mysqli_result с описанием схемы ответа. Это не объект с товарами. Вызов fetch_fields у этого объекта даёт описание каждой колонки: name содержит выходное имя, включая AS. Поэтому пустой SELECT сохраняет полноценную шапку. Строки затем читаются через исходный statement, и в нашем случае первый fetch сразу завершает чтение.

UPDATE не возвращает набор данных. Для него result_metadata возвращает false, field_count равен нулю, affected_rows показывает одну изменённую строку. Сам по себе false от result_metadata не доказывает сбой: нужно учитывать тип результата команды и диагностику. В программе UPDATE выполняется при включённых исключениях, отдельно проверяются errno=0 и фактический остаток 6.

Объект метаданных освобождается через его free; это не закрывает statement и не удаляет строки таблицы. Имена колонок безопасно использовать как заголовки после HTML-экранирования, но превращать произвольное имя из результата в новый SQL-идентификатор без проверки нельзя. Описание ответа также не заменяет полный каталог ограничений и индексов исходной таблицы.

Границы и частые ошибки

Не пытайтесь получить товары через fetch_assoc у result_metadata: он предназначен для описаний. Ноль строк SELECT и отсутствие набора у UPDATE — разные состояния. Ошибку подготовки SQL нужно обрабатывать как ошибку, а не превращать в «пустой отчёт». Данный пример не использует RETURNING других СУБД и не обещает одинаковые схемы результата всем драйверам.

Полная самостоятельная программа и файлы
Сохранить заголовки пустого отчёта и распознать UPDATEСамостоятельный пример
<?php
declare(strict_types=1);
require __DIR__.'/ms112-helper.php';
$db=ms112Connect();
$runtime=['php'=>PHP_VERSION,'client'=>$db->client_info,'server'=>$db->server_info];
$details=[];$artifacts=[];
try {
$db->query('CREATE TEMPORARY TABLE mysqli112_stmt_report (id INT PRIMARY KEY,title VARCHAR(40) NOT NULL,qty INT NOT NULL) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4');
$db->query("INSERT INTO mysqli112_stmt_report VALUES (1,'Перчатки',5)");
$select=$db->prepare('SELECT id AS product_id,title AS product_title,qty FROM mysqli112_stmt_report WHERE qty < ?');
$limit=0;$select->bind_param('i',$limit);$select->execute();
$metadata=$select->result_metadata();
if(!$metadata instanceof mysqli_result){throw new RuntimeException('Expected SELECT metadata');}
$fields=$metadata->fetch_fields();$names=array_map(fn($field)=>$field->name,$fields);$metadata->free();
$select->bind_result($id,$title,$qty);$fetchStatus=$select->fetch();$select->close();
$update=$db->prepare('UPDATE mysqli112_stmt_report SET qty=qty+1 WHERE id=?');
$productId=1;$update->bind_param('i',$productId);$update->execute();
$updateMetadata=$update->result_metadata();$affected=$update->affected_rows;$fieldCount=$update->field_count;$updateErrno=$update->errno;
$update->close();
$rows=[['SELECT: нет товаров с qty < 0','product_id, product_title, qty',0,0],['UPDATE: количество увеличено','Нет набора столбцов',0,$affected]];
$checks=['empty_select_keeps_columns'=>$names===['product_id','product_title','qty'] && $fetchStatus===null,'update_has_no_metadata'=>$updateMetadata===false && $fieldCount===0,'update_succeeded'=>$affected===1 && $updateErrno===0 && (int)ms112Rows($db,'SELECT qty FROM mysqli112_stmt_report WHERE id=1')[0]['qty']===6];
$details=['SELECT_field_names'=>$names,'SELECT_fetch_status'=>$fetchStatus,'UPDATE_metadata'=>$updateMetadata,'UPDATE_field_count'=>$fieldCount,'UPDATE_errno'=>$updateErrno];
} finally {$db->close();}
ms112Report('mysqli112-statement-metadata', ['Операция', 'Заголовки', 'Строк данных в наборе', 'Изменено строк'], $rows, $checks, $runtime, $details, $artifacts);
echo 'mysqli112-statement-metadata: verified result.html and result.json
';
Ожидаемый результат
mysqli112-statement-metadata: verified result.html and result.json

Откройте result.html и сравните фактические строки с таблицей выше. Подключение, временные данные и helper уже включены; предыдущие программы выполнять не нужно.

Открыть фактический HTML-результат · Скачать результат JSON

Попробуйте сами. Измените условие на qty < 10. Заголовки останутся прежними, но SELECT вернёт товар 1. Политику «пустая таблица с заголовками» менять не придётся.

Практический вопрос: Почему result_metadata возвращает false после UPDATE. Отсутствие набора столбцов было принято за ошибку изменения данных. Наш пример сравнивает пустой SELECT с успешным UPDATE, его affected_rows, errno и фактическим новым значением. Вопрос задаёт ситуацию; технические правила сверены с официальной документацией, наш пример написан отдельно.

Перечитать строку из буфера подготовленной команды

В небольшой странице из трёх товаров нужно сначала показать третью строку, затем вернуться к первой. Для этого ответ подготовленной команды должен быть получен в клиентский буфер. Простого execute недостаточно для такого произвольного перехода.

Исходные данные

СмещениеТовар IDНазвание
011Чай
112Кофе
213Мёд
Как меняется состояние
  1. execute: ответ ещё не целиком буферизован, num_rows=0
  2. store_result получает три строки в буфер statement
  3. data_seek(2) выбирает третью позицию; fetch даёт ID 13
  4. free_result освобождает буфер, новый execute снова начинает ответ

Что получится

Шагnum_rowsТовар IDНазвание
После execute, до буферизации0NULLNULL
После store_result3NULLNULL
data_seek(2), затем fetch313Мёд
data_seek(0), затем fetch311Чай
free_result, новый execute311Чай

Минимальная запись действия

$stmt->execute();
$stmt->store_result();
$stmt->bind_result($id, $title);
$stmt->data_seek(2); // Смещение 2 — третья строка, не ID товара.
$stmt->fetch();
echo $id, ': ', $title;
$stmt->free_result();

Это объясняющий фрагмент для уже подготовленной команды и объявленных переменных. Полная программа ниже самостоятельно создаёт таблицу, входные значения и итоговый отчёт.

store_result после execute сохраняет полный ответ в буфер подготовленной команды. Затем num_rows показывает его размер, а data_seek меняет позицию следующего чтения. Смещение начинается с нуля: 2 означает третью строку текущего упорядоченного SELECT, а не значение первичного ключа. Сам data_seek ничего не присваивает $id и $title — это делает следующий fetch.

Свойство num_rows имеет тип int|string: при количестве больше PHP_INT_MAX документация предусматривает строку. Ноль до получения полного ответа нельзя безусловно трактовать как отсутствие товаров. В нашем маленьком ответе после буферизации значение равно целому 3. Если нужен только общий счётчик, COUNT в SQL может решить задачу без загрузки всех строк.

free_result освобождает память результата, сохраняя сам шаблон statement для нового execute. Поэтому программа после освобождения выполняет ещё один SQL, затем повторяет подготовленный SELECT. Это отличается от close, который завершает работу с самой командой. Здесь вызовы идут по $stmt; ранее рассмотренный mysqli_result имеет собственный буфер и свои методы. Не смешивайте get_result и store_result как два обязательных шага одного получения.

Границы и частые ошибки

Буферизация расходует память на полный ответ. Мы ограничили SELECT тремя строками заранее; переносить этот приём на сотни тысяч строк ради одного счётчика или сортировки PHP нельзя. data_seek рассчитан на допустимое смещение внутри буферизованного набора. Порядок строк определяет ORDER BY в SQL; перемотка не создаёт порядок и не меняет саму таблицу.

Полная самостоятельная программа и файлы
Перечитать строку из буфера подготовленной командыСамостоятельный пример
<?php
declare(strict_types=1);
require __DIR__.'/ms112-helper.php';
$db=ms112Connect();
$runtime=['php'=>PHP_VERSION,'client'=>$db->client_info,'server'=>$db->server_info];
$details=[];$artifacts=[];
try {
$db->query('CREATE TEMPORARY TABLE mysqli112_stmt_page (id INT PRIMARY KEY,title VARCHAR(40) NOT NULL) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4');
$db->query("INSERT INTO mysqli112_stmt_page VALUES (11,'Чай'),(12,'Кофе'),(13,'Мёд')");
$stmt=$db->prepare('SELECT id,title FROM mysqli112_stmt_page ORDER BY id LIMIT 3');
$stmt->execute();$before=$stmt->num_rows;$stored=$stmt->store_result();$after=$stmt->num_rows;
$stmt->bind_result($id,$title);$stmt->data_seek(2);$thirdStatus=$stmt->fetch();$third=[$id,$title];
mysqli_stmt_data_seek($stmt,0);$firstStatus=mysqli_stmt_fetch($stmt);$first=[$id,$title];
$stmt->free_result();
$newSql=ms112Rows($db,'SELECT COUNT(*) AS count_rows FROM mysqli112_stmt_page');
$stmt->execute();$stmt->store_result();$stmt->bind_result($id,$title);$againStatus=$stmt->fetch();$again=[$id,$title];
$rows=[['После execute, до буферизации',$before,null,null],['После store_result',$after,null,null],['data_seek(2), затем fetch',$after,$third[0],$third[1]],['data_seek(0), затем fetch',$after,$first[0],$first[1]],['free_result, новый execute',$stmt->num_rows,$again[0],$again[1]]];
$checks=['buffer_counts'=>$before===0 && $after===3 && $stored===true,'positions_start_at_zero'=>$thirdStatus===true && $firstStatus===true && $third===[13,'Мёд'] && $first===[11,'Чай'],'buffer_can_be_released'=> (int)$newSql[0]['count_rows']===3,'statement_can_execute_again'=>$againStatus===true && $again===[11,'Чай']];
$details=['buffer_before'=>$before,'buffer_after'=>$after,'following_query_row_count'=>(int)$newSql[0]['count_rows']];
$stmt->free_result();$stmt->close();
} finally {$db->close();}
ms112Report('mysqli112-statement-statement-buffer', ['Шаг', 'num_rows', 'Товар ID', 'Название'], $rows, $checks, $runtime, $details, $artifacts);
echo 'mysqli112-statement-statement-buffer: verified result.html and result.json
';
Ожидаемый результат
mysqli112-statement-statement-buffer: verified result.html and result.json

Откройте result.html и сравните фактические строки с таблицей выше. Подключение, временные данные и helper уже включены; предыдущие программы выполнять не нужно.

Открыть фактический HTML-результат · Скачать результат JSON

Попробуйте сами. Замените data_seek(2) на data_seek(1). Следующий fetch вернёт ID 12 и Кофе. Изменение LIMIT на 2 делает прежнее смещение 2 недопустимым: перед переходом нужно согласовать позицию с num_rows.

Передать вложение порциями и восстановить те же байты

К записи заказа нужно приложить двоичный файл, не потеряв нулевые и непечатные байты. После записи и чтения файл должен восстановиться в точности. Успешного INSERT или совпадения одной длины для такой проверки недостаточно.

Исходные данные

ПолеЗначение
id42
bodyУчебные байты Receipt#42, перевод строки, 00 FF 01 и сумма
Размер порции5 байт, чтобы в маленьком примере увидеть несколько отправок
Как меняется состояние
  1. attachment.bin читается по 5 байт через fread
  2. bind_param ib связывает id и двоичный параметр
  3. send_long_data(1, chunk) добавляет байты ко второму параметру
  4. execute сохраняет одно цельное вложение
  5. SELECT → restored.bin; байты и SHA256 совпадают

Что получится

ФайлБайтовHEX содержимогоSHA256
Исходный attachment.bin25526563656970742334320a00ff01546f74616c3d323939300a6a39ab04f3ad4d8cdce13e9770f6650e222f37dd33a66ca3f1ebd036bf5c0933
Прочитанный restored.bin25526563656970742334320a00ff01546f74616c3d323939300a6a39ab04f3ad4d8cdce13e9770f6650e222f37dd33a66ca3f1ebd036bf5c0933

Минимальная запись действия

$stmt = $db->prepare('INSERT INTO files (id,body) VALUES (?,?)');
$id = 42;
$placeholder = null;
$stmt->bind_param('ib', $id, $placeholder);
$stmt->send_long_data(1, $firstChunk);
$stmt->send_long_data(1, $secondChunk);
$stmt->execute();
// Индекс 1 относится ко второму знаку ?, то есть body.

Это объясняющий фрагмент для уже подготовленной команды и объявленных переменных. Полная программа ниже самостоятельно создаёт таблицу, входные значения и итоговый отчёт.

Тип BLOB хранит двоичные данные, а не текст с языковой collation. В учебном файле есть байты00, FF и01, которые нельзя надёжно оценить глазами как строку HTML. Поэтому результат показывает HEX и SHA256. В полном примере файл сначала создаётся из явного набора hex-байтов; в приложении на его месте был бы уже существующий проверенный файл.

В bind_param строка ib означает: первый параметр — целое id, второй — двоичное значение b. $placeholder связывается по ссылке и изначально равен null. send_long_data получает номер параметра с нуля и очередную PHP-строку байтов. Индекс1 — второй вопросительный знак body. Несколько вызовов добавляют части одного значения; execute создаёт одну запись после их отправки.

Поток открыт в режиме rb. fread может вернуть строку либо false, поэтому ошибка чтения проверяется отдельно от пустой строки при конце файла. Вызов send_long_data также проверяется на успех; в строгом режиме MySQLi ошибка может быть исключением. finally закрывает файловый дескриптор. При чтении назад проверяются полное равенство байтов, длина и SHA256 обоих файлов; восстановленный restored.bin сохраняется рядом с программой.

Этот маленький тест демонстрирует механизм, а не измеряет экономию памяти. При большом входном файле порционная отправка позволяет не строить одну огромную PHP-строку заранее, но обратное чтение BLOB в этом учебном примере всё ещё получает полное значение. Проектирование большой выдачи файлов — отдельная задача.

Границы и частые ошибки

Порции не отменяют максимальный размер полного параметра, max_allowed_packet, предел BLOB-столбца и ресурсные ограничения. Официальное описание PHP содержит формулировку про большие пакеты; границу современного сервера сверяем отдельно по MySQL C API: общий параметр ограничен max_allowed_packet. Этот пример не меняет настройки сервера. Для больших вложений может быть удобнее файловое или объектное хранилище с ключом в таблице; выбор определяется требованиями к хранению и доступу.

Полная самостоятельная программа и файлы
Передать вложение порциями и восстановить те же байтыСамостоятельный пример
<?php
declare(strict_types=1);
require __DIR__.'/ms112-helper.php';
$db=ms112Connect();
$runtime=['php'=>PHP_VERSION,'client'=>$db->client_info,'server'=>$db->server_info];
$details=[];$artifacts=[];
try {
$db->query('CREATE TEMPORARY TABLE mysqli112_stmt_files (id INT PRIMARY KEY,body MEDIUMBLOB NOT NULL) ENGINE=InnoDB');
$source=hex2bin('526563656970742334320a00ff01546f74616c3d323939300a');
if($source===false){throw new RuntimeException('Invalid authored fixture');}
file_put_contents(__DIR__.'/attachment.bin',$source);
$stmt=$db->prepare('INSERT INTO mysqli112_stmt_files (id,body) VALUES (?,?)');
$fileId=42;$placeholder=null;$stmt->bind_param('ib',$fileId,$placeholder);
$stream=fopen(__DIR__.'/attachment.bin','rb');
if($stream===false){throw new RuntimeException('Cannot open attachment');}
$chunks=0;$sentBytes=0;
try {
    while(!feof($stream)){
        $chunk=fread($stream,5);
        if($chunk===false){throw new RuntimeException('Cannot read attachment');}
        if($chunk===''){continue;}
        if(!$stmt->send_long_data(1,$chunk)){throw new RuntimeException('Cannot send chunk');}
        $chunks++;$sentBytes+=strlen($chunk);
    }
} finally {fclose($stream);}
$stmt->execute();$stmt->close();
$stored=ms112Rows($db,'SELECT id,body FROM mysqli112_stmt_files WHERE id=42')[0];
file_put_contents(__DIR__.'/restored.bin',$stored['body']);$artifacts=['restored.bin'];
$inputHash=hash_file('sha256',__DIR__.'/attachment.bin');$outputHash=hash_file('sha256',__DIR__.'/restored.bin');
$rows=[['Исходный attachment.bin',strlen($source),bin2hex($source),$inputHash],['Прочитанный restored.bin',strlen($stored['body']),bin2hex($stored['body']),$outputHash]];
$checks=['all_bytes_sent'=>$sentBytes===strlen($source) && $chunks===5,'binary_roundtrip'=>$stored['body']===$source,'same_file_hash'=>$inputHash===$outputHash && $outputHash==='6a39ab04f3ad4d8cdce13e9770f6650e222f37dd33a66ca3f1ebd036bf5c0933','one_complete_value'=>(int)$stored['id']===42 && count(ms112Rows($db,'SELECT id FROM mysqli112_stmt_files'))===1];
$details=['chunk_limit_bytes'=>5,'nonempty_chunks'=>$chunks,'parameter_index'=>1,'sent_bytes'=>$sentBytes,'sha256'=>$outputHash,'fixture_has_binary_nul'=>str_contains($stored['body'],"\0")];
} finally {$db->close();}
ms112Report('mysqli112-statement-binary-chunks', ['Файл', 'Байтов', 'HEX содержимого', 'SHA256'], $rows, $checks, $runtime, $details, $artifacts);
echo 'mysqli112-statement-binary-chunks: verified result.html and result.json
';
Ожидаемый результат
mysqli112-statement-binary-chunks: verified result.html and result.json

Откройте result.html и сравните фактические строки с таблицей выше. Подключение, временные данные и helper уже включены; предыдущие программы выполнять не нужно.

Открыть фактический HTML-результат · Скачать результат JSON

Попробуйте сами. Измените размер fread с5 на8. Число непустых отправок уменьшится, но восстановленные байты и SHA256 должны остаться теми же. Обновить нужно ожидание числа порций, а не эталон содержимого.

Практический вопрос: Порционная отправка BLOB и предел размера параметра. Разбиение файла на порции само по себе не отменяет серверные ограничения. Наш небольшой двоичный файл показывает механизм и точность сохранения, а граница max_allowed_packet объяснена по документации MySQL. Вопрос задаёт ситуацию; технические правила сверены с официальной документацией, наш пример написан отдельно.

Отбросить незаписанные части вложения перед заменой

При подготовке вложения приложение уже отправило части старого черновика, но пользователь выбрал окончательный файл до INSERT. Если просто дописать новые части, они соединятся со старыми. Нужно явно отказаться от незавершённого значения.

Исходные данные

ФазаЧасти body
Старый выборobsolete- + draft
Новый выборfinal- + receipt
Как меняется состояние
  1. send_long_data передал obsolete- и draft; строки ещё нет
  2. reset возвращает команду к подготовленному состоянию
  3. Старая последовательность байтов отброшена
  4. Новые части и execute дают ровно final-receipt

Что получится

ШагСтрок в таблицеСохранённое содержимое
Отправлены obsolete- и draft; execute ещё не было0Строки нет
reset очистил части0Строки нет
Отправлены final- и receipt; выполнен execute1final-receipt

Минимальная запись действия

$stmt->send_long_data(1, 'obsolete-draft');
$stmt->reset();
$stmt->send_long_data(1, 'final-receipt');
$stmt->execute();
// В поле body окажется final-receipt, без obsolete-draft.

Это объясняющий фрагмент для уже подготовленной команды и объявленных переменных. Полная программа ниже самостоятельно создаёт таблицу, входные значения и итоговый отчёт.

reset очищает временное состояние подготовленной команды на клиенте и сервере, включая переданные long data, небуферизованный ответ и текущую ошибку. Привязки параметров и сам SQL сохраняются. Поэтому после reset программа не повторяет prepare и bind_param, а заново отправляет только нужные части.

Здесь reset вызывается процедурно: mysqli_stmt_reset($stmt). Это та же операция, что $stmt->reset(), с явным объектом первым аргументом. Возвращается true при успехе или false при отказе. До первого execute количество строк равно нулю и после reset остаётся нулём: байты старого черновика ещё не были отдельной строкой таблицы.

После отправки final- и receipt execute записывает одно значение final-receipt. Пример сравнивает полное содержимое, а не только факт наличия записи. Если ошибка произошла во время чтения исходного файла, не выполняйте INSERT с неожиданно неполным набором частей: явно сбросьте состояние или завершите команду и согласованно сообщите об отказе.

Границы и частые ошибки

reset не меняет SQL; другой запрос требует prepare. Он также не очищает уже буферизованный store_result-ответ: для этого предусмотрен free_result, новое выполнение или закрытие команды. Не путайте сброс незавершённых частей параметра с откатом записанной строки — следующий пример показывает эту границу. Программа не моделирует реальную HTTP-загрузку пользователя.

Полная самостоятельная программа и файлы
Отбросить незаписанные части вложения перед заменойСамостоятельный пример
<?php
declare(strict_types=1);
require __DIR__.'/ms112-helper.php';
$db=ms112Connect();
$runtime=['php'=>PHP_VERSION,'client'=>$db->client_info,'server'=>$db->server_info];
$details=[];$artifacts=[];
try {
$db->query('CREATE TEMPORARY TABLE mysqli112_stmt_pending (id INT PRIMARY KEY,body BLOB NOT NULL) ENGINE=InnoDB');
$stmt=$db->prepare('INSERT INTO mysqli112_stmt_pending (id,body) VALUES (?,?)');
$id=9;$placeholder=null;$stmt->bind_param('ib',$id,$placeholder);
$stmt->send_long_data(1,'obsolete-');$stmt->send_long_data(1,'draft');
$before=(int)ms112Rows($db,'SELECT COUNT(*) AS count_rows FROM mysqli112_stmt_pending')[0]['count_rows'];
$resetOK=mysqli_stmt_reset($stmt);
$afterReset=(int)ms112Rows($db,'SELECT COUNT(*) AS count_rows FROM mysqli112_stmt_pending')[0]['count_rows'];
$stmt->send_long_data(1,'final-');$stmt->send_long_data(1,'receipt');$stmt->execute();$stmt->close();
$saved=ms112Rows($db,'SELECT id,body FROM mysqli112_stmt_pending ORDER BY id');
$rows=[['Отправлены obsolete- и draft; execute ещё не было',$before,'Строки нет'],['reset очистил части',$afterReset,'Строки нет'],['Отправлены final- и receipt; выполнен execute',count($saved),$saved[0]['body']]];
$checks=['send_does_not_insert'=>$before===0,'reset_before_execute_keeps_table_empty'=>$resetOK===true && $afterReset===0,'only_replacement_saved'=>count($saved)===1 && (int)$saved[0]['id']===9 && $saved[0]['body']==='final-receipt'];
$details=['discarded_parts'=>['obsolete-','draft'],'saved_parts'=>['final-','receipt'],'SQL_parameter_bindings_reused'=>true];
} finally {$db->close();}
ms112Report('mysqli112-statement-reset-pending', ['Шаг', 'Строк в таблице', 'Сохранённое содержимое'], $rows, $checks, $runtime, $details, $artifacts);
echo 'mysqli112-statement-reset-pending: verified result.html and result.json
';
Ожидаемый результат
mysqli112-statement-reset-pending: verified result.html and result.json

Откройте result.html и сравните фактические строки с таблицей выше. Подключение, временные данные и helper уже включены; предыдущие программы выполнять не нужно.

Открыть фактический HTML-результат · Скачать результат JSON

Попробуйте сами. Уберите reset между двумя наборами частей. Получится obsolete-draftfinal-receipt, а проверка точного результата должна обнаружить ошибку. Не исправляйте проверку так, чтобы она принимала повреждённое значение: восстановите сброс.

Повторно использовать привязки и не спутать reset с откатом

Две записи журнала относятся к одной операции обработки заказа. После первой execute команда сбрасывается и используется повторно. Нужно понять, что сохраняется в PHP-привязках и какие действия действительно отменяют записи базы.

Исходные данные

IDСобытие
1Заказ принят
2Заказ собран
Как меняется состояние
  1. begin_transaction открывает общую транзакцию
  2. bind_param привязывает переменные $id и $label
  3. execute записывает событие 1; reset его не удаляет
  4. Новые значения тех же переменных + execute записывают событие 2
  5. rollback отменяет обе записи этой незавершённой транзакции

Что получится

ШагСтрок видно в этой транзакцииСодержимое
После INSERT 1 и reset1Заказ принят
После изменения тех же переменных и execute2Заказ принят; Заказ собран
После rollback транзакции0Строк нет

Минимальная запись действия

$stmt->bind_param('is', $id, $label);
$stmt->execute();
$stmt->reset();
$id = 2;
$label = 'Заказ собран';
$stmt->execute(); // Новые значения тех же привязанных переменных.
$db->rollback(); // Откатывает обе записи незавершённой транзакции.

Это объясняющий фрагмент для уже подготовленной команды и объявленных переменных. Полная программа ниже самостоятельно создаёт таблицу, входные значения и итоговый отчёт.

bind_param работает с переменными по ссылке. После первого выполнения $id и $label остаются привязанными, даже если reset очистил временное состояние команды. Изменение их значений перед вторым execute передаёт ID 2 и новый текст. Повторная привязка в этом примере не требуется; полная программа проверяет обе фактически записанные строки.

Сброс команды и транзакция — разные уровни. reset не отменяет уже выполненный INSERT. Внутри этого соединения после reset всё ещё видна первая запись. Только rollback завершает транзакцию откатом обеих записей. Для обычного успешного действия вместо учебного отката вы согласованно вызываете commit.

Временная InnoDB-таблица создана до begin_transaction, чтобы не смешивать DDL и бизнес-записи. Схема временной таблицы остаётся доступной после rollback, но обе вставленные в транзакции строки исчезают. Диагностика подсчитывает строки после каждого шага; это проверка состояния, а не вывод заранее заготовленных сообщений об успешном откате.

Границы и частые ошибки

Здесь нет двух конкурентных соединений: правила видимости между пользователями объясняются в отдельной главе. Если запись уже подтверждена commit или выполнялась при autocommit, последующий reset не вернёт её назад. Не подменяйте откат повторным execute и не используйте reset для замены SQL; эти операции имеют разные обязанности.

Полная самостоятельная программа и файлы
Повторно использовать привязки и не спутать reset с откатомСамостоятельный пример
<?php
declare(strict_types=1);
require __DIR__.'/ms112-helper.php';
$db=ms112Connect();
$runtime=['php'=>PHP_VERSION,'client'=>$db->client_info,'server'=>$db->server_info];
$details=[];$artifacts=[];
try {
$db->query('CREATE TEMPORARY TABLE mysqli112_stmt_events (id INT PRIMARY KEY,label VARCHAR(40) NOT NULL) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4');
$db->begin_transaction();
$stmt=$db->prepare('INSERT INTO mysqli112_stmt_events (id,label) VALUES (?,?)');
$id=1;$label='Заказ принят';$stmt->bind_param('is',$id,$label);$stmt->execute();
$resetOK=$stmt->reset();
$afterReset=ms112Rows($db,'SELECT id,label FROM mysqli112_stmt_events ORDER BY id');
$id=2;$label='Заказ собран';$stmt->execute();
$twoRows=ms112Rows($db,'SELECT id,label FROM mysqli112_stmt_events ORDER BY id');
$db->rollback();
$afterRollback=ms112Rows($db,'SELECT id,label FROM mysqli112_stmt_events ORDER BY id');
$rows=[['После INSERT 1 и reset',count($afterReset),'Заказ принят'],['После изменения тех же переменных и execute',count($twoRows),'Заказ принят; Заказ собран'],['После rollback транзакции',count($afterRollback),'Строк нет']];
$checks=['reset_keeps_executed_insert'=>$resetOK===true && count($afterReset)===1 && $afterReset[0]['label']==='Заказ принят','bound_variables_are_still_bound'=>array_column($twoRows,'label')===['Заказ принят','Заказ собран'] && array_map('intval',array_column($twoRows,'id'))===[1,2],'rollback_removes_transaction_writes'=>$afterRollback===[]];
$details=['reset_return'=>$resetOK,'rows_before_rollback'=>count($twoRows),'rows_after_rollback'=>count($afterRollback),'repeated_bind_param_calls'=>0];
$stmt->close();
} finally {$db->close();}
ms112Report('mysqli112-statement-reset-not-rollback', ['Шаг', 'Строк видно в этой транзакции', 'Содержимое'], $rows, $checks, $runtime, $details, $artifacts);
echo 'mysqli112-statement-reset-not-rollback: verified result.html and result.json
';
Ожидаемый результат
mysqli112-statement-reset-not-rollback: verified result.html and result.json

Откройте result.html и сравните фактические строки с таблицей выше. Подключение, временные данные и helper уже включены; предыдущие программы выполнять не нужно.

Открыть фактический HTML-результат · Скачать результат JSON

Попробуйте сами. Замените последний rollback на commit и снова прочитайте таблицу. Останутся обе строки, хотя reset вызывался между ними. Временная таблица исчезнет только после завершения соединения, а не из-за commit.

Дальше: отдельно отправить запрос и дождаться ответа

Подготовленная команда теперь имеет понятное состояние и границы. Когда независимых запросов несколько, переходите к асинхронному ожиданию MySQLi. Для согласованной записи нескольких таблиц нужны транзакции и конкуренция, а для отбора только нужных данных — SQL под конкретный экран.

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

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

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

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

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