diff --git a/src/Driver/Mysqli/Result.php b/src/Driver/Mysqli/Result.php index cb70940be20..89f94f368d8 100644 --- a/src/Driver/Mysqli/Result.php +++ b/src/Driver/Mysqli/Result.php @@ -39,10 +39,17 @@ final class Result implements ResultInterface /** * @internal The result can be only instantiated by its driver connection or statement. * + * @param Statement|null $statementReference Maintains a reference to the Statement that generated this result. This + * ensures that the lifetime of the Statement is managed in conjunction + * with its associated results, so they are destroyed together at the + * appropriate time, see {@see Statement::__destruct()}. + * * @throws Exception */ - public function __construct(private readonly mysqli_stmt $statement) - { + public function __construct( + private readonly mysqli_stmt $statement, + private ?Statement $statementReference = null, // @phpstan-ignore property.onlyWritten + ) { $meta = $statement->result_metadata(); $this->hasColumns = $meta !== false; $this->columnNames = $meta !== false ? array_column($meta->fetch_fields(), 'name') : []; diff --git a/src/Driver/Mysqli/Statement.php b/src/Driver/Mysqli/Statement.php index 8436fadfc6c..9a2151786e9 100644 --- a/src/Driver/Mysqli/Statement.php +++ b/src/Driver/Mysqli/Statement.php @@ -49,6 +49,11 @@ public function __construct(private readonly mysqli_stmt $stmt) $this->boundValues = array_fill(1, $paramCount, null); } + public function __destruct() + { + @$this->stmt->close(); + } + public function bindValue(int|string $param, mixed $value, ParameterType $type): void { assert(is_int($param)); @@ -72,7 +77,7 @@ public function execute(): Result throw StatementError::upcast($e); } - return new Result($this->stmt); + return new Result($this->stmt, $this); } /** diff --git a/tests/Functional/Driver/Mysqli/StatementTest.php b/tests/Functional/Driver/Mysqli/StatementTest.php new file mode 100644 index 00000000000..c53b2ab0d59 --- /dev/null +++ b/tests/Functional/Driver/Mysqli/StatementTest.php @@ -0,0 +1,46 @@ +connection->prepare('SELECT 1'); + + $property = new ReflectionProperty(WrapperStatement::class, 'stmt'); + $driverStatement = $property->getValue($statement); + + $mysqliProperty = new ReflectionProperty(Statement::class, 'stmt'); + $mysqliStatement = $mysqliProperty->getValue($driverStatement); + + unset($statement, $driverStatement); + + $this->expectException(Error::class); + $this->expectExceptionMessage('mysqli_stmt object is already closed'); + + $mysqliStatement->execute(); + } +}