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

Дерево руководства
Глава 111 · После подключения PDO_MYSQL и параметров
PDO_MYSQL: строки результата, словари, потоковое чтение и курсоры
Найденный товар с остатком 0, выбор FETCH-формы, сохранение групп, освобождение unbuffered-ответа и чтение всех rowset.
Строка, значение и курсор — разные решения
Успешный SQL — только начало. Экрану нужно понять, найден ли товар, не затерялся ли нулевой остаток и в каком виде читать поля. Выгрузке нужно освободить соединение после чтения. На одной маленькой таблице разделим эти решения.
| Ответ / объект | Смысл | Что делать |
|---|---|---|
| execute() вернул true | SQL выполнен | Отдельно прочитать результат |
| fetch вернул массив | Одна строка найдена | Читать ключи, сохраняя 0/NULL/пустоту |
| fetch вернул false | Строк больше нет; при другом error mode также возможен отказ | В режиме исключений отличить завершение от PDOException |
| PDOStatement ещё содержит ответ | Соединение может быть занято | Дочитать или closeCursor |
| nextRowset вернул true | Есть следующий набор | Учитывать другой набор полей |
- ID 1 → [name:Чай,stock:0,note:NULL]
- ID 2 → [name:Кофе,stock:4,note:пустая строка]
- ID 99 → fetch:false
- Только последний случай означает: товар не найден
Предыдущая глава объясняет соединение и передачу параметров. Полный справочник методов сохраняется в карточках 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 — три разных результата
Карточка товара не должна сообщать «не найден», когда у товара просто закончился остаток. Нужен отдельный признак существования строки.
- SELECT WHERE id=1 → массив с stock=0
- SELECT WHERE id=2 → массив с note пустая строка
- SELECT WHERE id=99 → false
- Решение определяется наличием строки, а не истинностью остатка
Что получится
| Запрошен ID | Строка найдена | Товар | Остаток | Примечание |
|---|---|---|---|---|
| 1 | true | Чай | 0 | NULL |
| 2 | true | Кофе | 4 | (пустая строка) |
| 99 | false | NULL | NULL | NULL |
Короткая запись действия
$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 в ячейке не означает отсутствие строки.
Полная самостоятельная программа и файлы для запуска
<?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-результата осознанно на одних товарных данных.
- Один SELECT id,name WHERE id=2
- ASSOC → ключи id/name
- NUM → позиции 0/1; OBJ → свойство name
- COLUMN всего SELECT name → список трёх названий
Что получится
| Способ чтения | Ключи / обращение | Полезное значение |
|---|---|---|
| FETCH_ASSOC | id, name | Кофе |
| FETCH_NUM | 0, 1 | Кофе |
| FETCH_OBJ | stdClass → 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-массив; для большой выгрузки выбирайте ограниченную выборку или потоковое чтение.
Полная самостоятельная программа и файлы для запуска
<?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→название, а для меню разделов — категория→несколько товаров. Одинаковый режим чтения для этих задач может молча потерять товар.
- Строки: (1,Чай),(2,Кофе),(3,Мёд)
- KEY_PAIR по id → три уникальных ключа
- KEY_PAIR по категории → Кофе заменяет Чай
- GROUP|COLUMN → Напитки:[Чай,Кофе]
Что получится
| Режим / первый столбец | Ключ | Получено |
|---|---|---|
| KEY_PAIR: id | 2 | Кофе |
| 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.
Полная самостоятельная программа и файлы для запуска
<?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 должен содержать три названия. Объясните, почему это свойство структуры, а не случайный сбой.
Прервали чтение: освободить соединение перед следующим запросом
Из большой выборки взяли только первый товар и решили выполнить другой запрос на том же соединении. При отключённой буферизации непрочитанный ответ ещё занимает его.
- Unbuffered SELECT: 3 строки
- fetch → Чай, 2 строки остались
- Новый query → ожидаемый 2014
- 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 для накопления всех товаров не обязателен.
- fetch Чай:0×12000 →0
- fetch Кофе:4×30000 →120000
- fetch Мёд:2×25000 →50000
- Конец ответа → итог 170000, соединение свободно
Что получится
| Товар | Остаток | Цена в копейках | Стоимость остатка |
|---|---|---|---|
| Чай | 0 | 12000 | 0 |
| Кофе | 4 | 30000 | 120000 |
| Мёд | 2 | 25000 | 50000 |
| Итого | NULL | NULL | 170000 |
Короткая запись действия
$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, как показано в выборке нужной страницы.
Полная самостоятельная программа и файлы для запуска
<?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) и сравните смысл результата, не объявляя оба способа одинаковыми по расходам.
Несколько наборов: прочитать каждый и вернуть соединение в работу
Одна операция возвращает несколько наборов с разными полями. Первый набор — имя товара, второй — сводка каталога. Нельзя считать чтение первого набора завершением всего ответа.
- Фиксированные SELECT name и SELECT COUNT/SUM
- Rowset 1: name=Чай
- nextRowset → Rowset 2: count=3, units=6
- Дочитаны все наборы → closeCursor →SELECT 11
Что получится
| Часть ответа | Поле | Значение |
|---|---|---|
| Набор 1 | name | Чай |
| Набор 2 | product_count | 3 |
| Набор 2 | units | 6 |
| Следующий отдельный запрос | ready | 11 |
Короткая запись действия
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. Примеры и объяснения написаны для этого руководства; источники и обсуждения доступны в каждой главе.


