Загрузка...

Шаблон проектирования «Репозиторий»

Шаблон Проектирования Репозиторий (Repository Pattern) в PHP: Полное руководство
php

Шаблон проектирования «Репозиторий» (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";
}

Ключевые преимущества использования шаблона

  1. Сокрытие сложности доступа к данным: Весь SQL-код, работу с API или файловой системой инкапсулирован в одном месте.
  2. Упрощение модульного тестирования: Для тестирования сервисов, использующих репозиторий, вы можете подменить DatabaseBookRepository на InMemoryBookRepository или создать Mock объект, реализующий BookRepositoryInterface. Это позволяет тестировать логику в изоляции от базы данных.
  3. Централизация логики запросов: Все запросы к источнику данных (например, к таблице books) находятся в одном классе. Это устраняет дублирование кода и упрощает его изменение.
  4. Гибкость архитектуры: Если вы решили сменить базу данных (например, с MySQL на PostgreSQL) или способ хранения (перейти с базы данных на внешнее API), вам нужно изменить всего один класс — реализацию репозитория. Остальной код приложения останется нетронутым.

Частые ошибки и лучшие практики

  • Не путайте Репозиторий и DAO (Data Access Object): Репозиторий работает с коллекциями объектов и оперирует понятиями предметной области (Domain-Driven Design). DAO — это более низкоуровневый паттерн, который часто просто отображает строки таблицы БД на объекты.
  • Избегайте «Универсального репозитория» (Generic Repository): Не стоит создавать один репозиторий для всех сущностей (class GenericRepository { ... }). У каждой сущности свой уникальный набор методов и запросов. Создавайте отдельный репозиторий под каждую агрегирующую сущность.
  • Используйте Интерфейсы: Всегда определяйте интерфейс для репозитория. Это основа для внедрения зависимостей и мокинга в тестах.
  • Внедрение зависимостей: Передавайте репозиторий в сервисы через конструктор (Dependency Injection). Это делает зависимости явными и код — более тестируемым.

Заключение

Шаблон Репозиторий — это не просто «еще один способ работы с базой данных». Это архитектурный инструмент, который помогает строить сложные приложения с четкой структурой. Он требует написания немного большего количества кода на начальном этапе, но с лихвой окупается при дальнейшем развитии, тестировании и поддержке проекта.

Начните использовать его в своих проектах на PHP, и вы сразу заметите, насколько проще стало управлять кодом и вносить изменения.


Дополнительные материалы для изучения

Добавить комментарий

Ваш адрес email не будет опубликован. Обязательные поля помечены *