Реализация класса для работы с базой данных в PHP: от простого к продвинутому

Раздел: Базы данных и PHP -> Библиотеки PHP

Класс для работы с базой данных в PHP: обзор решений

При разработке на PHP часто возникает задача инкапсулировать работу с базой данных в отдельный класс. Это улучшает поддержку кода, безопасность и переиспользование. Ниже рассмотрены несколько подходов с примерами и типичными ошибками.

Как создать простой класс для PDO?

Наиболее современный и универсальный вариант - использование расширения PDO, которое поддерживает множество СУБД. Класс оборачивает PDO и добавляет удобные методы.


class Database {
    private $pdo;
    public function __construct($dsn, $user, $pass) {
        try {
            $this->pdo = new PDO($dsn, $user, $pass);
            $this->pdo->setAttribute(PDO::ATTR_ERRMODE, PDO::ERRMODE_EXCEPTION);
        } catch (PDOException $e) {
            die('Connection failed: ' . $e->getMessage());
        }
    }
    public function select($sql, $params = []) {
        $stmt = $this->pdo->prepare($sql);
        $stmt->execute($params);
        return $stmt->fetchAll(PDO::FETCH_ASSOC);
    }
    public function insert($table, $data) {
        $keys = implode(',', array_keys($data));
        $placeholders = ':' . implode(',:', array_keys($data));
        $sql = "INSERT INTO $table ($keys) VALUES ($placeholders)";
        $this->pdo->prepare($sql)->execute($data);
        return $this->pdo->lastInsertId();
    }
    public function update($table, $data, $where, $whereParams = []) {
        $set = '';
        foreach ($data as $key => $value) {
            $set .= "$key = :$key,";
        }
        $set = rtrim($set, ',');
        $sql = "UPDATE $table SET $set WHERE $where";
        $stmt = $this->pdo->prepare($sql);
        return $stmt->execute(array_merge($data, $whereParams));
    }
    public function delete($table, $where, $params = []) {
        $sql = "DELETE FROM $table WHERE $where";
        $stmt = $this->pdo->prepare($sql);
        return $stmt->execute($params);
    }
}
  

Php db class (класс для работы с базой данных в php)

Пример использования:


$db = new Database('mysql:host=localhost;dbname=test', 'root', '');
$users = $db->select('SELECT * FROM users WHERE age > :age', ['age' => 18]);
  

Типичные проблемы:

  • Неправильный DSN - соединение не установится; проверяйте синтаксис.
  • Отсутствие обработки ошибок - используйте try-catch или ATTR_ERRMODE_EXCEPTION.
  • SQL-инъекция при конкатенации строк - всегда используйте подготовленные запросы.

Как реализовать класс для mysqli объектно?

Если проект жестко привязан к MySQL, можно использовать расширение mysqli. Его объектный стиль похож на PDO, но специфичен для MySQL.


class MySQL {
    private $mysqli;
    public function __construct($host, $user, $pass, $db) {
        $this->mysqli = new mysqli($host, $user, $pass, $db);
        if ($this->mysqli->connect_error) {
            die('Connect Error: ' . $this->mysqli->connect_error);
        }
        $this->mysqli->set_charset('utf8');
    }
    public function query($sql) {
        $result = $this->mysqli->query($sql);
        if (!$result) {
            throw new Exception('Query failed: ' . $this->mysqli->error);
        }
        return $result->fetch_all(MYSQLI_ASSOC);
    }
    public function prepareAndExecute($sql, $params) {
        $stmt = $this->mysqli->prepare($sql);
        if (!$stmt) {
            throw new Exception('Prepare failed: ' . $this->mysqli->error);
        }
        $types = str_repeat('s', count($params)); // упрощённо, все строки
        $stmt->bind_param($types, ...$params);
        $stmt->execute();
        $result = $stmt->get_result();
        return $result ? $result->fetch_all(MYSQLI_ASSOC) : [];
    }
}
  

Проблемы:

  • Нет автоматического экранирования при неиспользовании prepare - риск инъекции.
  • Типы параметров в bind_param нужно указывать вручную; неверный тип приводит к ошибке.
  • mysqli не поддерживает другие СУБД, что снижает гибкость.

Как сделать класс с паттерном Одиночка (Singleton) для соединения?

Одиночка гарантирует единственный экземпляр подключения к БД, что экономит ресурсы при многократных запросах в одном скрипте. Однако это затрудняет тестирование и создание нескольких разных соединений.


class DB {
    private static $instance = null;
    private $pdo;
    private function __construct() {
        $dsn = 'mysql:host=localhost;dbname=test';
        $this->pdo = new PDO($dsn, 'root', '');
    }
    public static function getInstance() {
        if (self::$instance === null) {
            self::$instance = new self();
        }
        return self::$instance;
    }
    public function getConnection() {
        return $this->pdo;
    }
}
  

Использование:


$pdo = DB::getInstance()->getConnection();
  

Недостатки:

  • Глобальное состояние - усложняет юнит-тестирование (нельзя подменить соединение).
  • Невозможно работать с несколькими разными базами одновременно.
  • Скрытая зависимость - класс, использующий Singleton, неявно привязан к нему.

Основное эффективное решение: гибридный класс с PDO, Query Builder и безопасностью

Рекомендуемый подход - класс, который использует PDO, реализует удобный Query Builder (без внешних зависимостей) и строгую типизацию. Ниже приведена базовая реализация.


class DatabaseManager {
    private PDO $pdo;
    public function __construct(string $dsn, string $user, string $pass, array $options = []) {
        $defaultOptions = [
            PDO::ATTR_ERRMODE            => PDO::ERRMODE_EXCEPTION,
            PDO::ATTR_DEFAULT_FETCH_MODE => PDO::FETCH_ASSOC,
            PDO::ATTR_EMULATE_PREPARES   => false,
        ];
        $this->pdo = new PDO($dsn, $user, $pass, array_merge($defaultOptions, $options));
    }
    public function select(string $table, array $columns = ['*'], array $where = [], string $extra = ''): array {
        $cols = implode(',', $columns);
        $sql = "SELECT $cols FROM $table";
        $params = [];
        if (!empty($where)) {
            $conditions = [];
            foreach ($where as $col => $val) {
                $conditions[] = "$col = :$col";
                $params[$col] = $val;
            }
            $sql .= ' WHERE ' . implode(' AND ', $conditions);
        }
        if ($extra) {
            $sql .= ' ' . $extra;
        }
        $stmt = $this->pdo->prepare($sql);
        $stmt->execute($params);
        return $stmt->fetchAll();
    }
    public function insert(string $table, array $data): int {
        $columns = implode(',', array_keys($data));
        $placeholders = ':' . implode(',:', array_keys($data));
        $sql = "INSERT INTO $table ($columns) VALUES ($placeholders)";
        $stmt = $this->pdo->prepare($sql);
        $stmt->execute($data);
        return (int) $this->pdo->lastInsertId();
    }
    public function update(string $table, array $data, array $where): int {
        $setParts = [];
        foreach ($data as $col => $val) {
            $setParts[] = "$col = :set_$col";
        }
        $whereParts = [];
        foreach ($where as $col => $val) {
            $whereParts[] = "$col = :where_$col";
        }
        $sql = "UPDATE $table SET " . implode(',', $setParts)
              . ' WHERE ' . implode(' AND ', $whereParts);
        $params = [];
        foreach ($data as $col => $val) {
            $params["set_$col"] = $val;
        }
        foreach ($where as $col => $val) {
            $params["where_$col"] = $val;
        }
        $stmt = $this->pdo->prepare($sql);
        $stmt->execute($params);
        return $stmt->rowCount();
    }
    public function delete(string $table, array $where): int {
        $conditions = [];
        foreach ($where as $col => $val) {
            $conditions[] = "$col = :$col";
        }
        $sql = "DELETE FROM $table WHERE " . implode(' AND ', $conditions);
        $stmt = $this->pdo->prepare($sql);
        $stmt->execute($where);
        return $stmt->rowCount();
    }
    public function rawQuery(string $sql, array $params = []): PDOStatement {
        $stmt = $this->pdo->prepare($sql);
        $stmt->execute($params);
        return $stmt;
    }
}
  

Примеры работы:


$db = new DatabaseManager('mysql:host=localhost;dbname=test', 'root', '');
$users = $db->select('users', ['id', 'name'], ['status' => 'active'], 'ORDER BY name');
$newId = $db->insert('users', ['name' => 'Иван', 'email' => 'ivan@example.com']);
$updatedRows = $db->update('users', ['name' => 'Петр'], ['id' => 1]);
$deletedRows = $db->delete('users', ['id' => 10]);
  

Возможные проблемы и их решения:

  • Проблема: Имена таблиц и столбцов не экранируются (риск инъекции через имена). Решение: не передавать пользовательские данные в имена таблиц/столбцов, либо использовать белые списки.
  • Проблема: Большое количество запросов в цикле. Решение: использовать массовые операции (bulk insert) или подготовить один запрос и менять параметры.
  • Проблема: Долгое выполнение сложных запросов. Решение: добавить кэширование результатов или использовать EXPLAIN для оптимизации.

Как построить класс Repository для отделения логики?

Repository - это паттерн, который изолирует логику работы с данными. Класс репозитория принимает объект DatabaseManager и предоставляет специфичные методы, например, getUserById() или saveUser(). Это улучшает организацию кода, но увеличивает количество классов.


class UserRepository {
    private DatabaseManager $db;
    public function __construct(DatabaseManager $db) {
        $this->db = $db;
    }
    public function findById(int $id): ?array {
        $result = $this->db->select('users', ['*'], ['id' => $id]);
        return $result[0] ?? null;
    }
    public function findAllActive(): array {
        return $this->db->select('users', ['*'], ['status' => 'active']);
    }
    public function save(array $user): int {
        if (isset($user['id'])) {
            $this->db->update('users', $user, ['id' => $user['id']]);
            return $user['id'];
        } else {
            return $this->db->insert('users', $user);
        }
    }
}
  

Типичные ошибки:

  • Дублирование кода запросов в разных репозиториях - выносить общие методы в базовый класс.
  • Игнорирование транзакций при сохранении связанных сущностей - использовать транзакции на уровне репозитория или сервисного слоя.
  • Отсутствие валидации данных перед записью - проверять данные на уровне репозитория или в отдельном классе.

Расширенные примеры для класса работы с БД

Пакетная вставка (Bulk Insert)

При вставке множества записей выгоднее использовать один INSERT с несколькими строками значений, а не цикл. Реализуем метод в классе.

Пример

public function bulkInsert(string $table, array $rows): int {
    if (empty($rows)) return 0;
    $columns = implode(',', array_keys($rows[0]));
    $placeholders = [];
    $values = [];
    $i = 0;
    foreach ($rows as $row) {
        $rowPlaceholders = [];
        foreach ($row as $col => $val) {
            $param = ":{$col}_{$i}";
            $rowPlaceholders[] = $param;
            $values[$param] = $val;
        }
        $placeholders[] = '(' . implode(',', $rowPlaceholders) . ')';
        $i++;
    }
    $sql = "INSERT INTO $table ($columns) VALUES " . implode(',', $placeholders);
    $stmt = $this->pdo->prepare($sql);
    $stmt->execute($values);
    return $stmt->rowCount();
}

Использование:

Пример

$rows = [
    ['name' => 'Alice', 'age' => 25],
    ['name' => 'Bob', 'age' => 30],
];
$inserted = $db->bulkInsert('users', $rows);
echo "Добавлено строк: $inserted";
Добавлено строк: 2

Транзакции с передачей замыкания

Удобно обернуть несколько операций в транзакцию, передав замыкание.

Пример

public function transaction(callable $callback) {
    $this->pdo->beginTransaction();
    try {
        $callback($this);
        $this->pdo->commit();
    } catch (Exception $e) {
        $this->pdo->rollBack();
        throw $e;
    }
}

Пример:

Пример

$db->transaction(function ($db) {
    $userId = $db->insert('users', ['name' => 'Vasya']);
    $db->insert('logs', ['action' => 'created_user', 'user_id' => $userId]);
});

Работа с JSON полями (MySQL 5.7+)

Используем JSON функции SQL через PDO.

Пример

$data = ['skills' => ['PHP', 'MySQL']];
$sql = "UPDATE users SET metadata = JSON_SET(metadata, '$.skills', :skills) WHERE id = :id";
$db->rawQuery($sql, ['skills' => json_encode($data['skills']), 'id' => 1]);

Получение данных:

Пример

$result = $db->select('users', ['id', 'metadata->"$.skills" as skills'], ['id' => 1]);
$skills = json_decode($result[0]['skills'], true);
print_r($skills);
Array
(
    [0] => PHP
    [1] => MySQL
)

Кэширование результатов запросов

Сохраняем результат в статический массив внутри метода select для повторного использования в рамках одного запроса.

Пример

private static array $cache = [];
public function selectWithCache(string $sql, array $params = [], int $ttl = 0): array {
    $key = md5($sql . serialize($params));
    if (isset(self::$cache[$key]) && ($ttl === 0 || time() - self::$cache[$key]['time'] < $ttl)) {
        return self::$cache[$key]['data'];
    }
    $stmt = $this->pdo->prepare($sql);
    $stmt->execute($params);
    $data = $stmt->fetchAll();
    self::$cache[$key] = ['data' => $data, 'time' => time()];
    return $data;
}

Пример вызова:

Пример

$users = $db->selectWithCache('SELECT * FROM users WHERE role = :role', ['role' => 'admin'], 60);
// При повторном вызове в течение 60 секунд вернёт закешированные данные (если скрипт не завершён).

Постраничный вывод (пагинация) через SQL LIMIT и OFFSET

Пример

public function paginate(string $table, int $page, int $perPage = 10, array $where = []): array {
    $offset = ($page - 1) * $perPage;
    $data = $this->select($table, ['*'], $where, "LIMIT $perPage OFFSET $offset");
    // Общее количество записей (упрощённо)
    $countSql = "SELECT COUNT(*) FROM $table";
    if (!empty($where)) {
        $conditions = [];
        foreach ($where as $col => $val) {
            $conditions[] = "$col = :$col";
        }
        $countSql .= ' WHERE ' . implode(' AND ', $conditions);
    }
    $total = $this->rawQuery($countSql, $where)->fetchColumn();
    return [
        'data'       => $data,
        'total'      => (int) $total,
        'page'       => $page,
        'perPage'    => $perPage,
        'lastPage'   => ceil($total / $perPage),
    ];
}

Использование:

Пример

$pageData = $db->paginate('users', 2, 20);
echo "Страница {$pageData['page']} из {$pageData['lastPage']}";

Класс для работы с базой данных в PHP - comments

En
Php db class (php)