Weave Code
Weaver
Helps Laravel developers discover, compare, and choose open-source packages. See popularity, security, maintainers, and scores at a glance to make better decisions.
Feedback
Share your thoughts, report bugs, or suggest improvements.
Subject
Message

Promotions Bundle Laravel Package

sylius/promotions-bundle

Sylius PromotionsBundle provides a flexible promotion engine for Sylius ecommerce: define cart and order promotions with rules and actions, apply discounts automatically, manage coupon codes and usage limits, and integrate promotions seamlessly with the checkout and order lifecycle.

View on GitHub
Deep Wiki
Context7

Getting Started

First, confirm what you actually have. This package is not a Laravel package and it is not a library you can install and call. It is a [READ-ONLY] Composer BC shim that exists so old composer.json files still resolve after Sylius moved its bundles into the sylius/sylius monorepo. There is nothing in it to use. The first step is always diagnosis, not installation:

composer why sylius/promotions-bundle
composer show sylius/promotions-bundle
grep -n "promotions-bundle" composer.json composer.lock

Three outcomes:

  1. It's a direct require with a VCS repository entry pointing at the fork — that's a legacy pin someone added. Delete the repository block, drop the explicit requirement, and pin sylius/sylius instead.
  2. It's transitive — a third-party package still demands it. Leave it alone; it resolves to a shim and does nothing.
  3. You're not on Sylius at all — stop here. Do not add it.

Where to look first. Not this repo. The real code lives in sylius/sylius under src/Sylius/Component/Promotion (the domain: Promotion, PromotionRuleInterface, PromotionActionInterface, PromotionCoupon, PromotionEligibilityChecker, PromotionProcessor) and src/Sylius/Bundle/PromotionBundle (Symfony wiring: tagged services, compiler passes, admin UI). Read those. The fork you're looking at is a snapshot of Composer metadata.

Your first use case depends on which of two developer you are:

  • Maintaining a legacy Sylius store — your first task is removing this explicit dependency and regression-testing cart/order promotion flows. Budget a PR, not a project.
  • Building discounts in a Laravel app — your first task is reading the Sylius promotion model as a design spec and writing your own. Concretely: a promotions table, a promotion_rules table with a type + JSON config, a promotion_actions table with type + JSON config, a coupons table with an atomic used counter, and a service that evaluates rules and applies actions. That spec is worth roughly a day of reading and is the only day-to-day leverage this package offers you.

Implementation Patterns

If you're on Sylius (legacy → monorepo migration). The pattern is to make the shim invisible and let the platform own the capability:

{
  "require": {
    "sylius/sylius": "^1.13"
  }
}

Remove the repositories vcs entry, run composer update --dry-run, confirm composer why still resolves the shim transitively if any plugin needs it, then smoke-test promotions end-to-end. Do not write code against sylius/promotions-bundle's namespace — it carries no classes of consequence. Write against Sylius\Component\Promotion\*.

If you're on Laravel (the realistic case). Don't port. Reproduce the four-part model, which is the genuinely good idea in Sylius:

// 1. Rule = predicate over cart state
interface PromotionRule {
    public function isEligible(Cart $cart, array $config): bool;
}

// 2. Action = strategy mutating order adjustments
interface PromotionAction {
    public function apply(Order $order, array $config): void;
}

// 3. Eligibility checker = gate that runs all rules for a promotion
final class PromotionEligibilityChecker
{
    public function isEligible(Promotion $promotion, Cart $cart): bool
    {
        foreach ($promotion->rules as $rule) {
            if (! $this->ruleRegistry->get($rule->type)->isEligible($cart, $rule->config)) {
                return false;
            }
        }
        return true;
    }
}

// 4. Processor = ordered, short-circuited application pass
final class PromotionProcessor
{
    public function process(Cart $cart): void
    {
        foreach ($this->promotions->activeOrderedByPriority() as $promotion) {
            if (! $this->checker->isEligible($promotion, $cart)) {
                continue;
            }
            foreach ($promotion->actions as $action) {
                $this->actionRegistry->get($action->type)->apply($cart->order, $action->config);
            }
            if ($promotion->exclusive) {
                break;
            }
        }
    }
}

The registry + tagged-service pattern is what makes this extensible without touching the engine — mirror it with Laravel's container:

// AppServiceProvider
$this->app->tag([CartTotalRule::class, ItemQuantityRule::class, CustomerGroupRule::class], 'promotion.rules');
$this->app->tag([PercentageDiscount::class, FixedDiscount::class, FreeShipping::class], 'promotion.actions');

Coupons as a first-class entity, not a promo field. The Sylius model treats coupons as separate with perCouponUsageLimit, expiresAt, and a promotion-level usageLimit. Copy that separation. Reserve usage at order placement, not at cart-add — see gotchas.

Wire promotion processing into the right lifecycle hook. In Sylius this is the cart/order pipeline. In Laravel that's wherever your cart totals are recalculated — a CartRecalculator called from add/update/remove and from the checkout step transition. Never from a model observer, or you'll get re-entrant processing.

Gotchas and Tips

You cannot "leverage this package." Nothing here is callable. If a teammate adds it expecting a promotions subsystem, you've created phantom dependency creep — a composer.json line that implies a capability that doesn't exist. Log a tech-debt ticket and reclassify it in your architecture notes so it isn't re-litigated next quarter.

The fork is a single point of build failure. It's a 0-star personal mirror. If it's deleted or force-pushed, your build breaks with no upstream substitute at that URL. Repoint to the canonical Packagist package or drop the pin entirely.

Framing correction for the team: despite any "Laravel/PHP" categorization, this is Symfony/Sylius. Sylius assumes Symfony's DI container and compiler passes, Doctrine entity mapping, the Sylius ResourceBundle/registry abstraction, the workflow component, and an event-driven cart pipeline. There is no adapter layer that makes this cheap; the cost is a second framework and a second persistence layer.

Double-applied adjustments are the classic bug. Promotion processing is often re-entrant (cart mutation → recalc → apply). If your action mutates order totals without first clearing its own prior adjustments, you'll stack discounts. Always rebuild adjustments from a clean state per pass, or key adjustments by promotion ID and replace.

Coupon counters race under concurrent checkout. perCouponUsageLimit must be enforced atomically — a transaction with a row lock, or an atomic UPDATE ... SET used = used + 1 WHERE used < limit that returns an affected-row count you check. Verify, don't assume.

Eligibility evaluation is a hot path. It runs across all active promotions on every cart mutation. Short-circuit by priority and specificity, cache eligibility keyed to cart state — and if you add that cache, invalidate it when an admin edits rules. Stale eligibility after a rule edit is a subtle and common failure.

Extension points are not here. If you're looking for where to register a custom rule or action: sylius/sylius, tagged services consumed by the promotion processor. The Sylius\Component\Promotion interfaces (RuleInterface, ActionInterface, PromotionApplicableInterface) are your extension surface, not this bundle.

Verify the major version before touching anything. The migration approach differs completely between a legacy per-bundle Sylius install and a monorepo install. composer show sylius/sylius answers this; guessing wastes a day.

Ramp-up asymmetry is the tell. A Sylius team reaches productivity in this domain instantly — there's nothing new. A Laravel-only team faces multi-week cost. That gap is the clearest signal that this artifact exists to keep old composer.json files installing, not to be adopted.

Debugging checklist when promotion flows break after an upgrade: confirm sylius/promotions-bundle resolves and to which version (composer why --tree); check for duplicate bundle registration in config/bundles.php; verify the promotion processor is still on the cart pipeline; replay coupon redemption with a usage limit; and re-run admin promotion editing. Four steps, ten minutes, catches most skew.

Weaver

How can I help you explore Laravel packages today?

Conversation history is not saved when not logged in.
Prompt
Add packages to context
No packages found.
alphabalex/messaging-made-easy
rawphp/warp
shootkiran/merobug
narakode/fineauth
numencode/wn-syncops-plugin
ibnnajjaar/mib-global-pay
ably/pubsub-server
mstfkhazaal/defender
mfonte/base62x
karnoweb/metapilot
tobya/bcsapiwrapper
swalker2/cpanel
vish4395/laravel-translation-manager
morningtrain/economic
teksite/systeminfo-laravel
atomcms/voting
run6/weather
siokas/laravelembeddirectives
adamlien/pkuuids
gilsonsouza/lara-doc-generator