setono / sylius-abandoned-cart-plugin
Reengage customers who abandoned their cart in Sylius
Package info
github.com/Setono/SyliusAbandonedCartPlugin
Type:sylius-plugin
pkg:composer/setono/sylius-abandoned-cart-plugin
Fund package maintenance!
Requires
- php: >=8.2
- ext-hash: *
- doctrine/orm: ^2.0 || ^3.0
- doctrine/persistence: ^2.5 || ^3.0
- knplabs/knp-menu: ^3.3
- ocramius/doctrine-batch-utils: ^2.4
- psr/event-dispatcher: ^1.0
- psr/log: ^1.1 || ^2.0 || ^3.0
- setono/composite-compiler-pass: ^1.2
- setono/doctrine-orm-trait: ^1.2
- sylius/channel: ^2.0
- sylius/core: ^2.0
- sylius/core-bundle: ^2.0
- sylius/mailer-bundle: ^2.0
- sylius/order: ^2.0
- sylius/resource-bundle: ^1.12
- sylius/ui-bundle: ^2.0
- symfony/config: ^6.4 || ^7.4
- symfony/console: ^6.4 || ^7.4
- symfony/dependency-injection: ^6.4 || ^7.4
- symfony/event-dispatcher: ^6.4 || ^7.4
- symfony/form: ^6.4 || ^7.4
- symfony/http-foundation: ^6.4 || ^7.4
- symfony/routing: ^6.4 || ^7.4
- symfony/validator: ^6.4 || ^7.4
- symfony/workflow: ^6.4 || ^7.4
- twig/twig: ^3.0
- webmozart/assert: ^1.12
Requires (Dev)
- api-platform/symfony: ~4.2.13
- dama/doctrine-test-bundle: ^8.2
- doctrine/doctrine-bundle: ^2.13
- lexik/jwt-authentication-bundle: ^3.1
- payum/core: ^1.7.5
- phpspec/prophecy-phpunit: ^2.3
- setono/sylius-plugin: ^2.0
- sylius/admin-bundle: ^2.0
- sylius/api-bundle: ^2.0
- sylius/payum-bundle: ^2.0
- sylius/shop-bundle: ^2.0
- sylius/sylius: ~2.2.5
- sylius/theme-bundle: ^2.0
- symfony/browser-kit: ^6.4 || ^7.4
- symfony/css-selector: ^6.4 || ^7.4
- symfony/debug-bundle: ^6.4 || ^7.4
- symfony/dotenv: ^6.4 || ^7.4
- symfony/intl: ^6.4 || ^7.4
- symfony/maker-bundle: ^1.0
- symfony/monolog-bundle: ^3.8
- symfony/property-info: ^6.4 || ^7.4
- symfony/serializer: ^6.4 || ^7.4
- symfony/web-profiler-bundle: ^6.4 || ^7.4
- symfony/webpack-encore-bundle: ^2.2
This package is auto-updated.
Last update: 2026-06-15 13:13:33 UTC
README
Recover lost sales on autopilot. This plugin spots shopping carts your customers leave behind, then emails them a friendly reminder with a one-click link back to their cart — and a compliant unsubscribe link. Everything is tracked in the admin panel so you can see exactly which carts were notified, recovered, or opted out.
Features
- 🛒 Automatic idle-cart detection — finds carts that have been sitting untouched for a configurable amount of time.
- ✉️ Re-engagement emails — a clean, localized email with a recovery call-to-action and an unsubscribe link.
- 🔁 One-click cart recovery — the recovery link restores the customer's cart and records the click for engagement tracking.
- 🚫 Built-in opt-out & eligibility rules — never email customers who unsubscribed, and optionally target newsletter subscribers only.
- 📊 Admin overview — a grid under Marketing → Abandoned cart listing every notification with its state, channel, customer and revenue.
- 🧩 Extensible by design — add your own eligibility rules, tweak the idle-cart query, or override the email template.
- 🌍 15 locales out of the box —
en,da,de,es,fi,fr,hu,it,nl,no,pl,pt,ro,sv,uk.
How it works
The plugin runs as a small pipeline, driven by two commands you schedule on a cron:
- Detect —
create-notificationsfinds carts that have been idle foridle_thresholdminutes and creates aNotificationfor each (initial state:pending). - Process —
process-notificationsruns every pending notification through the eligibility checks, sends the email, and transitions it tosent(orineligibleif a check fails). - Recover — the email contains a recovery link (restores the cart and tracks the click) and an unsubscribe link (records the opt-out so the customer is never emailed again).
Each notification moves through a state machine:
pending ──► processing ──► sent
└──► ineligible
(any state) ──► failed
Requirements
| Package | Version |
|---|---|
| PHP | 8.2+ |
| Sylius | ^2.0 |
| Symfony | 6.4 or 7.4 |
Using Sylius 1.x? This branch (
3.x) targets Sylius 2. For Sylius 1.x support, use the2.xbranch.
Installation
1. Require the plugin
composer require setono/sylius-abandoned-cart-plugin
2. Register the bundle
Add the plugin to config/bundles.php before SyliusGridBundle — otherwise you'll get a
non-existent parameter "setono_sylius_abandoned_cart.model.notification.class" error, because the
grid configuration references parameters the plugin registers.
<?php // config/bundles.php return [ // ... Setono\SyliusAbandonedCartPlugin\SetonoSyliusAbandonedCartPlugin::class => ['all' => true], Sylius\Bundle\GridBundle\SyliusGridBundle::class => ['all' => true], // ... ];
3. Import the routes
# config/routes/setono_sylius_abandoned_cart.yaml setono_sylius_abandoned_cart: resource: "@SetonoSyliusAbandonedCartPlugin/config/routes.yaml"
This registers the admin grid routes (under /admin/abandoned-cart) and the shop routes for cart
recovery and unsubscribe.
4. Configure the salt
The unsubscribe links are signed with a SHA-256 hash. Set a secret salt and change it in production — anyone who knows it could forge unsubscribe links.
# config/packages/setono_sylius_abandoned_cart.yaml setono_sylius_abandoned_cart: salt: '%env(ABANDONED_CART_SALT)%' # or any secret string
5. Install assets
bin/console assets:install
6. Update your database schema
bin/console doctrine:migrations:diff # generate a migration bin/console doctrine:migrations:migrate # apply it
7. Schedule the commands
The plugin needs create-notifications and process-notifications to run regularly. A typical
crontab:
# Detect newly-idle carts (run more often than `lookback_window`, e.g. every 5 minutes) */5 * * * * cd /path/to/your/app && bin/console setono:sylius-abandoned-cart:create-notifications # Send emails for pending notifications */5 * * * * cd /path/to/your/app && bin/console setono:sylius-abandoned-cart:process-notifications # Clean up old notifications once a day 0 3 * * * cd /path/to/your/app && bin/console setono:sylius-abandoned-cart:prune-notifications
Commands
| Command | What it does |
|---|---|
setono:sylius-abandoned-cart:create-notifications |
Finds idle carts and creates notifications. Supports --dry-run to preview. |
setono:sylius-abandoned-cart:process-notifications |
Runs eligibility checks and sends the emails. |
setono:sylius-abandoned-cart:prune-notifications |
Deletes notifications older than prune_older_than. |
Preview which carts would be picked up, without persisting anything:
bin/console setono:sylius-abandoned-cart:create-notifications --dry-run
Configuration reference
# config/packages/setono_sylius_abandoned_cart.yaml setono_sylius_abandoned_cart: # Secret salt used to sign unsubscribe URLs (SHA-256). Required — change it in production. salt: '%env(ABANDONED_CART_SALT)%' # Minutes a cart must be idle before it's eligible for a notification (default: 60) idle_threshold: 60 # Only carts that became idle within this many minutes are picked up, which caps how many # notifications are created per run. Run create-notifications more often than this. (default: 15) lookback_window: 15 # Prune notifications older than this many minutes (default: 43200 = 30 days) prune_older_than: 43200 eligibility_checkers: # Skip customers who actively unsubscribed (default: true) unsubscribed_customer: true # Only notify customers subscribed to the newsletter (default: false) subscribed_to_newsletter: false
Customization
Add a custom eligibility rule
Implement NotificationEligibilityCheckerInterface and return an EligibilityCheck. With Symfony
autoconfiguration enabled (the default), the plugin discovers and registers your checker
automatically — no service config needed.
<?php declare(strict_types=1); namespace App\EligibilityChecker; use Setono\SyliusAbandonedCartPlugin\EligibilityChecker\EligibilityCheck; use Setono\SyliusAbandonedCartPlugin\EligibilityChecker\NotificationEligibilityCheckerInterface; use Setono\SyliusAbandonedCartPlugin\Model\NotificationInterface; final class MinimumCartTotalEligibilityChecker implements NotificationEligibilityCheckerInterface { public function check(NotificationInterface $notification): EligibilityCheck { $cart = $notification->getCart(); if (null === $cart || $cart->getTotal() < 5000) { // The reason is stored on the notification for debugging. return new EligibilityCheck(false, 'Cart total is below the minimum'); } return new EligibilityCheck(true); } }
If you've disabled autoconfiguration, tag the service manually with
setono_sylius_abandoned_cart.notification_eligibility_checker.
Customize which carts are considered "idle"
The idle-cart query is dispatched as a QueryBuilderForIdleCartsCreated event before it runs, so you
can add your own constraints. The cart root alias is o:
<?php declare(strict_types=1); namespace App\EventListener; use Setono\SyliusAbandonedCartPlugin\Event\QueryBuilderForIdleCartsCreated; use Symfony\Component\EventDispatcher\Attribute\AsEventListener; #[AsEventListener] final class OnlyNotifyMainChannelListener { public function __invoke(QueryBuilderForIdleCartsCreated $event): void { $event->queryBuilder ->andWhere('o.channel = :channel') ->setParameter('channel', 1); } }
Override the email template
Copy the template into your app and edit it — Symfony's template overriding does the rest:
templates/bundles/SetonoSyliusAbandonedCartPlugin/email/notification.html.twig
Override translations
Override any string by redefining its key (under the setono_sylius_abandoned_cart.* prefix) in your
app's translations/messages.<locale>.yml.
Contributing
composer install composer phpunit # run the test suite composer analyse # PHPStan (max level) composer check-style # coding standards (ECS) composer fix-style # auto-fix coding standards
A full Sylius test application lives in tests/Application for functional tests and manual
testing. See CLAUDE.md for architecture notes and developer conventions.
License
This plugin is released under the MIT License.