Backend
Die Zugriffsregel
Leoparden\BusinessCustomProducts\Content\CustomProduct\CustomerAssignment ist die einzige Stelle, an der entschieden wird, ob ein Kunde einen Artikel haben darf. Sie arbeitet diese Reihenfolge ab:
Ist
is_custom_productnicht gesetzt → frei für alle.Ist kein Kunde angemeldet → gesperrt.
Ist
customer_assignmentgefüllt → nur die dort genannte Kunden-ID.Sonst: ist
customer_assignment_apigefüllt → Abgleich über die Kundennummer.Sonst → frei für jeden angemeldeten Kunden.
Schritt 4 ist der Grund, warum die Regel zwei Felder liest. Die Warenwirtschaft schreibt die Kundennummer nach customer_assignment_api; der CustomFieldEntityMatcher löst sie normalerweise in customer_assignment auf. Bleibt die Auflösung aus — der Kunde existiert im Shop nicht, oder die Nummer wurde mit Leerzeichen aufgefüllt — wäre der Artikel ohne Schritt 4 plötzlich für alle zugänglich. Genau das ist zu vermeiden: Ein Kundenartikel ohne auflösbaren Kunden gehört niemandem, nicht jedem.
Die Werte werden deshalb getrimmt verglichen, und customer_assignment_api wird sowohl als Einzelwert als auch als Liste akzeptiert.
Seite, Suche und Suchvorschläge
Subscriber\ProductSubscriber hängt sich an die Storefront-Events:
Event | Methode | Wirkung |
|---|---|---|
|
| Wirft eine |
|
| Setzt |
|
| Wie oben, für Suchvorschläge |
|
| Wendet die Zugriffsregel an |
|
| Sucht einen zugänglichen Ersatz |
onProductChecked() ist der Umleitungsschritt: Ist der aufgerufene Artikel eine Variante und gesperrt, lädt der Subscriber den Vaterartikel samt Geschwistern und liefert die erste zugängliche Variante aus. Findet sich keine, wird der Vaterartikel selbst zurückgegeben — mit geleerter Variantenliste und childCount = 0, damit er kaufbar bleibt.
Eigene Regeln einhängen
CheckProductEvent und ProductCheckEvent sind die Erweiterungspunkte. Wird ein Artikel von der eigenen Regel abgelehnt, verschickt das Plugin einen ProductCheckEvent — ein eigener Listener kann dort einen Ersatzartikel setzen oder die Ablehnung zurücknehmen.
Warenkorb
Content\Cart\Collector ist als shopware.cart.collector mit Priorität 9000 registriert und läuft damit vor dem Kern-Collector. Er lädt die Artikel des Warenkorbs über die ProductListRoute und erledigt zwei Dinge.
Fremde Positionen entfernen. Ist ein Artikel für den Kunden gesperrt, fliegt die Position aus dem Warenkorb und es wird ein ProductNotFoundError gemeldet. Weil das im Collector passiert und nicht beim Seitenaufruf, gilt die Regel für jeden Kanal — Storefront, Store-API und „Artikel per Artikelnummer hinzufügen" gleichermaßen.
Den Child-Count korrigieren. Der Kern lässt einen Artikel mit Varianten nicht in den Warenkorb: ProductCartProcessor::validateParents() entfernt jede Position, deren Artikel childCount > 0 hat. Der Collector zählt deshalb nur die Varianten, die dieser Kunde überhaupt sehen darf, und setzt childCount entsprechend herunter.
Zwei Feinheiten daran sind wichtig:
Der Wert wird nur dann in die
CartDataCollectiongeschrieben, wenn der Child-Count tatsächlich korrigiert wurde. Sonst würde der Kern seinen eigenen Gateway-Aufruf überspringen und dasProductGatewayCriteriaEventfiele für jeden Warenkorbartikel aus.Ohne diesen Eintrag lädt der Kern den Artikel mit seinem echten
childCountnach undvalidateParents()entfernt den Vaterartikel — ohne Fehlermeldung. Nach außen sieht das aus wie eine erfolgreiche Bestellung mit leerem Warenkorb.
Damit der ersetzte Artikel im Warenkorb vollständig bleibt, spiegelt die Criteria die Assoziationen des ProductGateway: cover.media, options.group, featureSet und properties.group.
Store-API
Routenname store-api.business_custom_products.custom-products. Die Route verlangt eine Anmeldung ($context->ensureLoggedIn(false)) und liefert die Artikel, deren customer_assignment die ID des angemeldeten Kunden enthält.
Sortiert wird standardmäßig absteigend nach productNumber. Eine Besonderheit: Sortierfelder, die auf customFields zeigen, lassen sich nicht in SQL sortieren — sie werden aus der Criteria herausgelöst und das Ergebnis wird nachträglich in PHP über den PropertyAccessor sortiert.
Jeder Artikel der Antwort bekommt zwei Erweiterungen:
Erweiterung | Typ | Inhalt |
|---|---|---|
|
|
|
|
| Der zugewiesene Kunde |
Dazu registriert Content\Extension\ProductExtension am ProductDefinition die beiden Laufzeitfelder direct_assignment (BoolField) und customer_assignment (StringField), jeweils mit Runtime - und ApiAware-Flag.
Events der Route
Event | Zeitpunkt |
|---|---|
| Vor der Suche — Criteria erweitern |
| Nach der Suche, vor den Erweiterungen |
| Nach den Erweiterungen, vor der Antwort |
| Cache-Schlüssel anpassen |
| Cache-Tags anpassen |
Cache
CachedCustomProductsRoute dekoriert die Route mit Priorität -1000. Die Invalidierung wird über den Container-Parameter shopware.cache.invalidation.custom-product-route gesteuert.
Tests
Die Testsuite liegt unter src/Test und läuft gegen einen echten Kernel.
CartCollectorTest deckt die Zugriffsmatrix und den Vaterartikel-Rückfall ab — inklusive der unaufgelösten ERP-Referenz und des Abgleichs über die Kundennummer. SubscriberTest prüft Seite und Suche, CustomProductsRouteTest die Store-API.