abderrahimghazali / sylius-upsell-plugin
Post-purchase upsell and cross-sell plugin for Sylius 2.x — Frequently Bought Together, one-click add-all-to-cart
Package info
github.com/abderrahimghazali/sylius-upsell-plugin
Type:sylius-plugin
pkg:composer/abderrahimghazali/sylius-upsell-plugin
v2.1.0
2026-03-28 18:57 UTC
Requires
- php: ^8.2
- doctrine/orm: ^2.17 || ^3.0
- sylius/sylius: ^2.1
- symfony/cache: ^7.0
- symfony/framework-bundle: ^7.0
- symfony/stimulus-bundle: ^2.0
Requires (Dev)
- phpstan/phpstan: ^1.10
- phpunit/phpunit: ^10.0
This package is auto-updated.
Last update: 2026-03-28 19:09:40 UTC
README
Sylius Upsell Plugin
Post-purchase upsell and cross-sell plugin for Sylius 2.x stores — Frequently Bought Together, checkout upsell modals, and one-click add-all-to-cart.
Screenshots
Admin — Upsell Offers Grid
Admin — Upsell Offer Form
Shop — Checkout Upsell Modal
Admin — Upsell Analytics Dashboard
Features
- Frequently Bought Together section on every product page with manual or algorithmic product suggestions
- One-click add-all-to-cart for the entire FBT bundle
- Algorithmic co-purchase detection from order history with configurable thresholds
- Checkout upsell modal — intercepts "Place order" with a special offer before completing the order
- Trigger product targeting — tie offers to specific products in the cart, or use catch-all
- Discount percentage — upsell product added to cart at discounted price
- Customizable copy — headline, body, CTA label, and decline label per offer
- Date scheduling — start and end dates for time-limited offers
- Priority system — control which offer wins when multiple match
- Admin CRUD for upsell offers under Marketing, and global FBT settings under Configuration
- Drag-and-drop reordering of manual FBT relations with optional discount badges
- Analytics dashboard — impressions, acceptance rate, extra revenue, daily chart (Chart.js)
- Impression tracking — automatic for both FBT and checkout upsell (shown / accepted / declined)
Requirements
- Sylius 2.1+
- Symfony 7.0+
- PHP 8.2+
Installation
- Require the plugin:
composer require abderrahimghazali/sylius-upsell-plugin
- Register the bundle in
config/bundles.php(if not auto-discovered):
return [ // ... Abderrahim\SyliusUpsellPlugin\SyliusUpsellPlugin::class => ['all' => true], ];
- Import routes — create
config/routes/sylius_upsell.yaml:
sylius_upsell: resource: '@SyliusUpsellPlugin/config/routes.yaml'
- Generate and run the migration:
bin/console doctrine:migrations:diff bin/console doctrine:migrations:migrate
- Register the Stimulus controllers in
assets/shop/controllers.json:
{
"controllers": {
"@abderrahimghazali/sylius-upsell-plugin": {
"fbt": {
"enabled": true,
"fetch": "eager"
},
"post-purchase": {
"enabled": true,
"fetch": "eager"
}
}
}
}
- Symlink the plugin assets and rebuild:
# Create the symlink (from your project root) mkdir -p node_modules/@abderrahimghazali ln -s ../../vendor/abderrahimghazali/sylius-upsell-plugin/assets node_modules/@abderrahimghazali/sylius-upsell-plugin # Rebuild assets yarn encore dev
Entity: UpsellRelation
| Field | Type | Description |
|---|---|---|
sourceProduct |
ManyToOne (Product) | The product page where the FBT section appears |
relatedProduct |
ManyToOne (Product) | The product suggested alongside the source |
position |
int | Display order (lower = first) |
discount |
int? | Optional discount percentage for the related product |
createdAt |
datetime | Timestamp of creation |
Entity: UpsellOffer
| Field | Type | Description |
|---|---|---|
name |
string | Admin label for the offer |
enabled |
boolean | Active/inactive toggle |
triggerProduct |
ManyToOne (Product) | The purchased product that triggers this offer |
offerProduct |
ManyToOne (Product) | The product offered in the upsell modal |
offerVariant |
ManyToOne (ProductVariant)? | Optional specific variant to offer |
discountPercent |
int | Discount percentage shown in the modal |
headline |
string | Modal heading (default: "Wait! A special offer just for you") |
body |
text? | Optional body text |
ctaLabel |
string | Accept button text (default: "Yes, add it!") |
declineLabel |
string | Decline button text (default: "No thanks") |
priority |
int | Higher = shown first when multiple offers match |
startsAt |
datetime? | Optional start date |
endsAt |
datetime? | Optional end date |
Entity: UpsellConfiguration
| Field | Type | Description |
|---|---|---|
enabled |
boolean | Global FBT feature toggle |
minCoPurchaseThreshold |
int | Minimum co-purchase count for algorithmic suggestions (default: 3) |
maxProductsShown |
int | Maximum products in the FBT section (default: 4) |
sectionTitle |
string | Heading text (default: "Frequently bought together") |
showDiscountBadge |
boolean | Show/hide discount badges on FBT products |
fallbackStrategy |
string | algorithmic, manual_only, or disabled |
Architecture
src/
├── Controller/
│ ├── Admin/
│ │ ├── UpsellConfigurationController.php # Global FBT settings
│ │ └── UpsellOfferController.php # CRUD for upsell offers
│ └── Shop/
│ ├── ImpressionController.php # Impression tracking API
│ └── PostPurchaseController.php # Accept/decline offer API
├── DependencyInjection/
│ ├── Configuration.php
│ └── SyliusUpsellExtension.php # Prepends resources, grids, hooks
├── Entity/
│ ├── UpsellConfiguration.php
│ ├── UpsellImpression.php
│ ├── UpsellOffer.php
│ ├── UpsellOfferInterface.php
│ ├── UpsellRelation.php
│ └── UpsellRelationInterface.php
├── EventListener/
│ ├── AdminMenuListener.php # Marketing menu items
│ └── ProductFormListener.php # FBT tab on product form
├── Form/Type/
│ ├── ProductUpsellType.php # Product FBT relations form
│ ├── UpsellConfigurationType.php # Global settings form
│ ├── UpsellOfferType.php # Upsell offer form
│ └── UpsellRelationType.php # Single relation row form
├── Repository/
│ ├── UpsellImpressionRepository.php # Analytics queries
│ ├── UpsellOfferRepository.php # Active offers query
│ └── UpsellRelationRepository.php # Relations by product
├── Service/
│ ├── FrequentlyBoughtTogetherResolver.php # FBT logic (manual + algorithmic)
│ ├── PostPurchaseOfferResolver.php # Best matching offer for an order
│ ├── UpsellAnalyticsService.php # Impression recording + analytics
│ └── UpsellConfigurationProvider.php # Cached config access
├── Twig/UpsellExtension.php # Twig function bridge
└── SyliusUpsellPlugin.php # Bundle class
assets/
├── controllers/
│ ├── fbt-controller.js # Add-all-to-cart Stimulus controller
│ └── post-purchase-controller.js # Upsell modal Stimulus controller
└── package.json
templates/
├── Admin/
│ ├── analytics.html.twig # Analytics dashboard with Chart.js
│ ├── configuration.html.twig # Global settings page
│ └── upsell_offer/
│ ├── create.html.twig
│ ├── update.html.twig
│ └── grid/field/product.html.twig
└── Shop/
├── frequently_bought_together.html.twig # FBT section on product page
└── post_purchase_offer.html.twig # Upsell modal after checkout
Testing
vendor/bin/phpunit
License
MIT. See LICENSE.



