Własna siatka administracyjna (admin grid) w Magento 2 to zadanie, które pojawia się przy każdym większym module. Magento dostarcza tu cały UI Component framework – deklaratywny XML definiuje kolumny, filtry i akcje, a PHP dostarcza tylko DataProvider. Pokazuję minimalny, ale kompletny przykład od migracji bazy przez model po listing.xml.
Struktura modułu
app/code/Vendor/Module/
├── etc/
│ ├── module.xml
│ ├── db_schema.xml
│ └── adminhtml/
│ ├── routes.xml
│ └── menu.xml
├── Model/
│ ├── Queue.php
│ └── ResourceModel/
│ ├── Queue.php
│ └── Queue/
│ └── Collection.php
├── Ui/
│ └── DataProvider/
│ └── Queue/
│ └── ListingDataProvider.php
├── Controller/
│ └── Adminhtml/
│ └── Queue/
│ ├── Index.php
│ └── Delete.php
└── view/
└── adminhtml/
├── layout/
│ └── vendor_module_queue_index.xml
└── ui_component/
└── vendor_module_queue_listing.xml
Schema bazy danych
<!-- etc/db_schema.xml -->
<schema xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:noNamespaceSchemaLocation="urn:magento:framework:Setup/Declaration/Schema/etc/schema.xsd">
<table name="vendor_module_queue" resource="default" engine="innodb"
comment="Module Queue">
<column xsi:type="int" name="queue_id" unsigned="true" nullable="false"
identity="true" comment="Queue ID"/>
<column xsi:type="varchar" name="status" nullable="false" length="32"
default="pending" comment="Status"/>
<column xsi:type="varchar" name="job_type" nullable="false" length="128"
comment="Job Type"/>
<column xsi:type="text" name="payload" nullable="true" comment="Payload JSON"/>
<column xsi:type="int" name="attempts" nullable="false" default="0"
comment="Retry attempts"/>
<column xsi:type="timestamp" name="created_at" nullable="false"
default="CURRENT_TIMESTAMP" comment="Created At"/>
<column xsi:type="timestamp" name="updated_at" nullable="false"
default="CURRENT_TIMESTAMP" on_update="true" comment="Updated At"/>
<constraint xsi:type="primary" referenceId="PRIMARY">
<column name="queue_id"/>
</constraint>
<index referenceId="VENDOR_MODULE_QUEUE_STATUS" indexType="btree">
<column name="status"/>
</index>
</table>
</schema>
Model, ResourceModel i Collection
<?php
// Model/Queue.php
namespace Vendor\Module\Model;
use Magento\Framework\Model\AbstractModel;
class Queue extends AbstractModel
{
protected function _construct(): void
{
$this->_init(ResourceModel\Queue::class);
}
}
// Model/ResourceModel/Queue.php
namespace Vendor\Module\Model\ResourceModel;
use Magento\Framework\Model\ResourceModel\Db\AbstractDb;
class Queue extends AbstractDb
{
protected function _construct(): void
{
$this->_init('vendor_module_queue', 'queue_id');
}
}
// Model/ResourceModel/Queue/Collection.php
namespace Vendor\Module\Model\ResourceModel\Queue;
use Magento\Framework\Model\ResourceModel\Db\Collection\AbstractCollection;
use Vendor\Module\Model\Queue;
use Vendor\Module\Model\ResourceModel\Queue as QueueResource;
class Collection extends AbstractCollection
{
protected function _construct(): void
{
$this->_init(Queue::class, QueueResource::class);
}
}
DataProvider
<?php
// Ui/DataProvider/Queue/ListingDataProvider.php
namespace Vendor\Module\Ui\DataProvider\Queue;
use Magento\Ui\DataProvider\AbstractDataProvider;
use Vendor\Module\Model\ResourceModel\Queue\CollectionFactory;
class ListingDataProvider extends AbstractDataProvider
{
public function __construct(
string $name,
string $primaryFieldName,
string $requestFieldName,
CollectionFactory $collectionFactory,
array $meta = [],
array $data = []
) {
parent::__construct($name, $primaryFieldName, $requestFieldName, $meta, $data);
$this->collection = $collectionFactory->create();
}
public function getData(): array
{
if (!$this->getCollection()->isLoaded()) {
$this->getCollection()->load();
}
$items = $this->getCollection()->toArray();
return [
'totalRecords' => $this->getCollection()->getSize(),
'items' => array_values($items['items'] ?? $items),
];
}
}
UI Component – listing.xml
<!-- view/adminhtml/ui_component/vendor_module_queue_listing.xml -->
<listing xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:noNamespaceSchemaLocation="urn:magento:module:Magento_Ui:etc/ui_configuration.xsd">
<argument name="data" xsi:type="array">
<item name="js_config" xsi:type="array">
<item name="provider" xsi:type="string">vendor_module_queue_listing.vendor_module_queue_listing_data_source</item>
</item>
</argument>
<settings>
<buttons>
<button name="add">
<url path="*/*/new"/>
<class>primary</class>
<label translate="true">Add New Job</label>
</button>
</buttons>
<spinner>vendor_module_queue_columns</spinner>
<deps>
<dep>vendor_module_queue_listing.vendor_module_queue_listing_data_source</dep>
</deps>
</settings>
<dataSource name="vendor_module_queue_listing_data_source" component="Magento_Ui/js/grid/provider">
<settings>
<storageConfig>
<param name="indexField" xsi:type="string">queue_id</param>
</storageConfig>
<updateUrl path="mui/index/render"/>
</settings>
<aclResource>Vendor_Module::queue</aclResource>
<dataProvider class="Vendor\Module\Ui\DataProvider\Queue\ListingDataProvider"
name="vendor_module_queue_listing_data_source">
<settings>
<requestFieldName>queue_id</requestFieldName>
<primaryFieldName>queue_id</primaryFieldName>
</settings>
</dataProvider>
</dataSource>
<listingToolbar name="listing_top">
<settings>
<sticky>true</sticky>
</settings>
<bookmark name="bookmarks"/>
<columnsControls name="columns_controls"/>
<filterSearch name="fulltext"/>
<filters name="listing_filters">
<settings>
<templates>
<filters>
<select>
<param name="template" xsi:type="string">ui/grid/filters/elements/ui-select</param>
<param name="component" xsi:type="string">Magento_Ui/js/form/element/ui-select</param>
</select>
</filters>
</templates>
</settings>
</filters>
<paging name="listing_paging"/>
</listingToolbar>
<columns name="vendor_module_queue_columns">
<selectionsColumn name="ids">
<settings>
<indexField>queue_id</indexField>
</settings>
</selectionsColumn>
<column name="queue_id">
<settings>
<filter>textRange</filter>
<label translate="true">ID</label>
<sorting>asc</sorting>
</settings>
</column>
<column name="job_type">
<settings>
<filter>text</filter>
<label translate="true">Job Type</label>
</settings>
</column>
<column name="status" component="Magento_Ui/js/grid/columns/select">
<settings>
<options class="Vendor\Module\Model\Source\Status"/>
<filter>select</filter>
<dataType>select</dataType>
<label translate="true">Status</label>
</settings>
</column>
<column name="attempts">
<settings>
<filter>textRange</filter>
<label translate="true">Attempts</label>
</settings>
</column>
<column name="created_at" class="Magento\Ui\Component\Listing\Columns\Date"
component="Magento_Ui/js/grid/columns/date">
<settings>
<filter>dateRange</filter>
<dataType>date</dataType>
<label translate="true">Created</label>
</settings>
</column>
<actionsColumn name="actions" class="Vendor\Module\Ui\Component\Listing\Column\QueueActions">
<settings>
<indexField>queue_id</indexField>
</settings>
</actionsColumn>
</columns>
</listing>
Kolumna akcji
<?php
// Ui/Component/Listing/Column/QueueActions.php
namespace Vendor\Module\Ui\Component\Listing\Column;
use Magento\Framework\UrlInterface;
use Magento\Framework\View\Element\UiComponent\ContextInterface;
use Magento\Framework\View\Element\UiComponentFactory;
use Magento\Ui\Component\Listing\Columns\Column;
class QueueActions extends Column
{
private const URL_PATH_DELETE = 'vendor_module/queue/delete';
private const URL_PATH_EDIT = 'vendor_module/queue/edit';
public function __construct(
ContextInterface $context,
UiComponentFactory $uiComponentFactory,
private readonly UrlInterface $urlBuilder,
array $components = [],
array $data = []
) {
parent::__construct($context, $uiComponentFactory, $components, $data);
}
public function prepareDataSource(array $dataSource): array
{
if (!isset($dataSource['data']['items'])) {
return $dataSource;
}
foreach ($dataSource['data']['items'] as &$item) {
$name = $this->getData('name');
if (!isset($item['queue_id'])) continue;
$item[$name] = [
'edit' => [
'href' => $this->urlBuilder->getUrl(self::URL_PATH_EDIT, ['queue_id' => $item['queue_id']]),
'label' => __('Edit'),
],
'delete' => [
'href' => $this->urlBuilder->getUrl(self::URL_PATH_DELETE, ['queue_id' => $item['queue_id']]),
'label' => __('Delete'),
'confirm' => [
'title' => __('Delete Job'),
'message' => __('Are you sure you want to delete this job?'),
],
'post' => true,
],
];
}
return $dataSource;
}
}
Po wygenerowaniu modułu: bin/magento setup:upgrade tworzy tabelę, bin/magento cache:flush odświeża UI Components. Grid pojawia się w panelu admin po skonfigurowaniu menu.xml i routes.xml. Masowe akcje (delete selected) dodaje się przez massaction element w listingToolbar oraz osobny kontroler MassDelete dziedziczący z AbstractMassAction.
