Bundle-Produkte in Magento 2: Optionen, Preislogik und Lagerverknüpfung
AI generated
M2
di.xml
Magento 2 · Bundle-Produkte · Hyvä · PHP 8.4
Bundle-Produkte in Magento 2
Optionen, Preislogik und Lagerverknüpfung im Detail

Bundle-Produkte gehören zu den komplexesten Produkttypen in Magento 2: Sie kombinieren ein eigenes Tabellenmodell aus Optionen und Selections, zwei grundverschiedene Preislogiken für Fixed und Dynamic Pricing, eine mehrstufige Lagerverknüpfung über Multi Source Inventory und einen eigenen Preisindex. Dieser Beitrag erklärt Datenmodell, Preisberechnung, Lagerverknüpfung, Hyvä-Frontend und Performance-Strategien für Bundle-Produkte anhand von echtem PHP 8.4 Code, Service Contracts und deklariertem Schema.

18 Min. Lesezeit Bundle-Produkte · Preisindex · MSI · Hyvä · Alpine.js Magento 2.4.8-p4 · PHP 8.4

1. Was Bundle-Produkte in Magento 2 wirklich lösen

Bundle-Produkte lösen ein konkretes Problem: Ein Kunde soll aus mehreren unabhängigen Komponenten eine eigene, individuell konfigurierte Zusammenstellung kaufen, wobei jede Komponente ein eigenständiges, separat lagergeführtes Produkt bleibt. Typische Einsatzfälle sind ein PC-Konfigurator, bei dem Prozessor, Arbeitsspeicher und Gehäuse frei kombinierbar sind, oder ein Geschenkset, bei dem der Kunde zwischen mehreren Duftrichtungen und Verpackungsgrößen wählt. Der entscheidende Unterschied zu einem einfachen Produkt mit Zusatzoptionen liegt darin, dass jede Auswahl in einem Bundle auf ein reales Katalogprodukt mit eigener SKU, eigenem Lagerbestand und eigener Preislogik verweist.

Gegenüber dem Configurable Product besteht der Unterschied darin, dass Configurable Varianten desselben Produkts abbildet, zum Beispiel Größe und Farbe eines T-Shirts, wobei am Ende genau ein Simple Product verkauft wird. Bundle-Produkte hingegen verkaufen mehrere unterschiedliche Produkte gleichzeitig, gruppiert in Optionen mit eigenen Auswahlregeln wie Pflichtfeld, Mehrfachauswahl oder änderbarer Menge. Gegenüber dem Grouped Product, das lediglich eine feste Liste von Produkten auf einer gemeinsamen Seite anbietet, ohne dass der Kunde etwas auswählt, bringen Bundles echte Entscheidungslogik und eigene Preisberechnung mit.

Wann man auf Bundle-Produkte verzichten sollte: Wenn es nur um Variantenauswahl geht, ist ein Configurable Product die richtige und deutlich einfacher zu indexierende Lösung. Wenn immer dieselbe feste Produktkombination verkauft werden soll, ohne dass der Kunde etwas ändert, reicht ein Grouped Product oder ein eigenes Kit-Simple-Product mit fixer Stückliste. Die zusätzliche Komplexität aus Options-Tabellen, Preisindex und MSI-Verknüpfung sollte durch echten Konfigurationsbedarf gerechtfertigt sein, sonst entsteht unnötiger Wartungsaufwand ohne Mehrwert für den Kunden.

2. Datenmodell: bundle_option, bundle_option_value, bundle_selection

Technisch ist "bundle" ein eigener Product-Entity-Type in Magento 2, gespeichert im Feld type_id von catalog_product_entity, implementiert über Magento\Bundle\Model\Product\Type als Erweiterung von Magento\Catalog\Model\Product\Type\AbstractType. Anders als reine EAV-Attribute liegen die konfigurierbaren Bestandteile eines Bundle-Produkts in drei dedizierten Tabellen, die nicht Teil des generischen EAV-Systems sind, sondern klassische relationale Tabellen mit Fremdschlüsseln: catalog_product_bundle_option für die Optionsdefinition, catalog_product_bundle_option_value für die store-view-spezifischen Optionstitel und catalog_product_bundle_selection für die eigentlichen wählbaren Kindprodukte.

Die Option definiert Typ (select, radio, checkbox, multi), Pflichtstatus und Position, referenziert über parent_id das Bundle-Produkt in catalog_product_entity. Die Selection referenziert über option_id die zugehörige Option und über product_id ein beliebiges, unabhängiges Simple Product, ergänzt um selection_price_type, selection_price_value, selection_qty und selection_can_change_qty. Genau diese Trennung macht Bundle-Produkte mächtig: Ein Simple Product kann gleichzeitig in mehreren Bundles als Selection auftauchen, ohne dupliziert zu werden, und behält seinen eigenen Lagerbestand und seine eigene Preispflege.

Ein vereinfachter Ausschnitt des deklarativen Schemas zeigt die zentralen Spalten und Fremdschlüssel, wie sie in db_schema.xml von Magento_Bundle definiert sind:


<?xml version="1.0"?>
<schema xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
        xsi:noNamespaceSchemaLocation="urn:magento:framework:Setup/Declaration/Schema/etc/schema.xsd">

    <table name="catalog_product_bundle_option" resource="default" engine="innodb" comment="Catalog Product Bundle Option">
        <column xsi:type="int" name="option_id" unsigned="true" nullable="false" identity="true" comment="Option ID"/>
        <column xsi:type="int" name="parent_id" unsigned="true" nullable="false" identity="false" comment="Parent Product ID"/>
        <column xsi:type="boolean" name="required" nullable="false" default="1" comment="Required"/>
        <column xsi:type="varchar" name="type" nullable="false" length="255" comment="select, radio, checkbox, multi"/>
        <column xsi:type="int" name="position" unsigned="true" nullable="false" default="0" comment="Position"/>
        <constraint xsi:type="primary" referenceId="PRIMARY">
            <column name="option_id"/>
        </constraint>
        <constraint xsi:type="foreign" referenceId="CAT_PRD_BNDL_OPT_PARENT_ID_CAT_PRD_ENTT_ENTT_ID"
                    table="catalog_product_bundle_option" column="parent_id"
                    referenceTable="catalog_product_entity" referenceColumn="entity_id" onDelete="CASCADE"/>
    </table>

    <table name="catalog_product_bundle_selection" resource="default" engine="innodb" comment="Catalog Product Bundle Selection">
        <column xsi:type="int" name="selection_id" unsigned="true" nullable="false" identity="true" comment="Selection ID"/>
        <column xsi:type="int" name="option_id" unsigned="true" nullable="false" identity="false" comment="Option ID"/>
        <column xsi:type="int" name="parent_product_id" unsigned="true" nullable="false" identity="false" comment="Parent Product ID"/>
        <column xsi:type="int" name="product_id" unsigned="true" nullable="false" identity="false" comment="Child Product ID"/>
        <column xsi:type="decimal" name="selection_price_value" scale="4" precision="12" nullable="true" comment="Selection Price Value"/>
        <column xsi:type="smallint" name="selection_price_type" unsigned="true" nullable="true" comment="0 = percent, 1 = fixed"/>
        <column xsi:type="decimal" name="selection_qty" scale="4" precision="12" nullable="true" default="1.0000" comment="Selection Qty"/>
        <column xsi:type="boolean" name="selection_can_change_qty" nullable="false" default="1" comment="Selection Can Change Qty"/>
        <column xsi:type="boolean" name="is_default" nullable="false" default="0" comment="Is Default"/>
        <constraint xsi:type="primary" referenceId="PRIMARY">
            <column name="selection_id"/>
        </constraint>
    </table>

</schema>

3. Preislogik: Fixed vs. Dynamic Pricing im Detail

Die gesamte Preisberechnung von Bundle-Produkten hängt am Attribut price_type, das genau zwei Zustände kennt: 0 für Dynamic und 1 für Fixed. Bei Dynamic Pricing trägt das Bundle-Produkt selbst keinen sinnvollen Basispreis, der Endpreis ergibt sich ausschließlich aus der Summe der aktuell ausgewählten Selections, deren Einzelpreis wiederum vom jeweils referenzierten Simple Product übernommen oder über selection_price_type und selection_price_value prozentual beziehungsweise fix angepasst wird. Bei Fixed Pricing dagegen trägt das Bundle-Produkt einen eigenen, festen Grundpreis, zu dem die Selections lediglich Aufschläge oder Abschläge beitragen, unabhängig vom Katalogpreis des referenzierten Kindprodukts. Verantwortlich für diese Verzweigung ist Magento\Bundle\Model\Product\Price, das je nach price_type komplett unterschiedliche Berechnungspfade für getPrice() und getFinalPrice() durchläuft.

Ein zweites, unabhängiges Attribut namens price_view steuert nur die Darstellung im Katalog und auf der Produktdetailseite, nicht die eigentliche Berechnung: Der Wert 0 zeigt "ab" an, also nur den niedrigstmöglichen Gesamtpreis über alle Pflichtoptionen mit ihrer günstigsten Selection, während Wert 1 eine Preisspanne von Minimum bis Maximum ausgibt. Diese Unterscheidung wirkt sich direkt auf den Preisindex aus, weil für "ab"-Preise nur der Minimalwert vorberechnet werden muss, für Preisspannen dagegen zusätzlich der Maximalwert über alle möglichen Kombinationen.

Im Checkout und in der Konfigurationsvorschau übernimmt eine Kette aus Magento\Bundle\Pricing\Price-Klassen wie BundleOptionPrice und ConfiguredPrice die konkrete Berechnung für die tatsächlich vom Kunden gewählte Kombination, im Unterschied zur katalogweiten Vorschau, die alle theoretisch möglichen Kombinationen berücksichtigen muss. Steuerklassen können pro Kindprodukt unterschiedlich sein, was bei Bundle-Produkten mit Fixed Pricing zu einer anteiligen Steueraufteilung auf Basis der einzelnen Selection-Preise führt, nicht auf Basis eines einzigen Steuersatzes für das gesamte Bundle.

4. Optionen und Selections programmatisch anlegen

Für die programmatische Pflege von Bundle-Produkten stellt Magento Service Contracts bereit, die Preferences und direkten Resource-Model-Zugriff überflüssig machen: Magento\Bundle\Api\ProductOptionRepositoryInterface zum Speichern von Optionen, Magento\Bundle\Api\Data\OptionInterfaceFactory zum Erzeugen neuer Optionsobjekte sowie Magento\Bundle\Api\ProductLinkManagementInterface zum Hinzufügen einzelner Selections zu einer bestehenden Option. Kombiniert mit Magento\Catalog\Api\ProductRepositoryInterface zum Laden des Bundle-Produkts entsteht ein sauberer, testbarer Ablauf ohne direkte SQL-Zugriffe auf die zuvor beschriebenen Tabellen.

Das folgende Beispiel nutzt PHP 8.4 mit Constructor Property Promotion und strict_types, wie es der Coding-Standard für Mironsoft-Module vorschreibt, und legt eine Pflichtoption vom Typ "select" mit zwei Selections an:


<?php

declare(strict_types=1);

namespace Mironsoft\CatalogSetup\Service;

use Magento\Bundle\Api\Data\LinkInterfaceFactory;
use Magento\Bundle\Api\Data\OptionInterfaceFactory;
use Magento\Bundle\Api\ProductLinkManagementInterface;
use Magento\Bundle\Api\ProductOptionRepositoryInterface;
use Magento\Catalog\Api\Data\ProductInterface;
use Magento\Catalog\Api\ProductRepositoryInterface;
use Magento\Framework\Exception\InputException;
use Magento\Framework\Exception\NoSuchEntityException;

/**
 * Creates a bundle option with selections on an existing bundle product via Service Contracts.
 */
final class BundleOptionBuilder
{
    /**
     * @param ProductRepositoryInterface $productRepository Loads the target bundle product.
     * @param OptionInterfaceFactory $optionFactory Factory for bundle option data objects.
     * @param LinkInterfaceFactory $linkFactory Factory for bundle selection (link) data objects.
     * @param ProductOptionRepositoryInterface $optionRepository Persists the bundle option.
     * @param ProductLinkManagementInterface $linkManagement Persists selections for an option.
     */
    public function __construct(
        private readonly ProductRepositoryInterface $productRepository,
        private readonly OptionInterfaceFactory $optionFactory,
        private readonly LinkInterfaceFactory $linkFactory,
        private readonly ProductOptionRepositoryInterface $optionRepository,
        private readonly ProductLinkManagementInterface $linkManagement,
    ) {
    }

    /**
     * Adds one required select option with the given child selections to a bundle product.
     *
     * @param string $bundleSku SKU of the existing bundle product.
     * @param string $title Storefront title of the new option.
     * @param array<int, array{sku: string, priceType: int, priceValue: float, qty: float, isDefault: bool}> $selections Child products.
     * @return void
     * @throws NoSuchEntityException
     * @throws InputException
     */
    public function addSelectOption(string $bundleSku, string $title, array $selections): void
    {
        /** @var ProductInterface $bundle */
        $bundle = $this->productRepository->get($bundleSku, true);

        $option = $this->optionFactory->create();
        $option->setTitle($title);
        $option->setType('select');
        $option->setRequired(true);
        $option->setPosition(0);
        $option->setSku($bundleSku);

        $option = $this->optionRepository->save($bundle, $option);

        foreach ($selections as $position => $data) {
            $link = $this->linkFactory->create();
            $link->setSku($data['sku']);
            $link->setOptionId((int) $option->getOptionId());
            $link->setPriceType($data['priceType']);
            $link->setPrice($data['priceValue']);
            $link->setQty($data['qty']);
            $link->setIsDefault($data['isDefault']);
            $link->setPosition($position);
            $link->setCanChangeQuantity(true);

            // addChild persists the row in catalog_product_bundle_selection
            $this->linkManagement->addChild($bundle, (string) $option->getOptionId(), $link);
        }
    }
}

Wichtig bei diesem Ansatz: ProductRepositoryInterface::get() muss mit dem zweiten Parameter true aufgerufen werden, um das Produkt im Editier-Modus zu laden, sonst schlägt das anschließende Speichern der Option fehl. Für Massenimporte vieler Bundle-Produkte empfiehlt sich, den Produktcache und den Preisindex bewusst im Schedule-Modus zu betreiben, damit nicht nach jedem einzelnen save()-Aufruf ein vollständiger Reindex ausgelöst wird, siehe Abschnitt 8.

5. Lagerverknüpfung: MSI, Stock Reservations und Salable Quantity

Die Verknüpfung zwischen einer Bundle-Selection und dem tatsächlichen Lagerbestand läuft über Magento\Bundle\Model\LinkManagement, dessen Methoden saveChild und getChildren die Zeilen in catalog_product_bundle_selection verwalten. Wichtig ist: Das Bundle-Produkt selbst führt in der Regel keinen eigenen physischen Lagerbestand, sondern wird mit deaktiviertem "Manage Stock" konfiguriert. Die tatsächliche Verfügbarkeit ergibt sich als abgeleiteter Zustand aus den Lagerbeständen aller Kindprodukte, die in den aktuell erforderlichen Optionen ausgewählt werden können.

Unter Multi Source Inventory wird die verkaufbare Menge jeder einzelnen Selection über Magento\InventorySalesApi\Api\GetProductSalableQtyInterface je Source und Stock ermittelt, exakt wie bei jedem anderen Simple Product auch. Für Bundle-Produkte bedeutet das: Damit das Bundle als verkaufbar gilt, muss in jeder Pflichtoption mindestens eine Selection eine positive Salable Quantity aufweisen, bei Optionen vom Typ checkbox oder multi mit mehreren gleichzeitig wählbaren Kindprodukten reicht eine verfügbare Selection aus der Gruppe. Diese Kombinationslogik wird nicht in den MSI-Kerntabellen selbst abgebildet, sondern in der Bundle-spezifischen Verfügbarkeitsprüfung berechnet, die auf die Ergebnisse der Simple-Product-Prüfungen aufsetzt.

Beim Kauf entstehen Reservierungen ausschließlich für die Kindprodukte, nicht für das Bundle selbst: Für jede gekaufte Selection legt Magento einen Eintrag in inventory_reservation an, dessen Menge sich aus der Bundle-Bestellmenge multipliziert mit selection_qty der jeweiligen Auswahl ergibt. Das bedeutet auch, dass Backorders, Mindestbestellmengen und Lagerortpriorisierung ausschließlich auf Ebene der Kindprodukte konfiguriert werden, das Bundle-Produkt selbst kennt in diesem Zusammenhang keine eigene Stock-Konfiguration jenseits der reinen Sichtbarkeitssteuerung.

6. Frontend-Rendering: Bundle-Optionen im Hyvä-Theme

Im Hyvä-Theme entfällt das klassische Luma-Pattern aus bundle.js und Knockout-Templates vollständig, stattdessen übernimmt Alpine.js die komplette clientseitige Zustandsverwaltung für die gewählten Optionen und die Live-Neuberechnung des Gesamtpreises. Die Preisdaten aller Selections werden serverseitig im phtml-Template über $block->getJsonConfig() als JSON in das x-data-Attribut geschrieben, wodurch kein separates JavaScript-Bundle für die Preislogik von Bundle-Produkten geladen werden muss und kein jQuery und kein Knockout.js im Spiel sind.

Das folgende Template zeigt die Grundstruktur: Für jede Option wird abhängig vom Typ ein Radio-Button oder eine Checkbox gerendert, der ausgewählte Wert landet über x-model direkt im Alpine-State, ein berechneter Getter total summiert Basispreis und alle aktuell gewählten Selection-Preise:


<?php
/** @var \Magento\Catalog\Block\Product\View\Type\Bundle $block */
/** @var \Magento\Framework\Escaper $escaper */
?>
<div x-data="{
        selected: {},
        prices: <?= /* @noEscape */ $block->getJsonConfig() ?>,
        basePrice: <?= (float) $block->getProduct()->getFinalPrice() ?>,
        get total() {
            return this.basePrice + Object.values(this.selected)
                .reduce((sum, id) => sum + (this.prices[id] || 0), 0);
        },
        formatPrice(value) {
            return new Intl.NumberFormat('de-DE', { style: 'currency', currency: 'EUR' }).format(value);
        }
     }"
     class="bundle-options"
>
    <?php foreach ($block->getOptions() as $option): ?>
        <div class="mb-6">
            <p class="font-semibold mb-2"><?= $escaper->escapeHtml($option->getTitle()) ?></p>
            <?php foreach ($option->getSelections() as $selection): ?>
                <label class="flex items-center gap-2 mb-1">
                    <input
                        type="radio"
                        name="bundle_option_<?= (int) $option->getOptionId() ?>"
                        value="<?= (int) $selection->getSelectionId() ?>"
                        x-model="selected[<?= (int) $option->getOptionId() ?>]"
                    >
                    <span><?= $escaper->escapeHtml($selection->getName()) ?></span>
                    <span class="text-sm text-gray-500" x-text="formatPrice(prices[<?= (int) $selection->getSelectionId() ?>])"></span>
                </label>
            <?php endforeach; ?>
        </div>
    <?php endforeach; ?>

    <p class="text-lg font-bold" x-text="formatPrice(total)"></p>
</div>

Solange die gesamte Logik als Ausdruck in x-data Platz findet, ist kein zusätzlicher Inline-Script-Block nötig. Wächst die Berechnung für komplexere Bundle-Produkte darüber hinaus, etwa bei gestaffelten Mengenrabatten pro Selection, wird die Logik in eine eigene Alpine.data()-Komponente ausgelagert, entweder als separate JS-Datei im Theme oder als Inline-Script-Block im Template. In letzterem Fall ist unmittelbar nach dem schließenden </script>-Tag der Aufruf $hyvaCsp->registerInlineScript() zwingend erforderlich, damit die Content Security Policy den Block nicht blockiert.

7. Checkout und Warenkorb: Bundle-Selections als Buy Request

Wenn ein Kunde ein Bundle-Produkt in den Warenkorb legt, werden alle getroffenen Auswahlen in einem sogenannten Buy Request gebündelt, einem assoziativen Array, das als serialisierte Option unter dem Schlüssel info_buyRequest am Quote Item hängt. Der Schlüssel bundle_option enthält je Options-ID entweder eine einzelne Selection-ID bei select oder radio, oder ein Array von Selection-IDs bei checkbox und multi. Der optionale Schlüssel bundle_option_qty überschreibt die Standardmenge einer Selection, aber nur wenn selection_can_change_qty für diese Auswahl aktiviert ist.

Diese Struktur wird direkt von Magento\Bundle\Model\Product\Type::processConfiguration() ausgewertet, um aus dem Buy Request konkrete Quote-Item-Kinder zu erzeugen. Ein realistischer Buy Request für ein Bundle-Produkt mit drei Optionen, davon eine als Mehrfachauswahl, sieht so aus:


{
    "product": 1234,
    "selected_configurable_option": "",
    "related_product": "",
    "qty": "1",
    "bundle_option": {
        "1": "5",
        "2": ["8", "9"],
        "3": "12"
    },
    "bundle_option_qty": {
        "3": "2"
    }
}

Beim Abschluss der Bestellung entsteht pro gewähltem Kindprodukt ein eigener Order Item vom Typ "simple", der über parent_item_id auf den übergeordneten Order Item vom Typ "bundle" verweist. Die Menge jedes Kind-Order-Items berechnet sich aus der bestellten Bundle-Menge multipliziert mit selection_qty beziehungsweise dem überschriebenen Wert aus bundle_option_qty. Zusätzlich speichert Magento in der serialisierten Spalte product_options von sales_order_item einen kompletten Snapshot aus Preis, Menge und Gewicht zum Bestellzeitpunkt, damit spätere Änderungen an den Bundle-Produkte-Definitionen oder an einzelnen Selections die historischen Bestelldaten nicht verändern.

8. Performance: Bundle-Preisindex und Reindex-Strategien

Für Bundle-Produkte existieren zwei dedizierte Preisindex-Tabellen: catalog_product_bundle_price_index speichert vorberechnete Minimal- und Maximalpreise je Kundengruppe und Website, catalog_product_bundle_selection_price_index hält die vorberechneten Preise der einzelnen Selections innerhalb dieser Kombinationen. Der Grund für diese Vorberechnung: Ohne Index müsste jede Katalogseite und jede Produktdetailseite die komplette Preismatrix aus allen Pflichtoptionen und deren günstigsten beziehungsweise teuersten Selections zur Laufzeit neu berechnen, was bei vielen Optionen mit jeweils vielen Selections kombinatorisch schnell teuer wird.

Ein praktischer Hinweis für Shops mit vielen Optionskombinationen: Fixed Pricing benötigt für die Indexierung deutlich weniger Rechenaufwand als Dynamic Pricing, weil beim festen Grundpreis nur die Aufschläge der Selections in die Min/Max-Berechnung einfließen, während Dynamic Pricing für jede denkbare Kombination die vollständige Summe neu bilden muss. Für Bundle-Produkte mit sehr vielen Pflichtoptionen und jeweils zweistelliger Selection-Anzahl empfiehlt sich daher, wo fachlich vertretbar, auf Fixed Pricing umzustellen, oder die Anzahl gleichzeitig indexierter Kombinationen bewusst zu begrenzen.

Für den laufenden Betrieb sollte der Indexer catalog_product_price nicht im Modus "Bei Speichern aktualisieren" laufen, sondern über bin/magento indexer:set-mode schedule catalog_product_price auf den Cron-basierten Schedule-Modus umgestellt werden, damit ein einzelner Produkt-Save mit vielen Selections nicht den Admin-Request blockiert. Wer die Preisberechnung einer einzelnen Selection zusätzlich um eigene Geschäftsregeln erweitern will, etwa Staffelrabatte je Kundengruppe, setzt einen Plugin an Magento\Bundle\Model\Product\Price statt die Klasse per Preference zu ersetzen:


<?xml version="1.0"?>
<config xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
        xsi:noNamespaceSchemaLocation="urn:magento:framework:ObjectManager/etc/config.xsd">

    <type name="Magento\Bundle\Model\Product\Price">
        <plugin name="mironsoft_custom_bundle_selection_price"
                type="Mironsoft\CatalogSetup\Plugin\CustomBundleSelectionPricePlugin"
                sortOrder="10"/>
    </type>

</config>

Nach einem größeren Import oder einer Massenänderung an Bundle-Produkten lohnt sich zusätzlich ein Blick auf bin/magento indexer:status, um zu prüfen, ob der Preisindex noch "Invalidiert" anzeigt und ein manueller Reindex mit bin/magento indexer:reindex catalog_product_price sinnvoll ist, bevor Storefront-Preise sichtbar veraltet sind.

9. Bundle-Produkte im Vergleich: Fixed vs. Dynamic vs. andere Produkttypen

Die Entscheidung zwischen den beiden Pricing-Varianten von Bundle-Produkten und den übrigen konfigurierbaren Produkttypen in Magento 2 hat direkte Auswirkungen auf Preisberechnung, Lagerverknüpfung, Indexierungsaufwand und den passenden Einsatzzweck. Die folgende Tabelle stellt die vier wichtigsten Varianten gegenüber.

Dimension Bundle Fixed Bundle Dynamic Configurable Grouped
Preisberechnung Fester Grundpreis plus Selection-Aufschläge Summe der gewählten Selection-Preise Preis der gewählten Variante (Simple Product) Summe unabhängiger Einzelpreise
Lagerverknüpfung Über Selections an Kindprodukte Über Selections an Kindprodukte Direkt am gewählten Simple Product Direkt an jedem verlinkten Produkt
Such-Indexierung Min/Max über Aufschläge, moderater Aufwand Min/Max über volle Kombinationssumme, hoher Aufwand Min/Max über Variantenpreise, geringer Aufwand Kein Preisindex, jedes Produkt einzeln indexiert
Einsatzzweck Konfigurator mit stabilem Grundpreis Konfigurator mit variablem Gesamtpreis Varianten desselben Produkts Feste Produktgruppe ohne Auswahl

In der Praxis entscheidet meist der Grad an Preisflexibilität, den der Kunde sehen soll: Ein PC-Konfigurator mit stark schwankenden Komponentenpreisen passt zu Dynamic Pricing, ein Geschenkset mit festem Verkaufspreis und kleinen Aufpreisen für Premium-Varianten passt besser zu Fixed Pricing. Bundle-Produkte sollten nie als Ersatz für einfache Variantenauswahl eingesetzt werden, dafür ist und bleibt Configurable die schlankere und besser indexierbare Lösung.

10. Zusammenfassung

Bundle-Produkte in Magento 2 lösen ein klar abgrenzbares Problem: unabhängige, eigenständig lagergeführte Produkte zu einer vom Kunden konfigurierbaren Einheit zusammenzufassen, mit eigener Preislogik je nach Fixed- oder Dynamic-Pricing-Modus. Das Datenmodell aus Option, Option-Value und Selection trennt Optionsstruktur, store-view-spezifische Titel und die eigentliche Produktverknüpfung sauber voneinander. Die Preisberechnung hängt vollständig am Attribut price_type, während price_view nur die Darstellung als "ab"-Preis oder Preisspanne steuert.

Die Lagerverknüpfung läuft konsequent über die Kindprodukte, das Bundle selbst führt keinen eigenen physischen Bestand, Reservierungen entstehen ausschließlich für die gewählten Selections. Im Hyvä-Frontend übernimmt Alpine.js die komplette Live-Preisberechnung ohne jQuery und ohne Knockout, im Checkout landet die Auswahl als strukturierter Buy Request am Quote Item. Für Performance bei vielen Optionskombinationen sind der dedizierte Bundle-Preisindex und ein Cron-basierter Schedule-Modus des Preisindexers entscheidend, damit Bundle-Produkte auch bei komplexen Konfigurationen schnell bleiben.

Bundle-Produkte in Magento 2: Das Wichtigste auf einen Blick

Datenmodell

catalog_product_bundle_option, _option_value und _selection trennen Optionsstruktur, Titel und Produktverknüpfung sauber voneinander.

Preislogik

price_type entscheidet Fixed vs. Dynamic, price_view steuert nur "ab"-Preis oder Preisspanne in der Darstellung.

Lagerverknüpfung

MSI berechnet Salable Quantity je Selection, Reservierungen entstehen nur für Kindprodukte, nie für das Bundle selbst.

Performance

Dedizierter Preisindex, Fixed Pricing bei vielen Kombinationen bevorzugen, Indexer im Schedule-Modus betreiben.

11. FAQ: Bundle-Produkte in Magento 2

1Was sind Bundle-Produkte in Magento 2?
Ein eigener Produkttyp, bei dem der Kunde aus mehreren Optionen jeweils ein oder mehrere unabhängige, eigenständig lagergeführte Produkte auswählt, etwa bei einem PC-Konfigurator oder Geschenkset.
2Unterschied zu Configurable-Produkten?
Configurable bildet Varianten desselben Produkts ab, am Ende wird ein Simple Product verkauft. Bundle verkauft mehrere unterschiedliche Produkte gleichzeitig, gruppiert in Optionen.
3Welche Tabellen speichern Optionen und Selections?
catalog_product_bundle_option für die Optionsdefinition, catalog_product_bundle_option_value für Titel, catalog_product_bundle_selection für die wählbaren Kindprodukte.
4Was bedeutet price_type?
Steuert Fixed (1) versus Dynamic (0) Pricing. Fixed nutzt einen eigenen Grundpreis, Dynamic summiert ausschließlich die gewählten Selection-Preise.
5Wie legt man Optionen programmatisch an?
Über ProductOptionRepositoryInterface, OptionInterfaceFactory und ProductLinkManagementInterface, kombiniert mit ProductRepositoryInterface zum Laden im Editier-Modus.
6Wie funktioniert die Lagerverknüpfung unter MSI?
Jede Selection wird wie ein Simple Product über GetProductSalableQtyInterface geprüft. Das Bundle ist verkaufbar, wenn jede Pflichtoption mindestens eine verfügbare Selection hat.
7Wie rendert man Optionen im Hyvä-Theme?
Selection-Preise als JSON über getJsonConfig in x-data schreiben, Alpine.js übernimmt Auswahl und Live-Preisberechnung ohne jQuery und ohne Knockout.js.
8Wie werden Auswahlen im Warenkorb gespeichert?
Als Buy Request unter info_buyRequest, mit bundle_option je Options-ID und optional bundle_option_qty für änderbare Mengen.
9Welche Indextabellen sind zuständig?
catalog_product_bundle_price_index für Min/Max je Kundengruppe und Website, catalog_product_bundle_selection_price_index für die einzelnen Selection-Preise.
10Warum ist Fixed Pricing performanter?
Nur die Aufschläge fließen in die Min/Max-Berechnung ein. Dynamic Pricing muss für jede mögliche Kombination die volle Summe neu bilden, das ist bei vielen Selections spürbar teurer.

Mironsoft

Magento 2 Entwicklung, Hyvä-Theming und Performance-Optimierung

Bundle-Produkte, die zuverlässig rechnen und schnell laden?

Wir bauen und optimieren Bundle-Produkte in Magento 2: sauberes Datenmodell, korrekte MSI-Lagerverknüpfung, Hyvä-Frontend mit Alpine.js und ein Preisindex, der auch bei vielen Optionskombinationen performant bleibt.

Bundle-Setup

Optionen, Selections und Preislogik nach Anforderung modellieren und programmatisch pflegen

Hyvä-Frontend

Alpine.js-basierte Bundle-Auswahl mit Live-Preisberechnung, ohne jQuery und ohne Knockout

Performance-Audit

Preisindex-Analyse, Reindex-Strategie und Indexer-Modi für große Bundle-Kataloge