Shopware 6 Plugin zur automatischen Positionstrennung im Warenkorb bei unterschiedlichen Kundeneingaben.
Wenn ein Kunde dasselbe Produkt mehrfach mit unterschiedlichen Eingaben (z. B. verschiedene Längen, Handlaufträger, Endkappen) in den Warenkorb legt, erzeugt dieses Plugin separate Warenkorbpositionen. Identische Eingaben erhöhen die Menge der bestehenden Position.
- Erkennt TMMS-Kundeneingaben (TmmsProductCustomerInputs) automatisch
- Verschiedene Eingabewerte -> separate Warenkorbpositionen
- Gleiche Eingabewerte -> Mengenerhöhung
- Sichert Eingabewerte pro Position im LineItem-Payload (payload-basiert, nicht nur Session)
- Liest Eingaben bevorzugt aus dem Request-Payload (Hidden-Felder, vom JS injiziert), mit Fallback auf TMMS-Session-Daten
- Korrigiert die TMMS "Eingabe prüfen"-Anzeige im Warenkorb: Split-Positionen zeigen die korrekten Werte aus dem Payload statt den Session-Wert
- Korrigiert Bestelldaten bei Bestellabschluss (TMMS schreibt sonst die letzte Eingabe auf alle Positionen)
- Kompatibel mit RcDynamicPrice (Meter-Suffix wird in den Hash einbezogen)
- Shopware 6.7 oder 6.8
- PHP 8.2+
- TmmsProductCustomerInputs (aktiv und konfiguriert)
php bin/console plugin:refresh
php bin/console plugin:install --activate RcCartSplitter
bin/build-storefront.sh
php bin/console cache:clear-
JavaScript: Überwacht TMMS-Eingabefelder. Bei jeder Änderung wird ein Hash aller Eingabewerte berechnet und als LineItem-ID gesetzt. Verschiedene Hashes = verschiedene IDs = separate Positionen.
-
BeforeLineItemAddedEvent (CartInputCaptureSubscriber): Sammelt die Eingaben aller registrierten
CartInputProviderInterface-Implementierungen und speichert sie im LineItem-Payload. Standard-Provider istTmmsCartInputProvider: bevorzugt werden die Werte aus dem Request-Payload (Hidden-Felder, vom JS injiziert) gelesen, als Fallback dienen die TMMS-Session-Daten. Weitere Input-Plugins können einen eigenen Provider unter dem Tagrc_cart_splitter.input_providerregistrieren. -
CartPageLoadedEvent (CartDisplayCorrectionSubscriber): Korrigiert die TMMS "Eingabe prüfen"-Anzeige im Warenkorb. TMMS setzt die LineItem-Extensions aus der Session, die pro Produktnummer gespeichert ist – bei Split-Positionen steht dort immer der gleiche Wert. Dieser Subscriber überschreibt die Extensions mit den korrekten Werten aus dem Payload.
-
CheckoutOrderPlacedEvent (OrderInputCorrectionSubscriber): Korrigiert die custom_fields pro Bestellposition mit den gesicherten Payload-Daten. Ohne diese Korrektur würde TMMS die letzte Eingabe auf alle Positionen desselben Produkts schreiben. Der Schreibvorgang läuft als einzelnes Batch-CASE-WHEN-UPDATE in einer Transaktion, damit Bestellungen mit vielen Split-Positionen nicht in N Roundtrips zerfallen; DB-Fehler werden geloggt, brechen den Checkout aber nicht ab.
Das Plugin konsumiert generisch alle data-rc*Suffix-Attribute am <form> über _collectAllSuffixes() und mischt sie automatisch in den LineItem-ID-Hash. Die Event-Anmeldung ist seit v2.0.0 ebenfalls generisch: ein einziges Event rcSuffixChanged triggert die Re-Berechnung. Ein neues Suffix-Plugin braucht keine Code-Änderung an dieser Datei mehr — zwei Schritte reichen:
- Suffix-Wert am Form setzen:
form.dataset.rcMaterialSuffix = 'eiche';
- Nach jeder Änderung das generische Cart-Splitter-Event dispatchen:
form.dispatchEvent(new CustomEvent('rcSuffixChanged', { detail: { source: 'rcMaterial', suffix: 'eiche' }, }));
detail.sourceist Pflichtfeld (vendor-eindeutiger Plugin-Name, camelCase).detail.suffixist empfohlen; weitere Felder sind plugin-spezifisch und für Konsumenten unverbindlich. Plugins, die das Event auch selbst abhören, filtern eigene Dispatches perevent.detail?.source !== '<eigene-source>'(Self-Loop-Schutz).
Der Event-Name ist als statische Konstante CartSplitterPlugin.SUFFIX_CHANGED_EVENT exponiert; ein JS-Unit-Test in tests/Js/cart-splitter.test.mjs verankert den Vertrag. Plugin-spezifische Events (rc{Name}Changed) bleiben für Plugin-interne Listener weiterhin zulässig.
Warum der neutrale Name (rcSuffixChanged, nicht rcCartSplitter:suffixChanged): der Event gehört dem Protokoll, nicht einem einzelnen Plugin. Suffix-Plugins funktionieren so auch in Standalone-Setups ohne RcCartSplitter (No-op-Dispatch ins Leere) und sind nicht namenstechnisch an einen Konsumenten gekoppelt.
Keine eigene Konfiguration nötig. Das Plugin erkennt TMMS-Eingabefelder automatisch.
Der Plugin-Standard ist DAL (EntityRepository); zwei Stellen weichen bewusst auf DBAL aus:
OrderInputCorrectionService::batchUpdateCustomFields()schreibtcustom_fieldsper Batch-UPDATEdirekt in die Tabelleorder_line_item. DAL würde bei jedem Schreibvorgang einEntityWrittenEventfeuern, das TmmsProductCustomerInputs abfängt und unsere Korrektur sofort wieder mit dem Session-Wert überschreibt. DBAL umgeht den Event-Bus. Zusätzlich wird ein einzelnesCASE id WHEN ... THEN ... END-Statement in einer Transaktion abgesetzt, damit Bestellungen mit vielen Split-Positionen nicht in N Einzel-Roundtrips zerfallen.TmmsCartInputProvider::fetchProductNumber()liest dieproduct_numberper Native-SELECTaus derproduct-Tabelle. DAL würde die kompletteProductEntityinklusive Translations und Associations laden — proAddToCart-Request wäre das unnötig teuer.
Beide Stellen verwenden Parameter-Binding (Uuid::fromHexToBytes) und sind durch Unit- und Integration-Tests abgedeckt.
Seit dem 28. Juni 2025 verlangt das BFSG für B2C-Shops WCAG 2.2 AA. Dieses Plugin rendert nur einen kleinen Block unter dem Cart-LineItem; alles andere (Buy-Box, Mini-Cart-Region, Fokus-Stil) liegt beim Storefront-Theme.
- Semantische
<dl>/<dt>/<dd>-Struktur für Begriff-Wert-Paare statt<ul>/<li>/<strong>(WCAG 1.3.1 — Beziehungen) - Programmatischer Gruppenkontext über
aria-label="{{ 'rc-cart-splitter.lineItemInputs'|trans|sw_sanitize }}"an der<dl>(Snippets DE/EN untersrc/Resources/snippet/, WCAG 1.3.1) - Bootstrap-Token
text-body-secondarystatttext-muted(WCAG 1.4.3 — dokumentierter Kontrast) - Maximale Feldzahl zentral aus
TmmsConstants::INPUT_COUNT, kein Drift zwischen PHP/JS/Twig
text-body-secondary löst sich in Bootstrap 5.3 zu rgba(var(--bs-body-color-rgb), 0.75) auf. Mit dem Default --bs-body-color-rgb: 33, 37, 41 ergibt sich nach Alpha-Komposition über weißem Hintergrund die effektive Farbe rgb(88, 92, 94):
| Hintergrund | effektives Verhältnis | WCAG-AA (kleiner Text) |
|---|---|---|
#ffffff (Card-/Body-Default) |
6.76:1 | bestanden |
#f8f9fa (--bs-tertiary-bg) |
6.41:1 | bestanden |
- Kontrast ≥ 4.5:1 im aktiven Theme —
text-body-secondaryist Token-basiert, der finale Wert hängt vom Theme - Sichtbarer Fokus auf Buy-Form-Elementen (
:focus-visible) <html lang="de">(oder Sprach-Code des Storefronts)- Tastaturbedienbarkeit der Buy-Box inkl. TMMS-Eingabefelder
- Mini-Cart-Re-Render per AJAX: Container muss
aria-live="polite"sein, sonst meldet kein Screenreader die neue Eingabeliste
Dieses Plugin ist eine Brücke: Es repariert das Verhalten von TmmsProductCustomerInputs im
Warenkorb. Sobald RcCustomFields die Kundeneingaben selbst übernimmt, wird es überflüssig.
RcCustomFields bringt dafür bereits den Befehl rc-custom-fields:migrate-tmms mit (idempotent,
transaktional, mit Rücknahme).
Vor dem ersten Schritt lesen: Die Reihenfolge unten ist nicht beliebig. Wer dieses Plugin abschaltet, solange Positionen mit Kundeneingaben im Warenkorb eines Kunden liegen, nimmt diesen Positionen ihre Zuordnung — die Eingaben stehen dann nicht mehr an der richtigen Position.
-
Bestandsaufnahme. Wie viele Bestellungen tragen Positions-Eingaben dieses Plugins?
SELECT COUNT(*) AS positionen, COUNT(DISTINCT order_id) AS bestellungen, MAX(created_at) AS zuletzt FROM order_line_item WHERE JSON_EXTRACT(payload, '$.rcTmmsActive') IS NOT NULL;
Solange
zuletztin den letzten Tagen liegt, ist das Plugin im aktiven Einsatz. Der Marker heißtrcTmmsActive, die Werte selbst stehen daneben alsrcTmmsField<N>Value— auf dem Live-Spiegel gemessen;rc_tmms_inputskommt nur im Session-Rückfall vor und fehlt in den Bestellungen. -
RcCustomFields einrichten und die Produkt-Felder übernehmen:
php bin/console rc-custom-fields:migrate-tmms --dry-run # zeigt, was passieren würde php bin/console rc-custom-fields:migrate-tmms -
Ruhige Minute abwarten. Keine offenen Warenkörbe mit Kundeneingaben — die Abfrage aus Schritt 1 auf
cartstattorder_line_itemangewendet zeigt es; im Zweifel außerhalb der Geschäftszeiten umstellen. -
RcCartSplitter deaktivieren, Cache leeren, im Frontend gegenprüfen: Ein Artikel mit Kundeneingabe zweimal mit verschiedenen Werten in den Warenkorb — es müssen weiterhin zwei Positionen entstehen, jetzt von RcCustomFields.
-
Deinstallieren mit erhaltenen Daten:
php bin/console plugin:uninstall --keep-user-data RcCartSplitter
Die Positions-Payloads in bestehenden Bestellungen bleiben damit unangetastet. Sie sind Bestandsdaten: Was ein Kunde bestellt hat, muss nachlesbar bleiben.
-
Endgültig aufräumen erst nach der Aufbewahrungsfrist des Shops und nur, wenn keine Bestellung aus Schritt 1 mehr benötigt wird.
Bis einschließlich Schritt 4 ist der Weg ohne Datenverlust umkehrbar:
php bin/console rc-custom-fields:migrate-tmms --rollback
php bin/console plugin:activate RcCartSplitter
php bin/console cache:clearAb Schritt 5 ist die Rückkehr eine Neuinstallation — die Bestelldaten bleiben, die Plugin-Konfiguration ist neu zu setzen.
| Symptom | Bedeutung |
|---|---|
| Eine Position zeigt die Eingabe einer anderen | Beide Plugins waren gleichzeitig aktiv — eines abschalten |
| Zwei gleiche Artikel mit verschiedenen Eingaben landen in einer Position | Keines der beiden Plugins greift; Schritt 4 rückgängig machen |
| Eingaben fehlen in neuen Bestellungen, alte sind vollständig | Die Übernahme aus Schritt 2 lief nicht oder nicht für alle Produkte |
| Änderung | Befehl |
|---|---|
| Erstinstallation / JS-Änderung | bin/build-storefront.sh |
| Nur PHP-Änderung | php bin/console cache:clear |
composer test # Unit-Tests ausführen
composer test:js # JS-Unit-Tests für cart-splitter.plugin.js (Node ≥ 18, ohne npm-Dependencies)
composer phpstan # Statische Analyse (Level 8)
composer cs-check # Code-Style prüfen (PSR-12)
composer cs-fix # Code-Style automatisch korrigieren
composer lint:xml # services.xml und Co. auf well-formed prüfen (PHP-DOM)
composer lint:twig # Storefront-Templates über Twig-Lexer prüfen (Syntax)
composer coverage # PHPUnit mit Clover-Coverage-Report (coverage.xml)
composer coverage:gate # Aggregat-Coverage gegen Schwellen prüfen
composer quality # Alle Checks (cs-check + lint:xml + lint:twig + phpstan + test)composer coverage setzt einen aktiven Coverage-Treiber voraus (pcov empfohlen, alternativ xdebug mit XDEBUG_MODE=coverage). Aggregat-Coverage-Schwellen werden in bin/coverage-gate.php gepflegt:
src/Service/: ≥ 80 % Line-Coveragesrc/Subscriber/: ≥ 60 % Line-Coverage
CI ruft composer coverage und anschließend composer coverage:gate; ein Schwellen-Verstoß bricht den Build. Der Clover-Report wird als Workflow-Artefakt hochgeladen.
composer lint:twig arbeitet ohne Plattform-Boot und prüft daher nur die Twig-Syntax (Lexer-Stufe). Tag- und Filter-Existenz (sw_extends, sw_sanitize, sw_icon) wird gegen eine vollständige Shopware-Installation mit dem gebooteten Konsolen-Befehl gegenvalidiert:
bin/console lint:twig src/Resources/views
bin/console lint:xml src/Resources/configCI läuft automatisch bei Push und Pull Requests via GitHub Actions.
Die Storefront-Logik in cart-splitter.plugin.js ist über Node-eigene Test-Tools (node:test) abgedeckt. Keine npm-Dependencies, keine package.json — der Test-Runner liest die Quelldatei direkt ein, evaluiert sie gegen eine Plugin-Stub-Klasse und prüft _fnv32a, _computeId, _collectAllSuffixes, _cleanLabel und _getTmmsFieldLabel. Determinismus von FNV-1a ist über öffentliche Referenzwerte (z. B. 0xbf9cf968 für "foobar") belegt.
node --test tests/Js/cart-splitter.test.mjs
# oder:
composer test:jsCI führt diese Tests in einem eigenen Job (js-tests) bei jedem Push und Pull Request aus.
Tests in tests/Integration/ sichern den Korrektur-Pfad gegen eine echte Shopware-Test-Datenbank (DBAL-Batch-UPDATE auf order_line_item.custom_fields). Sie laufen ausschließlich in einer Plattform-Test-Umgebung mit gesetztem KERNEL_CLASS:
KERNEL_CLASS=Shopware\\Core\\Kernel vendor/bin/phpunit --testsuite=IntegrationOhne Bootstrap überspringen die Tests sich selbst — composer test führt nur die Unit-Suite aus.
Vollständige Versions-Historie: CHANGELOG_de-DE.md (deutsch) bzw. CHANGELOG_en-GB.md (englisch). Die Dateien folgen der Shopware-Plugin-Manager-Konvention und werden im Admin direkt angezeigt.
Rollback bei fehlgeschlagenem Update: Downgrade über den Plugin-Manager reicht — das Plugin bringt keine eigenen Migrationen mit.
Proprietär – siehe composer.json.