Skip to content

Commit 1a186f3

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 1a186f3

File tree

3 files changed

+70
-4
lines changed

3 files changed

+70
-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,49 @@
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+
/** @requires extension mysqli */
15+
class StatementTest extends FunctionalTestCase
16+
{
17+
protected function setUp(): void
18+
{
19+
parent::setUp();
20+
21+
if (TestUtil::isDriverOneOf('mysqli')) {
22+
return;
23+
}
24+
25+
self::markTestSkipped('This test requires the mysqli driver.');
26+
}
27+
28+
public function testStatementsAreDeallocatedProperly(): void
29+
{
30+
$statement = $this->connection->prepare('SELECT 1');
31+
32+
$property = new ReflectionProperty(WrapperStatement::class, 'stmt');
33+
$property->setAccessible(true);
34+
35+
$driverStatement = $property->getValue($statement);
36+
37+
$mysqliProperty = new ReflectionProperty(Statement::class, 'stmt');
38+
$mysqliProperty->setAccessible(true);
39+
40+
$mysqliStatement = $mysqliProperty->getValue($driverStatement);
41+
42+
unset($statement, $driverStatement);
43+
44+
$this->expectException(Error::class);
45+
$this->expectExceptionMessage('mysqli_stmt object is already closed');
46+
47+
$mysqliStatement->execute();
48+
}
49+
}

0 commit comments

Comments
 (0)