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

Pricing Bundle Laravel Package

sylius/pricing-bundle

Sylius PricingBundle provides the pricing engine for Sylius e-commerce applications. It resolves and calculates product variant prices using configurable price providers, calculators, and context, with support for channel-specific pricing and custom pricing logic.

View on GitHub
Deep Wiki
Context7

Getting Started

First, confirm you should be here at all. The bundle is archived, Symfony-only (DI bundle + compiler passes + Doctrine mappings), and has been folded into sylius/sylius core. In a Laravel app you will never composer require it successfully on PHP 8.2+ / Laravel 10–12 without --ignore-platform-reqs, which is not a production posture. The realistic entry point is read-and-reimplement, so budget an afternoon for reading and a week for owning the result.

Read in this order (the codebase is small):

  1. Pricing/PriceResolver.php — the delegating resolver that maps a subject's class to a concrete resolver via a registry.
  2. Pricing/PriceCalculator.php — the delegating calculator that does the same for calculations (discounts, taxes, custom rules).
  3. Model/PriceInterface.php / the price entity — this is your schema spec: amount + currency, tied to a priceable subject.
  4. DependencyInjection/SyliusPricingExtension.php — the only source of truth for real config keys; the README drifted.

The core insight to carry into Laravel is the separation of concerns: a price provider returns stored base prices for a subject+context; a calculator transforms a price (discounts, taxes); a resolver picks the right pair for the object it is handed. That's roughly 200–300 lines of logic to reimplement natively.

First Laravel use case — resolve the effective price for one purchasable subject:

// app/Contracts/PriceResolver.php
interface PriceResolver
{
    public function supports(object $subject): bool;
    public function resolve(object $subject, PricingContext $context): ?Money;
}

// app/Services/Pricing/ChainPriceResolver.php
final class ChainPriceResolver
{
    /** @param iterable<PriceResolver> $resolvers */
    public function __construct(private iterable $resolvers) {}

    public function resolve(object $subject, PricingContext $context): Money
    {
        foreach ($this->resolvers as $resolver) {
            if (! $resolver->supports($subject)) {
                continue;
            }
            if ($price = $resolver->resolve($subject, $context)) {
                return $price;
            }
        }
        throw new UnresolvablePrice($subject, $context); // never silently return 0
    }
}

Bind the chain in a service provider and register concrete resolvers with a tag. Your first end-to-end test should be a single variant, single currency, base price only — then layer channel overrides, then tiered/customer-group, then discounts.

Implementation Patterns

Ordered resolver chain, not discovery order. The bundle's registry implies priority; make it explicit. In Laravel use a tagged collection sorted by a priority() method or a config array, and iterate deterministically. Most-specific-rule-wins is the policy stakeholders expect (contract price > customer-group tier > channel override > base price).

Provider vs. calculator split. Keep them separate exactly as the bundle does:

  • Providers read from storage (Eloquent) and are pure/cacheable: (priceable, currency, channel) → base price row.
  • Calculators are pure transforms on a Money value: base − discount + tax. Order them explicitly in the contract (convert currency once, then discount, then tax) and test that order.

Context as a value object. The bundle's "multiple currencies and custom pricing contexts" maps to an immutable DTO you pass everywhere:

final class PricingContext
{
    public function __construct(
        public readonly string $currency,      // ISO 4217
        public readonly ?int $channelId,
        public readonly ?int $customerGroupId,
        public readonly int $quantity = 1,
        public readonly ?CarbonImmutable $at = null,
    ) {}
}

Eloquent data model. Don't port Doctrine mappings. One prices table keyed by (priceable_type, priceable_id, currency_code, channel_id, price_type, min_quantity), amounts stored as integer minor units. Add a price_version counter bumped on any mutation for cache keys.

Pipeline for calculators. For multi-step calculation, Illuminate\Pipeline\Pipeline gives you explicit ordering and short-circuiting; for the resolver chain a simple sorted loop is clearer.

Wrap the legacy service in a decorator if any old code still calls sylius.pricing.price_resolver during migration — delegate to the new chain so there's exactly one price path.

Shadow-mode cutover. Behind a feature flag, compute both the old and new price for every request, log mismatches, and only flip when the diff is clean for a defined window. Pricing regressions surface via finance and customers, not tests.

Cache aggressively, invalidate by version. A resolver result cache keyed by (subject, currency, channel, quantity, price_version) is cheap to add and essential at catalog scale.

Gotchas and Tips

  • Archived = zero upstream. No patches, no CVE channel, no PHP-version fixes. A security advisory in a transitive Symfony/Doctrine dep leaves you without a supported fix. Treat any adoption as "we are now the maintainer."
  • Framework coupling is total. Compiler passes, bundle registration, Doctrine resource mapping, sylius/registry — none of it ports. Only the idea ports; translating the Symfony shape into Eloquent/pipeline idioms by inertia is the classic failure mode.
  • Silent zero-price bug. The original resolver returns null when nothing matches; if a caller coerces that to 0, you render $0.00 line items. Fail loudly in non-production, log-and-alert in production. Never default to zero on the price path.
  • Currency assertions at the boundary. Assert the resolved price's currency equals the context currency. A fallback to base price when a channel override exists is the other common wrong-price bug.
  • Rounding drift from composition order. Discount-then-convert vs. convert-then-discount produce different totals. Fix the order in the calculator contract and add a rounding test with awkward rates.
  • Resolver ordering regressions. Adding a resolver must not change precedence by accident. Make ordering configuration, not registration order.
  • N+1 on catalog/cart pages. A per-object resolver chain invites one price lookup per row. Eager-load price sets and cache by version; keep resolvers pure and side-effect-free so caching stays safe.
  • Config drift. README keys disagree with SyliusPricingExtension; never trust docs, read the extension's load() and the service definitions it wires.
  • Extension points to copy, not depend on: the resolver/calculator interfaces, the registry-with-priority pattern, and the provider/calculator split. Everything else is Symfony plumbing you replace with a ServiceProvider and container tags.
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.
microsoft/fluentui-system-icons
mmccook/php-json-canonicalization-scheme
middlewares/firewall
qubiqx/montapacking
nyoncode/laravel-package-console
pnscripts/filament-broadquill-lite
pnscripts/laravel-sentfold-lite
vleap/mx-sdk-laravel-internal
twidpay/utils
htetmyathlaing/scout-elasticsearch-driver
artisan-toolbox/front-awesome
ls/client-assistant
users-au/laravel-client
cgv/localized-resource
noith/settings
kda/laravel-package
kda/filament-dynamic-navigation
bypersoft/signbox
pairus/product-data
fayne/laravel-filament-cms-core