sylius/sales-bundle
Sylius Sales Bundle provides the core sales order model and processing logic for the Sylius e-commerce platform, including orders, order items, adjustments, and order state handling—a foundational component for building flexible online stores and custom checkout workflows.
Do not composer require this package in a Laravel app. sylius/sales-bundle is an archived, read-only subtree split from Sylius built for Symfony 2.x. Requiring it will drag in Symfony 2.x components, Doctrine ORM annotations, and PHP constraints that no modern Laravel install satisfies. Treat it as a reference implementation and domain spec, not a runtime dependency.
Minimal steps that actually work:
composer install needed to read it.Model/Order.php — the aggregate root (items, adjustments, totals, state).Model/OrderItem.php — the line item.Model/OrderItemUnit.php — the per-quantity unit split.Model/Adjustment.php — the pricing ledger (tax, shipping, promotion, discount).DependencyInjection/Configuration.php and the resource/config YAML — to see which classes are meant to be overridden.First use case for a Laravel developer: you're about to add orders.quantity as an integer and orders.total as a decimal column. Read OrderItemUnit first. Sylius splits every ordered quantity into individual unit rows so that a single line of "3 × T-shirt" can later be partially refunded, partially shipped, or partially discounted without inventing a refunded_quantity column. That one idea is worth the price of admission and maps cleanly onto an Eloquent order_item_units table.
Where to look next for the day-to-day: the Adjustment model and the total recalculation services (OrderProcessor, OrderItemUnitProcessor). If your backlog includes "apply a coupon" or "add tax per line", the bundle's answer — a signed adjustment row attached to either an order or an order item, recomputed on every transition — is the pattern you will actually reuse.
Port the model graph, not the code. The package is Doctrine-mapped-superclass oriented; in Laravel you rebuild the same shape with Eloquent:
orders → order_items → order_item_units (one row per quantity).adjustments polymorphic to order and order item (adjustable_type / adjustable_id).sum(item.unit_price) + sum(adjustments.amount) — never a single mutable total column that you patch in five places.Adjustments are the pattern to steal wholesale. Every price modifier — tax, shipping, promotion, discount — is an Adjustment row with a type, a label, a signed integer amount, and an origin (which promotion or tax rate created it). That means:
// Recompute instead of mutate
$order->total = $order->items->sum(fn ($i) => $i->total())
+ $order->adjustments->sum('amount');
Recalculation on every state transition (cart update, coupon apply, shipping selection, refund) is idempotent and auditable. You get a free promotion/tax history and can answer "why was this order €4 cheaper?" months later. In Laravel, wrap that in an OrderTotalCalculator service and call it from a single observer or domain event, not from controllers.
Model state as an explicit machine. The bundle drives order status through Symfony's state machine (cart → addressed → shipping_selected → payment_selected → confirmed → fulfilled/cancelled). Mirror this in Laravel with symfony/workflow (it runs fine standalone), or spatie/laravel-model-states, and keep all transitions in one class. Do not scatter if ($order->state === 'confirmed') checks — that is exactly what the bundle is designed to prevent, and it is the habit most likely to regress after a redesign.
Checkout completion as an ordered set of processors. The bundle's checkout flow is a pipeline: validate → price → adjust → confirm. In Laravel, implement the same as a CheckoutPipeline you can test line by line:
class CheckoutPipeline {
public function __construct(private array $steps) {}
public function run(Order $order): Order {
return collect($this->steps)->reduce(fn ($o, $s) => $s($o), $order);
}
}
Binding the steps in a service provider gives you the extension point the bundle offers via its tagged services, without a Symfony container.
Sequential order numbers are a generator, not a column default. The bundle uses a number generator service so you can swap between sequential, UUID, or per-channel formats. In Laravel, implement OrderNumberGenerator as an interface bound in a service provider; do not rely on auto_increment for a customer-facing number — it leaks volume and breaks when you migrate.
Multi-channel orders. The bundle tags orders with a channel and a currency. If you support more than one storefront or currency, carry both fields on orders from day one and key pricing off them. Retrofitting currency into a pricing model later is expensive; the bundle bakes currency into adjustments for exactly this reason.
Events at the boundaries. The bundle fires domain events for order creation and state transitions. Map these to Laravel events (OrderPlaced, OrderConfirmed, OrderRefunded) and let listeners handle side effects — email, inventory reservation, ERP sync. Never put those side effects inside the processor itself; that is the failure mode the bundle's event-driven design avoids.
It will not install. Attempting composer require sylius/sales-bundle in a Laravel 10/11 project fails on PHP and Symfony constraints almost immediately. If you must inspect it, run composer require --no-update then composer why-not for a clear picture, or just clone the repo. There is no service provider, no facade, no config file that Laravel's discovery can pick up.
No Laravel integration points exist. No service provider, no Eloquent mapping, no event bridge, no migrations. Any "integration" is a full rewrite of the model layer against the bundle's designs. Budget accordingly: reading is a two-hour task, porting is a two-to-four week task depending on payment and tax scope.
Archived means archived. No security patches, no PHP 8.3+ compatibility, no one to escalate to. Do not let a partially-working port grow into a permanent fork — take the design and leave the code. If it already appears in a legacy composer.json, that is a roadmap signal, not a maintenance task.
Doctrine superclass mapping does not translate 1:1. Sylius models are Doctrine mapped superclasses that consumers extend. Eloquent has no equivalent base-entity pattern that is as clean; you will either use traits or a single concrete model with polymorphic children. The trait approach is closer to the bundle's spirit and stays testable.
Unit rows are opinionated about quantity. If your business sells only unique items (serialized goods, license keys), the OrderItemUnit layer is overhead. It is worth keeping only where partial refunds, partial shipments, or per-unit promotion limits are real requirements. Do not add it just because Sylius does.
Adjustments are signed, and the convention matters. Positive = surcharge (tax, shipping), negative = discount. Pick the convention on day one, document it in the model's docblock, and enforce it in a setter. Mixed sign conventions are the single most common source of "totals look wrong" bugs in adjustment-based pricing models.
Rounding is a domain decision, not a presentation one. The bundle stores monetary amounts as integers (minor units) and lets the presentation layer format. Do the same in Laravel — decimal(12,4) for pricing intermediates if you must, but store the customer-facing amount as an integer and format in a value object. Do not round() at the controller.
Watch the state machine for hidden transitions. Sylius allows "cancel" from several states and "fulfill" only from "confirmed". When you port the graph, draw it out (Mermaid or a whiteboard photo in the PR) before writing the transition config. The bundle's YAML is the reference; the guard conditions (e.g., "cannot confirm with zero-value items") are the part developers forget to carry over.
Extension points to copy, not use. The bundle's extension pattern — override the configured class name in DI — becomes Laravel binding overrides in a service provider. Keep the same escape hatch (interface + default implementation bound in AppServiceProvider) so a tenant-specific order number format or adjustment calculator can be swapped without editing the core.
Debugging: if you find a bug in your port, check whether the bundle has a corresponding test in spec/ or tests/. The behavior specs (phpspec) are the clearest prose description of expected behavior the repo contains — more useful than the README for edge cases around totals and adjustments.
Do not try to preserve Doctrine annotations or XML mappings. They are the least transferable part of the package. Your output should be Eloquent migrations, models, and a calculator service — nothing else.
How can I help you explore Laravel packages today?