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

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

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

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

Глава 111 · После подключения PDO_MYSQL и параметров

PDO_MYSQL: строки результата, словари, потоковое чтение и курсоры

Найденный товар с остатком 0, выбор FETCH-формы, сохранение групп, освобождение unbuffered-ответа и чтение всех rowset.

Строка, значение и курсор — разные решения

Успешный SQL — только начало. Экрану нужно понять, найден ли товар, не затерялся ли нулевой остаток и в каком виде читать поля. Выгрузке нужно освободить соединение после чтения. На одной маленькой таблице разделим эти решения.

Ответ / объектСмыслЧто делать
execute() вернул trueSQL выполненОтдельно прочитать результат
fetch вернул массивОдна строка найденаЧитать ключи, сохраняя 0/NULL/пустоту
fetch вернул falseСтрок больше нет; при другом error mode также возможен отказВ режиме исключений отличить завершение от PDOException
PDOStatement ещё содержит ответСоединение может быть занятоДочитать или closeCursor
nextRowset вернул trueЕсть следующий наборУчитывать другой набор полей
Не смешиваем отсутствие строки и значения
  1. ID 1 → [name:Чай,stock:0,note:NULL]
  2. ID 2 → [name:Кофе,stock:4,note:пустая строка]
  3. ID 99 → fetch:false
  4. Только последний случай означает: товар не найден

Предыдущая глава объясняет соединение и передачу параметров. Полный справочник методов сохраняется в карточках PDO. Эти новые программы дополняют его реальным MySQL-запуском; ранее отмеченные lint-примеры других драйверов не становятся выполненными автоматически.

Самостоятельные исходные данные и результаты

Полные архивы рассчитаны на PHP 8.4+ с pdo_mysql/mysqlnd; контрольное окружение — PHP 8.5.11 и MySQL 8.4.13. Новый PDO::connect и подкласс Pdo\Mysql появились в PHP 8.4. Прежний new PDO остаётся доступен, но весь комплект не заявлен как совместимый с PHP 8.2. Установка PHP-расширения не устанавливает сам MySQL-сервер.

Сначала подготовьте отдельную локальную базу и ограниченного пользователя по инструкции подключения. MySQL Workbench — клиент управления, а не сам сервер. Используйте свои значения PHPMAN_MYSQL_HOST, PHPMAN_MYSQL_PORT, PHPMAN_MYSQL_DATABASE, PHPMAN_MYSQL_USER, PHPMAN_MYSQL_PASSWORD в окружении терминала. На контрольном стенде адрес 127.0.0.1:33070; у вашей установки может быть другой порт. Пример команды PowerShell: $env:PHPMAN_MYSQL_DATABASE='ваша_учебная_база'. Для sh: export PHPMAN_MYSQL_DATABASE='ваша_учебная_база'. Остальные переменные задаются аналогично; пароль не помещайте в публичный исходник.

Распакуйте один архив в отдельную папку, выполните php example.php, откройте result.html. Рядом появятся result.json с теми же значениями и metrics.json с версиями/проверками. README и helper уже включены. В каждом запуске создаются собственные TEMPORARY-таблицы pdo109_*; данные предыдущего примера не нужны. Постоянные таблицы и схема не удаляются. Это CLI-проверка PHP/MySQL, а не действующая форма сайта.

Нет строки, нулевой остаток и NULL — три разных результата

Карточка товара не должна сообщать «не найден», когда у товара просто закончился остаток. Нужен отдельный признак существования строки.

Путь данных: Нет строки, нулевой остаток и NULL — три разных результата
  1. SELECT WHERE id=1 → массив с stock=0
  2. SELECT WHERE id=2 → массив с note пустая строка
  3. SELECT WHERE id=99 → false
  4. Решение определяется наличием строки, а не истинностью остатка

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

Запрошен IDСтрока найденаТоварОстатокПримечание
1trueЧай0NULL
2trueКофе4(пустая строка)
99falseNULLNULLNULL

Короткая запись действия

$st->execute([1]);
$product = $st->fetch(PDO::FETCH_ASSOC);
if ($product === false) { echo 'Товар не найден'; }
else { echo $product['stock']; } // 0 — найденный товар

Фрагмент предполагает уже созданные соединение и учебные таблицы; $st/$statement — результат нужной команды. Полная программа ниже создаёт исходные данные сама. Это выдержка для объяснения, а не отдельный дополнительный запуск.

execute возвращает успешность выполнения SQL, а не наличие товара. fetch(PDO::FETCH_ASSOC) возвращает массив найденной строки или false при завершении результата. Поэтому проверяется $product === false. У ID 1 найден массив с stock=0 и note=NULL; у ID 2 note — пустая строка. У ID 99 нет самого массива. В таблице отчёта это видно в отдельном столбце «Строка найдена».

Дополнительная проверка читает stock ID 1 через fetchColumn: первый ответ равен 0, следующий — false, потому что строк больше нет. Нестрогое if (!$value) смешало бы эти случаи. Для настоящих булевых полей fetchColumn неудобен: false может быть значением и признаком завершения; используйте fetch с массивом. В режиме исключений SQL-ошибка попадает в PDOException, её не следует превращать в «товар не найден».

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

Не определяйте наличие SELECT-строк через rowCount как общий договор PDO: поведение зависит от драйвера и режима буферизации. Для одного товара достаточно fetch; для отдельного общего количества используйте COUNT с тем же фильтром. NULL в ячейке не означает отсутствие строки.

Полная самостоятельная программа и файлы для запуска
Нет строки, нулевой остаток и NULL — три разных результатаСамостоятельный пример
<?php
declare(strict_types=1);
require __DIR__.'/pdo109-results-helper.php';

$details=[];
$db=pdo109Connect(); $runtime=pdo109Runtime($db); pdo109Catalog($db);
$st=$db->prepare('SELECT name,stock,note FROM pdo109_products WHERE id=?'); $rows=[]; $seen=[];
foreach ([1,2,99] as $id) {
    $st->execute([$id]); $r=$st->fetch(PDO::FETCH_ASSOC); $seen[]=$r;
    $rows[]=[$id,$r!==false,$r===false?null:$r['name'],$r===false?null:(int)$r['stock'],$r===false?null:$r['note']];
    $st->closeCursor();
}
$one=$db->query('SELECT stock FROM pdo109_products WHERE id=1'); $zero=$one->fetchColumn(); $end=$one->fetchColumn(); $one->closeCursor();
$checks=['zero_is_existing'=>$seen[0]!==false && (int)$seen[0]['stock']===0,'missing_is_false'=>$seen[2]===false,'null_and_empty'=>$seen[0]['note']===null && $seen[1]['note']==='','column_zero_not_eof'=>$zero!==false && (int)$zero===0 && $end===false];
$details=['fetchColumn_zero_type'=>get_debug_type($zero),'fetchColumn_end_is_false'=>$end===false]; $st=$one=null; $db=null;
pdo109Report('pdo109-missing-zero', ['Запрошен ID', 'Строка найдена', 'Товар', 'Остаток', 'Примечание'], $rows, $checks, $runtime, $details);
echo 'missing-zero: saved result.html and result.json
';
Ожидаемый результат
missing-zero: saved result.html and result.json

Откройте сохранённый result.html: результат — конкретные строки таблицы выше. JSON содержит те же значения. Helper и README вложены; отдельные предыдущие примеры запускать не нужно. Проверяется PHP CLI с MySQL, не HTTP.

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

Попробуйте сами. Измените запрошенный список на [1,3,99]. Назовите три разных ветки интерфейса: найден без остатка; найден с остатком; отсутствует.

Выбрать форму строки для шаблона, CSV и списка

Шаблону удобны имена полей, CSV — позиции, простому списку — один столбец. Выберем форму PHP-результата осознанно на одних товарных данных.

Путь данных: Выбрать форму строки для шаблона, CSV и списка
  1. Один SELECT id,name WHERE id=2
  2. ASSOC → ключи id/name
  3. NUM → позиции 0/1; OBJ → свойство name
  4. COLUMN всего SELECT name → список трёх названий

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

Способ чтенияКлючи / обращениеПолезное значение
FETCH_ASSOCid, nameКофе
FETCH_NUM0, 1Кофе
FETCH_OBJstdClass → nameКофе
FETCH_COLUMNсписок значенийЧай, Кофе, Мёд

Короткая запись действия

$product = $st->fetch(PDO::FETCH_ASSOC); // $product['name']
// В другом выполнении того же SELECT:
$product = $st->fetch(PDO::FETCH_OBJ);   // $product->name

Фрагмент предполагает уже созданные соединение и учебные таблицы; $st/$statement — результат нужной команды. Полная программа ниже создаёт исходные данные сама. Это выдержка для объяснения, а не отдельный дополнительный запуск.

FETCH_ASSOC даёт массив ['id'=>2,'name'=>'Кофе']; FETCH_NUM — позиции 0 и 1 в порядке SELECT. FETCH_OBJ возвращает stdClass, поэтому имя читается через ->name. В полном примере один и тот же SELECT выполняется отдельно для каждой формы: successive fetch не возвращают одну строку в разных обёртках, а продвигаются вперёд.

FETCH_COLUMN в fetchAll собирает оставшиеся значения выбранного столбца в список; здесь SELECT name возвращает Чай, Кофе, Мёд. Способ чтения не добавляет поля, не сортирует ответ и не меняет SQL. Объект stdClass не получает бизнес-методы автоматически. Для обычного HTML удобно явно выбрать FETCH_ASSOC, чтобы обращение не зависело от порядка колонок.

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

В исходном PDO default FETCH_BOTH добавляет именованные и числовые ключи. Helper явно устанавливает FETCH_ASSOC, но каждый учебный вызов с важной формой указывает режим. fetchAll загружает оставшийся результат в PHP-массив; для большой выгрузки выбирайте ограниченную выборку или потоковое чтение.

Полная самостоятельная программа и файлы для запуска
Выбрать форму строки для шаблона, CSV и спискаСамостоятельный пример
<?php
declare(strict_types=1);
require __DIR__.'/pdo109-results-helper.php';

$details=[];
$db=pdo109Connect(); $runtime=pdo109Runtime($db); pdo109Catalog($db);
$sql='SELECT id,name FROM pdo109_products WHERE id=2';
$st=$db->query($sql); $assoc=$st->fetch(PDO::FETCH_ASSOC); $st->closeCursor();
$st=$db->query($sql); $numeric=$st->fetch(PDO::FETCH_NUM); $st->closeCursor();
$st=$db->query($sql); $object=$st->fetch(PDO::FETCH_OBJ); $st->closeCursor();
$st=$db->query('SELECT name FROM pdo109_products ORDER BY id'); $names=$st->fetchAll(PDO::FETCH_COLUMN); $st->closeCursor();
$rows=[['FETCH_ASSOC',implode(', ',array_keys($assoc)),$assoc['name']],['FETCH_NUM',implode(', ',array_keys($numeric)),$numeric[1]],['FETCH_OBJ',get_class($object).' → name',$object->name],['FETCH_COLUMN','список значений',implode(', ',$names)]];
$checks=['associative_fields'=>array_keys($assoc)===['id','name'],'numeric_positions'=>array_keys($numeric)===[0,1],'object_name'=>$object->name==='Кофе','column_list'=>$names===['Чай','Кофе','Мёд']];
$st=null; $db=null;
pdo109Report('pdo109-fetch-shapes', ['Способ чтения', 'Ключи / обращение', 'Полезное значение'], $rows, $checks, $runtime, $details);
echo 'fetch-shapes: saved result.html and result.json
';
Ожидаемый результат
fetch-shapes: saved result.html and result.json

Откройте сохранённый result.html: результат — конкретные строки таблицы выше. JSON содержит те же значения. Helper и README вложены; отдельные предыдущие примеры запускать не нужно. Проверяется PHP CLI с MySQL, не HTTP.

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

Попробуйте сами. Поменяйте порядок SELECT на name,id. В FETCH_NUM изменятся позиции; в FETCH_ASSOC имя по-прежнему доступно как ['name']. Предскажите обе структуры.

Словарь по ID и группы товаров: не потерять повторяющийся ключ

Для выпадающего списка нужен словарь ID→название, а для меню разделов — категория→несколько товаров. Одинаковый режим чтения для этих задач может молча потерять товар.

Путь данных: Словарь по ID и группы товаров: не потерять повторяющийся ключ
  1. Строки: (1,Чай),(2,Кофе),(3,Мёд)
  2. KEY_PAIR по id → три уникальных ключа
  3. KEY_PAIR по категории → Кофе заменяет Чай
  4. GROUP|COLUMN → Напитки:[Чай,Кофе]

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

Режим / первый столбецКлючПолучено
KEY_PAIR: id2Кофе
KEY_PAIR: categoryНапиткиКофе
GROUP | COLUMNНапиткиЧай, Кофе
GROUP | COLUMNПродуктыМёд

Короткая запись действия

$st = $db->query('SELECT category,name FROM pdo109_products ORDER BY id');
$groups = $st->fetchAll(PDO::FETCH_GROUP | PDO::FETCH_COLUMN);
// $groups['Напитки'] = ['Чай', 'Кофе'];

Фрагмент предполагает уже созданные соединение и учебные таблицы; $st/$statement — результат нужной команды. Полная программа ниже создаёт исходные данные сама. Это выдержка для объяснения, а не отдельный дополнительный запуск.

FETCH_KEY_PAIR требует ровно два выбранных столбца: первый становится ключом, второй — значением. Уникальный id подходит для словаря. category не уникальна: Чай и Кофе принадлежат Напиткам. При таком KEY_PAIR поздняя строка заменяет прежнее значение ключа, и остаётся только Кофе. В запросе есть ORDER BY id, поэтому показанный порядок и замена воспроизводимы.

FETCH_GROUP | FETCH_COLUMN оставляет первый столбец ключом группы, а вторые значения собирает в список. Получается ['Напитки'=>['Чай','Кофе'],'Продукты'=>['Мёд']]. Это уже вложенный массив. Символ | объединяет флаги режима. Такая группировка формирует PHP-структуру; SQL GROUP BY для агрегирования строк — другой инструмент, который может изменить сами данные ответа.

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

FETCH_UNIQUE также требует действительно уникального ключа, иначе строки теряются. Проверяйте уникальность по модели данных, а не по текущим двум товарам. Для очень большого каталога не загружайте всё ради маленького меню: задайте границы SELECT. Имена колонок в FETCH_ASSOC при JOIN делайте уникальными через aliases.

Полная самостоятельная программа и файлы для запуска
Словарь по ID и группы товаров: не потерять повторяющийся ключСамостоятельный пример
<?php
declare(strict_types=1);
require __DIR__.'/pdo109-results-helper.php';

$details=[];
$db=pdo109Connect(); $runtime=pdo109Runtime($db); pdo109Catalog($db);
$st=$db->query('SELECT id,name FROM pdo109_products ORDER BY id'); $byId=$st->fetchAll(PDO::FETCH_KEY_PAIR); $st->closeCursor();
$st=$db->query('SELECT category,name FROM pdo109_products ORDER BY id'); $byCategory=$st->fetchAll(PDO::FETCH_KEY_PAIR); $st->closeCursor();
$st=$db->query('SELECT category,name FROM pdo109_products ORDER BY id'); $groups=$st->fetchAll(PDO::FETCH_GROUP|PDO::FETCH_COLUMN); $st->closeCursor();
$rows=[['KEY_PAIR: id',2,$byId[2]],['KEY_PAIR: category','Напитки',$byCategory['Напитки']],['GROUP | COLUMN','Напитки',implode(', ',$groups['Напитки'])],['GROUP | COLUMN','Продукты',implode(', ',$groups['Продукты'])]];
$checks=['id_dictionary'=>$byId===[1=>'Чай',2=>'Кофе',3=>'Мёд'],'duplicate_key_replaced'=>$byCategory['Напитки']==='Кофе','group_preserves_both'=>$groups['Напитки']===['Чай','Кофе'],'all_products_grouped'=>array_sum(array_map('count',$groups))===3];
$details=['dictionary'=>$byId,'groups'=>$groups]; $st=null; $db=null;
pdo109Report('pdo109-keyed-groups', ['Режим / первый столбец', 'Ключ', 'Получено'], $rows, $checks, $runtime, $details);
echo 'keyed-groups: saved result.html and result.json
';
Ожидаемый результат
keyed-groups: saved result.html and result.json

Откройте сохранённый result.html: результат — конкретные строки таблицы выше. JSON содержит те же значения. Helper и README вложены; отдельные предыдущие примеры запускать не нужно. Проверяется PHP CLI с MySQL, не HTTP.

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

Попробуйте сами. Добавьте Какао в Напитки. KEY_PAIR снова сохранит только одно значение категории; GROUP должен содержать три названия. Объясните, почему это свойство структуры, а не случайный сбой.

Прервали чтение: освободить соединение перед следующим запросом

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

Путь данных: Прервали чтение: освободить соединение перед следующим запросом
  1. Unbuffered SELECT: 3 строки
  2. fetch → Чай, 2 строки остались
  3. Новый query → ожидаемый 2014
  4. closeCursor → query SELECT 9 →9

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

ДействиеНаблюдаемый результат
Прочитана только первая строкаЧай
Следующий запрос до закрытияHY000 / 2014
closeCursor вернулtrue
Новый запрос после закрытия9

Короткая запись действия

$st = $db->query('SELECT id,name FROM pdo109_products ORDER BY id');
$first = $st->fetch(PDO::FETCH_ASSOC);
$st->closeCursor(); // Остальные строки больше не нужны
$next = $db->query('SELECT 9 AS next_value');

Фрагмент предполагает уже созданные соединение и учебные таблицы; $st/$statement — результат нужной команды. Полная программа ниже создаёт исходные данные сама. Это выдержка для объяснения, а не отдельный дополнительный запуск.

ATTR_USE_BUFFERED_QUERY=false задан при создании соединения. После первого fetch остаются ещё две строки. Пример намеренно пробует SELECT 9 и сохраняет ожидаемый код HY000/2014. Затем closeCursor освобождает предыдущий ответ; новый SELECT 9 действительно выполняется и возвращает 9. Это воспроизводимая граница MySQL-клиента, не отказ второго SQL из-за его синтаксиса.

closeCursor не закрывает соединение PDO, не удаляет строки таблицы и не выполняет rollback. Он освобождает результат; для некоторых драйверов это включает дочитывание/отбрасывание данных, поэтому действие не обязательно мгновенно. Statement можно снова выполнить. Если данные нужны, прочитайте их до конца; если уже не нужны — явно завершите работу с результатом в finally.

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

Не добавляйте fetchAll только ради устранения ошибки, если результат велик: это перенесёт весь ответ в память. Не выполняйте новый запрос внутри unbuffered-цикла на том же соединении, пока оно занято. Второе соединение меняет транзакционный контекст и не является автоматической универсальной заменой.

Полная самостоятельная программа и файлы для запуска
Прервали чтение: освободить соединение перед следующим запросомСамостоятельный пример
<?php
declare(strict_types=1);
require __DIR__.'/pdo109-results-helper.php';

$details=[];
$db=pdo109Connect([Pdo\Mysql::ATTR_USE_BUFFERED_QUERY=>false]); $runtime=pdo109Runtime($db); pdo109Catalog($db);
$st=$db->query('SELECT id,name FROM pdo109_products ORDER BY id'); $first=$st->fetch();
$state='not-rejected'; $driverCode=0;
try { $db->query('SELECT 9 AS next_value')->closeCursor(); }
catch (PDOException $e) { $state=$e->getCode(); $driverCode=(int)($e->errorInfo[1] ?? 0); }
$closed=$st->closeCursor();
$next=$db->query('SELECT 9 AS next_value'); $value=(int)$next->fetchColumn(); $next->closeCursor();
$rows=[['Прочитана только первая строка',$first['name']],['Следующий запрос до закрытия',$state.' / '.$driverCode],['closeCursor вернул',$closed],['Новый запрос после закрытия',$value]];
$checks=['first_product'=>$first['name']==='Чай','pending_result_rejected'=>$state==='HY000' && $driverCode===2014,'cursor_released'=>$closed && $value===9];
$st=$next=null; $db=null;
pdo109Report('pdo109-unbuffered-release', ['Действие', 'Наблюдаемый результат'], $rows, $checks, $runtime, $details);
echo 'unbuffered-release: saved result.html and result.json
';
Ожидаемый результат
unbuffered-release: saved result.html and result.json

Откройте сохранённый result.html: результат — конкретные строки таблицы выше. JSON содержит те же значения. Helper и README вложены; отдельные предыдущие примеры запускать не нужно. Проверяется PHP CLI с MySQL, не HTTP.

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

Попробуйте сами. Уберите намеренную неудачную попытку, но оставьте closeCursor перед SELECT 9. Это обычный корректный путь приложения. Затем разберите следующий пример с полным чтением.

Практический вопрос: Следующий PDO-запрос не выполняется, пока предыдущий ответ не освобождён. Вопрос показывает путаницу между чтением результата и готовностью соединения к следующему запросу. Наши отдельные программы показывают недочитанные строки и несколько наборов. Вопрос помогает выбрать ситуацию; технический договор сверён с PHP/MySQL, код и данные написаны отдельно.

Дочитать поток строк и получить итог без fetchAll

Нужно обработать каждую строку выгрузки и посчитать стоимость складского остатка. fetchAll для накопления всех товаров не обязателен.

Путь данных: Дочитать поток строк и получить итог без fetchAll
  1. fetch Чай:0×12000 →0
  2. fetch Кофе:4×30000 →120000
  3. fetch Мёд:2×25000 →50000
  4. Конец ответа → итог 170000, соединение свободно

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

ТоварОстатокЦена в копейкахСтоимость остатка
Чай0120000
Кофе430000120000
Мёд22500050000
ИтогоNULLNULL170000

Короткая запись действия

$total = 0;
while (($product = $st->fetch(PDO::FETCH_ASSOC)) !== false) {
    $total += $product['stock'] * $product['price_cents'];
}
$st->closeCursor();

Фрагмент предполагает уже созданные соединение и учебные таблицы; $st/$statement — результат нужной команды. Полная программа ниже создаёт исходные данные сама. Это выдержка для объяснения, а не отдельный дополнительный запуск.

Цикл fetch читает по товару и прибавляет stock × price_cents. Для Чая это 0, для Кофе 120000 копеек, для Мёда 50000; итог 170000 копеек. Проверка !== false дочитывает ответ до конца. После закрытия курсора выполняется отдельный COUNT и подтверждает три исходные строки. В приложении вместо добавления каждой строки в массив отчёта можно сразу писать её в CSV или передавать обработчику.

Здесь маленький массив $rows всё же сохраняется для наглядной таблицы из трёх товаров. Поэтому пример показывает чтение без fetchAll, но не доказывает постоянное потребление памяти всей программы. Состояние реального экспортёра должно оставаться ограниченным: счётчик/итог и поток назначения. Если нужен только итог, выполните SELECT SUM(stock * price_cents) AS total FROM pdo109_products: MySQL вернёт одну строку. Здесь цикл выбран для построчного отчёта с итогом, где каждую строку тоже надо обработать.

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

Цены представлены целыми копейками, а не float. Нужна проверка диапазонов при очень больших суммах. Unbuffered уменьшает клиентский буфер, но не гарантирует, что MySQL не сортирует/материализует данные на сервере. Для экрана ограничивайте SQL, как показано в выборке нужной страницы.

Полная самостоятельная программа и файлы для запуска
Дочитать поток строк и получить итог без fetchAllСамостоятельный пример
<?php
declare(strict_types=1);
require __DIR__.'/pdo109-results-helper.php';

$details=[];
$db=pdo109Connect([Pdo\Mysql::ATTR_USE_BUFFERED_QUERY=>false]); $runtime=pdo109Runtime($db); pdo109Catalog($db);
$st=$db->prepare('SELECT name,stock,price_cents FROM pdo109_products ORDER BY id'); $st->execute();
$rows=[]; $total=0; $read=0;
while (($product=$st->fetch(PDO::FETCH_ASSOC)) !== false) {
    $value=(int)$product['stock']*(int)$product['price_cents']; $total+=$value; $read++;
    $rows[]=[$product['name'],(int)$product['stock'],(int)$product['price_cents'],$value];
}
$st->closeCursor();
$after=$db->query('SELECT COUNT(*) FROM pdo109_products'); $count=(int)$after->fetchColumn(); $after->closeCursor();
$checks=['all_rows_read'=>$read===3 && $count===3,'exact_stock_value'=>$total===170000,'zero_stock_preserved'=>$rows[0][3]===0];
$details=['read_count'=>$read,'total_cents'=>$total,'count_after_drain'=>$count,'bounded_teaching_report'=>true];
$rows[]=['Итого',null,null,$total]; $st=$after=null; $db=null;
pdo109Report('pdo109-stream-summary', ['Товар', 'Остаток', 'Цена в копейках', 'Стоимость остатка'], $rows, $checks, $runtime, $details);
echo 'stream-summary: saved result.html and result.json
';
Ожидаемый результат
stream-summary: saved result.html and result.json

Откройте сохранённый result.html: результат — конкретные строки таблицы выше. JSON содержит те же значения. Helper и README вложены; отдельные предыдущие примеры запускать не нужно. Проверяется PHP CLI с MySQL, не HTTP.

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

Попробуйте сами. Представьте, что требуется только общий итог без CSV. Запишите SELECT SUM(stock * price_cents) и сравните смысл результата, не объявляя оба способа одинаковыми по расходам.

Несколько наборов: прочитать каждый и вернуть соединение в работу

Одна операция возвращает несколько наборов с разными полями. Первый набор — имя товара, второй — сводка каталога. Нельзя считать чтение первого набора завершением всего ответа.

Путь данных: Несколько наборов: прочитать каждый и вернуть соединение в работу
  1. Фиксированные SELECT name и SELECT COUNT/SUM
  2. Rowset 1: name=Чай
  3. nextRowset → Rowset 2: count=3, units=6
  4. Дочитаны все наборы → closeCursor →SELECT 11

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

Часть ответаПолеЗначение
Набор 1nameЧай
Набор 2product_count3
Набор 2units6
Следующий отдельный запросready11

Короткая запись действия

do {
    if ($st->columnCount() > 0) { $rows = $st->fetchAll(PDO::FETCH_ASSOC); }
} while ($st->nextRowset());
$st->closeCursor();

Фрагмент предполагает уже созданные соединение и учебные таблицы; $st/$statement — результат нужной команды. Полная программа ниже создаёт исходные данные сама. Это выдержка для объяснения, а не отдельный дополнительный запуск.

Только этот комплект включает ATTR_MULTI_STATEMENTS=true и PDO::ATTR_EMULATE_PREPARES=true при создании соединения. Второй параметр позволяет query отправить несколько команд как текстовый запрос: при native-подготовке MySQL принимает одну команду, и два SELECT через точку с запятой дают ошибку 1064. SQL здесь — фиксированная строка из двух написанных автором SELECT, без пользовательской вставки. Первый rowset содержит name=Чай; второй — product_count=3 и units=6. nextRowset переключает набор, а не строку внутри текущего набора. Поэтому перед чтением проверяется columnCount и используется новый массив полей.

Цикл do/while обрабатывает первый набор до вызова nextRowset. Когда наборов больше нет, переход завершает цикл. finally закрывает курсор, после чего отдельный SELECT 11 подтверждает готовность соединения. При ошибке режим ERRMODE_EXCEPTION поднимет исключение, поэтому false не следует трактовать как универсальное доказательство отсутствия ошибок при любых настройках. Хранимые процедуры тоже могут возвращать несколько наборов, но данная программа не создаёт и не проверяет процедуру.

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

Обычные примеры пакета отключают multi statements. Не включайте их ради склеивания пользовательского SQL. Native prepare рассчитан на отдельную подготовленную команду; одного ATTR_MULTI_STATEMENTS=true недостаточно при выключенной эмуляции. Данный сценарий использует query с явно включённой эмуляцией для доверенного фиксированного текста. Для обычного запроса с входными параметрами сохраняйте отдельный prepare/execute и настройки, выбранные для него. Наборы могут иметь разные столбцы и включать ответы без строк; обработчик не должен читать одинаковые ключи вслепую.

Полная самостоятельная программа и файлы для запуска
Несколько наборов: прочитать каждый и вернуть соединение в работуСамостоятельный пример
<?php
declare(strict_types=1);
require __DIR__.'/pdo109-results-helper.php';

$details=[];
$db=pdo109Connect([Pdo\Mysql::ATTR_MULTI_STATEMENTS=>true, PDO::ATTR_EMULATE_PREPARES=>true]); $runtime=pdo109Runtime($db); pdo109Catalog($db);
$sql="SELECT name FROM pdo109_products WHERE id=1; SELECT COUNT(*) AS product_count,SUM(stock) AS units FROM pdo109_products";
$st=$db->query($sql); $sets=[];
try {
    do { if ($st->columnCount()>0) { $sets[]=$st->fetchAll(PDO::FETCH_ASSOC); } } while ($st->nextRowset());
} finally { $st->closeCursor(); }
$next=$db->query('SELECT 11 AS ready'); $ready=(int)$next->fetchColumn(); $next->closeCursor();
$rows=[['Набор 1','name',$sets[0][0]['name']],['Набор 2','product_count',(int)$sets[1][0]['product_count']],['Набор 2','units',(int)$sets[1][0]['units']],['Следующий отдельный запрос','ready',$ready]];
$checks=['two_rowsets'=>count($sets)===2,'different_shapes'=>array_keys($sets[0][0])===['name'] && array_keys($sets[1][0])===['product_count','units'],'correct_values'=>$sets[0][0]['name']==='Чай' && (int)$sets[1][0]['product_count']===3 && (int)$sets[1][0]['units']===6,'connection_ready'=>$ready===11];
$details=['trusted_fixed_sql_only'=>true,'procedure_execution_claimed'=>false]; $st=$next=null; $db=null;
pdo109Report('pdo109-next-rowsets', ['Часть ответа', 'Поле', 'Значение'], $rows, $checks, $runtime, $details);
echo 'next-rowsets: saved result.html and result.json
';
Ожидаемый результат
next-rowsets: saved result.html and result.json

Откройте сохранённый result.html: результат — конкретные строки таблицы выше. JSON содержит те же значения. Helper и README вложены; отдельные предыдущие примеры запускать не нужно. Проверяется PHP CLI с MySQL, не HTTP.

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

Попробуйте сами. Добавьте третьим фиксированным SELECT число товаров с stock=0. В обработчике различите форму третьего набора и сохраните его в отдельную часть отчёта.

Дальше: запись как единая операция

Когда результат читается предсказуемо, объедините несколько связанных записей в транзакцию PDO. Для предупреждений, SQLSTATE и влияния настроек используйте диагностику PDO_MYSQL. Сохраните рабочий отчёт рядом с входными данными: он объясняет, что должна сделать программа.

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

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

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

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

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