Долгое время работаю с одним проектом. В проекте было довольно много связок между инфоблоками/хайлоадами и в результате шило в одном месте сподвигло перевести все данные на сущности. В результате - вышло около 60 сущностей, связанных между собой.
И тут возникла проблемка - отображение всего этого безобразия для людей, далеких от программирования, битрикса, и вообще. Забегу наперед, до единого инструмента редактирования сущностей еще не добрался. Отображение в хайлоадах тоже не дает возможности показать данные связанных сущностей
(тут сам чуть не запутался в описании) и привязку с доп. данными.
Типовой механизм создания административной страницы есть, но как по мне он не совсем удобен. Нет, сам код хорош, но расписывать для каждой сущности файл на 300 строк меня не вдохновил. Запасся кофе, сигаретами и в результате появился немного более удобный инструмент, которым хочу поделиться.
ЗЫ: Тапочками, овощами и яйцами прошу не кидаться - класс довольно сырой, не рассчитан на миллион записей связанной сущности, но со своей задачей справляется. Может кому пригодится.
Итак, что у нас есть и что мы умеем.
Интерфейс:
1. Типовое отображение таблицы из CDBResult с помощью CAdminList
2. Отображение NAME-cвойства связанной сущности, из значения серилизованного свойства текущей в вариациях:
2.1. array("ID_другой_сущности" => значение, ["ID_другой_сущности" => значение, ...])
2.2 array("ID_другой_сущности", ["ID_другой_сущности", ...])
3. Фильтрация по int, string полям текущей сущности и связанных с ней других сущностей, через ReferenceField
Создал для примера три самые простые сущности (буду тренироваться на
кошках б/у машинах на доноров)
1. Тип машины
<?
/**
* @author Vadim Podovalov <lscin.ib@gmail.com>
*/
namespace IB\Main\Car;
use Bitrix\Main\Entity;
class TypeTable extends Entity\DataManager
{
public static function getTableName()
{
return 'test_type_car';
}
public static function getMap()
{
return array(
new Entity\IntegerField('ID', array(
'primary' => true,
'autocomplete' => true
)),
new Entity\StringField('NAME', array(
'required' => true,
'column_name' => 'UF_NAME'
)),
);
}
}
?>
|
2. Сама машина
Связана с сущностью "Тип машины".
В поле PARTS буду писать серилизованный массив, ключами которого будут ID из 3-ей сущности, а значениями - количество "оставшихся" в машине запчастей.
<?
/**
* @author Vadim Podovalov <lscin.ib@gmail.com>
*/
namespace IB\Main\Car;
use Bitrix\Main\Entity;
class ModelTable extends Entity\DataManager
{
public static function getTableName()
{
return 'ib_main_car_model';
}
public static function getMap()
{
return array(
new Entity\IntegerField('ID', array(
'primary' => true,
'autocomplete' => true
)),
new Entity\StringField('MODEL', array(
'required' => true,
'column_name' => 'UF_MODEL'
)),
new Entity\IntegerField('TYPE_ID', array(
'required' => true,
'column_name' => 'UF_TYPE'
)),
new Entity\StringField('PARTS', array(
'column_name' => 'UF_PARTS',
'serialized' => true
)),
new Entity\IntegerField('YEAR', array(
'required' => true,
'column_name' => 'UF_YEAR'
)),
new Entity\ReferenceField(
'TYPE',
'IB\Main\Car\Type',
array('=this.TYPE_ID' => 'ref.ID'),
array('join_type' => 'LEFT')
),
);
}
}
?>
|
3. Запчасти
<?
/**
* @author Vadim Podovalov <lscin.ib@gmail.com>
*/
namespace IB\Main\Car;
use Bitrix\Main\Entity;
class PartsTable extends Entity\DataManager
{
public static function getTableName()
{
return 'test_parts_car';
}
public static function getMap()
{
return array(
new Entity\IntegerField('ID', array(
'primary' => true,
'autocomplete' => true
)),
new Entity\StringField('NAME', array(
'required' => true,
'column_name' => 'UF_NAME'
)),
);
}
}
?>
|
Эти три сущности, де-факто описывают один объект (машину). Это тестовый пример - на боевом проекте у меня может объект составляться из 5-8 сущностей, которые в свою очередь, тоже имеют свои связи. И это все нужно удобно показать менеджеру в админке сайта.
Как выглядит результат:
У нас отображается объект Машина, поле NAME (могут быть абсолютно любые) связанной с ней сущности Тип, а данные из поля PARTS тянут поле NAME связанной сущности, вместо ключа массива.
Фильтр генерируется в зависимости от выбранных полей в select. Работает как по "родным" полям, так и по полям связанной сущности:
А теперь заглянем "под капот":
1. Сам абстрактный "базовый" класс, от которого будем наследоваться. Добавил комментариев.
<?
/**
* @author Vadim Podovalov <lscin.ib@gmail.com>
*/
namespace IB\Main\General\Admin;
abstract class ShowAdminObject
{
protected $obAdmin = null;
protected $obFilter = null;
protected $bShowAll = true;
protected $sTableID = "";
protected $arFields = array();
protected $arSelect = array();
protected $arFilter = array();
protected $arSetFilter = array();
protected $arVarData = array();
function __construct($sTableID)
{
$this->sTableID = $sTableID;
$this->SetSelect();
/*
Класс описывает параметры сортировки строки таблицы списка элементов на административной странице по любой из колонок,
для которых предусмотрена сортировка.
Параметр Описание
table_id Идентификатор таблицы.
by_inital Идентификатор колонки, по которой идет сортировка по умолчанию.
order_inital Направление сортировки по умолчанию ({'asc'|'desc'}).
by_name Имя GET-переменной, в которой будет содержаться идентификатор колонки, по которой производится сортировка. Не обязательный параметр. По умолчанию - 'by'
order_name Имя GET-переменной, в которой будет содержаться направление сортировки. Не обязательный параметр. По умолчанию - 'order'
*/
$oSort = new \CAdminSorting($this->sTableID, "ID", "asc");
/*
Конструктор класса списка.
Параметр Описание
ID Уникальный на странице идентификатор таблицы списка.
oSort Экземпляр класса CAdminSorting. Необязательный параметр.
*/
$this->obAdmin = new \CAdminList($this->sTableID, $oSort);
}
/**
* Процедура указания списока полей, которые выводить в фильтре
*
* @param array $arFilter
*/
protected function AddFilter ($arFilter)
{
foreach($arFilter as $key => $arField)
{
$strFilter = str_replace('.', '_', $strFilter);
$arFilter[$key]['FILTER_NAME'] = $strFilter;
}
// опишем элементы фильтра
foreach($arFilter as $arField)
{
$arFilterName[] = $arField['CODE'];
$FilterArr[] = "find_" . strtolower($arField['CODE']);
$this->arFilter[$arField['CODE']] = "find_" . strtolower($arField['CODE']);
}
// инициализируем фильтр
$this->obAdmin->InitFilter($FilterArr);
$this->obFilter = new \CAdminFilter(
$this->sTableID."_filter_id",
$arFilterName
);
}
/**
* Процедура создания массива, который уйдет в getList
*/
protected function SetFilter ()
{
// создаем массив фильтрации для выборки на основе значений фильтра
if($_REQUEST['del_filter'] != 'Y')
{
foreach($this->arSelect as $strField)
{
if(strpos($strField, '.') !== false)
$var = 'find_' . strtolower($this->sTableID . '_' . str_replace('.', '_', $strField));
else
$var = 'find_' . strtolower($strField);
global ${$var};
if(!empty(${$var}))
$this->arSetFilter[strtoupper($strField)] = ${$var};
}
}
}
/**
* Процедура получения объекта CDBResult для вывода в таблицу
*/
public abstract function GetList ();
/**
* Добавляем разбор данных, полученных их серилизованного массива свойства и привязанных к другой сущности.
* return array = ['NAME_PROPERTY' = ORM:ReferenceField.POSITION_ON_ARRAY(KEY|VALUE)]
*/
public abstract function SetVarData ();
/**
* Процедура устанавливает список полей для select GetList'a
*/
public abstract function SetSelect ();
/**
* Процедура генерации объекта таблицы
*/
protected function CreateTable ()
{
$this->SetFilter();
$rsData = $this->GetList();
// преобразуем список в экземпляр класса CAdminResult
$rsData = new \CAdminResult($rsData, $this->sTableID);
// аналогично CDBResult инициализируем постраничную навигацию.
$rsData->NavStart();
// отправим вывод переключателя страниц в основной объект $lAdmin
$this->obAdmin->NavText($rsData->GetNavPrint("Страница"));
if($this->bShowAll == true)
$this->arFields = array();
// добавляем разбор данных, полученных их серилизованного массива свойства и привязанных к другой сущности.
if($arVarData = $this->SetVarData())
{
foreach($arVarData as $strNameProp => $strVar)
{
$arData = explode(':', $strVar);
if(!empty($arData))
{
$arRule = explode('.', $arData[1]);
if(!$arGlobalVar[$strNameProp])
{
$arTmpVar = $arData[0]::getList(array(
'select' => array('ID', 'NAME')
))->fetchAll();
foreach($arTmpVar as $arTmpVarData)
{
$arGlobalVar[$strNameProp][$arTmpVarData[$arRule[0]]] = $arTmpVarData;
$arGlobalVar[$strNameProp]['AR_POSITION'] = $arRule[1];
}
}
}
}
}
while($arRes = $rsData->NavNext(true, "f_"))
{
if($this->bShowAll == true)
{
foreach($arRes as $strKeyCode => $value)
{
$this->arFields[$strKeyCode] = array(
'CODE' => $strKeyCode,
'NAME' => $strKeyCode,
'DEFAULT' => true
);
if(!is_array($value))
$this->arFields[$strKeyCode]["IS_FILTERED"] = true;
}
}
// Создаем строку. результат - экземпляр класса CAdminListRow
$row = $this->obAdmin->AddRow($f_ID, $arRes);
foreach($arRes as $strKeyCode => $value)
{
// Подскавим в ансерилизованные массивы NAME вместо ключа ID
if($arGlobalVar[$strKeyCode])
{
$strField = "";
if($arGlobalVar[$strKeyCode]['AR_POSITION'] == 'KEY')
{
foreach($value as $key => $finalValue)
{
if($arGlobalVar[$strKeyCode][$key])
$strField .= $arGlobalVar[$strKeyCode][$key]['NAME']. ": " . $finalValue . "<br/>";
}
}
elseif($arGlobalVar[$strKeyCode]['AR_POSITION'] == 'VALUE')
{
foreach($value as $key => $finalValue)
{
if($arGlobalVar[$strKeyCode][$key])
$strField .= $arGlobalVar[$strKeyCode][$finalValue]['NAME'] . "<br/>";
}
}
$row->AddViewField($strKeyCode, $strField);
}
}
$can_edit = true;
// сформируем контекстное меню на светлое будущее
$arActions = array();
$arActions[] = array(
"ICON" => "edit",
"TEXT" => GetMessage($can_edit ? "MAIN_ADMIN_MENU_EDIT" : "MAIN_ADMIN_MENU_VIEW"),
"ACTION" => $this->obAdmin->ActionRedirect("ib_list.php?ID=".$f_ID."&lang=".LANGUAGE_ID),
"DEFAULT" => true
);
$arActions[] = array(
"ICON"=>"delete",
"TEXT" => GetMessage("MAIN_ADMIN_MENU_DELETE"),
"ACTION" => "if(confirm('".GetMessageJS('HLBLOCK_ADMIN_DELETE_ROW_CONFIRM')."')) ".
$this->obAdmin->ActionRedirect("ib_list.php?action=delete?ID=".$f_ID."&lang=".LANGUAGE_ID."&".bitrix_sessid_get())
);
// вставим разделитель
$arActions[] = array("SEPARATOR"=>true);
// если последний элемент - разделитель, почистим мусор - выдрал заплатку из ядра
if(is_set($arActions[count($arActions)-1], "SEPARATOR"))
unset($arActions[count($arActions)-1]);
// применим контекстное меню к строке
$row->AddActions($arActions);
}
// Добавим фильтр из полученных полей для отображения над таблицей
if(!empty($this->arFields))
$this->AddFilter($this->arFields);
}
protected function AddFooter ()
{
global $APPLICATION;
/*
Формирование списка колонок таблицы списка. В качестве параметра указывается массив колонок таблицы. Каждая колонка описывается массивом, содержащим следующие ключи:
Ключ Описание
id Идентификатор колонки.
content Заголовок колонки.
sort Значение параметра GET-запроса для сортировки.
default Параметр, показывающий, будет ли колонка по умолчанию отображаться в списке (true|false)
*/
foreach($this->arFields as $arField)
{
$arHeaders[] = array(
'id' => $arField["CODE"],
'content' => $arField["NAME"],
'sort' => $arField["CODE"],
'default' => ($arField["DEFAULT"] == true)?true:false
);
}
$this->obAdmin->AddHeaders($arHeaders);
$this->obAdmin->AddFooter(
array(
array("counter"=>true, "title"=>GetMessage("MAIN_ADMIN_LIST_CHECKED"), "value"=>"0"), // счетчик выбранных элементов на то-же светлое будущее
)
);
// групповые действия (см. пунк о светлом будущем)
$this->obAdmin->AddGroupActionTable(Array(
"delete"=>GetMessage("MAIN_ADMIN_LIST_DELETE"), // удалить выбранные элементы
"activate"=>GetMessage("MAIN_ADMIN_LIST_ACTIVATE"), // активировать выбранные элементы
"deactivate"=>GetMessage("MAIN_ADMIN_LIST_DEACTIVATE"), // деактивировать выбранные элементы
));
/*
Также можно задать административное меню, которое обычно отображается над таблицей со списком (только если у текущего пользователя есть права на редактирование). Административное формируется в виде массива, элементами которого являются ассоциативные массивы с ключами:
Ключ Описание
TEXT Текст пункта меню.
TITLE Текст всплывающей подсказки пункта меню.
LINK Ссылка на кнопке.
LINK_PARAM Дополнительные параметры ссылки (напрямую подставляются в тэг <A>).
ICON CSS-класс иконки действия.
HTML Задание пункта меню напрямую HTML-кодом.
SEPARATOR Разделитель между пунктами меню (true|false).
NEWBAR Новый блок элементов меню (true|false).
MENU Создание выпадающего подменю. Значение задается аналогично контекстному меню строки таблицы.
*/
$this->obAdmin->AddAdminContextMenu(array(array(
"TEXT" => "ADD",
"TITLE" => "ADD",
"LINK" => "ib_list.php?lang=".LANGUAGE_ID,
"ICON" => "btn_new"
)));
// альтернативный вывод
$this->obAdmin->CheckListMode();
}
/**
* Процедура подготовки данных для вывода
*/
public function PrepareData ()
{
$this->CreateTable();
$this->AddFooter();
}
/**
* Отобразим разметку фильтра на странице
*/
public function ShowFilter ()
{
global $APPLICATION;
echo '<fo rm name="find_form" method="GET" action="">';
$this->obFilter->Begin();
foreach($this->arFilter as $strName => $strInput)
{
echo '<tr>';
echo '<td>'.$strName.'</td>';
echo '<td><input type="text" name="'.$strInput.'" size="47" value=""></td>';
echo '</tr>';
}
$this->obFilter->Buttons(array("table_id"=>$this->sTableID, "url"=>$APPLICATION->GetCurPage(), "form"=>"find_form"));
$this->obFilter->End();
echo '</form>';
}
/*
* Выведем таблицу
*/
public function ShowTable ()
{
$this->obAdmin->DisplayList();
}
}
?>
|
Все, основную часть сделали, теперь как выглядит страница в /bitrix/admin/ib_list.php, которая и будет отображать нашу сущность.
<?
define("ADMIN_MODULE_NAME", "ib.main");
require_once($_SERVER["DOCUMENT_ROOT"]."/bitrix/modules/main/include/prolog_admin_before.php");
require_once($_SERVER["DOCUMENT_ROOT"]."/bitrix/modules/main/interface/admin_lib.php");
global $APPLICATION, $USER;
IncludeModuleLangFile(__FILE__);
if (!$USER->IsAdmin())
{
$APPLICATION->AuthForm(GetMessage("ACCESS_DENIED"));
}
if (!CModule::IncludeModule(ADMIN_MODULE_NAME))
{
$APPLICATION->AuthForm(GetMessage("ACCESS_DENIED"));
}
// Отнаследуем абстрактный класс и опишем нужные процедуры
class AdminShowCars extends \IB\Main\General\Admin\ShowAdminObject
{
function GetList ()
{
// выберем список
$obCars = new \IB\Main\Car\ModelTable();
$rsData = $obCars->getList(array(
'filter' => $this->arSetFilter,
'select' => $this->arSelect
));
return $rsData;
}
function SetSelect ()
{
$this->arSelect = array(
'ID',
'TYPE.NAME',
'MODEL',
'YEAR',
'PARTS'
);
}
/**
* Опишем серилизованные свойства. Привязка к другой сущности может быть как в KEY массива, так и в VALUE
*
*return array
*/
function SetVarData ()
{
return $arVarData = array(
'PARTS' => '\IB\Main\Car\PartsTable:ID.KEY',
);
}
}
$entity = \IB\Main\Car\ModelTable::getEntity();
$entity_data_class = $entity->getDataClass();
$sTableID = $entity_data_class::getTableName();
// У меня название таблиц свопадает с namespace! Если у вас отличается - тут нужно указать путь - к примеру ib_main_car_model (взято из \IB\Main\Car\Model)
$obBuildongsTable = new AdminShowCars($sTableID);
$obBuildongsTable->PrepareData();
$APPLICATION->SetTitle('TEST');
// не забудем разделить подготовку данных и вывод
require_once($_SERVER["DOCUMENT_ROOT"]."/bitrix/modules/main/include/prolog_admin_after.php");
$obBuildongsTable->ShowFilter();
$obBuildongsTable->ShowTable();
require_once($_SERVER["DOCUMENT_ROOT"]."/bitrix/modules/main/include/epilog_admin.php");
?>
|
При использовании, обратите внимание, что у меня название таблицы из которой я беру информацию совпадает с путем namespase:
$entity = \IB\Main\Car\ModelTable::getEntity();
$entity_data_class = $entity->getDataClass();
$sTableID = $entity_data_class::getTableName();
// У меня название таблиц свопадает с namespace! Если у вас отличается - тут нужно указать путь - к примеру ib_main_car_model (взято из \IB\Main\Car\Model)
$obBuildongsTable = new AdminShowCars($sTableID);
|
Это нужно для того, чтобы определить имена переменных, принимаемые из фильтра - в классе ShowAdminObject эта строка будет вырезаться из названия полей CDBResult.
Функционал добавления и редактирования пока в планах, ибо основную задачу - вывести объект из связанных сущностей работает.
Если кому мешает функциона над и под таблицей - можете просто закоментировать вызов AddGroupActionTable и AddAdminContextMenu.
P.P.S.От себя добавлю, что было очень не удобно из за отсутствия возможности вытащить из объектов описания полей Entity свойство initialParameters, ибо они protected...
В таком случае, можно было бы передавать кастомные описания поля еще при создании сущности. Публичного метода выковыривания не нашел - буду благодарен, если кто подскажет таковой:
$entity = \IB\Main\Car\ModelTable::getEntity();
\IB\Main\CHandler::pre($entity->getFields());
|