Коротко: простая архитектура Symfony — слои с единственной ответственностью: контроллеры валидируют, сервисы содержат логику, репозитории делают запросы, Entity отражают таблицы.

Простейшая адекватная архитектура Symfony-приложения — это разделение на слои: контроллеры принимают и валидируют данные, сервисы содержат бизнес-логику, репозитории делают сложные запросы к БД, Entity отражают таблицы. Главное правило — единственная ответственность каждого слоя. Разберём схему и типичные ошибки. Статья для новичков.

Архитектура — большой и во многом спорный вопрос, но с чего-то начинать надо. Сам фреймворк имеет наработки в этом направлении, но не все им следуют, поэтому предложу максимально близкую к фреймворку и, на мой субъективный взгляд, адекватную схему распределения функционала.

Как разделить приложение на слои?

Entity, как и диктует фреймворк, храним в папке Entity. Контроллеры по умолчанию в Controller — тут тоже всё хорошо. Но дальше начинается самое интересное: у нас есть точка входа (контроллер) и БД — их надо увязать и где-то написать бизнес-логику. Фреймворк предлагает много подходов, но в документации это подано сумбурно, и новички часто пишут «ерунду»: то всё слито в один класс, то раскидано по случайно названным классам.

Наша цель — систематизировать и хотя бы первично разделить приложение на слои:

  1. Контроллеры — приём данных, валидация, выдача данных на фронтенд;
  2. Сервисы — бизнес-логика приложения;
  3. Репозитории — сложные запросы в БД (нативно или через QueryBuilder);
  4. Entity — сущность, отражающая таблицу в БД;
  5. Listener / Security / Interfaces и т.д. — прочие сервисы, встроенные во фреймворк или написанные самостоятельно.

Теперь схематически расположим их так, как они должны взаимодействовать:

Схема архитектуры Symfony: Frontend, Controller, Service, Repository, Entity с нумерованными потоками данныхНа схеме чёрным цветом показано, как классы должны общаться между собой, красным — как делать не стоит.

Как проходит запрос: зелёный сценарий

От фронтенда приходит запрос в контроллер (1). Контроллер принимает данные (Request) и валидирует их через ValidatorInterface — это логически его работа (приём и валидация ради безопасности приложения). Дальше нужна логика, не относящаяся к обязанностям контроллера.

Поняв, что данные валидны, контроллер зовёт нужного «работника» — вызывает соответствующий сервис и его метод, передав туда полученные с фронта данные.

Сервис обычно логически относится к одной бизнес-сущности: есть Entity/Article — будет и src/Service/ArticleService с бизнес-логикой работы со статьями.

Контроллер хочет получить статью и связанные сущности — например, 3 статьи того же автора не старше 3 месяцев. С этими «хотелками» он обращается в сервис, а тот — в репозиторий (3). Репозиторий составляет несколько запросов в БД, выполняет их и отдаёт данные обратно в сервис (4).

Дальше сервис знает: по бизнес-логике при просмотре статьи надо увеличить счётчик её просмотров и счётчик показов рекомендованных статей. Сервис взаимодействует, возможно несколько раз, с Entity/Article (5 и 6), а затем обращается к EntityManagerInterface для сохранения данных методом flush() (7 и 8).

Выполнив всю бизнес-логику, сервис отдаёт данные контроллеру (9). Контроллер проверяет лишь одно — валиден ли ответ: вернул сервис ожидаемое — отдаёт фронту 200 и данные; вернул ошибку — отдаёт 400 и текст ошибки. Это простейший способ организации архитектуры в Symfony.

Чего делать не стоит (красные стрелки)

Это не строгие запреты, но за такой код потом в лучшем случае стыдно, а в худшем он принесёт кучу проблем. Почти всё ниже следует из первого принципа SOLID — Single Responsibility (единственной ответственности), то есть деления приложения на слои ответственности.

  1. Нельзя вызывать EntityManager в контроллере — это сервис фреймворка для работы с Entity, поиска и сохранения в БД; работе с ним не место в контроллере.
  2. Нельзя из репозитория отдавать наверх объекты типа QueryBuilder — это объекты для работы с БД, им не место за пределами репозитория.
  3. Не нужно писать простые запросы вида select * from user where id = 2 в репозитории — они спокойно выполняются через EntityManagerInterface в сервисе методами findBy, findOneBy, find. Методы репозитория должны быть действительно сложными.
  4. Не нужно располагать в Entity методы для работы с сущностью, даже простые, и тем более с осторожностью добавлять магические методы с логикой, меняющей штатное поведение Entity.
  5. Каждый класс должен работать только со своими объектами. Например, в сервис лучше передать массив или данные поштучно, а не Request, с которым логически должен работать только контроллер.

Всё перечисленное — рекомендации, а не жёсткие правила. Но помните: если вы внедряете собственные практики или архитектурные подходы, всегда будьте готовы аргументированно их защитить, особенно в сильной команде. И главный принцип в архитектурных спорах — не бойтесь признавать ошибки: важно не оказаться правым, а прийти к истине. Всегда чётко отличайте аргумент, основанный на фактах, от чьей-то привычки, «хотелки» или легаси.

Если хотите выстроить понятную слоистую архитектуру на Symfony — помогу на консультации.