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

Дерево руководства
Глава 48 · После первого SELECT, массивов и настройки отдельного PHP CLI
Mysqlnd: где работает плагин и как наблюдать запросы
Разделить API PHP и C, проверить настоящий нативный модуль и получить страницу каталога вместе со счётчиками ограниченного окна. Полный обзор официальных задач, источников API, версий и сравнения с MySQL Proxy.
Зачем наблюдать обращения к базе и где находится mysqlnd
Страница каталога показывает два товара, но непонятно, сколько раз её код обращается к базе. Особенно трудно разобраться, если одна часть приложения использует mysqli, а другая — PDO. Здесь сначала найдём общий уровень этих обращений, а затем получим две вещи вместе: настоящие строки каталога и небольшой отчёт о выполненных запросах. Содержимое товаров остаётся результатом SQL; счётчики дополняют его и не заменяют чтение данных.
- PHP вызывает mysqli::query: выбрать нужные поля доступных товаров.
- mysqli передаёт операцию клиентскому драйверу mysqlnd в том же процессе PHP.
- Наш C-плагин отмечает попытку и вызывает сохранённую исходную функцию mysqlnd.
- Исходный драйвер обменивается данными с MySQL; сервер выполняет SELECT.
- Строки возвращаются PHP-приложению, а отдельный счётчик хранит исход операции.
Плагин здесь — нативное расширение PHP на C. Это не плагин WordPress, не класс в папке проекта и не дополнительная таблица MySQL. C-код загружается в процесс интерпретатора. Название mysqlnd означает MySQL Native Driver: клиентскую часть, которая умеет разговаривать с сервером MySQL. API — набор доступных операций; у PHP-программы и у C-расширения эти наборы различаются.
Первое действие: проверить, загружен ли драйвер
<?php
$loaded = extension_loaded('mysqlnd');
var_export($loaded);В учебном окружении результат — true. В круглых скобках передаём строку с именем расширения; функция возвращает логическое значение. Знак = сохраняет его в переменной $loaded, а var_export показывает именно true или false. Эта проверка не подключается к базе и не доказывает доступность сервера. Параметр и результат extension_loaded; вывод значения var_export. Переменная и присваивание, вызов функции и возвращаемое значение.
| Уровень | Чем пользуется читатель | Что действительно происходит |
|---|---|---|
| PHP-приложение | mysqli::query или PDO::query | Пишет SQL, получает строки или обрабатывает отказ. Прикладной API остаётся привычным. |
| Драйвер | mysqlnd внутри PHP | Обрабатывает клиентскую часть протокола. PDO_MYSQL и mysqli могут использовать этот общий драйвер. |
| Плагин драйвера | Учебный C-модуль и его отдельный API наблюдения | Перехватывает выбранную операцию и передаёт её исходной реализации. Он не превращает все C-имена в PHP-функции. |
| Сервер | MySQL и собственная учебная таблица | Выполняет SELECT, фильтрацию и сортировку. Его версия и состояние отличаются от версии клиента. |
Модель похожа на счётчик проходов у служебного входа: он видит только проходящие через этот вход действия. Она не означает, что один hook видит каждый пакет протокола, каждый SQL-подзапрос на сервере или работу других языков. Нативные prepared statements имеют отдельные операции; это покажет пример с prepare и execute.
До полного запуска полезно уметь создать таблицу и прочитать SELECT: первое подключение и таблица и справочник mysqli. Начальный диагностический пример ниже не требует базы. Для измеряемого каталога архив содержит файлы запуска, исходники учебного модуля и инструкцию сборки. Собранный модуль привязан к соответствующему PHP и его заголовкам; переносить Linux-файл .so в Windows нельзя.
Что может плагин и что означают исторические имена
Таблицы функций mysqlnd можно расширять: перед нужной операцией выполняется наш код, затем — сохранённый исходный обработчик. Такой путь может работать без переписывания каждого вызова mysqli/PDO. Прозрачность означает сохранение поведения приложения; её нужно проверять на реально выбранных путях и ошибках. Наличие расширения само по себе не добавляет репликацию, кэш или защиту запросов.
Официальная страница также упоминает ext/mysql среди старых пользователей драйвера. Это историческое расширение объявлено устаревшим в PHP 5.5 и удалено в PHP 7. В современных примерах используются mysqli и PDO_MYSQL; старые mysql_query здесь не восстанавливаются.
| Группа задач | Что описывает официальный раздел | Что придётся решить в настоящем сервисе |
|---|---|---|
| Распределение нагрузки | Разделение чтения и записи, переключение при отказе, round-robin и выбор менее загруженного узла. | Какие данные уже видны на реплике, куда идёт транзакция и можно ли безопасно повторить операцию. Для одного каталога измерение числа запросов ещё не решает эти вопросы. |
| Наблюдение | Журналирование, анализ и аудит запросов. | Какие события измеряются, где заканчивается интервал, кому доступны данные и как удержать объём журнала. Здесь записываются счётчики и исходы без SQL-текста. |
| Производительность | Кэширование, ограничение потока и распределение данных по узлам. | Когда результат устаревает, какой запрос можно кэшировать и как собрать согласованный ответ. Более быстрый доступ к неверным данным не решает задачу пользователя. |
Исторические имена из официального списка
| Имя | Назначение в списке PHP Manual |
|---|---|
| mysqlnd_mc | Multi Connect: разделение чтения между несколькими соединениями/узлами. |
| mysqlnd_ms | Master Slave: маршрутизация чтения и записи для репликации. |
| mysqlnd_qc | Query Cache: кэширование результатов запросов на стороне клиента. |
| mysqlnd_pscache | Prepared Statement Handle Cache: кэш дескрипторов подготовленных запросов. |
| mysqlnd_sip | SQL Injection Protection: аудит по набору правил разрешённых запросов. |
| mysqlnd_uh | User Handler: исторический мост к обработчикам на PHP. |
Список сохранён как часть официального материала, а не как список готовых зависимостей для PHP 8. Установка и поддержка каждого такого проекта проверяются отдельно. Страница PECL прямо помечает mysqlnd_uh как неподдерживаемый пакет; там опубликована alpha-версия 2010 года. Мы не подменяем отсутствующий модуль PHP-классом и не предлагаем использовать его как проверенное современное решение. Официальный статус mysqlnd_uh.
Исторический пример mysqlnd_mc делит SELECT на части с LIMIT, направляет их разным серверам и объединяет ответ на клиенте. Это иллюстрация уровня вмешательства, а не универсальный рецепт правильного разбиения любой выборки. Для конкретного каталога ещё нужно определить порядок, расположение данных и согласованность частей.
Наш небольшой модуль решает одну проверяемую задачу — наблюдение. Он не реализует перечисленные кэши, маршрутизацию или правила безопасности. В частности, имя mysqlnd_sip в старом списке не отменяет параметры подготовленного запроса: данные пользователя по-прежнему передаются как значения, а не склеиваются с SQL.
- Нужно объяснить лишние обращения одного PHP-каталога → измерить ограниченный участок его выполнения.
- Нужно направлять запросы PHP, Python и фоновых служб → сначала рассмотреть общую сетевую топологию.
- Для обоих вариантов сначала определить поведение ошибок и транзакций, затем выбирать механизм.
Первое применение разобрано в измерении каталога. Второй выбор раскрывает сравнение с сетевым посредником. Если менять можно небольшой слой доступа к данным своего приложения, явная PHP-обёртка часто проще нативного расширения: она может измерять именно бизнес-операцию, но не перехватит автоматически чужие вызовы, обходящие эту обёртку.
Найти реальный драйвер и отличить PHP-функции от C API
Вы перенесли пример на другую машину и получили undefined function. До подключения к серверу нужно выяснить, нет ли нужного расширения, перепутан ли C API с PHP API и действительно ли загружен ваш собранный наблюдатель.
- Проверяем имена расширений текущего PHP-процесса.
- function_exists ищет PHP-функции; два имени из C-заголовка дают false.
- ReflectionFunction связывает phpman_mysqlnd_snapshot с нативным модулем, а не с файлом-муляжом.
- reset и snapshot дают пустое окно; база для этой диагностики не открывалась.
Сначала маленький вызов
<?php
var_export(function_exists('mysqli_query'));
echo PHP_EOL;
var_export(function_exists('mysqlnd_plugin_register'));В нашем профиле выводятся true, затем false. function_exists принимает строковое имя и ищет функцию PHP. Отсутствие C-имени в этом списке нормально: оно используется при сборке расширения. PHP_EOL даёт перевод строки. Дальше массив $availablePhp собирает несколько таких проверок, а ReflectionFunction показывает происхождение одной функции. Это дополнительные сведения, а не попытка вызвать сервер или обойти отсутствующий модуль.
function_exists и его ограничения; Reflection: узнать происхождение функции.
Что получится
| Наблюдение | Результат |
|---|---|
| mysqlnd загружен | да |
| mysqli доступен | да |
| Драйвер PDO_MYSQL доступен | да |
| mysqlnd_plugin_register как PHP-функция | нет |
| mysqlnd_conn_data_get_methods как PHP-функция | нет |
| Наблюдатель действительно из C-модуля | да |
| Прямых запросов в новом окне | 0 |
| Prepared execute в новом окне | 0 |
| Открыто соединение с базой | нет |
Полная программа и архив для запуска
<?php
declare(strict_types=1);
require __DIR__.'/lab.php';
$db=$config=null; $owned=$removed=$rows=$checks=$observations=[];
$runtime=n148Runtime();
try {
$loaded = [
'mysqlnd'=>extension_loaded('mysqlnd'),
'mysqli'=>extension_loaded('mysqli'),
'PDO'=>extension_loaded('PDO'),
'pdo_mysql'=>extension_loaded('pdo_mysql'),
'phpman_mysqlnd'=>extension_loaded('phpman_mysqlnd'),
];
if (in_array(false, $loaded, true)) {
throw new RuntimeException('Загрузите mysqlnd, mysqli, PDO, pdo_mysql и собранный учебный модуль по README.');
}
$phpNames = ['mysqli_query', 'phpman_mysqlnd_snapshot', 'phpman_mysqlnd_reset', 'phpman_mysqlnd_observe'];
$cNames = ['mysqlnd_plugin_register', 'mysqlnd_conn_data_get_methods'];
$availablePhp = $availableC = [];
foreach ($phpNames as $name) {$availablePhp[$name] = function_exists($name);}
foreach ($cNames as $name) {$availableC[$name] = function_exists($name);}
$pdoDriver = in_array('mysql', PDO::getAvailableDrivers(), true);
$reflection = new ReflectionFunction('phpman_mysqlnd_snapshot');
$nativeFunction = $reflection->isInternal() && $reflection->getExtensionName() === 'phpman_mysqlnd';
phpman_mysqlnd_reset();
$snapshot = phpman_mysqlnd_snapshot();
$emptyWindow = $snapshot['query_attempts'] === 0 && $snapshot['stmt_attempts'] === 0
&& $snapshot['tracked_connections'] === 0 && $snapshot['connections'] === [];
$observations = [
'extensions'=>$loaded, 'php_function_map'=>$availablePhp, 'c_names_visible_as_php'=>$availableC,
'pdo_mysql_driver'=>$pdoDriver, 'snapshot_is_internal'=>$reflection->isInternal(),
'snapshot_extension'=>$reflection->getExtensionName(),
'mysqlnd_version'=>phpversion('mysqlnd'), 'mysqli_client_version'=>mysqli_get_client_info(),
'plugin_version'=>phpversion('phpman_mysqlnd'), 'snapshot'=>$snapshot,
'database_connection_opened'=>$db !== null,
];
$checks = [
'required_extensions'=>!in_array(false, $loaded, true),
'php_functions_available'=>!in_array(false, $availablePhp, true),
'c_names_are_not_php'=>!in_array(true, $availableC, true),
'pdo_driver_available'=>$pdoDriver,
'native_function_origin'=>$nativeFunction,
'empty_measurement_window'=>$emptyWindow,
'no_database_connection'=>$db === null,
];
$rows = [
['mysqlnd загружен', $loaded['mysqlnd']],
['mysqli доступен', $availablePhp['mysqli_query']],
['Драйвер PDO_MYSQL доступен', $pdoDriver],
['mysqlnd_plugin_register как PHP-функция', $availableC['mysqlnd_plugin_register']],
['mysqlnd_conn_data_get_methods как PHP-функция', $availableC['mysqlnd_conn_data_get_methods']],
['Наблюдатель действительно из C-модуля', $nativeFunction],
['Прямых запросов в новом окне', $snapshot['query_attempts']],
['Prepared execute в новом окне', $snapshot['stmt_attempts']],
['Открыто соединение с базой', $db !== null],
];
} finally {
if (function_exists('phpman_mysqlnd_observe')) {phpman_mysqlnd_observe(null);}
if ($db instanceof mysqli) {
try {$removed=n148Cleanup($db,$owned);} finally {$db->close();}
}
}
n148Report('mysql148-nd-api-layer-map', 'Найти реальный драйвер и отличить PHP-функции от C API', $rows, $checks, $observations, $runtime, $owned, $removed);
Ожидаемый результат
mysqlnd загружен — да
mysqli доступен — да
Драйвер PDO_MYSQL доступен — да
mysqlnd_plugin_register как PHP-функция — нет
mysqlnd_conn_data_get_methods как PHP-функция — нет
Наблюдатель действительно из C-модуля — да
Прямых запросов в новом окне — 0
Prepared execute в новом окне — 0
Открыто соединение с базой — нет
Распакуйте архив в отдельную папку. README.txt описывает окружение и запуск; README-extension.txt — сборку настоящего C-модуля. Вместе с PHP-файлами приложены phpman_mysqlnd.c, config.m4 и build-extension.sh. Карта API не открывает базу. Каталог создаёт одну собственную таблицу и удаляет её в finally. result.html и result.json сохраняют результат выполнения, metrics.json — проверки.
Таблица фактического запуска · Результат и наблюдения JSON.
Границы, ошибки и варианты развития
Проверка доступности функции не доказывает успешность соединения, SQL или файловой записи. Здесь мы не вызываем отсутствующие C-имена и не создаём PHP-заглушки вместо расширения. phpversion и client info сохраняются как реальные строки: их значение изменится в другом профиле. Встроенный mysqlnd и наш phpman_mysqlnd — разные модули. reset() очищает только окно наблюдения, snapshot() без аргументов возвращает массив; они не изменяют базу и не закрывают соединения. Без загрузки учебного модуля эти три phpman_* функции отсутствуют.
Разработчик видит названия mysqlnd и libmysqlclient и не понимает, являются ли они новым способом писать SELECT в PHP. Исходная практическая проблема. Разделяем прикладной API mysqli/PDO, его драйвер и сервер. Проверяем реально загруженные компоненты; функцию C из заголовка не предлагаем вызывать как PHP-функцию.
Попробуйте изменить
Уберите -d extension=...phpman_mysqlnd.so из команды отдельного учебного запуска. Как отличить это от недоступного MySQL?
Разбор изменения
Сначала проверьте extension_loaded и function_exists в том PHP, который запускает пример. Если модуль отсутствует, нужно вернуться к сборке/загрузке из README. В этом примере соединения вообще нет, поэтому менять пароль или порт базы бессмысленно. Не добавляйте функцию с тем же именем в PHP: это скроет отсутствие C-наблюдателя и изменит смысл проверки.
Сначала определить, что именно измеряется
«Запросов стало меньше» — полезная подсказка, если сравниваются одинаковые действия и известна граница измерения. Для каталога начинаем окно после создания таблицы и загрузки исходных товаров, заканчиваем после чтения нужных строк. Удаление учебной таблицы выполняется позже и не входит в результат окна.
- Подготовка: своё соединение, таблица и исходные товары — вне окна.
- Сброс счётчиков: начинаем измерять конкретную прикладную операцию.
- Чтение выбранных товаров: SQL выполняется сервером, PHP читает реальные строки.
- Снимок счётчиков: фиксируем попытки и исходы, затем сохраняем отчёт.
- Очистка: удаляем только собственную учебную таблицу — уже после снимка.
| Наблюдение | Что оно отвечает | Чего оно не доказывает |
|---|---|---|
| Счётчик прямых query | Сколько раз выбранный путь драйвера вызван в этом окне. | Он не равен числу всех серверных подзапросов и не покрывает нативный execute без отдельного hook. |
| Ошибки этого пути | Сколько этих обращений завершилось отказом исходного обработчика. | Ноль ошибок не доказывает правильность бизнес-расчёта; нужны значения прочитанных строк. |
| Количество полученных товаров | Сколько строк действительно прочитала эта программа. | Это не сетевой объём, не число просмотренных сервером строк и не расход памяти PHP. |
| Снимок без SQL и параметров | Какие безопасные числовые признаки выбраны для отчёта. | Такой снимок не восстановит текст проблемного запроса и не является полным журналом аудита. |
Для байтов и штатных счётчиков есть отдельная статистика mysqlnd. Для времени и памяти нужны отдельные измерения. Даже правильно измеренное время одного запуска не превращается в обещание постоянной производительности. Сравнивайте одинаковые данные, выбранные поля и путь исполнения.
Два частых применения этого отчёта: заметить лишний запрос в цикле по товарам и проверить, что после изменения кода не исчезло чтение обязательного поля. В первом случае число обращений помогает найти место; во втором только проверка реальных значений подтверждает полезный результат. Нельзя ускорять каталог загрузкой всех строк в PHP и дальнейшей сортировкой: условия, порядок и нужный размер выборки задаются в SQL.
Показать страницу каталога и посчитать обращения внутри её операции
Каталогу нужны два самых доступных по цене опубликованных товара с положительным остатком и общее число подходящих товаров. Получим оба ответа из базы и проверим, сколько прямых вызовов query приходится именно на это действие. Прочие товары не загружаются в PHP ради последующей сортировки.
- Своя таблица содержит CLAMP, CABLE, LED, TAPE и HIDDEN; два последних не проходят фильтр.
- После подготовки reset начинает пустое окно наблюдения.
- Первый SELECT возвращает CLAMP и CABLE в заданном сервером порядке; PHP читает две строки.
- Второй SELECT с тем же WHERE считает три подходящих товара; это число для страницы каталога.
- snapshot сохраняет два прямых вызова и их исходы до удаления учебной таблицы.
Сначала маленький вызов
phpman_mysqlnd_reset();
$result = $db->query("SELECT sku, price FROM $qualified ORDER BY price, sku LIMIT 2");
$products = $result->fetch_all(MYSQLI_ASSOC);
$metrics = phpman_mysqlnd_snapshot();Это фрагмент внутри уже открытого учебного соединения $db: $qualified содержит проверенное имя собственной таблицы из полного примера. reset() и snapshot() — безаргументные функции нашего модуля. Первая начинает окно, вторая возвращает массив счётчиков. Оператор -> вызывает метод объекта; query возвращает объект ответа для SELECT, fetch_all читает его строки. MYSQLI_ASSOC просит ключи с именами столбцов. Здесь будет один прямой query; полная версия ниже добавляет WHERE и второй SELECT с COUNT(*), чтобы экран получил и два товара, и общий итог.
WHERE выбирает нужные строки, ORDER BY задаёт порядок, LIMIT ограничивает страницу; дополнительный sku делает порядок определённым при одинаковой цене. COUNT(*) — агрегат: он возвращает число подходящих строк одним полем. Как строить выборку под экран.
Что получится
| Наблюдение | Результат |
|---|---|
| Первый товар | CLAMP — Крепление |
| Цена первого товара | 25.50 |
| Остаток первого товара | 5 |
| Второй товар | CABLE — Кабель |
| Цена второго товара | 120.00 |
| Остаток второго товара | 8 |
| Товаров на странице | 2 |
| Всего доступных товаров | 3 |
| Прямых query в окне | 2 |
| Ошибок прямых query | 0 |
| Prepared execute в окне | 0 |
| Соединений в окне | 1 |
| Снимок содержит только оговорённые поля | да |
Полная программа и архив для запуска
<?php
declare(strict_types=1);
require __DIR__.'/lab.php';
$db=$config=null; $owned=$removed=$rows=$checks=$observations=[];
$runtime=n148Runtime();
try {
$config=n148Config(); $db=n148Connect($config); $runtime=n148Runtime($db,$config);
$name = n148OwnTable($db, $owned, 'catalog_window',
'(sku VARCHAR(32) PRIMARY KEY, title VARCHAR(64) NOT NULL, price DECIMAL(10,2) NOT NULL, '
.'stock INT NOT NULL, published TINYINT NOT NULL) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4');
$qualified = n148Qualified($name);
$db->query("INSERT INTO $qualified (sku,title,price,stock,published) VALUES
('CABLE','Кабель',120.00,8,1),
('CLAMP','Крепление',25.50,5,1),
('LED','Лампа',250.00,3,1),
('TAPE','Лента',70.00,0,1),
('HIDDEN','Черновик товара',890.00,2,0)");
phpman_mysqlnd_reset();
$pageResult = $db->query("SELECT sku,title,price,stock FROM $qualified
WHERE published=1 AND stock>0 ORDER BY price ASC,sku ASC LIMIT 2");
$products = $pageResult->fetch_all(MYSQLI_ASSOC);
$pageResult->free();
$countResult = $db->query("SELECT COUNT(*) AS available_count FROM $qualified
WHERE published=1 AND stock>0");
$countRow = $countResult->fetch_assoc();
$countResult->free();
$snapshot = phpman_mysqlnd_snapshot();
$expectedProducts = [
['sku'=>'CLAMP', 'title'=>'Крепление', 'price'=>'25.50', 'stock'=>'5'],
['sku'=>'CABLE', 'title'=>'Кабель', 'price'=>'120.00', 'stock'=>'8'],
];
$total = (int)$countRow['available_count'];
$snapshotKeys = array_keys($snapshot);
sort($snapshotKeys);
$allowedKeys = ['extension_version','query_attempts','query_failed','stmt_attempts','stmt_failed',
'tracked_connections','closed_connections','dropped_connections','observer_calls','observer_errors',
'observer_reentrant_skips','connections'];
sort($allowedKeys);
$connectionsSafe = true;
foreach ($snapshot['connections'] as $connection) {
$keys = array_keys($connection); sort($keys);
$allowed = ['id','query_attempts','query_failed','stmt_attempts','stmt_failed','closed']; sort($allowed);
if ($keys !== $allowed) {$connectionsSafe = false;}
}
$safeSnapshotShape = $snapshotKeys === $allowedKeys && $connectionsSafe;
$observations = [
'products'=>$products, 'count_row'=>$countRow, 'available_count'=>$total,
'page_size'=>count($products), 'snapshot'=>$snapshot,
'measurement'=>['reset_after_seed'=>true, 'snapshot_before_cleanup'=>true,
'direct_query_calls_in_window'=>2, 'captures_sql_text'=>false, 'captures_parameter_values'=>false],
];
$checks = [
'exact_catalogue_page'=>$products === $expectedProducts,
'matching_filter_total'=>$total === 3,
'bounded_page'=>count($products) === 2,
'direct_query_window'=>$snapshot['query_attempts'] === 2 && $snapshot['query_failed'] === 0,
'no_prepared_execute_in_window'=>$snapshot['stmt_attempts'] === 0 && $snapshot['stmt_failed'] === 0,
'one_tracked_connection'=>$snapshot['tracked_connections'] === 1 && count($snapshot['connections']) === 1,
'no_connection_overflow'=>$snapshot['dropped_connections'] === 0,
'snapshot_has_only_safe_fields'=>$safeSnapshotShape,
];
$rows = [
['Первый товар', $products[0]['sku'].' — '.$products[0]['title']],
['Цена первого товара', $products[0]['price']],
['Остаток первого товара', (int)$products[0]['stock']],
['Второй товар', $products[1]['sku'].' — '.$products[1]['title']],
['Цена второго товара', $products[1]['price']],
['Остаток второго товара', (int)$products[1]['stock']],
['Товаров на странице', count($products)],
['Всего доступных товаров', $total],
['Прямых query в окне', $snapshot['query_attempts']],
['Ошибок прямых query', $snapshot['query_failed']],
['Prepared execute в окне', $snapshot['stmt_attempts']],
['Соединений в окне', $snapshot['tracked_connections']],
['Снимок содержит только оговорённые поля', $safeSnapshotShape],
];
} finally {
if (function_exists('phpman_mysqlnd_observe')) {phpman_mysqlnd_observe(null);}
if ($db instanceof mysqli) {
try {$removed=n148Cleanup($db,$owned);} finally {$db->close();}
}
}
n148Report('mysql148-nd-request-metrics', 'Показать страницу каталога и посчитать обращения внутри её операции', $rows, $checks, $observations, $runtime, $owned, $removed);
Ожидаемый результат
Первый товар — CLAMP — Крепление
Цена первого товара — 25.50
Остаток первого товара — 5
Второй товар — CABLE — Кабель
Цена второго товара — 120.00
Остаток второго товара — 8
Товаров на странице — 2
Всего доступных товаров — 3
Прямых query в окне — 2
Ошибок прямых query — 0
Prepared execute в окне — 0
Соединений в окне — 1
Снимок содержит только оговорённые поля — да
Распакуйте архив в отдельную папку. README.txt описывает окружение и запуск; README-extension.txt — сборку настоящего C-модуля. Вместе с PHP-файлами приложены phpman_mysqlnd.c, config.m4 и build-extension.sh. Карта API не открывает базу. Каталог создаёт одну собственную таблицу и удаляет её в finally. result.html и result.json сохраняют результат выполнения, metrics.json — проверки.
Таблица фактического запуска · Результат и наблюдения JSON.
Границы, ошибки и варианты развития
Здесь все SQL-значения фиксированы автором; ввод посетителя требует параметров подготовленного запроса. Цена DECIMAL сохраняется строкой, без потери нулей после точки; остаток и COUNT(*) в этом прямом mysqli-пути прочитаны строками и явно преобразованы в целые только для числового вывода. COUNT(*) считает строки на сервере и не равен SELECT *: в PHP приходит один итог, а не все товары. На большой таблице такой подсчёт всё равно имеет стоимость — измеряйте план и индексы. Счётчик 2 доказывает выбранное окно, а не скорость сайта и не отсутствие N+1 во всех маршрутах. Два SELECT без общей транзакции могут увидеть разные состояния при параллельных изменениях: учебные данные здесь неизменны. При SQL-ошибке строгий mysqli прерывает основной сценарий исключением; finally всё равно убирает собственную таблицу. Успешный отчёт не записывается поверх неудачи. Snapshot имеет ограниченные поля, а отдельный result.json намеренно содержит прочитанные товары; это не обещание скрыть данные всего приложения.
Название метода верхнего API не всегда совпадает с hook: в проверенном профиле PDO с выключенной эмуляцией даже PDO::query идёт через prepared execute. На это нельзя переносить ожидание query=2 из текущего mysqli-примера. Сравнить реальные пути mysqli и PDO; увидеть сохранённую SQL-ошибку и успешное продолжение.
Для оценки запроса требуется «размер результата», но в обсуждении смешиваются число строк, сетевой обмен и память PHP. Исходная практическая проблема. Наш отчёт явно называет единицы: число попыток прямого запроса, число ошибок и число прочитанных строк. Эти числа не переименовываем в байты или расход памяти. Для иной величины нужен отдельный измеритель и объяснение его границ.
Разработчик хочет увидеть запрос PDO вместе со значениями параметров; простая подстановка строк создаёт неточный диагностический SQL и раскрывает данные. Исходная практическая проблема. Для нашей задачи достаточно вида операции и её исхода. Учебный C-плагин не сохраняет текст SQL и значения. Подготовленные запросы проходят отдельный путь, который следующая глава проверяет собственным hook; из одного счётчика query нельзя заключить, что учтены все варианты работы с БД.
Попробуйте изменить
Измените LIMIT 2 на LIMIT 1. Какие результаты должны измениться, а какие — остаться прежними?
Разбор изменения
На странице останется CLAMP за 25.50. COUNT(*) всё ещё даст 3, число прямых запросов останется 2. Это показывает, что размер страницы, число подходящих товаров и количество обращений — три разные величины. В коде нужно согласованно изменить ожидаемую страницу и вывод, не обращаться к отсутствующему products[1].
Откуда берётся C API и как сверить версию
Для счётчика внутри драйвера недостаточно скачать файл с функцией PHP. Нужны заголовки того PHP, для которого собирается расширение: это объявления C-структур, типов и функций. Plugin API входит в ext/mysqlnd в исходном дереве PHP; отдельная PHP-библиотека с таким именем не требуется. Разработка этого API началась в декабре 2009 года, исходники распространяются через репозиторий PHP и архивы исходного кода.
- Выбрать конкретный PHP и соответствующие заголовки, включая ext/mysqlnd.
- Собрать небольшой C-модуль инструментами этого PHP.
- Загрузить полученный модуль в отдельный учебный процесс PHP.
- Проверить реальные PHP-функции модуля и выполнить запрос с сохранённым исходным обработчиком.
Версия загруженного расширения доступна обычному PHP
<?php
$version = phpversion('mysqlnd');
var_export($version);Аргумент phpversion — имя расширения; результат является строкой версии либо false, если сведения недоступны. Это клиентская версия, а не версия сервера MySQL. В нашем Linux-профиле PHP используется 8.3.6. Самостоятельная карта API сохраняет фактическую строку запущенного процесса. У веб-сервера и у терминала могут быть разные PHP и конфигурации, поэтому проверять нужно нужный процесс. Полный контракт phpversion.
В C-заголовке имя и смысл проверяются отдельно
| Имя | Официальное описание и точная граница |
|---|---|
| MYSQLND_VERSION | В тексте PHP Manual это строковая версия mysqlnd. В matching-заголовке PHP 8.3.6 строковый макрос называется PHP_MYSQLND_VERSION. Не копируйте старое имя в сборку без проверки заголовка. |
| PHP_MYSQLND_VERSION | В mysqlnd.h из PHP 8.3.6 складывается из строки mysqlnd и PHP_VERSION. Это макрос языка C, не PHP-константа. |
| MYSQLND_VERSION_ID | Числовая версия; в заголовке 8.3.6 она связана с PHP_VERSION_ID. Правило записи: major × 10000 + minor × 100 + patch. Для 8.3.6 получается 80306. |
| MYSQLND_PLUGIN_API_VERSION | В этом же заголовке равен 2. Номер интерфейса плагинов не является ни 8.3.6, ни версией сервера MySQL. |
/* Фрагмент C, не содержимое PHP-файла */
#include "ext/mysqlnd/mysqlnd.h"
/* Проверяем реальные определения из выбранных заголовков:
PHP_MYSQLND_VERSION
MYSQLND_VERSION_ID
MYSQLND_PLUGIN_API_VERSION
*/Строка #include подключает объявления при компиляции C; комментарий /* ... */ объясняет код и не выполняется. Этот фрагмент не является самостоятельной программой. Полный config.m4, исходник C и последовательность phpize/configure/make даны в главе сборки нативного модуля. phpize подготавливает сборочную среду расширения; успешная компиляция ещё должна завершиться загрузкой модуля и проверкой его поведения.
| Практическая задача | Что проверяем до установки |
|---|---|
| Перенести модуль с учебной машины на другой сервер | Сопоставить версию PHP, его API и заголовки, архитектуру и режим сборки. Старый бинарный файл не становится совместимым от переименования. |
| Исправить старый пример C из документации | Сверить имена таблиц методов и сигнатуры с точным исходным кодом PHP. Проверка номера версии помогает выбрать ветвь совместимости, но не заменяет чтение изменившегося API. |
Например, mysqlnd_conn_get_methods — имя C API. В проверенном заголовке рядом существуют отдельные mysqlnd_conn_data_get_methods и таблица методов данных соединения. Вызов function_exists в PHP не ищет объявления в этих заголовках. Это различие можно увидеть в диагностике, не подключаясь к MySQL. Точные определения PHP 8.3.6; Таблицы методов.
Плагин в PHP и отдельный сетевой посредник
Представим два проекта. В первом каталог целиком написан на PHP и нужен счётчик его запросов. Во втором те же данные читают PHP-сайт, Python-импорт и отдельная утилита. У них разные точки, в которых удобно наблюдать обращения: процесс PHP либо общий сетевой маршрут.
- Локальный путь: PHP → mysqli/PDO → mysqlnd с C-плагином → MySQL.
- Сетевой путь: PHP, Python или другой клиент → отдельный proxy → MySQL.
- Счётчик внутри PHP видит свой процесс; proxy видит только клиентов, подключённых через него.
| Свойство | Плагин mysqlnd | MySQL Proxy в историческом сравнении |
|---|---|---|
| Размещение | Расширение на каждом сервере PHP-приложения. | Отдельный процесс на сервере приложения либо выделенном узле. |
| Какие клиенты проходят через него | PHP-расширения, использующие mysqlnd. | Разные MySQL-клиенты, если их подключения направлены через proxy. |
| Настройка логики | C; в документации также описан исторический мост mysqlnd_uh к PHP. | C и Lua; Lua был основным языком пользовательской логики MySQL Proxy. |
| Состояние между запросами | Обычное состояние запроса PHP заканчивается вместе с ним. Общие данные требуют явно выбранного хранилища. | Демон живёт дольше отдельного запроса и может сохранять состояние между обращениями. |
| Общий результат для нескольких серверов | Нужно внешнее разделяемое хранилище, например отдельный сервис кэша. | Общий proxy может хранить такое состояние у себя; согласованность и сбои всё равно требуют проекта. |
| Уровень вмешательства | C API драйвера; внутренние части mysqlnd позволяют работать и ниже. | Сетевой протокол клиент–сервер; логика зависит от корректной обработки его сообщений. |
Это сохраняет смысл официального сравнения с MySQL Proxy, а не предлагает установить старый продукт в качестве современной инфраструктуры. C-плагину на уровне API обычно не нужно заново разбирать пакеты: этим занимается mysqlnd. Изменения протокола могут быть скрыты драйвером, но совместимость самого C API с новой версией PHP всё равно проверяется.
Как читать аргументы о масштабировании и отказе
Размещение компонента рядом с каждым приложением не добавляет один общий центральный узел и позволяет увеличивать число таких пар вместе с приложением. Если все подключения проходят через один выделенный proxy, его отказ затрагивает этот маршрут. Это свойство конкретной топологии: резервирование меняет картину. Отсутствие отдельного proxy не делает нативный модуль безопасным — ошибка C-кода может повредить процесс PHP.
| Ситуация | Обоснованный следующий шаг |
|---|---|
| Внутри одного PHP-запуска нужно найти лишние обращения | Сначала выбрать короткий измеряемый участок и сохранить его реальные строки результата. Это наша программа request-metrics. |
| Нужны общие правила для клиентов на разных языках | Спроектировать сетевой маршрут, отказоустойчивость, права и состояние. Один установленный PHP-модуль не наблюдает чужие процессы. |
| Нужно помнить данные между HTTP-запросами | Явно определить владельца и время жизни данных. Счётчик одного запроса нельзя выдавать за общий счётчик сервиса. |
Нативный плагин не является песочницей: он работает с внутренними структурами PHP. Для учебного измерения модуль загружается явно в отдельный CLI-процесс, работает с собственными данными и не перенаправляет соединение на другой сервер. Подробное сохранение исходного обработчика, пути prepared statements и защита повторного входа разобраны в следующей главе о C-плагине.
Дальше: собрать модуль и сохранить исходное поведение
Теперь известна разница между функцией PHP, C API драйвера и сервером, а каталог имеет собственный проверяемый результат и границу измерения. Следующий шаг — сборка и устройство C-наблюдателя: исходный обработчик, prepared statements, два соединения и ограниченный PHP-callback. Если нужен привычный PHP-уровень метрик без своей C-сборки, вернитесь к штатной статистике mysqlnd.
Источники и обсуждения
- PHP Manual: Плагин mysqlnd: место, задачи и границы
- PHP Manual: Откуда берётся C API и как сверить версию
- PHP Manual: Плагин в PHP и отдельный сетевой посредник
- PHP 8.3.6: заголовок mysqlnd.h
- PHP 8.3.6: расширяемые таблицы методов
- PECL: официальный статус mysqlnd_uh
- PHP: extension_loaded
- PHP: function_exists
- PHP: phpversion
- PHP: mysqli и уровни доступа к MySQL
- PHP 7: удаление исторического ext/mysql
- PHP: устаревшее расширение ext/mysql
- PHP: отдельный путь подготовленного запроса mysqli
- PHP: статистика mysqlnd
- PHP: подготовленные запросы PDO
- PHP: phpize и сборка расширения
Код и руководство с собой
Скачайте самостоятельные примеры с исходными данными, инструкциями и ожидаемыми результатами. Примеры HTTP и фрагменты для проекта помечены отдельно: им нужно указанное окружение.
Код не исполняется на сайте. Скачайте нужный пример и запускайте его в своём локальном окружении.
Материалы сверены 2026-10-12. Примеры и объяснения написаны для этого руководства; источники и обсуждения доступны в каждой главе.


