Data providers¶
A grid asks a data provider for rows. The provider is chosen by alias in the definition's dataSource, and the alias is bound to a class in DI. Two things follow: a JSON file can never instantiate an arbitrary class, and the same definition can be pointed at different data by changing one DI binding.
The collection provider¶
grid.collection is bound to Nebula\Grid\Model\DataProvider\CollectionProvider. It wraps any object that extends Magento\Framework\Data\Collection: plain database collections, EAV collections such as the product collection, and in-memory list collections such as the cache and index management grids.
"dataSource": {
"provider": "grid.collection",
"config": {
"collection": "customer_group.collection",
"method": "addTaxClass"
}
}
config key |
Meaning |
|---|---|
collection |
An alias bound on CollectionRegistry. Required. Sixty-six already ship bound, listed under Collections already bound. |
method |
An argument-less method called on the fresh collection before filters are applied, for a join or a scope the collection offers as a method. The three shipped uses are addTaxClass, addCustomerDetails and useOnlyActual. A method that takes an argument cannot go here. prepareCollection() checks the name with method_exists() and then calls it with no arguments, outside the try/catch that guards the fetch, so "method": "addStoreFilter" raises an uncaught ArgumentCountError rather than rendering an empty grid. Store scoping belongs in a provider that extends CollectionProvider and overrides prepareCollection(), the way the reviews grid adds its per-store visibility join. scope binds a grid to one parent record and is not a store filter. |
scope |
Binds the grid to one parent record: {"field": "<collection field>", "param": "<request param>"}. CollectionProvider reads this key itself and filters the collection on field with the value of the request parameter named by param. It fails closed: with scope declared and no value in the request, the grid renders empty rather than every row. The param is carried on every server-rendered grid link and on the export URL. See the scoped-grid example. |
A new collection instance is created per request, because collections carry filter and page state.
Binding a collection¶
<type name="Nebula\Grid\Model\Registry\CollectionRegistry">
<arguments>
<argument name="bindings" xsi:type="array">
<item name="vendor_module_note.collection" xsi:type="string">Vendor\Module\Model\ResourceModel\Note\Collection</item>
</argument>
</arguments>
</type>
An alias that is not bound raises UnknownAliasException, which is logged, and the grid renders empty. A class name in the JSON is treated as an alias and fails the same way.
Collections already bound¶
Sixty-six aliases ship bound on CollectionRegistry, covering most stock entities. Reuse one rather than binding a second alias to the same collection.
The 66 shipped collection aliases
Nebula_Theme
| Alias | Collection |
|---|---|
catalog_rule.collection |
Magento\CatalogRule\Model\ResourceModel\Rule\Collection |
cms_block.collection |
Magento\Cms\Model\ResourceModel\Block\Collection |
cms_page.collection |
Magento\Cms\Model\ResourceModel\Page\Collection |
customer_group.collection |
Magento\Customer\Model\ResourceModel\Group\Collection |
customer_online.collection |
Magento\Customer\Model\ResourceModel\Online\Grid\Collection |
login_as_customer_log.collection |
Magento\LoginAsCustomerLog\Model\ResourceModel\Log\Collection |
email_template.collection |
Magento\Email\Model\ResourceModel\Template\Collection |
inventory_source.collection |
Magento\Inventory\Model\ResourceModel\Source\Collection |
inventory_stock.collection |
Magento\Inventory\Model\ResourceModel\Stock\Collection |
integration.collection |
Magento\Integration\Model\ResourceModel\Integration\Collection |
newsletter_subscriber.collection |
Magento\Newsletter\Model\ResourceModel\Subscriber\Collection |
order_status.collection |
Magento\Sales\Model\ResourceModel\Order\Status\Collection |
product_attributes.collection |
Magento\Catalog\Model\ResourceModel\Product\Attribute\Collection |
sales_order.collection |
Magento\Sales\Model\ResourceModel\Order\Grid\Collection |
sales_order_creditmemo.collection |
Magento\Sales\Model\ResourceModel\Order\Creditmemo\Grid\Collection |
sales_order_invoice.collection |
Magento\Sales\Model\ResourceModel\Order\Invoice\Grid\Collection |
sales_order_shipment.collection |
Magento\Sales\Model\ResourceModel\Order\Shipment\Grid\Collection |
paypal_billing_agreement.collection |
Magento\Paypal\Model\ResourceModel\Billing\Agreement\Collection |
sales_rule.collection |
Magento\SalesRule\Model\ResourceModel\Rule\Collection |
newsletter_queue.collection |
Magento\Newsletter\Model\ResourceModel\Queue\Grid\Collection |
newsletter_template.collection |
Magento\Newsletter\Model\ResourceModel\Template\Collection |
review_product.collection |
Magento\Review\Model\ResourceModel\Review\Product\Collection |
sales_transaction.collection |
Magento\Sales\Model\ResourceModel\Transaction\Grid\Collection |
search_synonyms.collection |
Magento\Search\Model\ResourceModel\Synonyms\Grid\Collection |
search_term.collection |
Magento\Search\Model\ResourceModel\Query\Collection |
sitemap.collection |
Magento\Sitemap\Model\ResourceModel\Sitemap\Collection |
tax_rate.collection |
Magento\Tax\Model\ResourceModel\Calculation\Rate\Collection |
tax_rule.collection |
Magento\Tax\Model\ResourceModel\Calculation\Rule\Collection |
url_rewrite.collection |
Magento\UrlRewrite\Model\ResourceModel\UrlRewriteCollection |
widget_instance.collection |
Magento\Widget\Model\ResourceModel\Widget\Instance\Collection |
Nebula_Reports
| Alias | Collection |
|---|---|
report_product_viewed.collection |
Nebula\Reports\Model\ResourceModel\Report\Product\Viewed\Collection |
refresh_statistics.collection |
Magento\Reports\Model\ResourceModel\Refresh\Collection |
report_product_lowstock.collection |
Nebula\Reports\Model\ResourceModel\Product\Lowstock\Collection |
report_product_sold.collection |
Nebula\Reports\Model\ResourceModel\Product\Sold\Collection |
report_product_downloads.collection |
Nebula\Reports\Model\ResourceModel\Product\Downloads\Collection |
report_review_customer.collection |
Magento\Reports\Model\ResourceModel\Review\Customer\Collection |
report_review_product.collection |
Magento\Reports\Model\ResourceModel\Review\Product\Collection |
report_review_detail.collection |
Magento\Reports\Model\ResourceModel\Review\Collection |
report_shopcart_product.collection |
Nebula\Reports\Model\ResourceModel\Shopcart\Product\Collection |
report_shopcart_abandoned.collection |
Nebula\Reports\Model\ResourceModel\Shopcart\Abandoned\Collection |
search_term_report.collection |
Magento\Search\Model\ResourceModel\Query\Collection |
report_sales_sales.collection |
Nebula\Reports\Model\ResourceModel\Sales\Order\Collection |
report_sales_tax.collection |
Nebula\Reports\Model\ResourceModel\Sales\Tax\Collection |
report_sales_invoiced.collection |
Nebula\Reports\Model\ResourceModel\Sales\Invoiced\Collection |
report_sales_refunded.collection |
Nebula\Reports\Model\ResourceModel\Sales\Refunded\Collection |
report_sales_shipping.collection |
Nebula\Reports\Model\ResourceModel\Sales\Shipping\Collection |
report_sales_coupons.collection |
Nebula\Reports\Model\ResourceModel\Sales\Coupons\Collection |
report_sales_bestsellers.collection |
Nebula\Reports\Model\ResourceModel\Sales\Bestsellers\Collection |
report_customer_accounts.collection |
Nebula\Reports\Model\ResourceModel\Customer\Accounts\Collection |
report_customer_orders.collection |
Nebula\Reports\Model\ResourceModel\Customer\Orders\Collection |
report_customer_totals.collection |
Nebula\Reports\Model\ResourceModel\Customer\Totals\Collection |
paypal_settlement.collection |
Magento\Paypal\Model\ResourceModel\Report\Settlement\Row\Collection |
Nebula_System
| Alias | Collection |
|---|---|
import_history.collection |
Magento\ImportExport\Model\ResourceModel\History\Collection |
bulk.collection |
Nebula\System\Model\ResourceModel\Bulk\Grid\Collection |
variable.collection |
Magento\Variable\Model\ResourceModel\Variable\Collection |
notification.collection |
Magento\AdminNotification\Model\ResourceModel\Grid\Collection |
cache_types.collection |
Magento\Backend\Model\Cache\ResourceModel\Grid\Collection |
indexer_list.collection |
Magento\Indexer\Ui\DataProvider\Indexer\DataCollection |
backup.collection |
Magento\Backup\Model\Fs\Collection |
Nebula_Store
| Alias | Collection |
|---|---|
design_schedule.collection |
Magento\Theme\Model\ResourceModel\Design\Collection |
design_theme.collection |
Magento\Theme\Model\ResourceModel\Theme\Grid\Collection |
store.collection |
Magento\Store\Model\ResourceModel\Website\Grid\Collection |
Nebula_User
| Alias | Collection |
|---|---|
admin_user.collection |
Magento\User\Model\ResourceModel\User\Collection |
admin_user_locked.collection |
Magento\User\Model\ResourceModel\User\Locked\Collection |
admin_role.collection |
Magento\Authorization\Model\ResourceModel\Role\Grid\Collection |
Nebula_LandingPage
| Alias | Collection |
|---|---|
nebula_landing_page.collection |
Nebula\LandingPage\Model\ResourceModel\LandingPage\Collection |
Nebula_CrudFixture binds cms_block.collection and cms_block.form to its own classes. It is generator output for the generator's tests and is never installed on a store, so those two bindings are not part of the set above. See Modules.
Where a column's data comes from¶
Every column key is read from the collection's items with getData($key). If the column is not in the collection's select, the cell is empty and a filter on it fails. To add a joined column, add the join in the collection: a plugin on _initSelect(), or an argument-less method in config that performs the join.
What the request looks like¶
The grid carries these on its own links and on the export URL, and GridDataLoader
hands them to the provider under these names:
| Parameter | Type | Meaning |
|---|---|---|
page |
int | 1-based page number |
pageSize |
int | rows per page, clamped |
sort |
string | column key |
sortDir |
asc or desc |
the request param is dir on the page and sortDir on the export URL |
filters |
map | column key to value; a range or date filter arrives as <key>_from and <key>_to |
search |
string | keyword search over searchable columns |
GridRequestApplier applies them with three guarantees:
- A request can only sort by a column the grid declares. A sort key that is not a declared column is ignored.
- A request can only filter on a column the grid declares. The same check, made before any DB identifier is derived from the key.
- A page is always bounded. The ceiling is 200 rows for an interactive page and 100,000 for an export. A missing or non-positive
pageSizefalls back toDEFAULT_PAGE_SIZE, which is 20, on an interactive request and toMAX_EXPORT_PAGE_SIZE, which is 100,000, on an export. It never means "everything".
What applyFilters() does with a declared filter is narrower than the grid UI suggests. It handles two shapes: a select filter as equality, and everything else as LIKE %value%. It does not assemble range or date filters, because price_from is not a declared column and is skipped by the same whitelist. Use declaredFilters() instead, which deliberately keeps the _from and _to keys by matching them against the base column name, then build the ['from' => $from, 'to' => $to] condition yourself and move date bounds from the store timezone to UTC the way CollectionProvider::dayBoundary() does. The generic grid.collection provider already does all of this, so it is only a concern in a provider of your own: a range or date filter left to applyFilters() alone silently does nothing.
Writing your own provider¶
Implement Nebula\Component\Api\V1\GridDataProviderInterface:
namespace Vendor\Module\Model\DataProvider;
use Nebula\Component\Api\V1\GridDataProviderInterface;
use Nebula\Grid\Model\DataProvider\GridRequestApplier;
class ExternalOrdersProvider implements GridDataProviderInterface
{
public function __construct(
private readonly \Vendor\Module\Model\Api\Client $client,
private readonly GridRequestApplier $applier,
) {
}
/**
* @param array<string, mixed> $config dataSource.config from the definition, plus `columns`
* @param array<string, mixed> $params page, pageSize, sort, sortDir, filters, search, and export on an export request
* @return array{items: array<int, array<string, mixed>>, totalCount: int}
*/
public function getData(array $config, array $params = []): array
{
$columns = $config['columns'] ?? [];
$export = !empty($params['export']);
$ceiling = $export ? GridRequestApplier::MAX_EXPORT_PAGE_SIZE : GridRequestApplier::MAX_PAGE_SIZE;
$pageSize = (int) ($params['pageSize'] ?? 0);
if ($pageSize <= 0) {
$pageSize = $export ? $ceiling : GridRequestApplier::DEFAULT_PAGE_SIZE;
}
$pageSize = min($pageSize, $ceiling);
$page = max(1, (int) ($params['page'] ?? 1));
$sort = (string) ($params['sort'] ?? '');
if (!isset($columns[$sort]) || empty($columns[$sort]['sortable'])) {
$sort = 'created_at';
}
$result = $this->client->search(
filters: $this->applier->declaredFilters($params, $columns),
sort: $sort,
direction: ($params['sortDir'] ?? 'desc') === 'asc' ? 'asc' : 'desc',
offset: ($page - 1) * $pageSize,
limit: $pageSize,
);
return ['items' => $result->rows(), 'totalCount' => $result->total()];
}
}
Return items as a list of associative arrays keyed by column key, and totalCount as the count before paging so the pager is right. The engine passes the definition's columns in config['columns']. GridRequestApplier::declaredFilters() returns only the filters whose keys are declared columns, and its applyPaging(), applySorting(), applyFilters() and applySearch() methods do the rest when your source is a Magento collection, apart from the range and date filters described above. When it is not a collection, keep the three guarantees yourself as above: clamp the page size, whitelist the sort key, and read filters through declaredFilters().
Bind it:
<type name="Nebula\Component\Model\Registry\DataProviderRegistry">
<arguments>
<argument name="bindings" xsi:type="array">
<item name="vendor.external_orders" xsi:type="string">Vendor\Module\Model\DataProvider\ExternalOrdersProvider</item>
</argument>
</arguments>
</type>
And use it:
Anything in config is passed to your provider untouched, so it is the place for provider-specific settings.
Providers already bound¶
Fifty aliases ship bound on DataProviderRegistry, and between them they back every shipped screen. Check the list before writing a provider: pointing a definition at an alias that already exists costs one line. The .form and .grid suffixes are a naming convention and not a rule the engine enforces.
The 50 shipped provider aliases
Nebula_Grid
| Alias | Class |
|---|---|
grid.collection |
Nebula\Grid\Model\DataProvider\CollectionProvider |
nebula.grid.row |
Nebula\Grid\Model\InlineEdit\RowFormDataProvider |
Nebula_Theme
| Alias | Class |
|---|---|
agreement.grid |
Nebula\Theme\Model\DataProvider\AgreementGridProvider |
attribute.form |
Nebula\Theme\Model\DataProvider\AttributeFormProvider |
attribute_set.grid |
Nebula\Theme\Model\DataProvider\AttributeSetProvider |
catalog_rule.form |
Nebula\Theme\Model\DataProvider\CatalogRuleProvider |
category.form |
Nebula\Theme\Model\DataProvider\CategoryFormProvider |
cms_block.form |
Nebula\Theme\Model\DataProvider\CmsBlockProvider |
cms_page.form |
Nebula\Theme\Model\DataProvider\CmsPageProvider |
customer.form |
Nebula\Theme\Model\DataProvider\CustomerFormProvider |
customer.grid |
Nebula\Theme\Model\DataProvider\CustomerGridProvider |
customer_group.form |
Nebula\Theme\Model\DataProvider\CustomerGroupProvider |
email_template.form |
Nebula\Theme\Model\DataProvider\EmailTemplateProvider |
newsletter_template.form |
Nebula\Theme\Model\DataProvider\NewsletterTemplateProvider |
newsletter_queue.form |
Nebula\Theme\Model\DataProvider\NewsletterQueueProvider |
sitemap.form |
Nebula\Theme\Model\DataProvider\SitemapProvider |
search_term.form |
Nebula\Theme\Model\DataProvider\SearchTermProvider |
order_status.form |
Nebula\Theme\Model\DataProvider\OrderStatusProvider |
sales_order_address.form |
Nebula\Theme\Model\DataProvider\OrderAddressProvider |
inventory_source.form |
Nebula\Theme\Model\DataProvider\SourceProvider |
inventory_stock.form |
Nebula\Theme\Model\DataProvider\StockProvider |
search_synonyms.form |
Nebula\Theme\Model\DataProvider\SynonymGroupProvider |
order_status.grid |
Nebula\Theme\Model\DataProvider\OrderStatusGridProvider |
review.form |
Nebula\Theme\Model\DataProvider\ReviewFormProvider |
checkout_agreement.form |
Nebula\Theme\Model\DataProvider\CheckoutAgreementProvider |
grid.review.collection |
Nebula\Theme\Model\DataProvider\ReviewCollectionProvider |
grid.review.pending.collection |
Nebula\Theme\Model\DataProvider\PendingReviewCollectionProvider |
product.form |
Nebula\Theme\Model\DataProvider\ProductFormProvider |
product.grid |
Nebula\Theme\Model\DataProvider\ProductProvider |
sales_rule.form |
Nebula\Theme\Model\DataProvider\SalesRuleProvider |
sales_rule.coupons.grid |
Nebula\Theme\Model\DataProvider\SalesRuleCouponsGridProvider |
rating.grid |
Nebula\Theme\Model\DataProvider\RatingGridProvider |
review_rating.form |
Nebula\Theme\Model\DataProvider\RatingProvider |
tax_rate.form |
Nebula\Theme\Model\DataProvider\TaxRateProvider |
integration.form |
Nebula\Theme\Model\DataProvider\IntegrationProvider |
tax_rule.form |
Nebula\Theme\Model\DataProvider\TaxRuleProvider |
url_rewrite.form |
Nebula\Theme\Model\DataProvider\UrlRewriteProvider |
Nebula_System
| Alias | Class |
|---|---|
variable.form |
Nebula\System\Model\DataProvider\VariableProvider |
backup.grid |
Nebula\System\Model\DataProvider\BackupGridProvider |
Nebula_Store
| Alias | Class |
|---|---|
design_config.grid |
Nebula\Store\Model\DataProvider\DesignConfigProvider |
design_config.form |
Nebula\Store\Model\DataProvider\DesignConfigFormProvider |
store_group.form |
Nebula\Store\Model\DataProvider\StoreGroupProvider |
store_view.form |
Nebula\Store\Model\DataProvider\StoreViewProvider |
store_website.form |
Nebula\Store\Model\DataProvider\WebsiteProvider |
design_schedule.form |
Nebula\Store\Model\DataProvider\DesignScheduleProvider |
design_theme.form |
Nebula\Store\Model\DataProvider\ThemeFormProvider |
Nebula_User
| Alias | Class |
|---|---|
admin_user.form |
Nebula\User\Model\DataProvider\AdminUserProvider |
admin_role.form |
Nebula\User\Model\DataProvider\AdminRoleProvider |
admin_account.form |
Nebula\User\Model\DataProvider\AccountProvider |
Nebula_LandingPage
| Alias | Class |
|---|---|
nebula_landing_page.form |
Nebula\LandingPage\Model\DataProvider\LandingPageFormDataProvider |
Filter option sources¶
A select filter takes its options from filterOptions inline, or from a filterOptionsSource alias bound on FilterOptionSourceRegistry. A picker filter takes them from a filterSource alias bound on SelectionSourceRegistry, which searches as the administrator types and is what the product grid's category filter uses. Both are described in the Grid reference.
Events¶
nebula_grid_data_fetched_after fires after a provider returns, carrying the grid id, the resolved config, the request params, the rows and the total count, so an observer can decorate rows without replacing the provider. It does not fire for exports, which call the provider directly. Payloads are listed in ACL resources and events.