From 4d23f8a1b96b9eb9fbbbb203051e25d02dd9f134 Mon Sep 17 00:00:00 2001 From: Kamil Tekiela Date: Sun, 19 Oct 2025 00:13:00 +0100 Subject: [PATCH 1/3] Remove escape when not with string --- src/Mysqli/MysqliDriver.php | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/Mysqli/MysqliDriver.php b/src/Mysqli/MysqliDriver.php index 5d1329bd..0d55bfb6 100644 --- a/src/Mysqli/MysqliDriver.php +++ b/src/Mysqli/MysqliDriver.php @@ -509,7 +509,7 @@ public function getTableCreate($tables) foreach ($tables as $table) { // Set the query to get the table CREATE statement. - $row = $this->setQuery('SHOW CREATE TABLE ' . $this->quoteName($this->escape($table)))->loadRow(); + $row = $this->setQuery('SHOW CREATE TABLE ' . $this->quoteName($table))->loadRow(); // Populate the result array based on the create statements. $result[$table] = $row[1]; @@ -536,7 +536,7 @@ public function getTableColumns($table, $typeOnly = true) $result = []; // Set the query to get the table fields statement. - $fields = $this->setQuery('SHOW FULL COLUMNS FROM ' . $this->quoteName($this->escape($table)))->loadObjectList(); + $fields = $this->setQuery('SHOW FULL COLUMNS FROM ' . $this->quoteName($table))->loadObjectList(); // If we only want the type as the value add just that to the list. if ($typeOnly) { From e56b182f50473025df11df37b3dd167f97f4dff8 Mon Sep 17 00:00:00 2001 From: Kamil Tekiela Date: Sun, 26 Oct 2025 11:52:48 +0000 Subject: [PATCH 2/3] Fix MysqliStatement to work with named params (#351) --- Tests/Mysqli/MysqliPreparedStatementTest.php | 45 +++++++++++++++++++- src/Mysqli/MysqliStatement.php | 5 ++- 2 files changed, 48 insertions(+), 2 deletions(-) diff --git a/Tests/Mysqli/MysqliPreparedStatementTest.php b/Tests/Mysqli/MysqliPreparedStatementTest.php index 34fe60a7..38d73d56 100644 --- a/Tests/Mysqli/MysqliPreparedStatementTest.php +++ b/Tests/Mysqli/MysqliPreparedStatementTest.php @@ -51,6 +51,14 @@ protected function setUp(): void ) ); } + + $insertQuery = 'INSERT INTO dbtest (title, description, start_date) VALUES (:title, :description, :start_date)'; + $mysqliStatementObject = new MysqliStatement(static::$connection->getConnection(), $insertQuery); + $mysqliStatementObject->execute([ + ':title' => 'Test Title', + ':description' => 'Test Description', + ':start_date' => '2023-01-01', + ]); } /** @@ -148,10 +156,45 @@ public function testPreparedStatementWithSingleKey() $statement = 'SELECT * FROM dbtest WHERE `title` LIKE :search OR `description` LIKE :search2'; $mysqliStatementObject = new MysqliStatement(static::$connection->getConnection(), $statement); $dummyValue = 'test'; - $dummyValue2 = 'test'; $mysqliStatementObject->bindParam(':search', $dummyValue); $mysqliStatementObject->bindParam(':search2', $dummyValue); $mysqliStatementObject->execute(); } + + /** + * Regression test to ensure running queries with bound variables still works + */ + public function testPreparedStatementWithBinding() + { + $statement = 'SELECT id FROM dbtest WHERE `title` LIKE :search'; + $mysqliStatementObject = new MysqliStatement(static::$connection->getConnection(), $statement); + $title = 'Test Title'; + $mysqliStatementObject->bindParam(':search', $title); + $mysqliStatementObject->execute(); + $result = $mysqliStatementObject->fetchColumn(); + + $title = 'changed'; + $mysqliStatementObject->execute(); + $result2 = $mysqliStatementObject->fetchColumn(); + $this->assertNotEquals($result, $result2); + } + + /** + * Regression test to ensure running queries with bound variables still works + */ + public function testPreparedStatementWithoutBinding() + { + $statement = 'SELECT id FROM dbtest WHERE `title` LIKE :search'; + $mysqliStatementObject = new MysqliStatement(static::$connection->getConnection(), $statement); + $title = 'Test Title'; + $params = [':search' => $title]; + $mysqliStatementObject->execute($params); + $result = $mysqliStatementObject->fetchColumn(); + + $params[':search'] = 'changed'; + $mysqliStatementObject->execute($params); + $result2 = $mysqliStatementObject->fetchColumn(); + $this->assertNotEquals($result, $result2); + } } diff --git a/src/Mysqli/MysqliStatement.php b/src/Mysqli/MysqliStatement.php index afda7b72..040e2cb7 100644 --- a/src/Mysqli/MysqliStatement.php +++ b/src/Mysqli/MysqliStatement.php @@ -306,7 +306,10 @@ private function bindValues(array $values) if (!empty($this->parameterKeyMapping)) { foreach ($values as $key => &$value) { - $params[$this->parameterKeyMapping[$key]] =& $value; + $paramKey = $this->parameterKeyMapping[$key]; + foreach ($paramKey as $currentKey) { + $params[$currentKey] =& $value; + } } ksort($params); From a567e9bfd688948903b2b9dd6265ca3e2b7ecc7e Mon Sep 17 00:00:00 2001 From: Kamil Tekiela Date: Sun, 9 Nov 2025 10:48:46 +0000 Subject: [PATCH 3/3] [3.x] Add mysqli exception mode (#349) * Add mysqli exception mode * Remove error suppression --- src/Mysqli/MysqliDriver.php | 98 +++++++++++++++++++--------------- src/Mysqli/MysqliStatement.php | 38 ++++++------- 2 files changed, 73 insertions(+), 63 deletions(-) diff --git a/src/Mysqli/MysqliDriver.php b/src/Mysqli/MysqliDriver.php index 0d55bfb6..b7705e48 100644 --- a/src/Mysqli/MysqliDriver.php +++ b/src/Mysqli/MysqliDriver.php @@ -202,7 +202,10 @@ public function connect() throw new UnsupportedAdapterException('The MySQLi extension is not available'); } - $this->connection = mysqli_init(); + // Enable mysqli error reporting + mysqli_report(MYSQLI_REPORT_ERROR | MYSQLI_REPORT_STRICT); + + $this->connection = new \mysqli(); $connectionFlags = 0; @@ -232,21 +235,21 @@ public function connect() ); } - // Attempt to connect to the server, use error suppression to silence warnings and allow us to throw an Exception separately. - $connected = @$this->connection->real_connect( - $this->options['host'], - $this->options['user'], - $this->options['password'], - null, - $this->options['port'], - $this->options['socket'], - $connectionFlags - ); - - if (!$connected) { + try { + $this->connection->real_connect( + $this->options['host'], + $this->options['user'], + $this->options['password'], + null, + $this->options['port'], + $this->options['socket'], + $connectionFlags + ); + } catch (\mysqli_sql_exception $e) { throw new ConnectionFailureException( - 'Could not connect to database: ' . $this->connection->connect_error, - $this->connection->connect_errno + 'Could not connect to database: ' . $e->getMessage(), + $e->getCode(), + $e ); } @@ -783,8 +786,10 @@ public function select($database) return false; } - if (!$this->connection->select_db($database)) { - throw new ConnectionFailureException('Could not connect to database.'); + try { + $this->connection->select_db($database); + } catch (\mysqli_sql_exception $e) { + throw new ConnectionFailureException('Could not connect to database: ' . $e->getMessage(), $e->getCode(), $e); } return true; @@ -810,20 +815,26 @@ public function setUtf() // Which charset should I use, plain utf8 or multibyte utf8mb4? $charset = $this->utf8mb4 && $this->options['utf8mb4'] ? 'utf8mb4' : 'utf8'; - $result = @$this->connection->set_charset($charset); - - /* - * If I could not set the utf8mb4 charset then the server doesn't support utf8mb4 despite claiming otherwise. This happens on old MySQL - * server versions (less than 5.5.3) using the mysqlnd PHP driver. Since mysqlnd masks the server version and reports only its own we - * can not be sure if the server actually does support UTF-8 Multibyte (i.e. it's MySQL 5.5.3 or later). Since the utf8mb4 charset is - * undefined in this case we catch the error and determine that utf8mb4 is not supported! - */ - if (!$result && $this->utf8mb4 && $this->options['utf8mb4']) { - $this->utf8mb4 = false; - $result = @$this->connection->set_charset('utf8'); + try { + $this->connection->set_charset($charset); + } catch (\mysqli_sql_exception) { + /* + * If I could not set the utf8mb4 charset then the server doesn't support utf8mb4 despite claiming otherwise. This happens on old MySQL + * server versions (less than 5.5.3) using the mysqlnd PHP driver. Since mysqlnd masks the server version and reports only its own we + * can not be sure if the server actually does support UTF-8 Multibyte (i.e. it's MySQL 5.5.3 or later). Since the utf8mb4 charset is + * undefined in this case we catch the error and determine that utf8mb4 is not supported! + */ + if ($this->utf8mb4 && $this->options['utf8mb4']) { + $this->utf8mb4 = false; + try { + $this->connection->set_charset('utf8'); + } catch (\mysqli_sql_exception) { + return false; + } + } } - return $result; + return true; } /** @@ -841,8 +852,11 @@ public function transactionCommit($toSavepoint = false) if (!$toSavepoint || $this->transactionDepth <= 1) { $this->connect(); - if ($this->connection->commit()) { + try { + $this->connection->commit(); $this->transactionDepth = 0; + } catch (\mysqli_sql_exception) { + // TODO: Handle commit failure? } return; @@ -866,8 +880,11 @@ public function transactionRollback($toSavepoint = false) if (!$toSavepoint || $this->transactionDepth <= 1) { $this->connect(); - if ($this->connection->rollback()) { + try { + $this->connection->rollback(); $this->transactionDepth = 0; + } catch (\mysqli_sql_exception) { + // TODO: Handle rollback failure? } return; @@ -922,12 +939,11 @@ protected function executeUnpreparedQuery($sql) { $this->connect(); - $cursor = $this->connection->query($sql); - - // If an error occurred handle it. - if (!$cursor) { - $errorNum = (int) $this->connection->errno; - $errorMsg = (string) $this->connection->error; + try { + $this->connection->query($sql); + } catch (\mysqli_sql_exception $e) { + $errorNum = $e->getCode(); + $errorMsg = $e->getMessage(); // Check if the server was disconnected. if (!$this->connected()) { @@ -935,7 +951,7 @@ protected function executeUnpreparedQuery($sql) // Attempt to reconnect. $this->connection = null; $this->connect(); - } catch (ConnectionFailureException $e) { + } catch (ConnectionFailureException) { // If connect fails, ignore that exception and throw the normal exception. throw new ExecutionFailureException($sql, $errorMsg, $errorNum); } @@ -948,12 +964,6 @@ protected function executeUnpreparedQuery($sql) throw new ExecutionFailureException($sql, $errorMsg, $errorNum); } - $this->freeResult(); - - if ($cursor instanceof \mysqli_result) { - $cursor->free_result(); - } - return true; } diff --git a/src/Mysqli/MysqliStatement.php b/src/Mysqli/MysqliStatement.php index 040e2cb7..3e8118bb 100644 --- a/src/Mysqli/MysqliStatement.php +++ b/src/Mysqli/MysqliStatement.php @@ -135,10 +135,10 @@ public function __construct(\mysqli $connection, string $query) $query = $this->prepareParameterKeyMapping($query); - $this->statement = $connection->prepare($query); - - if (!$this->statement) { - throw new PrepareStatementFailureException($this->connection->error, $this->connection->errno); + try { + $this->statement = $connection->prepare($query); + } catch (\mysqli_sql_exception $e) { + throw new PrepareStatementFailureException($e->getMessage(), $e->getCode(), $e); } } @@ -397,19 +397,21 @@ public function execute(?array $parameters = null) array_unshift($params, implode('', $types)); - if (!\call_user_func_array([$this->statement, 'bind_param'], $params)) { - throw new PrepareStatementFailureException($this->statement->error, $this->statement->errno); + try { + \call_user_func_array([$this->statement, 'bind_param'], $params); + } catch (\Exception $e) { + throw new PrepareStatementFailureException($e->getMessage(), $e->getCode(), $e); } } elseif ($parameters !== null) { - if (!$this->bindValues($parameters)) { - throw new PrepareStatementFailureException($this->statement->error, $this->statement->errno); + try { + $this->bindValues($parameters); + } catch (\Exception $e) { + throw new PrepareStatementFailureException($e->getMessage(), $e->getCode(), $e); } } try { - if (!$this->statement->execute()) { - throw new ExecutionFailureException($this->query, $this->statement->error, $this->statement->errno); - } + $this->statement->execute(); } catch (\Throwable $e) { throw new ExecutionFailureException($this->query, $e->getMessage(), $e->getCode(), $e); } @@ -442,9 +444,7 @@ public function execute(?array $parameters = null) $refs[$key] =& $value; } - if (!\call_user_func_array([$this->statement, 'bind_result'], $refs)) { - throw new \RuntimeException($this->statement->error, $this->statement->errno); - } + \call_user_func_array([$this->statement, 'bind_result'], $refs); } $this->result = true; @@ -488,10 +488,6 @@ public function fetch(?int $fetchStyle = null, int $cursorOrientation = FetchOri return false; } - if ($values === false) { - throw new \RuntimeException($this->statement->error, $this->statement->errno); - } - switch ($fetchStyle) { case FetchMode::NUMERIC: return $values; @@ -543,7 +539,11 @@ public function fetchColumn($columnIndex = 0) */ private function fetchData() { - $return = $this->statement->fetch(); + try { + $return = $this->statement->fetch(); + } catch (\Throwable $e) { + throw new \RuntimeException($e->getMessage(), $e->getCode(), $e); + } if ($return === true) { $values = [];