Doctrine DBAL для PHP: полное руководство с примерами

Онлайн-тренажер Python для начинающих

Изучайте Python легко и без перегрузки теорией. Решайте практические задачи с автоматической проверкой, получайте подсказки на русском языке и пишите код прямо в браузере — без необходимости что-либо устанавливать.

Начать курс

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 для пагинации, а не выборку всех строк.

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

Рекомендаций