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

Две критические уязвимости в PHP, раскрытые утром этого злополучного дня. Точных деталей в паблике еще небыло. Говорилось, что вендор уже выкатил фикс в 8.5.12, а все, что ниже — официально в красной зоне. Такая информация была на момент осознания масштаба проблемы.

У меня на поддержке около сотни сайтов. Разброс по серверам — от убитого shared‑хостинга, где админ спит еще с нулевых, до пары десятков VPS. Версии PHP — от гордого 8.3 на свежих проектах до музейных экспонатов.

Первая мысль была рациональной: «Завтра с утра спокойно пропишем обновление в CI/CD, прогоняем тесты, выкатываем». Вторая мысль, более честная, пришла через секунду: «если дыры опубликовали сегодня, эксплойты появятся в паблике завтра к обеду». Спать я в ту ночь лег в 04:30.

Что за уязвимости и почему именно 8.5.12

Деталей CVE на момент алерта не было. Точнее не было у меня. Было другое: PHP Group четко написала в релизе — апдейт критический, рекомендуется немедленное обновление.

В 2026 году PHP 8.5 — это уже вполне рабочий мейнстрим, но в продакшене он стоит далеко не везде. Многие клиенты сидят на 8.2 или 8.3, потому что «работает — не трогай». А у меня в заначке были те самые 32 сайта, которые до сих пор дышали на ладан под PHP 5.4 и 5.6.

Это старые самописные CRM, интернет‑магазины на Joomla 1.5 и форумы на vBulletin, которые клиенты отказываются переписывать, потому что «там СТОЛЬКО всего за десять лет».

Вариант «просто обновиться до 8.5» для них означал мгновенную белую страницу. Расширение mysql_* вырезали из ядра еще в PHP 7.0. Любой вызов mysql_connect() или mysql_query() на восьмерке роняет скрипт с фаталом.

Я молчу про стрикт‑типизацию и многое другое, что у 8+ под капотом.

Марафон переноса: сто сайтов за трое суток

План был простой и жестокий.

  • Shared‑хостинги, где у меня нет рута — тикеты в поддержку с криками, слезами или угрозами — «срочно накатите 8.5.12». Выжили не все, два провайдера ответили, что у них в планах апгрейд на декабрь. Пришлось экстренно поднимать для них контейнеры на своих VPS и перекидывать DNS.

  • Свои VPS. Тут полегче: apt update && apt install php8.5-cli php8.5-fpm. Но дьявол, как обычно, сидел в php.ini и extension=.

А еще:

  • Отвалились старые IonCube Loader для PHP 7.4, которые зачем‑то тянули старые билды WordPress и Ктулху его сзнает чего еще.

  • Сломался mcrypt на одном древнем биллинге (пришлось в три часа ночи писать враппер вокруг openssl).

  • Composer на паре проектов сошел с ума от того, что в composer.json жестко прибито "php": ">=5.6, <8.0". Пришлось массово резать эти строки через sed.

  • Deprecated‑предупреждения от create_function() и неявных приведений типов забили логи так, что ротация не справлялась.

Из сотни сайтов 68 поднялись на 8.5.12 после простой смены версии в FPM и легкой правки конфигов, ну и пары исправлений в самих скриптах. Это были относительно свежие WordPress, Laravel, OpenCart и самописы. Я прогонял их через grep -R "mysql_", чтобы уж убедиться окончательно. — чисто.

Оставшиеся 32 проекта при переключении на восьмерку выдавали Fatal error: Uncallable function mysql_connect() и много‑много чего еще…

Почему нельзя «просто переписать»

mysql_* — это проклятие легаси. В этих 32 проектах функции mysql_connect, mysql_query, mysql_fetch_assoc размазаны по сотням файлов. Где‑то они обернуты в свои классы, где‑то вызываются напрямую в процедурном лапше‑коде.

Переписать это на PDO или хотя бы mysqli за неделю? Математически невозможно. В одном только проекте старой логистической компании было 400+ вхождений mysql_query. Даже если тратить на замену одной функции с тестированием две минуты (что очень оптимистично, ага), это 800 минуты чистого времени. Почти 14 часов на один проект. А их 32. И это не считая переписывания логики обработки ошибок, так как PDO по умолчанию кидает исключения, а старый код ждет, что mysql_query() просто молча вернет false.

Даунтайм на две недели, пока я буду в одно лицо разгребать этот спагетти‑код, клиент не оплатит. А оставлять их на PHP 5.6, когда в сети уже крутят PoC для свежих CVE — это подписать акт о профессиональном самоубийстве.

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

Анатомия «таблетки»: mysql‑tablet.php

Я не первый, кто столкнулся с этой проблемой, но готовые врапперы вроде mysqlnd-uh или mysqli_compat часто тянут лишнюю магию, которая на PHP 8.5 ведет себя непредсказуемо.

Мне нужен был максимально тупой, прозрачный слой. Один файл, который инклудится в auto_prepend_file в php.ini или.user.ini, ну или вообще просто в скрипт.

Я назвал его mysql-tablet.php. Идея простая: проверить, не объявлены ли функции mysql_* (вдруг хостинг чудит и оставил их для обратной совместимости), и если нет — объявить их поверх mysqli.

Вот как это работает под капотом.

1. Затыкаем исключения и определяем константы

Легаси‑код 2012 года не знает про try-catch. Если mysqli при соединении словит ошибку, он по умолчанию бросит исключение. Старый if (!$link) просто не сработает, скрипт умрет некрасиво.

if (function_exists('mysqli_report') && defined('MYSQLI_REPORT_OFF')) {
     mysqli_report(MYSQLI_REPORT_OFF);
 }

Эта строка переводит mysqli в тихий режим. Теперь он ведет себя как старый mysql: возвращает false и пишет ошибку в свой внутренний лог, а не роняет скрипт.

Дальше — константы типов выборки. В коде везде написано mysql_fetch_array($res, MYSQL_ASSOC). В PHP 8.5 эти константы не определены глобально, если не включено расширение mysql. Пришлось объявить их вручную, маппя на MYSQLI_*:

if (!defined('MYSQL_ASSOC')) {
     define('MYSQL_ASSOC', defined('MYSQLI_ASSOC') ? MYSQLI_ASSOC : 1);
 }

2. Магия mysql_connect

Функция mysql_connect() исторически умеет принимать не только localhost, но и hostname:3306, :/tmp/mysql.sock и даже p:hostname для персистентных соединений. Мой слой должен это распарсить и правильно передать в mysqli_real_connect.

function mysql_connect($server, $username, $password, $new_link = false, $client_flags = 0)
        {
            try {
                if (!function_exists('mysqli_init')) {
                    return false;
                }

                $host = (string) $server;
                $port = null;
                $socket = null;
                $persistentPrefix = '';

                if (str_starts_with($host, 'p:')) {
                    $persistentPrefix = 'p:';
                    $host = substr($host, 2);
                }

                if ($host === '') {
                    $host = 'localhost';
                } elseif (str_starts_with($host, ':')) {
                    $socket = substr($host, 1);
                    $host = 'localhost';
                } elseif (str_contains($host, ':')) {
                    [$hostname, $extra] = explode(':', $host, 2);

                    if ($hostname !== '') {
                        $host = $hostname;
                    }

                    if ($extra !== '') {
                        if ($extra[0] === '/') {
                            $socket = $extra;
                        } else {
                            $port = (int) $extra;
                        }
                    }
                }

                $host = $persistentPrefix . $host;

                $link = mysqli_init();

                if (!$link instanceof mysqli) {
                    return false;
                }

                $ok = @mysqli_real_connect(
                    $link,
                    $host,
                    (string) $username,
                    (string) $password,
                    null,
                    $port,
                    $socket,
                    (int) $client_flags
                );

                if (!$ok) {
                    $GLOBALS['mysql_oldstyle_connect_errno'] = function_exists('mysqli_connect_errno')
                        ? mysqli_connect_errno()
                        : 0;

                    $GLOBALS['mysql_oldstyle_connect_error'] = function_exists('mysqli_connect_error')
                        ? mysqli_connect_error()
                        : '';

                    return false;
                }

                $GLOBALS['mysql_oldstyle_link'] = $link;
                $GLOBALS['mysql_oldstyle_connect_errno'] = 0;
                $GLOBALS['mysql_oldstyle_connect_error'] = '';

                return $link;
            } catch (Throwable $e) {
                $GLOBALS['mysql_oldstyle_connect_errno'] = (int) $e->getCode();
                $GLOBALS['mysql_oldstyle_connect_error'] = $e->getMessage();

                return false;
            }
        }

Почему линк кладется в $GLOBALS['mysql_oldstyle_link']? Потому что в старом коде часто опускают параметр $link в последующих функциях, полагаясь на последнее открытое соединение. Глобал — грязно, но это единственный способ заставить процедурный код 15-летней давности работать без явной передачи хендла.

3. Fetch‑функции и строгая проверка на false

Это самый тонкий момент, на котором ломается большая половина легаси. В старом mysql_fetch_assoc() при достижении конца выборки возвращался false. В mysqli_fetch_assoc() при пустом результате возвращается null.

Код вида while ($row = mysql_fetch_assoc($res)) { ... } на null работает. Но код вида if ($row === false) { /* какая-то спец-логика */ } на null падает.

Поэтому в таблетке я принудительно нормализую результат:

function mysql_fetch_assoc($res) {

    if (!$res instanceof mysqli_result) {
        return false;
    }

    $row = mysqli_fetch_assoc($res);
    return $row === null ? false : $row; 
}

То же самое сделано для mysql_fetch_row и mysql_fetch_array. Мы обманываем старый код, возвращая ему привычный false вместо null.

4. Ошибки соединения

Если mysql_connect() упал, старый код полезет в mysql_errno() и mysql_error(). Но если мы не сохранили ошибку в глобальные переменные, эти функции вернут пустоту.

if (!$ok) {
     $GLOBALS['mysql_oldstyle_connect_errno'] = function_exists('mysqli_connect_errno') ? mysqli_connect_errno() : 0;
    $GLOBALS['mysql_oldstyle_connect_error'] = function_exists('mysqli_connect_error') ? mysqli_connect_error() : ''; return false; 
}

А сами функции mysql_errno() и mysql_error() теперь просто читают эти глобалы, если им не передали конкретный линк.

Грабли и честные ограничения

Сразу предупреждаю: mysql-tablet.php — это не панацея. Это морфий, который позволяет дотащить раненого до операционной, а не вырастить ему новую ногу.

Обратить внимание: Если в коде открыто два соединения через mysql_connect() и функции вызываются без явного указания линка, таблетка будет работать с последним открытым. В 95% случаев легаси использует один линк, но я уже словил пару экзотических систем, где это ломалось.

Еще нужно обратить внимание на:

  • Поведение ошибок. Мы выключили исключения в mysqli, но сами mysql_* функции внутри таблетки все равно могут возвращать false там, где оригинал на PHP 5.6 мог вернуть ресурс с предупреждением. Логи FPM в первую ночь выглядели как новогодняя елка.

  • Безопасность. Самое важное. Эта таблетка НЕ лечит SQL‑инъекции. Если в том самом коде 2012 года переменная $id из $_GET без фильтрации вставляется в mysql_query("SELECT * FROM users WHERE id = $id"), она останется уязвимой. Мы просто перенесли дыру на новый интерпретатор.

  • Производительность. Микросекунды накладных расходов на враппер. На сотне сайтов это незаметно, но под высокой нагрузкой я бы десять раз подумал.

И так. Сайты живут. Уязвимости закрыты. У меня есть 6–12 месяцев относительно спокойной жизни, чтобы в каждом из этих 32 проектов выпилить mysql-tablet.php и перевести код, хотя бы, на mysqli с подготовленными выражениями. Буду делать это спринтами. Скорее всего, по одному‑два проекту в месяц.

Что в итоге

На календаре 24 октября. Все ~100 сайтов работают на PHP 8.5.12. 32 легаси‑зомби поднялись на таблетке за 20 минут деплоя.

Почти во всех проектах, были файлы начинающиеся с <?, а не с <?php. Странно. И ведь я сам это, в большинстве случаев, писал и даже не обращал на это внимания. А на PHP 8.5.12 это важно.

Конечно же null, которым PHP 8+ постоянно давиться. Благо, таких мест было не много и они были исправлены ручками за короткое время. Но, пару седых волос, мне они прибавили все же.

И конечно же count(), который встретился несколько раз, но тоже был подперт костылем из ?? [] и оставлен впокое.

В целом, все прошло, хоть и нервозно, но достаточно стабильно. Я ожидал намного большего ужаса и мрака, чем оказалось в реальности.

Чего и вам желаю!

Ну собственно, вот полный код Таблетки:

Листинг кода
<?php

/**
 * mysql-tablet.php — слой совместимости для легаси-функций mysql_*.
 *
 * Восстанавливает API удалённого в PHP 7.0 расширения ext/mysql,
 * реализуя его поверх mysqli. Позволяет запускать старый код без правок
 * на PHP 8.x, включая 8.5.
 *
 * Требования окружения:
 *  - PHP >= 8.0 (используются str_starts_with() и str_contains());
 *  - включённое расширение mysqli.
 *
 * Побочные эффекты при подключении файла:
 *  - отключается режим исключений mysqli (MYSQLI_REPORT_OFF);
 *  - объявляются константы MYSQL_ASSOC, MYSQL_NUM, MYSQL_BOTH;
 *  - объявляются функции mysql_*, если они отсутствуют в ядре.
 *
 * ВНИМАНИЕ: файл является «костылём» для легаси-кода и не защищает
 * от SQL-инъекций, уже присутствующих в этом коде.
 *
 * @package MysqlTablet
 * @author  Сергей Абрамов и Макс-канал "Веб Мастерская"
 * @version 1.0.0
 * @see     https://www.php.net/manual/ru/book.mysqli.php
 * @see     https://www.php.net/manual/ru/book.mysql.php
 */

/**
 * Отключает выброс исключений mysqli.
 *
 * Легаси-код ожидает «тихий» возврат false и ручную проверку ошибок
 * через mysql_errno() / mysql_error(), поэтому режим исключений
 * (включён по умолчанию в PHP 8.1+) необходимо погасить.
 */
if (function_exists('mysqli_report') && defined('MYSQLI_REPORT_OFF')) {
    mysqli_report(MYSQLI_REPORT_OFF);
}

/**
 * Эмулирует константу ext/mysql MYSQL_ASSOC (ассоциативная выборка).
 *
 * Значение берётся из MYSQLI_ASSOC; при его отсутствии используется
 * историческое числовое значение 1.
 */
if (!defined('MYSQL_ASSOC')) {
    define('MYSQL_ASSOC', defined('MYSQLI_ASSOC') ? MYSQLI_ASSOC : 1);
}

/**
 * Эмулирует константу ext/mysql MYSQL_NUM (нумерованная выборка).
 *
 * Значение берётся из MYSQLI_NUM; при его отсутствии используется
 * историческое числовое значение 2.
 */
if (!defined('MYSQL_NUM')) {
    define('MYSQL_NUM', defined('MYSQLI_NUM') ? MYSQLI_NUM : 2);
}

/**
 * Эмулирует константу ext/mysql MYSQL_BOTH (ассоциативная + нумерованная выборка).
 *
 * Значение берётся из MYSQLI_BOTH; при его отсутствии используется
 * историческое числовое значение 3.
 */
if (!defined('MYSQL_BOTH')) {
    define('MYSQL_BOTH', defined('MYSQLI_BOTH') ? MYSQLI_BOTH : 3);
}

/**
 * Блок объявления функций-заглушек.
 *
 * Функции объявляются только при отсутствии одноимённых в ядре
 * (то есть на PHP >= 7.0, где ext/mysql удалено). Проверка
 * function_exists() защищает от фатальной ошибки повторного объявления.
 */
if (!function_exists('mysql_connect')) {

    /**
     * Открывает соединение с MySQL-сервером (эмуляция mysql_connect()).
     *
     * Поддерживает исторические форматы строки сервера:
     *  - «host:port» — явное указание порта;
     *  - «:/path/to/socket» или «host:/path/to/socket» — UNIX-сокет;
     *  - префикс «p:» — персистентное соединение.
     *
     * Успешное соединение сохраняется в $GLOBALS['mysql_oldstyle_link']
     * и используется остальными функциями как соединение по умолчанию.
     * Ошибки соединения фиксируются в $GLOBALS['mysql_oldstyle_connect_errno']
     * и $GLOBALS['mysql_oldstyle_connect_error'] и доступны через
     * mysql_errno() / mysql_error(). Исключения наружу не пробрасываются.
     *
     * @param  string $server       Хост сервера, опционально «:порт», «:/путь/к/сокету» и/или префикс «p:».
     * @param  string $username     Имя пользователя MySQL.
     * @param  string $password     Пароль пользователя MySQL.
     * @param  bool   $new_link     Не используется; сохранён исключительно для совместимости сигнатуры.
     * @param  int    $client_flags Битовая маска клиентских флагов mysqli (MYSQLI_CLIENT_*).
     *
     * @return mysqli|false Объект соединения либо false при ошибке подключения.
     *
     * @global mysqli|null $mysql_oldstyle_link           Соединение по умолчанию ($GLOBALS['mysql_oldstyle_link']).
     * @global int         $mysql_oldstyle_connect_errno  Код последней ошибки соединения ($GLOBALS['mysql_oldstyle_connect_errno']).
     * @global string      $mysql_oldstyle_connect_error  Текст последней ошибки соединения ($GLOBALS['mysql_oldstyle_connect_error']).
     *
     * @deprecated Только для совместимости с легаси; в новом коде используйте mysqli или PDO.
     * @see        mysqli_real_connect()
     */
    function mysql_connect($server, $username, $password, $new_link = false, $client_flags = 0)
    {
        try {
            if (!function_exists('mysqli_init')) {
                return false;
            }

            $host = (string) $server;
            $port = null;
            $socket = null;
            $persistentPrefix = '';

            // Префикс «p:» означает персистентное соединение в mysqli.
            if (str_starts_with($host, 'p:')) {
                $persistentPrefix = 'p:';
                $host = substr($host, 2);
            }

            // Разбор исторических форматов строки сервера ext/mysql.
            if ($host === '') {
                $host = 'localhost';
            } elseif (str_starts_with($host, ':')) {
                $socket = substr($host, 1);
                $host = 'localhost';
            } elseif (str_contains($host, ':')) {
                [$hostname, $extra] = explode(':', $host, 2);

                if ($hostname !== '') {
                    $host = $hostname;
                }

                if ($extra !== '') {
                    if ($extra[0] === '/') {
                        $socket = $extra;   // Всё, что начинается с «/», — путь к сокету.
                    } else {
                        $port = (int) $extra; // Иначе — номер порта.
                    }
                }
            }

            $host = $persistentPrefix . $host;

            $link = mysqli_init();

            if (!$link instanceof mysqli) {
                return false;
            }

            // «@» глушит warning'и: легаси само проверяет ошибки через mysql_errno().
            $ok = @mysqli_real_connect(
                $link,
                $host,
                (string) $username,
                (string) $password,
                null,
                $port,
                $socket,
                (int) $client_flags
            );

            if (!$ok) {
                $GLOBALS['mysql_oldstyle_connect_errno'] = function_exists('mysqli_connect_errno')
                    ? mysqli_connect_errno()
                    : 0;

                $GLOBALS['mysql_oldstyle_connect_error'] = function_exists('mysqli_connect_error')
                    ? mysqli_connect_error()
                    : '';

                return false;
            }

            $GLOBALS['mysql_oldstyle_link'] = $link;
            $GLOBALS['mysql_oldstyle_connect_errno'] = 0;
            $GLOBALS['mysql_oldstyle_connect_error'] = '';

            return $link;
        } catch (Throwable $e) {
            $GLOBALS['mysql_oldstyle_connect_errno'] = (int) $e->getCode();
            $GLOBALS['mysql_oldstyle_connect_error'] = $e->getMessage();

            return false;
        }
    }

    /**
     * Выбирает базу данных по умолчанию для соединения (эмуляция mysql_select_db()).
     *
     * @param  string        $database_name Имя базы данных.
     * @param  mysqli|null   $link          Соединение; null — соединение по умолчанию из $GLOBALS.
     *
     * @return bool true при успехе, false при ошибке или невалидном соединении.
     *
     * @global mysqli|null $mysql_oldstyle_link Соединение по умолчанию ($GLOBALS['mysql_oldstyle_link']).
     *
     * @deprecated Только для совместимости с легаси; в новом коде используйте mysqli или PDO.
     * @see        mysqli_select_db()
     */
    function mysql_select_db($database_name, $link = null)
    {
        if ($link === null) {
            $link = $GLOBALS['mysql_oldstyle_link'] ?? null;
        }

        if (!$link instanceof mysqli) {
            return false;
        }

        return mysqli_select_db($link, (string) $database_name);
    }

    /**
     * Выполняет SQL-запрос (эмуляция mysql_query()).
     *
     * @param  string      $sql  Текст SQL-запроса.
     * @param  mysqli|null $link Соединение; null — соединение по умолчанию из $GLOBALS.
     *
     * @return mysqli_result|bool Объект результата для SELECT-подобных запросов,
     *                            true для успешных запросов без результата,
     *                            false при ошибке или невалидном соединении.
     *
     * @global mysqli|null $mysql_oldstyle_link Соединение по умолчанию ($GLOBALS['mysql_oldstyle_link']).
     *
     * @deprecated Только для совместимости с легаси; в новом коде используйте mysqli или PDO.
     * @see        mysqli_query()
     */
    function mysql_query($sql, $link = null)
    {
        if ($link === null) {
            $link = $GLOBALS['mysql_oldstyle_link'] ?? null;
        }

        if (!$link instanceof mysqli) {
            return false;
        }

        return mysqli_query($link, (string) $sql);
    }

    /**
     * Выбирает следующую строку результата в виде нумерованного массива (эмуляция mysql_fetch_row()).
     *
     * В отличие от mysqli_fetch_row() возвращает false вместо null при выходе
     * за конец результата — именно на такое поведение рассчитан легаси-код.
     *
     * @param  mysqli_result|mixed $res Результат запроса, возвращённый mysql_query().
     *
     * @return array<int, string|null>|false Строка результата либо false, если строк больше нет
     *                                       или передан не-результат.
     *
     * @deprecated Только для совместимости с легаси; в новом коде используйте mysqli или PDO.
     * @see        mysqli_fetch_row()
     */
    function mysql_fetch_row($res)
    {
        if (!$res instanceof mysqli_result) {
            return false;
        }

        $row = mysqli_fetch_row($res);

        return $row === null ? false : $row;
    }

    /**
     * Выбирает следующую строку результата в виде ассоциативного массива (эмуляция mysql_fetch_assoc()).
     *
     * В отличие от mysqli_fetch_assoc() возвращает false вместо null при выходе
     * за конец результата.
     *
     * @param  mysqli_result|mixed $res Результат запроса, возвращённый mysql_query().
     *
     * @return array<string, string|null>|false Строка результата либо false, если строк больше нет
     *                                          или передан не-результат.
     *
     * @deprecated Только для совместимости с легаси; в новом коде используйте mysqli или PDO.
     * @see        mysqli_fetch_assoc()
     */
    function mysql_fetch_assoc($res)
    {
        if (!$res instanceof mysqli_result) {
            return false;
        }

        $row = mysqli_fetch_assoc($res);

        return $row === null ? false : $row;
    }

    /**
     * Выбирает следующую строку результата в заданном формате (эмуляция mysql_fetch_array()).
     *
     * Исторические константы MYSQL_ASSOC / MYSQL_NUM / MYSQL_BOTH прозрачно
     * отображаются на MYSQLI_ASSOC / MYSQLI_NUM / MYSQLI_BOTH.
     * Возвращает false вместо null при выходе за конец результата.
     *
     * @param  mysqli_result|mixed $res         Результат запроса, возвращённый mysql_query().
     * @param  int|null            $result_type Одна из констант MYSQL_ASSOC, MYSQL_NUM, MYSQL_BOTH;
     *                                          null эквивалентен MYSQL_BOTH.
     *
     * @return array<int|string, string|null>|false Строка результата либо false, если строк больше нет
     *                                               или передан не-результат.
     *
     * @deprecated Только для совместимости с легаси; в новом коде используйте mysqli или PDO.
     * @see        mysqli_fetch_array()
     */
    function mysql_fetch_array($res, $result_type = null)
    {
        if (!$res instanceof mysqli_result) {
            return false;
        }

        if ($result_type === null) {
            $result_type = MYSQL_BOTH;
        }

        $result_type = (int) $result_type;

        // Маппинг исторических констант ext/mysql на константы mysqli.
        $map = [
            MYSQL_ASSOC => MYSQLI_ASSOC,
            MYSQL_NUM   => MYSQLI_NUM,
            MYSQL_BOTH  => MYSQLI_BOTH,
        ];

        $mode = $map[$result_type] ?? MYSQLI_BOTH;

        $row = mysqli_fetch_array($res, $mode);

        return $row === null ? false : $row;
    }

    /**
     * Выбирает следующую строку результата в виде объекта (эмуляция mysql_fetch_object()).
     *
     * Возвращает false вместо null при выходе за конец результата.
     * Ошибки создания экземпляра класса перехватываются и также приводят к false.
     *
     * @param  mysqli_result|mixed $res       Результат запроса, возвращённый mysql_query().
     * @param  string              $classname Имя класса создаваемого объекта.
     * @param  array<int, mixed>   $params    Аргументы конструктора класса.
     *
     * @return object|false Экземпляр $classname с полями строки либо false, если строк больше нет,
     *                      передан не-результат или не удалось создать объект.
     *
     * @deprecated Только для совместимости с легаси; в новом коде используйте mysqli или PDO.
     * @see        mysqli_fetch_object()
     */
    function mysql_fetch_object($res, $classname = 'stdClass', $params = [])
    {
        if (!$res instanceof mysqli_result) {
            return false;
        }

        try {
            $object = mysqli_fetch_object(
                $res,
                (string) $classname,
                (array) $params
            );
        } catch (Throwable $e) {
            return false;
        }

        return $object === null ? false : $object;
    }

    /**
     * Возвращает число строк, затронутых последним запросом (эмуляция mysql_affected_rows()).
     *
     * @param  mysqli|null $link Соединение; null — соединение по умолчанию из $GLOBALS.
     *
     * @return int Число затронутых строк; -1 при ошибке запроса или невалидном соединении.
     *
     * @global mysqli|null $mysql_oldstyle_link Соединение по умолчанию ($GLOBALS['mysql_oldstyle_link']).
     *
     * @deprecated Только для совместимости с легаси; в новом коде используйте mysqli или PDO.
     * @see        mysqli_affected_rows()
     */
    function mysql_affected_rows($link = null)
    {
        if ($link === null) {
            $link = $GLOBALS['mysql_oldstyle_link'] ?? null;
        }

        if (!$link instanceof mysqli) {
            return -1;
        }

        return mysqli_affected_rows($link);
    }

    /**
     * Возвращает ID строки, вставленной последним запросом (эмуляция mysql_insert_id()).
     *
     * @param  mysqli|null $link Соединение; null — соединение по умолчанию из $GLOBALS.
     *
     * @return int|string ID последней вставленной строки (string для значений BIGINT);
     *                    0 при невалидном соединении.
     *
     * @global mysqli|null $mysql_oldstyle_link Соединение по умолчанию ($GLOBALS['mysql_oldstyle_link']).
     *
     * @deprecated Только для совместимости с легаси; в новом коде используйте mysqli или PDO.
     * @see        mysqli_insert_id()
     */
    function mysql_insert_id($link = null)
    {
        if ($link === null) {
            $link = $GLOBALS['mysql_oldstyle_link'] ?? null;
        }

        if (!$link instanceof mysqli) {
            return 0;
        }

        return mysqli_insert_id($link);
    }

    /**
     * Возвращает код последней ошибки (эмуляция mysql_errno()).
     *
     * Если соединение отсутствует или невалидно, отдаёт код ошибки соединения,
     * сохранённый функцией mysql_connect() в $GLOBALS.
     *
     * @param  mysqli|null $link Соединение; null — соединение по умолчанию из $GLOBALS.
     *
     * @return int Код последней ошибки; 0, если ошибок не было.
     *
     * @global mysqli|null $mysql_oldstyle_link          Соединение по умолчанию ($GLOBALS['mysql_oldstyle_link']).
     * @global int         $mysql_oldstyle_connect_errno Код последней ошибки соединения ($GLOBALS['mysql_oldstyle_connect_errno']).
     *
     * @deprecated Только для совместимости с легаси; в новом коде используйте mysqli или PDO.
     * @see        mysqli_errno()
     */
    function mysql_errno($link = null)
    {
        if ($link === null) {
            $link = $GLOBALS['mysql_oldstyle_link'] ?? null;
        }

        if (!$link instanceof mysqli) {
            return (int) ($GLOBALS['mysql_oldstyle_connect_errno'] ?? 0);
        }

        return mysqli_errno($link);
    }

    /**
     * Возвращает текст последней ошибки (эмуляция mysql_error()).
     *
     * Если соединение отсутствует или невалидно, отдаёт текст ошибки соединения,
     * сохранённый функцией mysql_connect() в $GLOBALS.
     *
     * @param  mysqli|null $link Соединение; null — соединение по умолчанию из $GLOBALS.
     *
     * @return string Текст последней ошибки; пустая строка, если ошибок не было.
     *
     * @global mysqli|null $mysql_oldstyle_link          Соединение по умолчанию ($GLOBALS['mysql_oldstyle_link']).
     * @global string      $mysql_oldstyle_connect_error Текст последней ошибки соединения ($GLOBALS['mysql_oldstyle_connect_error']).
     *
     * @deprecated Только для совместимости с легаси; в новом коде используйте mysqli или PDO.
     * @see        mysqli_error()
     */
    function mysql_error($link = null)
    {
        if ($link === null) {
            $link = $GLOBALS['mysql_oldstyle_link'] ?? null;
        }

        if (!$link instanceof mysqli) {
            return (string) ($GLOBALS['mysql_oldstyle_connect_error'] ?? '');
        }

        return mysqli_error($link);
    }

    /**
     * Возвращает число строк в результате запроса (эмуляция mysql_num_rows()).
     *
     * @param  mysqli_result|mixed $res Результат запроса, возвращённый mysql_query().
     *
     * @return int|false Число строк в результате либо false, если передан не-результат.
     *
     * @deprecated Только для совместимости с легаси; в новом коде используйте mysqli или PDO.
     * @see        mysqli_num_rows()
     */
    function mysql_num_rows($res)
    {
        if (!$res instanceof mysqli_result) {
            return false;
        }

        return mysqli_num_rows($res);
    }

    /**
     * Освобождает память, занятую результатом запроса (эмуляция mysql_free_result()).
     *
     * @param  mysqli_result|mixed $res Результат запроса, возвращённый mysql_query().
     *
     * @return bool true, если память освобождена; false, если передан не-результат.
     *
     * @deprecated Только для совместимости с легаси; в новом коде используйте mysqli или PDO.
     * @see        mysqli_free_result()
     */
    function mysql_free_result($res)
    {
        if (!$res instanceof mysqli_result) {
            return false;
        }

        mysqli_free_result($res);

        return true;
    }

    /**
     * Закрывает соединение с сервером (эмуляция mysql_close()).
     *
     * Если закрывается соединение по умолчанию, оно дополнительно удаляется
     * из $GLOBALS, чтобы остальные функции не использовали «мёртвый» линк.
     * Закрытие невалидного соединения считается успешным (true), как в ext/mysql.
     *
     * @param  mysqli|null $link Соединение; null — соединение по умолчанию из $GLOBALS.
     *
     * @return bool true при успешном закрытии (или невалидном соединении), false при ошибке.
     *
     * @global mysqli|null $mysql_oldstyle_link Соединение по умолчанию ($GLOBALS['mysql_oldstyle_link']).
     *
     * @deprecated Только для совместимости с легаси; в новом коде используйте mysqli или PDO.
     * @see        mysqli_close()
     */
    function mysql_close($link = null)
    {
        if ($link === null) {
            $link = $GLOBALS['mysql_oldstyle_link'] ?? null;
        }

        if (!$link instanceof mysqli) {
            return true;
        }

        $result = mysqli_close($link);

        if (isset($GLOBALS['mysql_oldstyle_link']) && $GLOBALS['mysql_oldstyle_link'] === $link) {
            unset($GLOBALS['mysql_oldstyle_link']);
        }

        return $result;
    }

    /**
     * Экранирует специальные символы в строке для использования в SQL (эмуляция mysql_real_escape_string()).
     *
     * @param  string      $sql  Исходная строка.
     * @param  mysqli|null $link Соединение; null — соединение по умолчанию из $GLOBALS.
     *
     * @return string|false Экранированная строка либо false при невалидном соединении.
     *
     * @global mysqli|null $mysql_oldstyle_link Соединение по умолчанию ($GLOBALS['mysql_oldstyle_link']).
     *
     * @deprecated Только для совместимости с легаси; в новом коде используйте подготовленные выражения mysqli/PDO.
     * @see        mysqli_real_escape_string()
     */
    function mysql_real_escape_string($sql, $link = null)
    {
        if ($link === null) {
            $link = $GLOBALS['mysql_oldstyle_link'] ?? null;
        }

        if (!$link instanceof mysqli) {
            return false;
        }

        return mysqli_real_escape_string($link, (string) $sql);
    }

    /**
     * Возвращает версию MySQL-сервера (эмуляция mysql_get_server_info()).
     *
     * @param  mysqli|null $link Соединение; null — соединение по умолчанию из $GLOBALS.
     *
     * @return string|false Строка версии сервера либо false при невалидном соединении.
     *
     * @global mysqli|null $mysql_oldstyle_link Соединение по умолчанию ($GLOBALS['mysql_oldstyle_link']).
     *
     * @deprecated Только для совместимости с легаси; в новом коде используйте mysqli или PDO.
     * @see        mysqli_get_server_info()
     */
    function mysql_get_server_info($link = null)
    {
        if ($link === null) {
            $link = $GLOBALS['mysql_oldstyle_link'] ?? null;
        }

        if (!$link instanceof mysqli) {
            return false;
        }

        return mysqli_get_server_info($link);
    }

    /**
     * Устанавливает кодировку соединения (эмуляция mysql_set_charset()).
     *
     * @param  string      $charset          Имя кодировки (например, «utf8mb4»).
     * @param  mysqli|null $link_identifier  Соединение; null — соединение по умолчанию из $GLOBALS.
     *
     * @return bool true при успехе, false при ошибке или невалидном соединении.
     *
     * @global mysqli|null $mysql_oldstyle_link Соединение по умолчанию ($GLOBALS['mysql_oldstyle_link']).
     *
     * @deprecated Только для совместимости с легаси; в новом коде используйте mysqli или PDO.
     * @see        mysqli_set_charset()
     */
    function mysql_set_charset($charset, $link_identifier = null)
    {
        if ($link_identifier === null) {
            $link_identifier = $GLOBALS['mysql_oldstyle_link'] ?? null;
        }

        if (!$link_identifier instanceof mysqli) {
            return false;
        }

        return mysqli_set_charset($link_identifier, (string) $charset);
    }
}

?>