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

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

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

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

Глава 48 · После первого SELECT, массивов и настройки отдельного PHP CLI

Mysqlnd: где работает плагин и как наблюдать запросы

Разделить API PHP и C, проверить настоящий нативный модуль и получить страницу каталога вместе со счётчиками ограниченного окна. Полный обзор официальных задач, источников API, версий и сравнения с MySQL Proxy.

Зачем наблюдать обращения к базе и где находится mysqlnd

Страница каталога показывает два товара, но непонятно, сколько раз её код обращается к базе. Особенно трудно разобраться, если одна часть приложения использует mysqli, а другая — PDO. Здесь сначала найдём общий уровень этих обращений, а затем получим две вещи вместе: настоящие строки каталога и небольшой отчёт о выполненных запросах. Содержимое товаров остаётся результатом SQL; счётчики дополняют его и не заменяют чтение данных.

Один запрос каталога и два разных результата
  1. PHP вызывает mysqli::query: выбрать нужные поля доступных товаров.
  2. mysqli передаёт операцию клиентскому драйверу mysqlnd в том же процессе PHP.
  3. Наш C-плагин отмечает попытку и вызывает сохранённую исходную функцию mysqlnd.
  4. Исходный драйвер обменивается данными с MySQL; сервер выполняет SELECT.
  5. Строки возвращаются 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_mcMulti Connect: разделение чтения между несколькими соединениями/узлами.
mysqlnd_msMaster Slave: маршрутизация чтения и записи для репликации.
mysqlnd_qcQuery Cache: кэширование результатов запросов на стороне клиента.
mysqlnd_pscachePrepared Statement Handle Cache: кэш дескрипторов подготовленных запросов.
mysqlnd_sipSQL Injection Protection: аудит по набору правил разрешённых запросов.
mysqlnd_uhUser Handler: исторический мост к обработчикам на PHP.

Список сохранён как часть официального материала, а не как список готовых зависимостей для PHP 8. Установка и поддержка каждого такого проекта проверяются отдельно. Страница PECL прямо помечает mysqlnd_uh как неподдерживаемый пакет; там опубликована alpha-версия 2010 года. Мы не подменяем отсутствующий модуль PHP-классом и не предлагаем использовать его как проверенное современное решение. Официальный статус mysqlnd_uh.

Исторический пример mysqlnd_mc делит SELECT на части с LIMIT, направляет их разным серверам и объединяет ответ на клиенте. Это иллюстрация уровня вмешательства, а не универсальный рецепт правильного разбиения любой выборки. Для конкретного каталога ещё нужно определить порядок, расположение данных и согласованность частей.

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

Как выбрать подход по двум реальным задачам
  1. Нужно объяснить лишние обращения одного PHP-каталога → измерить ограниченный участок его выполнения.
  2. Нужно направлять запросы PHP, Python и фоновых служб → сначала рассмотреть общую сетевую топологию.
  3. Для обоих вариантов сначала определить поведение ошибок и транзакций, затем выбирать механизм.

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

Найти реальный драйвер и отличить PHP-функции от C API

Вы перенесли пример на другую машину и получили undefined function. До подключения к серверу нужно выяснить, нет ли нужного расширения, перепутан ли C API с PHP API и действительно ли загружен ваш собранный наблюдатель.

От нужного действия к наблюдаемому результату
  1. Проверяем имена расширений текущего PHP-процесса.
  2. function_exists ищет PHP-функции; два имени из C-заголовка дают false.
  3. ReflectionFunction связывает phpman_mysqlnd_snapshot с нативным модулем, а не с файлом-муляжом.
  4. 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-функции от C APIСамостоятельный пример
<?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-наблюдателя и изменит смысл проверки.

Сначала определить, что именно измеряется

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

Граница наблюдения каталога
  1. Подготовка: своё соединение, таблица и исходные товары — вне окна.
  2. Сброс счётчиков: начинаем измерять конкретную прикладную операцию.
  3. Чтение выбранных товаров: SQL выполняется сервером, PHP читает реальные строки.
  4. Снимок счётчиков: фиксируем попытки и исходы, затем сохраняем отчёт.
  5. Очистка: удаляем только собственную учебную таблицу — уже после снимка.
НаблюдениеЧто оно отвечаетЧего оно не доказывает
Счётчик прямых queryСколько раз выбранный путь драйвера вызван в этом окне.Он не равен числу всех серверных подзапросов и не покрывает нативный execute без отдельного hook.
Ошибки этого путиСколько этих обращений завершилось отказом исходного обработчика.Ноль ошибок не доказывает правильность бизнес-расчёта; нужны значения прочитанных строк.
Количество полученных товаровСколько строк действительно прочитала эта программа.Это не сетевой объём, не число просмотренных сервером строк и не расход памяти PHP.
Снимок без SQL и параметровКакие безопасные числовые признаки выбраны для отчёта.Такой снимок не восстановит текст проблемного запроса и не является полным журналом аудита.

Для байтов и штатных счётчиков есть отдельная статистика mysqlnd. Для времени и памяти нужны отдельные измерения. Даже правильно измеренное время одного запуска не превращается в обещание постоянной производительности. Сравнивайте одинаковые данные, выбранные поля и путь исполнения.

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

Показать страницу каталога и посчитать обращения внутри её операции

Каталогу нужны два самых доступных по цене опубликованных товара с положительным остатком и общее число подходящих товаров. Получим оба ответа из базы и проверим, сколько прямых вызовов query приходится именно на это действие. Прочие товары не загружаются в PHP ради последующей сортировки.

От нужного действия к наблюдаемому результату
  1. Своя таблица содержит CLAMP, CABLE, LED, TAPE и HIDDEN; два последних не проходят фильтр.
  2. После подготовки reset начинает пустое окно наблюдения.
  3. Первый SELECT возвращает CLAMP и CABLE в заданном сервером порядке; PHP читает две строки.
  4. Второй SELECT с тем же WHERE считает три подходящих товара; это число для страницы каталога.
  5. 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
Ошибок прямых query0
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 и архивы исходного кода.

От исходников к запущенному наблюдателю
  1. Выбрать конкретный PHP и соответствующие заголовки, включая ext/mysqlnd.
  2. Собрать небольшой C-модуль инструментами этого PHP.
  3. Загрузить полученный модуль в отдельный учебный процесс PHP.
  4. Проверить реальные 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 либо общий сетевой маршрут.

Два места для дополнительного обработчика
  1. Локальный путь: PHP → mysqli/PDO → mysqlnd с C-плагином → MySQL.
  2. Сетевой путь: PHP, Python или другой клиент → отдельный proxy → MySQL.
  3. Счётчик внутри PHP видит свой процесс; proxy видит только клиентов, подключённых через него.
СвойствоПлагин mysqlndMySQL 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.

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

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

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

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

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