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