From 19b8ae91085a37e18abefbaa760f1d08a5f57eeb Mon Sep 17 00:00:00 2001 From: Thomas Vargiu Date: Wed, 6 Jan 2021 18:48:03 +0100 Subject: [PATCH] Full Statement support. Fixes #43 ### Added * Added compatibility with doctrine/dbal > 2.11 Statement * Added ability to reconnect when creating `Mysqli` statement ### Changed * `Statement` now extends the original one, so all methods are implemented now * `Connection::refresh()` is deprecated, you should use the original `Connection::ping()` --- CHANGELOG.md | 10 +- Dockerfile | 4 +- src/ConnectionTrait.php | 23 +-- src/Statement.php | 171 +++++------------ tests/functional/AbstractFunctionalTest.php | 192 ++++++++++++++++++++ tests/functional/FunctionalTest.php | 113 ------------ tests/functional/MysqliTest.php | 28 +++ tests/functional/PDOMySqlTest.php | 28 +++ tests/unit/StatementTest.php | 178 ------------------ 9 files changed, 314 insertions(+), 433 deletions(-) create mode 100644 tests/functional/AbstractFunctionalTest.php delete mode 100644 tests/functional/FunctionalTest.php create mode 100644 tests/functional/MysqliTest.php create mode 100644 tests/functional/PDOMySqlTest.php delete mode 100644 tests/unit/StatementTest.php diff --git a/CHANGELOG.md b/CHANGELOG.md index 78de96a..e819d64 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,7 +5,15 @@ All notable changes to this project will be documented in this file. The format is based on [Keep a Changelog](http://keepachangelog.com/en/1.0.0/) and this project adheres to [Semantic Versioning](http://semver.org/spec/v2.0.0.html). -## [1.9.0] - TBD +## [1.10.0] - TBD +### Added +* Added compatibility with doctrine/dbal > 2.11 Statement +* Added ability to reconnect when creating `Mysqli` statement +### Changed +* `Statement` now extends the original one, so all methods are implemented now +* `Connection::refresh()` is deprecated, you should use the original `Connection::ping()` + +## [1.9.0] - 2020-11-02 ### Added * Added compatibility with doctrine/dbal 2.11 * Added Github Actions for CI diff --git a/Dockerfile b/Dockerfile index 187279f..2452a6f 100644 --- a/Dockerfile +++ b/Dockerfile @@ -10,9 +10,9 @@ RUN set -ex \ autoconf \ make \ g++ \ - && pecl install -o xdebug-2.9.8 && docker-php-ext-enable xdebug \ + && pecl install -o xdebug-3.0.2 && docker-php-ext-enable xdebug \ && apk del build-dependencies -ARG COMPOSER_VERSION=2.0.3 +ARG COMPOSER_VERSION=2.0.8 RUN curl -sS https://getcomposer.org/installer | php -- \ --install-dir=/usr/local/bin --filename=composer --version=$COMPOSER_VERSION diff --git a/src/ConnectionTrait.php b/src/ConnectionTrait.php index bc9c583..4e9c6db 100644 --- a/src/ConnectionTrait.php +++ b/src/ConnectionTrait.php @@ -10,6 +10,7 @@ use Doctrine\DBAL\Connection as DBALConnection; use Doctrine\DBAL\Driver; use Doctrine\DBAL\Driver\ResultStatement; +use Doctrine\DBAL\FetchMode; use Exception; use Facile\DoctrineMySQLComeBack\Doctrine\DBAL\Driver\ServerGoneAwayExceptionsAwareInterface; use ReflectionClass; @@ -26,6 +27,9 @@ trait ConnectionTrait /** @var ReflectionProperty|null */ private $selfReflectionNestingLevelProperty; + /** @var int */ + protected $defaultFetchMode = FetchMode::ASSOCIATIVE; + /** * @param array $params * @param Driver|ServerGoneAwayExceptionsAwareInterface $driver @@ -205,24 +209,17 @@ public function prepare($sql) */ protected function prepareWrapped($sql) { - return new Statement($sql, $this); - } + $stmt = new Statement($sql, $this); + $stmt->setFetchMode($this->defaultFetchMode); - /** - * do not use, only used by Statement-class - * needs to be public for access from the Statement-class. - * - * @internal - */ - public function prepareUnwrapped($sql) - { - // returns the actual statement - return parent::prepare($sql); + return $stmt; } /** * Forces reconnection by doing a dummy query. * + * @deprecated Use ping() + * * @throws Exception */ public function refresh() @@ -231,7 +228,7 @@ public function refresh() } /** - * @param $attempt + * @param int $attempt * @param bool $ignoreTransactionLevel * * @return bool diff --git a/src/Statement.php b/src/Statement.php index 8e696ef..f15d1d8 100644 --- a/src/Statement.php +++ b/src/Statement.php @@ -4,54 +4,58 @@ namespace Facile\DoctrineMySQLComeBack\Doctrine\DBAL; -use Doctrine\DBAL\Driver\Statement as DriverStatement; -use IteratorAggregate; -use PDO; -use Traversable; +use Doctrine\DBAL\ParameterType; /** - * Class Statement. + * @internal */ -class Statement implements IteratorAggregate, DriverStatement +class Statement extends \Doctrine\DBAL\Statement { /** - * @var string + * The connection this statement is bound to and executed on. + * + * @var Connection */ - protected $sql; + protected $conn; /** - * @var \Doctrine\DBAL\Statement + * @var mixed[][] */ - protected $stmt; + private $boundValues = []; /** - * @var Connection + * @var mixed[][] */ - protected $conn; - - private $boundValues = []; - private $boundParams = []; - private $fetchMode; - /** - * @param $sql - * @param ConnectionInterface $conn + * @var mixed[]|null */ - public function __construct($sql, ConnectionInterface $conn) - { - $this->sql = $sql; - $this->conn = $conn; - $this->createStatement(); - } + private $fetchMode; /** - * Create Statement. + * @param $sql + * @param Connection $conn */ - private function createStatement() + public function __construct($sql, Connection $conn) { - $this->stmt = $this->conn->prepareUnwrapped($this->sql); + // Mysqli executes statement on Statement constructor, so we should retry to reconnect here too + $attempt = 0; + $retry = true; + while ($retry) { + $retry = false; + try { + parent::__construct($sql, $conn); + } catch (\Exception $e) { + if ($conn->canTryAgain($attempt) && $conn->isRetryableException($e, $sql)) { + $conn->close(); + ++$attempt; + $retry = true; + } else { + throw $e; + } + } + } } /** @@ -59,7 +63,8 @@ private function createStatement() */ private function recreateStatement() { - $this->createStatement(); + $this->stmt = $this->conn->getWrappedConnection()->prepare($this->sql); + if (null !== $this->fetchMode) { call_user_func_array([$this->stmt, 'setFetchMode'], $this->fetchMode); } @@ -86,7 +91,7 @@ public function execute($params = null) while ($retry) { $retry = false; try { - $stmt = $this->stmt->execute($params); + $stmt = parent::execute($params); } catch (\Exception $e) { if ($this->conn->canTryAgain($attempt) && $this->conn->isRetryableException($e, $this->sql)) { $this->conn->close(); @@ -109,9 +114,9 @@ public function execute($params = null) * * @return bool */ - public function bindValue($name, $value, $type = PDO::PARAM_STR) + public function bindValue($name, $value, $type = ParameterType::STRING) { - if ($this->stmt->bindValue($name, $value, $type)) { + if (parent::bindValue($name, $value, $type)) { $this->boundValues[$name] = [$name, $value, $type]; return true; @@ -121,17 +126,17 @@ public function bindValue($name, $value, $type = PDO::PARAM_STR) } /** - * @param string $name - * @param mixed $var + * @param string|int $param + * @param mixed $variable * @param int $type * @param int|null $length * * @return bool */ - public function bindParam($name, &$var, $type = PDO::PARAM_STR, $length = null) + public function bindParam($param, &$variable, $type = ParameterType::STRING, $length = null) { - if ($this->stmt->bindParam($name, $var, $type, $length)) { - $this->boundParams[$name] = [$name, &$var, $type, $length]; + if (parent::bindParam($param, $variable, $type, $length)) { + $this->boundParams[$param] = [$param, &$variable, $type, $length]; return true; } @@ -140,38 +145,8 @@ public function bindParam($name, &$var, $type = PDO::PARAM_STR, $length = null) } /** - * @return bool - */ - public function closeCursor() - { - return $this->stmt->closeCursor(); - } - - /** - * @return int - */ - public function columnCount() - { - return $this->stmt->columnCount(); - } - - /** - * @return int - */ - public function errorCode() - { - return $this->stmt->errorCode(); - } - - /** - * @return array - */ - public function errorInfo() - { - return $this->stmt->errorInfo(); - } - - /** + * @deprecated Use one of the fetch- or iterate-related methods. + * * @param int $fetchMode * @param mixed $arg2 * @param mixed $arg3 @@ -180,7 +155,7 @@ public function errorInfo() */ public function setFetchMode($fetchMode, $arg2 = null, $arg3 = null) { - if ($this->stmt->setFetchMode($fetchMode, $arg2, $arg3)) { + if (parent::setFetchMode($fetchMode, $arg2, $arg3)) { $this->fetchMode = [$fetchMode, $arg2, $arg3]; return true; @@ -188,60 +163,4 @@ public function setFetchMode($fetchMode, $arg2 = null, $arg3 = null) return false; } - - /** - * @return Traversable - */ - public function getIterator() - { - return $this->stmt; - } - - /** - * @param int|null $fetchMode - * @param int $cursorOrientation Only for doctrine/DBAL >= 2.6 - * @param int $cursorOffset Only for doctrine/DBAL >= 2.6 - * @return mixed - */ - public function fetch($fetchMode = null, $cursorOrientation = PDO::FETCH_ORI_NEXT, $cursorOffset = 0) - { - return $this->stmt->fetch($fetchMode, $cursorOrientation, $cursorOffset); - } - - /** - * @param int|null $fetchMode - * @param int $fetchArgument Only for doctrine/DBAL >= 2.6 - * @param null $ctorArgs Only for doctrine/DBAL >= 2.6 - * @return mixed - */ - public function fetchAll($fetchMode = null, $fetchArgument = null, $ctorArgs = null) - { - return $this->stmt->fetchAll($fetchMode, $fetchArgument, $ctorArgs); - } - - /** - * @param int $columnIndex - * - * @return mixed - */ - public function fetchColumn($columnIndex = 0) - { - return $this->stmt->fetchColumn($columnIndex); - } - - /** - * @return int - */ - public function rowCount() - { - return $this->stmt->rowCount(); - } - - /** - * @return \Doctrine\DBAL\Statement - */ - public function getWrappedStatement() - { - return $this->stmt; - } } diff --git a/tests/functional/AbstractFunctionalTest.php b/tests/functional/AbstractFunctionalTest.php new file mode 100644 index 0000000..deeba87 --- /dev/null +++ b/tests/functional/AbstractFunctionalTest.php @@ -0,0 +1,192 @@ +createConnection($attempts); + $connection->query('SELECT 1'); + + return $connection; + } + + protected function createTestTable(Connection $connection): void + { + $connection->executeStatement(<<<'TABLE' +CREATE TABLE IF NOT EXISTS test ( + id INT UNSIGNED AUTO_INCREMENT PRIMARY KEY, + updatedAt TIMESTAMP DEFAULT CURRENT_TIMESTAMP() ON UPDATE CURRENT_TIMESTAMP() +); +TABLE + ); + + $connection->executeStatement('DELETE FROM `test`;'); + $connection->executeStatement('INSERT INTO test (id) VALUES (1);'); + } + + protected function getConnectionParams(): array + { + return [ + 'driver' => getenv('MYSQL_DRIVER') ?: $GLOBALS['db_driver'] ?? 'pdo_mysql', + 'dbname' => getenv('MYSQL_DBNAME') ?: $GLOBALS['db_dbname'] ?? 'test', + 'user' => getenv('MYSQL_USER') ?: $GLOBALS['db_user'] ?? 'root', + 'password' => getenv('MYSQL_PASS') ?: $GLOBALS['db_pass'] ?? '', + 'host' => getenv('MYSQL_HOST') ?: $GLOBALS['db_host'] ?? 'localhost', + 'port' => (int) (getenv('MYSQL_PORT') ?: $GLOBALS['db_port'] ?? 3306), + ]; + } + + /** + * Disconnect other sessions + */ + protected function forceDisconnect(\Doctrine\DBAL\Connection $connection): void + { + /** @var Connection $connection */ + $connection2 = DriverManager::getConnection(array_merge( + $this->getConnectionParams(), + [ + 'wrapperClass' => Connection::class, + 'driverClass' => Driver::class, + 'driverOptions' => array( + 'x_reconnect_attempts' => 1 + ) + ] + )); + + $ids = $connection->fetchFirstColumn('SELECT CONNECTION_ID()'); + + foreach ($ids as $id) { + $connection2->executeStatement('KILL ' . $id); + } + $connection2->close(); + } + + public function testExecuteQueryShouldNotReconnect(): void + { + $connection = $this->getConnectedConnection(0); + $this->assertSame(1, $connection->connectCount); + $this->forceDisconnect($connection); + + $this->expectException(DBALException::class); + + $connection->executeQuery('SELECT 1'); + } + + public function testExecuteQueryShouldReconnect(): void + { + $connection = $this->getConnectedConnection(1); + $this->assertSame(1, $connection->connectCount); + $this->forceDisconnect($connection); + + $connection->executeQuery('SELECT 1')->execute(); + $this->assertSame(2, $connection->connectCount); + } + + public function testQueryShouldReconnect(): void + { + $connection = $this->getConnectedConnection(1); + $this->assertSame(1, $connection->connectCount); + $this->forceDisconnect($connection); + + $connection->query('SELECT 1')->execute(); + $this->assertSame(2, $connection->connectCount); + } + + public function testExecuteUpdateShouldReconnect(): void + { + $connection = $this->getConnectedConnection(1); + $this->createTestTable($connection); + $this->assertSame(1, $connection->connectCount); + $this->forceDisconnect($connection); + + $connection->executeUpdate('UPDATE test SET updatedAt = CURRENT_TIMESTAMP WHERE id = 1'); + $this->assertSame(2, $connection->connectCount); + } + + public function testExecuteStatementShouldReconnect(): void + { + $connection = $this->getConnectedConnection(1); + $this->createTestTable($connection); + $this->assertSame(1, $connection->connectCount); + $this->forceDisconnect($connection); + + $connection->executeStatement('UPDATE test SET updatedAt = CURRENT_TIMESTAMP WHERE id = 1'); + $this->assertSame(2, $connection->connectCount); + } + + public function testShouldReconnectOnStatementExecuteError(): void + { + $connection = $this->getConnectedConnection(1); + $this->assertSame(1, $connection->connectCount); + $this->forceDisconnect($connection); + + $statement = $connection->prepare("SELECT 'foo'"); + $statement->execute(); + $result = $statement->fetchAll(); + $this->assertSame([['foo' => 'foo']], $result); + $this->assertSame(2, $connection->connectCount); + } + + public function testShouldResetStatementOnStatementExecuteError(): void + { + $connection = $this->getConnectedConnection(1); + $this->assertSame(1, $connection->connectCount); + $this->forceDisconnect($connection); + + $statement = $connection->prepare("SELECT 'foo', ?, ?, ?, ?"); + $statement->setFetchMode(\PDO::FETCH_NUM); + $statement->bindValue(1, 2); + $statement->bindValue(2, 'fooB'); + $statement->bindValue(3, 'fooC'); + $param1 = 5; + $statement->bindParam(4, $param1); + $statement->execute(); + $result = $statement->fetchAll(); + $this->assertSame([[ + 0 => 'foo', + 1 => '2', + 2 => 'fooB', + 3 => 'fooC', + 4 => '5', + ]], $result); + $this->assertSame(2, $connection->connectCount); + } + + public function testShouldReconnectOnStatementFetchAllAssociative(): void + { + $connection = $this->getConnectedConnection(1); + $this->assertSame(1, $connection->connectCount); + $this->forceDisconnect($connection); + + $statement = $connection->prepare("SELECT 'foo'"); + $statement->execute(); + $result = $statement->fetchAllAssociative(); + $this->assertSame([['foo' => 'foo']], $result); + $this->assertSame(2, $connection->connectCount); + } + + public function testShouldReconnectOnStatementFetchAllNumeric(): void + { + $connection = $this->getConnectedConnection(1); + $this->assertSame(1, $connection->connectCount); + $this->forceDisconnect($connection); + + $statement = $connection->prepare("SELECT 'foo'"); + $statement->execute(); + $result = $statement->fetchAllNumeric(); + $this->assertSame([['0' => 'foo']], $result); + $this->assertSame(2, $connection->connectCount); + } +} diff --git a/tests/functional/FunctionalTest.php b/tests/functional/FunctionalTest.php deleted file mode 100644 index ed82a3f..0000000 --- a/tests/functional/FunctionalTest.php +++ /dev/null @@ -1,113 +0,0 @@ - getenv('MYSQL_DRIVER') ?: $GLOBALS['db_driver'] ?? 'pdo_mysql', - 'dbname' => getenv('MYSQL_DBNAME') ?: $GLOBALS['db_dbname'] ?? 'test', - 'user' => getenv('MYSQL_USER') ?: $GLOBALS['db_user'] ?? 'root', - 'password' => getenv('MYSQL_PASS') ?: $GLOBALS['db_pass'] ?? '', - 'host' => getenv('MYSQL_HOST') ?: $GLOBALS['db_host'] ?? 'localhost', - 'port' => (int) (getenv('MYSQL_PORT') ?: $GLOBALS['db_port'] ?? 3306), - ]; - } - - private function createConnection(int $attempts): Connection - { - /** @var Connection $connection */ - $connection = DriverManager::getConnection(array_merge( - $this->getConnectionParams(), - [ - 'wrapperClass' => Connection::class, - 'driverClass' => Driver::class, - 'driverOptions' => array( - 'x_reconnect_attempts' => $attempts - ) - ] - )); - - $connection->executeStatement(<<<'TABLE' -CREATE TABLE IF NOT EXISTS test ( - id INT UNSIGNED AUTO_INCREMENT PRIMARY KEY, - updatedAt TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP -); -TRUNCATE test; -INSERT INTO test (id) VALUES (1); -TABLE - ); - - return $connection; - } - - private function setConnectionTimeout(\Doctrine\DBAL\Connection $connection, int $timeout): void - { - $connection->executeStatement('SET SESSION wait_timeout=' . $timeout); - $connection->executeStatement('SET SESSION interactive_timeout=' . $timeout); - } - - public function testExecuteQueryShouldNotReconnect(): void - { - $connection = $this->createConnection(0); - $this->assertSame(1, $connection->connectCount); - $this->setConnectionTimeout($connection, 2); - sleep(3); - - $this->expectException(DBALException::class); - - $connection->executeQuery('SELECT 1'); - } - - public function testExecuteQueryShouldReconnect(): void - { - $connection = $this->createConnection(1); - $this->assertSame(1, $connection->connectCount); - $this->setConnectionTimeout($connection, 2); - sleep(3); - $connection->executeQuery('SELECT 1')->execute(); - $this->assertSame(2, $connection->connectCount); - } - - public function testQueryShouldReconnect(): void - { - $connection = $this->createConnection(1); - $this->assertSame(1, $connection->connectCount); - $this->setConnectionTimeout($connection, 2); - sleep(3); - $connection->query('SELECT 1')->execute(); - $this->assertSame(2, $connection->connectCount); - } - - public function testExecuteUpdateShouldReconnect(): void - { - $connection = $this->createConnection(1); - $this->assertSame(1, $connection->connectCount); - $this->setConnectionTimeout($connection, 2); - sleep(3); - $connection->executeUpdate('UPDATE test SET updatedAt = CURRENT_TIMESTAMP WHERE id = 1'); - $this->assertSame(2, $connection->connectCount); - } - - public function testExecuteStatementShouldReconnect(): void - { - $connection = $this->createConnection(1); - $this->assertSame(1, $connection->connectCount); - $this->setConnectionTimeout($connection, 2); - sleep(3); - $connection->executeStatement('UPDATE test SET updatedAt = CURRENT_TIMESTAMP WHERE id = 1'); - $this->assertSame(2, $connection->connectCount); - } -} diff --git a/tests/functional/MysqliTest.php b/tests/functional/MysqliTest.php new file mode 100644 index 0000000..2918177 --- /dev/null +++ b/tests/functional/MysqliTest.php @@ -0,0 +1,28 @@ +getConnectionParams(), + [ + 'wrapperClass' => Connection::class, + 'driverClass' => Driver::class, + 'driverOptions' => array( + 'x_reconnect_attempts' => $attempts + ) + ] + )); + + return $connection; + } +} diff --git a/tests/functional/PDOMySqlTest.php b/tests/functional/PDOMySqlTest.php new file mode 100644 index 0000000..f30a9c8 --- /dev/null +++ b/tests/functional/PDOMySqlTest.php @@ -0,0 +1,28 @@ +getConnectionParams(), + [ + 'wrapperClass' => Connection::class, + 'driverClass' => Driver::class, + 'driverOptions' => array( + 'x_reconnect_attempts' => $attempts + ) + ] + )); + + return $connection; + } +} diff --git a/tests/unit/StatementTest.php b/tests/unit/StatementTest.php deleted file mode 100644 index 9020375..0000000 --- a/tests/unit/StatementTest.php +++ /dev/null @@ -1,178 +0,0 @@ -prophesize(Connection::class); - $connection - ->prepareUnwrapped($sql) - ->shouldBeCalledTimes(1); - - $statement = new Statement($sql, $connection->reveal()); - - $this->assertInstanceOf(Statement::class, $statement); - } - - public function test_retry(): void - { - $sql = 'SELECT :param'; - /** @var DriverStatement|ObjectProphecy $driverStatement1 */ - $driverStatement1 = $this->prophesize(DriverStatement::class); - /** @var DriverStatement|ObjectProphecy $driverStatement2 */ - $driverStatement2 = $this->prophesize(DriverStatement::class); - /** @var Connection|ObjectProphecy $connection */ - $connection = $this->prophesize(Connection::class); - $connection - ->prepareUnwrapped($sql) - ->willReturn($driverStatement1->reveal(), $driverStatement2->reveal()) - ->shouldBeCalledTimes(2); - - $statement = new Statement($sql, $connection->reveal()); - - $exception = new DBALException('Test'); - $driverStatement1->execute(['param' => 'value'])->willThrow($exception)->shouldBeCalledTimes(1); - - $connection->canTryAgain(0)->willReturn(true)->shouldBeCalledTimes(1); - $connection->isRetryableException($exception, $sql)->willReturn(true)->shouldBeCalledTimes(1); - - // retry - $connection->close()->shouldBeCalledTimes(1); - $driverStatement2->execute(['param' => 'value'])->willReturn(true)->shouldBeCalledTimes(1); - - $this->assertTrue($statement->execute(['param' => 'value'])); - } - - public function test_retry_with_state(): void - { - $sql = 'SELECT :value, :param'; - /** @var DriverStatement|ObjectProphecy $driverStatement1 */ - $driverStatement1 = $this->prophesize(DriverStatement::class); - /** @var DriverStatement|ObjectProphecy $driverStatement2 */ - $driverStatement2 = $this->prophesize(DriverStatement::class); - /** @var Connection|ObjectProphecy $connection */ - $connection = $this->prophesize(Connection::class); - $connection - ->prepareUnwrapped($sql) - ->willReturn($driverStatement1->reveal(), $driverStatement2) - ->shouldBeCalledTimes(2); - - $statement = new Statement($sql, $connection->reveal()); - - $param = 1; - $driverStatement1->bindParam('param', $param, PDO::PARAM_INT, null)->willReturn(true)->shouldBeCalledTimes(1); - $driverStatement1->bindValue('value', 'foo', PDO::PARAM_STR)->willReturn(true)->shouldBeCalledTimes(1); - $driverStatement1->setFetchMode(PDO::FETCH_COLUMN, 1, null)->willReturn(true)->shouldBeCalledTimes(1); - - $this->assertTrue($statement->bindParam('param', $param, PDO::PARAM_INT)); - $this->assertTrue($statement->bindValue('value', 'foo')); - $this->assertTrue($statement->setFetchMode(PDO::FETCH_COLUMN, 1)); - - $exception = new DBALException('Test'); - $driverStatement1->execute(null)->willThrow($exception)->shouldBeCalledTimes(1); - - $connection->canTryAgain(0)->willReturn(true)->shouldBeCalledTimes(1); - $connection->isRetryableException($exception, $sql)->willReturn(true)->shouldBeCalledTimes(1); - - // retry - $connection->close()->shouldBeCalledTimes(1); - $driverStatement2->bindParam('param', $param, PDO::PARAM_INT, null)->willReturn(true)->shouldBeCalledTimes(1); - $driverStatement2->bindValue('value', 'foo', PDO::PARAM_STR)->willReturn(true)->shouldBeCalledTimes(1); - $driverStatement2->setFetchMode(PDO::FETCH_COLUMN, 1, null)->willReturn(true)->shouldBeCalledTimes(1); - $driverStatement2->execute(null)->willReturn(true)->shouldBeCalledTimes(1); - - $this->assertTrue($statement->execute()); - } - - public function test_retry_fails(): void - { - $sql = 'SELECT 1'; - /** @var DriverStatement|ObjectProphecy $driverStatement1 */ - $driverStatement1 = $this->prophesize(DriverStatement::class); - /** @var DriverStatement|ObjectProphecy $driverStatement2 */ - $driverStatement2 = $this->prophesize(DriverStatement::class); - /** @var Connection|ObjectProphecy $connection */ - $connection = $this->prophesize(Connection::class); - $connection - ->prepareUnwrapped($sql) - ->willReturn($driverStatement1->reveal(), $driverStatement2->reveal()) - ->shouldBeCalledTimes(2); - - $statement = new Statement($sql, $connection->reveal()); - - $exception1 = new DBALException('Test1'); - $driverStatement1->execute(null)->willThrow($exception1)->shouldBeCalledTimes(1); - - $connection->canTryAgain(0)->willReturn(true)->shouldBeCalledTimes(1); - $connection->isRetryableException($exception1, $sql)->willReturn(true)->shouldBeCalledTimes(1); - - // retry - $connection->close()->shouldBeCalledTimes(1); - $exception2 = new DBALException('Test2'); - $driverStatement2->execute(null)->willThrow($exception2)->shouldBeCalledTimes(1); - - $connection->canTryAgain(1)->willReturn(true)->shouldBeCalledTimes(1); - $connection->isRetryableException($exception2, $sql)->willReturn(false)->shouldBeCalledTimes(1); - - $this->expectException(get_class($exception2)); - $this->expectExceptionMessage($exception2->getMessage()); - - $this->assertTrue($statement->execute()); - } - - public function test_state_cache_only_changed_on_success(): void - { - $sql = 'SELECT :value, :param'; - /** @var DriverStatement|ObjectProphecy $driverStatement1 */ - $driverStatement1 = $this->prophesize(DriverStatement::class); - /** @var DriverStatement|ObjectProphecy $driverStatement2 */ - $driverStatement2 = $this->prophesize(DriverStatement::class); - /** @var Connection|ObjectProphecy $connection */ - $connection = $this->prophesize(Connection::class); - $connection - ->prepareUnwrapped($sql) - ->willReturn($driverStatement1->reveal(), $driverStatement2) - ->shouldBeCalledTimes(2); - - $statement = new Statement($sql, $connection->reveal()); - - $param = 1; - $driverStatement1->bindParam('param', $param, PDO::PARAM_INT, null)->willReturn(false)->shouldBeCalledTimes(1); - $driverStatement1->bindValue('value', 'foo', PDO::PARAM_STR)->willReturn(false)->shouldBeCalledTimes(1); - $driverStatement1->setFetchMode(PDO::FETCH_COLUMN, 1, null)->willReturn(false)->shouldBeCalledTimes(1); - - $this->assertFalse($statement->bindParam('param', $param, PDO::PARAM_INT)); - $this->assertFalse($statement->bindValue('value', 'foo')); - $this->assertFalse($statement->setFetchMode(PDO::FETCH_COLUMN, 1)); - - $exception = new DBALException('Test'); - $driverStatement1->execute(null)->willThrow($exception)->shouldBeCalledTimes(1); - - $connection->canTryAgain(0)->willReturn(true)->shouldBeCalledTimes(1); - $connection->isRetryableException($exception, $sql)->willReturn(true)->shouldBeCalledTimes(1); - - // retry - $connection->close()->shouldBeCalledTimes(1); - $driverStatement2->bindParam(Argument::cetera())->shouldNotBeCalled(); - $driverStatement2->bindValue(Argument::cetera())->shouldNotBeCalled(); - $driverStatement2->setFetchMode(Argument::cetera())->shouldNotBeCalled(); - $driverStatement2->execute(null)->willReturn(true)->shouldBeCalledTimes(1); - - $this->assertTrue($statement->execute()); - } -}