Вечером 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);
    }
}

?>

Комментарии (13)


  1. kirik
    24.10.2026 19:14

    Версии PHP — от гордого 8.3 на свежих проектах до музейных экспонатов

    если это так, то вас, возможно, ждут множество сюрпризов, если "музейный экспонат" это хотя бы 7.4. Одной лишь "таблеткой" не обойтись.


  1. NotSlow
    24.10.2026 19:14

    Или как вариант можно было просто пользоваться не "убитым", а нормальным shared'ом, где в тот же вечер 21го 8.2-8.5 уже работали свежие. А через день-два и все до 5.4 было пропатчено.

    Перескочить так резко на 8.5 под воздействием волшебного пендаля конечно похвально, но уверен, что дальше все также будет заброшено. Или с релизом 8.6 все хозяйство снова резко на нее переключится?


  1. TheRaven
    24.10.2026 19:14

    Мне не нужно (у меня большинство правок кода случилось еще на переезде на 7.х, а потом на 8.2), но спасибо что поделились своим "морфием".


  1. kolabaister
    24.10.2026 19:14

    Но ведь фиксы выпустили для большинства версий php, даже для 7.4


  1. dyadyaSerezha
    24.10.2026 19:14

    дышали на ладан под PHP 5.4 и 5.6

    То есть, со времен 5.4 не было найдено ни одной критической уязвимости и эти сайты спокойно работали, а тут впервые нашли такой ужас-ужас, что сразу надо немедленно всё обновлять? Или вы что-то не договариваете и в поток обычного многолетнего раздолбайства вдруг вклинился неожиданный героический порыв всё немедленно пофиксить?


    1. redfox0
      24.10.2026 19:14

      Скорее подвернулась отличная оплачиваемая работёнка на не один месяц.


  1. janvarev
    24.10.2026 19:14

    В момент перехода на PHP 7 (когда выпилили mysql) такие полифиллы уже писали, вспомнил, например, такой https://github.com/dshafik/php7-mysql-shim

    Просто вероятнее, он лучше оттестирован.


  1. redfox0
    24.10.2026 19:14

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


  1. alexalexes
    24.10.2026 19:14

    $link = $GLOBALS['mysql_oldstyle_link'] ?? null;

    А вы точно тестировали этот код в PHP ниже 7-й версиях?

    В PHP 5.6 точно не будет парситься клауза ?? - поправьте, пожалуйста, синтаксис на развернутый вариант, чтобы с 5.6 можно было переехать.

    PS: Если нейронке не сказать, какая версия PHP используется по нижней границе, она будет наровить вставить типы данных в описание функции и использовать ?? без обратной совместимости со старыми версиями PHP.


  1. Ptktysq
    24.10.2026 19:14

    Всего лишь подключение к БД? Везунчик!
    Под php 5.4 прекрасно работал код, написанный под php 4. Например, доступ к байтам строки через индекс в фигурных скобках. Или create_function — чем он им не угодил? Это же обычный eval, в каждом скриптовом языке есть. И конечно никаких тестов в старом коде не было. Что еще вылезет после перехода на свежий php...


  1. Ptktysq
    24.10.2026 19:14

    А вот вам еще одно отличие mysql от mysqli: первый не терял свой link_id при сборе мусора, а ваша переменная будет уничтожена. Где сломается? — Сохранение данных сессии в БД! Костылики нужны: либо замыкание в функции записи сессии, либо переподключаться.


  1. XanderBass
    24.10.2026 19:14

    IMHO. Когда на обслуживании десятки проектов, есть смысл немного жёстче работать с заказчиками. И в частности ставить условие: либо код проекта обновляется с "музейной" версии хотя бы на более-менее "живую", либо проект снимается с поддержки. Серьёзно, "кушать кактус" с каждым годом будет всё дороже для самого разработчика.


    1. YourWriter Автор
      24.10.2026 19:14

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