diff --git a/phpstan-baseline.neon b/phpstan-baseline.neon index 5107fd0..b18f68b 100644 --- a/phpstan-baseline.neon +++ b/phpstan-baseline.neon @@ -125,11 +125,6 @@ parameters: count: 1 path: src/Connection.php - - - message: "#^Method ipl\\\\Sql\\\\Connection\\:\\:__construct\\(\\) has parameter \\$config with no value type specified in iterable type iterable\\.$#" - count: 1 - path: src/Connection.php - - message: "#^Method ipl\\\\Sql\\\\Connection\\:\\:delete\\(\\) has parameter \\$table with no value type specified in iterable type array\\.$#" count: 1 @@ -215,11 +210,6 @@ parameters: count: 1 path: src/Connection.php - - - message: "#^Method ipl\\\\Sql\\\\Connection\\:\\:prepexec\\(\\) has parameter \\$values with no value type specified in iterable type array\\.$#" - count: 1 - path: src/Connection.php - - message: "#^Method ipl\\\\Sql\\\\Connection\\:\\:update\\(\\) has parameter \\$data with no value type specified in iterable type iterable\\.$#" count: 1 @@ -270,16 +260,6 @@ parameters: count: 1 path: src/Connection.php - - - message: "#^Parameter \\#2 \\$values of method ipl\\\\Sql\\\\Connection\\:\\:prepexec\\(\\) expects array\\|string\\|null, mixed given\\.$#" - count: 1 - path: src/Connection.php - - - - message: "#^Parameter \\#2 \\$values of method ipl\\\\Sql\\\\Connection\\:\\:yieldPairs\\(\\) expects array\\|null, mixed given\\.$#" - count: 1 - path: src/Connection.php - - message: "#^Property ipl\\\\Sql\\\\Connection\\:\\:\\$adapter \\(ipl\\\\Sql\\\\Contract\\\\Adapter\\) does not accept object\\.$#" count: 1 diff --git a/src/Connection.php b/src/Connection.php index de84c72..6238693 100644 --- a/src/Connection.php +++ b/src/Connection.php @@ -35,7 +35,7 @@ class Connection implements Quoter * * {@link init()} is called after construction. * - * @param Config|iterable $config + * @param Config|iterable $config * * @throws InvalidArgumentException If there's no adapter for the given database available */ @@ -302,6 +302,7 @@ public function yieldAll($stmt, ...$args) if (! empty($args)) { if (is_array($args[0])) { + /** @var array $values */ $values = array_shift($args); } } @@ -382,7 +383,7 @@ public function yieldPairs($stmt, array $values = null) * Prepare and execute the given statement * * @param Delete|Insert|Select|Update|string $stmt The SQL statement to prepare and execute - * @param string|array $values Values to bind to the statement, if any + * @param null|string|array $values Values to bind to the statement, if any * * @return PDOStatement */ diff --git a/src/RetryConnection.php b/src/RetryConnection.php new file mode 100644 index 0000000..0417447 --- /dev/null +++ b/src/RetryConnection.php @@ -0,0 +1,102 @@ +backoff = new ExponentialBackoff($numberRetries); + } + + /** + * Get whether the given (PDO) exception can be fixed by reconnecting to the database. + * + * @param Exception $err + * + * @return bool + */ + public static function isRetryable(Exception $err): bool + { + $message = $err->getMessage(); + foreach (static::$retryableErrors as $error) { + if (strpos($message, $error) !== false) { + return true; + } + } + + return false; + } + + public function prepexec($stmt, $values = null) + { + /** @var PDOStatement $result */ + $result = $this->backoff->retry(function (Exception $err = null) use ($stmt, $values) { + if ($err && ! static::isRetryable($err)) { + throw $err; + } + + if ($err) { + $this->disconnect(); + } + + return parent::prepexec($stmt, $values); + }); + + return $result; + } + + public function beginTransaction(): bool + { + /** @var bool $result */ + $result = $this->backoff->retry(function (Exception $err = null): bool { + if ($err && ! static::isRetryable($err)) { + throw $err; + } + + if ($err) { + $this->disconnect(); + } + + return parent::beginTransaction(); + }); + + return $result; + } +} diff --git a/tests/RetryConnectionTest.php b/tests/RetryConnectionTest.php new file mode 100644 index 0000000..05c2dbc --- /dev/null +++ b/tests/RetryConnectionTest.php @@ -0,0 +1,39 @@ +getConnection(); + + $this->assertTrue($db::isRetryable(new Exception('SQLState: Connection refused by the server'))); + $this->assertTrue($db::isRetryable(new Exception('SQLState: Error writing data to the connection'))); + $this->assertTrue($db::isRetryable(new Exception('SQLState: No such file or directory found'))); + + $this->assertFalse($db::isRetryable(new Exception('SQLState: Cannot start transaction'))); + $this->assertFalse($db::isRetryable(new Exception('Cannot establish the connection to SQL server'))); + $this->assertFalse($db::isRetryable(new Exception('Fatal error encountered during command execution'))); + } + + public function testExecutionRetriesGivesUpAfterMaxRetries() + { + $this->expectException(Exception::class); + $this->expectExceptionMessage('SQLSTATE[HY000] [2002] No such file or directory'); + + $this->getConnection(2)->transaction(function () { + }); + } + + protected function getConnection(int $retries = 1): RetryConnection + { + return new RetryConnection([ + 'db' => 'mysql', + 'dbname' => 'foo', + ], $retries); + } +}