Как известно, с ростом размера кода приложения его становится все сложнее и сложнее поддерживать. Рассмотрим подход, как с наименьшими усилиями структурировать код Symfony приложения так, чтобы снизить затраты на внесение в него изменений и упростить переиспользование или замену его частей. По каким принципам разбивать функционал на модули, как обобщать, как называть, разберем на примере. У нас будет цельное приложение, но если понадобится, выделить нужный компонент мы сможем с минимальными усилиями.
И так, берем за основу какое-то базовое понятие, например, товар, и вокруг него начинаем обобщать. Создаем папку Product. У товара может быть категория и другие сущности, можем поместить их внутрь. Какой-то функционал можно выделить отдельно, и он будет зависеть от Product. Надо смотреть насколько эта логика связана, насколько сложно ее разделить, и насколько это необходимо. Это разделение можно менять со временем, делать рефакторинг.
Так, по мере необходимости, можно будет выделять компоненты. Обозначим некоторые особенности компонента. Компонет может зависеть от другого компонента, но этот другой компонент не может зависить от него, и циклической зависимости через другие компоненты не должно быть. Иначе это все будет один цельный компонент, разделенный на модули. Компонент мы можем переиспользовать или заменить на подобный компонент с минимальными усилиями. Внутри можно делать его, как удобно разработчикам, главное, чтобы было понятно как им пользоваться. Можно разделить его на слои. Можно не делить или делить не полностью. Можно выделить какой-то слой в отдельный компонент. Необязательно давать слоям конкретные имена слоев, это могут быть просто разные классы, папки. Не забываем, слои верхнего уровня не зависят от слоев нижнего уровня. Если так происходит, применяем инверсию зависимости или делаем адаптер. Нижний уровень находится наиболее близко к I/O — это репозитории, стороннее API, шаблоны Twig. Далее идут контроллеры и наши сервисы с логикой. Делаем по-простому, усложняя по мере необходимости. Либо сразу делаем сложно, если знаем, что это может понадобиться. Про SOLID не забываем, но тоже применяем по необходимости.
Для удобства можно оставить общие папки с Entity, Repository, Controller, как в Symfony сделано по умолчанию, чтобы не прописывать каждый раз к ним пути в конфиге, либо нужно автоматизировать этот процесс. Yaml конфиги, шаблоны, переводы так же остаются по умолчанию. Остальной код, код модулей можно поместить по традиции в папку Service. Внутри папок можно дублировать названия модулей. В дальнейшем так проще будет выделить переиспользуемый компонент, бандл, если понадобится.
Entity отображают таблицы, можно работать с ними, как с простыми структурами, DTO. Не стоит путать с концепцией Entity из слоистой, гексагональной архитектуры, DDD. Это просто данные, минимум логики. Так мы не привязываем структуру наших классов с логикой к структуре таблиц БД, можем пользоваться DI контейнером, и при необходимости через интерфейсы или DTO можно будет довольно легко отделить логику от конкретной сущности. Если код сущности сильно разрастается, можно делить его на трейты PHP или делать связи один к одному.
Всю логику пишем в сервисах, называем сервис по его назначению. Например, если логика относится к Product, кладем ее в ProductService. Если сервис будет расти, мы сможем выделить из него, например, ProductStoresService. Соответственно, лучше сразу писать логику и учитывать, что она, возможно, будет отделена. И лучше, конечно, сразу правильно ее разделить, чтобы в дальнейшем меньше времени потратить на рефакторинг.
Если ProductStoresService лежит в папке Product, то мы можем сократить название до StoresService. А можно переименовать, к примеру, в InventoryControl (складской учет). Можно выделить его отдельно. В общем, вариантов много. Название ProductStoresService универсальное, говорит нам, что алгоритм сервиса обрабатывает данные складов по отношению к товару. Так же может быть создан StoreService, в котором содержится логика для одного склада, независимая от товара, если она имеется, и так далее.
А если будет происходить взаимодействие складов не только с товарами, а с чем-то еще, например, с пользователями, заведующими складами, то может появиться другой StoresService, в другой папке — UserStores. А UserStores будет зависеть от User и Product. Но если мы не хотим, чтобы UserStore зависел от целого Product, придется выделять из Product отдельно Store. Но тогда Product начнет зависеть от Store, а товар вполне себе может существовать без склада. То есть нам не нужна зависимость Product от Store. И чтобы убрать ее, нужно сделать отдельно еще ProductStores, который их связывает вместе. А товар без склада у нас не имеет смысла, нам больше нечего хранить на складе, кроме товаров. Поэтому мы можем включить Store внутрь ProductStores. И так, у нас остаются: Product, ProductStores, User, UserStores. UserStore зависит от User и ProductStores, а ProductStores — от Product. Теперь UserStores зависит от Product не напрямую, а косвенно через ProductStores. Конечно, на практике вряд ли нам может понадобиться UserStores без Product, поэтому ProductStores можно включить в Product. А User может существовать и без Store. И если мы включим UserStores в User, то User будет тянуть за собой Product, что довольно странно. В итоге оставляем: Product, User, UserStore.
Repository можно оставлять пустыми и использовать их в наших репозиториях, которые мы будем создавать в модулях, расположенных в Service. Эти репозитории — простые сервисы, которые используют Doctrine Repository. Репозитории нужны, чтобы абстрагироваться от хранилища. Так же для каждого модуля можно создавать PHP конфиги в виде сервисов и не использовать YAML.
Если код небольшой, можно писать его прямо в контроллере. Но можно и в отдельном сервисе или нескольких сервисах. Можем назвать их UseCase, сценарии использования. Они, в свою очередь, будут вызывать другие сервисы более высокого уровня. В классе контроллера Symfony содержится довольно много полезных функций, оберток, поэтому абстрактный юзкейс можно наследовать от него, либо создать свой.