pdo (PdoDriver, не сырой \PDO — но с тем же query()/prepare()/... * API, включая fetch-методы через Statement). */ abstract class Repository { /** @var string Имя первичного ключа */ public string $primary_col = 'id'; /** @var bool Первичный ключ — автоинкремент (влияет на processData()/create()) */ public bool $is_auto_increment = true; /** @var PdoDriver Подключение */ protected PdoDriver $pdo; /** @var string Таблица, с которой работает репозиторий */ protected string $table; /** @var class-string Класс, который должен возвращаться из get()/getItemWhere() */ private string $obj_class; /** * @param string $table Таблица * @param class-string $obj_class Класс строки (fetchObject()); по умолчанию — лёгкий Model * @param string|null $connection Имя подключения (Database::instance($connection)); null — по умолчанию */ public function __construct(string $table, string $obj_class = Model::class, ?string $connection = null) { $this->pdo = Database::instance($connection); $this->table = $table; $this->obj_class = $obj_class; } /** * Начинает транзакцию; на вложенном уровне — SAVEPOINT (см. PdoDriver — он же считает * вложенность: у него общий счётчик на всё подключение, а не свой на каждый Repository, * поэтому несколько репозиториев на одном Database::instance() безопасно участвуют * в одной внешней транзакции). * * @return bool */ public function beginTransaction(): bool { return $this->pdo->beginTransaction(); } /** * Фиксирует транзакцию; на вложенном уровне — RELEASE SAVEPOINT. * * @return bool */ public function commit(): bool { return $this->pdo->commit(); } /** * Откатывает транзакцию; на вложенном уровне — ROLLBACK TO SAVEPOINT. * * @return bool */ public function rollBack(): bool { return $this->pdo->rollback(); } /** * Выполняет $callback в транзакции: begin → callback → commit; * при исключении — rollBack и повторный throw. * * @param callable $callback * @return mixed * @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; } } /** * Получить 1 запись по первичному ключу. * * @param int|string $id * @return T|false */ public function get(int|string $id): mixed { $sql = "SELECT * FROM $this->table WHERE $this->primary_col = :id"; $stmt = $this->pdo->prepare($sql); $stmt->bindValue(':id', $id, is_int($id) ? PDO::PARAM_INT : PDO::PARAM_STR); $stmt->execute(); return $stmt->fetchObject($this->obj_class); } /** * Получить 1 запись по произвольному условию (без плейсхолдеров — $where подставляется в SQL как есть). * * @param string $where SQL-условие после WHERE * @return T|false|null null, если $where пуст */ public function getItemWhere(string $where): mixed { if ($where === '') { return null; } $sql = "SELECT * FROM $this->table WHERE $where"; return $this->pdo->query($sql)->fetchObject($this->obj_class); } /** * Получить список. * * @param string $cols 'id, col_1, col_2, ...' * @param array $where ['table_col' => 'value', ...] (объединяются через AND; значение 'IS NULL'/'IS NOT NULL' — без плейсхолдера) * @param string $order Колонка сортировки * @param string $sort ASC|DESC * @param int $fetch Режим PDO::FETCH_*; для FETCH_CLASS (по умолчанию) строки — $this->obj_class * @return bool|T[] */ public function getList( string $cols, array $where = [], string $order = '', string $sort = 'ASC', int $fetch = PDO::FETCH_CLASS ): bool|array { $str = ''; if (!empty($where)) { $conditions = []; foreach ($where as $key => $value) { if (is_string($value) && (strtoupper($value) === 'IS NOT NULL' || strtoupper($value) === 'IS NULL')) { $conditions[] = "$key $value"; unset($where[$key]); } else { $conditions[] = "$key = :$key"; } } $str = 'AND ' . implode(' AND ', $conditions); } $sql = "SELECT $cols FROM $this->table WHERE 1 $str"; if ($order !== '') { $sql .= " ORDER BY $order $sort"; } $stmt = $this->pdo->prepare($sql); $stmt->execute($where); return $fetch === PDO::FETCH_CLASS ? $stmt->fetchAll($fetch, $this->obj_class) : $stmt->fetchAll($fetch); } /** * Создать запись. * * @param array $data * @return int lastInsertId */ public function create(array $data): int { $create_data = $this->processData($data); $fields = implode(',', $create_data['fields']); $keys = implode(',', $create_data['keys']); $sql = "INSERT INTO $this->table ($fields) VALUES ($keys)"; $stmt = $this->pdo->prepare($sql); foreach ($create_data['bind'] as $param) { $stmt->bindValue(...$param); } $stmt->execute(); return (int)$this->pdo->lastInsertId(); } /** * Изменить запись по $primary_col. * * @param array $data [$primary_col обязателен] * @return int Число задетых строк */ public function update(array $data): int { if (!isset($data[$this->primary_col])) { throw new MyException('Массив не содержит :key', [':key' => $this->primary_col]); } $update_data = $this->processData($data); $set = implode(', ', $update_data['set']); $sql = "UPDATE $this->table SET $set WHERE $this->primary_col = :id"; $stmt = $this->pdo->prepare($sql); $stmt->bindValue(':id', $data[$this->primary_col], is_int($data[$this->primary_col]) ? PDO::PARAM_INT : PDO::PARAM_STR); foreach ($update_data['bind'] as $param) { $stmt->bindValue(...$param); } $stmt->execute(); return $stmt->rowCount(); } /** * Удалить запись по $primary_col. * * @param int|string $id * @return bool */ public function delete(int|string $id): bool { $sql = "DELETE FROM $this->table WHERE $this->primary_col = :id"; $stmt = $this->pdo->prepare($sql); $stmt->bindValue(':id', $id, is_int($id) ? PDO::PARAM_INT : PDO::PARAM_STR); return $stmt->execute(); } /** * Удалить записи по произвольному условию. * * @param string $where SQL-условие после WHERE (с плейсхолдерами) * @param array $params Параметры для плейсхолдеров * @return bool */ public function deleteWhere(string $where, array $params): bool { $sql = "DELETE FROM $this->table WHERE $where"; return $this->pdo->prepare($sql)->execute($params); } /** * Обработать данные для create()/update(): собрать fields/keys (INSERT) или set (UPDATE) * и bind-параметры с определением типа PDO::PARAM_* по значению. $primary_col пропускается, * если is_auto_increment (в create()); в set попадает, если явно присутствует в $data. * * @param array $data * @return array{fields?:array,keys?:array,set?:array,bind:array} */ protected function processData(array $data): array { $result = ['bind' => []]; foreach ($data as $key => $value) { if ($this->is_auto_increment && $key === $this->primary_col) { continue; } if (array_key_exists($this->primary_col, $data)) { $result['set'][] = "`$key` = :$key"; } $result['fields'][] = "`$key`"; $result['keys'][] = ":$key"; if (is_int($value)) { $result['bind'][] = [":$key", $value, PDO::PARAM_INT]; } elseif (is_string($value) || is_float($value)) { $result['bind'][] = [":$key", $value, PDO::PARAM_STR]; } elseif (is_bool($value)) { $result['bind'][] = [":$key", $value, PDO::PARAM_BOOL]; } elseif ($value === null) { $result['bind'][] = [":$key", $value, PDO::PARAM_NULL]; } elseif (is_array($value)) { try { $result['bind'][] = [":$key", json_encode($value, JSON_THROW_ON_ERROR), PDO::PARAM_STR]; } catch (JsonException) { $result['bind'][] = [":$key", null, PDO::PARAM_STR]; } } } return $result; } }