Разработка кастомного плагина Sylius
При разработке кастомного плагина Sylius мы используем Resource System. Стандартный Sylius покрывает 80% задач, но бизнес-логика часто требует уникальных механик: программа лояльности, интеграция с ERP, кастомные скидки. Разработчики часто копируют код в ядро, что превращает обновление в кошмар — каждый релиз Sylius может сломать логику. Мы создаём изолированные плагины-базлы на основе Resource System. Это гарантирует совместимость с обновлениями и сокращает время на поддержку в 2-3 раза по сравнению с ручным форком.
В одном проекте мы внедряли лояльность для интернет-магазина с 50 000 заказов в месяц. Плагин на Resource System обрабатывал начисления баллов без N+1 запросов и работал стабильно после трёх мажорных обновлений Sylius. Экономия на поддержке составила 60%.
Какие проблемы решаем
N+1 запросы при работе с кастомными связями — стандарт Sylius оптимизирован под свои сущности, но добавленные отношения без join в репозиториях приводят к лавине запросов. Плагин включает оптимизированные репозитории с eager loading. Например, при загрузке 1000 заказов с баллами лояльности — всего 2 запроса вместо 1001.
Дублирование кода между проектами — часто одну и ту же логику пишут заново. Мы упаковываем её в Composer-пакет, который можно переиспользовать. Это сокращает время на следующий проект на 40%.
Конфликты при обновлении Sylius — патчи ядра заменяются декларативными конфигурациями через sylius_*.yaml. При обновлении достаточно пересобрать плагин с новыми зависимостями.
Некорректная работа с транзакциями — плагин гарантирует атомарность операций через Doctrine ORM, что исключает частичное начисление баллов или потерю данных при сбоях.
Почему Sylius Resource System — лучший способ расширения?
Resource System автоматически генерирует CRUD, API-эндпоинты, формы и Grid-таблицы. Вы описываете только сущность и её отображение — остальное Sylius делает за вас. Это сокращает объём кода на 40% по сравнению с ручной реализацией. Для сравнения: создание программы лояльности вручную занимает 3-4 недели, с Resource System — 1-2 недели. По сравнению с кастомными Bundle, Resource System обеспечивает единообразие кода и ускоряет разработку в 2 раза.
Как мы это делаем: пример программы лояльности
Рассмотрим программу лояльности: нужно хранить баллы, историю начислений и списаний, отображать баланс в личном кабинете и админке. Плагин строится на Resource System, а код выглядит так:
// src/SyliusLoyaltyPlugin/SyliusLoyaltyPlugin.php
namespace Acme\SyliusLoyaltyPlugin;
use Sylius\Bundle\CoreBundle\Application\SyliusPluginTrait;
use Symfony\Component\HttpKernel\Bundle\Bundle;
final class SyliusLoyaltyPlugin extends Bundle
{
use SyliusPluginTrait;
}
// src/SyliusLoyaltyPlugin/Entity/LoyaltyAccount.php
namespace Acme\SyliusLoyaltyPlugin\Entity;
use Doctrine\ORM\Mapping as ORM;
use Sylius\Component\Customer\Model\CustomerInterface;
#[ORM\Entity(repositoryClass: LoyaltyAccountRepository::class)]
#[ORM\Table(name: 'acme_loyalty_account')]
class LoyaltyAccount
{
#[ORM\Id]
#[ORM\GeneratedValue]
#[ORM\Column(type: 'integer')]
private ?int $id = null;
#[ORM\OneToOne(targetEntity: CustomerInterface::class)]
#[ORM\JoinColumn(nullable: false, onDelete: 'CASCADE')]
private CustomerInterface $customer;
#[ORM\Column(type: 'integer', options: ['default' => 0])]
private int $points = 0;
#[ORM\Column(type: 'json')]
private array $transactions = [];
#[ORM\Column(type: 'datetime_immutable')]
private \DateTimeImmutable $createdAt;
public function __construct()
{
$this->createdAt = new \DateTimeImmutable();
}
public function addPoints(int $points, string $reason, ?string $orderId = null): void
{
$this->points += $points;
$this->transactions[] = [
'type' => 'earn',
'points' => $points,
'reason' => $reason,
'order_id' => $orderId,
'date' => (new \DateTimeImmutable())->format(\DateTimeInterface::ATOM),
];
}
public function spendPoints(int $points, string $reason): void
{
if ($this->points < $points) {
throw new \DomainException('Недостаточно баллов');
}
$this->points -= $points;
$this->transactions[] = [
'type' => 'spend',
'points' => $points,
'reason' => $reason,
'date' => (new \DateTimeImmutable())->format(\DateTimeInterface::ATOM),
];
}
public function getId(): ?int { return $this->id; }
public function getPoints(): int { return $this->points; }
public function getTransactions(): array { return $this->transactions; }
}
// src/SyliusLoyaltyPlugin/EventListener/OrderPlacedListener.php
namespace Acme\SyliusLoyaltyPlugin\EventListener;
use Acme\SyliusLoyaltyPlugin\Repository\LoyaltyAccountRepository;
use Doctrine\ORM\EntityManagerInterface;
use Sylius\Bundle\ResourceBundle\Event\ResourceControllerEvent;
use Sylius\Component\Core\Model\OrderInterface;
final class OrderPlacedListener
{
public function __construct(
private LoyaltyAccountRepository $accountRepository,
private EntityManagerInterface $em,
) {}
public function onOrderComplete(ResourceControllerEvent $event): void
{
/** @var OrderInterface $order */
$order = $event->getSubject();
$customer = $order->getCustomer();
if (!$customer) {
return; // гостевой заказ
}
$pointsToAward = (int) floor($order->getTotal() / 10000); // 1 балл = 100 руб
$account = $this->accountRepository->findOneByCustomer($customer);
if (!$account) {
$account = new LoyaltyAccount();
$account->setCustomer($customer);
}
$account->addPoints(
$pointsToAward,
sprintf('Заказ #%s', $order->getNumber()),
$order->getId()
);
$this->em->persist($account);
$this->em->flush();
}
}
<!-- src/SyliusLoyaltyPlugin/Resources/config/services.xml -->
<service id="acme.loyalty.event_listener.order_placed"
class="Acme\SyliusLoyaltyPlugin\EventListener\OrderPlacedListener">
<argument type="service" id="acme.loyalty.repository.loyalty_account"/>
<argument type="service" id="doctrine.orm.entity_manager"/>
<tag name="kernel.event_listener"
event="sylius.order.post_complete"
method="onOrderComplete"/>
</service>
<service id="acme.loyalty.menu.admin_menu_listener"
class="Acme\SyliusLoyaltyPlugin\Menu\AdminMenuListener">
<tag name="kernel.event_listener"
event="sylius.menu.admin.main"
method="addAdminMenuItems"/>
</service>
// src/SyliusLoyaltyPlugin/Api/Resource/LoyaltyAccountResource.php
namespace Acme\SyliusLoyaltyPlugin\Api\Resource;
use ApiPlatform\Metadata\ApiResource;
use ApiPlatform\Metadata\Get;
use Acme\SyliusLoyaltyPlugin\Api\Provider\LoyaltyAccountProvider;
#[ApiResource(
shortName: 'LoyaltyAccount',
operations: [
new Get(
uriTemplate: '/shop/loyalty-account',
provider: LoyaltyAccountProvider::class,
),
],
normalizationContext: ['groups' => ['loyalty:read']],
)]
final class LoyaltyAccountResource
{
public int $points = 0;
public array $transactions = [];
}
Регистрация плагина в config/bundles.php и миграция структуры БД через doctrine:migrations:diff && doctrine:migrations:migrate.
Как избежать конфликтов при обновлении Sylius?
Мы используем события и декораторы вместо наследования. Плагин реагирует на события ядра (например, sylius.order.post_complete) и расширяет функциональность через сервисные теги. Никаких изменений в vendor-коде — только собственные Bundle и конфигурации. При обновлении Sylius плагин просто адаптируется под новую версию через зависимости Composer.
Сроки и гарантии
| Тип плагина | Срок разработки | Сложность |
|---|---|---|
| Простой (1 ресурс, CRUD) | 2 недели | Низкая |
| Средний (3-5 ресурсов, события, API) | 3-4 недели | Средняя |
| Комплексный (много ресурсов, интеграции, админ-панель) | 5-8 недель | Высокая |
Стоимость фиксируется после аудита. На все плагины даём гарантию совместимости с текущей мажорной версией Sylius. Опыт команды — 5+ лет в Symfony-экосистеме, более 20 успешных интеграций с Sylius.
| Критерий | Плагин на Resource System | Fork Sylius или хак ядра |
|---|---|---|
| Обновление Sylius | Безболезненно (composer update) | Конфликты, патчи вручную |
| Тестирование | Автоматические тесты | Ручное регрессионное |
| Поддержка | Через композер-пакет | Кодовая база проекта |
Процесс работы
- Аудит текущего Sylius-приложения: версия, установленные плагины, кастомизации.
- Проектирование структуры: определение ресурсов, событий, API.
- Реализация: написание сущностей, листенеров, конфигураций.
- Тестирование: юнит-тесты на PHPUnit + Behat-сценарии для acceptance.
- Интеграция и деплой: merge в репозиторий, миграции, настройка CI/CD.
Что входит в разработку
- Полный код плагина с сущностями, сервисами, конфигурациями.
- Документация по установке, настройке и администрированию.
- Миграции для обновления структуры БД.
- Тесты (PHPUnit + Behat).
- Обучение команды заказчика (2–4 часа).
- Гарантийная поддержка 3 месяца.
Получите консультацию и предварительную оценку вашего проекта — мы проанализируем архитектуру и предложим оптимальное решение. Свяжитесь с нами для предварительной оценки вашего проекта. Закажите разработку плагина — получите надёжное расширение без головной боли с обновлениями.
Подробнее о Sylius можно прочитать в Wikipedia.







