Я регулярно вижу статьи в стиле "как использовать шаблон Репозиторий с 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 и очень хочется побаловаться шаблонами проектирования, то в следующей статье я покажу как можно частично применить шаблон Репозиторий и получить от этого пользу.
