Разработка классов-помощников для MySQL в PHP

Раздел: Работа с базами данных в PHP -> Выполнение SQL запросов

Классы для работы с MySQL в PHP: основные подходы

Как построить класс для безопасного выполнения SQL запросов с использованием PDO?

Наиболее эффективное решение основано на расширении PDO. Класс инкапсулирует соединение и предоставляет методы для типовых операций: выборка, вставка, обновление, удаление. Использование подготовленных выражений гарантирует защиту от SQL-инъекций.


class Database {
    private PDO $pdo;

    public function __construct(array $config) {
        $dsn = "mysql:host={$config['host']};dbname={$config['dbname']};charset=utf8mb4";
        $this->pdo = new PDO($dsn, $config['user'], $config['pass'], [
            PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION,
            PDO::ATTR_DEFAULT_FETCH_MODE => PDO::FETCH_ASSOC,
            PDO::ATTR_EMULATE_PREPARES => false,
        ]);
    }

    public function query(string $sql, array $params = []): PDOStatement {
        $stmt = $this->pdo->prepare($sql);
        $stmt->execute($params);
        return $stmt;
    }

    public function fetchAll(string $sql, array $params = []): array {
        return $this->query($sql, $params)->fetchAll();
    }

    public function fetchOne(string $sql, array $params = []): ?array {
        $row = $this->query($sql, $params)->fetch();
        return $row ?: null;
    }

    public function insert(string $table, array $data): int {
        $columns = implode(', ', array_keys($data));
        $placeholders = implode(', ', array_fill(0, count($data), '?'));
        $sql = "INSERT INTO $table ($columns) VALUES ($placeholders)";
        $this->query($sql, array_values($data));
        return (int) $this->pdo->lastInsertId();
    }

    public function update(string $table, array $data, string $where, array $whereParams = []): int {
        $set = implode(', ', array_map(fn($col) => "$col = ?", array_keys($data)));
        $sql = "UPDATE $table SET $set WHERE $where";
        $stmt = $this->query($sql, array_merge(array_values($data), $whereParams));
        return $stmt->rowCount();
    }

    public function delete(string $table, string $where, array $whereParams = []): int {
        $sql = "DELETE FROM $table WHERE $where";
        $stmt = $this->query($sql, $whereParams);
        return $stmt->rowCount();
    }
}
  

Engine classes mysql php (классы для работы с mysql в php)

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


$db = new Database([
    'host' => 'localhost',
    'dbname' => 'test',
    'user' => 'root',
    'pass' => ''
]);

$users = $db->fetchAll('SELECT * FROM users WHERE age > ?', [18]);
$user = $db->fetchOne('SELECT * FROM users WHERE id = ?', [1]);
$newId = $db->insert('users', ['name' => 'Иван', 'age' => 25]);
$db->update('users', ['age' => 26], 'id = ?', [1]);
$db->delete('users', 'id = ?', [1]);
  
Типичные ошибки: Неправильная передача параметров (порядок для позиционных плейсхолдеров), использование эмуляции подготовленных выражений (PDO::ATTR_EMULATE_PREPARES => true) может привести к инъекциям при некоторых кодировках, отсутствие обработки исключений при недоступности сервера.

Как реализовать единый доступ к базе данных через синглтон?

Синглтон гарантирует, что в приложении существует только один экземпляр класса Database. Это удобно для небольших проектов, где не требуется несколько соединений.


class DatabaseSingleton {
    private static ?self $instance = null;
    private PDO $pdo;

    private function __construct(array $config) {
        // аналогично Database
    }

    public static function getInstance(array $config = []): self {
        if (self::$instance === null) {
            self::$instance = new self($config);
        }
        return self::$instance;
    }

    // методы query, fetchAll и т.д. такие же
}
  

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


$db = DatabaseSingleton::getInstance($config);
$users = $db->fetchAll('SELECT * FROM users');
  
Проблемы: Синглтон усложняет тестирование (зависимость от глобального состояния), не позволяет переключать базы данных во время выполнения, трудно управлять транзакциями при вложенных вызовах. Рекомендуется использовать контейнер внедрения зависимостей.

Как обернуть mysqli в класс?

Для проектов, где PDO недоступен (устаревшие версии PHP), можно создать класс на основе mysqli. Реализация требует внимания к подготовленным выражениям и обработке ошибок.


class MySQLiWrapper {
    private mysqli $mysqli;

    public function __construct(array $config) {
        $this->mysqli = new mysqli($config['host'], $config['user'], $config['pass'], $config['dbname']);
        if ($this->mysqli->connect_errno) {
            throw new RuntimeException('Ошибка подключения: ' . $this->mysqli->connect_error);
        }
        $this->mysqli->set_charset('utf8mb4');
    }

    public function query(string $sql, array $params = []): mysqli_stmt {
        $stmt = $this->mysqli->prepare($sql);
        if ($stmt === false) {
            throw new RuntimeException('Ошибка подготовки: ' . $this->mysqli->error);
        }
        if (!empty($params)) {
            $types = str_repeat('s', count($params)); // упрощенно: все строки
            $stmt->bind_param($types, ...$params);
        }
        $stmt->execute();
        return $stmt;
    }

    public function fetchAll(string $sql, array $params = []): array {
        $stmt = $this->query($sql, $params);
        $result = $stmt->get_result();
        return $result->fetch_all(MYSQLI_ASSOC);
    }

    // остальные методы аналогично
}
  

Недостаток: связывание параметров требует указания типов, что усложняет универсальность. При передаче большого количества параметров можно использовать отражение для определения типов.

Ошибки: Неверное количество типов в bind_param, порядок параметров, игнорирование ошибок запроса (необходимо проверять $stmt->error).

Как применить паттерн Репозиторий для отделения логики запросов?

Репозиторий абстрагирует работу с конкретной таблицей, предоставляя методы, соответствующие предметной области. Класс Database используется внутри репозитория как низкоуровневое API.


class UserRepository {
    private Database $db;

    public function __construct(Database $db) {
        $this->db = $db;
    }

    public function findById(int $id): ?array {
        return $this->db->fetchOne('SELECT * FROM users WHERE id = ?', [$id]);
    }

    public function findActive(): array {
        return $this->db->fetchAll('SELECT * FROM users WHERE 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);
        }
    }

    public function remove(int $id): void {
        $this->db->delete('users', 'id = ?', [$id]);
    }
}
  

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


$db = new Database($config);
$userRepo = new UserRepository($db);
$user = $userRepo->findById(42);
$userRepo->save(['name' => 'Новый пользователь', 'age' => 30]);
  
Сложности: Репозиторий часто смешивает логику запросов с бизнес-логикой. Рекомендуется возвращать объекты, а не массивы. Необходимо следить за консистентностью транзакций на уровне сервисов.

Расширенные примеры использования классов MySQL

Как выполнять транзакции с точками сохранения?

Пример

$db->beginTransaction();
$db->query('UPDATE accounts SET balance = balance - 100 WHERE id = ?', [1]);
$db->query('UPDATE accounts SET balance = balance + 100 WHERE id = ?', [2]);
$db->commit();
Транзакция завершена. Если возникает исключение, автоматический откат через PDO::ERRMODE_EXCEPTION.

Как реализовать массовую вставку (bulk insert) с повторным использованием подготовленного выражения?

Пример

$stmt = $db->getPdo()->prepare('INSERT INTO logs (message, level) VALUES (?, ?)');
$logs = [
    ['Старт', 'info'],
    ['Ошибка', 'error'],
    ['Завершение', 'info'],
];
foreach ($logs as $log) {
    $stmt->execute($log);
}
echo 'Вставлено ' . count($logs) . ' записей';
Вставлено 3 записей

Как использовать именованные плейсхолдеры для сложных запросов?

Пример

$sql = 'SELECT * FROM orders WHERE user_id = :uid AND status = :status ORDER BY created_at DESC LIMIT :limit';
$orders = $db->fetchAll($sql, ['uid' => 10, 'status' => 'pending', 'limit' => 5]);
Вернёт массив заказов с указанными параметрами. Именованные плейсхолдеры повышают читаемость.

Как обработать NULL значения в параметрах?

Пример

$db->insert('users', ['name' => 'Петр', 'email' => null]);
// PDO корректно передаёт NULL. В SQL значение будет NULL.

Как получить затронутые строки после UPDATE?

Пример

$affected = $db->update('products', ['price' => 99.99], 'category_id = ?', [5]);
echo "Обновлено продуктов: $affected";

Как выполнить запрос с JOIN и группировкой?

Пример

$sql = 'SELECT u.name, COUNT(o.id) as orders_count
        FROM users u
        LEFT JOIN orders o ON u.id = o.user_id
        WHERE u.status = :status
        GROUP BY u.id
        HAVING orders_count > :min';
$result = $db->fetchAll($sql, ['status' => 'active', 'min' => 0]);
Вернёт массив с именем пользователя и количеством заказов.

Как обрабатывать ошибки с кастомными исключениями?

Пример

class DatabaseException extends RuntimeException {}

// В классе Database:
public function __construct(array $config) {
    try {
        // ...
    } catch (PDOException $e) {
        throw new DatabaseException('Не удалось подключиться к БД: ' . $e->getMessage(), 0, $e);
    }
}

Как использовать несколько соединений в одном приложении?

Пример

$master = new Database($masterConfig);
$slave = new Database($slaveConfig);
// Запись только на мастер, чтение с обоих
$users = $slave->fetchAll('SELECT * FROM users');
$master->insert('logs', ['action' => 'read']);

Как реализовать пагинацию с LIMIT и OFFSET?

Пример

$page = 2;
$perPage = 10;
$offset = ($page - 1) * $perPage;
$users = $db->fetchAll('SELECT * FROM users LIMIT ? OFFSET ?', [$perPage, $offset]);

Как сохранить результат запроса в файл?

Пример

$stmt = $db->query('SELECT * FROM large_table');
$file = fopen('export.csv', 'w');
while ($row = $stmt->fetch()) {
    fputcsv($file, $row);
}
fclose($file);
Создан файл export.csv с данными без загрузки всего результата в память.

Классы для работы с MySQL в PHP - comments

En
Engine classes mysql php (php)