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

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

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

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

Глава 110 · После первой таблицы и SELECT

PDO_MYSQL: подключение, кодировка и точная передача параметров

От трёх товаров в HTML до PDO::connect, UTF-8, native/emulated prepare, bindValue/bindParam и различия NULL/пустоты/нуля.

PDO, драйвер и сервер: кто что делает

Вы уже можете описать таблицу и написать SELECT, но приложение использует PDO. Эта глава доводит путь до результата: три товара из MySQL становятся строками HTML, эмодзи сохраняется без потерь, параметры выбирают именно нужный товар. Общий синтаксис PDO не делает драйверы и SQL разных серверов одинаковыми.

Три разных объекта на пути каталога
  1. MySQL: таблица pdo109_products с 3 товарами
  2. PDO: $db — соединение с этим сервером
  3. PDOStatement: $statement — ответ конкретного SELECT
  4. array: $product — одна строка с ключами id/name/stock
  5. HTML: Чай: 0, Кофе: 4, Мёд: 2
ЗаписьЧто означает
PDOPHP-интерфейс подключения и выполнения запросов
pdo_mysqlРасширение-драйвер, связывающее PDO с MySQL
MySQL-серверОтдельный процесс, хранит базу и выполняет SQL
База / таблицаВыбранная схема и набор строк внутри неё
$dsnДоверенная строка адреса и настроек соединения
$optionsМассив правил ошибок, параметров и чтения

В записи mysql:host=127.0.0.1;port=33070;dbname=phpmanual_training;charset=utf8mb4 префикс mysql выбирает драйвер, host/port — сервер, dbname — существующую базу, charset — обмен текстом. Имя пользователя и пароль передаются отдельными аргументами. На Unix localhost может выбрать Unix socket; 127.0.0.1 задаёт TCP. unix_socket используют вместо host/port. Эти значения берутся из доверенной конфигурации, не из поля формы.

Перед первым запросом зададим понятные правила: PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION превращает ошибку SQL в исключение; PDO::ATTR_DEFAULT_FETCH_MODE => PDO::FETCH_ASSOC выбирает именованные ключи; PDO::ATTR_EMULATE_PREPARES => false выбирает native-подготовку для показанных команд. Специфические константы драйвера в PHP 8.4+ доступны через Pdo\Mysql; старые PDO::MYSQL_ATTR_* помечены deprecated в PHP 8.5. Helper хранит эти настройки вместе, но не является обязательной частью четырёхстрочного вызова PDO.

Если ещё незнакомы таблица, поле, SELECT или перебор строк, начните с первой базы и HTML-каталога и модели таблиц и связей. Здесь сохраняем этот смысл и разбираем договор именно PDO_MYSQL.

Подготовить свой запуск и открыть отчёт

Полные архивы рассчитаны на 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, а не действующая форма сайта.

Первый каталог: соединение PDO, запрос и строки

Нужен список товаров в PHP: название и остаток для каждой строки каталога. MySQL хранит данные, а PDO передаёт SQL серверу и позволяет прочитать его ответ.

Путь данных: Первый каталог: соединение PDO, запрос и строки
  1. Временная таблица: Чай: 0, Кофе: 4, Мёд: 2
  2. query → SELECT id,name,stock ORDER BY id
  3. fetch → по одной строке с именованными ключами
  4. HTML: три товарные строки, включая Чай: 0

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

IDТоварОстаток
1Чай0
2Кофе4
3Мёд2

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

$db = new PDO($dsn, $user, $password, $options);
$statement = $db->query('SELECT id,name,stock FROM pdo109_products ORDER BY id');
$product = $statement->fetch(PDO::FETCH_ASSOC);
echo $product['name']; // Чай

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

В программе CREATE TEMPORARY TABLE создаёт учебную таблицу, INSERT заполняет три строки. Сам экран строится запросом SELECT id,name,stock: выбираем нужные поля, а ORDER BY id задаёт устойчивый порядок. query возвращает PDOStatement, а не массив всех товаров. Каждый fetch(PDO::FETCH_ASSOC) возвращает следующую строку с ключами id, name, stock. Когда строки кончились, получается false.

Переменная $db содержит объект соединения. $statement содержит результат конкретной команды; $product — одну строку данных. Выражение $product['name'] читает значение под ключом name. Оператор -> вызывает метод объекта. Цикл while читает товар, проверяет !== false и добавляет данные в таблицу отчёта. Чай с нулевым остатком остаётся в результате: этот запрос его не отфильтровывает. HTML-отчёт экранирует каждое значение, поэтому текст товара не становится разметкой.

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

Успешное создание объекта PDO не означает, что таблица уже есть. CREATE/INSERT в примере нужны для воспроизводимого учебного запуска; в рабочем сайте схему и начальные данные обычно готовят отдельной миграцией. query подходит здесь потому, что SQL полностью фиксирован. Значение из формы передавайте через prepare/execute.

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

$details=[];
$db=pdo109Connect([], 'generic'); $runtime=pdo109Runtime($db); pdo109Catalog($db);
$statement=$db->query('SELECT id,name,stock FROM pdo109_products ORDER BY id');
$rows=[];
while (($product=$statement->fetch(PDO::FETCH_ASSOC)) !== false) {
    $rows[]=[(int)$product['id'],$product['name'],(int)$product['stock']];
}
$statement->closeCursor();
$checks=['three_products'=>$rows===[[1,'Чай',0],[2,'Кофе',4],[3,'Мёд',2]],'pdo_mysql_driver'=>$db->getAttribute(PDO::ATTR_DRIVER_NAME)==='mysql'];
$details=['connection_class'=>get_class($db),'selected_schema'=>$db->query('SELECT DATABASE()')->fetchColumn()];
$statement=null; $db=null;
pdo109Report('pdo109-first-catalog', ['ID', 'Товар', 'Остаток'], $rows, $checks, $runtime, $details);
echo 'first-catalog: saved result.html and result.json
';
Ожидаемый результат
first-catalog: saved result.html and result.json

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

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

Попробуйте сами. Добавьте в фиксированный SELECT условие stock > 0. В отчёте должны остаться Кофе и Мёд. Сначала предскажите строки, затем сравните результат.

Выбрать new PDO, PDO::connect или Pdo\Mysql

В новом проекте нужен общий интерфейс PDO, а в диагностике MySQL — дополнительный метод драйвера. Проверим, какой объект действительно создают три записи подключения.

Путь данных: Выбрать new PDO, PDO::connect или Pdo\Mysql
  1. Одинаковый доверенный mysql: DSN
  2. new PDO → PDO
  3. PDO::connect → Pdo\Mysql; new Pdo\Mysql → Pdo\Mysql
  4. Общий SELECT → Каталог во всех случаях

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

СозданиеФактический классЭто PDOЭто Pdo\MysqlЗначение SELECT
genericPDOtruefalseКаталог
factoryPdo\MysqltruetrueКаталог
typedPdo\MysqltruetrueКаталог

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

$db = PDO::connect($dsn, $user, $password, $options);
echo get_class($db); // Pdo\Mysql для mysql: DSN
$mysql = new Pdo\Mysql($dsn, $user, $password, $options);

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

new PDO создаёт базовый PDO. Начиная с PHP 8.4 статический PDO::connect выбирает специализированный подкласс по DSN, если драйвер его поддерживает. Для mysql: это Pdo\Mysql. Прямой new Pdo\Mysql сразу указывает нужный драйвер. Все три объекта предоставляют общие методы PDO и читают одно значение Каталог. Это три самостоятельных подключения, не три названия одного уже открытого соединения.

Вызов get_class показывает фактический класс. Проверка instanceof PDO истинна и для Pdo\Mysql, потому что он наследует PDO. Проверка instanceof Pdo\Mysql ложна у обычного new PDO. Имя класса не меняет SQL MySQL на универсальный язык для всех серверов. Специализированный getWarningCount разобран в диагностике предупреждений.

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

PDO::connect и классы Pdo\... требуют PHP 8.4+. Для прежних поддерживаемых версий применялся new PDO с тем же mysql: DSN, но без новых методов подкласса. Полные архивы этого пакета используют PHP 8.4+ и современные константы Pdo\Mysql::ATTR_*. Это не механизм автопереподключения и не пул соединений.

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

$details=[];
$rows=[]; $classes=[];
foreach (['generic','factory','typed'] as $construction) {
    $db=pdo109Connect([], $construction); $runtime=pdo109Runtime($db);
    $st=$db->query("SELECT 'Каталог' AS screen"); $screen=$st->fetchColumn(); $st->closeCursor();
    $class=get_class($db); $classes[]=$class;
    $rows[]=[$construction,$class,$db instanceof PDO,$db instanceof Pdo\Mysql,$screen];
    $st=null; $db=null;
}
$checks=['three_classes'=>$classes===['PDO','Pdo\\Mysql','Pdo\\Mysql'],'same_screen'=>array_column($rows,4)===['Каталог','Каталог','Каталог']];
pdo109Report('pdo109-connection-factory', ['Создание', 'Фактический класс', 'Это PDO', 'Это Pdo\\Mysql', 'Значение SELECT'], $rows, $checks, $runtime, $details);
echo 'connection-factory: saved result.html and result.json
';
Ожидаемый результат
connection-factory: saved result.html and result.json

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

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

Попробуйте сами. Поменяйте только construction=factory на generic и предскажите обе проверки instanceof. Полученное значение SELECT должно остаться Каталог.

Сохранить апостроф, кириллицу и четырёхбайтовый символ

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

Путь данных: Сохранить апостроф, кириллицу и четырёхбайтовый символ
  1. PHP UTF-8: O'Reilly и Чай 🍵
  2. charset=utf8mb4 для соединения и поля
  3. prepare + execute передают исходные значения
  4. SELECT возвращает точный текст; 5 символов / 11 байтов

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

IDСохранённая подписьСимволовБайтов UTF-8
1O'Reilly88
2Чай 🍵511

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

$dsn = 'mysql:host=127.0.0.1;port=33070;dbname=phpmanual_training;charset=utf8mb4';
$insert = $db->prepare('INSERT INTO pdo109_labels (id,label) VALUES (?,?)');
$insert->execute([2, 'Чай 🍵']);

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

В DSN указан charset=utf8mb4, а текстовая таблица создана с DEFAULT CHARSET=utf8mb4. Первая настройка относится к обмену клиента с сервером; вторая — к хранению поля. В подписи Чай 🍵 пять символов, но одиннадцать байтов UTF-8: три кириллические буквы по два байта, пробел и четырёхбайтовый символ. CHAR_LENGTH и OCTET_LENGTH отвечают на разные вопросы.

INSERT имеет два маркера ?. Массив execute передаёт ID и полную строку отдельно от SQL. Апостроф в O'Reilly остаётся частью значения: вручную писать обратные слеши или SQL-кавычки вокруг маркера не нужно. Проверка читает реально сохранённую подпись и сравнивает её с исходной. Кодировку соединения дополнительно проверяют три server variable: client, connection и results.

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

utf8mb4 не исправляет уже испорченные байты во входном файле. PHP-файл должен быть сохранён в UTF-8; HTTP/HTML также должны объявлять свою кодировку. CHAR_LENGTH считает кодовые точки, а не все возможные пользовательские графемы; составные эмодзи могут состоять из нескольких точек. Collation отдельно определяет сравнение/сортировку.

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

$details=[];
$db=pdo109Connect(); $runtime=pdo109Runtime($db);
$db->exec('CREATE TEMPORARY TABLE pdo109_labels (id INT PRIMARY KEY, label VARCHAR(80) NOT NULL) DEFAULT CHARSET=utf8mb4');
$labels=[1=>"O'Reilly",2=>'Чай 🍵'];
$insert=$db->prepare('INSERT INTO pdo109_labels (id,label) VALUES (?,?)');
foreach ($labels as $id=>$label) { $insert->execute([$id,$label]); }
$st=$db->query('SELECT id,label,CHAR_LENGTH(label) AS characters,OCTET_LENGTH(label) AS bytes FROM pdo109_labels ORDER BY id');
$rows=[]; $actual=[];
while (($r=$st->fetch()) !== false) { $actual[(int)$r['id']]=$r['label']; $rows[]=[(int)$r['id'],$r['label'],(int)$r['characters'],(int)$r['bytes']]; }
$st->closeCursor();
$charset=$db->query('SELECT @@character_set_client,@@character_set_connection,@@character_set_results')->fetch(PDO::FETCH_NUM);
$checks=['text_roundtrip'=>$actual===$labels,'connection_utf8mb4'=>$charset===['utf8mb4','utf8mb4','utf8mb4'],'characters_and_bytes'=>$rows[1][2]===5 && $rows[1][3]===11];
$details=['connection_charsets'=>$charset]; $insert=null; $st=null; $db=null;
pdo109Report('pdo109-utf8-roundtrip', ['ID', 'Сохранённая подпись', 'Символов', 'Байтов UTF-8'], $rows, $checks, $runtime, $details);
echo 'utf8-roundtrip: saved result.html and result.json
';
Ожидаемый результат
utf8-roundtrip: saved result.html and result.json

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

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

Попробуйте сами. Сохраните подпись Кофе 🍵. Предскажите CHAR_LENGTH и OCTET_LENGTH, затем сверьте столбцы. Не заменяйте utf8mb4 на старое utf8mb3.

Один ввод в двух местах: уникальные маркеры native prepare

Один ID нужно использовать в двух условиях. После отключения эмуляции старый запрос с повторяющимся :key перестаёт выполняться. Покажем причину и переносимый вариант.

Путь данных: Один ввод в двух местах: уникальные маркеры native prepare
  1. Вход ID 2 нужен двум условиям
  2. native :key/:key → HY093
  3. native :first=2/:second=2 → Кофе
  4. emulated сравнение → тоже Кофе, другой механизм

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

РежимМаркерыРезультат выполненияПолученный товар
native:key / :keyHY093(пустая строка)
native:first / :secondуспехКофе
emulated:key / :keyуспехКофе

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

$st = $db->prepare('SELECT name FROM pdo109_products WHERE id=:first OR id=:second');
$st->execute(['first' => 2, 'second' => 2]);
$name = $st->fetchColumn(); // Кофе

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

PDO_MYSQL по умолчанию использует эмуляцию prepare; здесь режим задан явно через ATTR_EMULATE_PREPARES=false. Для показанного SELECT сервер получает подготовленную команду с отдельными позициями параметров. Два появления одного имени :key не являются двумя самостоятельно связанными значениями. Контрольный native-вариант ожидаемо даёт HY093; далее корректный SQL использует :first и :second, даже когда их значения равны 2.

Сравнительный запуск с эмуляцией true на том же соединении возвращает Кофе и при повторном :key. Его задача — объяснить изменение поведения, а не предложить включать эмуляцию при любой ошибке. Native и emulated различаются местом подготовки и обработки маркеров. Для прикладного запроса проще сохранить уникальные имена: это не зависит от случайной настройки предыдущего разработчика.

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

Маркер обозначает целое значение. Им нельзя подставить имя таблицы, направление ORDER BY, кусок SQL или сразу весь список IN. Не смешивайте ? и :name в одном шаблоне. Подготовленные запросы не проверяют бизнес-правила. Динамический IN, допустимая сортировка и LIKE разобраны в прикладных фильтрах.

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

$details=[];
$db=pdo109Connect(); $runtime=pdo109Runtime($db); pdo109Catalog($db);
$bad=$db->prepare('SELECT name FROM pdo109_products WHERE id=:key OR id=:key ORDER BY id');
$state='not-rejected';
try { $bad->execute(['key'=>2]); } catch (PDOException $e) { $state=$e->getCode(); }
$bad->closeCursor();
$good=$db->prepare('SELECT name FROM pdo109_products WHERE id=:first OR id=:second ORDER BY id');
$good->execute(['first'=>2,'second'=>2]); $fixed=$good->fetchAll(PDO::FETCH_COLUMN); $good->closeCursor();
$db->setAttribute(PDO::ATTR_EMULATE_PREPARES,true);
$emulated=$db->prepare('SELECT name FROM pdo109_products WHERE id=:key OR id=:key ORDER BY id');
$emulated->execute(['key'=>2]); $viaEmulation=$emulated->fetchAll(PDO::FETCH_COLUMN); $emulated->closeCursor();
$rows=[['native',':key / :key',$state,''],['native',':first / :second','успех',implode(', ',$fixed)],['emulated',':key / :key','успех',implode(', ',$viaEmulation)]];
$checks=['duplicate_native_rejected'=>$state==='HY093','unique_native_correct'=>$fixed===['Кофе'],'emulated_comparison'=>$viaEmulation===$fixed];
$bad=$good=$emulated=null; $db=null;
pdo109Report('pdo109-native-markers', ['Режим', 'Маркеры', 'Результат выполнения', 'Полученный товар'], $rows, $checks, $runtime, $details);
echo 'native-markers: saved result.html and result.json
';
Ожидаемый результат
native-markers: saved result.html and result.json

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

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

Попробуйте сами. Сделайте два разных значения: first=1, second=3. Корректный вариант должен вернуть Чай и Мёд. Повторение имени и повторение значения — не одно и то же.

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

bindValue и bindParam: когда берётся значение фильтра

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

Путь данных: bindValue и bindParam: когда берётся значение фильтра
  1. bindValue при ID 1 сохраняет 1
  2. Переменная стала 2; execute → Чай
  3. bindParam ссылается на переменную
  4. Переменная 2→Кофе, переменная 3→Мёд

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

СпособID при связыванииID перед executeПолученный товар
bindValue12Чай
bindParam12Кофе
bindParam, повтор13Мёд

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

$id = 1;
$st->bindParam(':id', $id, PDO::PARAM_INT);
$id = 2;
$st->execute(); // Передан 2; bindValue сохранил бы прежний 1

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

bindValue сохраняет переданное значение при вызове. В первой ветке ID 1 уже связан с параметром; присваивание $id=2 позже не заменяет его, и сервер возвращает Чай. bindParam связывает саму переменную по ссылке: значение читается при execute. Во второй ветке к этому моменту $id равен 2, поэтому получается Кофе; следующее execute после $id=3 возвращает Мёд.

Первый аргумент :id выбирает маркер, второй — значение или переменную, третий PDO::PARAM_INT задаёт тип передачи. bindParam требует переменную: результат выражения и буквальный 2 не являются удобным местом для ссылки. При явном связывании вызывайте execute() без нового массива параметров. Если отдельного управления типами и моментом связывания не нужно, execute(['id'=>2]) короче; это отдельный способ передать вход.

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

Ссылка на переменную — не ссылка на строку в базе. Изменение $id не посылает запрос само по себе. bindValue можно вызвать повторно с другим значением. Приведение к PDO::PARAM_INT не проверяет, что ID разрешён пользователю или вообще существует. Проверку доступа выполняют в условии нужного запроса.

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

$details=[];
$db=pdo109Connect(); $runtime=pdo109Runtime($db); pdo109Catalog($db);
$sql='SELECT name FROM pdo109_products WHERE id=:id';
$byValue=$db->prepare($sql); $id=1; $byValue->bindValue(':id',$id,PDO::PARAM_INT); $id=2;
$byValue->execute(); $a=$byValue->fetchColumn(); $byValue->closeCursor();
$byReference=$db->prepare($sql); $id=1; $byReference->bindParam(':id',$id,PDO::PARAM_INT); $id=2;
$byReference->execute(); $b=$byReference->fetchColumn(); $byReference->closeCursor();
$id=3; $byReference->execute(); $c=$byReference->fetchColumn(); $byReference->closeCursor();
$rows=[['bindValue',1,2,$a],['bindParam',1,2,$b],['bindParam, повтор',1,3,$c]];
$checks=['value_is_snapshot'=>$a==='Чай','reference_is_current'=>$b==='Кофе','reference_reused'=>$c==='Мёд'];
$byValue=$byReference=null; $db=null;
pdo109Report('pdo109-binding-time', ['Способ', 'ID при связывании', 'ID перед execute', 'Полученный товар'], $rows, $checks, $runtime, $details);
echo 'binding-time: saved result.html and result.json
';
Ожидаемый результат
binding-time: saved result.html and result.json

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

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

Попробуйте сами. Между bindParam и execute присвойте $id=99. Выполнение SQL может быть успешным, но fetchColumn вернёт false из-за отсутствия строки. Перейдите к следующей главе и разберите это различие.

Практический вопрос: Чем отличается bindParam от bindValue. Автору вопроса неясно, почему два способа связывания параметров нужны отдельно. Учебный сценарий изменяет одну переменную до execute и показывает разные товары. Вопрос помогает выбрать ситуацию; технический договор сверён с PHP/MySQL, код и данные написаны отдельно.

Тип параметра: сохранить код с нулями, количество 0 и NULL

Артикул 0007 нельзя превратить в число 7, количество 0 нельзя принять за пустоту, а отсутствие примечания отличается от введённой пустой строки.

Путь данных: Тип параметра: сохранить код с нулями, количество 0 и NULL
  1. sku=0007,qty=0,note отсутствует
  2. PARAM_STR / PARAM_INT / PARAM_NULL
  3. MySQL хранит VARCHAR / INT / NULL
  4. Обратное чтение: 0007, 0, NULL; IS NULL=true

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

АртикулКоличествоПримечаниеSQL IS NULL
00070NULLtrue
00082(пустая строка)false

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

$st->bindValue(':sku', '0007', PDO::PARAM_STR);
$st->bindValue(':qty', 0, PDO::PARAM_INT);
$st->bindValue(':note', null, PDO::PARAM_NULL);
$st->execute();

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

В таблице sku — VARCHAR, qty — INT, note допускает NULL. bindValue связывает артикул как PARAM_STR, количество как PARAM_INT, отсутствие текста как PARAM_NULL. Во второй строке note передан как пустая строка с PARAM_STR. SELECT и отдельное note IS NULL показывают, какие значения реально сохранились.

Тип параметра управляет передачей значения, но серверная схема определяет хранение. Если поле sku сделать целым числом, одних PARAM_STR недостаточно для сохранения ведущих нулей. Вход формы сначала проверяют по смыслу: разрешённое целое количество, допустимый артикул и выбранная политика отсутствующего поля. Нельзя считать (int)'не число' полноценной валидацией. При передаче массива execute PDO документирует обработку входов как PARAM_STR; явные bindValue делают требуемый договор наглядным.

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

NULL означает отсутствие значения; сравнивают его через IS NULL, а не = NULL. Эта программа демонстрирует типы входа. Политика типов полученных значений, DECIMAL и ATTR_STRINGIFY_FETCHES отдельно разобраны в диагностике типов результата.

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

$details=[];
$db=pdo109Connect(); $runtime=pdo109Runtime($db);
$db->exec('CREATE TEMPORARY TABLE pdo109_input (sku VARCHAR(20) PRIMARY KEY, qty INT NOT NULL, note VARCHAR(80) NULL) DEFAULT CHARSET=utf8mb4');
$st=$db->prepare('INSERT INTO pdo109_input (sku,qty,note) VALUES (:sku,:qty,:note)');
$st->bindValue(':sku','0007',PDO::PARAM_STR); $st->bindValue(':qty',0,PDO::PARAM_INT); $st->bindValue(':note',null,PDO::PARAM_NULL); $st->execute();
$st->bindValue(':sku','0008',PDO::PARAM_STR); $st->bindValue(':qty',2,PDO::PARAM_INT); $st->bindValue(':note','',PDO::PARAM_STR); $st->execute(); $st->closeCursor();
$rows=[];
foreach ($db->query('SELECT sku,qty,note,note IS NULL AS absent FROM pdo109_input ORDER BY sku') as $r) { $rows[]=[$r['sku'],(int)$r['qty'],$r['note'],(bool)$r['absent']]; }
$checks=['leading_zero_preserved'=>$rows[0][0]==='0007','zero_saved'=>$rows[0][1]===0,'null_not_empty'=>$rows[0][2]===null && $rows[1][2]==='' && $rows[0][3] && !$rows[1][3]];
$st=null; $db=null;
pdo109Report('pdo109-typed-input', ['Артикул', 'Количество', 'Примечание', 'SQL IS NULL'], $rows, $checks, $runtime, $details);
echo 'typed-input: saved result.html and result.json
';
Ожидаемый результат
typed-input: saved result.html and result.json

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

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

Попробуйте сами. Добавьте третий артикул с qty=0 и note='0'. В отчёте должны различаться число 0, строка 0, пустая строка и NULL. Изменение должно быть объяснимо по схеме, не по внешнему виду ячейки.

Дальше: выбрать форму результата

Возьмите один реальный запрос своего каталога. Запишите DSN без пароля, режим prepare, имена/типы входов и ожидаемые поля. Затем перейдите к чтению строк и освобождению результата. Это удобнее, чем менять параметры соединения наугад.

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

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

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

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

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