====== PHP RFC: MySQLi Execute Query ====== * Version: 1 * RFC Started: 2022-04-21 * RFC Updated: 2022-05-11 * Voting Start: 2022-05-11 15:00 UTC / 16:00 BST * Voting End: 2022-05-25 15:00 UTC / 16:00 BST * Author: Kamil Tekiela, and Craig Francis [craig#at#craigfrancis.co.uk] * Status: Accepted * Target Version: PHP 8.2 * First Published at: https://wiki.php.net/rfc/mysqli_execute_query * GitHub Repo: https://github.com/craigfrancis/php-mysqli-execute-query-rfc * Implementation: [[https://github.com/php/php-src/compare/master...kamil-tekiela:execute_query|From Kamil Tekiela]] (proof of concept) ===== Introduction ===== Make parameterised MySQLi queries easier, with //mysqli_execute_query($sql, $params)//. This will further reduce the complexity of using parameterised queries - making it easier for developers to move away from //mysqli_query()//, and the dangerous/risky escaping of user values. ===== Proposal ===== This new function is a simple combination of: - //mysqli_prepare()// - //mysqli_execute()// - //mysqli_stmt_get_result()// It follows the original Draft RFC [[https://wiki.php.net/rfc/mysqli_execute_parameters|MySQLi Execute with Parameters]], which proposed a single function to execute a parameterised query; and the Implemented RFC [[https://wiki.php.net/rfc/mysqli_bind_in_execute|mysqli bind in execute]], which addressed the difficulties with //bind_param()//. Using an example, assume we start with: $db = new mysqli('localhost', 'user', 'password', 'database'); $name = '%a%'; $type1 = 1; // Admin $type2 = 2; // Editor Traditionally someone might use escaping, which is [[https://github.com/craigfrancis/php-is-literal-rfc/blob/main/justification/escaping.php?ts=4|very error prone]], e.g. foreach ($db->query('SELECT * FROM user WHERE name LIKE "' . $db->real_escape_string($name) . '" AND type_id IN (' . $db->real_escape_string($type1) . ', ' . $db->real_escape_string($type2) . ')') as $row) { // INSECURE print_r($row); } To avoid mistakes, parameterised queries should be used (with a [[https://eiv.dev/|literal-string]]), but can be fairly complex: $statement = $db->prepare('SELECT * FROM user WHERE name LIKE ? AND type_id IN (?, ?)'); $statement->bind_param('sii', $name, $type1, $type2); $statement->execute(); foreach ($statement->get_result() as $row) { print_r($row); } Since PHP 8.1, we no longer have problems with binding by reference, or needing to specify the variable types via the first argument to //bind_param()//, e.g. $statement = $db->prepare('SELECT * FROM user WHERE name LIKE ? AND type_id IN (?, ?)'); $statement->execute([$name, $type1, $type2]); foreach ($statement->get_result() as $row) { print_r($row); } This proposed function will simplify this even further, by allowing developers to write this in a one line foreach: foreach ($db->execute_query('SELECT * FROM user WHERE name LIKE ? AND type_id IN (?, ?)', [$name, $type1, $type2]) as $row) { print_r($row); } In pseudo-code it's basically: function mysqli_execute_query(mysqli $mysqli, string $sql, array $params = null) { $driver = new mysqli_driver(); $stmt = $mysqli->prepare($sql); if (!($driver->report_mode & MYSQLI_REPORT_STRICT) && $mysqli->error) { return false; } $stmt->execute($params); if (!($driver->report_mode & MYSQLI_REPORT_STRICT) && $stmt->error) { return false; } return $stmt->get_result(); } ===== Notes ===== ==== Function Name ==== The name was inspired by [[https://www.doctrine-project.org/projects/doctrine-dbal/en/latest/reference/data-retrieval-and-manipulation.html#executequery|Doctrine\DBAL\Connection::executeQuery()]]. ==== Returning false ==== The implementation is effectively calling [[https://www.php.net/mysqli_stmt_get_result|mysqli_stmt_get_result()]] last. While it will return //false// on failure, it will also return //false// for queries that do not produce a result set (e.g. //UPDATE//). Historically this has been addressed by using //mysqli_errno()//, but since 8.1 the [[https://wiki.php.net/rfc/mysqli_default_errmode|Change Default mysqli Error Mode RFC]] was accepted, and Exceptions are used by default. ==== Properties ==== Because [[https://www.php.net/manual/en/class.mysqli-stmt.php|mysqli_stmt]] is not returned, it's not possible to use its properties directly: - int|string **$affected_rows** - use //$mysqli->affected_rows// or //mysqli_affected_rows($mysqli)// - int|string **$insert_id** - use //$mysqli->insert_id// or //mysqli_insert_id($mysqli)// - int|string **$num_rows** - also available on //mysqli_result// - int **$param_count** - int **$field_count** - also available on //mysqli_result// - int **$errno** - use //mysqli_errno($mysqli)//, //$mysqli->errno// - string **$error** - use //mysqli_error($mysqli)//, //$mysqli->error// - array **$error_list** - use //mysqli_error_list($mysqli)//, //$mysqli->error_list// - string **$sqlstate** - use //mysqli_sqlstate($mysqli)//, //$mysqli->sqlstate// - int **$id** It's also worth noting the error property usage will hopefully reduce, as more developers use //mysqli_sql_exception// for errors (because the mysqli Error Mode now defaults to //MYSQLI_REPORT_ERROR | MYSQLI_REPORT_STRICT//). ==== Re-using Statements ==== The implementation discards the //mysqli_stmt// object immediately, so you cannot re-issue a statement with new parameters. Anyone who would benefit from this (to skip running prepare again), can still use //mysqli_prepare()//. ==== Updating Existing Functions ==== Cannot change //mysqli_query()// because its second argument is //$resultmode//. Cannot replace the deprecated //mysqli_execute()// function, which is an alias for //mysqli_stmt_execute()//, because it would create a backwards compatibility issue. ==== Why Now ==== Because the [[https://wiki.php.net/rfc/mysqli_support_for_libmysql|Remove support for libmysql from mysqli RFC]] has been accepted, it makes it much easier to implement with //mysqlnd//. ===== Backward Incompatible Changes ===== None ===== Proposed PHP Version(s) ===== PHP 8.2 ===== RFC Impact ===== ==== To SAPIs ==== None known ==== To Existing Extensions ==== - mysqli, adding a new function. ==== To Opcache ==== None known ==== New Constants ==== None ==== php.ini Defaults ==== None ===== Open Issues ===== None ===== Unaffected PHP Functionality ===== N/A ===== Future Scope ===== N/A ===== Voting ===== Accept the RFC * Yes * No ===== Implementation ===== [[https://github.com/php/php-src/compare/master...kamil-tekiela:execute_query|From Kamil Tekiela]] (proof of concept) This implementation copies some details to the mysqli object, but not the affected rows. This means //mysqli_affected_rows($mysqli)// and //$mysqli->affected_rows// will currently return -1. ===== References ===== N/A ===== Rejected Features ===== None