elcodi/coupon-bundle
Coupon Bundle is part of the Elcodi e-commerce suite of Symfony components and bundles, providing coupon functionality for online stores. Note that this package is read-only; issues, questions, and pull requests belong in the main elcodi/elcodi repository.
Elcodi's coupon-bundle is a read-only subtree split of elcodi/elcodi. Install it for dev-tracking convenience but treat the monorepo as your source of truth:
composer require elcodi/coupon-bundle
Then enable it in AppKernel.php (order matters — it depends on CartBundle, ProductBundle, and RuleBundle):
new Elcodi\Bundle\CartBundle\ElcodiCartBundle(),
new Elcodi\Bundle\RuleBundle\ElcodiRuleBundle(),
new Elcodi\Bundle\CouponBundle\ElcodiCouponBundle(),
Where to look first, in order:
Resources/config/services.yml — the DI wiring tells you the real service IDs (elcodi.coupon_manager, elcodi.coupon_factory, elcodi.coupon_repository, elcodi.coupon_transformer, etc.).Entity/Coupon.php + Entity/Interfaces/CouponInterface.php — the shape of a coupon (code, type, value, valid dates, usage counters, rules collection).Entity/CouponRule.php (and the rule classes under Rule/) — Elcodi leans on a rule/expression engine rather than hard-coded conditions, so most customisation happens here.Manager/CouponManager.php — the actual apply/validate façade you will inject into controllers and services.Your first use case is almost always "apply a coupon code typed by a shopper into a cart, and return a usable discount or a reason it failed." That flow is:
$coupon = $this->get('elcodi.coupon_repository')->findOneByCode($code);
if (null === $coupon) {
// unknown code — do not leak existence
}
$this->get('elcodi.coupon_manager')->applyCoupon($coupon, $cart);
Do not reach for CouponRepository::findOneByCode() and start mutating the cart yourself — the manager is what coordinates rule evaluation, date validity checks, usage counters and cart totals. Skipping it is the fastest way to produce a coupon that "works" in admin but silently fails in checkout.
Pattern 1 — Programmatic coupon creation (fixtures, imports, admin imports). Use the factory, not new Coupon(); the factory wires the ID, enabled flag, timestamps and empty rules collection:
$coupon = $this->get('elcodi.coupon_factory')->create();
$coupon
->setCode('SUMMER10')
->setType('percentage') // or 'fixed_amount'
->setValue(10)
->setValidFrom(new \DateTime())
->setValidTo(new \DateTime('+30 days'))
->setCount(0)
->setEnabled(true);
$this->get('elcodi.coupon_repository')->save($coupon);
Pattern 2 — Rule-driven eligibility. Rather than branching on coupon attributes in your controller, attach CouponRule entities. Each rule has a name, an expression, and a target rule_type. This is how the CouponBundle composes things like "minimum basket subtotal", "only on first order", "restricted to product X". Keep the logic in rules so the same coupon validates identically at cart-time and order-time.
Pattern 3 — Apply at cart, persist at order. The cart holds coupons in-memory via CartCouponManager; the order persists coupon references through order listeners. Two useful integration points:
elcodi.cart_coupon_applier or your own CartEvent subscriber) that re-validates coupons on every cart mutation — coupons must survive quantity changes, not just the moment they were entered.OrderOnCreatedEvent) that calls couponManager->consumeCoupon($coupon, $order) so the usage counter only increments on successful checkout, not on add-to-cart.Pattern 4 — Expose failures, not booleans. The manager's coupon-apply path throws on hard failures and returns enriched coupons on success. Wrap in a try/catch and map exceptions to user-facing flash messages ("This coupon has expired", "Coupon not valid for this basket"). Your template then renders the discount line item from Cart::getCoupons() rather than recomputing it.
Pattern 5 — Admin CRUD. The bundle ships no admin UI (Elcodi's admin lives above the bundles). Wire a form for CouponType (the bundle exposes the entity, you build the form type) and always validate code uniqueness via the Coupon entity's unique constraint — otherwise checkout will 500 on the first duplicate insert rather than show a friendly error.
elcodi/elcodi. The split has no composer.json branch aliasing beyond dev-master. If you pin a tag here, you inherit the monorepo's release cadence; if you track dev-master here, you are testing the whole suite. Prefer the elcodi/elcodi requirement in production.symfony/* overrides or you will fight deprecations in the event-dispatcher and doctrine-bridge wiring.setType() takes percentage / fixed_amount (and the coupon types in CouponTypes). Misspellings fail silently — nothing validates the type against the enum. Wrap this in a constant or your own value object.10 as a fixed-amount coupon means 10 minor units (cents) unless your currency config differs. Percentage coupons are evaluated against the subtotal, not the post-discount total. Print $coupon->getValue() and the resulting Cart::getAmount() side-by-side when a 10% discount "looks wrong" — 95% of these tickets are unit confusion.validFrom/validTo. The database stores naive datetimes; the comparison uses PHP's default timezone. Set date_default_timezone_set() or php.ini consistently between dev and prod, or expiry boundaries will drift.count / count_used are incremented in PHP. Under concurrent checkouts you can exceed a limited-use coupon. If you sell limited coupons, add a SELECT ... FOR UPDATE in your own repository method or a DB CHECK (or unique "coupon_usage" row per order).$coupon->getRules() and each rule's getName()/getExpression() on invalid results, and run the expression against the cart manually. The RuleBundle's compiler is where the actual failure occurs, not in CouponManager.CouponRule subclass plus a Doctrine entity, registered via a compiler pass in your own bundle — do not edit the vendor tree (subtree splits make that unrecoverable). For new coupon types, override the elcodi.coupon_applier service definition with your own class implementing the same interface.How can I help you explore Laravel packages today?