Table Module в PHP: примеры и инструкции

Раздел: Управление модулями PHP -> Таблицы модулей

Table Module в PHP: подходы к работе с таблицами базы данных

При разработке на PHP часто возникает необходимость организовать код, работающий с таблицами базы данных. Паттерн Table Module предлагает единый класс для каждой таблицы, который инкапсулирует все операции: выборку, вставку, обновление, удаление, а также бизнес-логику, связанную с этой таблицей. Это альтернатива Active Record и Data Mapper. Рассмотрим различные варианты реализации и их применимость.

Основное решение: класс Table Module для таблицы users

Предположим, есть таблица users со столбцами id, name, email, created_at. Создадим класс UserTable, который принимает PDO-соединение и предоставляет методы для работы с этой таблицей.

class UserTable {
    private PDO $pdo;
    public function __construct(PDO $pdo) {
        $this->pdo = $pdo;
    }
    public function findById(int $id): ?array {
        $stmt = $this->pdo->prepare('SELECT * FROM users WHERE id = ?');
        $stmt->execute([$id]);
        return $stmt->fetch(PDO::FETCH_ASSOC) ?: null;
    }
    public function findByEmail(string $email): ?array {
        $stmt = $this->pdo->prepare('SELECT * FROM users WHERE email = ?');
        $stmt->execute([$email]);
        return $stmt->fetch(PDO::FETCH_ASSOC) ?: null;
    }
    public function insert(array $data): int {
        $stmt = $this->pdo->prepare('INSERT INTO users (name, email) VALUES (?, ?)');
        $stmt->execute([$data['name'], $data['email']]);
        return (int)$this->pdo->lastInsertId();
    }
    public function update(int $id, array $data): void {
        $fields = [];
        $values = [];
        foreach ($data as $key => $value) {
            $fields[] = "$key = ?";
            $values[] = $value;
        }
        $values[] = $id;
        $sql = 'UPDATE users SET ' . implode(', ', $fields) . ' WHERE id = ?';
        $stmt = $this->pdo->prepare($sql);
        $stmt->execute($values);
    }
    public function delete(int $id): void {
        $stmt = $this->pdo->prepare('DELETE FROM users WHERE id = ?');
        $stmt->execute([$id]);
    }
}

Table module php (модуль таблиц в php)

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

$table = new UserTable($pdo);
$user = $table->findById(42);
echo $user['name'];
$newId = $table->insert(['name' => 'Иван', 'email' => 'ivan@example.com']);
$table->update($newId, ['name' => 'Петр']);

Проблема: при большом количестве таблиц классы разрастаются, требуется повторяющийся код для CRUD. Решение: создать абстрактный базовый класс или использовать генерацию кода.

Типичная ошибка: передача непроверенных данных в запрос (SQL-инъекция). В примере применяются подготовленные запросы. Всегда следует экранировать или использовать PDO.

Вариант 1: Active Record (каждый объект - строка)

Как сделать, чтобы объект сам отвечал за сохранение в таблицу?

Класс User наследует от некоторого базового Active Record или сам содержит методы save, delete, find. Каждый экземпляр соответствует одной строке.

class User {
    private ?int $id = null;
    private string $name;
    private string $email;
    private static PDO $pdo;

    public static function setPdo(PDO $pdo): void {
        self::$pdo = $pdo;
    }

    public static function find(int $id): ?self {
        $stmt = self::$pdo->prepare('SELECT * FROM users WHERE id = ?');
        $stmt->execute([$id]);
        $data = $stmt->fetch(PDO::FETCH_ASSOC);
        if (!$data) return null;
        $user = new self($data['name'], $data['email']);
        $user->id = (int)$data['id'];
        return $user;
    }

    public function save(): void {
        if ($this->id) {
            $stmt = self::$pdo->prepare('UPDATE users SET name=?, email=? WHERE id=?');
            $stmt->execute([$this->name, $this->email, $this->id]);
        } else {
            $stmt = self::$pdo->prepare('INSERT INTO users (name, email) VALUES (?, ?)');
            $stmt->execute([$this->name, $this->email]);
            $this->id = (int)self::$pdo->lastInsertId();
        }
    }
}

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

$user = User::find(1);
$user->name = 'Алексей';
$user->save();

$newUser = new User('Мария', 'maria@example.com');
$newUser->save();

Недостаток: нарушение принципа единой ответственности: объект содержит и данные, и логику работы с БД. При изменении схемы таблицы могут потребоваться изменения в классе. Для простых проектов подходит, но для больших - лучше Table Module или Data Mapper.

Вариант 2: Data Mapper (отделение бизнес-логики от хранения)

Как полностью отделить бизнес-объекты от базы данных?

Создаются два типа классов: сущности (например, User) и мапперы (UserMapper). Маппер содержит SQL-запросы и преобразует строки в объекты и обратно. Сущность не знает о БД.

class User {
    public function __construct(
        private ?int $id,
        private string $name,
        private string $email
    ) {}
    // геттеры, сеттеры...
}

class UserMapper {
    private PDO $pdo;
    public function __construct(PDO $pdo) {
        $this->pdo = $pdo;
    }
    public function findById(int $id): ?User {
        $stmt = $this->pdo->prepare('SELECT * FROM users WHERE id = ?');
        $stmt->execute([$id]);
        $row = $stmt->fetch(PDO::FETCH_ASSOC);
        if (!$row) return null;
        return new User((int)$row['id'], $row['name'], $row['email']);
    }
    public function insert(User $user): void {
        $stmt = $this->pdo->prepare('INSERT INTO users (name, email) VALUES (?, ?)');
        $stmt->execute([$user->getName(), $user->getEmail()]);
        $user->setId((int)$this->pdo->lastInsertId());
    }
}

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

$mapper = new UserMapper($pdo);
$user = $mapper->findById(1);
$newUser = new User(null, 'Ольга', 'olga@example.com');
$mapper->insert($newUser);

Проблема: больше классов, требуется управление Identity Map для предотвращения дублирования объектов, если они загружаются повторно. При использовании Table Module такой проблемы нет, так как маппинг отсутствует.

Вариант 3: Table Module с обобщённым базовым классом

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

Можно создать абстрактный класс AbstractTable, который принимает название таблицы и PDO, и реализует общие методы. Конкретные таблицы наследуют этот класс и добавляют специфичные методы.

abstract class AbstractTable {
    protected PDO $pdo;
    protected string $table;
    public function __construct(PDO $pdo, string $table) {
        $this->pdo = $pdo;
        $this->table = $table;
    }
    public function findById(int $id): ?array {
        $stmt = $this->pdo->prepare("SELECT * FROM {$this->table} WHERE id = ?");
        $stmt->execute([$id]);
        return $stmt->fetch(PDO::FETCH_ASSOC) ?: null;
    }
    // ... другие общие методы
}

class UserTable extends AbstractTable {
    public function __construct(PDO $pdo) {
        parent::__construct($pdo, 'users');
    }
    public function findByEmail(string $email): ?array {
        // переопределяем или добавляем свой метод
    }
}

Осторожно: прямое подставление имени таблицы в SQL может быть опасным, если имя таблицы приходит извне. Лучше сделать маппинг допустимых таблиц.

Расширенные примеры использования модуля таблиц

1. Реализация связей между таблицами

Пусть есть таблицы users и orders. В UserTable можно добавить метод для получения заказов пользователя.

Пример
class UserTable {
    // ... предыдущие методы
    public function getOrders(int $userId): array {
        $stmt = $this->pdo->prepare('SELECT * FROM orders WHERE user_id = ?');
        $stmt->execute([$userId]);
        return $stmt->fetchAll(PDO::FETCH_ASSOC);
    }
}
$userTable = new UserTable($pdo);
$orders = $userTable->getOrders(42);
foreach ($orders as $order) {
    echo $order['total'] . '\n';
}

2. Транзакционная вставка связанных записей

При регистрации пользователя нужно создать запись в users и в profiles. Используем транзакцию.

Пример
class UserTable {
    public function createUserWithProfile(array $userData, array $profileData): int {
        try {
            $this->pdo->beginTransaction();
            $userId = $this->insert($userData);
            $stmt = $this->pdo->prepare('INSERT INTO profiles (user_id, bio) VALUES (?, ?)');
            $stmt->execute([$userId, $profileData['bio']]);
            $this->pdo->commit();
            return $userId;
        } catch (PDOException $e) {
            $this->pdo->rollBack();
            throw $e;
        }
    }
}
$table = new UserTable($pdo);
$id = $table->createUserWithProfile(
    ['name' => 'Иван', 'email' => 'ivan@test.ru'],
    ['bio' => 'Разработчик']
);
echo 'ID: ' . $id;

3. Постраничная навигация (пагинация)

Добавим метод для выборки с лимитом и смещением, и подсчета общего количества.

Пример
class UserTable {
    public function getPage(int $page, int $perPage = 20): array {
        $offset = ($page - 1) * $perPage;
        $stmt = $this->pdo->prepare("SELECT * FROM users ORDER BY id LIMIT ? OFFSET ?");
        $stmt->execute([$perPage, $offset]);
        $items = $stmt->fetchAll(PDO::FETCH_ASSOC);
        $total = $this->pdo->query("SELECT COUNT(*) FROM users")->fetchColumn();
        return ['items' => $items, 'total' => (int)$total];
    }
}
$table = new UserTable($pdo);
$page1 = $table->getPage(1, 10);
foreach ($page1['items'] as $user) {
    echo $user['name'] . '\n';
}
echo 'Всего: ' . $page1['total'];

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

Можно добавить внутренний кэш, чтобы избежать повторных запросов в рамках одного запроса.

Пример
class UserTable {
    private array $cache = [];
    public function findByIdCached(int $id): ?array {
        if (isset($this->cache[$id])) {
            return $this->cache[$id];
        }
        $data = $this->findById($id);
        $this->cache[$id] = $data;
        return $data;
    }
}
$table = new UserTable($pdo);
$user1 = $table->findByIdCached(1); // запрос в БД
$user2 = $table->findByIdCached(1); // из кэша
var_dump($user1 === $user2); // true, одинаковые массивы

5. Использование композитных первичных ключей

Если таблица использует составной ключ (например, order_id, product_id), методы должны принимать массив ключей.

Пример
class OrderItemTable {
    public function find(array $pk): ?array {
        $sql = 'SELECT * FROM order_items WHERE order_id = ? AND product_id = ?';
        $stmt = $this->pdo->prepare($sql);
        $stmt->execute([$pk['order_id'], $pk['product_id']]);
        return $stmt->fetch(PDO::FETCH_ASSOC) ?: null;
    }
}
$table = new OrderItemTable($pdo);
$item = $table->find(['order_id' => 10, 'product_id' => 555]);
if ($item) {
    echo $item['quantity'];
}

6. Реализация мягкого удаления (soft delete)

Добавим поле deleted_at в таблицу, а методы будут автоматически исключать удаленные записи.

Пример
class UserTable {
    public function findActiveById(int $id): ?array {
        $stmt = $this->pdo->prepare('SELECT * FROM users WHERE id = ? AND deleted_at IS NULL');
        $stmt->execute([$id]);
        return $stmt->fetch(PDO::FETCH_ASSOC) ?: null;
    }
    public function softDelete(int $id): void {
        $stmt = $this->pdo->prepare('UPDATE users SET deleted_at = NOW() WHERE id = ?');
        $stmt->execute([$id]);
    }
}
$table = new UserTable($pdo);
$table->softDelete(42);
$user = $table->findActiveById(42); // null

Модуль таблиц в PHP - comments

En
Table module php (php)