Abweichende Artikeldaten Help

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:

public static function isAccessible(ProductEntity $product, ?CustomerEntity $customer): bool
  1. Ist is_custom_product nicht gesetzt → frei für alle.

  2. Ist kein Kunde angemeldet → gesperrt.

  3. Ist customer_assignment gefüllt → nur die dort genannte Kunden-ID.

  4. Sonst: ist customer_assignment_api gefüllt → Abgleich über die Kundennummer.

  5. 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

ProductPageLoadedEvent

onProductPageLoaded()

Wirft eine ProductNotFoundException → 404

SearchPageLoadedEvent

onSearchPageLoaded()

Setzt active/available gesperrter Treffer auf false

SuggestPageLoadedEvent

onSuggestPageLoaded()

Wie oben, für Suchvorschläge

CheckProductEvent

onCheckProduct()

Wendet die Zugriffsregel an

ProductCheckEvent

onProductChecked()

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.

if ($this->hideInaccessibleChildren($product, $customer)) { $data->set('product-' . $product->getId(), $product); }

Zwei Feinheiten daran sind wichtig:

  • Der Wert wird nur dann in die CartDataCollection geschrieben, wenn der Child-Count tatsächlich korrigiert wurde. Sonst würde der Kern seinen eigenen Gateway-Aufruf überspringen und das ProductGatewayCriteriaEvent fiele für jeden Warenkorbartikel aus.

  • Ohne diesen Eintrag lädt der Kern den Artikel mit seinem echten childCount nach und validateParents() 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

POST /store-api/custom-products

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

direct_assignment

DirectAssignmentStruct

direct = true, wenn der Artikel dem Anfragenden selbst gehört

customer_assignment

CustomerEntity

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

CustomProductsCriteriaEvent

Vor der Suche — Criteria erweitern

CustomProductsSearchedEvent

Nach der Suche, vor den Erweiterungen

CustomProductsLoadedEvent

Nach den Erweiterungen, vor der Antwort

CustomProductsRouteCacheKeyEvent

Cache-Schlüssel anpassen

CustomProductsRouteCacheTagsEvent

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.

composer test

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.

Last modified: 06 August 2026