Mass Actions in Magento-2-Admin-Grids: Eigene Bulk-Operationen implementieren
AI generated
M2
di.xml
Magento 2 · Admin UI Components
Eigene Mass Actions mit Bulk-API
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.

13 Min. Lesezeit Mass Actions Bulk API AsynchronousOperations RabbitMQ Consumer

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.

11. FAQ: Bulk-Aktionen in Magento-2-Admin-Grids: Das Wichtigste auf einen Blick

1Wann lohnt sich die Bulk-API gegenüber einer einfachen synchronen Mass Action?
Sobald Operationen regelmäßig über ein paar hundert Zeilen hinausgehen oder die Verarbeitung pro Zeile mehr als Millisekunden dauert, etwa durch externe API-Aufrufe, wird eine synchrone Verarbeitung riskant und die Bulk-API die zuverlässigere Wahl.
2Muss der Bulk-Consumer manuell gestartet werden?
In Produktionsumgebungen läuft der Consumer meist als dauerhafter Prozess über einen Supervisor oder wird durch den consumers_runner-Cronjob regelmäßig angestoßen, ein einmaliger manueller Start reicht für Produktionsbetrieb nicht aus.
3Was passiert mit einer Bulk-Operation, wenn der Consumer-Prozess abstürzt?
Bereits als in Bearbeitung markierte, aber nicht abgeschlossene Operations bleiben in diesem Status hängen, bis ein manueller Eingriff oder ein Recovery-Mechanismus sie zurücksetzt, ein Monitoring auf lange offene Operations ist deshalb sinnvoll.
4Wie unterscheidet sich der Excluded-Modus vom Selected-Modus technisch?
Beim Excluded-Modus überträgt das Frontend ein Flag plus eine Liste der ausgeschlossenen IDs zusammen mit dem aktiven Filter-Set, der MassactionFilter löst daraus serverseitig die tatsächlich zu verarbeitende Ergebnismenge auf.
5Ist RabbitMQ zwingend für die Bulk-API notwendig?
Nein, Magento unterstützt auch eine reine Datenbank-Queue ohne RabbitMQ, für sehr große oder häufige Bulk-Operationen mit hohem Durchsatzbedarf ist RabbitMQ aber deutlich performanter und besser skalierbar.
6Wie zeigt man dem Nutzer den Fortschritt einer laufenden Bulk-Operation?
Magento bringt eine Standard-Bulk-Notification im Admin-Header mit, für fachlich wichtige Operationen lohnt sich zusätzlich eine eigene Statusseite, die den Fortschritt über OperationRepositoryInterface abfragt.
7Wie werden fehlgeschlagene Operations sinnvoll behandelt?
Transiente Fehler sollten als retriably failed markiert werden, damit ein späterer Lauf sie automatisch wiederholt, dauerhafte fachliche Fehler dagegen als not retriably failed, um endlose Wiederholungen zu vermeiden.
8Kann eine Bulk-Operation von einem Nutzer abgebrochen werden?
Standardmäßig bietet Magento keinen direkten Abbruch-Mechanismus im Admin-UI, ein eigener Controller kann aber offene Operations gezielt über OperationRepositoryInterface als not retriably failed markieren, um die weitere Verarbeitung zu stoppen.
9Wie viele Produkte sollte eine einzelne Operation innerhalb einer Bulk-Operation umfassen?
Eine einzelne Produkt-ID pro Operation maximiert die Granularität für Fortschrittsanzeige und Fehlerbehandlung, bei sehr großen Mengen kann ein kleines Batch von etwa zehn bis fünfzig IDs pro Operation den Overhead pro Nachricht reduzieren.
10Ist die Bulk-API auch für Cron-basierte, nicht nutzerinitiierte Massenverarbeitung geeignet?
Grundsätzlich ja, für rein zeitgesteuerte Jobs ohne unmittelbaren Nutzerkontext ist ein klassischer Magento-Cronjob mit eigener Batch-Logik aber oft einfacher zu betreiben, da er ohne zusätzliche Queue-Infrastruktur auskommt.