Разработка собственного класса SQL для PHP
Создание класса для работы с SQL в PHP
Для структурированного и безопасного взаимодействия с базами данных в PHP часто используют собственные классы-обёртки. Такой подход позволяет инкапсулировать логику соединения, подготовку запросов, обработку ошибок и повторное использование кода. Ниже рассмотрен наиболее эффективный вариант на основе PDO, а также альтернативные решения.
Основное решение: класс-обёртка для PDO
Как создать универсальный класс для работы с MySQL через PDO с поддержкой подготовленных запросов и транзакций?
Класс DB использует PDO для соединения, выполнение запросов через prepare/execute, управление транзакциями и обработку исключений. Подходит для проектов любого размера.
class DB {
private static ?PDO $pdo = null;
public static function connect(string $dsn, string $user, string $pass): void {
try {
self::$pdo = new PDO($dsn, $user, $pass, [
PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION,
PDO::ATTR_DEFAULT_FETCH_MODE => PDO::FETCH_ASSOC,
]);
} catch (PDOException $e) {
throw new RuntimeException('Ошибка подключения: ' . $e->getMessage());
}
}
public static function query(string $sql, array $params = []): PDOStatement {
if (!self::$pdo) {
throw new LogicException('Соединение не установлено. Вызовите DB::connect()');
}
$stmt = self::$pdo->prepare($sql);
$stmt->execute($params);
return $stmt;
}
// Дополнительные методы: fetch, fetchAll, beginTransaction, commit, rollBack
}Php sql insert (insert в php)
Пояснения: Статическое свойство хранит единственный экземпляр PDO. Метод connect настраивает режим ошибок и способ выборки. query принимает SQL и массив параметров, возвращает объект PDOStatement.
Типичные ошибки:
- Повторное создание PDO без проверки приводит к лишним подключениям. Решение - синглтон или проверка на null.
- Необработанные исключения при ошибках SQL. Включите ERRMODE_EXCEPTION и используйте try/catch.
- Неправильный DSN для MySQL: mysql:host=localhost;dbname=test;charset=utf8mb4.
Цели использования: создание безопасных запросов без ручного экранирования, работа с разными СУБД через PDO, упрощение кода при многократных запросах.
Альтернатива 1: класс на основе mysqli
Как написать простой класс для MySQL с процедурным стилем без PDO?
Для проектов, где используется только MySQL, можно обойтись встроенным расширением mysqli.
class MySQL {
private mysqli $conn;
public function __construct(string $host, string $user, string $pass, string $dbname) {
$this->conn = new mysqli($host, $user, $pass, $dbname);
if ($this->conn->connect_error) {
throw new RuntimeException('Ошибка подключения: ' . $this->conn->connect_error);
}
$this->conn->set_charset('utf8mb4');
}
public function exec(string $sql, string $types = '', array $params = []): mysqli_stmt | bool {
$stmt = $this->conn->prepare($sql);
if ($stmt === false) {
throw new RuntimeException('Ошибка подготовки: ' . $this->conn->error);
}
if ($types) {
$stmt->bind_param($types, ...$params);
}
$stmt->execute();
return $stmt;
}
}Sql where php (условие where в sql-запросах php)
Пояснения: Привязка параметров требует указания типов (s, i, d). Метод exec возвращает подготовленный запрос, из которого можно получить результат через get_result().
Проблемы: требуется явно указывать типы параметров, нет встроенного переключения между СУБД, сложнее управлять транзакциями.
Альтернатива 2: прямое использование PDO без класса-обёртки
Когда целесообразно обойтись без собственного класса и использовать PDO напрямую в контроллерах?
Для небольших скриптов или когда фреймворк уже предоставляет свою абстракцию.
$dsn = 'mysql:host=localhost;dbname=test;charset=utf8mb4';
$pdo = new PDO($dsn, 'root', '', [PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION]);
$stmt = $pdo->prepare('SELECT * FROM users WHERE id = ?');
$stmt->execute([$id]);
$user = $stmt->fetch();
Sql инъекция php (sql-инъекции в php)
Пояснения: Простая инициализация, отсутствие дополнительных слоёв. Минус - дублирование кода подключения и обработки ошибок в каждом месте.
Риски: легко забыть установить режим ошибок, трудно поддерживать при росте проекта.
Альтернатива 3: использование ORM (например, Eloquent)
Какие преимущества даёт полноценная ORM вместо самодельного класса?
Для крупных проектов, где требуется работа с объектами и сложными связями.
// Composer: illuminate/database
$capsule = new Illuminate\Database\Capsule\Manager;
$capsule->addConnection([
'driver' => 'mysql',
'host' => 'localhost',
'database' => 'test',
'username' => 'root',
'password' => '',
]);
$capsule->setAsGlobal();
$users = User::where('active', 1)->get();Пояснения: Активная запись, гидратация объектов, миграции. Требует установки через Composer и настройки автозагрузки.
Недостатки: избыточность для простых задач, снижение производительности на больших выборках, жёсткая привязка к ORM.
Расширенные примеры работы с классом DB
Ниже приведены нестандартные сценарии использования собственного класса для SQL.
Пример 1: Транзакция с множественными вставками и откатом
DB::beginTransaction();
try {
DB::query('INSERT INTO log (message) VALUES (?)', ['Старт']);
foreach ($items as $item) {
DB::query('INSERT INTO items (name, price) VALUES (?, ?)', [$item['name'], $item['price']]);
}
DB::commit();
} catch (Exception $e) {
DB::rollBack();
error_log('Транзакция отменена: ' . $e->getMessage());
throw $e;
}// При ошибке ни одна из вставок не сохраняется. При успехе все записи фиксируются.
Пояснение: Использование beginTransaction/commit/rollBack гарантирует атомарность.
Пример 2: Получение одной записи в виде объекта
$stmt = DB::query('SELECT * FROM users WHERE id = ?', [1]);
$user = $stmt->fetch(PDO::FETCH_OBJ);
echo $user->name;Иван
Пояснение: Изменение режима выборки через параметр fetch позволяет получать данные в нужном формате.
Пример 3: Использование хранимой процедуры с выходными параметрами (MySQL)
// Процедура: CREATE PROCEDURE sp_get_user_count(OUT total INT) ...
$stmt = DB::query('CALL sp_get_user_count(@total)');
$stmt->closeCursor();
$stmt = DB::query('SELECT @total AS total');
$row = $stmt->fetch();
echo 'Всего пользователей: ' . $row['total'];Всего пользователей: 42
Пояснение: Для работы с выходными переменными необходим отдельный запрос SELECT.
Пример 4: Bulk insert через один запрос
$data = [
['name' => 'A', 'age' => 20],
['name' => 'B', 'age' => 25],
];
$placeholders = implode(',', array_fill(0, count($data), '(?,?)'));
$params = [];
foreach ($data as $row) {
$params[] = $row['name'];
$params[] = $row['age'];
}
$sql = "INSERT INTO users (name, age) VALUES $placeholders";
DB::query($sql, $params);// Вставлено 2 строки за один вызов prepare/execute
Пояснение: Подготовка одного запроса с несколькими наборами значений ускоряет массовую вставку.
Пример 5: Обработка ошибок с пользовательским исключением
class DatabaseException extends RuntimeException {}
try {
DB::query('SELECT * FROM non_existent_table');
} catch (PDOException $e) {
throw new DatabaseException('Ошибка базы данных: ' . $e->getMessage(), 0, $e);
}// Выбрасывается DatabaseException с подробным сообщением
Пояснение: Обёртка исключений позволяет унифицировать обработку в приложении.