Skip to content

Commit 29fe1e0

Browse files
committed
bugfix: deallocate mysqli prepared statement
Long running processes might hit the `max_prepared_stmt_count` due not deallocating the statement correctly.
1 parent 0ac3d64 commit 29fe1e0

File tree

3 files changed

+78
-4
lines changed

3 files changed

+78
-4
lines changed

src/Driver/Mysqli/Result.php

+15-3
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,15 @@ final class Result implements ResultInterface
2020
{
2121
private mysqli_stmt $statement;
2222

23+
/**
24+
* Maintains a reference to the Statement that generated this result. This ensures that the lifetime of the
25+
* Statement is managed in conjunction with its associated results, so they are destroyed together
26+
* at the appropriate time {@see Statement::__destruct()}.
27+
*
28+
* @phpstan-ignore property.onlyWritten
29+
*/
30+
private ?Statement $statementReference = null;
31+
2332
/**
2433
* Whether the statement result has columns. The property should be used only after the result metadata
2534
* has been fetched ({@see $metadataFetched}). Otherwise, the property value is undetermined.
@@ -42,9 +51,12 @@ final class Result implements ResultInterface
4251
*
4352
* @throws Exception
4453
*/
45-
public function __construct(mysqli_stmt $statement)
46-
{
47-
$this->statement = $statement;
54+
public function __construct(
55+
mysqli_stmt $statement,
56+
?Statement $statementReference = null
57+
) {
58+
$this->statement = $statement;
59+
$this->statementReference = $statementReference;
4860

4961
$meta = $statement->result_metadata();
5062

src/Driver/Mysqli/Statement.php

+6-1
Original file line numberDiff line numberDiff line change
@@ -61,6 +61,11 @@ public function __construct(mysqli_stmt $stmt)
6161
$this->boundValues = array_fill(1, $paramCount, null);
6262
}
6363

64+
public function __destruct()
65+
{
66+
@$this->stmt->close();
67+
}
68+
6469
/**
6570
* @deprecated Use {@see bindValue()} instead.
6671
*
@@ -159,7 +164,7 @@ public function execute($params = null): ResultInterface
159164
throw StatementError::new($this->stmt);
160165
}
161166

162-
return new Result($this->stmt);
167+
return new Result($this->stmt, $this);
163168
}
164169

165170
/**
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,57 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
namespace Doctrine\DBAL\Tests\Functional\Driver\Mysqli;
6+
7+
use Doctrine\DBAL\Driver\Mysqli\Statement;
8+
use Doctrine\DBAL\Statement as WrapperStatement;
9+
use Doctrine\DBAL\Tests\FunctionalTestCase;
10+
use Doctrine\DBAL\Tests\TestUtil;
11+
use Error;
12+
use ReflectionProperty;
13+
14+
use const PHP_VERSION_ID;
15+
16+
/** @requires extension mysqli */
17+
class StatementTest extends FunctionalTestCase
18+
{
19+
protected function setUp(): void
20+
{
21+
parent::setUp();
22+
23+
if (TestUtil::isDriverOneOf('mysqli')) {
24+
return;
25+
}
26+
27+
self::markTestSkipped('This test requires the mysqli driver.');
28+
}
29+
30+
public function testStatementsAreDeallocatedProperly(): void
31+
{
32+
$statement = $this->connection->prepare('SELECT 1');
33+
34+
$property = new ReflectionProperty(WrapperStatement::class, 'stmt');
35+
$property->setAccessible(true);
36+
37+
$driverStatement = $property->getValue($statement);
38+
39+
$mysqliProperty = new ReflectionProperty(Statement::class, 'stmt');
40+
$mysqliProperty->setAccessible(true);
41+
42+
$mysqliStatement = $mysqliProperty->getValue($driverStatement);
43+
44+
unset($statement, $driverStatement);
45+
46+
47+
if (PHP_VERSION_ID < 80000) {
48+
$this->expectError();
49+
$this->expectErrorMessage('mysqli_stmt::execute(): Couldn\'t fetch mysqli_stmt');
50+
} else {
51+
$this->expectException(Error::class);
52+
$this->expectExceptionMessage('mysqli_stmt object is already closed');
53+
}
54+
55+
$mysqliStatement->execute();
56+
}
57+
}

0 commit comments

Comments
 (0)