sylius/association-bundle
The Sylius Association Bundle adds product association management to Sylius stores, letting administrators group products into named association types such as related items, cross-sells, or accessories, with configurable types and admin UI for editing them.
Read this first: the package is archived and read-only. sylius/association-bundle is a monorepo split of Sylius/Sylius, published so the parent repo can be consumed as separate Composer packages. It is not developed, triaged, or patched independently — bug reports and PRs go to Sylius/Sylius. With ~0 dependents and 3 stars, treat it as an internal implementation detail of Sylius, not a library you adopt.
Practical entry point. Do not composer require sylius/association-bundle standalone for new work. Get it transitively via sylius/sylius (or sylius/product-bundle), which pins a compatible version. If you are building a non-Sylius Symfony app, depend on sylius/association (the component) directly — the bundle only adds DI wiring and Doctrine mappings, and you can replicate that in ~30 lines of services.yaml.
Where to look, in order:
DependencyInjection/SyliusAssociationExtension — the whole bundle is essentially here. It registers the registry service and loads Doctrine XML mappings.Resources/config/doctrine/*.orm.xml — the mapping for AssociationType and Association. If you override models, this is what you must shadow.Sylius\Component\Association\Registry\AssociationTypeRegistry — the runtime API you actually interact with.AssociationTypesInterface — the built-in type name constants (RELATED, UP_SELL, CROSS_SELL).First use case: add a new association type (e.g. "frequently bought together") between two models and expose it on the storefront. You register an AssociationType row keyed by a string name, then create Association entities that reference owner, associated object, and that type.
// services.yaml
services:
App\Association\FrequentlyBoughtTogetherType:
tags: ['sylius.association_type']
Enable the bundle in config/bundles.php (SyliusAssociationBundle is normally already enabled by the Sylius recipe). Then verify wiring with bin/console debug:container sylius.association_type.registry.
Registry as the seam. Almost all interaction goes through Sylius\Component\Association\Registry\AssociationTypeRegistryInterface (sylius.association_type.registry). Inject it rather than the concrete class; it exposes getTypeByName(string $name): AssociationTypeInterface and getTypes(): iterable. Everything else in the bundle is Doctrine plumbing.
Custom association type via the registry. Two ways, and they differ in lifecycle:
// Runtime — registered on every container boot, no DB row until persisted.
final class ProductBundlesType implements AssociationTypeInterface
{
public function getName(): string { return 'product_bundles'; }
public function getLabel(): string { return 'sylius.association_type.product_bundles'; }
}
final class AssociationTypeRegistrar
{
public function __construct(private AssociationTypeRegistryInterface $registry) {}
public function register(): void
{
$this->registry->addType(new ProductBundlesType()); // name may differ by version
}
}
The DB-backed route (a AssociationType entity row plus a Factory/fixture) is what Sylius itself uses, and it is the safer one because the admin UI and fixtures read from the repository. Prefer it unless you have a reason not to.
Adding associations to an aggregate. Associations are not automatically reciprocal. Creating an association from $a to $b writes one row with $a as owner and $b as associated. If your domain needs the reverse to be visible from $b, add the second Association explicitly — do not expect the model to synthesize the inverse.
$association = $associationFactory->createNew();
$association->setOwner($product);
$association->setAssociated($relatedProduct);
$association->setType($registry->getTypeByName(AssociationTypesInterface::RELATED));
$product->addAssociation($association); // check the actual collection API for your version
Querying by type. Filtering through the owning collection is fine for a handful of rows and terrible at scale; the collection is loaded eagerly by Doctrine's default. For listing pages, use the association repository with a JOIN on association_type.name and paginate there rather than hydrating a full Product aggregate.
Integration with forms. The bundle does not ship user-facing form types; those live in ProductBundle/AdminBundle. For a custom type, build the form yourself: an EntityType over your associated resource, plus a hidden/choice field for the association type name. Reuse Sylius' translation domain (sylius.association_type) for labels so admin columns render consistently.
Resource overrides, not subclassing the bundle. To add a field to AssociationType (an icon, a sort order, a description), override the model class in config (sylius.association_type.class) with a class extending the base model, and shadow the Doctrine XML in your app config. Never extend the bundle's PHP classes — they carry no BC promise and the split repo means they change without notice.
Fixtures. Register association types as reference data (idempotent, load-before-products in your fixture order). If you load them in the same pass as products, you will hit foreign-key ordering failures on fresh databases.
sylius/sylius pins — pin the monorepo, let it pin this, and never float this package independently.Sylius/Sylius and file there; otherwise you are patching a package that will be overwritten on the next composer update.name string, not an integer ID. Renaming a type in a fixture or constant orphans every existing association row and silently breaks storefront listings. Treat names as immutable identifiers; put human copy in the label/translation, never in the name.Association to add a field, copy the entire mapping XML into your bundle and remap the resource. A partial override leaves fields unmapped and Doctrine fails at schema-tool time with an unhelpful "no mapping" error.(owner, associated, type). Programmatic double-adds are easy and produce duplicate related-product blocks on the storefront. Add a unique constraint in your own migration if you own the schema.debug:container to confirm the registry service exists → debug:doctrine on the two entities to confirm mapping → check the association_type table is actually populated → only then look at your service code. The failure is almost always missing reference data, not broken wiring.sylius.association_type.class, sylius.association.class). Both survive minor upgrades. Subclassing bundle internals does not.sylius/association into your own bundle: one service definition for the registry, one doctrine.orm.mappings entry pointing at the component's XML. That is the entire content of this repo, and you get a dependency you can actually patch.How can I help you explore Laravel packages today?