Zum Inhalt springen

Events

Stockly veröffentlicht, was im Lager geschieht, damit eine Integration seine Tabellen nicht abfragen oder, schlimmer, in product.stock schreiben muss, das dem Modul gehört und von ihm überschrieben wird.

Der Katalog unten ist nach Ebene gruppiert. bin/console p2lab-stockly:events druckt den lebenden Katalog samt Listener-Zahlen, und das ist zugleich der schnellste Weg, „ist mein Listener tatsächlich angeschlossen” zu beantworten.

Abonnieren Sie über den Namen des Events, lesen Sie die Nutzdaten über ententypisierte Getter. Nirgends ein use einer Stockly-Klasse, keine Abhängigkeit in composer.json; das Plugin funktioniert dann ohne Stockly, mit Stockly und über Versionen hinweg, die eine Klasse verschoben haben.

public static function getSubscribedEvents(): array
{
return ['p2lab_stockly.backorder.opened' => 'onBackorderOpened'];
}
public function onBackorderOpened(object $event): void
{
if (!method_exists($event, 'getPayload')) {
return;
}
$data = $event->getPayload();
// $data['orderId'], $data['lineItemId'], $data['shortfallQuantity'], …
}

Jedes Event trägt denselben Umschlag, eine allgemeine Brücke (Webhook, Warteschlange, Prüfprotokoll) braucht also kein Wissen über irgendeine konkrete Klasse und arbeitet auch für später hinzugefügte Events weiter:

public function forward(object $event): void
{
if (!method_exists($event, 'getPayload') || !method_exists($event, 'getName')) {
return;
}
$this->http->post($this->url, ['json' => [
'event' => $event->getName(),
'id' => $event->getEventId(), // Schlüssel zur Entduplizierung
'at' => $event->getOccurredAt()->format(DATE_ATOM),
'data' => $event->getPayload(),
]]);
}
GetterBedeutung
getName()stabiler öffentlicher Name, z. B. p2lab_stockly.demand.declared
getEventId()eindeutige 32 Zeichen Hex — damit entduplizieren
getSchemaVersion()nur erhöht, wenn ein Nutzdatenschlüssel entfernt wird oder seine Bedeutung ändert
getOccurredAt()ein DateTimeImmutable
getCorrelationId()bindet eine Arbeitseinheit zusammen (Bestell-Id, Einkaufsbestell-Id)
getActor()['type' => user|storefront|cli|system, 'id' => ?string, 'name' => ?string]
getPayload()die ganze Tatsache, als JSON-serialisierbare Skalare und Arrays
getContext()der Shopware-Context

Nutzdaten sind Skalare, Nulls und Arrays davon, nie Entitäten, Structs oder DateTime-Objekte; Daten sind ISO-8601-Zeichenketten. Jede Id ist 32 Zeichen Hex in Kleinbuchstaben ohne Bindestriche, bereit für UNHEX().

Zwei Schreibweisen, und sie sind nicht austauschbar

Abschnitt betitelt „Zwei Schreibweisen, und sie sind nicht austauschbar“
SchreibweiseArtSie
p2lab.stockly.… — ein Punkt nach dem Anbieterein Hookbeantworten ihn
p2lab_stockly.… — ein Unterstricheine Tatsachebeobachten sie

Die Namen mit Punkt gehören zu den Versand- und Druck-Hooks: p2lab.stockly.parcels.plan, p2lab.stockly.label.print und ihren Geschwistern, beschrieben unter Stockly anbinden.

Bedarf — was eine Bestellung will und was ihn deckt

Abschnitt betitelt „Bedarf — was eine Bestellung will und was ihn deckt“
EventWannWichtige Nutzdaten
p2lab_stockly.demand.declaredder Bedarf einer Position wird erstmals erfasstquantity, allocatedQuantity, shortfallQuantity, warehouseIds[], productId, orderedProductId
p2lab_stockly.demand.changedBedarf oder Deckung einer bestehenden Position hat sich bewegtdas Obige plus previousQuantity, previousAllocated, previousShortfall
p2lab_stockly.demand.deductedWare hat das Regal für die Bestellung körperlich verlassentotalQuantity, lines[] mit warehouseId / binLocationId / batchId / handlingUnitId
p2lab_stockly.demand.releaseddie Bestellung fordert nicht mehrreleasedQuantity, reasonCode (cancel / delete / edit), returnMode
p2lab_stockly.sourcing.decidedeine Position wurde gegen einen Standort gebucht, auf einen anderen verschoben oder ließ sich gar nicht buchenchosenWarehouseId, previousWarehouseId, rule, ruleSource, degraded, candidateCount, candidates[], triggerSource, requirement, settingsFingerprint

quantity = allocatedQuantity + shortfallQuantity gilt auf declared und changed immer.

sourcing.decided greift nur, wenn das Ergebnis eine Meldung wert ist: Der Standort hat gewechselt, die Entscheidung wurde herabgestuft, oder der Bedarf ließ sich nicht buchen. Eine wiederholte gleiche Entscheidung erhöht stattdessen einen Zähler auf der Zuordnungszeile, ein Listener sieht also die Geschichte der Leitung statt des Rauschens von Bestelländerungen.

productId ist das Produkt, das den Bestand trägt; orderedProductId ist das auf der Bestellposition. Sie unterscheiden sich, wenn ein zusammenarbeitendes Plugin Bestand umleitet, etwa ein Bundle, das aus seinen Bestandteilen zieht.

EventWannWichtige Nutzdaten
p2lab_stockly.backorder.openedwas einer Position geschuldet wird, ist gestiegenshortfallQuantity (die neue Summe), previousShortfall (0 = eine echte Eröffnung), totalOutstanding
p2lab_stockly.backorder.coveredEinheiten wurden einer wartenden Position vorgemerktcoveredQuantity, remainingShortfall, triggerReason (receipt / transfer / release / correction)
p2lab_stockly.backorder.closeddie Position schuldet nichts mehrclosedQuantity, closeReason (fulfilled / cancelled)

covered greift auch bei teilweiser Deckung; remainingShortfall ist die Zahl, auf die es ankommt. Auf eine vollständige Deckung folgt closed.

Eine Position, deren Zuordnungszeile rundweg gelöscht wird, erzeugt überhaupt kein closed, weil die Zeile, auf der die Fehlmenge lebte, mit ihr ging. Beobachten Sie für diesen Fall demand.released.

EventWannWichtige Nutzdaten
p2lab_stockly.goods.receivedein Wareneingang zu einer Bestellung wurde gebuchtpurchaseOrderId, supplierId, warehouseId, totalQuantity, items[] mit Menge, Lagerplatz, Charge, Ablaufdatum, Ladungsträger und Einstandskosten

Ein Event je Wareneingang, nicht je Position, denn ein Wareneingang ist eine Entscheidung. Die Aufteilung steckt in items[].

Bestand — was sich bewegt hat und was es mit der Verfügbarkeit tat

Abschnitt betitelt „Bestand — was sich bewegt hat und was es mit der Verfügbarkeit tat“
EventWannWichtige Nutzdaten
p2lab_stockly.stock.movedWare traf ein, ging hinaus oder bewegte sichmovementType, quantity, delta, quantityAfter, from / to {warehouseId, binLocationId, lpCode}, batchId, expiresAt, orderId, purchaseOrderId, operationId
p2lab_stockly.stock.availability_changedwas verkauft werden kann, hat sich geändertpreviousAtp, currentAtp, shelfTotal, shortfallTotal, isNegative, previousAvailable, currentAvailable

Die Richtung liegt in from / to: Ein Zugang hat from = null und ein positives delta, ein Abgang hat to = null und ein negatives, eine Bewegung hat beides und delta = 0, weil sich die Lagersumme nicht geändert hat.

Wareneingang, Einlagerung, Zählen und Palettieren sind alle stock.moved mit einem anderen movementType. Eine Tatsache, ein Event: Filtern Sie nach dem Typ, statt ein eigenes Event je Vorgang zu suchen.

availability_changed ist mit Absicht getrennt. Eine Reservierung bewegt die Verfügbarkeit, ohne dass sich Ware bewegt, und ein Umräumen zwischen Lagerplätzen bewegt Ware, ohne die Verfügbarkeit anzutasten. Es wird je Produkt und Arbeitseinheit entdupliziert, currentAtp ist also immer die gebuchte Zahl und nie eine Zwischengröße. Sie darf negativ sein und wird unbegrenzt veröffentlicht: drei mehr verkauft, als vorhanden sind, ist eine Antwort, keine Beschädigung.

Arbeit — was die Menschen im Gebäude getan haben

Abschnitt betitelt „Arbeit — was die Menschen im Gebäude getan haben“
EventWannWichtige Nutzdaten
p2lab_stockly.work.pick_wave_plannedArbeit wurde einem Kommissionierer übergebendie Welle und ihre Positionen
p2lab_stockly.work.short_pick_reportedein Kommissionierer fand ein Fach leer oder zu leerexpectedQuantity, foundQuantity, missingQuantity, reasonCode, stockCorrected, varianceHeld, reallocatedQuantity
p2lab_stockly.work.short_pick_withdrawndiese Meldung wurde zurückgenommen — die Ware war doch daexpectedQuantity, missingQuantity, restoredQuantity, earmarkRestored
p2lab_stockly.work.pick_line_completedStücke kamen in den Kartonquantity, quantityPicked, quantityRequired, isComplete, pickLineId
p2lab_stockly.work.stocktake_committedeine Bestandskontrollsitzung wurde gebuchtlineCount, varianceLineCount, totalVarianceUnits, lines[], linesTruncated
p2lab_stockly.work.quality_decidedeine Charge in Quarantäne wurde freigegeben oder abgelehntdecision, quantity, batchNumber, failAction, reason
p2lab_stockly.work.transfer_state_changedeine Umlagerung wurde versandt, kam an, wurde abgeschlossen oder storniertfromState, toState, sourceWarehouseId, targetWarehouseId, lines[]

short_pick_reported greift, ob das Abweichungstor das Fach korrigiert hat oder nicht; die Sichtung ist so oder so eine Veröffentlichung wert. short_pick_withdrawn ist ein eigenes Event statt einer Meldung mit umgedrehten Zahlen: Wer auf die Fehlmenge hin gehandelt hat, muss erfahren, dass sie zurückgenommen wurde, und restoredQuantity / earmarkRestored sagen, was die Ausgleichsbuchung tatsächlich gebucht hat.

stocktake_committed greift auch, wenn nichts abwich, weil eine saubere Zählung die Zahl ist, an der Genauigkeit gemessen wird. Es begrenzt lines[] auf 500, größte Abweichung zuerst, und sagt das in linesTruncated.

pick_line_completed greift nur für angenommene Scans; Doppelte und Konflikte veröffentlichen nichts, denn sie haben niemandes Arbeit gutgeschrieben.

EventWannWichtige Nutzdaten
p2lab_stockly.shipping.shipment_bookedein Paket hat eine Sendungsnummer bekommenorderNumber, parcelId, sequence, trackingCode, carrierProfile, weightKg, parcelCount, remainingParcels
p2lab_stockly.shipping.order_shippedjedes Paket einer Bestellung hat eine SendungsnummerorderNumber, parcelCount, trackingCodes[], trackingCodeList
p2lab_stockly.shipping.shipment_failedder Dienstleister hat abgelehnt und die Wiederholungen sind erschöpftorderNumber, parcelId, sequence, carrierProfile, attempts, error
p2lab_stockly.shipping.batch_handed_overein Fahrer hat einen Satz Pakete genommen und dafür unterschriebencode, carrierProfile, warehouseId, shipDate, parcelCount, orderCount, manifestExternalId, closedByName

remainingParcels ist das, was eine Teilsendung ausdrückbar macht: Die Lieferzustandsmaschine von Shopware hat „teilweise versendet”, und ohne den Zähler könnte ein Flow immer nur „versendet” sagen, was unwahr ist, bis das letzte Paket gebucht ist.

order_shipped darf für dieselbe Bestellung mehr als einmal greifen, und das ist so gewollt. Ein Paket zu einer bereits versandten Bestellung hinzuzufügen und es zu buchen erfüllt die Bedingung erneut, und das ist eine zweite Sendung und kein Fehler. Ein darauf reagierender Flow sollte den Statuswechsel erzwingen, sonst scheitert der zweite Lauf an einer Bestellung, die bereits im Zielstatus ist.

batch_handed_over trägt mit Absicht keine einzelne Bestellung, weil ein Stapel so viele Bestellungen umspannt, wie auf den Wagen passten. Automatisierung je Bestellung gehört an die anderen beiden.

EventWannWichtige Nutzdaten
p2lab_stockly.returns.announcedjemand hat eine Retoure angelegt; noch hat sich nichts bewegtreturnNumber, orderId, type (withdrawal / complaint / cancellation), source (storefront / admin / system), contactEmail, requestedResolution, lineCount, totalQuantity, reasonCodes[]
p2lab_stockly.returns.receiveddie Ware traf ein und wurde in den Retouren-Haltebereich gebuchtreturnNumber, warehouseId, binLocationId, receivedQuantity, announcedQuantity, outstandingQuantity, complete, lines[]
p2lab_stockly.returns.dispositionedjemand hat entschieden, was mit den Einheiten geschiehtreturnNumber, lineId, productId, disposition (restock / scrap / hold), quantity, reasonCode, liability, remainingOnHold, documentComplete
p2lab_stockly.returns.refundedGeld ging an den Kunden zurückreturnNumber, orderId, amount (diese Zahlung; negativ bei einer Rückbuchung), refundedTotal, refundState, source (manual / payment), comment

returns.refunded folgt aus keinem der anderen drei. Eine Anmeldung wird regelmäßig angenommen, entschieden und geschlossen, während die Überweisung noch aussteht, deshalb hat das Geld eine eigene Statusachse. Es greift einmal je Zahlung, eine in zwei Raten beglichene Anmeldung sendet es also zweimal: amount ist diese Rate, refundedTotal ist, wo die Anmeldung danach steht.

source und type stehen auf den Nutzdaten, weil drei verschiedene Wege denselben Beleg erzeugen: der Assistent im Shop-Frontend, ein Mitarbeiter, der ein Telefonat annimmt, und der Stornierungsweg ganz ohne Menschen. Ein Flow, der „wir haben Ihre Retourenanmeldung erhalten” verschickt, will den ersten und nicht die anderen zwei.

announcedQuantity reist mit Absicht neben receivedQuantity: Dass die beiden auseinandergehen, ist der gewöhnliche Fall und meist das, worauf zu reagieren lohnt.

EventWann
p2lab_stockly.stock_integrity_criticaleine Prüfung fand mehr offene kritische Befunde als die eingestellte Schwelle
p2lab_stockly.external_stock_write_detectedjemand hat product.stock von außen geschrieben, und der Wächter hat es erfasst

external_stock_write_detected trägt productId, expectedValue (was Stockly hielt), foundValue (was der Schreiber hinterließ), delta, source (dal für eine API- oder DAL-Schreibung, sql für eine unmittelbar in der Datenbank) und policyApplied (off / correct / restore). Eine Auslösung je Produkt und Arbeitseinheit, nach dem Commit. Siehe externe Änderungen.

stock_integrity_critical ist für den Flow Builder geschrieben und trägt skalare Werte statt des Umschlags; jedes andere Event auf dieser Seite beantwortet getPayload().

Zwei Events sind älter als das Namensschema und werden unter ihrem voll qualifizierten Klassennamen ausgelöst. Das wird sich nicht ändern, denn Plugins sind bereits darauf abonniert.

KlasseWann
P2Lab\Stockly\Event\StockChangedEventein billiges „dieses Produkt hat sich geändert, lies es neu”-Signal
P2Lab\Stockly\Event\BeforeStockOperationEventvor Zugang, Abgang oder Bewegung — der Veto-Hook, beschrieben unter Policy Hooks

Tatsachen werden nach dem Commit veröffentlicht. Was ein Event beschreibt, ist bereits geschehen und wird nachträglich nicht zurückgerollt.

Ein scheiternder Listener kann das Lager nicht kaputtmachen. Exceptions aus Tatsachen-Listenern werden protokolliert und geschluckt. Symfony hat allerdings keine Isolierung je Listener, ein werfender Listener schneidet die dahinter eingereihten also ab. Policy-Listener sind das Gegenteil: Ihre Exceptions pflanzen sich fort, denn das Ergebnis zu beeinflussen ist der Zweck.

Es gibt keine Zustellgarantie. Events sind Funktionsaufrufe im Prozess. Stirbt der Prozess zwischen dem Commit und der Auslösung, steckt die Änderung in der Datenbank, und das Event hat niemanden erreicht, und es wird später nicht wiederholt, denn der Abgleich ist idempotent, und der nächste Durchgang sieht nichts zu tun. Eine Integration, die nicht auseinanderlaufen darf, braucht einen eigenen regelmäßigen Abgleich, nicht nur die Events.

Es gibt keine Reihenfolgegarantie zwischen Ebenen. In einer Arbeitseinheit dürfen ein Rückstands-Event und ein Bedarfs-Event in beliebiger Reihenfolge ankommen. getCorrelationId() plus getOccurredAt() erlaubt es, sie zu gruppieren und zu sortieren.

Ein Listener ist nicht in Stocklys Transaktion. Die Schreibvorgänge eines Listeners zurückzurollen rollt die von Stockly nicht zurück.

Beim Lesen wird nichts veröffentlicht. Berichte und Listen geben keine Events aus.

Dreiundzwanzig Events werden als Flow-Builder-Auslöser angeboten, ein Händler kann sie also ohne eine Zeile Code an eine Mail, einen Webhook oder ein Tag hängen: jedes Event von oben außer den fünf, die je Position, je Bewegung oder je Scan greifen, nämlich demand.declared, demand.changed, sourcing.decided, stock.moved und work.pick_line_completed.

Dieselben Events erreichen Webhooks des App Systems. Shopware behandelt jedes Flow-fähige Event als hakbar, eine App kann also abonnieren, und die Zustellung läuft über das eigene Webhook-Protokoll und die Nachrichtenwarteschlange des Kerns, mit Wiederholung und Aufräumen. Für ein externes System ist das der verlässliche Weg, und er braucht nichts von diesem Plugin.

Zwei Regeln bestimmen die Liste, und beide gibt es, um sie ehrlich zu halten.

Ein Lebenszyklus wird ganz angeboten. opened / covered / closed reisen zusammen, und deducted reist mit released. Ein halber Lebenszyklus im Flow Builder ist schlimmer als keiner: Er lässt einen Händler eine Automatisierung bauen, die einem Kunden sagt, seine Ware sei unterwegs, nachdem er um eine Erstattung gebeten hat.

Hochfrequente Events bleiben draußen. Die fünf oben greifen je Bestellposition, je Bewegung und je Scan. Sie in der Oberfläche anzubieten endet mit einer Automatisierung, die tausendmal am Tag mailt. Wer sie wirklich braucht, schreibt ein Plugin und abonniert über den Namen, wo es keine solche Grenze gibt. stock.availability_changed steht trotz seiner geschäftigen Ebene auf der Liste, und die Entduplizierung ist der Grund: Es greift, wenn die Verfügbarkeit sich tatsächlich bewegt hat, nicht wenn irgendetwas geschah.

Ereignisnamen ändern sich nie. Konstruktoren bekommen nur nachgestellte optionale Parameter hinzu. getPayload() darf ohne Ankündigung Schlüssel hinzubekommen; ein Schlüssel wird nur mit einer Erhöhung von getSchemaVersion() entfernt oder umdefiniert.