Ошибка 500 PDO LIKE bindParam в PHP 8.4: как исправить
Решение проблемы с PDO LIKE и bindParam после обновления до PHP 8.4: 500 ошибка из-за передачи выражения в bindParam. Используйте bindValue, переменную $like или execute с массивом. Примеры кода и отладка.
Проблема с PDO LIKE и bindParam после обновления до PHP 8.4: запрос возвращает 500 ошибку
Есть сортировка по буквам брендов часов. При клике на букву (например, ‘H’) через jQuery отправляется AJAX POST-запрос на /admin/sortbya с параметром name равным этой букве.
Код jQuery:
$(document).on('click', ".jq-char", function() {
bk = $(this).html();
$.ajax({
url: '/admin/sortbya',
type: 'POST',
data: { name: bk },
cache: false,
success: function (data) {
console.log(data);
$('.table-news').html(data);
}
});
});
В PHP обработчик вызывает метод:
public static function allA($name)
{
$db = Db::getConnection();
$sql = 'SELECT * FROM watch WHERE watch_brand like :name and sold !="yes"';
$result = $db->prepare($sql);
$result->bindParam(':name', "$name%", PDO::PARAM_STR);
$result->setFetchMode(PDO::FETCH_ASSOC);
$result->execute();
return $result->fetchAll();
}
До обновления с PHP 7 всё работало корректно: запрос возвращал бренды, начинающиеся на указанную букву. Прямой SQL-запрос в БД работает. Если в функции просто вывести данные без PDO-запроса, ошибок нет. В чём причина проблемы с LIKE :name после перехода на PHP 8.4? Как исправить bindParam для LIKE с суффиксом ‘%’?
Проблема в том, что PDOStatement::bindParam привязывает переменную по ссылке, а в PHP 8.1+ передача выражения/литерала (например "like = $name . ‘%’) и привязать её, либо использовать bindValue(), либо передать значение через execute([‘:name’ => $name . ‘%’]). Для отладки включите режим ошибок PDO (ERRMODE_EXCEPTION), чтобы увидеть реальное исключение.
Содержание
- Краткое объяснение проблемы
- Почему bindParam вызывает 500 в PHP 8.4
- Три рабочих решения для PDO LIKE и bindParam
- Рекомендации: ERRMODE, EMULATE_PREPARES и безопасность
- Как тестировать и отлаживать (видимость 500)
- FAQ
- Источники
- Заключение
Краткое объяснение проблемы
Ваш код вызывает
$result->bindParam(':name', "$name%", PDO::PARAM_STR);
и это выглядело нормально на PHP 7, но после обновления до PHP 8.4 приводит к 500. Дело в том, что bindParam ожидает переменную, потому что привязка выполняется по ссылке и значение читается в момент execute(). Передача выражения/литерала (например “$name%”) — не переменная — приводит к ошибке типа “Cannot pass parameter 2 by reference” / “Only variables can be passed by reference”. См. документацию по bindParam для деталей: https://www.php.net/manual/en/pdostatement.bindparam.php.
Почему bindParam вызывает 500 в PHP 8.4
- bindParam привязывает именно переменную (ссылка), а не копию значения. Значение будет взято при вызове execute(). Это ключевой момент: bindParam требует реальную переменную, а не выражение или строковый литерал. Подробно — в мануале PHP: https://www.php.net/manual/en/pdostatement.bindparam.php.
- Передача выражения вроде “$name%” даёт ошибку передачи по ссылке; это часто документировано и обсуждалось в сообществах: пример обсуждения ошибки — https://stackoverflow.com/questions/31179556/bindparam-causes-a-fatal-error.
- Для LIKE‑шаблонов нельзя писать ‘%:keyword%’ в SQL и надеяться, что плейсхолдер развернётся внутри строки; правильный подход — оставить плейсхолдер чистым и добавить ‘%’ в значение, либо использовать bindValue/execute. См. практический разбор: https://stackoverflow.com/questions/11068230/using-like-in-bindparam-for-a-mysql-pdo-query и сравнение bindParam vs bindValue: https://sqlpey.com/php/pdo-bindparam-vs-bindvalue-behavior/.
Коротко: PHP 8 строже относился к передачим по ссылке/временным значениям, поэтому выражение “$name%” при bindParam теперь даёт фатал.
Три рабочих решения для PDO LIKE и bindParam
Ниже — три корректных способа, отсортированы по простоте и распространённости.
- bindValue() — копирует значение сразу (рекомендуется для одиночного запроса)
$sql = 'SELECT * FROM watch WHERE watch_brand LIKE :name AND sold != "yes"';
$stmt = $db->prepare($sql);
$stmt->bindValue(':name', $name . '%', PDO::PARAM_STR);
$stmt->execute();
$rows = $stmt->fetchAll(PDO::FETCH_ASSOC);
Плюс: можно передавать выражение ($name . ‘%’) напрямую, так как bindValue принимает значение, а не ссылку.
- bindParam() с заранее сформированной переменной
$sql = 'SELECT * FROM watch WHERE watch_brand LIKE :name AND sold != "yes"';
$like = $name . '%';
$stmt = $db->prepare($sql);
$stmt->bindParam(':name', $like, PDO::PARAM_STR);
$stmt->execute();
$rows = $stmt->fetchAll(PDO::FETCH_ASSOC);
Плюс: bindParam удобен при повторных execute() в цикле (меняете $like и вызываете execute() снова). Главное — передавать именно переменную.
- Передача значения в execute() — самый короткий и часто используемый вариант
$sql = 'SELECT * FROM watch WHERE watch_brand LIKE :name AND sold != "yes"';
$stmt = $db->prepare($sql);
$stmt->execute([':name' => $name . '%']);
$rows = $stmt->fetchAll(PDO::FETCH_ASSOC);
Плюс: компактно, читабельно, безопасно.
Важно: не писать в SQL ‘%:name%’ — плейсхолдеры не разворачиваются внутри строковых литералов, поэтому wildcard должен быть в значении, а не в SQL-литерале. Пример обсуждения: https://stackoverflow.com/questions/11068230/using-like-in-bindparam-for-a-mysql-pdo-query.
Рекомендации: ERRMODE, EMULATE_PREPARES и безопасность
- Включите исключения для PDO, чтобы 500 превращался в понятное исключение во время разработки:
$db->setAttribute(PDO::ATTR_ERRMODE, PDO::ERRMODE_EXCEPTION);
- Эмуляция подготовленных выражений:
$db->setAttribute(PDO::ATTR_EMULATE_PREPARES, false); // предпочтительно
Иногда включение эмуляции (true) меняет поведение драйвера и может скрыть проблему, но это влияет на типизацию и безопасность — использовать как временный обходной путь, а не как постоянное решение.
- Экранирование символов для LIKE: если пользователь может вводить %, _, или , и вы хотите искать буквальные символы, экранируйте их:
$raw = $name;
$raw = str_replace(['\\','%','_'], ['\\\\','\\%','\_'], $raw);
$like = $raw . '%';
$stmt->bindValue(':name', $like, PDO::PARAM_STR);
Или используйте SQL ESCAPE, если нужно.
Как тестировать и отлаживать (видимость 500)
- На dev‑сервере включите отображение ошибок (display_errors=On) и режим исключений PDO. Либо поймайте исключение:
try {
$stmt = $db->prepare($sql);
$stmt->execute([':name' => $name . '%']);
$rows = $stmt->fetchAll(PDO::FETCH_ASSOC);
} catch (PDOException $e) {
error_log($e->getMessage());
http_response_code(500);
echo 'DB error: ' . $e->getMessage(); // в dev; в prod логируйте и не выводите детали
}
- Проверьте логи веб‑сервера (nginx/apache) и PHP‑error log: часто там будет точный текст ошибки — “Cannot pass parameter 2 by reference” и трейс.
- Если AJAX получает пустой ответ 500, откройте Network → Response в DevTools, чтобы увидеть сообщение сервера (если вы выводите его), или смотрите server logs.
FAQ
В: Почему в PHP 7 этот трюк работал?
О: Возможные причины — разная конфигурация PDO (эмуляция подготовленных выражений), отличия драйвера базы или настройки отображения/логирования ошибок; иногда в старой среде ошибка просто не проявлялась из‑за других настроек. Тем не менее корректный код всегда должен передавать переменную в bindParam или использовать bindValue/execute.
В: Можно ли использовать bindParam в цикле?
О: Да. bindParam полезен, если вы хотите менять значение переменной и вызывать execute() многократно (переменная будет прочитана при каждом execute()).
В: Что безопаснее — bindValue или execute([…])?
О: Оба безопасны, т.к. параметры передаются отдельно от SQL. execute([…]) — самый компактный и часто используемый способ; bindValue полезен, если хотите явно указать тип (PDO::PARAM_INT и т.д.).
В: Как экранировать % и _ в LIKE?
О: Замените % и _ на ‘%’ и ‘_’ (и корректно экранируйте слэши), либо используйте ESCAPE в SQL.
Источники
- PHP Manual - PDOStatement::bindParam
- Stack Overflow — Using LIKE in bindParam for a MySQL PDO Query
- PHP PDO bindParam vs bindValue - Behavior (sqlpey)
- Stack Overflow — bindParam causes a fatal error
Заключение
Коротко: проблема в том, что bindParam привязывает переменную по ссылке, а передача выражения “$name%” в качестве аргумента даёт фатал в PHP 8.4. Самые простые исправления — использовать bindValue(), сформировать переменную $like = $name . ‘%’ и привязать её через bindParam, или передать значение напрямую в execute([‘:name’ => $name . ‘%’]). Не забудьте включить PDO::ERRMODE_EXCEPTION для нормальной отладки и следить за настройкой PDO::ATTR_EMULATE_PREPARES. Эти шаги решат 500‑ошибку и восстановят корректный поиск по LIKE в вашей сортировке брендов.