The Jerusalem PostShe made aliyah at 16 - today, she's teaching the next generation about AIESPN🏈 NFL Week 7 predictions: Upsets, fantasy sleepers and moreESPN DeportesValencia tuvo el segundo, se lo anularon y el Villarreal le empatRTP DesportoTiago Pereira conquista título de pares no Lisboa Belém OpenZDF heuteAktuelle Pressemitteilungen des ZDF20 Minuten«Mein Bruder wollte mir seine Ex-Frau schlechtreden»EngadgetSteam rolls out Personal Calendar feature that constantly updates recommended gamesCBS NewsPolice and protesters clash in France after day of demonstrationsThe Sydney Morning HeraldYoung investors love AI stocks. Are they falling into a debt trap?The South AfricanKaizer Chiefs make decision on Da Cruz’s future after CAF exitOnet"Z partyzanta dzwonię". Policjantka miała ostrzec podejrzanego w sprawie zoo w ŁącznejFutura SciencesPatient bizarre : il arrive aux urgences avec un mal de ventre, la radio révèle une masse métallique dans son estomac !
The Daily Newsstand · Free, Always
Saturday, October 24, 2026

Экстренный рефакторинг 32 проектов с mysql_* на PHP 8.5 без единой строчки правок в бизнес-логике

Translate

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

?>
View the original on Хабр →

KioskNews shows a cleaned-up reading view extracted from the publisher’s page — the original always lives on their site, not ours.