Класс модели в PHP: от простого к сложному
Основные подходы к реализации модели в PHP
Как реализовать модель, полностью отделенную от базы данных с помощью Data Mapper?
Data Mapper - это паттерн, при котором модель (доменный объект) не содержит никакой логики работы с хранилищем. Вместо этого отдельный класс (маппер) отвечает за загрузку, сохранение и удаление объектов. Такой подход соблюдает принцип единственной ответственности и упрощает тестирование.
// Класс модели (чистый домен)
class User {
private int $id;
private string $name;
private string $email;
public function __construct(string $name, string $email, ?int $id = null) {
$this->id = $id ?? 0;
$this->name = $name;
$this->email = $email;
}
public function getId(): int { return $this->id; }
public function getName(): string { return $this->name; }
public function getEmail(): string { return $this->email; }
public function setName(string $name): void { $this->name = $name; }
public function setEmail(string $email): void { $this->email = $email; }
}
// Маппер (работа с БД)
class UserMapper {
private \PDO $pdo;
public function __construct(\PDO $pdo) {
$this->pdo = $pdo;
}
public function find(int $id): ?User {
$stmt = $this->pdo->prepare('SELECT * FROM users WHERE id = :id');
$stmt->execute([':id' => $id]);
$row = $stmt->fetch();
if (!$row) return null;
return new User($row['name'], $row['email'], (int)$row['id']);
}
public function save(User $user): void {
if ($user->getId() === 0) {
$stmt = $this->pdo->prepare('INSERT INTO users (name, email) VALUES (:name, :email)');
$stmt->execute([':name' => $user->getName(), ':email' => $user->getEmail()]);
// установить новый ID (если auto increment)
} else {
$stmt = $this->pdo->prepare('UPDATE users SET name = :name, email = :email WHERE id = :id');
$stmt->execute([':name' => $user->getName(), ':email' => $user->getEmail(), ':id' => $user->getId()]);
}
}
public function delete(User $user): void {
$stmt = $this->pdo->prepare('DELETE FROM users WHERE id = :id');
$stmt->execute([':id' => $user->getId()]);
}
}
Class model php (класс модели php)
Типичные проблемы и ошибки:
- Увеличение объёма кода - для каждой сущности нужно писать отдельный маппер.
- Необходимость управлять транзакциями вручную, если сохраняется несколько объектов.
- Сложность реализации Identity Map (кэширования объектов) для предотвращения дублирования.
Как создать простую модель с прямыми SQL запросами на PDO?
Самый простой вариант - класс модели сам содержит методы для работы с БД через PDO. Подходит для небольших проектов.
class User {
private static function getPdo(): \PDO {
return new \PDO('mysql:host=localhost;dbname=test', 'root', '');
}
public static function find(int $id): ?self {
$pdo = self::getPdo();
$stmt = $pdo->prepare('SELECT * FROM users WHERE id = ?');
$stmt->execute([$id]);
$data = $stmt->fetch();
if (!$data) return null;
$user = new self();
$user->id = $data['id'];
$user->name = $data['name'];
$user->email = $data['email'];
return $user;
}
// ... аналогично save, delete
}
Ошибки и сложности:
- Дублирование SQL-запросов в разных методах.
- Трудности в тестировании из-за жёсткой привязки к PDO.
- Нарушение принципа единственной ответственности (модель знает о БД).
Как реализовать модель, которая сама управляет сохранением (ActiveRecord)?
Каждый объект модели является строкой в БД и содержит методы save(), delete(). Удобно для RAD.
abstract class ActiveRecord {
protected static string $table;
protected array $attributes = [];
public function __get(string $key) { return $this->attributes[$key] ?? null; }
public function __set(string $key, $value) { $this->attributes[$key] = $value; }
public function save(): void {
if ($this->attributes['id'] ?? false) {
// UPDATE
} else {
// INSERT
}
}
public static function find(int $id): ?self {
// SELECT
}
}
class User extends ActiveRecord {
protected static string $table = 'users';
}
$user = new User();
$user->name = 'John';
$user->email = 'john@example.com';
$user->save();
Проблемы:
- Модель зависит от активного соединения с БД.
- Трудно менять хранилище (например, файл вместо БД).
- Невозможность использовать модель без сохранения (например, временные объекты).
Как использовать Eloquent ORM для быстрой разработки моделей?
Eloquent - встроенная ORM фреймворка Laravel. Позволяет объявить модель, просто унаследовавшись от Illuminate\Database\Eloquent\Model.
use Illuminate\Database\Eloquent\Model;
class User extends Model {
protected $table = 'users';
protected $fillable = ['name', 'email'];
}
// Использование
$user = User::find(1);
echo $user->name;
$user = new User();
$user->name = 'Jane';
$user->save();
Сложности:
- Жёсткая привязка к Laravel (нельзя использовать вне фреймворка без дополнительных пакетов).
- Может быть избыточен для простых скриптов.
- Скрытая магия (магические методы, автоматическое разрешение зависимостей).
Как использовать Doctrine ORM для enterprise приложений?
Doctrine - мощная ORM с DQL, аннотациями и полным отделением. Сущности описываются через аннотации или YAML/XML.
/**
* @Entity
* @Table(name="users")
*/
class User {
/** @Id @Column(type="integer") @GeneratedValue */
private int $id;
/** @Column(type="string") */
private string $name;
/** @Column(type="string", unique=true) */
private string $email;
public function __construct(string $name, string $email) {
$this->name = $name;
$this->email = $email;
}
public function getId(): int { return $this->id; }
public function getName(): string { return $this->name; }
public function getEmail(): string { return $this->email; }
}
// Использование через EntityManager
$entityManager = EntityManager::create(/* настройки */);
$user = new User('Alice', 'alice@example.com');
$entityManager->persist($user);
$entityManager->flush();
Проблемы и ошибки:
- Большая сложность настройки (требуется метаданные, конфигурация, прокси-классы).
- Медленная работа на малых объёмах данных из-за overhead.
- Требуется знание DQL и механизма жизненного цикла (Unit of Work).
Расширенные примеры реализации моделей
Полная реализация Data Mapper с репозиторием и интерфейсом
Маппер часто используется вместе с репозиторием - коллекцией объектов для работы с хранилищем.
interface UserRepositoryInterface {
public function findById(int $id): ?User;
public function save(User $user): void;
public function delete(User $user): void;
}
class UserDoctrineRepository implements UserRepositoryInterface {
private \Doctrine\ORM\EntityManager $em;
public function __construct(\Doctrine\ORM\EntityManager $em) {
$this->em = $em;
}
public function findById(int $id): ?User {
return $this->em->find(User::class, $id);
}
public function save(User $user): void {
$this->em->persist($user);
$this->em->flush();
}
public function delete(User $user): void {
$this->em->remove($user);
$this->em->flush();
}
}
// Пример использования
$repo = new UserDoctrineRepository($entityManager);
$user = new User('Bob', 'bob@example.com');
$repo->save($user);
$found = $repo->findById(1);
Результат: вызов $repo->findById(1) вернёт объект User с данными из БД или null. Метод save автоматически вставит или обновит запись.
Собственный ActiveRecord с поддержкой связей
Пример упрощённой реализации связей "один ко многим" в ActiveRecord.
class User extends ActiveRecord {
protected static string $table = 'users';
public function posts(): array {
return Post::where('user_id', $this->id);
}
}
class Post extends ActiveRecord {
protected static string $table = 'posts';
public function user(): ?User {
return User::find($this->user_id);
}
}
// Использование
$user = User::find(1);
foreach ($user->posts() as $post) {
echo $post->title;
}
Результат: при наличии таблиц users и posts выводятся заголовки постов пользователя с id=1.
Использование Doctrine с DQL для сложных запросов
DQL (Doctrine Query Language) позволяет писать запросы в стиле SQL, но к объектам.
$query = $entityManager->createQuery(
'SELECT u FROM User u WHERE u.email LIKE :email'
)->setParameter('email', '%@example.com');
$users = $query->getResult();
// Результат: массив объектов User
foreach ($users as $user) {
echo $user->getName() . ' (' . $user->getEmail() . ')';
}
Результат вывода:
Alice (alice@example.com)
Bob (bob@example.com)
Сохранение объектов через Unit of Work в Doctrine
Doctrine отслеживает изменения объектов автоматически, используя Unit of Work.
$user = $entityManager->find(User::class, 1);
$user->setName('Alice Updated');
$entityManager->flush(); // автоматически выполнит UPDATE
Результат: после вызова flush() изменения записываются в БД. Без явного persist, так как объект уже находится под управлением EntityManager.
Использование PDO маппера с транзакциями
Для обеспечения целостности данных применяются транзакции.
$pdo->beginTransaction();
try {
$userMapper = new UserMapper($pdo);
$user = new User('Charlie', 'charlie@example.com');
$userMapper->save($user);
// ещё операции
$pdo->commit();
} catch (\Exception $e) {
$pdo->rollBack();
throw $e;
}
Результат: при возникновении ошибки все изменения в БД откатываются. Без транзакции возможны частичные вставки.
Заключение: выбор подхода зависит от масштаба проекта, требований к тестируемости и предпочтений команды. Для сложных доменных моделей предпочтителен Data Mapper или Doctrine, для быстрой разработки - ActiveRecord или Eloquent.