Prepared statements have become more important of late. By breaking a query into instructions and data, we avoid the problem of SQL injection. While MySQLi supports prepared statements, it has a structural deficiency that PDO does not share. MySQLi does not support binding parameters separately.
Consider the following query (which we will put into a variable called $sql)
INSERT INTO db.visits(page, ip) VALUES(?, ?)
Within PDO we can do the following
$stmt = $db->prepare($sql);
$stmt->bindParam(1, $page, PDO::PARAM_STR);
$stmt->bindParam(2, $ip, PDO::PARAM_STR);
MySQLi takes a different approach, by binding all the parameters at the same time (and mandating that they be passed by reference)
$stmt = $db->prepare($sql);
$stmt->bind_param('ss', $page, $ip);
This is functional, but only for smaller queries. As the parameter list grows, this methodology becomes unwieldy
$stmt->bind_param('ssssisssid', $name, $company, $billing_addr, $billing_city, $billing_state, $billing_zip...);
Not only is hard to maintain (you have to manually count parameters if you get an error), it also introduces a structural problem, in that if we want a conditional query and parameters, we have to get even more unwieldy
$sql = 'SELECT name FROM db.customer WHERE record_id = ?';
$params = 'i';
$data = [&$_GET['record_id']];
if(isset($_GET['zip'])) {
$params .= 's';
$data[] = &$_GET['zip'];
$sql .= ' AND billing_zip = ?';
}
// The first argument for mysqli_stmt_bind_param must be the data type list
array_unshift($params, $data);
$stmt = $db->prepare($sql);
call_user_func_array([$stmt, 'bind_param'], $data);
$stmt->execute();
PDO manages to make this work much more elegantly
$sql = 'SELECT name FROM db.customer WHERE record_id = ?';
if(isset($_GET['zip'])) $sql .= ' AND billing_zip = ?';
$stmt = $db->prepare($sql);
$stmt->bindParam(1, $_GET['record_id'], PDO::PARAM_INT);
if(isset($_GET['zip'])) $stmt->bindParam(2, $_GET['zip'], PDO::PARAM_STR);
$stmt->execute();
It's clear the MySQLi interface lags behind PDO here, yet still remains a staple in PHP. Many projects dutifully switched from the removed MySQL interface, yet, due to these issues, they still write procedural code to generate SQL because it's easier to simply escape the data and understand the code, than it is to work these extra layers in to secure their queries with prepared statements.
MySQLi desparately needs a single bind like PDO has. I would propose a new function, named mysqli_stmt_bind_single. Reusing the PDO query above with proposed syntax
$stmt = $db->prepare($sql);
$stmt->bind_single('i', $_GET['record_id'], 1);
if(isset($_GET['zip'])) $stmt->bind_single('s', $_GET['zip'], 2);
$stmt->execute();
This proposal would leave the previous mysqli_stmt_bind_param mostly untouched. Instead, the two could be used in tandem
$sql = 'SELECT name FROM db.customer WHERE record_id = ? AND shipping_zip = ?';
if(isset($_GET['zip'])) $sql .= ' AND billing_zip = ?';
$stmt = $db->prepare($sql);
$stmt->bind_param('is', $_GET['record_id'], $_GET['shipping_zip']);
if(isset($_GET['zip'])) $stmt->bind_single('s', $_GET['billing_zip'], 3);
$stmt->execute();
This necessitates a small change to mysqli_stmt_bind_param, in that the current function has a parameter check to ensure that the number of binds matches the number of parameters in the query (or else it emits an E_WARNING). That check would have to move to mysqli_stmt_execute, if it is still to be performed.
public mysqli_stmt::bind_single( mixed type, mixed $var [, int $position]) : bool
$position can be omitted, and it will take the next spot in the internal data array. Passing an invalid postion would emit an E_WARNING. $postion can overwrite an existing value silently, with a final check coming on the call of mysqli_stmt_execute, as described above. This is how PDO operates now.
On Mon, Nov 25, 2019 at 8:06 PM Joel Hutchinson
Joel.Hutchinson@onlinecommercegroup.com wrote:
This proposal would leave the previous mysqli_stmt_bind_param mostly untouched. Instead, the two could be used in tandem
$sql = 'SELECT name FROM db.customer WHERE record_id = ? AND shipping_zip = ?';
if(isset($_GET['zip'])) $sql .= ' AND billing_zip = ?';$stmt = $db->prepare($sql);
$stmt->bind_param('is', $_GET['record_id'], $_GET['shipping_zip']);
if(isset($_GET['zip'])) $stmt->bind_single('s', $_GET['billing_zip'], 3);
$stmt->execute();
This necessitates a small change to mysqli_stmt_bind_param, in that the current function has a parameter check to ensure that the number of binds matches the number of parameters in the query (or else it emits an E_WARNING). That check would have to move to mysqli_stmt_execute, if it is still to be performed.
If bind_param
is allowed to do incomplete bind (as in your example
where you supply 2 out of 3 required bound parameters) t
hen there's no need for additional method, as you could simply do:
if (isset($_GET['zip'])) $stmt->bind_param('s', $_GET['zip']);
--
Best regards,
Bruce Weirdan mailto:weirdan@gmail.com
I'd really like to mirror PDO here, and with bind_param
supporting an unlimited number of arguments, it makes that third positional argument clunky.
I still say a separate function to bind a sole value would be enough and avoids any confusion with the existing method.
Joel Hutchinson
-----Original Message-----
From: Bruce Weirdan weirdan@gmail.com
Sent: Monday, November 25, 2019 5:41 PM
To: Joel Hutchinson Joel.Hutchinson@onlinecommercegroup.com
Cc: internals@lists.php.net
Subject: Re: [PHP-DEV] mysqli needs a method to bind a single parameter
This proposal would leave the previous mysqli_stmt_bind_param mostly
untouched. Instead, the two could be used in tandem $sql = 'SELECT
name FROM db.customer WHERE record_id = ? AND shipping_zip = ?';
if(isset($_GET['zip'])) $sql .= ' AND billing_zip = ?';$stmt = $db->prepare($sql);
$stmt->bind_param('is', $_GET['record_id'], $_GET['shipping_zip']);
if(isset($_GET['zip'])) $stmt->bind_single('s', $_GET['billing_zip'],
3); $stmt->execute(); This necessitates a small change to
mysqli_stmt_bind_param, in that the current function has a parameter check to ensure that the number of binds matches the number of parameters in the query (or else it emits an E_WARNING). That check would have to move to mysqli_stmt_execute, if it is still to be performed.
If bind_param
is allowed to do incomplete bind (as in your example where you supply 2 out of 3 required bound parameters) t hen there's no need for additional method, as you could simply do:
if (isset($_GET['zip'])) $stmt->bind_param('s', $_GET['zip']);
--
Best regards,
Bruce Weirdan mailto:weirdan@gmail.com