Шаблон проектирования «Репозиторий» (Repository Pattern) — это мощный структурный подход, который помогает создать четкую границу между бизнес-логикой вашего приложения и логикой доступа к данным. Его главная цель — сделать код более чистым, сопровождаемым, тестируемым и гибким к изменениям.
В этой статье мы подробно разберем, как правильно реализовать этот шаблон на PHP, какие преимущества он дает и как избежать распространенных ошибок.
Зачем нужен Репозиторий? Простая аналогия
Представьте большую библиотеку. Чтобы найти, добавить или взять книгу, вы не ходите по стеллажам самостоятельно — вы идете к библиотекарю.
- Вы (клиентский код): Просите библиотекаря найти книгу по названию. Вам не важно, как он это сделает: посмотрит в бумажный каталог, электронную базу или спросит у коллеги.
- Библиотекарь (Реализация Репозитория): Знает все внутренние механизмы доступа к данным (где и как хранятся книги). Если система хранения изменится (перейдут с бумажных карточек на цифровую систему), это будет заботой только библиотекаря. Для вас его интерфейс останется прежним: «найди книгу», «добавь книгу».
Именно это абстрагирование и является сутью шаблона Репозиторий.
Реализация шаблона Репозиторий в PHP
Давайте реализуем этот шаблон шаг за шагом. Мы начнем с простого примера в памяти, а затем усложним его до работы с базой данных.
1. Определяем интерфейс (Контракт)
Первым делом создаем интерфейс. Это контракт, который описывает, что мы можем делать с нашими данными, но не как это делать. Это ключ к гибкости и тестируемости.
<?php
interface BookRepositoryInterface
{
public function find(int $id): ?Book;
public function findAll(): array;
public function save(Book $book): void;
public function delete(Book $book): void;
// Дополнительные полезные методы
public function findByTitle(string $title): ?Book;
public function findPublishedAfter(\DateTime $date): array;
}
2. Создаем модель данных (Сущность)
Модель — это простой объект, представляющий наши данные.
<?php
class Book
{
private ?int $id;
private string $title;
private string $author;
private \DateTime $publishedDate;
public function __construct(?int $id, string $title, string $author, \DateTime $publishedDate)
{
$this->id = $id;
$this->title = $title;
$this->author = $author;
$this->publishedDate = $publishedDate;
}
// Геттеры (и возможно сеттеры)
public function getId(): ?int
{
return $this->id;
}
public function getTitle(): string
{
return $this->title;
}
// ... getAuthor(), getPublishedDate() и т.д.
}
3. Пишем конкретную реализацию Репозитория
Теперь реализуем интерфейс. Вот два варианта: простой (в памяти) и реалистичный (с базой данных через PDO).
Вариант 1: Реализация в памяти (для тестов)
<?php
class InMemoryBookRepository implements BookRepositoryInterface
{
private array $books = [];
public function find(int $id): ?Book
{
return $this->books[$id] ?? null;
}
public function findAll(): array
{
return array_values($this->books);
}
public function save(Book $book): void
{
if ($book->getId() === null) {
$book = new Book(count($this->books) + 1, $book->getTitle(), $book->getAuthor(), $book->getPublishedDate());
}
$this->books[$book->getId()] = $book;
}
public function delete(Book $book): void
{
unset($this->books[$book->getId()]);
}
// ... реализация других методов
}
Вариант 2: Реализация с базой данных (через PDO)
Это та реализация, которая используется в реальном проекте.
<?php
class DatabaseBookRepository implements BookRepositoryInterface
{
public function __construct(private PDO $pdo) {}
public function find(int $id): ?Book
{
$stmt = $this->pdo->prepare('SELECT * FROM books WHERE id = :id');
$stmt->execute(['id' => $id]);
$data = $stmt->fetch(PDO::FETCH_ASSOC);
if (!$data) {
return null;
}
return $this->hydrateBook($data);
}
public function findAll(): array
{
$stmt = $this->pdo->query('SELECT * FROM books');
$books = [];
while ($data = $stmt->fetch(PDO::FETCH_ASSOC)) {
$books[] = $this->hydrateBook($data);
}
return $books;
}
public function save(Book $book): void
{
if ($book->getId() === null) {
$this->insert($book);
} else {
$this->update($book);
}
}
private function insert(Book $book): void
{
$stmt = $this->pdo->prepare('INSERT INTO books (title, author, published_date) VALUES (:title, :author, :published_date)');
$stmt->execute([
'title' => $book->getTitle(),
'author' => $book->getAuthor(),
'published_date' => $book->getPublishedDate()->format('Y-m-d')
]);
// Устанавливаем ID для новой книги
// $book = new Book($this->pdo->lastInsertId(), ...);
// Лучше возвращать новую сущность или использовать отдельный маппер.
}
// ... методы update, delete, findByTitle и т.д.
private function hydrateBook(array $data): Book
{
// Преобразуем данные из базы в объект Book
return new Book(
(int) $data['id'],
$data['title'],
$data['author'],
new \DateTime($data['published_date'])
);
}
}
4. Используем Репозиторий в нашем сервисе
Красота шаблона проявляется здесь: бизнес-логика не зависит от того, как хранятся данные.
<?php
// Создаем соединение с БД (где-то в точке входа в приложение, например, bootstrap.php)
$pdo = new PDO('mysql:host=localhost;dbname=test', 'username', 'password');
// Внедряем нужную реализацию репозитория
// $bookRepository = new InMemoryBookRepository(); // Для тестов
$bookRepository = new DatabaseBookRepository($pdo); // Для продакшена
// Создаем новую книгу
$newBook = new Book(null, "Преступление и наказание", "Ф.М. Достоевский", new DateTime('1866-01-01'));
// Сохраняем ее (репозиторий сам решит, делать INSERT или UPDATE)
$bookRepository->save($newBook);
// Ищем книгу — бизнес-логике все равно, как это происходит
$foundBook = $bookRepository->findByTitle("Преступление и наказание");
if ($foundBook) {
echo $foundBook->getTitle() . " - " . $foundBook->getAuthor();
}
// Получаем все книги, опубликованные после 1900 года
$recentBooks = $bookRepository->findPublishedAfter(new DateTime('1900-01-01'));
foreach ($recentBooks as $book) {
echo $book->getTitle() . "\n";
}
Ключевые преимущества использования шаблона
- Сокрытие сложности доступа к данным: Весь SQL-код, работу с API или файловой системой инкапсулирован в одном месте.
- Упрощение модульного тестирования: Для тестирования сервисов, использующих репозиторий, вы можете подменить
DatabaseBookRepositoryнаInMemoryBookRepositoryили создать Mock объект, реализующийBookRepositoryInterface. Это позволяет тестировать логику в изоляции от базы данных. - Централизация логики запросов: Все запросы к источнику данных (например, к таблице
books) находятся в одном классе. Это устраняет дублирование кода и упрощает его изменение. - Гибкость архитектуры: Если вы решили сменить базу данных (например, с MySQL на PostgreSQL) или способ хранения (перейти с базы данных на внешнее API), вам нужно изменить всего один класс — реализацию репозитория. Остальной код приложения останется нетронутым.
Частые ошибки и лучшие практики
- Не путайте Репозиторий и DAO (Data Access Object): Репозиторий работает с коллекциями объектов и оперирует понятиями предметной области (Domain-Driven Design). DAO — это более низкоуровневый паттерн, который часто просто отображает строки таблицы БД на объекты.
- Избегайте «Универсального репозитория» (Generic Repository): Не стоит создавать один репозиторий для всех сущностей (
class GenericRepository { ... }). У каждой сущности свой уникальный набор методов и запросов. Создавайте отдельный репозиторий под каждую агрегирующую сущность. - Используйте Интерфейсы: Всегда определяйте интерфейс для репозитория. Это основа для внедрения зависимостей и мокинга в тестах.
- Внедрение зависимостей: Передавайте репозиторий в сервисы через конструктор (Dependency Injection). Это делает зависимости явными и код — более тестируемым.
Заключение
Шаблон Репозиторий — это не просто «еще один способ работы с базой данных». Это архитектурный инструмент, который помогает строить сложные приложения с четкой структурой. Он требует написания немного большего количества кода на начальном этапе, но с лихвой окупается при дальнейшем развитии, тестировании и поддержке проекта.
Начните использовать его в своих проектах на PHP, и вы сразу заметите, насколько проще стало управлять кодом и вносить изменения.
Дополнительные материалы для изучения
- Для чтения: Статья Martin Fowler о шаблоне Repository (англ.) — классика от эксперта в области проектирования.
- Для просмотра: Паттерн Репозиторий на практике — видео с разбором практического применения.