Разработка кастомного Sulu Bundle: изолируем функциональность

Мы часто сталкиваемся с ситуацией: один и тот же функционал (отзывы, рейтинги, кастомные страницы) нужен на трёх-четырёх сайтах. Копировать контроллеры и шаблоны из проекта в проект — путь к хаосу. На одном из проектов — сеть интернет-магазинов — мы разработали ReviewBundle для Sulu CMS. Результат:

Разработка и обслуживание любых видов сайтов:

Информационные сайты или веб-приложения
Сайты визитки, landing page, корпоративные сайты, онлайн каталоги, квиз, промо-сайты, блоги, новостные ресурсы, информационные порталы, форумы, агрегаторы
Сайты или веб-приложения электронной коммерции
Интернет-магазины, B2B-порталы, маркетплейсы, онлайн-обменники, кэшбэк-сайты, биржи, дропшиппинг-платформы, парсеры товаров
Веб-приложения для управления бизнес-процессами
CRM-системы, ERP-системы, корпоративные порталы, системы управления производством, парсеры информации
Сайты или веб-приложения электронных услуг
Доски объявлений, онлайн-школы, онлайн-кинотеатры, конструкторы сайтов, порталы предоставления электронных услуг, видеохостинги, тематические порталы

Это лишь некоторые из технических типов сайтов, с которыми мы работаем, и каждый из них может иметь свои специфические особенности и функциональность, а также быть адаптированным под конкретные потребности и цели клиента

Услуги, которые мы предлагаем
Показано 1 из 1Все 2062 услуг
Разработка кастомного Sulu Bundle: изолируем функциональность
Сложный
~1-2 недели

Наши компетенции:

Часто задаваемые вопросы

Последние работы

  • image_website-b2b-advance_0.webp
    Разработка сайта компании B2B ADVANCE
    1414
  • image_web-applications_feedme_466_0.webp
    Разработка веб-приложения для компании FEEDME
    1285
  • image_websites_belfingroup_462_0.webp
    Разработка веб-сайта для компании БЕЛФИНГРУПП
    980
  • image_ecommerce_furnoro_435_0.webp
    Разработка интернет магазина для компании FURNORO
    1240
  • image_crm_enviok_479_0.webp
    Разработка веб-приложения для компании Enviok
    982
  • image_bitrix-bitrix-24-1c_fixper_448_0.webp
    Разработка веб-сайта для компании ФИКСПЕР
    994

Мы часто сталкиваемся с ситуацией: один и тот же функционал (отзывы, рейтинги, кастомные страницы) нужен на трёх-четырёх сайтах. Копировать контроллеры и шаблоны из проекта в проект — путь к хаосу. На одном из проектов — сеть интернет-магазинов — мы разработали ReviewBundle для Sulu CMS. Результат: обновление функционала занимает 10 минут вместо 8 часов, а баг-фиксы сократились в 3 раза. Решение — вынести логику в изолированный Symfony Bundle, который подключается через Composer. Ниже на примере ReviewBundle разберём архитектуру, процесс разработки и типичные ошибки. Если у вас есть схожая задача, получите консультацию — свяжитесь с нами.

Какие проблемы решает Bundle?

  • Дублирование кода. Одна правка в логике модерации — и нужно менять три копии. Ошибки при копировании неизбежны. Bundle устраняет дублирование: исправляете в одном месте — обновляете во всех проектах через Composer.
  • Сложность версионирования. Когда функционал живёт в монолите, нельзя легко откатить его версию. В Bundle используем Git-теги и composer.json, управляем зависимостями централизованно.
  • Отсутствие готового backoffice. Без кастомного Bundle каждую сущность приходится администрировать через прямые SQL-запросы или самописные скрипты. Мы создаём Admin-класс с навигацией, списком и формой редактирования — прямо как родные разделы Sulu.

Почему Bundle выгоднее копирования?

Изолированный Bundle даёт три ключевых преимущества. Во-первых, версионирование: вы выпускаете релизы через Composer, а не вспоминаете, в каких проектах скопировали файлы. Во-вторых, тестируемость: пишете юнит-тесты один раз и прогоняете при каждом обновлении. В-третьих, читаемость: новый разработчик видит чёткую структуру папок и знает, что всё относящееся к отзывам — в одном месте. На практике мы наблюдали сокращение времени на баг-фиксы в 2–3 раза после миграции с копий на Bundle. Экономия на поддержке может достигать 40 часов в месяц.

Аспект Копирование кода Bundle
Время на обновление 8 часов 10 минут
Риск ошибок при копировании Высокий Нет
Версионирование Отсутствует Git-теги
Тестирование Требует копирования тестов Одна кодовая база

Архитектура Bundle: ключевые компоненты

Структура папок ReviewBundle — разработка кастомного sulu

src/ └── ReviewBundle/ ├── Admin/ │ └── ReviewAdmin.php ├── Controller/ │ ├── Admin/ │ │ └── ReviewController.php │ └── Website/ │ └── ReviewWidgetController.php ├── DependencyInjection/ │ ├── ReviewExtension.php │ └── Configuration.php ├── Document/ ├── Entity/ │ └── Review.php ├── Repository/ │ └── ReviewRepository.php ├── Resources/ │ ├── config/ │ │ ├── doctrine/ │ │ │ └── Review.orm.xml │ │ ├── routes_admin.yaml │ │ └── services.xml │ └── js/ │ ├── index.js │ ├── views/ │ └── containers/ ├── ReviewBundle.php └── composer.json 

Extension и конфигурация

// DependencyInjection/ReviewExtension.php namespace App\ReviewBundle\DependencyInjection; use Symfony\Component\Config\FileLocator; use Symfony\Component\DependencyInjection\ContainerBuilder; use Symfony\Component\DependencyInjection\Loader\XmlFileLoader; use Symfony\Component\HttpKernel\DependencyInjection\Extension; class ReviewExtension extends Extension { public function load(array $configs, ContainerBuilder $container): void { $configuration = new Configuration(); $config = $this->processConfiguration($configuration, $configs); $container->setParameter('review.per_page', $config['per_page']); $container->setParameter('review.moderation', $config['moderation']); $loader = new XmlFileLoader( $container, new FileLocator(__DIR__ . '/../Resources/config') ); $loader->load('services.xml'); } } 

Admin-класс и backoffice

// Admin/ReviewAdmin.php namespace App\ReviewBundle\Admin; use Sulu\Bundle\AdminBundle\Admin\Admin; use Sulu\Bundle\AdminBundle\Admin\Navigation\NavigationItem; use Sulu\Bundle\AdminBundle\Admin\Navigation\NavigationItemCollection; use Sulu\Bundle\AdminBundle\Admin\View\ToolbarAction; use Sulu\Bundle\AdminBundle\Admin\View\ViewBuilderFactoryInterface; use Sulu\Bundle\AdminBundle\Admin\View\ViewCollection; use Sulu\Component\Security\Authorization\PermissionTypes; use Sulu\Component\Security\Authorization\SecurityCheckerInterface; class ReviewAdmin extends Admin { const REVIEW_LIST_VIEW = 'review.list'; const REVIEW_EDIT_VIEW = 'review.edit_form'; const SECURITY_CONTEXT = 'sulu.review.reviews'; public function __construct( private readonly ViewBuilderFactoryInterface $viewBuilderFactory, private readonly SecurityCheckerInterface $securityChecker ) {} public function configureNavigationItems(NavigationItemCollection $collection): void { if (!$this->securityChecker->hasPermission(self::SECURITY_CONTEXT, PermissionTypes::VIEW)) { return; } $item = new NavigationItem('review.reviews'); $item->setPosition(40); $item->setView(self::REVIEW_LIST_VIEW); $item->setIcon('su-star'); $collection->add($item); } public function configureViews(ViewCollection $collection): void { $listView = $this->viewBuilderFactory ->createListViewBuilder(self::REVIEW_LIST_VIEW, '/reviews') ->setResourceKey('reviews') ->setListKey('reviews') ->setTitle('review.reviews') ->addListAdapters(['table']) ->setEditView(self::REVIEW_EDIT_VIEW) ->addToolbarActions([ new ToolbarAction('sulu_admin.add'), new ToolbarAction('sulu_admin.delete'), ]); $editView = $this->viewBuilderFactory ->createResourceTabViewBuilder(self::REVIEW_EDIT_VIEW, '/reviews/:id') ->setResourceKey('reviews') ->setBackView(self::REVIEW_LIST_VIEW); $collection->add($listView); $collection->add($editView); } public function getSecurityContexts(): array { return [ self::SECURITY_CONTEXT => [ PermissionTypes::VIEW, PermissionTypes::ADD, PermissionTypes::EDIT, PermissionTypes::DELETE, ], ]; } } 

REST API

// Controller/Admin/ReviewController.php namespace App\ReviewBundle\Controller\Admin; use App\ReviewBundle\Repository\ReviewRepository; use FOS\RestBundle\Controller\AbstractFOSRestController; use FOS\RestBundle\View\ViewHandlerInterface; use Sulu\Component\Rest\ListBuilder\Doctrine\DoctrineListBuilderFactoryInterface; use Sulu\Component\Rest\ListBuilder\Metadata\FieldDescriptorFactoryInterface; use Sulu\Component\Rest\RestHelperInterface; use Symfony\Component\HttpFoundation\Request; use Symfony\Component\HttpFoundation\Response; use Symfony\Component\Routing\Annotation\Route; class ReviewController extends AbstractFOSRestController { public function __construct( ViewHandlerInterface $viewHandler, private readonly ReviewRepository $repository, private readonly RestHelperInterface $restHelper, private readonly FieldDescriptorFactoryInterface $fieldDescriptorFactory, private readonly DoctrineListBuilderFactoryInterface $listBuilderFactory ) { parent::__construct($viewHandler); } #[Route('/api/reviews', methods: ['GET'])] public function cgetAction(Request $request): Response { $fieldDescriptors = $this->fieldDescriptorFactory->getFieldDescriptors('reviews'); $listBuilder = $this->listBuilderFactory->create(Review::class); $this->restHelper->initializeListBuilder($listBuilder, $fieldDescriptors); $list = new ListRepresentation( $listBuilder->execute(), 'reviews', 'review_api_review_cget', $request->query->all(), $listBuilder->getCurrentPage(), $listBuilder->getLimit(), $listBuilder->count() ); return $this->handleView($this->view($list)); } #[Route('/api/reviews', methods: ['POST'])] public function postAction(Request $request): Response { $data = $request->toArray(); $review = $this->repository->createFromArray($data); $this->repository->save($review, true); return $this->handleView($this->view($review, 201)); } #[Route('/api/reviews/{id}', methods: ['DELETE'])] public function deleteAction(int $id): Response { $this->repository->removeById($id); return $this->handleView($this->view(null, 204)); } } 

Что входит в разработку?

  • Полный анализ требований и документ с описанием сущностей, API и связей.
  • Создание структуры Bundle, регистрация Admin-классов, типов свойств, REST API.
  • Doctrine-миграции и конфигурация Symfony (services.xml, routes).
  • Кастомные backoffice view (React/Preact) при необходимости.
  • Комплексное тестирование: юнит-тесты, функциональные тесты, проверка интеграции с Sulu.
  • Документация: README, руководство по обновлению, описание конфигурации.
  • Гарантийная поддержка 2 недели после сдачи.

Процесс работы и сроки

  1. Анализ и прототип — определяем сущности, поля, связи, API-эндпоинты. Создаём заготовку Bundle. Срок: 1–2 дня.
  2. Проектирование — проектируем Admin-классы, ContentType, миграции. Согласовываем с вами UX backoffice. Срок: 1 день.
  3. Разработка — реализуем все компоненты, пишем код и тесты. Срок: 3–10 дней.
  4. Интеграция — установка и настройка в вашем проекте. Срок: 1 день.
  5. Документация — README, инструкция по обновлению. Срок: 0.5 дня.
Этап Результат Примерный срок
Анализ и прототип Документ с описанием сущностей и API 1–2 дня
Проектирование Схема Bundle, шаблоны Admin-views 1 день
Разработка Полный код Bundle с тестами 3–10 дней
Интеграция Установка и настройка в вашем проекте 1 день
Документация README, инструкция по обновлению 0.5 дня
Гарантийная поддержка 2 недели после сдачи

Какие ошибки чаще всего допускают при разработке Bundle?

  • Забыли зарегистрировать сервис в services.xml — без тега sulu.admin Admin-класс не появится в навигации.
  • Неправильный namespace — Symfony не найдёт класс, если namespace не совпадает с путём.
  • Не подключили миграции Doctrine — сущности не создадутся в БД. Добавьте Bundle в config/packages/doctrine.yaml.
  • Не настроили права доступа — пользователи не увидят раздел. Реализуйте getSecurityContexts() как в примере выше.

Сроки и стоимость

Базовый Bundle с Doctrine-сущностью, REST API и регистрацией в backoffice (без кастомного фронтенда) — 5–7 дней. С кастомным фронтендом backoffice (React/Preact компоненты), миграциями и кастомным типом свойства — 2–3 недели. Точную оценку даём после анализа вашей задачи. Оставьте заявку на сайте, и мы свяжемся с вами в течение дня.

Мы разрабатываем кастомные Sulu Bundle более 5 лет, за плечами 30+ успешных проектов. Гарантируем стабильность интеграции и соблюдение код-стайла Symfony. Получите консультацию по вашему проекту — свяжитесь с нами.