Problem/Motivation

We did two tasks for creating and improving to generate bundles from combinations of products.
Still we lack some flexibility which would be per project required - so we essentially need event subscriber where we would be able to alter number of combinations, fill maybe some data during creation, etc..

Steps to reproduce

Proposed resolution

Add event subscriber to react prior bundle variations are created - to unset some combinations, to fill some data
Move perhaps some of the logic in dedicated service so that would allow more re-usability of this logic, outside of current form and people can build their own UI interface and use that service

Remaining tasks

User interface changes

API changes

Data model changes

CommentFileSizeAuthor
#3 Screenshot From 2026-08-26 16-10-11.png142.71 KBvalic
Command icon Show commands

Start within a Git clone of the project using the version control instructions.

Or, if you do not have SSH keys set up on git.drupalcode.org:

Comments

valic created an issue. See original summary.

valic’s picture

StatusFileSize
new142.71 KB

Updated UI

bundles

valic’s picture

Follow-up on the bulk generation form. Three things this adds: the generation
becomes callable without the form, subscribers get to decide which combinations
are worth creating, and the generated titles become configurable.

1. Generation extracted into a service

BundleVariationGenerator, its interface and a small
BundleVariationGenerationResult, registered as
commerce_variation_bundle.variation_generator with an interface
alias. Nothing in it touches form state, so a drush command, a migration, a
queue worker or a different UI can generate bundles without going through
GenerateBundleVariationsForm.

buildCombinations(array $selection): array
getCombinations(ProductInterface $product, array $selection, array $options = []): array
generate(ProductInterface $product, array $selection, array $options = []): BundleVariationGenerationResult
isBundleProduct(ProductInterface $product): bool
getSourceProductTypeIds(): array

$options accepts variation_type,
template, fields, sku and
title. The variation type is derived from the product when it is
left out. SKU building gained prefix, separator and
source (sku or id) on top of what the
form had.

The form is 127 lines shorter and delegates to the service.
buildSku() and cartesianProduct() moved into it.

2. Two events

Event When What a subscriber can do
GENERATE_VARIATIONS Once, after the combinations are built and before anything is created Alter or filter the whole list
GENERATE_VARIATION Per combination, entities built but not yet saved Fill in field values, or skip()

Which combinations are worth selling is a question about the catalogue, and
the module cannot answer it. Combining four products of 4, 11, 10 and 3
variations offers 1320 combinations; on the site this was written for, roughly
240 of those are real products and the rest pair sizes, pack counts or market
variants that do not go together. Removing them here is much cheaper than
creating variations and deleting them afterwards.

Two details worth calling out:

  • Bundle item creation moved to before the save, so
    skip() leaves no orphaned commerce_bundle_item
    entities behind.
  • setCombinations() reindexes its argument, so a subscriber can
    use array_filter() without renumbering the result.

getCombinations() is the method that dispatches, and the form's
combination count goes through it. Without that the form previewed and
limit-checked the raw cartesian product: on a real selection it reported 960
combinations and refused the run against the 500 limit, while only 288 would
have been created. A UI counting combinations should use
getCombinations(); buildCombinations() is the plain
arithmetic. Subscribers therefore run on every preview and are expected to be
cheap and free of side effects.

3. Configurable generated titles

Both title formats were hardcoded —
sprintf('%sx %s', ...) in BundleItem::generateTitle()
and implode(' / ', ...) in
VariationBundle::generateTitle(). They are now configuration, on
the config entity that owns the title each one formats:

Setting Lives on Edited at Default
titlePattern commerce_bundle_item_type, as a property Bundle item type form @quantityx @title
title_separator commerce_product_variation_type, as a third-party setting Variation type form &

Exposed as BundleItemTypeInterface::DEFAULT_TITLE_PATTERN and
VariationBundleInterface::DEFAULT_TITLE_SEPARATOR.

A bundle item cannot look up the variation type that holds it — there
is no back-reference — so the item pattern has to live on the item type
rather than alongside the separator. That is what forces the split; it is not a
preference.

The generate form can override both for a single run, but only when the
variation type does not generate titles for itself. A type that does has its
title recomputed from the type on every save, so an override could not survive;
that case shows the resulting format read-only and links to the variation type
instead.

4. Form changes

  • Bundle products are kept out of the source autocomplete with
    target_bundles. #validate_reference is set to
    FALSE alongside it, so a product typed in full is refused by
    addProduct() with a reason — it is a bundle, or it is this
    very product — rather than by core with "The referenced entity does not
    exist", which is both untrue and unhelpful.
  • Both SKU options now default to on.
  • With title generation ticked, the title field is hidden and its
    #required cleared. Core validates required elements before any
    #validate handler runs, so hiding it with #states
    alone left the form unsubmittable.

5. Update hook

commerce_variation_bundle_update_10001() — the module's
first, the installed schema was 8000. It adds the default
titlePattern to bundle item types created before the property
existed, skipping any that already carry one, so it is safe to re-run.

valic’s picture

Status: Active » Fixed

Now that this issue is closed, review the contribution record.

As a contributor, attribute any organization that helped you, or if you volunteered your own time.

Maintainers, credit people who helped resolve this issue.

Status: Fixed » Closed (fixed)

Automatically closed - issue fixed for 2 weeks with no activity.