Разработка кастомного Bundle для Sulu

Наша компания занимается разработкой, поддержкой и обслуживанием сайтов любой сложности. От простых одностраничных сайтов до масштабных кластерных систем построенных на микро сервисах. Опыт разработчиков подтвержден сертификатами от вендоров.

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

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

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

Предлагаемые услуги
Показано 1 из 1 услугВсе 2065 услуг
Разработка кастомного Bundle для Sulu
Сложная
~1-2 недели
Часто задаваемые вопросы

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

Этапы разработки

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

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

Разработка кастомного Bundle для Sulu

Bundle в Sulu — это Symfony Bundle с дополнительными интеграциями: регистрация типов свойств, маршрутов backoffice, AdminPool-зависимостей, миграций Doctrine. Разрабатывается тогда, когда функциональность нужно переиспользовать в нескольких проектах или изолировать доменную логику.

Структура Bundle

src/
└── ReviewBundle/
    ├── Admin/
    │   └── ReviewAdmin.php         # регистрация в backoffice
    ├── 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/                     # фронтенд backoffice
    │       ├── index.js
    │       ├── views/
    │       └── containers/
    ├── ReviewBundle.php
    └── composer.json

Регистрация Bundle

// src/ReviewBundle/ReviewBundle.php
namespace App\ReviewBundle;

use Symfony\Component\HttpKernel\Bundle\Bundle;

class ReviewBundle extends Bundle
{
    public function getPath(): string
    {
        return \dirname(__DIR__);
    }
}
// config/bundles.php
return [
    // ...
    App\ReviewBundle\ReviewBundle::class => ['all' => true],
];

DependencyInjection 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 контроллер для backoffice

// 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));
    }
}

Кастомный тип свойства (Content Type)

// ContentType/ReviewListContentType.php
namespace App\ReviewBundle\ContentType;

use Sulu\Component\Content\Compat\PropertyInterface;
use Sulu\Component\Content\SimpleContentType;

class ReviewListContentType extends SimpleContentType
{
    public function __construct(private readonly ReviewRepository $repository) {}

    public function read(
        NodeInterface $node,
        PropertyInterface $property,
        string $webspaceKey,
        string $languageCode,
        string $segmentKey
    ): void {
        $value = $node->getPropertyValueWithDefault($property->getName(), null);
        $property->setValue($value);
    }

    public function getContentData(PropertyInterface $property): array
    {
        $config = $property->getValue();
        if (!$config) return [];

        return $this->repository->findPublished(
            limit: $config['limit'] ?? 5,
            rating: $config['min_rating'] ?? null
        );
    }

    public function getViewData(PropertyInterface $property): array
    {
        return $property->getValue() ?? [];
    }
}

Регистрация сервисов

<!-- Resources/config/services.xml -->
<services>
    <service id="review.admin" class="App\ReviewBundle\Admin\ReviewAdmin">
        <argument type="service" id="sulu_admin.view_builder_factory"/>
        <argument type="service" id="sulu_security.security_checker"/>
        <tag name="sulu.admin"/>
        <tag name="sulu.context" context="admin"/>
    </service>

    <service id="review.content_type.review_list"
             class="App\ReviewBundle\ContentType\ReviewListContentType">
        <argument type="service" id="review.repository"/>
        <tag name="sulu.content.type" alias="review_list"/>
    </service>
</services>

Сроки

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