_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); } /** * Готовит и выполняет запрос (профилирование времени — см. Statement::execute()). * * @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); $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(); } }