Eigene Filter für Admin-UI-Component-Grids in Magento 2 entwickeln
AI generated
M2
di.xml
Magento 2 · Admin UI Components
Eigene Filter für Admin-Grids
UI-Component-Deklaration, Custom-Filter-Typen und Collection-Modifier im Zusammenspiel

Die Standard-Filter der Admin-UI-Components decken die meisten Grids gut ab, stoßen aber bei Wertespannen, dynamischen Optionslisten oder fachlichen Sonderfällen schnell an ihre Grenzen. Wer weiß, wie Filter-Deklaration, Frontend-Komponente und Collection-Modifier zusammenspielen, kann jeden gewünschten Filter-Typ sauber nachrüsten, statt Workarounds über JavaScript-Hacks im Grid zu bauen.

12 Min. Lesezeit UI Components FilterPool Collection Modifier KnockoutJS Grid-Performance

1. Wo die Standard-Filter der UI-Components enden

Ein Admin-Grid in Magento besteht aus einer listing.xml, die Spalten, Massenaktionen und eben auch Filter deklarativ beschreibt. Für die häufigsten Fälle, Textsuche, Select-Dropdown, Datumsbereich, liefert Magento fertige Filter-Komponenten mit, die ohne eine einzige Zeile Custom-Code auskommen. Sobald ein Filter jedoch eine Wertespanne über ein numerisches Attribut abbilden soll, etwa einen Lagerbestand zwischen zwei Grenzwerten, oder eine Mehrfachauswahl aus einer zur Laufzeit ermittelten Optionsliste anbietet, reicht die Standardpalette nicht mehr aus.

Der naheliegende, aber falsche Reflex ist dann, den Filter clientseitig mit einem eigenen JavaScript-Snippet nachzubauen und die Collection ungefiltert an den Client zu senden. Das funktioniert bei kleinen Datenmengen, bricht aber bei jedem Katalog mit mehr als ein paar tausend Zeilen zusammen, weil die eigentliche Filterung dann im Browser statt in der Datenbank passiert. Der saubere Weg führt über die drei Bausteine, die Magento für genau diesen Zweck vorsieht: die Filter-Deklaration in der listing.xml, die Frontend-Komponente in JavaScript und den serverseitigen Collection-Modifier.

2. Filter im UI-Component-XML deklarieren

Jede Spalte in der listing.xml kann über einen filters-Knoten oder direkt über das Argument der Spaltenkomponente einen eigenen Filter-Typ zugewiesen bekommen. Magento unterscheidet dabei zwischen dem sichtbaren Filter im Formular oberhalb des Grids und der dahinterliegenden Datenquelle, die den ausgewählten Wert später an die Collection weiterreicht. Für einen eigenen Range-Filter wird die Standardkomponente filterRange als Basis verwendet und um ein eigenes UI-Component-XML-Element ergänzt, das zwei Eingabefelder für Minimal- und Maximalwert rendert.

Wichtig ist die exakte Übereinstimmung des dataScope zwischen Filter-Komponente und dem Feldnamen, den der Collection-Modifier später erwartet. Ein Tippfehler an dieser Stelle führt zu einem stillen Fehler: Das Filterformular sieht korrekt aus, der Filter wird aber nie an die Collection weitergereicht, weil die UI-Component-Registry den Wert unter einem anderen Schlüssel ablegt als erwartet.


<!-- app/code/Mironsoft/AdminGridFilters/view/adminhtml/ui_component/product_stock_listing.xml -->
<column name="qty" sortOrder="30">
    <settings>
        <filter>rangeFilter</filter>
        <label translate="true">Lagerbestand</label>
        <dataType>number</dataType>
    </settings>
</column>

<argument name="data" xsi:type="array">
    <item name="js_config" xsi:type="array">
        <item name="component" xsi:type="string">
            Mironsoft_AdminGridFilters/js/grid/filters/range-filter
        </item>
    </item>
</argument>

3. Eigenen Range-Filter als KnockoutJS-Komponente implementieren

Die Frontend-Komponente für einen Range-Filter erbt idealerweise von Magento_Ui/js/grid/filters/filter, dem gemeinsamen Basis-Prototyp aller Filter-Widgets. Zwei Observable-Felder für den unteren und oberen Grenzwert reichen für die eigentliche Logik aus, entscheidend ist die korrekte Implementierung von getPreview für die lesbare Vorschau des aktiven Filters und getDataScope für die Übermittlung des Werts an die zentrale Filter-Registry, aus der der Server die Query-Parameter der Grid-Anfrage liest.

Der zweite Baustein ist das zugehörige Knockout-Template, das zwei nebeneinanderliegende Zahlenfelder mit Bindestrich-Trenner rendert. Da die Template-Datei unter view/adminhtml/web/template liegt und nicht automatisch geladen wird, muss sie in der js_config des Filters explizit referenziert werden, sonst rendert die Komponente nur ein leeres div ohne sichtbare Eingabefelder.


// app/code/Mironsoft/AdminGridFilters/view/adminhtml/web/js/grid/filters/range-filter.js
define(['Magento_Ui/js/grid/filters/filter'], function (Filter) {
    'use strict';

    return Filter.extend({
        defaults: {
            template: 'Mironsoft_AdminGridFilters/filters/range',
            valueFrom: '',
            valueTo: '',
            listens: {
                valueFrom: 'updatePreview',
                valueTo: 'updatePreview'
            }
        },

        /**
         * Get value that is displayed in a preview.
         *
         * @returns {String}
         */
        getPreview: function () {
            if (!this.valueFrom() && !this.valueTo()) {
                return '';
            }
            return this.valueFrom() + ' - ' + this.valueTo();
        },

        /**
         * Get filter data scope for server query parameters.
         *
         * @returns {Object}
         */
        getDataScope: function () {
            return {
                from: this.valueFrom(),
                to: this.valueTo()
            };
        }
    });
});

4. Multi-Select-Filter mit dynamischen Optionen

Ein Multi-Select-Filter mit statischer Optionsliste ist über die Standardkomponente Magento_Ui/js/grid/filters/elements/ui-select bereits abgedeckt und benötigt keinen Custom-Code. Anders sieht es aus, wenn die Optionsliste erst zur Laufzeit aus der Datenbank bestimmt wird, etwa alle aktuell vergebenen Lieferantennummern eines Katalogs. Für diesen Fall braucht die Filter-Komponente eine eigene optionsProvider-Klasse auf PHP-Seite, die das ArrayInterface implementiert und die Optionen aus der jeweiligen Tabelle liest, statt sie im XML hart zu kodieren.

Damit die Optionsliste nicht bei jedem einzelnen Grid-Request neu aus der Datenbank gelesen wird, lohnt sich ein einfacher In-Memory-Cache innerhalb der optionsProvider-Instanz für die Dauer eines Requests. Ein request-übergreifender Cache über die Magento-Cache-Frameworks ist hier meist nicht nötig, da die Optionsliste ohnehin nur beim ersten Rendern des Filterformulars und nicht bei jedem Sortier- oder Seitenwechsel neu abgefragt wird.


<?php

declare(strict_types=1);

namespace Mironsoft\AdminGridFilters\Model\Source;

use Magento\Framework\Data\OptionSourceInterface;
use Magento\Framework\App\ResourceConnection;

/**
 * Liefert die aktuell im Katalog vorhandenen Lieferantennummern
 * als dynamische Optionsliste für den Multi-Select-Grid-Filter.
 */
class SupplierCodeSource implements OptionSourceInterface
{
    /**
     * @param ResourceConnection $resourceConnection Zugriff auf die Datenbankverbindung
     */
    public function __construct(
        private readonly ResourceConnection $resourceConnection,
    ) {
    }

    /**
     * Baut die Optionsliste aus den distinct-Werten der Lieferantenspalte.
     *
     * @return array<int, array{value: string, label: string}>
     */
    public function toOptionArray(): array
    {
        $connection = $this->resourceConnection->getConnection();
        $select = $connection->select()
            ->from(
                $this->resourceConnection->getTableName('catalog_product_entity_varchar'),
                ['value']
            )
            ->distinct(true)
            ->where('value IS NOT NULL');

        $options = [];
        foreach ($connection->fetchCol($select) as $code) {
            $options[] = ['value' => $code, 'label' => $code];
        }

        return $options;
    }
}

5. Die zentrale Filter-Registry und der FilterPool

Auf Serverseite landet jeder aktive Filter zunächst nicht direkt bei der Collection, sondern beim Magento\Framework\View\Element\UiComponent\DataProvider\FilterPool. Dieser Pool ordnet jedem deklarierten Filter-Typ eine ApplierInterface-Implementierung zu, die weiß, wie ein bestimmter Filter-Wert in eine tatsächliche Where-Bedingung übersetzt wird. Für Standard-Filter existieren bereits Applier für Text-, Select- und Datumsbereich, ein Range-Filter über ein numerisches Attribut braucht dagegen einen eigenen Applier.

Die Registrierung eines eigenen Appliers erfolgt über di.xml als virtueller Type innerhalb der filter_pool-Argumente des betreffenden UI-Component-DataProviders. Wird dieser Schritt vergessen, bleibt der Filter im Formular sichtbar und aktivierbar, hat aber keinerlei Effekt auf die zurückgegebenen Grid-Zeilen, ein Fehlerbild, das sich in der Praxis erst bei genauerem Blick in den generierten SQL-Query erklärt.


<!-- app/code/Mironsoft/AdminGridFilters/etc/adminhtml/di.xml -->
<type name="Magento\Framework\View\Element\UiComponent\DataProvider\FilterPool">
    <arguments>
        <argument name="appliers" xsi:type="array">
            <item name="qty_range" xsi:type="object">
                Mironsoft\AdminGridFilters\Model\Filter\RangeFilterApplier
            </item>
        </argument>
    </arguments>
</type>

6. Der Collection-Modifier für die echte Datenbank-Filterung

Der eigentliche Applier ist eine schlanke Klasse, die das ApplierInterface implementiert und in ihrer apply-Methode die vom Filter übermittelten Rohdaten in eine addFieldToFilter-Bedingung auf der Collection übersetzt. An dieser Stelle greift die aus dem PHPStan-Regelwerk bekannte Vorgabe, addFieldToFilter niemals mit einem rohen Integer, sondern immer mit der expliziten Array-Form aufzurufen, weil sonst je nach Attributtyp falsch interpretierte Vergleichsoperatoren entstehen können.

Für den Range-Filter bedeutet das zwei verkettete Bedingungen, eine für den unteren und eine für den oberen Grenzwert, die beide nur gesetzt werden, wenn der jeweilige Wert im Request tatsächlich vorhanden ist. Wird nur die untere Grenze übermittelt, muss die obere Bedingung komplett entfallen, statt mit einem Leerwert eine immer wahre oder immer falsche Bedingung zu erzeugen, die das restliche Filterergebnis verfälscht.


<?php

declare(strict_types=1);

namespace Mironsoft\AdminGridFilters\Model\Filter;

use Magento\Framework\View\Element\UiComponent\DataProvider\Document;
use Magento\Framework\View\Element\UiComponent\DataProvider\Filter;
use Magento\Framework\View\Element\UiComponent\DataProvider\ApplierInterface;
use Magento\Framework\Data\Collection;

/**
 * Uebersetzt den Range-Filter-Wert in zwei verkettete
 * addFieldToFilter-Bedingungen auf der Grid-Collection.
 */
class RangeFilterApplier implements ApplierInterface
{
    /**
     * Wendet die Range-Bedingung auf die übergebene Collection an.
     *
     * @param Collection $collection Aktive Grid-Collection
     * @param Filter $filter Aktiver Filter mit Rohwerten aus dem Request
     * @return void
     */
    public function apply(Collection $collection, Filter $filter): void
    {
        $condition = $filter->getCondition();
        $field = $filter->getField();

        if (!is_array($condition)) {
            return;
        }

        if (!empty($condition['from'])) {
            $collection->addFieldToFilter($field, ['gteq' => $condition['from']]);
        }
        if (!empty($condition['to'])) {
            $collection->addFieldToFilter($field, ['lteq' => $condition['to']]);
        }
    }
}

7. Anbindung an einen benutzerdefinierten DataProvider

Grids, die auf einer eigenen Collection statt einer der Standard-Collections basieren, brauchen einen eigenen DataProvider, der von Magento\Ui\DataProvider\AbstractDataProvider erbt. Innerhalb dieses Providers wird der FilterPool über Dependency Injection eingebunden und in getSearchResult beziehungsweise in prepareUpdateUrl korrekt referenziert. Der klassische Fehler besteht darin, den FilterPool zwar zu injizieren, aber nie tatsächlich mit der Collection zu verbinden, sodass Filter zwar syntaktisch korrekt deklariert und registriert sind, aber schlicht nie aufgerufen werden.

Bei Custom-Collections, die mehrere Tabellen über Joins verbinden, muss außerdem sichergestellt sein, dass das im Filter referenzierte Feld eindeutig einer Tabelle zugeordnet werden kann. Existiert eine Spalte gleichen Namens in zwei gejointen Tabellen, wirft addFieldToFilter zur Laufzeit einen mehrdeutigen Spaltenfehler, der sich am einfachsten durch eine explizite Tabellen-Präfix-Notation im Feldnamen vermeiden lässt.

8. Typische Fehlerbilder beim Debugging eigener Filter

Drei Fehlerquellen tauchen in der Praxis immer wieder auf: ein dataScope-Mismatch zwischen Filter-Komponente und Applier-Erwartung, ein vergessener Cache-Flush nach Änderungen an der ui_component-XML, weil UI-Component-Deklarationen im generierten Layout-Cache landen, und ein Applier, der zwar registriert, aber im falschen Scope, adminhtml statt global, hinterlegt wurde. Alle drei Fehlerbilder äußern sich identisch: Der Filter sieht im Frontend korrekt aus, hat aber keine Wirkung auf die angezeigten Zeilen.

Am schnellsten lässt sich die Ursache über die Netzwerk-Ansicht der Browser-Entwicklertools eingrenzen: Der an den Server gesendete Request enthält im Query-Parameter filters die tatsächlich übermittelten Rohwerte. Fehlt dort der erwartete Schlüssel komplett, liegt das Problem im dataScope der Frontend-Komponente. Ist der Schlüssel vorhanden, das Ergebnis aber trotzdem ungefiltert, liegt der Fehler mit hoher Wahrscheinlichkeit im Applier oder dessen Registrierung.

9. Performance-Aspekte bei eigenen Grid-Filtern

Ein eigener Filter ist nur so performant wie die Spalte, auf die er angewendet wird. Range-Filter über EAV-Attribute, die in einer eigenen Wertetabelle statt in der Flat-Struktur des Grids liegen, führen ohne zusätzlichen Index schnell zu Full-Table-Scans, sobald der Katalog mehrere Hunderttausend Zeilen umfasst. Ein zusammengesetzter Index über entity_id und attribute_id reduziert die Kosten hier deutlich, sollte aber nur angelegt werden, wenn der Filter auch tatsächlich regelmäßig im Admin-Alltag genutzt wird.

Bei dynamischen Multi-Select-Optionen lohnt sich außerdem ein Blick auf die Anzahl der tatsächlich erzeugten IN-Klausel-Werte. Mehr als ein paar Dutzend gleichzeitig ausgewählte Optionen führen bei manchen Datenbank-Konfigurationen zu spürbar langsameren Ausführungsplänen als ein äquivalenter Join gegen eine temporäre Werteliste, ein Detail, das sich in der Praxis erst bei sehr großen Sortimenten überhaupt bemerkbar macht.

Filter-Typ Frontend-Komponente Server-seitiger Baustein Typischer Einsatzfall
select Magento_Ui/js/grid/filters/elements/select Standard-Applier (Core) Statuswerte, feste Enums
dateRange Magento_Ui/js/grid/filters/range Standard-Applier (Core) Erstellungs- und Änderungsdatum
Range-Filter (custom) eigene Erweiterung von filter.js eigener ApplierInterface Lagerbestand, Preis-Spanne
Multi-Select dynamisch ui-select mit optionsProvider eigene OptionSourceInterface-Klasse Lieferantencodes, Store-spezifische Werte
Text-Volltextsuche Magento_Ui/js/grid/filters/elements/input Standard-Applier (Core) SKU, Name, Freitext

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

Eigene Grid-Filter in Magento 2: Das Wichtigste auf einen Blick

Deklaration

Filter-Typ in listing.xml zuweisen und dataScope exakt auf den Feldnamen im Applier abstimmen.

Frontend

KnockoutJS-Komponente von Magento_Ui/js/grid/filters/filter ableiten, eigenes Template referenzieren.

Backend

ApplierInterface implementieren, im FilterPool via di.xml registrieren, addFieldToFilter nur mit Array-Form nutzen.

Performance

Bei EAV-Range-Filtern auf große Kataloge Indizes und IN-Klausel-Größe im Blick behalten.

11. FAQ: Eigene Grid-Filter in Magento 2: Das Wichtigste auf einen Blick

1Reicht ein eigenes JavaScript-Widget statt der vollen UI-Component-Filter-Pipeline?
Für sehr kleine, rein clientseitig gerenderte Grids kann das funktionieren, bei jedem Grid mit serverseitiger Pagination filtert ein reines Frontend-Widget aber nur die aktuell geladene Seite und nicht den gesamten Datensatz, was für Admin-Nutzer verwirrend ist.
2Warum wird mein eigener Filter im Formular angezeigt, hat aber keine Wirkung?
In den meisten Fällen fehlt die Registrierung des Appliers im FilterPool per di.xml, oder der dataScope der Frontend-Komponente stimmt nicht mit dem vom Applier erwarteten Feldnamen überein.
3Kann ein Range-Filter auch auf ein EAV-Attribut angewendet werden?
Ja, dafür muss der Applier statt eines direkten addFieldToFilter-Aufrufs die entsprechende EAV-Attribut-Join-Logik der jeweiligen Collection nutzen, da EAV-Werte nicht direkt als Spalte in der Haupttabelle liegen.
4Muss der Cache nach Änderungen an der ui_component-XML manuell geleert werden?
Ja, UI-Component-Deklarationen werden im Layout-Cache gehalten, ein bin/magento cache:clean layout ist nach Struktur-Änderungen an der XML in der Regel notwendig.
5Wie werden mehrere gleichzeitig aktive Filter kombiniert?
Der FilterPool ruft für jeden aktiven Filter den zugehörigen Applier separat auf, addFieldToFilter-Aufrufe innerhalb der Collection werden automatisch als UND-Verknüpfung kombiniert.
6Wo liegt der Unterschied zwischen filterSelect und einem eigenen Multi-Select?
filterSelect deckt statische, im XML fest hinterlegte Optionslisten ab, ein eigener Multi-Select mit optionsProvider wird für dynamisch aus der Datenbank ermittelte Optionslisten benötigt.
7Wie testet man einen eigenen Applier ohne den vollständigen Admin-Grid im Browser zu laden?
Ein Integrationstest, der den DataProvider direkt instanziiert und einen simulierten Filter-Request übergibt, prüft die erzeugte SQL-Bedingung ohne UI-Rendering und läuft dabei deutlich schneller als ein Browser-Test.
8Können Filter-Werte auch aus der URL vorbelegt werden?
Ja, die UI-Component-Registry liest aktive Filter aus dem bookmark-Zustand, der als Query-Parameter in der URL kodiert wird, ein direkter Link mit vorbelegtem Filter ist damit möglich.
9Was passiert, wenn der Applier eine Exception wirft?
Die Exception propagiert bis zur Grid-Request-Verarbeitung hoch und führt in der Regel zu einer generischen Fehlermeldung im Admin-Grid, weshalb ein Applier defensiv programmiert werden sollte und ungültige Eingaben lieber ignoriert als eine Exception auszulösen.
10Lohnt sich ein eigener Range-Filter für ein Attribut mit wenigen möglichen Werten?
Bei einer überschaubaren Anzahl fester Werte ist ein einfacher Select-Filter meist die bessere Wahl, ein Range-Filter lohnt sich erst bei echten numerischen Spannen wie Preis, Menge oder Datum.