_pdo !== null) { return; } $dialect = $this->config['dialect'] ?? 'mysql'; $options = [ PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION, PDO::ATTR_DEFAULT_FETCH_MODE => PDO::FETCH_ASSOC, ]; if ($dialect === 'sqlite') { $this->_pdo = $this->connectPdo($this->buildDsn($dialect, null), $options); return; } $hosts = $this->config['hosts'] ?? [$this->config['host'] ?? 'localhost']; $errors = []; foreach ($hosts as $host) { try { $this->_pdo = $this->connectPdo($this->buildDsn($dialect, $host), $options); return; } catch (PDOException $e) { $errors[] = "$host: {$e->getMessage()}"; } } throw new MyException( 'Не удалось подключиться ни к одному хосту БД: :errors', [':errors' => implode('; ', $errors)] ); } /** * Собирает DSN под конкретный PDO-диалект. * * @param string $dialect mysql|pgsql|sqlite * @param string|null $host Хост; не используется для sqlite * @return string */ protected function buildDsn(string $dialect, ?string $host): string { $dbname = $this->config['dbname'] ?? ''; $port = $this->config['port'] ?? null; $port_suffix = $port !== null ? ";port=$port" : ''; return match ($dialect) { 'sqlite' => "sqlite:$dbname", 'pgsql' => "pgsql:host=$host;dbname=$dbname$port_suffix", default => "mysql:host=$host;dbname=$dbname;charset=" . ($this->config['charset'] ?? 'utf8mb4') . $port_suffix, }; } /** * Открывает PDO-подключение по готовому DSN и настраивает Statement::class. * sqlite не использует user/password — PDO принимает для него null. * * @param string $dsn * @param array $options * @return PDO */ protected function connectPdo(string $dsn, array $options): PDO { $is_sqlite = str_starts_with($dsn, 'sqlite:'); $user = $is_sqlite ? null : ($this->config['user'] ?? ''); $password = $is_sqlite ? null : ($this->config['password'] ?? ''); $pdo = new PDO($dsn, $user, $password, $options); $pdo->setAttribute(PDO::ATTR_STATEMENT_CLASS, [Statement::class, [$this]]); return $pdo; } /** * @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 на одном PdoConnection) используют 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(); } }