Хороший вопрос. Как раз показывает убогость стандартной системы плейсхолдеров.
В моей
библиотеке для работы c MySQL тип указывается самым простым и эффективным способом - прямо в плейсхолдере:
//первый запрос (лимит здесь не нужен)
$id = $db->getOne("SELECT id FROM users WHERE mail = ?s AND pass = ?s", $mail, $pass);
// второй запрос
$sql = "SELECT id, name FROM news WHERE category = ?s AND subcategory = ?s LIMIT ?i";
$news = $db->getIndCol('id', $sql, $cat, $subcat, $limit);
Как видно из этих примеров, тип ставится в самом плейсхолдере и все входящие данные обрабатываются корректно.
Вернемся теперь к несчастному PDO.
Цикл с подстановкой в bindParam?
Цикл, увы, не поможет. Потому что мы не знаем, какой тип использовать для привязки. То есть, все сведется к дефолтному PARAM_STR и в итоге мы получим то же самое execute() с массивом, только в профиль.
Если вдруг возникнет идея определять тип по составу переменной, то делать это
НИ В КОЕМ СЛУЧАЕ НЕЛЬЗЯ. Если бы мог, я бы выделил ещё большим шрифтом и красным цветом. Потому что практически каждый продвинутый пользователь похапе рано или поздно наступает на эти грабли. Если число, хранящееся в MySQL, всегда можно безопасно сравнивать со строкой, то наоборот - это будет катастрофа: MySQL будет пытаться привести содержимое поля к числу. То есть, если взять пример из вопроса, и пытаться определить тип привязки по содержимому переменной, то при введенном пароле 12345 ctype_digit() скажет нам использовать INT и в итоге
пароль 12345 подойдет к любому паролю вида "12345буквы".
Так что цикл - не вариант.
Я читал, что можно еще отключить режим эмуляции, но я не знаю, как это повлияет на безопасность
Режим эмуляции на безопасность не влияет. Некоторые неграмотные разработчики считают, что отключение режима эмуляции повышает безопасность, но это неправда. В обоих вариантах PDO работает одинаково безопасно.
В любом случае, отключение эмуляции действительно решает проблему. Так что в данном случае это будет самое простое решение.
Так что либо в параметрах DSN, либо с помощью
$pdo->setAttribute(PDO::ATTR_EMULATE_PREPARES, FALSE);
отключаем эмуляцию и наслаждается лимитом (правда, в этом случае перестанет работать такая фича, как несколько именованных плейсхолдеров с одним и тем же именем, но тут уж приходится выбирать).
И последнее замечание.
Функцию для выполнения запросов писать в общем-то нет смысла. PDO достаточно лаконичен и сам по себе. Единственное, что мешает писать однострочники - это дурацкая execute(), которая возвращает не себя, а булево значение. Но это легко исправить, и в итоге код получится ненамного длиннее, чем при вызове функции, но гораздо более гибким (ненавижу скроллинг, поэтому выношу параметры на другие строчки):
// с функцией
$sql = 'SELECT `user_pass` FROM `users` WHERE `user_mail` = :mail LIMIT :lim';
$param = array(':mail' => 'vlad-dub1994@mail.ru', ':lim' => 1);
$data = select($sql,$data);
// с патченым PDO
$data = DB::prepare($sql)->execute($data)->fetch();
// или другой вариант записи
$data = DB::prepare('SELECT user_pass FROM users WHERE user_mail = :mail LIMIT :lim')
->execute([':mail' => 'vlad-dub1994@mail.ru', ':lim' => 1])
->fetch();
Всего на пару слов больше, но зато можно исполнять любые запросы (INSERT к примеру):
$sql = 'INSERT INTO users VALUES(?,?,?)';
$param = array(NULL, 'vlad-dub1994@mail.ru', 'pass');
DB::prepare($sql)->execute($data); //OK
select($sql,$data); // ошибка из-за fetch()
и использовать любые варианты получения данных, которые поддерживает PDO:
$sql = 'SELECT id FROM tree WHERE parent_id=?';
$subcat = DB::prepare($sql)->execute([$parent])->fetchAll(PDO::FETCH_COLUMN);