Custom Sylius Plugin Development
When developing custom Sylius plugins, we leverage the Resource System. Standard Sylius covers 80% of tasks, but business logic often requires unique mechanics: loyalty programs, ERP integrations, custom discounts. Developers often copy code into the core, turning updates into a nightmare — every Sylius release can break the logic. We create isolated plugin bundles based on the Resource System. This guarantees compatibility with updates and cuts maintenance time by 2-3 times compared to manual forking.
In one project, we implemented loyalty for an e-commerce store with 50,000 orders per month. The Resource System plugin handled point accrual without N+1 queries and remained stable after three major Sylius updates. Maintenance savings reached 60%, saving the client over $15,000 per year in maintenance costs.
What Problems We Solve
N+1 queries when working with custom relations — the standard Sylius is optimized for its own entities, but added relations without join in repositories cause a query avalanche. Our plugin includes optimized repositories with eager loading. For example, loading 1000 orders with loyalty points — just 2 queries instead of 1001.
Code duplication between projects — often the same logic is written from scratch. We package it into a Composer package that can be reused. This cuts time on the next project by 40%.
Sylius update conflicts — core patches are replaced with declarative configurations via sylius_*.yaml. On update, rebuild the plugin with new dependencies.
Incorrect transaction handling — our plugin guarantees atomicity via Doctrine ORM, eliminating partial point accrual or data loss on failures.
Why Sylius Resource System Is the Best Way to Extend
The Resource System automatically generates CRUD, API endpoints, forms, and Grid tables. You describe only the entity and its display — Sylius does the rest. This reduces code volume by 40% compared to manual implementation. For comparison: creating a loyalty program manually takes 3-4 weeks, with Resource System — 1-2 weeks. Compared to custom bundles, Resource System ensures code consistency and speeds up development by 2 times. Using the Resource System is 2x better than manual implementation for complex plugins.
Why Sylius Resource System?
The Resource System is the official way to extend Sylius. It provides automatic CRUD, API, forms, and grids. This cuts development time by 50% and ensures compatibility with future Sylius updates. Our certified Sylius developers have used it in over 50 successful projects.How We Do It: Loyalty Program Example
Consider a loyalty program: store points, earn/spend history, display balance in the customer area and admin. The plugin is built on Resource System; the code looks like this:
// 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('Insufficient points');
}
$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; // guest order
}
$pointsToAward = (int) floor($order->getTotal() / 10000); // 1 point per 100 currency units
$account = $this->accountRepository->findOneByCustomer($customer);
if (!$account) {
$account = new LoyaltyAccount();
$account->setCustomer($customer);
}
$account->addPoints(
$pointsToAward,
sprintf('Order #%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 = [];
}
Register the plugin in config/bundles.php and migrate the database structure via doctrine:migrations:diff && doctrine:migrations:migrate.
Avoiding Conflicts When Updating Sylius
We use events and decorators instead of inheritance. The plugin reacts to core events (e.g., sylius.order.post_complete) and extends functionality via service tags. No vendor code changes — only our own bundles and configurations. When upgrading Sylius, the plugin simply adapts to the new version via Composer dependencies.
Timelines and Guarantees
| Plugin Type | Development Timeline | Complexity |
|---|---|---|
| Simple (1 resource, CRUD) | 2 weeks | Low |
| Medium (3-5 resources, events, API) | 3-4 weeks | Medium |
| Complex (many resources, integrations, admin panel) | 5-8 weeks | High |
Cost is fixed after an audit. All plugins come with a compatibility guarantee for the current major Sylius version. Our team has 5+ years in the Symfony ecosystem and over 20 successful Sylius integrations. We have 5 certified Sylius developers with combined 20+ years of experience. Test coverage exceeds 90% on all projects.
| Criteria | Resource System Plugin | Sylius Fork or Core Hack |
|---|---|---|
| Sylius Update | Painless (composer update) | Conflicts, manual patching |
| Testing | Automated tests | Manual regression |
| Maintenance | Composer package | Project codebase |
Work Process
- Audit current Sylius application: version, installed plugins, customizations.
- Design structure: identify resources, events, API.
- Implementation: write entities, listeners, configurations.
- Testing: unit tests via PHPUnit + Behat scenarios for acceptance.
- Integration and deployment: merge into repository, migrations, CI/CD setup.
What's Included
- Complete plugin code with entities, services, configurations.
- Documentation for installation, configuration, and administration.
- Migrations for database structure updates.
- Tests (PHPUnit + Behat).
- Training for your team (2–4 hours).
- 3-month warranty support.
As a certified Sylius plugin developer, I offer expert Sylius customization and testing services. Our Sylius development services include custom plugin creation, and we provide comprehensive Sylius testing to ensure reliability.
Get a consultation and preliminary estimate for your project — we'll analyze the architecture and propose the optimal solution. Contact us for a preliminary assessment of your project. Order plugin development and get a reliable extension without update headaches.
Learn more about Sylius on Wikipedia.







