Бандлы в Symfony: рабочие приёмы для переиспользуемого кода
Когда выделять функциональность в отдельный бандл, как правильно объявлять сервисы и конфигурацию, а где хватит обычной папки в src. Разбор на примере Symfony 6.4 и 7.x.

С Symfony 4 бандл перестал быть способом организации кода внутри приложения — теперь это единица переиспользования между проектами. Ваш код приложения живёт в src/ без всяких бандлов, а бандл нужен, только если вы собираетесь ставить эту функциональность через Composer в несколько приложений. Путаница между «структурой приложения» и «переиспользуемым пакетом» — самая частая ошибка при переходе с Symfony 2–3, и она порождает бандлы, которые никому, кроме одного проекта, не нужны.
Разберём, когда бандл оправдан, как объявлять в нём сервисы и конфигурацию так, чтобы приложение-потребитель могло всё переопределить, и где вы платите за гибкость лишней сложностью. Примеры — для Symfony 6.4 LTS и 7.x, где структура бандла упростилась через AbstractBundle.
Когда бандл нужен, а когда — нет
Простой критерий: бандл имеет смысл, если код будет установлен минимум в два приложения через composer require. Всё остальное — папка в src/ вашего приложения.
- Нужен бандл: библиотека для внутреннего переиспользования между микросервисами компании, интеграция с внешним API, которую вы выкладываете на Packagist, набор команд и сервисов для нескольких клиентских проектов.
- Не нужен бандл: модуль «Заказы» внутри одного монолита, слой работы с платёжным шлюзом только этого проекта, группировка контроллеров по разделам админки. Это просто пространства имён внутри
App\.
Если вы создаёте бандл, чтобы «навести порядок» в одном приложении, — вы наводите беспорядок. Порядок внутри приложения наводится структурой каталогов и неймспейсами, а не искусственной границей пакета.
Что вы платите за бандл
Бандл — это отдельный composer.json, отдельный репозиторий или сабмодуль, свой цикл релизов и семантическое версионирование. Каждое изменение контракта — потенциально ломающее для потребителей. Пока код живёт в одном приложении, вы правите его свободно; как только он стал бандлом, у вас появляется API, который нельзя ломать без bump мажорной версии.
Минимальный бандл на Symfony 7.x
С Symfony 6.1 появился класс AbstractBundle, который убирает большую часть церемоний: не нужны отдельные классы Extension и Configuration для простых случаев. Минимальный бандл выглядит так.
<?php
// src/AcmePaymentBundle.php
namespace Acme\PaymentBundle;
use Symfony\Component\DependencyInjection\ContainerBuilder;
use Symfony\Component\DependencyInjection\Loader\Configurator\ContainerConfigurator;
use Symfony\Component\HttpKernel\Bundle\AbstractBundle;
class AcmePaymentBundle extends AbstractBundle
{
// Загрузка сервисов бандла из его собственного services.php
public function loadExtension(
array $config,
ContainerConfigurator $container,
ContainerBuilder $builder
): void {
$container->import('../config/services.php');
// Прокидываем пользовательскую конфигурацию в параметр контейнера
$container->services()
->get(\Acme\PaymentBundle\Client\PaymentClient::class)
->arg('$timeout', $config['timeout']);
}
}Файл composer.json бандла должен объявлять автозагрузку и тип пакета:
{
"name": "acme/payment-bundle",
"type": "symfony-bundle",
"require": {
"php": ">=8.2",
"symfony/framework-bundle": "^6.4|^7.0"
},
"autoload": {
"psr-4": { "Acme\\PaymentBundle\\": "src/" }
}
}Сервисы бандла лучше объявлять приватными и не автоконфигурировать чужой код бандл-широко — иначе потребитель получит сюрпризы в своём контейнере.
Конфигурация: дайте потребителю переопределять
Главная ценность бандла — то, что приложение может его настроить, не трогая исходники. В AbstractBundle схему конфигурации описывают прямо в методе configure() через DefinitionConfigurator.
use Symfony\Component\Config\Definition\Configurator\DefinitionConfigurator;
public function configure(DefinitionConfigurator $definition): void
{
$definition->rootNode()
->children()
->integerNode('timeout')
->defaultValue(30)
->end()
->scalarNode('base_url')
->isRequired()
->cannotBeEmpty()
->end()
->end();
}После этого потребитель настраивает бандл через свой config/packages/acme_payment.yaml:
acme_payment:
base_url: 'https://api.provider.example'
timeout: 15Обратите внимание: имя корневого узла (acme_payment) выводится из имени класса бандла автоматически. Это удобно, но легко забыть и искать конфигурацию под другим ключом.
Где лежат сервисы — services.php против services.yaml
Внутри бандла предпочтителен PHP-конфигуратор сервисов, а не YAML: он даёт автодополнение в IDE и ловит опечатки в именах классов на этапе анализа, а не в рантайме. Для приложения-потребителя формат не важен — он видит только публичный контракт бандла.
Регистрация и точки расширения
С Flex-рецептами бандл регистрируется автоматически в config/bundles.php. Если рецепта нет, потребитель добавляет строку сам:
// config/bundles.php
return [
// ...
Acme\PaymentBundle\AcmePaymentBundle::class => ['all' => true],
];Три рабочих приёма для точек расширения, которые делают бандл по-настоящему переиспользуемым:
- Интерфейсы вместо конкретных классов. Публичный сервис бандла типизируйте интерфейсом. Потребитель сможет подменить реализацию через свой
services.yaml, не форкая бандл. - Теги для сбора реализаций. Если бандл собирает набор обработчиков (стратегии оплаты, форматтеры), используйте
CompilerPassс тегами или атрибут#[AutoconfigureTag]— приложение добавит свою реализацию, повесив тег. - События вместо хардкода. Вместо того чтобы зашивать поведение, диспетчеризируйте события Symfony EventDispatcher — потребитель подпишется своим слушателем.
Сравнение: бандл, папка в src и отдельный пакет-библиотека
| Критерий | Папка в src/ | Бандл | Библиотека без бандла |
|---|---|---|---|
| Переиспользование между проектами | Нет | Да | Да |
| Интеграция с контейнером Symfony | Полная | Полная | Нет (только автозагрузка классов) |
| Свой цикл релизов | Нет | Да | Да |
| Конфигурируется через config/packages | Нет | Да | Нет |
| Когда выбирать | Код одного приложения | Symfony-специфичная функциональность для нескольких приложений | Логика без привязки к фреймворку |
Третий вариант часто недооценивают: если код не зависит от контейнера Symfony (чистая доменная логика, обёртка над HTTP-клиентом), делайте обычную PHP-библиотеку. Она будет пригодна и в Laravel, и в консольном скрипте, а бандл-обёртку для интеграции с Symfony при необходимости выделяют отдельным тонким пакетом.
Что ломается на практике
- Приватные сервисы, к которым лезут напрямую. С Symfony 4 сервисы по умолчанию приватны. Если потребитель пытается достать сервис бандла через
$container->get(), он получит ошибку. Публичным делайте только то, что действительно часть контракта. - Жёсткие пути к шаблонам и ресурсам. Ссылайтесь на шаблоны бандла через нотацию
@AcmePayment/..., а не через относительные пути — иначе после установки в другое приложение шаблоны не найдутся. - Конфликт имён параметров контейнера. Параметры бандла префиксуйте именем бандла (
acme_payment.timeout), чтобы не затирать чужие. - Забытая обратная совместимость. Смена сигнатуры публичного сервиса или ключа конфигурации ломает потребителей. Помечайте устаревшее через
trigger_deprecation()и удаляйте только в мажорной версии.
Что делать дальше
Начните с обычной папки в src/ и вынесите код в бандл только когда появится второй проект-потребитель — раньше это преждевременная абстракция. Когда дойдёте до выделения: опишите configure(), сделайте один публичный интерфейс как точку входа, напишите Flex-рецепт для автоматической регистрации. Проверяйте обратную совместимость через инструмент проверки BC break между релизами и мерьте, сколько ваших изменений оказываются ломающими — если много, значит контракт бандла спроектирован слишком широко.
Универсальный ассистент: Обычный чат: тексты, идеи, разбор, перевод, расчёты.
Спросить за 8 ₽Источники: Symfony Documentation — The Bundle System, Symfony Documentation — Best Practices for Reusable Bundles
Частые вопросы
У меня Symfony 5.4, а в статье AbstractBundle. Как быть?
Обязательно ли делать Flex-рецепт для своего бандла?
Можно ли внутри одного монолита разбить код на бандлы для порядка?
Не получается достать сервис бандла — ошибка про приватный сервис. Что делать?
Как не сломать приложения-потребители при обновлении бандла?
Что лучше для интеграции с внешним API — бандл или обычная библиотека?
Материал носит информационный характер и подготовлен редакцией «Агентуры». Он не является офертой, рекламой или индивидуальной консультацией. Упомянутые продукты, компании и торговые знаки принадлежат их правообладателям. Перед принятием решений, влекущих юридические или финансовые последствия, обратитесь к профильному специалисту.