From 9f1fabac998ab514a84530eb83a9758e0d88dd45 Mon Sep 17 00:00:00 2001 From: Matthias Neid Date: Tue, 3 Feb 2026 16:05:37 +0100 Subject: [PATCH 1/2] refactor mysqli, use object, reconnect on connection error --- .../Mysqli/MysqlConnectionFailedException.php | 13 --- src/Driver/Mysqli/MysqlException.php | 38 -------- src/Driver/Mysqli/Mysqli.php | 94 ++++++++++++++----- .../Mysqli/MysqliConnectionException.php | 7 ++ src/Driver/Mysqli/MysqliException.php | 25 +++++ .../Exception/RedisConnectionException.php | 2 +- src/Driver/Redis/Redis.php | 8 +- 7 files changed, 105 insertions(+), 82 deletions(-) delete mode 100644 src/Driver/Mysqli/MysqlConnectionFailedException.php delete mode 100644 src/Driver/Mysqli/MysqlException.php create mode 100644 src/Driver/Mysqli/MysqliConnectionException.php create mode 100644 src/Driver/Mysqli/MysqliException.php diff --git a/src/Driver/Mysqli/MysqlConnectionFailedException.php b/src/Driver/Mysqli/MysqlConnectionFailedException.php deleted file mode 100644 index d9efdd9..0000000 --- a/src/Driver/Mysqli/MysqlConnectionFailedException.php +++ /dev/null @@ -1,13 +0,0 @@ -getCode(), $previous); - } -} diff --git a/src/Driver/Mysqli/MysqlException.php b/src/Driver/Mysqli/MysqlException.php deleted file mode 100644 index 3a9d9c1..0000000 --- a/src/Driver/Mysqli/MysqlException.php +++ /dev/null @@ -1,38 +0,0 @@ -connection || !@mysqli_ping($this->connection)) { - $this->connection = mysqli_connect($this->host, $this->username, $this->password, $this->database, $this->port, $this->socket); - if (!$this->connection) { - throw new MysqlConnectionFailedException(new MysqlException($this->connection)); - } + if ($this->connection) { + return; + } + + mysqli_report(MYSQLI_REPORT_ERROR | MYSQLI_REPORT_STRICT); + try { + $this->connection = new \mysqli($this->host, $this->username, $this->password, $this->database, $this->port, $this->socket); + } catch (mysqli_sql_exception $e) { + throw MysqliConnectionException::fromException($e); } } + protected function reconnect(): void + { + $this->connection = null; + $this->connect(); + } + /** * Execute a mysql query * * @param string $query * @return bool|mysqli_result - * @throws MysqlConnectionFailedException if connecting to the mysql database fails - * @throws MysqlException if a mysql error occurs while executing the query + * @throws MysqliConnectionException if connecting to the mysql database fails + * @throws MysqliException if a mysql error occurs while executing the query */ protected function rawQuery(string $query): mysqli_result|true { $this->connect(); - $result = mysqli_query($this->connection, $query); - MysqlException::checkConnection($this->connection); - return $result; + $retries = $this->connectionRetries; + while (true) { + try { + return $this->connection->query($query); + } catch (mysqli_sql_exception $e) { + // no more retries left + if ($retries <= 0) { + throw MysqliException::fromException($e); + } + + // connection error, try to reconnect and retry + if ($e->getCode() === 2006 || $e->getCode() === 2013) { + $this->reconnect(); + $retries--; + continue; + } + + // other error, throw exception + throw MysqliException::fromException($e); + } + } } /** @@ -130,8 +162,8 @@ protected function rawQuery(string $query): mysqli_result|true * * @param ModelInterface $model * @return bool - * @throws MysqlConnectionFailedException if connecting to the mysql database fails - * @throws MysqlException if a mysql error occurs while executing the query + * @throws MysqliConnectionException if connecting to the mysql database fails + * @throws MysqliException if a mysql error occurs while executing the query */ public function save(ModelInterface $model): bool { @@ -177,8 +209,8 @@ public function save(ModelInterface $model): bool * @param mixed $id * @param ModelInterface|null $model * @return ModelInterface|null - * @throws MysqlConnectionFailedException if connecting to the mysql database fails - * @throws MysqlException if a mysql error occurs while executing the query + * @throws MysqliConnectionException if connecting to the mysql database fails + * @throws MysqliException if a mysql error occurs while executing the query */ public function get(string $modelClass, mixed $id, ?ModelInterface $model = null): ?ModelInterface { @@ -204,8 +236,8 @@ public function get(string $modelClass, mixed $id, ?ModelInterface $model = null * * @param ModelInterface $model * @return bool - * @throws MysqlConnectionFailedException if connecting to the mysql database fails - * @throws MysqlException if a mysql error occurs while executing the query + * @throws MysqliConnectionException if connecting to the mysql database fails + * @throws MysqliException if a mysql error occurs while executing the query */ public function delete(ModelInterface $model): bool { @@ -224,8 +256,8 @@ public function delete(ModelInterface $model): bool * * @param Query $query * @return QueryResult - * @throws MysqlConnectionFailedException if connecting to the mysql database fails - * @throws MysqlException if a mysql error occurs while executing the query + * @throws MysqliConnectionException if connecting to the mysql database fails + * @throws MysqliException if a mysql error occurs while executing the query */ public function query(Query $query): QueryResult { @@ -264,7 +296,7 @@ public function query(Query $query): QueryResult /** * @param string|null $host - * @return Mysqli + * @return $this */ public function setHost(?string $host): Mysqli { @@ -274,7 +306,7 @@ public function setHost(?string $host): Mysqli /** * @param int|null $port - * @return Mysqli + * @return $this */ public function setPort(?int $port): Mysqli { @@ -284,7 +316,7 @@ public function setPort(?int $port): Mysqli /** * @param string|null $username - * @return Mysqli + * @return $this */ public function setUsername(?string $username): Mysqli { @@ -294,7 +326,7 @@ public function setUsername(?string $username): Mysqli /** * @param string|null $password - * @return Mysqli + * @return $this */ public function setPassword(?string $password): Mysqli { @@ -304,7 +336,7 @@ public function setPassword(?string $password): Mysqli /** * @param string|null $socket - * @return Mysqli + * @return $this */ public function setSocket(?string $socket): Mysqli { @@ -314,11 +346,21 @@ public function setSocket(?string $socket): Mysqli /** * @param string $database - * @return Mysqli + * @return $this */ public function setDatabase(string $database): Mysqli { $this->database = $database; return $this; } + + /** + * @param int $connectionRetries + * @return $this + */ + public function setConnectionRetries(int $connectionRetries): static + { + $this->connectionRetries = $connectionRetries; + return $this; + } } diff --git a/src/Driver/Mysqli/MysqliConnectionException.php b/src/Driver/Mysqli/MysqliConnectionException.php new file mode 100644 index 0000000..92507af --- /dev/null +++ b/src/Driver/Mysqli/MysqliConnectionException.php @@ -0,0 +1,7 @@ +getCode() . ": " . $exception->getMessage(), $exception->getCode(), $exception); + } + + public function __construct(string $message, int $code = 0, ?Throwable $previous = null) + { + parent::__construct($message, $code, $previous); + } +} diff --git a/src/Driver/Redis/Exception/RedisConnectionException.php b/src/Driver/Redis/Exception/RedisConnectionException.php index 69b6d89..8b693d8 100644 --- a/src/Driver/Redis/Exception/RedisConnectionException.php +++ b/src/Driver/Redis/Exception/RedisConnectionException.php @@ -15,7 +15,7 @@ class RedisConnectionException extends RedisModelException * @param Throwable $exception * @return static */ - static function wrapping(Throwable $exception): static + static function fromException(Throwable $exception): static { return new static($exception->getMessage(), $exception->getCode(), $exception); } diff --git a/src/Driver/Redis/Redis.php b/src/Driver/Redis/Redis.php index bf56a63..ddea7a9 100644 --- a/src/Driver/Redis/Redis.php +++ b/src/Driver/Redis/Redis.php @@ -83,7 +83,7 @@ protected function connect(): void $this->connection->connect($this->socket); } } catch (RedisException $e) { - throw RedisConnectionException::wrapping($e); + throw RedisConnectionException::fromException($e); } } } @@ -119,7 +119,7 @@ public function save(ModelInterface $model): bool $this->connection->set($key, json_encode($model), $model->getCacheTime()); RedisQueryException::checkConnection($this->connection); } catch (RedisException $e) { - throw RedisConnectionException::wrapping($e); + throw RedisConnectionException::fromException($e); } return true; } @@ -144,7 +144,7 @@ public function get(string $modelClass, mixed $id, ?ModelInterface $model = null $rawData = $this->connection->get($this->generateCacheKey($modelClass, $id)); RedisQueryException::checkConnection($this->connection); } catch (RedisException $e) { - throw RedisConnectionException::wrapping($e); + throw RedisConnectionException::fromException($e); } if (!$rawData) { @@ -181,7 +181,7 @@ public function delete(ModelInterface $model): bool $this->connection->del($key); RedisQueryException::checkConnection($this->connection); } catch (RedisException $e) { - throw RedisConnectionException::wrapping($e); + throw RedisConnectionException::fromException($e); } return true; } From 89213b7ce2748108f7f814939201e1eb7ad69585 Mon Sep 17 00:00:00 2001 From: Matthias Neid Date: Tue, 3 Feb 2026 16:17:44 +0100 Subject: [PATCH 2/2] also handle connection errors when escaping, replace more function calls --- src/Driver/Mysqli/Mysqli.php | 52 ++++++++++++++++++++++++++++-------- 1 file changed, 41 insertions(+), 11 deletions(-) diff --git a/src/Driver/Mysqli/Mysqli.php b/src/Driver/Mysqli/Mysqli.php index c8ca986..0d66a41 100644 --- a/src/Driver/Mysqli/Mysqli.php +++ b/src/Driver/Mysqli/Mysqli.php @@ -157,6 +157,38 @@ protected function rawQuery(string $query): mysqli_result|true } } + /** + * Escape a string for use in a mysql query + * + * @param string $data + * @return string + */ + protected function escape(string $data): string + { + $this->connect(); + $retries = $this->connectionRetries; + while (true) { + try { + return $this->connection->real_escape_string($data); + } catch (mysqli_sql_exception $e) { + // no more retries left + if ($retries <= 0) { + throw MysqliException::fromException($e); + } + + // connection error, try to reconnect and retry + if ($e->getCode() === 2006 || $e->getCode() === 2013) { + $this->reconnect(); + $retries--; + continue; + } + + // other error, throw exception + throw MysqliException::fromException($e); + } + } + } + /** * Save the model * @@ -181,7 +213,7 @@ public function save(ModelInterface $model): bool } else if (is_null($value)) { $values[] = "NULL"; } else { - $values[] = "'" . mysqli_real_escape_string($this->connection, $value) . "'"; + $values[] = "'" . $this->escape($value) . "'"; } } @@ -192,7 +224,7 @@ public function save(ModelInterface $model): bool } else if (is_null($modelValue)) { $updates[] = "`" . $column . "`=NULL"; } else { - $updates[] = "`" . $column . "`='" . mysqli_real_escape_string($this->connection, $modelValue) . "'"; + $updates[] = "`" . $column . "`='" . $this->escape($modelValue) . "'"; } } @@ -217,14 +249,14 @@ public function get(string $modelClass, mixed $id, ?ModelInterface $model = null $this->connect(); $table = $modelClass::getName(); - $escapedId = mysqli_real_escape_string($this->connection, $id); + $escapedId = $this->escape($id); $query = "SELECT * FROM `" . $table . "` WHERE `" . $modelClass::getIdField() . "` = '" . $escapedId . "'"; $result = $this->rawQuery($query); - if (!$result || mysqli_num_rows($result) === 0) { + if (!$result || $result->num_rows === 0) { return null; } - $row = mysqli_fetch_assoc($result); + $row = $result->fetch_assoc(); if ($model) { return $model->applyData($row); } @@ -244,7 +276,7 @@ public function delete(ModelInterface $model): bool $this->connect(); $table = $model::getName(); - $id = mysqli_real_escape_string($this->connection, $model->getId()); + $id = $this->escape($model->getId()); $query = "DELETE FROM `" . $table . "` WHERE `" . $model->getIdField() . "` = '" . $id . "'"; $this->rawQuery($query); @@ -263,9 +295,7 @@ public function query(Query $query): QueryResult { $this->connect(); - $generator = new SQL(function ($value) { - return mysqli_real_escape_string($this->connection, $value); - }); + $generator = new SQL($this->escape(...)); $queryString = $generator->generate($query); @@ -274,7 +304,7 @@ public function query(Query $query): QueryResult $result = new QueryResult(); $result->setQueryString($queryString); if ($query instanceof UpdateQuery || $query instanceof DeleteQuery) { - $result->setAffectedRows(mysqli_affected_rows($this->connection)); + $result->setAffectedRows($this->connection->affected_rows); return $result; } @@ -282,7 +312,7 @@ public function query(Query $query): QueryResult return $result; } - while ($row = mysqli_fetch_assoc($rawQueryResult)) { + while ($row = $rawQueryResult->fetch_assoc()) { /** @var class-string $modelClass */ $modelClass = $query->modelClassName; $model = $modelClass::getModelFromData($row);