Table Module в 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