Skip to content

Commit ce263fe

Browse files
authored
Merge pull request #360 from richard67/4.x-dev-upmerge-2026-02-17
[4.x] Upmerge 2026-02-17
2 parents 2e744e7 + 860ed8b commit ce263fe

4 files changed

Lines changed: 141 additions & 102 deletions

File tree

.github/workflows/ci.yml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -208,6 +208,6 @@ jobs:
208208
- name: Install Composer dependencies
209209
run: |
210210
git config --global --add safe.directory $GITHUB_WORKSPACE
211-
composer install --no-progress --ignore-platform-reqs
211+
composer update
212212
- name: Run Unit tests
213213
run: php vendor/bin/phpunit -c phpunit.appveyor_sql2019.xml.dist

Tests/Mysqli/MysqliPreparedStatementTest.php

Lines changed: 44 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -51,6 +51,14 @@ protected function setUp(): void
5151
)
5252
);
5353
}
54+
55+
$insertQuery = 'INSERT INTO dbtest (title, description, start_date) VALUES (:title, :description, :start_date)';
56+
$mysqliStatementObject = new MysqliStatement(static::$connection->getConnection(), $insertQuery);
57+
$mysqliStatementObject->execute([
58+
':title' => 'Test Title',
59+
':description' => 'Test Description',
60+
':start_date' => '2023-01-01',
61+
]);
5462
}
5563

5664
/**
@@ -148,10 +156,45 @@ public function testPreparedStatementWithSingleKey()
148156
$statement = 'SELECT * FROM dbtest WHERE `title` LIKE :search OR `description` LIKE :search2';
149157
$mysqliStatementObject = new MysqliStatement(static::$connection->getConnection(), $statement);
150158
$dummyValue = 'test';
151-
$dummyValue2 = 'test';
152159
$mysqliStatementObject->bindParam(':search', $dummyValue);
153160
$mysqliStatementObject->bindParam(':search2', $dummyValue);
154161

155162
$mysqliStatementObject->execute();
156163
}
164+
165+
/**
166+
* Regression test to ensure running queries with bound variables still works
167+
*/
168+
public function testPreparedStatementWithBinding()
169+
{
170+
$statement = 'SELECT id FROM dbtest WHERE `title` LIKE :search';
171+
$mysqliStatementObject = new MysqliStatement(static::$connection->getConnection(), $statement);
172+
$title = 'Test Title';
173+
$mysqliStatementObject->bindParam(':search', $title);
174+
$mysqliStatementObject->execute();
175+
$result = $mysqliStatementObject->fetchColumn();
176+
177+
$title = 'changed';
178+
$mysqliStatementObject->execute();
179+
$result2 = $mysqliStatementObject->fetchColumn();
180+
$this->assertNotEquals($result, $result2);
181+
}
182+
183+
/**
184+
* Regression test to ensure running queries with bound variables still works
185+
*/
186+
public function testPreparedStatementWithoutBinding()
187+
{
188+
$statement = 'SELECT id FROM dbtest WHERE `title` LIKE :search';
189+
$mysqliStatementObject = new MysqliStatement(static::$connection->getConnection(), $statement);
190+
$title = 'Test Title';
191+
$params = [':search' => $title];
192+
$mysqliStatementObject->execute($params);
193+
$result = $mysqliStatementObject->fetchColumn();
194+
195+
$params[':search'] = 'changed';
196+
$mysqliStatementObject->execute($params);
197+
$result2 = $mysqliStatementObject->fetchColumn();
198+
$this->assertNotEquals($result, $result2);
199+
}
157200
}

src/Mysqli/MysqliDriver.php

Lines changed: 56 additions & 46 deletions
Original file line numberDiff line numberDiff line change
@@ -202,7 +202,10 @@ public function connect()
202202
throw new UnsupportedAdapterException('The MySQLi extension is not available');
203203
}
204204

205-
$this->connection = mysqli_init();
205+
// Enable mysqli error reporting
206+
mysqli_report(MYSQLI_REPORT_ERROR | MYSQLI_REPORT_STRICT);
207+
208+
$this->connection = new \mysqli();
206209

207210
$connectionFlags = 0;
208211

@@ -232,21 +235,21 @@ public function connect()
232235
);
233236
}
234237

235-
// Attempt to connect to the server, use error suppression to silence warnings and allow us to throw an Exception separately.
236-
$connected = @$this->connection->real_connect(
237-
$this->options['host'],
238-
$this->options['user'],
239-
$this->options['password'],
240-
null,
241-
$this->options['port'],
242-
$this->options['socket'],
243-
$connectionFlags
244-
);
245-
246-
if (!$connected) {
238+
try {
239+
$this->connection->real_connect(
240+
$this->options['host'],
241+
$this->options['user'],
242+
$this->options['password'],
243+
null,
244+
$this->options['port'],
245+
$this->options['socket'],
246+
$connectionFlags
247+
);
248+
} catch (\mysqli_sql_exception $e) {
247249
throw new ConnectionFailureException(
248-
'Could not connect to database: ' . $this->connection->connect_error,
249-
$this->connection->connect_errno
250+
'Could not connect to database: ' . $e->getMessage(),
251+
$e->getCode(),
252+
$e
250253
);
251254
}
252255

@@ -509,7 +512,7 @@ public function getTableCreate($tables)
509512

510513
foreach ($tables as $table) {
511514
// Set the query to get the table CREATE statement.
512-
$row = $this->setQuery('SHOW CREATE TABLE ' . $this->quoteName($this->escape($table)))->loadRow();
515+
$row = $this->setQuery('SHOW CREATE TABLE ' . $this->quoteName($table))->loadRow();
513516

514517
// Populate the result array based on the create statements.
515518
$result[$table] = $row[1];
@@ -536,7 +539,7 @@ public function getTableColumns($table, $typeOnly = true)
536539
$result = [];
537540

538541
// Set the query to get the table fields statement.
539-
$fields = $this->setQuery('SHOW FULL COLUMNS FROM ' . $this->quoteName($this->escape($table)))->loadObjectList();
542+
$fields = $this->setQuery('SHOW FULL COLUMNS FROM ' . $this->quoteName($table))->loadObjectList();
540543

541544
// If we only want the type as the value add just that to the list.
542545
if ($typeOnly) {
@@ -783,8 +786,10 @@ public function select($database)
783786
return false;
784787
}
785788

786-
if (!$this->connection->select_db($database)) {
787-
throw new ConnectionFailureException('Could not connect to database.');
789+
try {
790+
$this->connection->select_db($database);
791+
} catch (\mysqli_sql_exception $e) {
792+
throw new ConnectionFailureException('Could not connect to database: ' . $e->getMessage(), $e->getCode(), $e);
788793
}
789794

790795
return true;
@@ -810,20 +815,26 @@ public function setUtf()
810815
// Which charset should I use, plain utf8 or multibyte utf8mb4?
811816
$charset = $this->utf8mb4 && $this->options['utf8mb4'] ? 'utf8mb4' : 'utf8';
812817

813-
$result = @$this->connection->set_charset($charset);
814-
815-
/*
816-
* If I could not set the utf8mb4 charset then the server doesn't support utf8mb4 despite claiming otherwise. This happens on old MySQL
817-
* server versions (less than 5.5.3) using the mysqlnd PHP driver. Since mysqlnd masks the server version and reports only its own we
818-
* 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
819-
* undefined in this case we catch the error and determine that utf8mb4 is not supported!
820-
*/
821-
if (!$result && $this->utf8mb4 && $this->options['utf8mb4']) {
822-
$this->utf8mb4 = false;
823-
$result = @$this->connection->set_charset('utf8');
818+
try {
819+
$this->connection->set_charset($charset);
820+
} catch (\mysqli_sql_exception) {
821+
/*
822+
* If I could not set the utf8mb4 charset then the server doesn't support utf8mb4 despite claiming otherwise. This happens on old MySQL
823+
* server versions (less than 5.5.3) using the mysqlnd PHP driver. Since mysqlnd masks the server version and reports only its own we
824+
* 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
825+
* undefined in this case we catch the error and determine that utf8mb4 is not supported!
826+
*/
827+
if ($this->utf8mb4 && $this->options['utf8mb4']) {
828+
$this->utf8mb4 = false;
829+
try {
830+
$this->connection->set_charset('utf8');
831+
} catch (\mysqli_sql_exception) {
832+
return false;
833+
}
834+
}
824835
}
825836

826-
return $result;
837+
return true;
827838
}
828839

829840
/**
@@ -841,8 +852,11 @@ public function transactionCommit($toSavepoint = false)
841852
if (!$toSavepoint || $this->transactionDepth <= 1) {
842853
$this->connect();
843854

844-
if ($this->connection->commit()) {
855+
try {
856+
$this->connection->commit();
845857
$this->transactionDepth = 0;
858+
} catch (\mysqli_sql_exception) {
859+
// TODO: Handle commit failure?
846860
}
847861

848862
return;
@@ -866,8 +880,11 @@ public function transactionRollback($toSavepoint = false)
866880
if (!$toSavepoint || $this->transactionDepth <= 1) {
867881
$this->connect();
868882

869-
if ($this->connection->rollback()) {
883+
try {
884+
$this->connection->rollback();
870885
$this->transactionDepth = 0;
886+
} catch (\mysqli_sql_exception) {
887+
// TODO: Handle rollback failure?
871888
}
872889

873890
return;
@@ -922,20 +939,19 @@ protected function executeUnpreparedQuery($sql)
922939
{
923940
$this->connect();
924941

925-
$cursor = $this->connection->query($sql);
926-
927-
// If an error occurred handle it.
928-
if (!$cursor) {
929-
$errorNum = (int) $this->connection->errno;
930-
$errorMsg = (string) $this->connection->error;
942+
try {
943+
$this->connection->query($sql);
944+
} catch (\mysqli_sql_exception $e) {
945+
$errorNum = $e->getCode();
946+
$errorMsg = $e->getMessage();
931947

932948
// Check if the server was disconnected.
933949
if (!$this->connected()) {
934950
try {
935951
// Attempt to reconnect.
936952
$this->connection = null;
937953
$this->connect();
938-
} catch (ConnectionFailureException $e) {
954+
} catch (ConnectionFailureException) {
939955
// If connect fails, ignore that exception and throw the normal exception.
940956
throw new ExecutionFailureException($sql, $errorMsg, $errorNum);
941957
}
@@ -948,12 +964,6 @@ protected function executeUnpreparedQuery($sql)
948964
throw new ExecutionFailureException($sql, $errorMsg, $errorNum);
949965
}
950966

951-
$this->freeResult();
952-
953-
if ($cursor instanceof \mysqli_result) {
954-
$cursor->free_result();
955-
}
956-
957967
return true;
958968
}
959969

0 commit comments

Comments
 (0)