From 29fe1e0ee8b73461d9e2b85eae0aeb5fa8ac9b7c Mon Sep 17 00:00:00 2001 From: Peter Mein Date: Thu, 2 Jan 2025 16:43:01 +0100 Subject: [PATCH] bugfix: deallocate mysqli prepared statement Long running processes might hit the `max_prepared_stmt_count` due not deallocating the statement correctly. --- src/Driver/Mysqli/Result.php | 18 +++++- src/Driver/Mysqli/Statement.php | 7 ++- .../Driver/Mysqli/StatementTest.php | 57 +++++++++++++++++++ 3 files changed, 78 insertions(+), 4 deletions(-) create mode 100644 tests/Functional/Driver/Mysqli/StatementTest.php diff --git a/src/Driver/Mysqli/Result.php b/src/Driver/Mysqli/Result.php index c7dc65d1db5..bfd558b4fbc 100644 --- a/src/Driver/Mysqli/Result.php +++ b/src/Driver/Mysqli/Result.php @@ -20,6 +20,15 @@ final class Result implements ResultInterface { private mysqli_stmt $statement; + /** + * 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 Statement::__destruct()}. + * + * @phpstan-ignore property.onlyWritten + */ + private ?Statement $statementReference = null; + /** * Whether the statement result has columns. The property should be used only after the result metadata * has been fetched ({@see $metadataFetched}). Otherwise, the property value is undetermined. @@ -42,9 +51,12 @@ final class Result implements ResultInterface * * @throws Exception */ - public function __construct(mysqli_stmt $statement) - { - $this->statement = $statement; + public function __construct( + mysqli_stmt $statement, + ?Statement $statementReference = null + ) { + $this->statement = $statement; + $this->statementReference = $statementReference; $meta = $statement->result_metadata(); diff --git a/src/Driver/Mysqli/Statement.php b/src/Driver/Mysqli/Statement.php index fec7c95c3c2..c4548d9538e 100644 --- a/src/Driver/Mysqli/Statement.php +++ b/src/Driver/Mysqli/Statement.php @@ -61,6 +61,11 @@ public function __construct(mysqli_stmt $stmt) $this->boundValues = array_fill(1, $paramCount, null); } + public function __destruct() + { + @$this->stmt->close(); + } + /** * @deprecated Use {@see bindValue()} instead. * @@ -159,7 +164,7 @@ public function execute($params = null): ResultInterface throw StatementError::new($this->stmt); } - 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..03c99694d27 --- /dev/null +++ b/tests/Functional/Driver/Mysqli/StatementTest.php @@ -0,0 +1,57 @@ +connection->prepare('SELECT 1'); + + $property = new ReflectionProperty(WrapperStatement::class, 'stmt'); + $property->setAccessible(true); + + $driverStatement = $property->getValue($statement); + + $mysqliProperty = new ReflectionProperty(Statement::class, 'stmt'); + $mysqliProperty->setAccessible(true); + + $mysqliStatement = $mysqliProperty->getValue($driverStatement); + + unset($statement, $driverStatement); + + + if (PHP_VERSION_ID < 80000) { + $this->expectError(); + $this->expectErrorMessage('mysqli_stmt::execute(): Couldn\'t fetch mysqli_stmt'); + } else { + $this->expectException(Error::class); + $this->expectExceptionMessage('mysqli_stmt object is already closed'); + } + + $mysqliStatement->execute(); + } +}