Bicycle/Services/DataBase/Classes/PdoDriver.php
Egor Isaev 9d4edf147a dev
2026-08-07 11:40:03 +03:00

218 lines
6.6 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;
/**
* Обёртка над 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, dbname, user, password, charset (по умолчанию utf8mb4)
*/
public function __construct(protected array $config)
{
}
/**
* Подключается к первому доступному хосту из host|hosts (failover).
* Не делает ничего, если подключение уже установлено.
*
* @return void
* @throws MyException Если не удалось подключиться ни к одному хосту
*/
public function connect(): void
{
if ($this->_pdo !== null) {
return;
}
$hosts = $this->config['hosts'] ?? [$this->config['host'] ?? 'localhost'];
$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";
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 Подключается при первом обращении
* @throws MyException
*/
public function pdo(): PDO
{
$this->connect();
return $this->_pdo;
}
/**
* Готовит выражение (через Statement — см. PDO::ATTR_STATEMENT_CLASS).
*
* @param string $sql SQL-запрос
* @return false|PDOStatement
* @throws MyException
*/
public function prepare(string $sql): false|PDOStatement
{
return $this->pdo()->prepare($sql);
}
/**
* Готовит, выполняет (с профилированием времени — см. ProfilerPDO) и возвращает выражение.
*
* @param string $sql SQL-запрос
* @param array $params Параметры для execute()
* @return false|PDOStatement
* @throws MyException
*/
public function query(string $sql, array $params = []): false|PDOStatement
{
$statement = $this->prepare($sql);
ProfilerPDO::wrap($sql, $params, static fn () => $statement->execute($params));
return $statement;
}
/**
* @param string $sql SQL-запрос
* @return int Число задетых строк
* @throws MyException
*/
public function exec(string $sql): int
{
return $this->pdo()->exec($sql);
}
/**
* Начинает транзакцию; на вложенном уровне — SAVEPOINT.
*
* @return bool
* @throws MyException
*/
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
* @throws MyException
*/
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
* @throws MyException
*/
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 MyException
*/
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 последней вставленной строки
* @throws MyException
*/
public function lastInsertId(): string|false
{
return $this->pdo()->lastInsertId();
}
}