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

Customer Bundle Laravel Package

sylius/customer-bundle

Sylius Customer Bundle provides customer management for Sylius e-commerce apps, including customer models, repositories, managers, address handling, profile forms, and integration with users and orders. It offers reusable services and UI components for creating, editing, and resolving customers.

View on GitHub
Deep Wiki
Context7

Getting Started

Establish which of the two scenarios you're in

sylius/customer-bundle is a read-only subtree split of the Sylius monorepo, and it's a Symfony bundle. There is no service provider, no config/customer.php to publish, no Eloquent. So "day-to-day leverage" resolves to one of:

  1. You're on Sylius/Symfony and need to change customer behaviour (add a field, change group pricing, adjust registration).
  2. You're on Laravel and you're using this bundle as an executable specification for the customer/group domain before writing your own migrations.

Decide this first, because the first scenario is a five-minute question and the second determines everything you read.

Never require it directly

# Wrong
composer require sylius/customer-bundle

# Right — the bundle arrives transitively, version-aligned
composer require sylius/sylius

sylius/order-bundle, sylius/user-bundle, sylius/core-bundle, and sylius/shop-bundle all type-hint Sylius\Component\Customer\Model\CustomerInterface. Pulling the split package directly guarantees a version skew that surfaces as confusing DI errors at boot, not at install time. Dependents: 0 and Stars: 3 are a distribution artefact of the split — adoption is measured through sylius/sylius.

Files to read first (in this order)

Path Why
Model/Customer.php The contract you'll either extend or port. Note email, emailCanonical, firstName, lastName, birthday, gender, phoneNumber, subscribedToNewsletter, group, defaultAddress, addresses, user, createdAt, updatedAt, anonymizedAt.
Model/CustomerInterface.php The seam. Everything downstream depends on this interface, not the class.
Model/CustomerGroup.php Trivial — a code + name. Its value is that it exists as the segmentation hook.
Resources/config/doctrine/model/Customer.orm.xml The actual schema: column lengths, indexes, join tables, cascade rules. Copy this, not the PHP.
Resources/config/services.xml What's actually registered: factory, repository, form types, the resource metadata.
Form/Type/CustomerType.php, CustomerProfileType.php, CustomerRegistrationType.php Where the real validation and field-level behaviour lives.
Resources/config/validation.xml Constraints, including unique email.

Addresses are not here — Customer::addresses maps to Sylius\Component\Addressing\Model\Address from sylius/addressing-bundle. Expect to read two bundles for one feature.

First use case

On Sylius: add a CustomerGroup-driven wholesale price. Create the group via the admin, subscribe a customer to it, write a PriceCalculator that checks $customer->getGroup()?->getCode().

On Laravel: port customers + customer_groups as your first two migrations and a Customer Eloquent model that has a group() belongsTo and a user() hasOne. This is a half-day exercise that saves a week of schema discovery.

Implementation Patterns

Pattern 1 — Extending the model (Sylius path)

The supported extension point is class inheritance plus resource config override, not editing vendor code:

// src/Entity/Customer.php
namespace App\Entity;

use Doctrine\ORM\Mapping as OR:MappedSuperclass;
use Sylius\Component\Customer\Model\Customer as BaseCustomer;

#[ORM\MappedSuperclass]
class Customer extends BaseCustomer
{
    #[ORM\Column(type: 'string', length: 64, nullable: true)]
    private ?string $loyaltyTier = null;
}
# config/packages/_sylius.yaml
sylius_customer:
    resources:
        customer:
            classes:
                model: App\Entity\Customer

Then re-run bin/console doctrine:schema:update (or generate a migration). The sylius_customer.resources.customer key is the single knob — factory, repository, form, and controller are all resolved from it via the resource registry.

Pattern 2 — Business logic in the repository, not the entity

The repository interface (CustomerRepositoryInterface::findOneByEmail, findByGroup, etc.) is what controllers and other bundles depend on. When you need "customers in this group created since date", add a method to the repository and override the sylius.repository.customer service class rather than putting queries in a controller:

sylius:
    repository:
        customer:
            class: App\Repository\CustomerRepository

Pattern 3 — Form extension instead of form replacement

Do not override CustomerProfileType wholesale — you break every other bundle that composes it. Use AbstractTypeExtension:

class CustomerProfileTypeExtension extends AbstractTypeExtension
{
    public static function getExtendedTypes(): iterable
    {
        return [CustomerProfileType::class];
    }

    public function buildForm(FormBuilderInterface $builder, array $options): void
    {
        $builder->add('loyaltyTier', TextType::class, ['required' => false]);
    }
}

Tag it form.type_extension. This survives Sylius minor upgrades.

Pattern 4 — Porting the domain to Laravel (the likely path)

Translate the Doctrine mapping, not the PHP. The migration from Customer.orm.xml:

Schema::create('customers', function (Blueprint $table) {
    $table->id();
    $table->string('email')->nullable();
    $table->string('email_canonical')->nullable()->unique();
    $table->string('first_name')->nullable();
    $table->string('last_name')->nullable();
    $table->date('birthday')->nullable();
    $table->string('gender')->nullable();
    $table->string('phone_number')->nullable();
    $table->boolean('subscribed_to_newsletter')->default(false);
    $table->foreignId('customer_group_id')->nullable()->constrained()->nullOnDelete();
    $table->foreignId('user_id')->nullable()->unique()->constrained()->nullOnDelete();
    $table->timestamp('anonymized_at')->nullable();
    $table->timestamps();
    $table->index(['last_name', 'first_name']);
});

Keep three things from the original:

  • The customer/user split. A Customer row exists the moment someone checks out as a guest; a User row appears only on registration. Collapsing them into one users table is the mistake that forces a painful migration later when you add SSO.
  • email_canonical as a separate unique column. Lets ops edit email for display (case, formatting) without breaking the identity key.
  • anonymized_at. Not deleted_at. GDPR erasure nulls the PII in place while preserving order history and aggregate reporting.

Pattern 5 — Group-based pricing resolver

The one piece of behaviour genuinely worth copying. CustomerGroup is a stable, code-identified bucket, designed to be consulted by pricing, tax, and catalog-visibility rules. In Laravel, resolve the effective group once per request into a scoped singleton rather than re-querying it in every price calculation:

$this->app->scoped(EffectiveGroup::class, fn () =>
    new EffectiveGroup(auth()->user()?->customer?->customerGroup)
);

Then inject it into your price and discount services. This mirrors how Sylius's CustomerGroup is consumed by promotion rules and pricing.

Pattern 6 — Guest-to-customer conversion

Sylius creates a Customer with no user at guest checkout, and attaches the ShopUser on registration, merging carts by Customer. If you're porting this, the sequence is:

  1. Guest checkout → Customer row, user_id = null.
  2. Registration → match on email_canonical, attach user_id, do not create a second Customer.
  3. Order assignment → always order.customer_id, never order.user_id.

The CustomerFactory in the bundle is what makes step 1 cheap; mirror it with a small factory/service class rather than Customer::create() scattered through controllers.

Gotchas and Tips

This repository takes no PRs. Issues and patches go to Sylius/Sylius. Any fork becomes a permanent merge burden against every upstream change. Price that in before patching a bug locally.

Addresses live in sylius/addressing-bundle. Searching this repo for Address finds only the defaultAddress relation and the interface. If you're porting the address model (default_billing_address, default_shipping_address pointers, one-to-many address book), you're reading the wrong package.

Never define your own Customer entity against the same table. Two Doctrine mappings for one table produce The table with name 'customers' already exists or silent partial hydration. Extend the MappedSuperclass; if you need a parallel entity, use a different table name and a mapping override.

Unique-email validation is global, not per-channel. The constraint on email predates multi-channel Sylius. If you need per-channel identities in your port, that's a new constraint, not a copy.

Version coupling is non-negotiable. This bundle's PHP and Symfony require constraints are dictated by the Sylius release line. You cannot upgrade it independently, and pinning it manually in composer.json is how you get a composer why-not rabbit hole.

Forms are Symfony Form component only. CustomerProfileType, CustomerRegistrationType, and friends are 300-line FormBuilder trees. Do not try to evaluate them for Laravel reuse — read them as a validation checklist (required fields, birthday format, gender choices) and write FormRequests.

Impersonation and password-reset touch Customer via ShopUser. Any change to the customer/user relation is a security change. Write integration tests around login, impersonation, and reset before and after the change.

Watch for N+1 on the customer graph. $customer->getAddresses(), ->getGroup(), and ->getUser() are each a lazy load. In admin list views and pricing loops in particular, eager-load them. This is the single most common performance complaint in Sylius shops.

anonymized_at is not optional in the EU. Sylius's anonymisation nulls PII but leaves the row. If your port only has soft deletes, erasure requests force you to rewrite rows that orders still reference. Build the column in from day one.

Fixtures for day-to-day dev. If you're on Sylius, sylius_fixtures loads customers via load_customers. Use it — hand-seeding customer groups makes the group-pricing work impossible to test realistically.

Debugging tips (Sylius path):

bin/console debug:container sylius.repository.customer
bin/console debug:container sylius.factory.customer
bin/console doctrine:mapping:info | grep -i customer
bin/console debug:config sylius_customer

The first thing to check on any "customer form doesn't show my field" bug is whether your AbstractTypeExtension is registered — debug:container --tag=form.type_extension lists it.

Extension point summary: model (MappedSuperclass + sylius_customer.resources.*.classes.model), repository (sylius.repository.customer.class), factory (sylius.factory.customer.class), forms (form.type_extension), validation (a validation.xml/constraint in your app bundle, not vendor edits). Everything else is a fork.

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