• Как сделать prepare запрос без bind_params?

    ipatiev
    @ipatiev
    Потомок старинного рода Ипатьевых-Колотитьевых
    Сначала я вообще ничего не понял, но поразмыслив, разобрался, про что этот вопрос.
    И поскольку он повторяет весьма распространённые заблуждения, то будет полезно на него подробно ответить.

    Можно ли как-то безопасно сделать такой запрос, который я соберу в строку?

    Нет, нельзя.

    Это распространённое, но очень смешное заблуждение. Если подумать, то само по себе prepare() ни от чего не защищает. Это не волшебное слово типа "Экспекто патронум!" - написал, и дальше уже можно не париться, оно само как-то магически защитит от инъекций. В реальной жизни таких слов ещё, увы, не придумали. А защищает именно замена актуальных переменных в запросе на знаки подстановки и последующая привязка через bind_param. А prepare просто говорит базе данных ,что запрос еще рано исполнять, это только схема запроса, а данные для него приедут позже.

    Я так понял, что если я выполняю $sql = $mysqli->prepare($request), то выполнится только один запрос, который первый, даже, если в него вложен будет второй, пусть я его строкой и засунул.

    Это тоже очень распространённое, и очень вредное заблуждение. Запрос выполнится только один, да. Тут всё верно. Но проблема в том, что SQL инъекция - это не один запрос
    INSERT INTO students (name) VALUES ('Robert');DROP TABLE students;
    , который рисуют в примерах для первоклассников. Инъекция - это любой посторонний код в запросе, вообще любой.

    Поэтому собирать в строку надо всё равно запрос со знаками подстановки, а потом его выполнить. Благо, в 5.6 версии пхп появился оператор распаковки параметров, который значительно облегчил эту задачу.

    Не хочется использовать лишние переменные и обработку для типов данных

    Без переменных тут не обойтись, но вот с типами данных можно не запариваться. Исторически mysql всегда спокойно принимает строки для любых типов данных. Исключений настолько мало, что ими можно пренебречь.

    Для запроса, который приведен в примере, код будет относительно простым, поскольку заменяется на вызов IN:

    $names = ["Stepan" , "Stiv"];
    $in    = str_repeat('?,', count($names) - 1) . '?'; // получаем строку вида "?,?"
    $sql   = "SELECT * FROM users WHERE name IN ($in)"; // поставляем её в SQL
    $stmt  = $mysqli->prepare($sql); // prepare
    $types = str_repeat('s', count($names)); // получаем строку "sss" по количеству переменных
    $stmt->bind_param($types, ...$names); // привязываем наш массив
    $stmt->execute(); // выполняем
    $result = $stmt->get_result(); // дальше всё как обычно
    $data = $result->fetch_all(MYSQLI_ASSOC);


    Для более сложных запросов принцип будет тот же: собираем строку с плейсхолдерами, а переменные для неё - в массив. И потом выполняем так же через bind_param($types, ...$parameters);, как-то так:

    $conditions = [];
    $parameters = [];
    if (!empty($_GET['name']))
    {
        $conditions[] = 'name LIKE ?';
        $parameters[] = '%'.$_GET['name']."%";
    }
    // любое количество таких условий
    if ($conditions)
    {
        $sql .= " WHERE ".implode(" AND ", $conditions);
    }

    и дальше выполняем обычным порядком.

    Ну и в завершение, ответим на вопрос из заголовка буквально
    Как сделать prepare запрос без bind_params?

    Как правильно заметил TheAndrey7 в комментариях, начиная с версии 8.1 можно будет отправить переменные сразу в execute():

    $names = ["Stepan" , "Stiv"];
    $in    = str_repeat('?,', count($names) - 1) . '?';
    $sql   = "SELECT * FROM users WHERE name IN ($in)";
    $stmt  = $mysqli->prepare($sql); 
    $stmt->execute($names); 
    $data = $stmt->get_result()->fetch_all(MYSQLI_ASSOC);
    Ответ написан
    4 комментария