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

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

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

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

Глава 113 · После PDOStatement и первой транзакции

PDO_MYSQL: ошибки, предупреждения и настройки результата

errorInfo соединения и запроса, SQLSTATE, getWarningCount, FOUND_ROWS, точные DECIMAL-строки и проверяемые профили чтения.

Ошибка, отсутствие изменения и предупреждение — разные исходы

У запроса есть несколько независимых исходов: отказ SQL, успешная операция с нулём изменённых строк, успешное чтение с предупреждением и чтение с неожиданным типом PHP. Для рабочего ответа надо различать их, а не проверять одну переменную «пусто или нет».

СигналПримерГде смотреть
PDOExceptionНарушен UNIQUE или неизвестна колонкаSQLSTATE и код MySQL у исключения
false в SILENTexecute не смог добавить повторный SKUerrorInfo именно у statement
rowCount=0UPDATE присвоил прежнее количествоРежим changed/found и условие WHERE
SQL warningSELECT с делением на 0 вернул NULLgetWarningCount и SHOW WARNINGS до нового запроса
Строка 12.30Точная DECIMAL-ценаСхема SQL и тип PHP, не вид текста в HTML
Диагностика рядом с причиной
  1. Выполнить конкретную операцию на PDO или PDOStatement
  2. Сразу снять нужный код, warning или счётчик
  3. Сопоставить с договором данных и настройками драйвера
  4. Показать безопасный ответ; техническое наблюдение хранить отдельно

Технические поля — не текст сообщения пользователю. Ниже публичные таблицы показывают коды и заранее выбранные объяснения; пароли, полный DSN и стек исключений не включаются. Для диагностики самого сервера уже есть различие клиента, сервера и SQL-соединений. Здесь акцент на объектах и настройках PDO.

Окружение и самостоятельные файлы

Самостоятельные архивы этой пары глав рассчитаны на PHP 8.4+ с PDO_MYSQL/mysqlnd и локальный MySQL 8.4 с InnoDB. Используется PDO::connect и драйверный подкласс Pdo\Mysql. Для PHP 8.3 и старше сначала изучите различия конструктора в подключении PDO_MYSQL: getWarningCount и новые имена драйверных констант там недоступны. В PHP 8.5 старые PDO::MYSQL_ATTR_* объявлены устаревшими; здесь используются Pdo\Mysql::ATTR_*. Общие PDO::ATTR_* сохраняют прежние имена.

Настройте отдельную учебную базу и пять переменных окружения PHPMAN_MYSQL_HOST, PHPMAN_MYSQL_PORT, PHPMAN_MYSQL_DATABASE, PHPMAN_MYSQL_USER, PHPMAN_MYSQL_PASSWORD. В архиве находятся example.php, pdo109-transactions-helper.php и README.txt. Команда php example.php создаёт result.html, result.json, observations.json и metrics.json. HTML показывает именно сохранённые значения; JSON позволяет использовать их дальше. Пароль вводится локально и в архив не входит.

Взять код ошибки у того объекта, который выполнил запрос

execute вернул false, а проверка $db->errorInfo() показывает 00000. Это не обязательно противоречие: ошибка могла произойти на объекте подготовленного запроса, а читается состояние другого объекта.

Входные данные

Уникальный SKU

idsku
1TEA

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

Ошибка относится к своей операции

ОперацияУспешнаSQLSTATEКод драйвера
execute: повтор SKUfalse230001062
PDO::errorInfo после executeNULL00000NULL
query: неизвестное полеfalse42S221054

Сохранённые товары

idsku
1TEA
Как изменяются данные
  1. $stmt->execute → false → читать $stmt->errorInfo
  2. Другой объект $db не хранит общую историю всех statements
  3. Ошибка $db->query → читать $db->errorInfo сразу после неё

Сначала ключевой фрагмент

Это часть самостоятельной программы ниже: $db — уже открытое PDO-соединение, а таблицы и подготовленные запросы созданы в её начале. Фрагмент показывает текущий механизм; все файлы для запуска находятся в архиве.

$ok = $stmt->execute([2, 'TEA']);
if ($ok === false) {
    $statementInfo = $stmt->errorInfo();
}

В этом опыте ERRMODE_SILENT включён специально, чтобы увидеть возвращаемые значения. Подготовка INSERT успешна; ошибка возникает именно при execute: SKU TEA уже есть. Сразу снимаем $stmt->errorInfo(): индекс 0 содержит SQLSTATE 23000, индекс 1 — код MySQL 1062, индекс 2 — технический текст. PDO::errorInfo не является общим журналом ошибок всех statements и остаётся 00000. Затем отдельный ошибочный query вызывается прямо на $db: его 42S22/1054 читаются у соединения. После фиксации этих состояний включается обычный режим исключений и читается неизменённая таблица.

Подробные параметры и возвращаемые значения: PDO::errorInfo(), PDOStatement::errorInfo(), PDOStatement::execute().

Ошибки, границы и альтернативы

Нельзя ждать до следующего успешного запроса и рассчитывать на прежнюю диагностику.00000 означает отсутствие ошибки для соответствующей последней операции, а не существование нужной записи. ERRMODE_SILENT требует проверки каждого false; в новом коде удобнее явно задать ERRMODE_EXCEPTION. Сырые errorInfo[2], DSN, SQL и стек не отправляйте пользователю: ниже показано разделение технического кода и понятного сообщения. Подмена SQLSTATE своим текстом лишает журнал возможности группировать сбои.

Полная самостоятельная программа PHP с входными данными
Взять код ошибки у того объекта, который выполнил запросСамостоятельный пример
<?php
declare(strict_types=1);
require __DIR__.'/pdo109-transactions-helper.php';
$db=pdo109Connect(); $others=[]; $created=[]; $tables=[]; $checks=[]; $observations=[];
try {
pdo109Create($db,$created,'pdo109_diag_errors','id INT PRIMARY KEY, sku VARCHAR(20) NOT NULL UNIQUE');
$db->exec("INSERT INTO pdo109_diag_errors VALUES(1,'TEA')");$db->setAttribute(PDO::ATTR_ERRMODE,PDO::ERRMODE_SILENT);
$stmt=$db->prepare('INSERT INTO pdo109_diag_errors VALUES(?,?)');$ok=$stmt->execute([2,'TEA']);
$statementInfo=$stmt->errorInfo();$connectionInfo=$db->errorInfo();
$direct=$db->query('SELECT missing_column FROM pdo109_diag_errors');$directInfo=$db->errorInfo();
$db->setAttribute(PDO::ATTR_ERRMODE,PDO::ERRMODE_EXCEPTION);
$rows=pdo109Read($db,'SELECT id,sku FROM pdo109_diag_errors');
$tables=[pdo109Table('Ошибка относится к своей операции',['Операция','Успешна','SQLSTATE','Код драйвера'],[['execute: повтор SKU',$ok,$statementInfo[0],(string)$statementInfo[1]],['PDO::errorInfo после execute',null,$connectionInfo[0],null],['query: неизвестное поле',$direct!==false,$directInfo[0],(string)$directInfo[1]]]),pdo109Table('Сохранённые товары',['id','sku'],$rows)];
$checks=['execute_false'=>$ok===false,'statement_duplicate'=>$statementInfo[0]==='23000' && $statementInfo[1]===1062,'connection_not_statement'=>$connectionInfo[0]==='00000','direct_error'=>$direct===false && $directInfo[0]==='42S22' && $directInfo[1]===1054,'no_extra_insert'=>$rows===[['1','TEA']]];
    pdo109Report('pdo109-statement-error',$tables,$checks,$db,$observations);
} finally {
    pdo109Cleanup($db,$others,$created);
}
echo "pdo109-statement-error: saved result.html and result.json\n";
Ожидаемый результат
pdo109-statement-error: saved result.html and result.json

Архив включает example.php, PDO-helper и README. Создаётся своя учебная схема таблиц, выполняется показанная операция, затем результат читается и сохраняется в HTML/JSON. Короткая строка stdout сообщает о файлах; сами полезные значения показаны выше. Версии и инварианты записаны в metrics.json.

Открыть сохранённый результат запуска · Скачать таблицы JSON · Наблюдения запуска.

Небольшое продолжение

Измените второй SKU на COFFEE: execute станет true, появится вторая запись, а statement SQLSTATE будет 00000. Затем верните TEA и изучите только код 1062. Не исправляйте отказ INSERT дополнительным INSERT без понимания уникального ключа.

Практический вопрос: PDO not inserting — error code 00000. Разработчик видит false при выполнении INSERT, но диагностический массив показывает 00000. В учебном решении явно разделены состояния PDO-соединения и PDOStatement; сложная обёртка из обсуждения не копируется. Постановка задачи взята из обсуждения; решение и данные написаны отдельно, семантика сверена по документации PHP/MySQL.

Разделить конфликт данных и ошибку SQL без вывода внутренностей

Повтор SKU можно объяснить пользователю и предложить другое значение. Опечатку в SQL пользователь исправить не может. Одно сообщение «не сохранилось» скрывает эту разницу, а полный текст PDOException раскрывает внутренние детали.

Входные данные

Две разные причины отказа

ЗаявкаПричина
Сохранить SKU TEA повторноОграничение UNIQUE
SELECT missing_columnОпечатка разработчика

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

Сообщение пользователю и технические коды

ОперацияSQLSTATEКод MySQLСообщение
duplicate230001062Такой SKU уже существует
bad-column42S221054Операция недоступна: ошибка запроса

Соединение осталось пригодным

Строк в таблице
1
Как изменяются данные
  1. SQL-вызов → PDOException
  2. SQLSTATE + код MySQL → различить известные причины
  3. Понятный ответ пользователю; техническая запись отдельно; неизвестное исключение наружу

Сначала ключевой фрагмент

Это часть самостоятельной программы ниже: $db — уже открытое PDO-соединение, а таблицы и подготовленные запросы созданы в её начале. Фрагмент показывает текущий механизм; все файлы для запуска находятся в архиве.

try {
    $stmt->execute([2, 'TEA']);
} catch (PDOException $error) {
    $state = (string) $error->getCode();
    $driver = (int) ($error->errorInfo[1] ?? 0);
}

ERRMODE_EXCEPTION задан при подключении. После ожидаемых отказов catch(PDOException $error) получает SQLSTATE через getCode и специфичный код драйвера через errorInfo[1]. Сочетание 23000/1062 означает конфликт уникальности в данном сценарии;42S22/1054 — неизвестную колонку. Общего 23000 недостаточно для сообщения «дубликат»: другой код этого класса может означать внешнюю связь. В таблицу выводятся заранее заданные безопасные сообщения. Неизвестное сочетание не проглатывается, а выбрасывается дальше. Новая выборка подтверждает, что первоначальный товар остался один.

Подробные параметры и возвращаемые значения: PDO::errorInfo(), PDOStatement::errorInfo(), PDOStatement::execute().

Ошибки, границы и альтернативы

Нельзя классифицировать по локализованному getMessage: текст и имена объектов могут измениться. В PHP 8 режим исключений является стандартным, но явная настройка делает договор читаемым. Реальный deadlock в этой программе не создаётся:40001/1213 рассматривается отдельно как повод отменить и ограниченно повторить всю допустимую транзакцию. Потерянный ответ COMMIT — другая проблема: сервер мог успеть подтвердить запись, поэтому слепой повтор может дублировать операцию.

Полная самостоятельная программа PHP с входными данными
Разделить конфликт данных и ошибку SQL без вывода внутренностейСамостоятельный пример
<?php
declare(strict_types=1);
require __DIR__.'/pdo109-transactions-helper.php';
$db=pdo109Connect(); $others=[]; $created=[]; $tables=[]; $checks=[]; $observations=[];
try {
pdo109Create($db,$created,'pdo109_diag_exception','id INT PRIMARY KEY, sku VARCHAR(20) NOT NULL UNIQUE');
$db->exec("INSERT INTO pdo109_diag_exception VALUES(1,'TEA')");$rows=[];$observed=[];
foreach (['duplicate','bad-column'] as $action) {
    try {
        if ($action==='duplicate') { $stmt=$db->prepare('INSERT INTO pdo109_diag_exception VALUES(?,?)');$stmt->execute([2,'TEA']); }
        else { $db->query('SELECT missing_column FROM pdo109_diag_exception'); }
        throw new LogicException('The teaching SQL error did not occur');
    } catch (PDOException $error) {
        $state=(string)$error->getCode();$driver=(int)($error->errorInfo[1]??0);
        if ($state==='23000' && $driver===1062) { $public='Такой SKU уже существует'; }
        elseif ($state==='42S22' && $driver===1054) { $public='Операция недоступна: ошибка запроса'; }
        else { throw $error; }
        $observed[]=[$state,$driver];$rows[]=[$action,$state,(string)$driver,$public];
    }
}
$count=(string)$db->query('SELECT COUNT(*) FROM pdo109_diag_exception')->fetchColumn();
$tables=[pdo109Table('Сообщение пользователю и технические коды',['Операция','SQLSTATE','Код MySQL','Сообщение'],$rows),pdo109Table('Соединение осталось пригодным',['Строк в таблице'],[[$count]])];
$checks=['classified_actual_errors'=>$observed===[['23000',1062],['42S22',1054]],'no_data_loss'=>$count==='1','safe_messages'=>!str_contains(json_encode($tables,JSON_THROW_ON_ERROR),'missing_column')];
    pdo109Report('pdo109-exception-codes',$tables,$checks,$db,$observations);
} finally {
    pdo109Cleanup($db,$others,$created);
}
echo "pdo109-exception-codes: saved result.html and result.json\n";
Ожидаемый результат
pdo109-exception-codes: saved result.html and result.json

Архив включает example.php, PDO-helper и README. Создаётся своя учебная схема таблиц, выполняется показанная операция, затем результат читается и сохраняется в HTML/JSON. Короткая строка stdout сообщает о файлах; сами полезные значения показаны выше. Версии и инварианты записаны в metrics.json.

Открыть сохранённый результат запуска · Скачать таблицы JSON · Наблюдения запуска.

Небольшое продолжение

Добавьте своё понятное сообщение для действительно ожидаемого ограничения, сохранив проверку обоих кодов. Не ловите все Throwable в обработчике «SKU занят»: логическая ошибка PHP и отказ базы — разные причины. Для повторной бизнес-заявки сохраняйте ключ операции вместе с её результатом.

Увидеть SQL-предупреждение при формально успешном SELECT

Отчёт о средней цене выполнился без исключения, но у одной позиции результат NULL. Нужно увидеть предупреждение MySQL и выбрать осмысленную формулу, а не подменить отсутствие цены нулём.

Входные данные

Данные отчёта

skurevenueunits
TEA10002
ZERO10000

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

Исходный SELECT

skuunit_price
TEA500.0000
ZERONULL

SHOW WARNINGS: устойчивые поля

LevelCode
Warning1365

Явное отсутствие цены

skuunit_price через NULLIF
TEA500.0000
ZERONULL

Счётчики предупреждений

Деление на 0После NULLIF
10
Как изменяются данные
  1. TEA1000/2 →500.0000; ZERO1000/0 →NULL + warning1365
  2. Полностью прочитать результат → getWarningCount → SHOW WARNINGS
  3. NULLIF(units,0) → явный NULL без деления на 0

Сначала ключевой фрагмент

Это часть самостоятельной программы ниже: $db — уже открытое PDO-соединение, а таблицы и подготовленные запросы созданы в её начале. Фрагмент показывает текущий механизм; все файлы для запуска находятся в архиве.

$db->setAttribute(PDO::ATTR_EMULATE_PREPARES, true);
$stmt = $db->query('SELECT sku,revenue/units AS unit_price FROM pdo109_diag_ratios ORDER BY sku');
$raw = $stmt->fetchAll(PDO::FETCH_ASSOC);
$stmt->closeCursor();
$warningCount = $db->getWarningCount();
$warnings = $db->query('SHOW WARNINGS')->fetchAll();

Для TEA выручка 1000 делится на 2 и даёт 500.0000; у ZERO количество 0. Учебная SQL-настройка явно включает ERROR_FOR_DIVISION_BY_ZERO, поэтому успешный SELECT сопровождается предупреждением 1365. После полного чтения результата getWarningCount возвращает 1. SHOW WARNINGS читается до другого обычного запроса. В этом самостоятельном примере заранее включён ATTR_EMULATE_PREPARES=true: SQL здесь фиксирован, пользовательские строки не склеиваются. SHOW WARNINGS не поддерживает нативную подготовку; при режиме false PDO_MYSQL сначала получает ошибку 1295 и затем переходит к прямому выполнению. Такая попытка может заменить исходное предупреждение 1365 диагностикой самой подготовки. Поэтому одного правила «прочитать SHOW WARNINGS сразу» недостаточно: важно не испортить диагностику способом отправки запроса. Этот локальный выбор режима не требует менять настройки остальных примеров или отказываться от привязки параметров. Затем выражение revenue/NULLIF(units,0) явно превращает нулевой делитель в NULL: значения отчёта сохраняются, но предупреждений нет. NULLIF означает «верни NULL, если два аргумента равны; иначе первый». Неизвестная/неопределённая цена остаётся NULL.

Подробные параметры и возвращаемые значения: Pdo\Mysql::getWarningCount(), PDO::query(), PDOStatement::closeCursor().

Ошибки, границы и альтернативы

Pdo\Mysql::getWarningCount существует с PHP 8.4 и не является методом старого базового объекта new PDO. В архиве PDO::connect возвращает драйверный подкласс. Серверное SQL-предупреждение отличается от PHP E_WARNING и от PDO::ERRMODE_WARNING. ERRMODE_EXCEPTION не обещает превратить каждый SQL warning в исключение. Счётчик относится к последнему запросу; диагностический SELECT версии, логгер с SQL или другой запрос могут изменить интересующее состояние. Для INSERT/UPDATE и других sql_mode реакция на деление/усечение может быть другой.

Полная самостоятельная программа PHP с входными данными
Увидеть SQL-предупреждение при формально успешном SELECTСамостоятельный пример
<?php
declare(strict_types=1);
require __DIR__.'/pdo109-transactions-helper.php';
$db=pdo109Connect(); $others=[]; $created=[]; $tables=[]; $checks=[]; $observations=[];
try {
// Фиксированный диагностический SQL: SHOW WARNINGS не поддерживает native prepare.
$db->setAttribute(PDO::ATTR_EMULATE_PREPARES,true);
$db->exec("SET SESSION sql_mode='STRICT_TRANS_TABLES,ERROR_FOR_DIVISION_BY_ZERO,NO_ENGINE_SUBSTITUTION'");
$db->exec('SET SESSION div_precision_increment=4');
pdo109Create($db,$created,'pdo109_diag_ratios','sku VARCHAR(20) PRIMARY KEY, revenue INT NOT NULL, units INT NOT NULL');
$db->exec("INSERT INTO pdo109_diag_ratios VALUES('TEA',1000,2),('ZERO',1000,0)");
$stmt=$db->query('SELECT sku,revenue/units AS unit_price FROM pdo109_diag_ratios ORDER BY sku');
$raw=$stmt->fetchAll(PDO::FETCH_ASSOC);$stmt->closeCursor();$warningCount=$db->getWarningCount();
$warnings=$db->query('SHOW WARNINGS')->fetchAll(PDO::FETCH_NUM);$warningRows=[];
foreach($warnings as $warning){$warningRows[]=[(string)$warning[0],(string)$warning[1]];}
$rows=[];foreach($raw as $row){$rows[]=[(string)$row['sku'],$row['unit_price']===null?null:(string)$row['unit_price']];}
$safe=pdo109Read($db,'SELECT sku,revenue/NULLIF(units,0) AS unit_price FROM pdo109_diag_ratios ORDER BY sku');$safeWarnings=$db->getWarningCount();
$tables=[pdo109Table('Исходный SELECT',['sku','unit_price'],$rows),pdo109Table('SHOW WARNINGS: устойчивые поля',['Level','Code'],$warningRows),pdo109Table('Явное отсутствие цены',['sku','unit_price через NULLIF'],$safe),pdo109Table('Счётчики предупреждений',['Деление на 0','После NULLIF'],[[(string)$warningCount,(string)$safeWarnings]])];
$checks=['result_preserved'=>$rows===[['TEA','500.0000'],['ZERO',null]],'warning_1365'=>$warningCount===1 && $warningRows===[['Warning','1365']],'nullif_explicit'=>$safe===$rows,'no_safe_warning'=>$safeWarnings===0];
$observations=[['SQLSTATE успешного SELECT',$db->errorCode()]];
    pdo109Report('pdo109-warning-count',$tables,$checks,$db,$observations);
} finally {
    pdo109Cleanup($db,$others,$created);
}
echo "pdo109-warning-count: saved result.html and result.json\n";
Ожидаемый результат
pdo109-warning-count: saved result.html and result.json

Архив включает example.php, PDO-helper и README. Создаётся своя учебная схема таблиц, выполняется показанная операция, затем результат читается и сохраняется в HTML/JSON. Короткая строка stdout сообщает о файлах; сами полезные значения показаны выше. Версии и инварианты записаны в metrics.json.

Открыть сохранённый результат запуска · Скачать таблицы JSON · Наблюдения запуска.

Небольшое продолжение

Поставьте units=4 у ZERO: обе цены определятся, getWarningCount должен стать 0. Для экранной подписи NULL выберите «нет данных», а не «0 руб.». В рабочем отчёте обоснуйте политику для нулевого количества и отрицательных возвратов отдельно.

Выбрать: считать изменённые или совпавшие строки UPDATE

Форма отправила прежнее количество 5. UPDATE успешно выполнен, но rowCount равен 0. Чтобы отличать принятую строку от отсутствующего id, нужно знать, какой именно счётчик настроен при подключении.

Входные данные

Два независимых учебных набора

idqtyUPDATE
155→5;5→7;попытка id99

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

Одна последовательность, две настройки подключения

Режим5→55→7id99Итог qty
изменённые0107
совпавшие1107
Как изменяются данные
  1. Одна строка id1/qty5 → UPDATE прежним значением
  2. changed mode →0; matched mode →1
  3. UPDATE отсутствующего id99 →0 в обоих режимах

Сначала ключевой фрагмент

Это часть самостоятельной программы ниже: $db — уже открытое PDO-соединение, а таблицы и подготовленные запросы созданы в её начале. Фрагмент показывает текущий механизм; все файлы для запуска находятся в архиве.

$connection = pdo109Connect([Pdo\Mysql::ATTR_FOUND_ROWS => true]);
$stmt = $connection->prepare('UPDATE pdo109_diag_found SET qty=? WHERE id=?');
$stmt->execute([5, 1]);
$matched = $stmt->rowCount(); // 1, хотя qty уже был равен5

Оба соединения получают одинаковую собственную временную таблицу. Без ATTR_FOUND_ROWS последовательность 5→5,5→7,id99 даёт 0,1,0; с этой настройкой —1,1,0. Конечные данные одинаковы: qty=7. Настройка передаётся конструктору через pdo109Connect; изменить этот режим постфактум обычным setAttribute нельзя. rowCount берётся у подготовленного запроса сразу после execute. Название Pdo\Mysql::ATTR_FOUND_ROWS обозначает клиентский флаг подсчёта совпавших строк UPDATE и не требует отдельного SQL FOUND_ROWS().

Подробные параметры и возвращаемые значения: PDOStatement::rowCount(), PDOStatement::execute(), PDO::connect().

Ошибки, границы и альтернативы

В режиме совпавших строк 1 не доказывает, что данные изменились. В обычном режиме 0 не доказывает, что строки нет. Этот счётчик не является ошибкой SQL: успех execute и число строк имеют разный смысл. Для SELECT не пишите переносимую проверку существования через rowCount: используйте fetch либо отдельный COUNT с тем же условием. Алгоритм «UPDATE0→INSERT» без UNIQUE/атомарного договора остаётся гонкой. Настройка также не заменяет версию записи при конкурентном редактировании.

Полная самостоятельная программа PHP с входными данными
Выбрать: считать изменённые или совпавшие строки UPDATEСамостоятельный пример
<?php
declare(strict_types=1);
require __DIR__.'/pdo109-transactions-helper.php';
$db=pdo109Connect(); $others=[]; $created=[]; $tables=[]; $checks=[]; $observations=[];
try {
$rows=[];$snapshots=[];
foreach ([false,true] as $found) {
    $connection=pdo109Connect([Pdo\Mysql::ATTR_FOUND_ROWS=>$found]);$others[]=$connection;
    $connection->exec('CREATE TEMPORARY TABLE pdo109_diag_found (id INT PRIMARY KEY, qty INT NOT NULL) ENGINE=InnoDB');
    $connection->exec('INSERT INTO pdo109_diag_found VALUES(1,5)');
    $stmt=$connection->prepare('UPDATE pdo109_diag_found SET qty=? WHERE id=?');
    $counts=[];foreach([[5,1],[7,1],[7,99]] as $values){$stmt->execute($values);$counts[]=$stmt->rowCount();}
    $final=(int)$connection->query('SELECT qty FROM pdo109_diag_found WHERE id=1')->fetchColumn();$snapshots[]=[$counts,$final];
    $rows[]=[$found?'совпавшие':'изменённые',(string)$counts[0],(string)$counts[1],(string)$counts[2],(string)$final];
    $connection->exec('DROP TEMPORARY TABLE pdo109_diag_found');
}
$tables=[pdo109Table('Одна последовательность, две настройки подключения',['Режим','5→5','5→7','id99','Итог qty'],$rows)];
$checks=['changed_counts'=>$snapshots[0]===[[0,1,0],7],'found_counts'=>$snapshots[1]===[[1,1,0],7],'same_data'=>$snapshots[0][1]===$snapshots[1][1]];
    pdo109Report('pdo109-found-rows',$tables,$checks,$db,$observations);
} finally {
    pdo109Cleanup($db,$others,$created);
}
echo "pdo109-found-rows: saved result.html and result.json\n";
Ожидаемый результат
pdo109-found-rows: saved result.html and result.json

Архив включает example.php, PDO-helper и README. Создаётся своя учебная схема таблиц, выполняется показанная операция, затем результат читается и сохраняется в HTML/JSON. Короткая строка stdout сообщает о файлах; сами полезные значения показаны выше. Версии и инварианты записаны в metrics.json.

Открыть сохранённый результат запуска · Скачать таблицы JSON · Наблюдения запуска.

Небольшое продолжение

Добавьте условие qty=5 в WHERE: после смены на 7 повтор перестанет находить строку даже при FOUND_ROWS. Объясните, означает ли это отсутствие товара или устаревшее условие. Для редактора используйте явный id+version и увеличение version.

Практический вопрос: PHP PDO finding found rows of an UPDATE. UPDATE прежними значениями возвращает 0, а автору надо знать, нашлась ли строка. Сценарий показывает обе constructor-настройки и отдельно предупреждает о гонке UPDATE0→INSERT. Постановка задачи взята из обсуждения; решение и данные написаны отдельно, семантика сверена по документации PHP/MySQL.

Сохранить точную цену и понять типы после чтения

API должен вернуть количество числом, точную денежную цену без округления и отсутствие заметки как null. После чтения из PDO эти значения могут иметь разные PHP-типы, даже если в HTML они выглядят одинаково.

Входные данные

Схема и запись

INT idDOUBLE ratioDECIMAL(8,2) pricenullable note
71.512.30NULL

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

Значение и тип PHP в native prepared result

НастройкаПолеЗначениеТип PHP
stringify=falseid7int
stringify=falseratio1.5float
stringify=falseprice12.30string
stringify=falsenoteNULLnull
stringify=trueid7string
stringify=trueratio1.5string
stringify=trueprice12.30string
stringify=truenoteNULLnull
Как изменяются данные
  1. SQL INT/DOUBLE/DECIMAL/NULL → native PDO fetch
  2. stringify=false → int/float/string/null
  3. stringify=true → string/string/string/null

Сначала ключевой фрагмент

Это часть самостоятельной программы ниже: $db — уже открытое PDO-соединение, а таблицы и подготовленные запросы созданы в её начале. Фрагмент показывает текущий механизм; все файлы для запуска находятся в архиве.

$db->setAttribute(PDO::ATTR_STRINGIFY_FETCHES, false);
$stmt->execute([7]);
$row = $stmt->fetch(PDO::FETCH_ASSOC);
// У прочитанного массива на указанном стенде:
// $row['id'] — int; $row['price'] — точная строка '12.30'.

Таблица содержит INT7, DOUBLE1.5, DECIMAL12.30 и SQL NULL. Чтение идёт через native prepared statement с mysqlnd. При stringify=false на этом стенде первые два значения имеют типы int и float, DECIMAL сохраняется строкой 12.30, NULL остаётся null. Включение ATTR_STRINGIFY_FETCHES превращает значения, отличные от NULL (включая 0), в строки, но не превращает NULL в пустоту. get_debug_type в отчёте показывает тип, отдельный столбец — представление значения. Вспомогательный HTML-рендер не используется как доказательство типа; проверки выполняются на исходном PHP-массиве.

Подробные параметры и возвращаемые значения: PDO::setAttribute(), PDOStatement::execute(), PDOStatement::fetch().

Ошибки, границы и альтернативы

stringify=false не значит «всё станет числами»: результат зависит от SQL-типа, драйвера, протокола и диапазона PHP int. Большой BIGINT может не помещаться в int. DECIMAL не надо принудительно превращать во float ради красивого JSON: можно потерять точность. Для денежных вычислений выберите целые минимальные единицы либо точную десятичную арифметику. Общий PDO API не гарантирует такие же типы у другого драйвера. Здесь указаны точные типы и mysqlnd-окружение.

Полная самостоятельная программа PHP с входными данными
Сохранить точную цену и понять типы после чтенияСамостоятельный пример
<?php
declare(strict_types=1);
require __DIR__.'/pdo109-transactions-helper.php';
$db=pdo109Connect(); $others=[]; $created=[]; $tables=[]; $checks=[]; $observations=[];
try {
pdo109Create($db,$created,'pdo109_diag_types','id INT PRIMARY KEY, ratio DOUBLE NOT NULL, price DECIMAL(8,2) NOT NULL, note VARCHAR(20) NULL');
$db->exec('INSERT INTO pdo109_diag_types VALUES(7,1.5,12.30,NULL)');$rows=[];$samples=[];
foreach([false,true] as $stringify){
    $db->setAttribute(PDO::ATTR_STRINGIFY_FETCHES,$stringify);
    $stmt=$db->prepare('SELECT id,ratio,price,note FROM pdo109_diag_types WHERE id=?');$stmt->execute([7]);$row=$stmt->fetch(PDO::FETCH_ASSOC);$stmt->closeCursor();$samples[]=$row;
    foreach($row as $name=>$value){$rows[]=[$stringify?'stringify=true':'stringify=false',$name,$value===null?null:(string)$value,get_debug_type($value)];}
}
$tables=[pdo109Table('Значение и тип PHP в native prepared result',['Настройка','Поле','Значение','Тип PHP'],$rows)];
$checks=['native_numeric'=>is_int($samples[0]['id']) && is_float($samples[0]['ratio']),'decimal_exact'=>$samples[0]['price']==='12.30' && $samples[1]['price']==='12.30','stringified'=>is_string($samples[1]['id']) && is_string($samples[1]['ratio']),'null_preserved'=>$samples[0]['note']===null && $samples[1]['note']===null];
$db->setAttribute(PDO::ATTR_STRINGIFY_FETCHES,false);
    pdo109Report('pdo109-fetch-types',$tables,$checks,$db,$observations);
} finally {
    pdo109Cleanup($db,$others,$created);
}
echo "pdo109-fetch-types: saved result.html and result.json\n";
Ожидаемый результат
pdo109-fetch-types: saved result.html and result.json

Архив включает example.php, PDO-helper и README. Создаётся своя учебная схема таблиц, выполняется показанная операция, затем результат читается и сохраняется в HTML/JSON. Короткая строка stdout сообщает о файлах; сами полезные значения показаны выше. Версии и инварианты записаны в metrics.json.

Открыть сохранённый результат запуска · Скачать таблицы JSON · Наблюдения запуска.

Небольшое продолжение

Сериализуйте исходный $row в JSON с JSON_THROW_ON_ERROR: при stringify=false id будет числом, price — строкой, note —null. Не добавляйте JSON_NUMERIC_CHECK ко всем строкам: артикул 0012 тоже может ошибочно стать числом. Преобразования делайте по схеме конкретного ответа.

Задать понятный профиль чтения и проверить его настройки

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

Входные данные

Задача

screenexport
Одна небольшая страница данныхПоследовательное чтение для выгрузки

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

Настройки прочитаны у реальных соединений

ПрофильБуферЭмуляцияstringifyТип idЦена
screentruefalsefalseint12.30
exportfalsefalsetruestring12.30

Неизвестный профиль

Отклонён до подключения
true
Как изменяются данные
  1. Имя профиля → разрешённый массив опций
  2. PDO::connect → getAttribute фактических настроек
  3. SELECT → тип id/цена → closeCursor; неизвестное имя отклонено

Сначала ключевой фрагмент

Это часть самостоятельной программы ниже: $db — уже открытое PDO-соединение, а таблицы и подготовленные запросы созданы в её начале. Фрагмент показывает текущий механизм; все файлы для запуска находятся в архиве.

$connection = pdo109Connect(pdo109Profile('export'));
$buffered = $connection->getAttribute(Pdo\Mysql::ATTR_USE_BUFFERED_QUERY);
$emulated = $connection->getAttribute(PDO::ATTR_EMULATE_PREPARES);

pdo109Profile принимает только screen или export. screen включает буфер и сохраняет числовые PHP-типы; export выключает буфер и выбирает строковые значения. Оба используют native prepares. После подключения getAttribute читает эффективные значения, а реальный prepared SELECT показывает тип id и точную строку цены. Неизвестное имя выбрасывает InvalidArgumentException до создания соединения. Один короткий результат здесь нужен для диагностики договора, а полный цикл потоковой выгрузки разобран в предыдущей главе. closeCursor завершает чтение перед следующей операцией.

Подробные параметры и возвращаемые значения: PDO::connect(), PDO::getAttribute(), PDO::setAttribute(), PDOStatement::closeCursor().

Ошибки, границы и альтернативы

Экспорт не обязан всегда возвращать строки: это осознанный договор конкретного потребителя. Имена профилей не дают пользователю права выбирать DSN, SQL или произвольный PDO-атрибут. Поддержка getAttribute зависит от драйвера; чтение ATTR_STRINGIFY_FETCHES добавлено в PHP 8.4. Буфер влияет на получение результата, а эмуляция — на обработку prepared statement; это независимые переключатели. Утверждение «небуферизованный всегда быстрее» из этого опыта не следует. Для маленького экрана сначала ограничьте SELECT, не выгружайте всю таблицу.

Полная самостоятельная программа PHP с входными данными
Задать понятный профиль чтения и проверить его настройкиСамостоятельный пример
<?php
declare(strict_types=1);
require __DIR__.'/pdo109-transactions-helper.php';
$db=pdo109Connect(); $others=[]; $created=[]; $tables=[]; $checks=[]; $observations=[];
try {
function pdo109Profile(string $name): array {
    return match($name) {
        'screen'=>[Pdo\Mysql::ATTR_USE_BUFFERED_QUERY=>true,PDO::ATTR_STRINGIFY_FETCHES=>false],
        'export'=>[Pdo\Mysql::ATTR_USE_BUFFERED_QUERY=>false,PDO::ATTR_STRINGIFY_FETCHES=>true],
        default=>throw new InvalidArgumentException('Unknown read profile'),
    };
}
$rows=[];$valid=true;
foreach(['screen','export'] as $name){
    $connection=pdo109Connect(pdo109Profile($name));$others[]=$connection;
    $buffered=(bool)$connection->getAttribute(Pdo\Mysql::ATTR_USE_BUFFERED_QUERY);
    $emulated=(bool)$connection->getAttribute(PDO::ATTR_EMULATE_PREPARES);
    $stringify=(bool)$connection->getAttribute(PDO::ATTR_STRINGIFY_FETCHES);
    $stmt=$connection->prepare('SELECT 7 AS id, CAST(12.30 AS DECIMAL(8,2)) AS price');$stmt->execute();$value=$stmt->fetch(PDO::FETCH_ASSOC);$stmt->closeCursor();
    $rows[]=[$name,$buffered,$emulated,$stringify,get_debug_type($value['id']),$value['price']];
    $valid=$valid && $buffered===($name==='screen') && !$emulated && $stringify===($name==='export') && $value['price']==='12.30';
}
$rejected=false;try{pdo109Profile('from-query-string');}catch(InvalidArgumentException $error){$rejected=true;}
$tables=[pdo109Table('Настройки прочитаны у реальных соединений',['Профиль','Буфер','Эмуляция','stringify','Тип id','Цена'],$rows),pdo109Table('Неизвестный профиль',['Отклонён до подключения'],[[$rejected]])];
$checks=['effective_options'=>$valid,'screen_int'=>$rows[0][4]==='int','export_string'=>$rows[1][4]==='string','unknown_rejected'=>$rejected];
    pdo109Report('pdo109-option-profiles',$tables,$checks,$db,$observations);
} finally {
    pdo109Cleanup($db,$others,$created);
}
echo "pdo109-option-profiles: saved result.html and result.json\n";
Ожидаемый результат
pdo109-option-profiles: saved result.html and result.json

Архив включает example.php, PDO-helper и README. Создаётся своя учебная схема таблиц, выполняется показанная операция, затем результат читается и сохраняется в HTML/JSON. Короткая строка stdout сообщает о файлах; сами полезные значения показаны выше. Версии и инварианты записаны в metrics.json.

Открыть сохранённый результат запуска · Скачать таблицы JSON · Наблюдения запуска.

Небольшое продолжение

Добавьте профиль screen-text с буфером true и stringify=true. Ожидайте string для id при той же цене 12.30. Не добавляйте десятки опций без конкретной задачи: каждой должно соответствовать наблюдаемое поведение и совместимость с используемым драйвером.

От диагностики к понятному ответу сервиса

Проверьте в договоре API: что является числом, что точной строкой, где NULL допустим и какое сообщение получает человек. Сохраните корреляционный номер ошибки отдельно от SQL. Для выборки нужного экрана используйте WHERE, ORDER BY, LIMIT и индекс, для последующей выгрузки — жизненный цикл результата PDO. Драйверные опции выбираются под задачу, а не ради количества настроек.

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

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

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

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

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