Doctrine DBAL для PHP: полное руководство с примерами
1. Что это и зачем нужна
Doctrine DBAL (Database Abstraction Layer) — это мощная библиотека для PHP, которая предоставляет унифицированный интерфейс для работы с различными реляционными базами данных. Она является частью экосистемы Doctrine, но может использоваться как самостоятельный компонент. Основная задача DBAL — абстрагировать разработчика от специфики конкретной СУБД (MySQL, PostgreSQL, SQLite, Oracle, SQL Server и др.), позволяя писать переносимый код.
Зачем нужна Doctrine DBAL? Во-первых, она значительно упрощает выполнение типовых операций: подключение к базе, выполнение запросов, работа с транзакциями. Во-вторых, она обеспечивает безопасность через подготовленные выражения (prepared statements) и автоматическое экранирование параметров, что защищает от SQL-инъекций. В-третьих, DBAL предоставляет удобный Query Builder для построения сложных запросов без написания сырого SQL. В отличие от полноценного ORM (Object-Relational Mapper), DBAL не требует маппинга объектов и сущностей, что делает её идеальным выбором для проектов, где нужен прямой контроль над SQL, но с дополнительными удобствами.
Библиотека активно поддерживается, имеет отличную документацию и широко используется в промышленной разработке. Она лежит в основе Doctrine ORM и многих популярных PHP-фреймворков (Symfony, Laravel — через интеграции).
2. Установка
Установка Doctrine DBAL производится через Composer — стандартный менеджер пакетов для PHP. Выполните в терминале в корне вашего проекта:
composer require doctrine/dbal
После установки в проекте появится папка vendor/ и файл composer.json с добавленной зависимостью. Для работы с конкретной СУБД может потребоваться установка соответствующего драйвера PDO (например, ext-pdo_mysql для MySQL).
3. Быстрый старт — минимальный рабочий пример
Создайте файл test.php и подключите автозагрузчик Composer. Пример подключения к SQLite (встроенная база данных, не требует установки сервера):
<?php
require_once 'vendor/autoload.php';
use Doctrine\DBAL\DriverManager;
// Параметры подключения
$connectionParams = [
'dbname' => 'test.db',
'driver' => 'pdo_sqlite',
];
// Создаем соединение
$conn = DriverManager::getConnection($connectionParams);
// Создаем таблицу
$conn->executeStatement('CREATE TABLE IF NOT EXISTS users (id INTEGER PRIMARY KEY AUTOINCREMENT, name TEXT, email TEXT)');
// Вставляем данные
$conn->insert('users', ['name' => 'John Doe', 'email' => 'john@example.com']);
// Выбираем данные
$users = $conn->fetchAllAssociative('SELECT * FROM users');
print_r($users);
Этот код создает базу SQLite, таблицу users, вставляет одну запись и выводит все строки.
4. Основные методы/функции/классы
4.1. DriverManager::getConnection()
Сигнатура: public static function getConnection(array $params, ?Configuration $config = null, ?EventManager $eventManager = null): Connection
Что делает: Создает и возвращает объект соединения с базой данных на основе переданных параметров. Это точка входа в DBAL.
$connectionParams = [
'dbname' => 'mydb',
'user' => 'root',
'password' => 'secret',
'host' => 'localhost',
'driver' => 'pdo_mysql',
];
$conn = DriverManager::getConnection($connectionParams);
4.2. Connection::executeStatement()
Сигнатура: public function executeStatement(string $sql, array $params = [], array $types = []): int
Что делает: Выполняет SQL-запрос, который не возвращает набор строк (INSERT, UPDATE, DELETE, DDL). Возвращает количество затронутых строк.
$affected = $conn->executeStatement('UPDATE users SET name = ? WHERE id = ?', ['Jane Doe', 1]);
echo "Обновлено строк: " . $affected;
4.3. Connection::fetchAllAssociative()
Сигнатура: public function fetchAllAssociative(string $sql, array $params = [], array $types = []): array
Что делает: Выполняет SELECT-запрос и возвращает все строки результата в виде массива ассоциативных массивов (ключи — имена столбцов).
$users = $conn->fetchAllAssociative('SELECT id, name FROM users WHERE active = ?', [1]);
foreach ($users as $user) {
echo $user['name'] . "\n";
}
4.4. Connection::fetchOne()
Сигнатура: public function fetchOne(string $sql, array $params = [], array $types = []): mixed
Что делает: Выполняет запрос и возвращает значение первого столбца первой строки. Удобно для получения скалярных значений (COUNT, MAX, и т.д.).
$count = $conn->fetchOne('SELECT COUNT(*) FROM users');
echo "Всего пользователей: " . $count;
4.5. Connection::insert()
Сигнатура: public function insert(string $table, array $data, array $types = []): int
Что делает: Упрощает вставку записи. Принимает имя таблицы и ассоциативный массив (ключ — столбец, значение — данные). Автоматически экранирует имена столбцов.
$conn->insert('users', [
'name' => 'Alice',
'email' => 'alice@example.com',
'created_at' => date('Y-m-d H:i:s'),
]);
4.6. Connection::update()
Сигнатура: public function update(string $table, array $data, array $criteria, array $types = []): int
Что делает: Обновляет записи в таблице. Принимает имя таблицы, массив новых значений и массив условий (WHERE).
$conn->update('users',
['name' => 'Bob'],
['id' => 5]
);
4.7. Connection::delete()
Сигнатура: public function delete(string $table, array $criteria, array $types = []): int
Что делает: Удаляет записи из таблицы по заданным критериям.
$conn->delete('users', ['id' => 10]);
4.8. QueryBuilder
Сигнатура: public function createQueryBuilder(): QueryBuilder
Что делает: Создает объект построителя запросов, который позволяет программно конструировать SQL-запросы, избегая конкатенации строк.
$qb = $conn->createQueryBuilder();
$qb->select('u.id', 'u.name')
->from('users', 'u')
->where('u.email LIKE :email')
->setParameter('email', '%@example.com')
->orderBy('u.name', 'ASC');
$users = $qb->fetchAllAssociative();
4.9. Connection::transactional()
Сигнатура: public function transactional(callable $func): mixed
Что делает: Выполняет переданную функцию внутри транзакции. Если функция выбрасывает исключение, транзакция откатывается; иначе — фиксируется. Возвращает результат функции.
$conn->transactional(function(Connection $conn) {
$conn->insert('accounts', ['user_id' => 1, 'amount' => 100]);
$conn->update('accounts', ['amount' => 50], ['user_id' => 2]);
// Если здесь произойдет ошибка, оба изменения откатятся
});
5. Реальный практический пример
Предположим, мы создаем простую систему управления заказами. Ниже приведен полный скрипт, демонстрирующий создание таблиц, вставку данных, выборку с JOIN и обработку ошибок.
<?php
require_once 'vendor/autoload.php';
use Doctrine\DBAL\DriverManager;
use Doctrine\DBAL\Exception;
// Конфигурация подключения к MySQL
$connectionParams = [
'dbname' => 'shop',
'user' => 'root',
'password' => '',
'host' => '127.0.0.1',
'driver' => 'pdo_mysql',
'charset' => 'utf8mb4',
];
try {
$conn = DriverManager::getConnection($connectionParams);
// Создание таблиц
$conn->executeStatement('CREATE TABLE IF NOT EXISTS customers (
id INT AUTO_INCREMENT PRIMARY KEY,
name VARCHAR(255) NOT NULL,
email VARCHAR(255) UNIQUE NOT NULL
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4');
$conn->executeStatement('CREATE TABLE IF NOT EXISTS orders (
id INT AUTO_INCREMENT PRIMARY KEY,
customer_id INT NOT NULL,
product VARCHAR(255) NOT NULL,
quantity INT NOT NULL DEFAULT 1,
price DECIMAL(10,2) NOT NULL,
created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
FOREIGN KEY (customer_id) REFERENCES customers(id)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4');
// Вставка данных через транзакцию
$conn->transactional(function(Connection $conn) {
// Добавляем клиента
$conn->insert('customers', [
'name' => 'Иван Петров',
'email' => 'ivan@example.com'
]);
$customerId = $conn->lastInsertId();
// Добавляем заказ
$conn->insert('orders', [
'customer_id' => $customerId,
'product' => 'Ноутбук',
'quantity' => 1,
'price' => 75000.00
]);
});
// Выборка с JOIN с использованием QueryBuilder
$qb = $conn->createQueryBuilder();
$qb->select('c.name', 'c.email', 'o.product', 'o.quantity', 'o.price', 'o.created_at')
->from('orders', 'o')
->innerJoin('o', 'customers', 'c', 'o.customer_id = c.id')
->orderBy('o.created_at', 'DESC')
->setMaxResults(10);
$orders = $qb->fetchAllAssociative();
echo "<h2>Последние заказы:</h2>";
echo "<table border='1'>";
echo "<tr><th>Клиент</th><th>Email</th><th>Товар</th><th>Кол-во</th><th>Цена</th><th>Дата</th></tr>";
foreach ($orders as $order) {
echo "<tr>";
echo "<td>" . htmlspecialchars($order['name']) . "</td>";
echo "<td>" . htmlspecialchars($order['email']) . "</td>";
echo "<td>" . htmlspecialchars($order['product']) . "</td>";
echo "<td>" . $order['quantity'] . "</td>";
echo "<td>" . number_format($order['price'], 2) . " руб.</td>";
echo "<td>" . $order['created_at'] . "</td>";
echo "</tr>";
}
echo "</table>";
} catch (Exception $e) {
echo "Ошибка базы данных: " . $e->getMessage();
}
6. Советы по производительности и типичные ошибки
Советы по производительности:
- Используйте подготовленные выражения. Всегда передавайте параметры отдельно от SQL (через
$params), а не вставляйте их напрямую. DBAL делает это автоматически при использовании методов с параметрами. - Пакетная вставка. Для вставки множества записей используйте один INSERT с несколькими VALUES, а не цикл с отдельными
insert(). Пример:INSERT INTO users (name) VALUES ('a'), ('b'), ('c'). - Кэширование метаданных. Если вы используете Schema Manager (для работы со структурой БД), включите кэширование, чтобы избежать повторных запросов к information_schema.
- Используйте индексы. DBAL не создает индексы автоматически. Убедитесь, что для столбцов, используемых в WHERE и JOIN, созданы индексы.
- Ограничивайте выборку. Используйте
setMaxResults()иsetFirstResult()в QueryBuilder для пагинации, а не выборку всех строк.
Типичные ошибки:
- Игнорирование типов параметров. Если не указать типы (
$types), DBAL может неправильно обработать некоторые данные (например,nullили большие числа). Всегда передавайте типы для критичных полей. - Забыли закрыть соединение. Хотя PHP автоматически закрывает соединения при завершении скрипта, в долгих процессах (например, воркеры) лучше явно вызывать
$conn->close(). - Неправильный драйвер. Убедитесь, что установлено соответствующее расширение PDO. Например, для MySQL нужно
ext-pdo_mysql, для PostgreSQL —ext-pdo_pgsql.