Я регулярно вижу статьи в стиле "как использовать шаблон Репозиторий с Eloquent" (одна такая попала в недавний PHP-дайджест). Обычное содержание их: давайте создадим интерфейс PostRepositoryInterface, EloquentPostRepository класс, красиво их забиндим в контейнере зависимостей и будем использовать вместо стандартных элоквентовских методов save и find.
Зачем этот шаблон нужен иногда вовсе не пишут ("Это же шаблон! Разве не достаточно?"), иногда что-то пишут про возможную смену базы данных (очень частое явление в каждом проекте), а также про тестирование и моки-стабы. Пользу от введения такого шаблона в обычный Laravel проект в таких статьях сложно уловить.
Попробуем разобраться, что к чему? Шаблон Репозиторий позволяет абстрагироваться от конкретной системы хранения (которая у нас обычно представляет собой базу данных), предоставляя абстрактное понятие коллекции сущностей.
Примеры с Eloquent Repository делятся на два вида:
- Двойственная Eloquent-array вариация
- Чистый Eloquent Repository
Двойственная Eloquent-array вариация
Пример первого (взят из рандомной статьи):
<?php
interface FaqRepository
{
public function all($columns = array('*'));
public function newInstance(array $attributes = array());
public function paginate($perPage = 15, $columns = array('*'));
public function create(array $attributes);
public function find($id, $columns = array('*'));
public function updateWithIdAndInput($id, array $input);
public function destroy($id);
}
class FaqRepositoryEloquent implements FaqRepository
{
protected $faqModel;
public function __construct(Faq $faqModel)
{
$this->faqModel = $faqModel;
}
public function newInstance(array $attributes = array())
{
if (!isset($attributes['rank'])) {
$attributes['rank'] = 0;
}
return $this->faqModel->newInstance($attributes);
}
public function paginate($perPage = 0, $columns = array('*'))
{
$perPage = $perPage ?: Config::get('pagination.length');
return $this->faqModel
->rankedWhere('answered', 1)
->paginate($perPage, $columns);
}
public function all($columns = array('*'))
{
return $this->faqModel->rankedAll($columns);
}
public function create(array $attributes)
{
return $this->faqModel->create($attributes);
}
public function find($id, $columns = array('*'))
{
return $this->faqModel->findOrFail($id, $columns);
}
public function updateWithIdAndInput($id, array $input)
{
$faq = $this->faqModel->find($id);
return $faq->update($input);
}
public function destroy($id)
{
return $this->faqModel->destroy($id);
}
}
Методы all, find, paginate возвращают Eloquent-объекты, однако create, updateWithIdAndInput ждут массив.
Само название updateWithIdAndInput говорит о том, что использоваться этот "репозиторий" будет только для CRUD операций.
Никакой нормальной бизнес-логики не предполагается, но мы попробуем реализовать простейшую:
<?php
class FaqController extends Controller
{
public function publish($id, FaqRepository $repository)
{
$faq = $repository->find($id);
//...Какая-нибудь проверка с $faq->...
$faq->published = true;
$repository->updateWithIdAndInput($id, $faq->toArray());
}
}
А если без репозитория:
<?php
class FaqController extends Controller
{
public function publish($id)
{
$faq = Faq::findOrFail($id);
//...Какая-нибудь проверка с $faq->...
$faq->published = true;
$faq->save();
}
}
Раза в два проще.
Зачем вводить в проект абстракцию, которая только усложнит его?
- Юнит тестирование?
Каждому известно, что обычный CRUD-проект на Laravel покрыт юнит-тестами чуть более чем на 100%.
Но юнит-тестирование мы обсудим чуть позже. - Ради возможности сменить базу данных?
Но Eloquent и так предоставляет несколько вариантов баз данных.
Использовать же Eloquent-сущности для неподдерживаемой им базы для приложения, которое содержит только CRUD-логику будет мучением и бесполезной тратой времени.
В этом случае репозиторий, который возвращает чистый PHP-массив и принимает тоже только массивы, выглядит намного естественнее.
Убрав Eloquent, мы получили настоящую абстракцию от хранилища данных.
Чистый Eloquent Repository
Пример репозитория с работой только с Eloquent(тоже нашёл в одной статье):
<?php
interface PostRepositoryInterface
{
public function get($id);
public function all();
public function delete($id);
public function save(Post $post);
}
class PostRepository implements PostRepositoryInterface
{
public function get($id)
{
return Post::find($id);
}
public function all()
{
return Post::all();
}
public function delete($id)
{
Post::destroy($id);
}
public function save(Post $post)
{
$post->save();
}
}
Не буду в этой статье ругать ненужный суффикс Interface.
Эта реализация чуть больше походит на то, о чем говорится в описании шаблона.
Реализация простейшей логики выглядит чуть более натурально:
<?php
class FaqController extends Controller
{
public function publish($id, PostRepositoryInterface $repository)
{
$post = $repository->find($id);
//...Какая-нибудь проверка с $post->...
$post->published = true;
$repository->save($post);
}
}
Однако, реализация репозитория для простейших постов в блог — это игрушка детишкам побаловаться.
Давайте попробуем что-нибудь посложнее.
Простая сущность с подсущностями. Например, опрос с возможными ответами (обычное голосование на сайте или в чате).
Кейс создания объекта такого опроса. Два варианта:
- Создать PollRepository и PollOptionRepository и использовать оба.
Проблема данного варианта в том, что абстракции не получилось.
Опрос с возможными ответами — это одна сущность и ее хранение в базе должно было быть реализовано одним классом PollRepository.
PollOptionRepository::delete будет непростым, поскольку ему нужен будет обьект Опроса, чтобы понять можно ли удалить данный вариант ответа (ведь если у опроса будет всего один вариант это будет не опрос).
Да и не предполагает шаблон Репозиторий реализацию бизнес-логики внутри репозитория. - Внутри PollRepository добавить методы saveOption и deleteOption.
Проблемы почти те же. Абстракция от хранения получается какая-то куцая… о вариантах ответа надо заботиться отдельно.
А что если сущность будет еще более сложная? С кучей других подсущностей?
Возникает тот же вопрос: а зачем это все?
Получить большую абстракцию от системы хранения, чем дает Eloquent — не получится.
Юнит тестирование?
Вот пример возможного юнит-теста из моей книги — https://gist.github.com/adelf/a53ce49b22b32914879801113cf79043
Делать такие громадные юнит-тесты для простейших операций мало кому доставит удовольствие.
Я почти уверен, что такие тесты в проекте будут заброшены.
Никто не захочет их поддерживать. Я был на проекте с такими тестами, знаю.
Гораздо проще и правильнее сосредоточиться на функциональном тестировании.
Особенно, если это API-проект.
Если же бизнес-логика так сложна, что очень хочется покрыть ее тестами, то лучше взять data mapper библиотеку вроде Doctrine и полностью отделить бизнес-логику от остального приложения. Юнит-тестирование станет раз в 10 проще.
Если же у вас в проекте Eloquent и очень хочется побаловаться шаблонами проектирования, то в следующей статье я покажу как можно частично применить шаблон Репозиторий и получить от этого пользу.
Автор: Adelf