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.
Inhaltsverzeichnis
- 1. Wo die Standard-Filter der UI-Components enden
- 2. Filter im UI-Component-XML deklarieren
- 3. Eigenen Range-Filter als KnockoutJS-Komponente implementieren
- 4. Multi-Select-Filter mit dynamischen Optionen
- 5. Die zentrale Filter-Registry und der FilterPool
- 6. Der Collection-Modifier für die echte Datenbank-Filterung
- 7. Anbindung an einen benutzerdefinierten DataProvider
- 8. Typische Fehlerbilder beim Debugging eigener Filter
- 9. Performance-Aspekte bei eigenen Grid-Filtern
- 10. Zusammenfassung
- 11. FAQ
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.