Я регулярно вижу статьи в стиле "как использовать шаблон Репозиторий с 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 и очень хочется побаловаться шаблонами проектирования, то в следующей статье я покажу как можно частично применить шаблон Репозиторий и получить от этого пользу.
Комментарии (29)
webviktor
21.03.2019 16:43— регулярно вижу статьи в стиле «как использовать шаблон Репозиторий с Eloquent»
Впервые о таком слышу. Статей ни одной не видел.
Adelf Автор
21.03.2019 17:05Вон в PHP-дайджесте недавнем была — habr.com/en/post/441584. Это кстати и стало поводом для написания этой статьи.
trawl
21.03.2019 18:06одна такая попала в недавний PHP-дайджест
Тоже негодовал по этому поводу (особенно с тем решением, что предлагалось в статье)
Не буду в этой статье ругать ненужный суффикс Interface
И не нужно ругать. Как-никак, PSR Naming Conventions
А в целом, согласен с автором.
xotey83
21.03.2019 18:57+1Внесу небольшую поправку по поводу PSR Naming Conventions: это внутренний документ, регулирующий правила именования для самих PSR. То есть один из стандартов для публикуемых рекомендаций.
PSR Naming Conventions предназначен для разработчиков PSR и не оформлен сам как PSR, а потому он не является рекомендацией для нейминга теми проектами, которые используют PSR.
UPD. Впрочем как и Symfony Conventions — это, соответственно, тоже правила и конвенции, предназначенные для разработчиков и контрибьютеров Symfony
greabock
21.03.2019 19:13Всегда добавляю суффикс Interface (за исключением тех случаев, когда название интерфейса является прилагательным). Но PSR Naming Conventions тут вообще не причем. Просто в глобальном поиске IDE (PHP Storm в моем случае) легче отличать интерфейс от имплементации. Вот такой вот я плохиш )
DaleMartinWatson
21.03.2019 19:37А зачем их отличать?
Видел и NameInterface, и NameContract, и IName и даже I_Name, но никокого практического смысла так из этих приемов и не извлек.
catanfa
21.03.2019 19:52+1Суффикс Interface — это венгерская нотация для более высокого уровня абстракции. Раньше тип переменных включали в имя переменной (intCount, strName, и т.д.). Теперь возможности языка и IDE таковы, что в венгерской нотации нет необходимости, в том числе и для интерфейсов. Почему мы все классы в проекте суффиксом Class не награждаем? Пора сделать код более читабельным и отказаться от мусорного суффикса для интерфейсов в том числе. Список литературы:
https://www.alainschlesser.com/interface-naming-conventions/
https://dev.to/scottshipp/when-hungarian-notation-lies-hidden-in-plain-sight-372
https://phpixie.com/blog/naming-interfaces-in-php.html
zelenin
21.03.2019 20:17интерфейс легко отличается по иконке
greabock
21.03.2019 22:15+2Всё верно, но бывает, что он даже не попадает в первый экран выдачи. А когда ты добавляешь заветное
I
, то оказывается прямо под рукой. Я например, для перехода кCacheInterface
забиваюCaI
, и шторм махом понимает что именно я хочу. В то время, как если бы это был простоCache
, то я бы собрал всю папку vendor и все, что там называется Cache, и не факт что интерфейс оказался бы в первом экране выдачи.
L0NGMAN
22.03.2019 01:36Не согласен с автором. Лучше прикрутить репозитории, чем писать eloquent запросы в контроллере. Мы же хотим использовать те же запросы и в других местах тоже? А ещё когда понадобится прикрутить кеширование? Если есть репозиторий то добавишь декораторы и будет удобное кеширование
Adelf Автор
22.03.2019 09:55Если же у вас в проекте Eloquent и очень хочется побаловаться шаблонами проектирования, то в следующей статье я покажу как можно частично применить шаблон Репозиторий и получить от этого пользу.
Как раз про это будет.
n0wheremany
22.03.2019 09:40Много слов.
В 99% случае в модели нужно описывать методы с отношениями к другим таблицам. Т. е. Ломается логика в получении данных не через репозитории зависимой модели, а через встроенный обработчик отношений.
Зачем такие танцы с бубнами? Нужен ActiveRecord — используй его полностью, а не создавай себе геморрой в голове со скрещиванием ужа с ежом.Adelf Автор
22.03.2019 10:43ну в какой-нибудь доктрине тоже отношения часто через прокси реализованы. смысл в персисте. во всех реализациях Active Record — приходится вручную их в базу кидать, либо минуя репозиторий, либо использовать другой «репозиторий».
zhulan0v
22.03.2019 10:11Поддерживаю, никогда не понимал сакрального смысла репозитория возвращающего eloquent модели
olafars
22.03.2019 14:34Я отчасти понимаю негодование автора, НО автор слишком заостряет внимание на шаблоне.
1. Шаблон это рекомендация.
2. Помимо разделения и тестирования есть ещё жизненный цикл. И подобный подход позволяет в определённой степени поддерживать и обновлять продукт гораздо проще со всеми итерациями рефакторинга и дальнейшей разработкой. С тестированием да, возникают проблемы.
3. Автору оригинала задавали уйму вопросов в комментариях, в том числе из разряда, что ваш репозиторий — не репозиторий вовсе.
Дочитывая пост до конца, остаётся чувство, будто на тебя вылили ушак помоев, но извиниться забыли. Зачем автор вместо того, чтобы писать пост «программистского гнева», который в целом понятен и я отчасти его поддерживаю, не предложил сразу альтернативный вариант, который отвечает всем «критериям» автора?
В любом случае, спасибо за материал, жду альтернативный подход.Adelf Автор
22.03.2019 14:34Пожалуй, с этой точки зрения действительно стоило не превращать это в две статьи, а написать все в одной…
evgwed
Дико плюсую автору! Хватит впиливать в проекты сложноподдерживаемые решения и потом мучиться с ними. Если так сильно хочется использовать репозитории, то переходите на Symfony. Это позволит сократить время, которое вам потребуется, чтобы поддерживать свой велосипед на репозиториях.
greabock
Начал за здравие, кончил за упокой. Я тоже полностью согласен с автором, но при чем тут Symfony? Это проблема модели взаимодействия с бд AR а не фреймворка.
Adelf Автор
Люди часто доктрину связывают с симфони. Я спокойно юзаю доктрину в ларавель проекте…
zelenin
следующий вопрос: причем тут доктрина? )
greabock
Оратор выше, видимо, имеет ввиду, что репозитории с Data Mapper работают прекрасно, в отличии от репозиториев с Active Record.
Doctrine — единственная более менее съедобная имплементация Data Mapper на PHP.
Я же указываю на то, что Doctrine является самостоятельным проектом, разработка которого напрямую никак не связана с Symfony (косвенно может и связана — они там все общаются же).
В общем, переходить на Symfony, просто потому что там Doctrine — глупо (у вас должны быть более веские основания чтобы сменить фреймворк, в котором уже наработан опыт), потому как Doctrine точно также подключается и в Laravel. Я даже знаю человека, который наоборот цеплял Eloquent (illuminate/database) в проект на symfony. Ума не приложу, зачем это ему, но речь не об этом. Я всего лишь хотел сказать, что эти конкретные ORM не завязаны на фреймворк, как таковые. И, при желании, они легко запиливаются/выпиливаются (хотя, с выпиливанеим Eloquent из Laravel, все несколько сложнее, но не безнадежно).
zelenin
А вот оно что — вы призыв перехода на симфони связали с тесной интеграцией с ней доктрины (хотя это прямо нигде не указано).
И тут же пишете, что "Doctrine является самостоятельным проектом" и "ORM не завязаны на фреймворк", хотя тред именно с вашей подачи идет в интерпретации "Симфони = Доктрина".
greabock
Это не я сказал. Репозитории можно использовать в том фреймворке, в котром вам вкусно их использовать. Но не с любой ORM.
P.S. я не понимаю в чем вы хотите меня уличить.
P.P.S Можно использовать репозтории даже с Eloquent, если считать, что модели Eloquent это не модели предметной области, а их маппинг на базу данных.
В этом случае, у вас полчится забавный каламбур с DM работающим поверх AR.
zelenin
лично я понял это как отсылку к излишней академичности решений на симфони. Ведь репозитории действительно можно использовать везде, а репозитории Доктрины — это явно не те абстрактные репозитории, о которых идет речь в статье.
greabock
Хм… а я последнее предложение понял буквально. Я почему-то подумал, что «сложноподдерживаемые решения» — это именно репозитории с AR, а не репозитории вообще.
Я уж к холивару приготовился. А тут просто недопонимание )