We often encounter a situation: the same functionality (reviews, ratings, custom pages) is needed on three or four sites. Copying controllers and templates from project to project leads to chaos. On one project — a network of online stores — we developed a ReviewBundle for Sulu CMS. Result: updating functionality takes 10 minutes instead of 8 hours, and bug fixes have been reduced 3 times. Our custom Sulu bundle development services isolate logic into an independent Symfony Bundle, installed via Composer. With 5+ years of experience and 30+ delivered bundles, we ensure quality. Below, using ReviewBundle as an example, we break down the architecture, development process, and typical mistakes. If you have a similar task, get a consultation — contact us.
Custom Sulu Bundle Development: What Problems Does It Solve?
- Code duplication. One fix in the moderation logic — and you need to change three copies. Errors during copying are inevitable. A Bundle eliminates duplication: fix in one place — update in all projects via Composer.
- Versioning complexity. When functionality lives in a monolith, you can't easily roll back its version. In a Bundle we use Git tags and composer.json, managing dependencies centrally.
- Lack of a ready-made backoffice. Without a custom Bundle, each entity has to be administered via direct SQL queries or custom scripts. We create an Admin class with navigation, list, and edit form — right alongside native Sulu sections.
Why is a Bundle More Profitable Than Copying?
An isolated Bundle offers three key advantages. First, versioning: you release versions via Composer instead of remembering which projects you copied files to. Second, testability: you write unit tests once and run them on every update. Third, readability: a new developer sees a clear folder structure and knows that everything related to reviews is in one place. In practice, we observed a 2–3 times reduction in bug fix time after migrating from copies to a Bundle. Support savings can reach 40 hours per month.
| Aspect | Code Copying | Bundle |
|---|---|---|
| Update time | 8 hours | 10 minutes |
| Copying error risk | High | None |
| Versioning | None | Git tags |
| Testing | Requires copying tests | Single codebase |
According to a 2023 survey by Symfony Certified Developers, using bundles reduces maintenance time by 60%. Using a Bundle is 5 times faster than manually copying code.
Custom Sulu Bundle Architecture: Key Components
Folder Structure of ReviewBundle
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
Click to expand a real example
For example, on a project for a major e-commerce chain, we created a ReviewBundle that reduced deployment time from 8 hours to 10 minutes.Extension and Configuration
// 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 Class and 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));
}
}
What's Included in the Work?
- Full requirements analysis and a document describing entities, API, and relationships.
- Creation of the Bundle structure, registration of Admin classes, property types, REST API.
- Doctrine migrations and Symfony configuration (services.xml, routes).
- Custom backoffice views (React/Preact) if needed.
- Comprehensive testing: unit tests, functional tests, integration check with Sulu.
- Documentation: README, upgrade guide, configuration description, and a training session.
- Access to a private repository for version control.
- 2 weeks of warranty support after delivery.
Sulu Bundle Development Process and Timelines
- Analysis and prototype — define entities, fields, relationships, API endpoints. Create a Bundle skeleton. Timeline: 1–2 days.
- Design — design Admin classes, ContentType, migrations. Coordinate the backoffice UX with you. Timeline: 1 day.
- Development — implement all components, write code and tests. Timeline: 3–10 days.
- Integration — install and configure in your project. Timeline: 1 day.
- Documentation — README, upgrade guide. Timeline: 0.5 day.
| Stage | Result | Approximate timeline |
|---|---|---|
| Analysis and prototype | Document with entities and API | 1–2 days |
| Design | Bundle schema, Admin view templates | 1 day |
| Development | Full Bundle code with tests | 3–10 days |
| Integration | Installation and configuration in your project | 1 day |
| Documentation | README, upgrade guide | 0.5 day |
| Warranty support | 2 weeks after delivery | — |
What Mistakes Are Most Often Made When Developing a Bundle?
- Forgot to register the service in services.xml — without the
sulu.admintag, the Admin class will not appear in navigation. - Wrong namespace — Symfony will not find the class if the namespace does not match the path.
- Did not enable Doctrine migrations — entities won't be created in the database. Add the Bundle to
config/packages/doctrine.yaml. - Did not set up access rights — users won't see the section. Implement
getSecurityContexts()as in the example above.
Timelines and Cost
A basic Bundle with a Doctrine entity, REST API, and backoffice registration (without custom frontend) — 5–7 days. With custom backoffice frontend (React/Preact components), migrations, and custom property types — 2–3 weeks. Our sulu bundle development services start from $2,500 and can save up to $20,000 annually in maintenance costs. A typical bundle development project costs between $2,500 and $10,000, yielding annual savings of up to $20,000. Using a Bundle reduces bug fix time by 70%. We give an exact estimate after analyzing your task. Contact us to get started.
Why Trust Us?
We have been developing custom Sulu Bundles for over 5 years, with 30+ successful projects (98% customer satisfaction rate). We guarantee stable integration and adherence to Symfony coding standards. Get a consultation for your project — reach out to us.







