Bicycle/Services/DataBase/Classes/PdoDriver.php
2026-08-10 23:24:03 +03:00

210 lines
6.3 KiB
PHP
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

<?php
/**
* @package Bicycle
* @author Egor Isaev
* @description PdoDriver.php
* @copyright (c) 07/08/2026
*/
namespace Services\DataBase\Classes;
use PDO;
use PDOException;
use PDOStatement;
use System\Classes\MyException;
use Throwable;
/**
* Обёртка над PDO: ленивое подключение (с перебором хостов при отказе — failover),
* запросы через Statement (см. Statement::showQuery()) и вложенные транзакции через SAVEPOINT.
*/
class PdoDriver
{
/** @var PDO|null Подключение; null до первого обращения (connect()/pdo()) */
protected ?PDO $_pdo = null;
/** @var int Текущий уровень вложенности транзакции (0 — вне транзакции) */
protected int $_transaction_level = 0;
/**
* @param array $config Параметры подключения: host|hosts, port, dbname, user, password, charset (по умолчанию utf8mb4)
*/
public function __construct(protected array $config)
{
}
/**
* Подключается к первому доступному хосту из host|hosts (failover).
* Не делает ничего, если подключение уже установлено.
*
* @return void
*/
public function connect(): void
{
if ($this->_pdo !== null) {
return;
}
$hosts = $this->config['hosts'] ?? [$this->config['host'] ?? 'localhost'];
$port = $this->config['port'] ?? null;
$dbname = $this->config['dbname'] ?? '';
$user = $this->config['user'] ?? '';
$password = $this->config['password'] ?? '';
$charset = $this->config['charset'] ?? 'utf8mb4';
$errors = [];
foreach ($hosts as $host) {
$dsn = "mysql:host=$host;dbname=$dbname;charset=$charset" . ($port !== null ? ";port=$port" : '');
try {
$pdo = new PDO($dsn, $user, $password, [
PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION,
PDO::ATTR_DEFAULT_FETCH_MODE => PDO::FETCH_ASSOC,
]);
$pdo->setAttribute(PDO::ATTR_STATEMENT_CLASS, [Statement::class, [$this]]);
$this->_pdo = $pdo;
return;
} catch (PDOException $e) {
$errors[] = "$host: {$e->getMessage()}";
}
}
throw new MyException(
'Не удалось подключиться ни к одному хосту БД: :errors', [':errors' => implode('; ', $errors)]
);
}
/**
* @return PDO Подключается при первом обращении
*/
public function pdo(): PDO
{
$this->connect();
return $this->_pdo;
}
/**
* Готовит выражение (через Statement — см. PDO::ATTR_STATEMENT_CLASS).
*
* @param string $sql SQL-запрос
* @return false|PDOStatement
*/
public function prepare(string $sql): false|PDOStatement
{
return $this->pdo()->prepare($sql);
}
/**
* Готовит и выполняет запрос (профилирование времени — см. Statement::execute()).
*
* @param string $sql SQL-запрос
* @param array $params Параметры для execute()
* @return false|PDOStatement
*/
public function query(string $sql, array $params = []): false|PDOStatement
{
$statement = $this->prepare($sql);
$statement->execute($params);
return $statement;
}
/**
* @param string $sql SQL-запрос
* @return int Число задетых строк
*/
public function exec(string $sql): int
{
return $this->pdo()->exec($sql);
}
/**
* Начинает транзакцию; на вложенном уровне — SAVEPOINT.
*
* @return bool
*/
public function beginTransaction(): bool
{
$result = $this->_transaction_level === 0
? $this->pdo()->beginTransaction()
: (bool) $this->pdo()->exec('SAVEPOINT sp_' . $this->_transaction_level);
$this->_transaction_level++;
return $result;
}
/**
* Фиксирует транзакцию; на вложенном уровне — RELEASE SAVEPOINT.
*
* @return bool
*/
public function commit(): bool
{
$this->_transaction_level--;
return $this->_transaction_level === 0
? $this->pdo()->commit()
: (bool) $this->pdo()->exec('RELEASE SAVEPOINT sp_' . $this->_transaction_level);
}
/**
* Откатывает транзакцию; на вложенном уровне — ROLLBACK TO SAVEPOINT.
*
* @return bool
*/
public function rollback(): bool
{
$this->_transaction_level--;
return $this->_transaction_level === 0
? $this->pdo()->rollBack()
: (bool) $this->pdo()->exec('ROLLBACK TO SAVEPOINT sp_' . $this->_transaction_level);
}
/**
* @return bool Идёт ли сейчас транзакция (на любом уровне вложенности)
*/
public function inTransaction(): bool
{
return $this->_transaction_level > 0;
}
/**
* Выполняет $callback в транзакции: begin → callback → commit;
* при исключении — rollback и повторный throw. Вложенные вызовы (в том числе
* из разных Repository на одном PdoDriver) используют SAVEPOINT прозрачно.
*
* @param callable $callback
* @return mixed Результат $callback
* @throws Throwable
*/
public function transaction(callable $callback): mixed
{
$this->beginTransaction();
try {
$result = $callback($this);
$this->commit();
return $result;
} catch (Throwable $e) {
$this->rollback();
throw $e;
}
}
/**
* @return string|false ID последней вставленной строки
*/
public function lastInsertId(): string|false
{
return $this->pdo()->lastInsertId();
}
}