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

Коли один і той самий функціонал потрібен на кількох сайтах, копіювання коду з проєкту в проєкт перетворюється на хаос. Ми розробляємо кастомні Sulu Bundle, які ізолюють логіку в окремий Symfony-компонент і підключаються через Composer. Наша команда реалізує проєкт під ключ — від архітектури до підтримки, забезпечуючи надійне розширення Sulu CMS без дублювання та з легким оновленням.

Розробка та обслуговування будь-яких видів сайтів:

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

Це лише деякі з технічних типів сайтів, з якими ми працюємо, і кожен із них може мати свої специфічні особливості та функціональність, а також бути адаптованим під конкретні потреби та цілі клієнта.

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

Ми часто стикаємося з ситуацією: один і той самий функціонал (відгуки, рейтинги, кастомні сторінки) потрібен на трьох-чотирьох сайтах. Копіювати контролери та шаблони з проєкту в проєкт — шлях до хаосу. На одному з проєктів — мережа інтернет-магазинів — ми розробили 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. Отримайте консультацію по вашому проєкту — зв'яжіться з нами.

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

Останні роботи

  • Розробка сайту компанії B2B ADVANCE
    Розробка сайту компанії B2B ADVANCE
    1542
  • Розробка веб-додатків для компанії FEEDME
    Розробка веб-додатків для компанії FEEDME
    1394
  • Розробка веб-сайту для компанії БЕЛФІНГРУП
    Розробка веб-сайту для компанії БЕЛФІНГРУП
    1081
  • Розробка інтернет магазину для компанії FURNORO
    Розробка інтернет магазину для компанії FURNORO
    1369
  • Розробка веб-додатків для компанії Enviok
    Розробка веб-додатків для компанії Enviok
    1141
  • Розробка веб-сайту для компанії ФІКСПЕР
    Розробка веб-сайту для компанії ФІКСПЕР
    1060