Comments 29
Имхо, логичнее было бы из контроллера вызывать сервис, который делает какую то свою бизнес-логику, вызывает Actionы (для записи) и репозитории (для чтения) или другие сервис классы. В таком случае контроллер занимается валидацией, преобразованием реквеста в DTO и ответом на запрос и код можно переиспользоваться в веб/апи роутах, консольных командах.
А в вашем варианте нужно по сути копировать код метода контролера для этого, да и в целом нарушается принцип использования Action классов. Они придуманы для выполнения какого то одного действия, а не вызовов сервисов и репозиториев.
Рекомендую к прочтению вот этого чела - https://martinjoo.dev/
Спасибо Вам за информацию. Обязательно ознакомлюсь.
Насчет единственного действия action здесь зависит как посмотреть.
Например есть бизнес-процесс оформления заказа который может состоять из шагов:
расчет скидок и пр.,
сохранение сущностей,
рассылка уведомлений и др.
Этот бизнес-процесс можно как раз и рассматривать как некоторое единое действие.
В таком случае у нас есть action который оформляет заказ, но содержит в себе сервисы и репозитории нужные для каждого из шагов. Далее этот же action можно использовать в консольных командах (тк данные на вход через DTO подаются). Кроме того эти же сервисы мы можем использовать отдельно в других местах. Как вариант можно заменить action сервисом который также будет в себе агрегировать другие сервисы соответствующие шагам оформления заказов.
Не обязательно рассматривать Actions как средства для записи в БД. Здесь речь идет скорее об Actions-слое(в терминал Фаулера - Transactions Script), который реализует бизнес-логику. Если вам не по нраву названия Actions, можете переименовать в Handlers или Invokers - без разницы, важна суть) То есть у DDD есть несколько разных реализаций, одна из которых приведена здесь. Автор говорит о том, что Actions не должны иметь зависимостей от БД(нужно использовать репозитории) и должны использовать сервисы, которые реализуют опять-таки интерфейсы.
Главный плюс такого подхода - это совершенно удобное и простое написание юнит-тестов на бизнес-логику, расположенную в Actions.
А как вы решаете бизнес процессы пересекаются? Например есть заказ и покупатель. В каком модуле должен происходить процесс создания заказа? В user или order? Или вы создаете отдельный связующий модуль?
В Вашем примере я бы вынес процесс оформления заказа в модуль Order в сервис OrderCreateService (заказ может создаваться например не только из под обычного пользователя, но и менеджером магазина, через обмен с внешними системами и др.).
Можно еще объединить модули которые имеют тесные связи, например, так для модулей магазина: Sale -> Order|Discount|etc, для модулей каталога Catalog -> Product|Feature|etc
Те есть модули агрегирующие в себе другие модули.
Пришел почти к такому же виду после нескольких лет работы с laravel, очень похожее получилось. И на самом деле схема очень удачная на мой взгляд. Но советую посмотреть в сторону вынесения бизнес логики в директорию source, что бы можно было отделить сервисы к примеру от неймспейса приложения и тогда в контроллерах use будет выглядеть след. образом use Source\Modules\User\CreateService;. Это первый момент а второй момент, в файлах директории source сразу будут видны зависимости из app и там можно пресечь протекание, не нужных зависимостей в бизнес логику, или допустим исключить попадание реквестов в бизнес логику. Но поработав с такими схемами пришел к выводу 1 - накопилось много кода на разных проектах который нужно выносить в отдельные репозитории, например абстрактный репозиторий там есть что сделать на самом деле, 2 - появились мысли о том как сделать еще более независимой логику, а это уже переход на подобие DDD, но вот пока не доберусь до реализации. А вообще радует что у кого то схожие мысли появились)))
На мой взгляд, подход здравый и оптимальный
Если нам необходима просто выборка, то можно не использовать сервисы, а обращаться напрямую к классу репозитория.
Зависит от бизнес-логики. В некоторых случаях выборку из репозитория стоит вызвать в сервисе, если предполагается дополнительная логика (какой-либо маппинг, рантайм фильтрация и т.п)
Controller обращается к action передавая DTO на вход
DTO могут создаваться через обычный конструктор, либо через вызова метода который возвращает сам DTO. Примерами может служить вызов
UserDTO::fromRequest($request)
UserDTO::fromArray($data
По хорошему DTO должен содержать только те поля, которые ему действительно нужны. Либо вместо примитивов другие DTO. DTO - это просто объект с данными. Собирать в нем самого себя из реквеста не стоит. Для таких целей удобнее всего создавать классы-ассемблеры для сборки необходимых вам DTO. Либо в пространстве App\Http\Assemblers, либо в App\Services\User\Assembler. Например: App\Services\User\Assembler\UserDTOAssembler.php
Код разбивается, исходя из логики принадлежности к Домену
Это уже касается скорее DDD, при таком подходе всю вашу структуру нужно переделывать, но далеко не везде и не всегда DDD нужен
В идеальном случае, модуль — это независимая часть бизнес-логики. При модульной организации кода структура у нас получается примерно следующей:
Если говорить о модульности, то все эти папки в целом не нужны - это отдельные под-проекты/микросервисы/библиотеки. Если на старте проекта изначально ясно, что будет какая-то админка - нужно создавать отдельный репозиторий для нее. Если же код для основного проекта и админки общий - выносить в отдельную библиотеку, желательно с DDD подходом. В таком случае код админки отвечает только за админку, шума и мусора в проектах становится меньше, поддерживаемость улучшается. Так же, стоит не забывать, что за таким подходом нужно тщательнее следить, тратить доп.время и т.д, поэтому все это индивидуально и зависит от требований конкретного бизнеса
P.S. Для простых проектов ваша структура хорошо подходит, но для более крупных уже нужно будет задумываться о реструктуризации
Более продвинутый вариант https://github.com/Mahmoudz/Porto
Зачем в Laravel использовать дополнительный слой с патерном репозиторий, если Eloquent уже его использует?
Основная идея в том чтобы создать слой абстракции для переиспользования кода + удобнее читать код (для примера есть выборка 80+ строк кода или та же выборка но уже в классе $productRepository->getUserProducts(...). Думаю что когда эта выборка обернута в репозиторий то такой код легче читается.
Все это обосновано, когда речь идет, например, о symfony. Для laravel достаточно иметь несколько сервисов, в которых и будет выборка. Тем самым вы сокращаете путь, убирая лишний промежуток. Если смотреть в разрезе ваш подход, то получится следующее: Controller -> Service -> Repository -> Eloquent (Repository)... Переиспользование кода с Eloquent уже реализовано из коробки. Там достаточно сделать функции-заготовки, которые возвращают Query Builder - переиспользуйте сколько захотите!
В своей практике Repository использую не только на уровне сервисов, но из в Controller обращаюсь. Если сложные выборки функции-заготовки (насколько понял Вы имеете ввиду универсальные заготовки по типу getList итд) частично решат проблему, но все же это будет менее читаемый код по-моему мнению.
Т.е. понятия "тонкий контроллер" вам не известно?..
ВЫ сейчас пытаетесь меня убедить в удобстве для вас, а не в общепринятых практиках. Ну как говорится "Каждый сходит с ума по-своему". Предлагаю закрыть этот thread и пусть каждый останется при своем.
Честно говоря не совсем понимаю почему такие выводы. Контроллеры все равно получаются тонкими. Например, есть выборка списка чего-либо. В контроллере есть репозиторий который отдает свой ответ оборачивая в Collection. В итоге получается максимум 5 строчек кода в контроллере.
По второму пункту полностью с Вами согласен)
есть логика и смысл в словах@SpinyMan, действительно, репозитории в laravel избыточны т.к. есть Eloquent, хотя в некоторых участках кода самого laravel используются названия Repository, но это никак не связано с паттерном. Возможно следует использовать свой EloquentBuilder и использовать его вместо Repository, там намного все встанет кананичнее имхо.
Из моей скромной практики на laravel репозиторий становится как некий раздутый шар.
Основной посыл репозиториев это реализовать контракт работы с базой, чтобы в любой момент заменить orm/бд на другую имплемнтацию
Часто меняете базы данных? :)
Даже если так, и вам не хватает баннальной смены граматики и драйвера на уровне фрэймворка. От мигриций, сидеров и фэйкеров скорее всего не уйти... получается зря потраченное время на код (((
Регулярно, и стоит понимать что меняем не обязательно всю бд, а несколько таблиц. Миграции и сидеры это все выносится в инфраструктурный код и он не должен протекать в другие слои приложения.
удобнее для будущих изменений
делаем интерфейс Репозитория, реализуем через класс EloquentModelRepository.
если понадобиться работать с нереляционной БД, то реализуем другой класс и подменяем, ну или через зависимость интерфейс указываем.
Вы о5, так же как и автор, смотрите в сторону symfony... Вы пытаетесь воспроизвести то, что не свойственно Laravel! Надстройку репозитория над репозиторием! Не нужно этого делать в laravel! Зависимости уже реализованы из коробки! Все, что вам нужно, это поменять настройки и выбрать нужную вам базу. Это похвально, что вы понимаете инверсию зависимостей, но не там вы ее применяете.
На самом деле если порезать Services на методы, то получатся Actions, а если порезать Repositories на методы получатся Queries. Action (пишет в базу) может вызывать другие Actions и Queries для выполнения своей бизнес задачи. Query (читает из базы) может вызывать только другие Query, Поскольку action может вызывать другой action может возникнуть кольцевая зависимость с чем и борется создатель упомянутого Porto. Но на практике на мелких проектах это должно ловится на деве и тестах и усложнять описанную схему я не хочу, Проблема в моём подходе пока одна - слишком много мелких классов т..к на getById нужен отдельный класс и мне придется объединять их в папки, которые по сути и будут репозиториеми, а папок и так уже много с учетом модулей, дто и прочего.
Кстати, всегда было интересно, почему все делают вот так
UserDTO::fromRequest($request)
вместо того чтобы делать вот так
$request->getUserDto()
ведь реквест уже содержит все данные и валидацию для них, а получение из него DTO уникально и больше нигде кроме места с реквестом не пригодится, так почему метод формирования у находится в DTO?
Спасибо за комментарий. По поводу UserDTO::fromRequest($request) Если мы привязываемся к $request, то когда нам понадобиться сделать DTO из чего-то отличного от объекта Request придется думать как это сделать (например из массива, файла итд).
При использовании UserDTO мы просто создадим еще несколько методов вида UserDTO::fromArray(...), UserDTO::fromFile(...) итд
Спасибо за ответ. Если дто делается из массива, то метод его создания кладется рядом, да если в другом месте будет точно такой же массив - будет неудобно) Но часто у Вас встречается fromArray из двух разных мест? у меня - никогда (тут стоит уточнить что дто я создаю относительно часто вот только для этого всегда лучше подходит конструктор, а чтобы применить fromArray массив должен быть прям один в один), хотя это может сильно зависеть от проекта... Использовать же fromFile т.е. делать парсинг файла внутри дто (я правильно понял?) мне не позволит совесть (по мне это нарушение srp)
Позвольте позанудничать.
А какое отношение статья имеет к Laravel? Если заменить название фрейморка на другое, ничего по большей части не изменится.
Смысл ДТО в транспортировке данных между слоями. Не понимаю почему в них так и норовят вкорячить логику fromArray, fromRequest... Это ужасно. Ещё не понимаю прикола с репозиториями, которые читают сохраняют данные одновременно. Так же не понятна идея передачей действий из controller в action, чтобы он потом передал в service и repository. Все это похоже на неудачные попытки затянуть фишки луковой архитектуры в трёхслойный MVC минуя нужные слои.
Всё уже придумано до нас как говорится.
Есть же паттерн Porto и построенная на нем обёртка под Laravel apiato. Я просто перешёл на неё с обычного Laravel
Организация кода в Laravel. Личный опыт