Класс модели в 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.

Класс модели PHP - comments

En
Class model php (php)