Сервис-контейнер Symfony: чек-лист перед продакшеном
Autowiring экономит время в разработке, но на проде даёт скрытые расходы: холодная сборка контейнера, лишние публичные сервисы, дубли инстансов. Разбираем, что проверить перед деплоем Symfony 6.x/7.x.

Приложение на Symfony 7.1 в dev-режиме собирало контейнер за 40 мс на каждый запрос, потому что кэш пересобирался при любом изменении файла. На проде та же сборка происходит один раз при прогреве, но если забыть выполнить cache:warmup при деплое, первый запрос пользователя платит за компиляцию всего контейнера — а это сотни определений сервисов, компиляторные проходы и резолвинг автовайринга. Дальше — чек-лист того, что стоит проверить в конфигурации сервис-контейнера, прежде чем катить в прод.
Статья про конкретные грабли автоконфигурации и DI, а не про то, как устроен компилятор контейнера изнутри. Формат — практика: несколько подходов, у каждого цена. Версии: Symfony 6.4 LTS и 7.x, PHP 8.2+.
Первое: контейнер должен быть скомпилирован и прогрет
Symfony компилирует контейнер в набор PHP-классов и кэширует их. В dev при APP_ENV=dev фреймворк отслеживает изменения и пересобирает кэш автоматически. В prod этого механизма нет — контейнер собирается один раз, и если кэш пустой, платит первый запрос.
Правильный порядок при деплое:
- Установить зависимости без dev-пакетов:
composer install --no-dev --optimize-autoloader. Флаг--no-devвыкидывает пакеты изrequire-dev, чтобы отладочные бандлы не попали в prod-контейнер. - Выставить окружение:
APP_ENV=prodиAPP_DEBUG=0в.env.localили через переменные окружения сервера. - Прогреть кэш:
php bin/console cache:clear(в актуальных версиях эта команда уже включает прогрев) либо явноcache:warmup. Проверьте текущее поведение команды в документации вашей версии — оно уточнялось между релизами. - Убедиться, что каталог
var/cache/prodсобран и права на запись есть только там, где нужно.
Как понять, что прогрев прошёл: в var/cache/prod появляются скомпилированные файлы контейнера (класс вида Container*/App_KernelProdContainer.php). Если каталог пуст, а сайт открывается — первый запрос компилирует контейнер на лету.
Правило простое: если контейнер собирается во время обработки пользовательского запроса, вы отдали пользователю счёт за то, что должно происходить на деплое.
Публичные и приватные сервисы
По умолчанию в современном Symfony все сервисы приватные. Приватный сервис нельзя достать из контейнера через $container->get() — он доступен только через инъекцию. Это не ограничение ради ограничения: приватные сервисы контейнер может инлайнить и удалять, если на них никто не ссылается, что уменьшает размер скомпилированного контейнера.
Проблема возникает, когда старый код или сторонний бандл тянет сервис руками. Тогда его делают публичным — и он остаётся в контейнере навсегда, даже если реально нужен в одном месте.
# config/services.yaml — базовая настройка
services:
_defaults:
autowire: true # автоматически резолвим зависимости по типу
autoconfigure: true # автоматически вешаем теги по интерфейсам
public: false # сервисы приватные по умолчанию
App\:
resource: '../src/'
exclude:
- '../src/DependencyInjection/'
- '../src/Entity/'
- '../src/Kernel.php'
Что проверить перед прод: пройдите по проекту и найдите все public: true. Каждый такой сервис должен иметь причину быть публичным. Список публичных сервисов можно посмотреть командой php bin/console debug:container --show-private — она показывает и приватные, но публичные видно по колонке.
Почему это влияет на прод
Лишние публичные сервисы раздувают скомпилированный контейнер и мешают оптимизации. На маленьком проекте разница незаметна, на контейнере из тысяч определений — это лишние килобайты кода, который парсится при загрузке.
Autowiring: где он молча создаёт проблемы
Autowiring резолвит зависимости по тайп-хинтам в конструкторе. Работает, пока на один интерфейс приходится одна реализация. Как только реализаций две, контейнер не знает, какую подставить, и падает при компиляции — что хорошо, потому что падает на деплое, а не в проде.
Опаснее другой случай — когда автовайринг подставляет не то, что вы думаете, но не падает. Типичный пример: несколько сервисов реализуют один интерфейс, вы настроили алиас на один из них, а в новом коде рассчитывали на другой.
| Подход к резолву зависимости | Плюс | Минус / цена |
|---|---|---|
| Autowiring по типу | Ноль конфигурации, читаемый конструктор | Ломается при нескольких реализациях; неявная связь |
| Именованный алиас интерфейса | Явно указываете реализацию по умолчанию | Нужно помнить обновить при добавлении реализации |
Атрибут #[Autowire] на параметре | Локально и явно, видно в конструкторе | Размазывает конфигурацию по коду |
| Ручное описание в services.yaml | Полный контроль, всё в одном месте | Многословно, легко разойтись с реальностью |
Явный алиас интерфейса выглядит так:
# config/services.yaml
services:
# какую реализацию подставлять по умолчанию для интерфейса
App\Payment\PaymentGatewayInterface: '@App\Payment\StripeGateway'
Проверить, что именно резолвится в конкретный сервис, можно так:
php bin/console debug:container App\\Payment\\OrderProcessor
Команда покажет аргументы конструктора и то, что контейнер в них подставляет. Если видите не тот сервис — правьте алиас или используйте #[Autowire] прямо на параметре.
Скоуп сервисов: shared по умолчанию
Сервисы в Symfony по умолчанию shared — контейнер создаёт один инстанс и отдаёт его всем, кто просит. Обычно это правильно. Но если сервис хранит состояние внутри себя (накапливает данные между вызовами), в рамках одного запроса он может отдать не то, что ожидаете, потому что состояние живёт от прошлого использования.
Отключить шаринг можно флагом shared: false — тогда на каждую инъекцию создаётся новый инстанс. Но это дороже: контейнер инстанцирует сервис заново каждый раз. Прежде чем ставить shared: false, задайте вопрос: почему сервис вообще хранит состояние. Часто правильнее сделать сервис stateless, а состояние передавать аргументами метода.
Lazy-сервисы
Если тяжёлый сервис инжектится в конструктор, но используется редко, его создание при каждом запросе — лишняя работа. Ленивые сервисы (lazy: true) откладывают инстанцирование до первого обращения через прокси. Цена — накладные расходы на прокси и то, что ошибку в конструкторе вы получите не при сборке, а при первом вызове. Ставьте ленивость точечно, для реально тяжёлых сервисов, а не всем подряд.
Autoconfigure и теги
autoconfigure: true автоматически вешает теги на сервисы по реализуемым интерфейсам: реализация EventSubscriberInterface становится подписчиком событий без ручного тега, класс с интерфейсом консольной команды регистрируется как команда. Удобно, но добавляет неявности.
Что проверить перед прод: если сервис должен быть подписчиком/обработчиком, а не срабатывает — убедитесь, что автоконфигурация включена и класс реализует нужный интерфейс. Обратная проблема — сервис случайно реализует интерфейс и получает тег, которого вы не хотели. Посмотреть, кто на какой тег висит:
php bin/console debug:container --tag=kernel.event_subscriber
Когда что выбирать
- Оставляйте autowire и autoconfigure включёнными для основной массы сервисов — это разумный дефолт для нового кода на Symfony 6.4/7.x.
- Задавайте явный алиас интерфейса, как только у интерфейса появляется вторая реализация. Не ждите, пока автовайринг выберет за вас.
- Делайте сервис публичным только если его достают из контейнера напрямую и переписать это нельзя. В остальных случаях — приватный.
- Ставьте lazy только на тяжёлые в создании сервисы, которые нужны не в каждом запросе.
Что ломается в проде
- Кэш не прогрет при деплое — первый пользователь ловит компиляцию контейнера. Лечится добавлением
cache:clearв скрипт деплоя. - Права на
var/cache— если веб-сервер пишет туда под другим пользователем, чем деплой-скрипт, получите ошибку записи. Собирайте кэш от того же пользователя, что обслуживает запросы, или выставляйте права заранее. - Dev-бандл в prod-контейнере — если бандл зарегистрирован для всех окружений в
config/bundles.php, а нужен только в dev, он потянет свои сервисы в prod. Проверьте окружения вbundles.php. - Разошедшийся кэш опкода — при активном OPcache старый скомпилированный контейнер может остаться в памяти после деплоя. Перезапускайте PHP-FPM или сбрасывайте OPcache после прогрева кэша.
Что сделать дальше
Добавьте в деплой-скрипт проверку: после cache:clear прогоните php bin/console lint:container — команда валидирует определения сервисов и падает, если что-то не резолвится, ещё до того как приложение примет трафик. Дальше настройте перезапуск PHP-FPM после прогрева и сброс OPcache. Инвалидация кэша роутов и Doctrine-метаданных — отдельная тема, здесь мы касались только сервис-контейнера.
Разрушитель авто-разводов: Фото заказ-наряда СТО: завышения, дубли работ, лишние позиции.
Спросить за 15 ₽Источники: Symfony Documentation — The DependencyInjection Component / Service Container, Symfony Documentation — Deploying to Production
Частые вопросы
У меня Symfony 6.4, а примеры для 7.x — всё то же самое?
cache:clear теперь сам прогревает кэш или нужен отдельный cache:warmup?
Первый запрос после деплоя всё равно тормозит — что смотреть?
Автовайринг падает с ошибкой про несколько реализаций интерфейса — что делать?
Стоит ли делать все сервисы lazy для скорости?
Как узнать, что именно подставляется в конкретный сервис?
Материал носит информационный характер и подготовлен редакцией «Агентуры». Он не является офертой, рекламой или индивидуальной консультацией. Упомянутые продукты, компании и торговые знаки принадлежат их правообладателям. Перед принятием решений, влекущих юридические или финансовые последствия, обратитесь к профильному специалисту.