Von der massaction-Deklaration bis zur asynchronen Verarbeitung über die AsynchronousOperations-Queue
Eine eigene Bulk-Aktion im Admin-Grid ist schnell deklariert, skaliert aber ohne die Bulk-API selten über ein paar hundert Datensätze hinaus. Wer versteht, wie massaction-Deklaration, Controller und die AsynchronousOperations-Queue zusammenspielen, kann Massen-Export, Massen-Status-Änderungen oder komplexere Bulk-Verarbeitung so bauen, dass sie auch bei zehntausenden markierten Zeilen zuverlässig durchläuft.
Inhaltsverzeichnis
- 1. Synchron oder asynchron: Wann welche Mass-Action-Variante passt
- 2. Mass Action in der listing.xml deklarieren
- 3. Eigener Controller für die Bulk-Aktion
- 4. Selektions-Handling: Selected versus Excluded
- 5. Grundlagen der Magento-Bulk-API
- 6. Eigenen Consumer registrieren und Queue-Topic verdrahten
- 7. Fehlerbehandlung und Retry-Strategie
- 8. Skalierung: Wann synchron, wann asynchron sinnvoll ist
- 9. ACL-Absicherung und Testing der Bulk-Verarbeitung
- 10. Zusammenfassung
- 11. FAQ
1. Synchron oder asynchron: Wann welche Mass-Action-Variante passt
Magento kennt zwei grundsätzlich verschiedene Wege, eine Mass Action zu verarbeiten. Der einfache Weg ruft für alle markierten Zeilen synchron einen einzigen Controller-Request auf, der die Aktion innerhalb der normalen Request-Laufzeit vollständig abarbeitet. Das reicht für kleine Grids und einfache Operationen wie eine Statusänderung an wenigen Zeilen völlig aus und ist mit deutlich weniger Infrastruktur-Aufwand verbunden als der zweite Weg.
Sobald jedoch mehrere tausend Zeilen markiert werden können oder die Verarbeitung pro Zeile selbst aufwendig ist, etwa ein Export mit externem API-Aufruf pro Produkt, wird der synchrone Ansatz zum Problem: Der PHP-Prozess läuft in ein Timeout, der Admin-Browser wartet auf eine Antwort, die nie kommt, und im schlimmsten Fall bricht die Operation mittendrin ab, ohne dass nachvollziehbar ist, welche Zeilen bereits verarbeitet wurden. Für diesen Fall bringt Magento die Bulk-API mit, die die eigentliche Verarbeitung in eine Warteschlange auslagert.
2. Mass Action in der listing.xml deklarieren
Die Deklaration einer neuen Mass Action erfolgt im massaction-Knoten der listing.xml, in der Regel als Kind des bestehenden Actions-Columns. Jede Aktion braucht einen eindeutigen Typ, eine Beschriftung, eine Ziel-URL und optional eine Bestätigungsabfrage, die vor irreversiblen Operationen wie einem Massen-Löschen sinnvoll ist. Der type-Wert wird später clientseitig verwendet, um dem Server mitzuteilen, welche Aktion aus den markierten Zeilen ausgeführt werden soll.
Für eine Bulk-Export-Aktion reicht die reine XML-Deklaration bereits aus, um den Button im Grid erscheinen zu lassen, die tatsächliche Logik liegt komplett im referenzierten Controller. Wichtig ist, dass die URL auf einen adminhtml-Controller mit dem passenden ACL-Resource-Eintrag zeigt, sonst bekommen Nutzer ohne die entsprechende Berechtigung trotzdem den Button angezeigt, scheitern aber beim Klick an einer Zugriffsverweigerung.
<!-- app/code/Mironsoft/BulkOperations/view/adminhtml/ui_component/product_listing.xml -->
<massaction name="listing_massaction">
<action name="bulk_export">
<settings>
<type>bulk_export</type>
<label translate="true">Massen-Export starten</label>
<url path="mironsoft_bulkoperations/product/massexport"/>
<confirm>
<title translate="true">Massen-Export</title>
<message translate="true">
Export für die ausgewählten Produkte im Hintergrund starten?
</message>
</confirm>
</settings>
</action>
</massaction>
3. Eigener Controller für die Bulk-Aktion
Der Controller hinter einer Mass Action erbt üblicherweise von Magento\Ui\Controller\Adminhtml\Massaction, der bereits einen Großteil der Selektions-Logik kapselt. Der zentrale Baustein ist die Auswertung des Filter-Kontexts über MassactionFilter, der aus dem Request entweder eine Liste konkret ausgewählter IDs oder, im Fall der Option Alle markieren, ein aktives Filter-Set liefert, das erst zur Laufzeit gegen die Collection aufgelöst wird.
Dieser zweite Fall ist der Grund, warum ein naiver Controller, der einfach eine ID-Liste aus dem Request liest, bei sehr großen Ergebnismengen scheitert: Wählt ein Nutzer alle Zeilen eines gefilterten Grids mit fünfzigtausend Treffern aus, würde eine ID-Liste als Request-Parameter die maximale Request-Größe sprengen. Der MassactionFilter löst dieses Problem, indem er stattdessen die Filterbedingungen selbst überträgt und die tatsächliche ID-Auflösung dem Server überlässt.
<?php
declare(strict_types=1);
namespace Mironsoft\BulkOperations\Controller\Adminhtml\Product;
use Magento\Ui\Component\MassAction\Filter;
use Magento\Catalog\Model\ResourceModel\Product\CollectionFactory;
use Magento\Backend\App\Action;
use Magento\Framework\Controller\ResultFactory;
use Mironsoft\BulkOperations\Model\BulkExportScheduler;
/**
* Startet den asynchronen Massen-Export für die im Grid
* ausgewählten oder per Filter selektierten Produkte.
*/
class MassExport extends Action
{
public const ADMIN_RESOURCE = 'Mironsoft_BulkOperations::export';
/**
* @param Action\Context $context Standard-Adminhtml-Context
* @param Filter $massactionFilter Loest Auswahl bzw. Filter-Set in eine Collection auf
* @param CollectionFactory $collectionFactory Erzeugt die Produkt-Collection
* @param BulkExportScheduler $bulkExportScheduler Kapselt den Bulk-API-Aufruf
*/
public function __construct(
Action\Context $context,
private readonly Filter $massactionFilter,
private readonly CollectionFactory $collectionFactory,
private readonly BulkExportScheduler $bulkExportScheduler,
) {
parent::__construct($context);
}
/**
* Führt die Massen-Aktion aus und leitet zurück zum Grid.
*
* @return \Magento\Framework\Controller\ResultInterface
*/
public function execute()
{
$collection = $this->massactionFilter->getCollection($this->collectionFactory->create());
$productIds = $collection->getAllIds();
$bulkUuid = $this->bulkExportScheduler->schedule($productIds, (int) $this->getRequest()->getParam('store', 0));
$this->messageManager->addSuccessMessage(
__('Export für %1 Produkte wurde im Hintergrund gestartet.', count($productIds))
);
$resultRedirect = $this->resultFactory->create(ResultFactory::TYPE_REDIRECT);
return $resultRedirect->setPath('catalog/product/index');
}
}
4. Selektions-Handling: Selected versus Excluded
Neben der klassischen Auswahl einzelner Checkboxen unterstützt die UI-Component-Grid-Bibliothek auch den Modus Alle markieren mit anschließendem Ausschluss einzelner Zeilen. Intern übermittelt das Frontend in diesem Fall ein isExcludeMode-Flag zusammen mit einer Liste der explizit ausgeschlossenen IDs, statt einer vollständigen Liste der eingeschlossenen. Der MassactionFilter löst beide Fälle transparent auf, ein eigener Controller muss diese Unterscheidung selbst nicht mehr behandeln, solange er konsequent über den Filter statt über direkte Request-Parameter arbeitet.
Ein häufiger Anfängerfehler besteht darin, den Request-Parameter selected direkt auszulesen und dabei den excluded-Fall komplett zu übersehen. Das führt in der Praxis zu Bulk-Operationen, die im Test mit kleinen, manuell ausgewählten Zeilen funktionieren, aber bei der Option Alle markieren scheinbar zufällig falsche oder leere Ergebnisse liefern, weil die tatsächliche ID-Liste nie korrekt aufgelöst wurde.
5. Grundlagen der Magento-Bulk-API
Die Bulk-API basiert auf dem Modul Magento_AsynchronousOperations und trennt drei Konzepte: eine Bulk-Operation als übergeordnete Einheit mit eigener UUID, einzelne Operations als atomare Arbeitseinheiten innerhalb dieser Bulk-Operation, und einen Message-Queue-Consumer, der die Operations asynchron aus RabbitMQ oder dem konfigurierten Queue-Backend abarbeitet. Diese Trennung erlaubt es, den Fortschritt einer sehr großen Massenoperation granular zu verfolgen, statt sie als einen einzigen langlaufenden Prozess zu behandeln.
Der zentrale Einstiegspunkt ist BulkManagementInterface::scheduleBulk, dem eine eindeutige Bulk-UUID, eine Liste von OperationInterface-Objekten und eine beschreibende Bezeichnung übergeben werden. Jede einzelne Operation referenziert einen Consumer-Namen und trägt die für die Verarbeitung nötigen Daten als serialisierten String, üblicherweise eine einzelne Produkt-ID oder ein kleines Batch von IDs statt der gesamten Ergebnismenge in einer einzigen Operation.
<?php
declare(strict_types=1);
namespace Mironsoft\BulkOperations\Model;
use Magento\AsynchronousOperations\Api\Data\OperationInterfaceFactory;
use Magento\Framework\Bulk\BulkManagementInterface;
use Magento\Framework\DataObject\IdentityGeneratorInterface;
/**
* Legt eine Bulk-Operation für den asynchronen Produkt-Export an
* und schedult je Produkt eine einzelne Operation in der Queue.
*/
class BulkExportScheduler
{
private const CONSUMER = 'mironsoft.bulk.product.export';
/**
* @param BulkManagementInterface $bulkManagement Zentrale Bulk-API
* @param OperationInterfaceFactory $operationFactory Erzeugt einzelne Operations
* @param IdentityGeneratorInterface $identityGenerator Erzeugt eindeutige UUIDs
*/
public function __construct(
private readonly BulkManagementInterface $bulkManagement,
private readonly OperationInterfaceFactory $operationFactory,
private readonly IdentityGeneratorInterface $identityGenerator,
) {
}
/**
* Plant den asynchronen Export der übergebenen Produkt-IDs.
*
* @param int[] $productIds Zu exportierende Produkt-IDs
* @param int $storeId Store-View für den Export
* @return string Die erzeugte Bulk-UUID
*/
public function schedule(array $productIds, int $storeId): string
{
$bulkUuid = $this->identityGenerator->generateId();
$operations = [];
foreach ($productIds as $productId) {
$operations[] = $this->operationFactory->create([
'data' => [
'bulk_uuid' => $bulkUuid,
'topic_name' => self::CONSUMER,
'serialized_data' => json_encode([
'product_id' => $productId,
'store_id' => $storeId,
]),
'status' => 0,
],
]);
}
$this->bulkManagement->scheduleBulk($bulkUuid, $operations, __('Massen-Export'));
return $bulkUuid;
}
}
6. Eigenen Consumer registrieren und Queue-Topic verdrahten
Damit die geplanten Operations tatsächlich verarbeitet werden, braucht das Modul drei zusammenspielende Deklarationen: ein Topic in communication.xml, einen Consumer in queue_consumer.xml und die Bindung des Topics an eine Queue in queue_topology.xml. Ein häufiges Missverständnis ist die Annahme, allein die Registrierung eines Consumers reiche aus, tatsächlich muss auch der Cronjob consumers_runner regelmäßig laufen oder ein dauerhafter Consumer-Prozess über bin/magento queue:consumers:start manuell gestartet werden, sonst stapeln sich Operations unverarbeitet in der Datenbanktabelle.
Der Consumer selbst implementiert eine einzelne Methode, die eine OperationInterface-Instanz entgegennimmt, die serialisierten Nutzdaten dekodiert, die eigentliche fachliche Verarbeitung durchführt und den Status der Operation über OperationManagementInterface auf erfolgreich oder fehlgeschlagen setzt. Dieser letzte Schritt wird in der Praxis häufig vergessen, mit der Folge, dass Operations im Bulk-Status-Widget dauerhaft als offen angezeigt werden, obwohl die fachliche Verarbeitung längst abgeschlossen ist.
<!-- app/code/Mironsoft/BulkOperations/etc/communication.xml -->
<topic name="mironsoft.bulk.product.export" request="Magento\AsynchronousOperations\Api\Data\OperationInterface"/>
<!-- app/code/Mironsoft/BulkOperations/etc/queue_consumer.xml -->
<consumer name="mironsoft.bulk.product.export"
queue="mironsoft.bulk.product.export"
connection="db"
handler="Mironsoft\BulkOperations\Model\Consumer\ExportConsumer::process"/>
<!-- app/code/Mironsoft/BulkOperations/etc/queue_topology.xml -->
<exchange name="magento" type="topic" connection="db">
<binding id="MironsoftBulkExport" topic="mironsoft.bulk.product.export"
destinationType="queue" destination="mironsoft.bulk.product.export"/>
</exchange>
7. Fehlerbehandlung und Retry-Strategie
OperationInterface unterscheidet vier Statuswerte: offen, in Bearbeitung, erfolgreich abgeschlossen und fehlgeschlagen, wobei fehlgeschlagene Operations wiederum in retriably failed und not retriably failed unterschieden werden. Ein transientes Problem, etwa ein kurzzeitig nicht erreichbarer externer Dienst beim Export, sollte als retriably failed markiert werden, damit ein späterer erneuter Lauf die Operation automatisch wiederholt, während ein dauerhaftes Problem wie eine ungültige Produkt-ID als not retriably failed markiert wird, um endlose Wiederholversuche zu vermeiden.
Für die Sichtbarkeit im Admin-Bereich bringt Magento bereits eine Bulk-Notification an, die fehlgeschlagene Operations im Benachrichtigungsbereich anzeigt. Für fachlich wichtige Bulk-Operationen lohnt sich zusätzlich eine eigene, dedizierte Statusseite, die den Fortschritt einer laufenden Bulk-UUID über OperationRepositoryInterface abfragt und dem Nutzer eine verständliche Zusammenfassung statt der recht technischen Standardansicht anzeigt.
8. Skalierung: Wann synchron, wann asynchron sinnvoll ist
Die Bulk-API ist kein Allheilmittel, sie bringt zusätzliche Infrastruktur-Komplexität mit sich: einen laufenden Consumer-Prozess, eine funktionierende Message-Queue-Konfiguration und ein Monitoring, das erkennt, wenn Consumer aus irgendeinem Grund gestoppt sind und sich Operations stapeln. Für Operationen, die zuverlässig unter ein paar Sekunden Verarbeitungszeit bleiben und selten mehr als ein paar hundert Zeilen gleichzeitig betreffen, ist der synchrone Weg oft die pragmatischere und wartungsärmere Wahl.
Bei RabbitMQ als Queue-Backend lässt sich die Verarbeitungsgeschwindigkeit zusätzlich über die Anzahl paralleler Consumer-Instanzen skalieren, was bei der Standard-Datenbank-Queue nicht in gleichem Maß möglich ist. Für Shops mit regelmäßigen, wirklich großen Bulk-Operationen, etwa nächtlichen Massen-Updates an mehreren zehntausend Produkten, lohnt sich der Umstieg auf RabbitMQ meist schon aus reinen Durchsatzgründen.
9. ACL-Absicherung und Testing der Bulk-Verarbeitung
Jede eigene Mass Action braucht eine eigene ADMIN_RESOURCE-Konstante im Controller und einen passenden Eintrag in acl.xml, sonst greift entweder gar keine Berechtigungsprüfung oder, schlimmer, die Aktion nutzt versehentlich die ACL-Resource eines anderen, bereits vorhandenen Controllers und macht Berechtigungen dadurch für Admin-Rollen unvorhersehbar. Gerade bei irreversiblen Bulk-Operationen wie einem Massen-Löschen lohnt sich eine eigene, granulare ACL-Resource statt der Wiederverwendung einer allgemeinen Katalog-Berechtigung.
Für automatisierte Tests reicht ein reiner Controller-Test selten aus, da die eigentliche Verarbeitung asynchron im Consumer stattfindet. Ein Integrationstest, der eine Bulk-Operation über BulkManagementInterface anlegt, den Consumer direkt mit einer simulierten Operation aufruft und anschließend den gesetzten Status prüft, deckt die komplette Kette von Planung bis Verarbeitung ab, ohne auf einen laufenden RabbitMQ-Consumer im Testsystem angewiesen zu sein.
| Ansatz | Verarbeitung | Skaliert bis | Typischer Einsatzfall |
|---|---|---|---|
| Synchrone Mass Action | innerhalb des Controller-Requests | einige hundert Zeilen | einfache Statusänderung |
| Bulk-API mit DB-Queue | asynchron über Cron-Consumer | einige tausend Zeilen | Massen-Export ohne hohen Durchsatzbedarf |
| Bulk-API mit RabbitMQ | asynchron, mehrere parallele Consumer | hunderttausende Zeilen | regelmäßige große Bulk-Jobs |
| Cron-basierte Batch-Verarbeitung | zeitgesteuert, außerhalb des Grids | beliebig, ohne Nutzerinteraktion | nächtliche Massenverarbeitung |
Mironsoft
Magento-Entwicklung, Modul-Beratung und Systemarchitektur
Magento-Projekt, das eine zweite Meinung oder erfahrene Umsetzung braucht?
Wir entwickeln individuelle Magento-Module, beraten bei Architekturentscheidungen und übernehmen komplexe Umsetzungen, von der Service-Contract-Planung bis zum produktionsreifen Deployment.
Architektur-Beratung
Modul- und Systemarchitektur vor der Umsetzung fundiert durchdenken lassen.
Custom-Modul-Entwicklung
Individuelle Magento-Module nach Best Practices sauber umsetzen.
Code-Review & Audit
Bestehende Module auf Performance, Sicherheit und Wartbarkeit prüfen lassen.
10. Zusammenfassung
Bulk-Aktionen in Magento-2-Admin-Grids: Das Wichtigste auf einen Blick
Deklaration
massaction-Knoten in listing.xml mit eindeutigem Typ, Label und ACL-geschützter Controller-URL.
Controller
MassactionFilter statt direkter Request-Parameter nutzen, um Selected- und Excluded-Modus korrekt aufzulösen.
Bulk-API
BulkManagementInterface::scheduleBulk plant Operations, Consumer verarbeitet sie asynchron aus der Queue.
Betrieb
Consumer-Prozess muss dauerhaft laufen, Statuscodes korrekt setzen, RabbitMQ für hohen Durchsatz erwägen.