Magento 2: добавление колонки к гриду админки
Под катом пример добавления в гриде админки Magento 2 дополнительной колонки с данными из таблицы, связанной с основной таблицей грида, и «грязный хак» для работы фильтра по дополнительной колонке. Допускаю, что это не вполне «Magento 2 way», но это как-то работает, а потому — имеет право на существование.
Структура данных
Я решал задачу по формированию реферального дерева клиентов (клиент-родитель привлекает клиента-потомка), поэтому я создал дополнительную таблицу, завязанную на customer_entity
. Если коротко, то дополнительная таблица содержит отношение «родитель-потомок» и информацию по дереву («глубина залегания» клиента и путь к клиенту в дереве).
CREATE TABLE prxgt_dwnl_customer (
customer_id int(10) UNSIGNED NOT NULL COMMENT 'Reference to the customer.',
parent_id int(10) UNSIGNED NOT NULL COMMENT 'Reference to the customer''s parent.',
depth int(10) UNSIGNED NOT NULL DEFAULT 0 COMMENT 'Depth of the node in the tree.',
path varchar(255) NOT NULL COMMENT 'Path to the node - /1/2/3/.../'
PRIMARY KEY (customer_id),
CONSTRAINT FK_CUSTOMER FOREIGN KEY (customer_id)
REFERENCES customer_entity (entity_id) ON DELETE RESTRICT ON UPDATE RESTRICT,
CONSTRAINT FK_PARENT FOREIGN KEY (parent_id)
REFERENCES prxgt_dwnl_customer (customer_id) ON DELETE RESTRICT ON UPDATE RESTRICT
)
UI Component
Моей целью являлись 2 дополнительные колонки к гриду клиентов, содержащие информацию о родителе текущего клиента и о глубине залегания клиента в дереве. Грид клиентов описывается в XML-файле vendor/magento/module-customer/view/adminhtml/ui_component/customer_listing.xml
. Нас интересует узел dataSource
, а конкретно — имя источника данных (customer_listing_data_source
):
customer_listing_data_source
...
(что из этого является именем источника данных — атрибут name или argument-узел с именем name, сказать сложно, в Magento еще с первой версии есть хорошая традиция использовать одинаковые названия для различных типов элементов, чтобы держать разработчиков в тонусе)
Data Provider
Источником данных для грида является коллекция, как бы ни банально это звучало. Вот описание источника данных с именем customer_listing_data_source
в файле vendor/magento/module-customer/etc/di.xml
:
- Magento\Customer\Model\ResourceModel\Grid\Collection
...
Т.е., класс, который поставляет данные для грида клиентов — \Magento\Customer\Model\ResourceModel\Grid\Collection
.
Модификация коллекции
Если влезть отладчиком внутрь коллекции, то можно увидеть, что SQL-запрос для выборки данных выглядит примерно так:
SELECT `main_table`.* FROM `customer_grid_flat` AS `main_table`
Это другая хорошая традиция в Magento — преодолевать повышенную неповоротливость приложения, связанную с повышенной гибкостью, путим использования вот таких вот «индексных таблиц». В случае с клиентами flat-таблица есть, вполне возможно, что можно было бы встроиться и в нее, но я искал более универсальный путь. Мне нужен был JOIN.
Возможность JOIN’а я нашел только в методе \Magento\Framework\Model\ResourceModel\Db\Collection\AbstractCollection::_beforeLoad
:
protected function _beforeLoad()
{
...
$this->_eventManager->dispatch('core_collection_abstract_load_before', ['collection' => $this]);
...
}
Я подписался в своем модулей на событие core_collection_abstract_load_before
(файл etc/events.xml
):
И создал класс, реагирующий на это событие, в котором и модифицировал первоначальный запрос:
class CoreCollectionAbstractLoadBefore implements ObserverInterface
{
const AS_FLD_CUSTOMER_DEPTH = 'prxgtDwnlCustomerDepth';
const AS_FLD_PARENT_ID = 'prxgtDwnlParentId';
const AS_TBL_CUST = 'prxgtDwnlCust';
public function execute(\Magento\Framework\Event\Observer $observer)
{
$collection = $observer->getData('collection');
if ($collection instanceof \Magento\Customer\Model\ResourceModel\Grid\Collection) {
$query = $collection->getSelect();
$conn = $query->getConnection();
/* LEFT JOIN `prxgt_dwnl_customer` AS `prxgtDwnlCust` */
$tbl = [self::AS_TBL_CUST => $conn->getTableName('prxgt_dwnl_customer')];
$on = self::AS_TBL_CUST . 'customer_id.=main_table.entity_id';
$cols = [
self::AS_FLD_CUSTOMER_DEPTH => 'depth',
self::AS_FLD_PARENT_ID => 'parent_id'
];
$query->joinLeft($tbl, $on, $cols);
$sql = (string)$query;
/* dirty hack for filters goes here ... */
}
return;
}
}
В итоге, после модификации SQL-запрос стал выглядеть примерно так:
SELECT
`main_table`.*,
`prxgtDwnlCust`.`depth` AS `prxgtDwnlCustomerDepth`
`prxgtDwnlCust`.`parent_id` AS `prxgtDwnlParentId`
FROM `customer_grid_flat` AS `main_table`
LEFT JOIN `prxgt_dwnl_customer` AS `prxgtDwnlCust`
ON prxgtDwnlCust.customer_id = main_table.entity_id
Т.к. я использую алиасы для данных из «своей» таблицы (prxgtDwnlCustomerDepth и prxgtDwnlParentId), то я могу не сильно опасаться, что какой-то другой разработчик, применив подобный подход, совпадет со мной по наименованию дополнительных полей (вряд ли кто-то начнет называть свои данные с prxgt), но это же и привело к тому, что фильтрация с грида перестала работать.
Добавление колонки
Чтобы доопределить колонки в гриде нужно создать в своем модуле XML-файл с таким же именем, как и описывающий оригинальный UI-компонент (view/adminhtml/ui_component/customer_listing.xml
), и создать в нем дополнительные колонки, используя в качестве имен полей данных алиасы:
-
- textRange
- Parent ID
-
- textRange
- Depth
Результат
(колонки я подвигал руками и попрятал лишнее — отличная функция в новой Magento)
«Грязный хак» для фильтра
Чтобы заработали фильтры по новым столбцам я не придумал ничего лучшего, как сделать обратное преобразование «алиас» => «таблица.поле» все с том же классе по добавлению JOIN’а к первоначальному запросу (CoreCollectionAbstractLoadBefore
):
public function execute(\Magento\Framework\Event\Observer $observer)
{
...
/* the dirty hack */
$where = $query->getPart('where');
$replaced = $this->_replaceAllAliasesInWhere($where);
$query->setPart('where', $replaced);
...
}
protected function _replaceAllAliasesInWhere($where)
{
$result = [];
foreach ($where as $item) {
$item = $this->_replaceAliaseInWhere($item, self::AS_FLD_CUSTOMER_DEPTH, self::AS_TBL_CUST, 'depth');
$item = $this->_replaceAliaseInWhere($item, self::AS_FLD_PARENT_ID, self::AS_TBL_CUST, 'parent_id');
$result[] = $item;
}
return $result;
}
protected function _replaceAliaseInWhere($where, $fieldAlias, $tableAlias, $fieldName)
{
$search = "`$fieldAlias`";
$replace = "`$tableAlias`.`$fieldName`";
$result = str_replace($search, $replace, $where);
return $result;
}