Здравствуйте! Изучаю php, плавно переходя от велосипедостроения к best practices. Проектируя очередное приложение понял, что будет очень уместно использовать паттерн Репозиторий, чтоб отделить бизнес-логику от CRUD + использовать разные хранилища для разных сущностей.
Сделал два интерфейса IOUserRepository, IOPostRepository для сущностей User и Post сооствественно.
Классы для работы с бд, реализующие эти интерфейсы MySQLUserRepository и RedisPostRepository и сами классы сущностей User и Post, которые содержат логику (например User->ban(), Post->approve() и т.д)
И сразу созрел вопрос, а как организовать связь этих сущностей? Т.е при использовании ActiveRecord , я делал так User::find(1)->post()->where('is_approved', '0')->first()->approve() (пример для ELoquent). Один из выходов, который я вижу:
1) В классе User делаем array $posts
2) В классе RedisPostRepository делаем метод getPostsByUserID($user_id), который возвращает массив статей для пользователя
3) В методе load() класса MySQLUserRepository вызываем вышеуказанный метод, для заполнения массива $posts из пункта 1, но для этого необходимо передать объект класса, который реализует IOPostRepository ( в DI-контейнере делает биндинг интерфейс->класс, используется Pimple)
Самом собой, в БД связь один-ко-многим (one User -> many Posts).
Получается, при использовании этого паттерна надо делать связь на уровне репозиториев ?
P.S. Очень возможно, что я все же неверно понял суть данного паттерна. Если это так, просьба ткнуть носом в ошибки.
сами классы сущностей User и Post, которые содержат логику (например User->ban(), Post->approve() и т.д) Это не правильно, это должно быть в сервисном уровне
Получается, при использовании этого паттерна надо делать связь на уровне репозиториев ? верно
А транзакции запускать в сервисах
Т.е получается, что логика хранится в отдельных классах-сервисах (которые загружается через сервис-провайдеры ? ), а сами классы User и Post, содержат только данные, т.е просто набор переменных ?
synapse_people, ага, ясно. Теперь последний вопрос, какая последовательность действий в контроллере? т.е например пришел запрос из админки на бан пользователя. В контроллере я:
1) Обращаюсь к репозиторию и загружаю по id\логину\etc сущность User
2) Передаю сущность в сервис UserService->setUser($user);
3) Выполняю действие логики UseService->ban()
4) Возвращается сущность User
5) Сохранию через репозиторий обратно (update)
Все верно?
Или сервис должен сам обращаться к репозиторию?
UPD: Пропустил пункт про транзакции в сервисах, получается мы передаем репозиторий в сервис и он сам сохраняет\удаляет из БД через репозиторий
LazyDaemon, нет, смысл такой
Контроллер вызывает метод ban($userId) на сервисе
Сервис стартует транзакцию
Сервис получает пользователя с таким ID из репозитория, обновляет что-нить или вставляет записи о бане в репозитории ( сервис может использовать много репозиториев сразу, а также вызывать методы на других сервисах)
Сервис завершает транзакцию и бросает исключение, если ошибки
Посмотрите тогда Dependency Injection (должен дать сервису реализацию требуемого репозитория), ORM (поможет для маппинга сущностей с таблицами)
В идеале, сущность из сервиса никуда не должна передаватся, то есть инкапсулирована
synapse_people, огромное спасибо, очень не хватало целостной картины!!! Про DI подробно читал, когда изучал Laravel, так что понимаю как передать реп сервису) Пока вместо ORM пишу обычный SQL, т.к хочу его тоже подтянуть, да и тестовый проект позволяет. Если дойдет до продакшена, само собой ORM подключу, благо репозиторий позволяет это без боли сделать)
synapse_people, воспользуюсь моментом и тоже спрошу ;) можно же сделать трейт к примеру Bannable для сервиса, чтобы все же юзеру добавить функционал? То есть из трейта уже обращаться к сервису. И как это лучше делать?
Да, здесь есть сервис для бана конечно же, но трейт просто делает интерфейс к сервису более простым и никак не нарушает какие то принципы. Просто в слое приложении ты добавляешь уже юзеру функционал сервиса, а можешь и не добавлять, а юзать сервис напрямую. Так что волне норм.
И кстати вы написали, что в сервис передавать id надо. Но лучше же интерфейс передавать, так как id нас очень ограничивает. Здесь, например, юзер реализует интерфейс Bannable, что думаю гораздо правильней.
Wentixon, в этом и суть, логика должна быть в сервисе, остальные компоненты должны использовать только апи интерфейса этого сервиса, желательно, чтобы все аргументы в методах на интерфейсе были примитивами
Легче будет сменить реализацию сервиса в случае необходимости