Skip to content

Commit 19a2b7d

Browse files
committed
Merge branch '3.9.x' into 4.2.x
* 3.9.x: bugfix: deallocate mysqli prepared statement (doctrine#6681)
2 parents bfe8fcf + ec16c82 commit 19a2b7d

File tree

3 files changed

+61
-3
lines changed

3 files changed

+61
-3
lines changed

src/Driver/Mysqli/Result.php

+9-2
Original file line numberDiff line numberDiff line change
@@ -39,10 +39,17 @@ final class Result implements ResultInterface
3939
/**
4040
* @internal The result can be only instantiated by its driver connection or statement.
4141
*
42+
* @param Statement|null $statementReference Maintains a reference to the Statement that generated this result. This
43+
* ensures that the lifetime of the Statement is managed in conjunction
44+
* with its associated results, so they are destroyed together at the
45+
* appropriate time, see {@see Statement::__destruct()}.
46+
*
4247
* @throws Exception
4348
*/
44-
public function __construct(private readonly mysqli_stmt $statement)
45-
{
49+
public function __construct(
50+
private readonly mysqli_stmt $statement,
51+
private ?Statement $statementReference = null, // @phpstan-ignore property.onlyWritten
52+
) {
4653
$meta = $statement->result_metadata();
4754
$this->hasColumns = $meta !== false;
4855
$this->columnNames = $meta !== false ? array_column($meta->fetch_fields(), 'name') : [];

src/Driver/Mysqli/Statement.php

+6-1
Original file line numberDiff line numberDiff line change
@@ -49,6 +49,11 @@ public function __construct(private readonly mysqli_stmt $stmt)
4949
$this->boundValues = array_fill(1, $paramCount, null);
5050
}
5151

52+
public function __destruct()
53+
{
54+
@$this->stmt->close();
55+
}
56+
5257
public function bindValue(int|string $param, mixed $value, ParameterType $type): void
5358
{
5459
assert(is_int($param));
@@ -72,7 +77,7 @@ public function execute(): Result
7277
throw StatementError::upcast($e);
7378
}
7479

75-
return new Result($this->stmt);
80+
return new Result($this->stmt, $this);
7681
}
7782

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

0 commit comments

Comments
 (0)