setono/sylius-abandoned-cart-plugin

Reengage customers who abandoned their cart in Sylius

Maintainers

Package info

github.com/Setono/SyliusAbandonedCartPlugin

Type:sylius-plugin

pkg:composer/setono/sylius-abandoned-cart-plugin

Fund package maintenance!

Setono

Statistics

Installs: 23 284

Dependents: 0

Suggesters: 0

Stars: 4

Open Issues: 4

v3.0.0-alpha 2026-06-15 12:44 UTC

README

Latest Stable Version Total Downloads License PHP Version Require build codecov Mutation testing badge

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 boxen, 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:

  1. Detectcreate-notifications finds carts that have been idle for idle_threshold minutes and creates a Notification for each (initial state: pending).
  2. Processprocess-notifications runs every pending notification through the eligibility checks, sends the email, and transitions it to sent (or ineligible if a check fails).
  3. 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 the 2.x branch.

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.