How to make a fully dynamic prepared statement using mysqli API?
An excellent question. And thank you for moving to prepared statements. It seems that after all those years of struggle, the idea finally is starting to take over.
Disclaimer: there will be links to my own site because I am helping people with PHP for 20+ years and got an obsession with writing articles about most common issues.
Yes, it's perfectly possible. Check out my article, How to create a search filter for mysqli for the fully functional example.
For the WHERE
part, all you need is to create two separate arrays - one containing query conditions with placeholders and one containing actual values for these placeholders, i.e:
WHERE
clause
$conditions = [];
$parameters = [];
if (!empty($_POST["content"])) {
$conditions[] = 'content LIKE ?';
$parameters[] = '%'.$_POST['content ']."%";
}
and so on, for all search conditions.
Then you could implode
all the conditions using AND
string as a glue, and get a first-class WHERE
clause:
if ($conditions)
{
$where .= " WHERE ".implode(" AND ", $conditions);
}
The routine is the same for all search conditions, but it will be a bit different for the IN()
clause.
IN()
clause
is a bit different as you will need more placeholders and more values to be added:
if (!empty($_POST["opID"])) {
$in = str_repeat('?,', count($array) - 1) . '?';
$conditions[] = "opID IN ($in)";
$parameters = array_merge($parameters, $_POST["opID"]);
}
this code will add as many ?
placeholders to the IN()
clause as many elements in the $_POST["opID"]
and will add all those values to the $parameters
array. The explanation can be found in the adjacent article in the same section on my site.
After you are done with WHERE
clause, you can move to the rest of your query
ORDER BY
clause
You cannot parameterize the order by clause, because field names and SQL keywords cannot be represented by a placeholder. And to tackle with this problem I beg you to use a whitelisting function I wrote for this exact purpose. With it you can make your ORDER BY clause 100% safe but perfectly flexible. All you need is to predefine an array with field names allowed in the order by clause:
$sortColumns = ["title","content","priority"]; // add your own
and then get safe values using this handy function:
$orderField = white_list($_POST["column"], $sortColumns, "Invalid column name");
$order = white_list($_POST["order"], ["ASC","DESC"], "Invalid ORDER BY direction");
this is a smart function, that covers three different scenarios
- in case no values were provided (i.e. $_POST["column"] is empty) the first value from the white list will be used, so it serves as a default value
- in case a correct value provided, it will be used in the query
- in case an incorrect value is provided, then an error will be thrown.
LIMIT
clause
LIMIT
values are perfectly parameterized so you can just add them to the $parameters
array:
$limit = "LIMIT ?, ?";
$parameters[] = $offset;
$parameters[] = $recordsPerPage;
The final assembly
In the end, your query will be something like this
$sql = "SELECT id, title, content, priority, date, delivery
FROM tasks INNER JOIN ... $where ORDER BY `$orderField` $order $limit";
And it can be executed using the following code
$stmt = $mysqli->prepare($sql);
$stmt->bind_param(str_repeat("s", count($parameters)), ...$parameters);
$stmt->execute();
$data = $stmt->get_result()->fetch_all(MYSQLI_ASSOC);
where $data
is a conventional array contains all the rows returned by the query.