Модель: "model" в терминах MVC. Класс, унаследованный от DataManager или реализующий аналогичный API.
Хэлпер: "view" в терминах MVC. Класс, реализующий отрисовку интерфейса списка или детальной страницы.
Роутер: "controller" в терминах MVC. Файл, принимающий все запросы к админке данного модуля, создающий нужные
хэлперы с нужными настройками. С ним напрямую работать не придётся.
Виджеты: "delegate" в терминах MVC. Классы, отвечающие за отрисовку элементов управления для отдельных полей
сущностей. В списке и на детальной.
Схема работы с модулем следующая:
Реализация класса AdminListHelper - для управления страницей списка элементов
Реализация класса AdminEditHelper - для управления страницей просмотра/редактирования элемента
Создание файла Interface.php с вызовом AdminBaseHelper::setInterfaceSettings(), в которую передается
конфигурация
полей админки и классы, используемые для её построения.
Если не хватает возможностей виджетов, идущих с модулем, можно реализовать свой виджет, унаследованный от любого
другого готового виджета или от абстрактного класса HelperWidget
Рекомендуемая файловая структура для модулей, использующих данный функционал:
Каталог admin. Достаточно поместить в него файл menu.php, отдельные файлы для списка и детальной
создавать не надо благодаря единому роутингу.
Каталог classes (или lib): содержит классы модели, представлений и делегатов.
-- classes/helper: каталог, содержащий классы "view", унаследованные от AdminListHelper и
AdminEditHelper.
-- classes/widget: каталог, содержащий виджеты ("delegate"), если для модуля пришлось создавать
свои.
-- classes/model: каталог с моделями, если пришлось переопределять поведение стандартных функций getList
и т.д.
Использовать данную структуру не обязательно, это лишь рекомендация, основанная на успешном опыте применения модуля
в ряде проектов.
## Разработчики
================================================
FILE: admin/route.php
================================================
buildList(array($by => $order));
}
elseif (is_subclass_of($helper, 'DigitalWand\AdminHelper\Helper\AdminBaseHelper')) {
$adminHelper = new $helper($fields, $tabs);
}
else {
include $_SERVER['DOCUMENT_ROOT'] . BX_ROOT . '/admin/404.php';
exit();
}
if ($isPopup) {
require($_SERVER["DOCUMENT_ROOT"] . "/bitrix/modules/main/include/prolog_popup_admin.php");
}
else {
require($_SERVER["DOCUMENT_ROOT"] . "/bitrix/modules/main/include/prolog_admin_after.php");
}
if ($helperType == 'list') {
$adminHelper->createFilterForm();
}
$adminHelper->show();
if ($isPopup) {
require($_SERVER["DOCUMENT_ROOT"] . "/bitrix/modules/main/include/epilog_popup_admin.php");
}
else {
require($_SERVER["DOCUMENT_ROOT"] . "/bitrix/modules/main/include/epilog_admin.php");
}
================================================
FILE: composer.json
================================================
{
"name": "digitalwand/digitalwand.admin_helper",
"description": "API for custom admin interface in Bitrix by DigitalWand and Notamedia agency",
"type": "bitrix-module",
"license": "MIT",
"keywords": [
"bitrix",
"admin",
"api"
],
"support": {
"issues": "https://github.com/DigitalWand/digitalwand.admin_helper/issues",
"source": "https://github.com/DigitalWand/digitalwand.admin_helper"
},
"authors": [
{
"name": "Alexey Volkov",
"email": "asgalex@gmail.com"
},
{
"name": "Nik Samokhvalov",
"homepage": "http://samokhvalov.info",
"email": "nik@samokhvalov.info"
}
],
"extra": {
"installer-name": "digitalwand.admin_helper"
},
"require": {
"php": ">=5.3.0",
"composer/installers": "~1"
},
"minimum-stability": "dev"
}
================================================
FILE: include.php
================================================
'lib/EventHandlers.php',
'DigitalWand\AdminHelper\Helper\Exception' => 'lib/helper/Exception.php',
'DigitalWand\AdminHelper\Helper\AdminInterface' => 'lib/helper/AdminInterface.php',
'DigitalWand\AdminHelper\Helper\AdminBaseHelper' => 'lib/helper/AdminBaseHelper.php',
'DigitalWand\AdminHelper\Helper\AdminListHelper' => 'lib/helper/AdminListHelper.php',
'DigitalWand\AdminHelper\Helper\AdminSectionListHelper' => 'lib/helper/AdminSectionListHelper.php',
'DigitalWand\AdminHelper\Helper\AdminEditHelper' => 'lib/helper/AdminEditHelper.php',
'DigitalWand\AdminHelper\Helper\AdminSectionEditHelper' => 'lib/helper/AdminSectionEditHelper.php',
'DigitalWand\AdminHelper\EntityManager' => 'lib/EntityManager.php',
'DigitalWand\AdminHelper\Sorting' => 'lib/Sorting.php',
'DigitalWand\AdminHelper\Widget\HelperWidget' => 'lib/widget/HelperWidget.php',
'DigitalWand\AdminHelper\Widget\CheckboxWidget' => 'lib/widget/CheckboxWidget.php',
'DigitalWand\AdminHelper\Widget\ComboBoxWidget' => 'lib/widget/ComboBoxWidget.php',
'DigitalWand\AdminHelper\Widget\StringWidget' => 'lib/widget/StringWidget.php',
'DigitalWand\AdminHelper\Widget\NumberWidget' => 'lib/widget/NumberWidget.php',
'DigitalWand\AdminHelper\Widget\FileWidget' => 'lib/widget/FileWidget.php',
'DigitalWand\AdminHelper\Widget\TextAreaWidget' => 'lib/widget/TextAreaWidget.php',
'DigitalWand\AdminHelper\Widget\HLIBlockFieldWidget' => 'lib/widget/HLIBlockFieldWidget.php',
'DigitalWand\AdminHelper\Widget\DateTimeWidget' => 'lib/widget/DateTimeWidget.php',
'DigitalWand\AdminHelper\Widget\IblockElementWidget' => 'lib/widget/IblockElementWidget.php',
'DigitalWand\AdminHelper\Widget\UrlWidget' => 'lib/widget/UrlWidget.php',
'DigitalWand\AdminHelper\Widget\VisualEditorWidget' => 'lib/widget/VisualEditorWidget.php',
'DigitalWand\AdminHelper\Widget\UserWidget' => 'lib/widget/UserWidget.php',
'DigitalWand\AdminHelper\Widget\OrmElementWidget' => 'lib/widget/OrmElementWidget.php',
)
);
================================================
FILE: install/admin/admin_helper_route.php
================================================
if (!@include_once $_SERVER["DOCUMENT_ROOT"] . "/bitrix/modules/digitalwand.admin_helper/admin/route.php") {
if (!@include_once $_SERVER["DOCUMENT_ROOT"] . "/local/modules/digitalwand.admin_helper/admin/route.php") {
include $_SERVER['DOCUMENT_ROOT'] . '/bitrix/admin/404.php';
}
}
================================================
FILE: install/index.php
================================================
MODULE_VERSION = ADMIN_HELPER_VERSION;
$this->MODULE_VERSION_DATE = ADMIN_HELPER_VERSION_DATE;
$this->MODULE_NAME = Loc::getMessage('ADMIN_HELPER_INSTALL_NAME');
$this->MODULE_DESCRIPTION = Loc::getMessage('ADMIN_HELPER_INSTALL_DESCRIPTION');
}
function DoInstall()
{
$eventManager = \Bitrix\Main\EventManager::getInstance();
RegisterModule($this->MODULE_ID);
$this->InstallFiles();
$eventManager->registerEventHandler(
'main',
'OnPageStart',
$this->MODULE_ID,
'\DigitalWand\AdminHelper\EventHandlers',
'onPageStart'
);
}
function DoUninstall()
{
$eventManager = \Bitrix\Main\EventManager::getInstance();
UnRegisterModule($this->MODULE_ID);
$eventManager->unRegisterEventHandler(
'main',
'OnPageStart',
$this->MODULE_ID,
'\DigitalWand\AdminHelper\EventHandlers',
'onPageStart'
);
}
function InstallFiles()
{
CopyDirFiles(__DIR__ . '/admin', $_SERVER['DOCUMENT_ROOT'] . '/bitrix/admin');
return true;
}
}
================================================
FILE: install/version.php
================================================
define('ADMIN_HELPER_VERSION', '2.0.0');
define('ADMIN_HELPER_VERSION_DATE', '2016-01-22');
?>
================================================
FILE: lang/ru/install/index.php
================================================
'Монстры на каникулах 2',
* 'YEAR' => 2015,
* // У сущности FilmTable есть связь с RelatedLinksTable через поле RELATED_LINKS.
* // Если передать ей данные, то они будут обработаны
* // Представим, что у сущности RelatedLinksTable есть поля ID и VALUE (в этом поле хранится ссылка), FILM_ID
* // В большинстве случаев, данные передаваемые связям генерируются множественными виджетами
* 'RELATED_LINKS' => array(
* // Переданный ниже массив будет обработан аналогично коду RelatedLinksTable::add(array('VALUE' =>
* 'yandex.ru')); array('VALUE' => 'yandex.ru'),
* // Если в массив добавить ID, то запись обновится: RelatedLinksTable::update(3, array('ID' => 3, 'VALUE'
* => 'google.com')); array('ID' => 3, 'VALUE' => 'google.com'),
* // ВНИМАНИЕ: данный класс руководствуется принципом: что передано для связи, то сохранится или обновится,
* что не передано, будет удалено
* // То есть, если в поле связи RELATED_LINKS передать пустой массив, то все значения связи будут удалены
* )
* ));
* $filmManager->save();
* ```
*
* Пример удаления сущности
* ```
* $articleManager = new EntityManager('\Vendor\Module\ArticlesTable', array(), 7, $adminHelper);
* $articleManager->delete();
* ```
*
* Как работает сохранение данных? Дополнительный пример
* Допустим, что есть модели NewsTable (новости) и NewsLinksTable (ссылки на дополнительную информацию о новости)
*
* У модели NewsTable есть связь с моделью NewsLinksTable через поле NEWS_LINKS:
* ```
* DataManager::getMap() {
* ...
* new Entity\ReferenceField(
* 'NEWS_LINKS',
* '\Vendor\Module\NewsLinksTable',
* array('=this.ID' => 'ref.NEWS_ID'),
* 'ref.FIELD' => new DB\SqlExpression('?s', 'NEWS_LINKS'),
* 'ref.ENTITY' => new DB\SqlExpression('?s', 'news'),
* ),
* ...
* }
* ```
*
* Попробуем сохранить
* ```
* $newsManager = new EntityManager(
* '\Vendor\Module\NewsTable',
* array(
* 'TITLE' => 'News title',
* 'CONTENT' => 'News content',
* 'NEWS_LINKS' => array(
* array('LINK' => 'test.ru'),
* array('LINK' => 'test2.ru'),
* array('ID' => 'id ссылки', 'LINK' => 'test3.ru'),
* )
* ),
* null,
* $adminHelper
* );
* $newsManager->save();
* ```
*
* В данном примере передаются данные для новости (заголовок и содержимое) и данные для поля-связи NEWS_LINKS.
*
* Далее EntityManager:
* 1. Вырезает данные, которые предназначены связям
* 2. Подставляет в них данные из основной модели на основе условий связи
* Например для связи с полем NEWS_LINKS подставятся данные:
*
* ```
* NewsLinksTable::ENTITY_ID => NewsTable::ID,
* NewsLinksTable::FIELD => 'NEWS_LINKS',
* NewsLinksTable::ENTITY => 'news'
* ```
*
* 3. После подстановки данных они будут переданы модели связи подобно коду ниже:
*
* ```
* NewsLinksTable::add(array(
* 'ENTITY' => 'news',
* 'FIELD' => 'NEWS_LINKS',
* 'ENTITY_ID' => 'id сущности, например новости',
* 'LINK' => 'test.ru'
* ));
* NewsLinksTable::add(array(
* 'ENTITY' => 'news',
* 'FIELD' => 'NEWS_LINKS',
* 'ENTITY_ID' => 'id сущности',
* 'LINK' => 'test2.ru'
* ));
* NewsLinksTable::update('id ссылки', array(
* 'ENTITY' => 'news',
* 'FIELD' => 'NEWS_LINKS',
* 'ENTITY_ID' => 'id сущности',
* 'LINK' => 'test3.ru'
* ));
* ```
*
* Обратите внимание, что в метод EntityManager::save() были изначально передано только поле LINK, поля ENTITY,
* ENTITY_ID и FIELD были подставлены классом EntityManager автоматически (предыдущий пункт) А так же важно, что для
* третьей ссылки был передан идентификатор, поэтому выполнился NewsLinksTable::update, а не NewsLinksTable::add
*
* 4. Далее `EntityManager` удаляет данные связанной модели `NewsLinksTable`, которые не были добавлены или обновлены.
*
* Как работает удаление?
*
* 1. EntityManager получает из `NewsTable::getMap()` поля-связи
* 2. Получает поля описанные в интерфейсе генератора админки
* 3. Удаляет значения для полей-связей, которые объявлены в интерфейсе
*
* Примечание.
* EntityManager управляет только данными, которые получает при помощи связи стандартными средставами битрикса.
* Например, при удалении NewsTable будут удалены только NewsLinksTable, где:
*
* ```
* NewsLinksTable::ENTITY_ID => NewsTable::ID,
* NewsLinksTable::FIELD => 'NEWS_LINKS',
* NewsLinksTable::ENTITY => 'news'
* ```
*
* @author Nik Samokhvalov
* @author Dmitriy Baibuhtin
*/
class EntityManager
{
/**
* @var string Класс модели.
*/
protected $modelClass;
/**
* @var Entity\Base Сущность модели.
*/
protected $model;
/**
* @var array Данные для обработки.
*/
protected $data;
/**
* @var integer Идентификатор записи.
*/
protected $itemId = null;
/**
* @var string Поле модели, в котором хранится идентификатор записи.
*/
protected $modelPk = null;
/**
* @var array Данные для связей (новые).
*/
protected $referencesData = array();
/**
* Вспомогательный массив метода $this->ReferenceDataSet()
* @var array Данные для связей (то чт уже было в базе).
*/
protected $referenceDataSave = array();
/**
* @var AdminBaseHelper Хелпер.
*/
protected $helper;
/**
* @var array Предупреждения.
*/
protected $notes = array();
/**
* @param string $modelClass Класс основной модели, наследника DataManager.
* @param array $data Массив с сохраняемыми данными.
* @param integer $itemId Идентификатор сохраняемой записи.
* @param AdminBaseHelper $helper Хелпер, инициирующий сохранение записи.
*/
public function __construct($modelClass, array $data = array(), $itemId = null, AdminBaseHelper $helper)
{
Loc::loadMessages(__FILE__);
$this->modelClass = $modelClass;
$this->model = $modelClass::getEntity();
$this->data = $data;
$this->modelPk = $this->model->getPrimary();
$this->helper = $helper;
if (!empty($itemId)) {
$this->setItemId($itemId);
}
}
/**
* Сохранить запись и данные связей.
*
* @return Entity\AddResult|Entity\UpdateResult
*/
public function save()
{
$this->collectReferencesData();
/**
* @var DataManager $modelClass
*/
$modelClass = $this->modelClass;
$db = $this->model->getConnection();
$db->startTransaction(); // начало транзакции
if (empty($this->itemId)) {
$result = $modelClass::add($this->data);
if ($result->isSuccess()) {
$this->setItemId($result->getId());
}
}
else {
$result = $modelClass::update($this->itemId, $this->data);
}
if ($result->isSuccess()) {
$referencesDataResult = $this->processReferencesData();
if($referencesDataResult->isSuccess()){
$db->commitTransaction(); // ошибок нет - применяем изменения
}else{
$result = $referencesDataResult; // возвращаем ReferencesResult что бы вернуть ошибку
$db->rollbackTransaction(); // что-то пошло не так - возвращаем все как было
}
} else {
$db->rollbackTransaction();
}
return $result;
}
/**
* Удаление запись и данные связей.
*
* @return Entity\DeleteResult
*/
public function delete()
{
// Удаление данных зависимостей
$db = $this->model->getConnection();
$db->startTransaction(); // начало транзакции
$result = $this->deleteReferencesData(); // удаляем зависимые сущности
if(!$result->isSuccess()){ // если хотя бы одна из них не удалилась
$db->rollbackTransaction(); // то восстанавливаем все
return $result; // возвращаем ошибку
}
$model = $this->modelClass;
//Если передается массив, то получаем ИД записи
if (!is_null($this->itemId)) {
$result = $model::delete($this->itemId); // удаляем основную сущность
} elseif (!is_array($this->helper->getPk())) {
$result = $model::delete($this->helper->getPk()); // удаляем основную сущность
} else {
$result = new Entity\DeleteResult();
$error = new Entity\EntityError('Can\'t find element\'s ID');
$result->addError($error);
}
if(!$result->isSuccess()){ // если не удалилась
$db->rollbackTransaction(); // то восстанавливаем зависимые сущности
return $result; // возвращаем ошибку
}
$db->commitTransaction(); // все прошло без ошибок применяем изменения
return $result;
}
/**
* Получить список предупреждений
* @return array
*/
public function getNotes()
{
return $this->notes;
}
/**
* Добавить предупреждение
*
* @param $note
* @param string $key Ключ для избежания дублирования сообщений
*
* @return bool
*/
protected function addNote($note, $key = null)
{
if ($key) {
$this->notes[$key] = $note;
}
else {
$this->notes[] = $note;
}
return true;
}
/**
* Установка текущего идентификатора модели.
*
* @param integer $itemId Идентификатор записи.
*/
protected function setItemId($itemId)
{
$this->itemId = $itemId;
$this->data[$this->modelPk] = $this->itemId;
}
/**
* Получение связей
*
* @return array
*/
protected function getReferences()
{
/**
* @var DataManager $modelClass
*/
$modelClass = $this->modelClass;
$entity = $modelClass::getEntity();
$fields = $entity->getFields();
$references = array();
foreach ($fields as $fieldName => $field) {
if ($field instanceof Entity\ReferenceField) {
$references[$fieldName] = $field;
}
}
return $references;
}
/**
* Извлечение данных для связей
*/
protected function collectReferencesData()
{
$result = new Entity\Result();
$references = $this->getReferences();
// Извлечение данных управляемых связей
foreach ($references as $fieldName => $reference) {
if (array_key_exists($fieldName, $this->data)) {
if (!is_array($this->data[$fieldName])) {
$result->addError(new Entity\EntityError(Loc::getMessage('DIGITALWAND_AH_RELATION_SHOULD_BE_MULTIPLE_FIELD')));
return $result;
}
// Извлечение данных для связи
$this->referencesData[$fieldName] = $this->data[$fieldName];
unset($this->data[$fieldName]);
}
}
return $result;
}
/**
* Обработка данных для связей.
*
* @throws ArgumentException
*/
protected function processReferencesData()
{
/**
* @var DataManager $modelClass
*/
$modelClass = $this->modelClass;
$entity = $modelClass::getEntity();
$fields = $entity->getFields();
$result = new Entity\Result(); // пустой Result у которого isSuccess вернет true
foreach ($this->referencesData as $fieldName => $referenceDataSet) {
if (!is_array($referenceDataSet)) {
continue;
}
/**
* @var Entity\ReferenceField $reference
*/
$reference = $fields[$fieldName];
$referenceDataSet = $this->linkDataSet($reference, $referenceDataSet);
$referenceStaleDataSet = $this->getReferenceDataSet($reference);
$fieldWidget = $this->getFieldWidget($fieldName);
$variantsField = $fieldWidget->getSettings('VARIANTS');
// Создание и обновление привязанных данных
$processedDataIds = array();
foreach ($referenceDataSet as $referenceData) {
if (empty($referenceData[$fieldWidget->getMultipleField('ID')])) {
// Создание связанных данных
if (!empty($referenceData[$fieldWidget->getMultipleField('VALUE')])) {
$result = $this->createReferenceData($reference, $referenceData);
if ($result->isSuccess()) {
$processedDataIds[] = $result->getId();
} else {
break; // ошибка, прерываем обработку данных
}
}
} else {
// Обновление связанных данных
$result = $this->updateReferenceData($reference, $referenceData, $referenceStaleDataSet);
if ($result->isSuccess()) {
$processedDataIds[] = $referenceData[$fieldWidget->getMultipleField('ID')];
} else {
break; // ошибка, прерываем обработку данных
}
}
}
if($result->isSuccess()){ // Удаление записей, которые не были созданы или обновлены
foreach ($referenceStaleDataSet as $referenceData) {
if (
!in_array($referenceData[$fieldWidget->getMultipleField('ID')], $processedDataIds)
OR
($variantsField
AND !in_array($referenceData[$fieldWidget->getMultipleField('ID')], $processedDataIds)
AND array_key_exists($referenceData[$fieldWidget->getMultipleField('VALUE')], $variantsField))
) {
if ($fieldWidget->getSettings('DELETE_REFERENCED_DATA')) {
$result = $this->deleteReferenceData($reference,
$referenceData[$fieldWidget->getMultipleField('ID')]);
} else {
$result = $this->deleteReference($reference,
$referenceData[$fieldWidget->getMultipleField('ID')]);
}
if(!$result->isSuccess()) {
break; // ошибка, прерываем удаление данных
}
}
}
}
}
$this->referencesData = array();
return $result;
}
/**
* Удаление данных всех связей, которые указаны в полях интерфейса раздела.
*/
protected function deleteReferencesData()
{
$references = $this->getReferences();
$fields = $this->helper->getFields();
$result = new Entity\Result();
/**
* @var string $fieldName
* @var Entity\ReferenceField $reference
*/
foreach ($references as $fieldName => $reference) {
// Удаляются только данные связей, которые объявлены в интерфейсе
if (!isset($fields[$fieldName])) {
continue;
}
$fieldWidget = $this->getFieldWidget($reference->getName());
$referenceStaleDataSet = $this->getReferenceDataSet($reference);
foreach ($referenceStaleDataSet as $referenceData) {
$result = $this->deleteReferenceData($reference, $referenceData[$fieldWidget->getMultipleField('ID')]);
if(!$result->isSuccess()){
return $result;
}
}
}
return $result;
}
/**
* Создание связанной записи.
*
* @param Entity\ReferenceField $reference
* @param array $referenceData
*
* @return \Bitrix\Main\Entity\AddResult
* @throws ArgumentException
* @throws \Exception
*/
protected function createReferenceData(Entity\ReferenceField $reference, array $referenceData)
{
$referenceName = $reference->getName();
$fieldParams = $this->getFieldParams($referenceName);
$fieldWidget = $this->getFieldWidget($referenceName);
if (!empty($referenceData[$fieldWidget->getMultipleField('ID')])) {
throw new ArgumentException(Loc::getMessage('DIGITALWAND_AH_ARGUMENT_CANT_CONTAIN_ID', array('%A%' => 'referenceData')), 'referenceData');
}
$refClass = $reference->getRefEntity()->getDataClass();
if(isset($referenceData['VALUE']) AND $fieldWidget->getMultipleField('VALUE') != 'VALUE'){
$referenceData[$fieldWidget->getMultipleField('VALUE')] = $referenceData['VALUE'];
unset($referenceData['VALUE']);
}
$createResult = $refClass::add($referenceData);
if (!$createResult->isSuccess()) {
$this->addNote(Loc::getMessage('DIGITALWAND_ADMIN_HELPER_RELATION_SAVE_ERROR',
array('#FIELD#' => $fieldParams['TITLE'])), 'CREATE_' . $referenceName);
}
return $createResult;
}
/**
* Обновление связанной записи
*
* @param Entity\ReferenceField $reference
* @param array $referenceData
* @param array $referenceStaleDataSet
*
* @return Entity\UpdateResult|null
* @throws ArgumentException
* @throws \Exception
*/
protected function updateReferenceData(
Entity\ReferenceField $reference,
array $referenceData,
array $referenceStaleDataSet
)
{
$referenceName = $reference->getName();
$fieldParams = $this->getFieldParams($referenceName);
$fieldWidget = $this->getFieldWidget($referenceName);
if (empty($referenceData[$fieldWidget->getMultipleField('ID')])) {
throw new ArgumentException(Loc::getMessage('DIGITALWAND_AH_ARGUMENT_SHOULD_CONTAIN_ID', array('%A%' => 'referenceData')), 'referenceData');
}
// Сравнение старых данных и новых, обновляется только при различиях
if ($this->isDifferentData($referenceStaleDataSet[$referenceData[$fieldWidget->getMultipleField('ID')]],
$referenceData)
) {
$refClass = $reference->getRefEntity()->getDataClass();
$primary = $referenceData[$fieldWidget->getMultipleField('ID')];
if(isset($referenceData['VALUE']) AND $fieldWidget->getMultipleField('VALUE') != 'VALUE'){
$referenceData[$fieldWidget->getMultipleField('VALUE')] = $referenceData['VALUE'];
unset($referenceData['VALUE']);
}
$updateResult = $refClass::update($primary, $referenceData);
if (!$updateResult->isSuccess()) {
$this->addNote(Loc::getMessage('DIGITALWAND_ADMIN_HELPER_RELATION_SAVE_ERROR',
array('#FIELD#' => $fieldParams['TITLE'])), 'UPDATE_' . $referenceName);
}
return $updateResult;
} else {
return new Entity\Result(); // пустой Result у которого isSuccess() вернет true
}
}
/**
* Удаление данных связи.
*
* @param Entity\ReferenceField $reference
* @param $referenceId
*
* @return \Bitrix\Main\Entity\Result
* @throws \Exception
*/
protected function deleteReferenceData(Entity\ReferenceField $reference, $referenceId)
{
$refClass = $reference->getRefEntity()->getDataClass();
$deleteResult = $refClass::delete($referenceId);
if (!$deleteResult->isSuccess()) {
$fieldParams = $this->getFieldParams($reference->getName());
$this->addNote(Loc::getMessage('DIGITALWAND_ADMIN_HELPER_RELATION_DELETE_ERROR',
array('#FIELD#' => $fieldParams['TITLE'])), 'DELETE_' . $reference->getName());
}
return $deleteResult;
}
/**
* Удаление данных связи.
*
* @param Entity\ReferenceField $reference
* @param $referenceId
*
* @return \Bitrix\Main\Entity\Result
* @throws \Exception
*/
protected function deleteReference(Entity\ReferenceField $reference, $referenceId)
{
$refClass = $reference->getRefEntity()->getDataClass();
$referenceName = $reference->getName();
$fieldWidget = $this->getFieldWidget($referenceName);
$updateResult = $refClass::update($referenceId, [$fieldWidget->getMultipleField('ENTITY_ID') => null]);
if (!$updateResult->isSuccess()) {
$fieldParams = $this->getFieldParams($reference->getName());
$this->addNote(Loc::getMessage('DIGITALWAND_ADMIN_HELPER_RELATION_DELETE_ERROR',
array('#FIELD#' => $fieldParams['TITLE'])), 'DELETE_' . $reference->getName());
}
return $updateResult;
}
/**
* Получение данных связи.
*
* @param Entity\ReferenceField $reference
*
* @return array
* @throws ArgumentException
*/
protected function getReferenceDataSet(Entity\ReferenceField $reference)
{
/**
* @var DataManager $modelClass
*/
$modelClass = $this->modelClass;
$dataSet = array();
$fieldWidget = $this->getFieldWidget($reference->getName());
// Возможно запрос для данного поля уже делалася, получение значения из массива
if ($this->referenceDataSave[ $reference->getName() ])
return $this->referenceDataSave[ $reference->getName() ];
$rsData = $modelClass::getList(array(
'select' => array('REF_' => $reference->getName() . '.*'),
'filter' => array('=' . $this->modelPk => $this->itemId)
));
while ($data = $rsData->fetch()) {
if (empty($data['REF_' . $fieldWidget->getMultipleField('ID')])) {
continue;
}
$row = array();
foreach ($data as $key => $value) {
$row[str_replace('REF_', '', $key)] = $value;
}
if (!isset($row['VALUE'])) {
$row['VALUE'] = $row[$fieldWidget->getMultipleField('VALUE')];
}
$dataSet[$data['REF_' . $fieldWidget->getMultipleField('ID')]] = $row;
}
// Сохранить результат, для избежания повторного запроса к базе
$this->referenceDataSave[ $reference->getName() ] = $dataSet;
// Вернуть результат запроса
return $dataSet;
}
/**
* В данные связи подставляются данные основной модели используя условия связи моделей из getMap().
*
* @param Entity\ReferenceField $reference
* @param array $referenceData Данные привязанной модели
*
* @return array
*/
protected function linkData(Entity\ReferenceField $reference, array $referenceData)
{
// Парсим условия связи двух моделей
$referenceConditions = $this->getReferenceConditions($reference);
// Получение ID связанного элемента
if ($ID = $this->getLinkDataId($referenceData, $this->getReferenceDataSet($reference)))
$referenceData['ID'] = $ID;
foreach ($referenceConditions as $refField => $refValue) {
// Так как в условиях связи между моделями в основном отношения типа this.field => ref.field или
// ref.field => SqlExpression, мы можем использовать это для подстановки данных
// this.field - это поле основной модели
// ref.field - поле модели из связи
// customValue - это строка полученная из new SqlExpression('%s', ...)
if (empty($refValue['thisField'])) {
$referenceData[$refField] = $refValue['customValue'];
}
else {
$referenceData[$refField] = $this->data[$refValue['thisField']];
}
}
return $referenceData;
}
/**
* Связывает набор связанных данных с основной моделю.
*
* @param Entity\ReferenceField $reference
* @param array $referenceDataSet
*
* @return array
*/
protected function linkDataSet(Entity\ReferenceField $reference, array $referenceDataSet)
{
foreach ($referenceDataSet as $key => $referenceData) {
$referenceDataSet[$key] = $this->linkData($reference, $referenceData);
}
return $referenceDataSet;
}
/**
* Возвращает ID связанного элемента. Для поиска используются ранее полученные данные,
* Что позволяет избежать дополнительных запросов для поиска ID элементов.
* В звязанной таблице имя поля идентификатора всегда 'ID', а имя поля значения всегда 'VALUE'
*
* @param array $referenceDataSet устанавливаемое значение
* @param array $referenceStaleDataSet установленные ранее значения
*/
protected function getLinkDataId(array $referenceDataSet, array $referenceStaleDataSet)
{
foreach ($referenceStaleDataSet as $item) {
if ($item['VALUE'] == $referenceDataSet['VALUE']) {
return $item['ID'];
}
}
return null;
}
/**
* Парсинг условий связи между моделями.
*
* Ничего сложного нет, просто определяются соответствия полей основной модели и модели из связи. Например:
*
* `FilmLinksTable::FILM_ID => FilmTable::ID (ref.FILM_ID => this.ID)`
*
* Или, например:
*
* `MediaTable::TYPE => 'FILM' (ref.TYPE => new DB\SqlExpression('?s', 'FILM'))`
*
* @param Entity\ReferenceField $reference Данные поля из getMap().
*
* @return array Условия связи преобразованные в массив вида $conditions[$refField]['thisField' => $thisField,
* 'customValue' => $customValue].
* $customValue - это результат парсинга SqlExpression.
* Если шаблон SqlExpression не равен %s, то условие исключается из результата.
*/
protected function getReferenceConditions(Entity\ReferenceField $reference)
{
$conditionsFields = array();
foreach ($reference->getReference() as $leftCondition => $rightCondition) {
$thisField = null;
$refField = null;
$customValue = null;
// Поиск this.... в левом условии
$thisFieldMatch = array();
$refFieldMatch = array();
if (preg_match('/=this\.([A-z]+)/', $leftCondition, $thisFieldMatch) == 1) {
$thisField = $thisFieldMatch[1];
} // Поиск ref.... в левом условии
else {
if (preg_match('/ref\.([A-z]+)/', $leftCondition, $refFieldMatch) == 1) {
$refField = $refFieldMatch[1];
}
}
// Поиск expression value... в правом условии
$refFieldMatch = array();
if ($rightCondition instanceof \Bitrix\Main\DB\SqlExpression) {
$customValueDirty = $rightCondition->compile();
$customValue = preg_replace('/^([\'"])(.+)\1$/', '$2', $customValueDirty);
if ($customValueDirty == $customValue) {
// Если значение выражения не обрамлено кавычками, значит оно не нужно нам
$customValue = null;
}
} // Поиск ref.... в правом условии
else {
if (preg_match('/ref\.([A-z]+)/', $rightCondition, $refFieldMatch) > 0) {
$refField = $refFieldMatch[1];
}
}
// Если не указано поле, которое нужно заполнить или не найдено содержимое для него, то исключаем условие
if (empty($refField) || (empty($thisField) && empty($customValue))) {
continue;
}
else {
$conditionsFields[$refField] = array(
'thisField' => $thisField,
'customValue' => $customValue,
);
}
}
return $conditionsFields;
}
/**
* Обнаружение отличий массивов
* Метод не сранивает наличие аргументов, сравниваются только значения общих параметров
*
* @param array $data1
* @param array $data2
*
* @return bool
*/
protected function isDifferentData(array $data1 = null, array $data2 = null)
{
if(is_null($data1)) return true;
foreach ($data1 as $key => $value) {
if (isset($data2[$key]) && $data2[$key] != $value) {
return true;
}
}
return false;
}
/**
* @param $fieldName
*
* @return array|bool
*/
protected function getFieldParams($fieldName)
{
$fields = $this->helper->getFields();
if (isset($fields[$fieldName]) && isset($fields[$fieldName]['WIDGET'])) {
return $fields[$fieldName];
}
else {
return false;
}
}
/**
* Получение виджета привязанного к полю.
*
* @param $fieldName
*
* @return HelperWidget|bool
*/
protected function getFieldWidget($fieldName)
{
$field = $this->getFieldParams($fieldName);
return isset($field['WIDGET']) ? $field['WIDGET'] : null;
}
}
================================================
FILE: lib/EventHandlers.php
================================================
*/
class EventHandlers
{
/**
* Автоматическое подключение модуля в админке.
*
* Таки образом, исключаем необходимость прописывать в генераторах админки своих модулей
* подключение «Админ-хелпера».
*
* @throws \Bitrix\Main\LoaderException
*/
public static function onPageStart()
{
if (Context::getCurrent()->getRequest()->isAdminSection())
{
Loader::includeModule('digitalwand.admin_helper');
}
}
}
================================================
FILE: lib/Sorting.php
================================================
by_name = $by_name;
$this->ord_name = $ord_name;
$this->table_id = $table_id;
$this->by_initial = $by_initial;
$this->order_initial = $order_initial;
$uniq = md5(($adminHelper ? get_class($adminHelper) : '') . '_' . $APPLICATION->GetCurPage()); // изменено
$aOptSort = array();
if(isset($GLOBALS[$this->by_name]))
{
$_SESSION["SESS_SORT_BY"][$uniq] = $GLOBALS[$this->by_name];
$_SESSION["SESS_SORT_BY"][$uniq] = $GLOBALS[$this->by_name];
}
elseif(isset($_SESSION["SESS_SORT_BY"][$uniq]))
{
$GLOBALS[$this->by_name] = $_SESSION["SESS_SORT_BY"][$uniq];
}
else
{
$aOptSort = \CUserOptions::GetOption("list", $this->table_id, array("by"=>$by_initial, "order"=>$order_initial));
if(!empty($aOptSort["by"]))
$GLOBALS[$this->by_name] = $aOptSort["by"];
elseif($by_initial !== false)
$GLOBALS[$this->by_name] = $by_initial;
}
if(isset($GLOBALS[$this->ord_name]))
{
$_SESSION["SESS_SORT_ORDER"][$uniq] = $GLOBALS[$this->ord_name];
}
elseif(isset($_SESSION["SESS_SORT_ORDER"][$uniq]))
{
$GLOBALS[$this->ord_name] = $_SESSION["SESS_SORT_ORDER"][$uniq];
}
else
{
if(empty($aOptSort["order"]))
$aOptSort = \CUserOptions::GetOption("list", $this->table_id, array("order"=>$order_initial));
if(!empty($aOptSort["order"]))
$GLOBALS[$this->ord_name] = $aOptSort["order"];
elseif($order_initial !== false)
$GLOBALS[$this->ord_name] = $order_initial;
}
$this->field = $GLOBALS[$this->by_name];
$this->order = $GLOBALS[$this->ord_name];
}
}
================================================
FILE: lib/helper/AdminBaseHelper.php
================================================
*
Мдель: "model" в терминах MVC. Класс, унаследованный от DataManager или реализующий аналогичный API.
*
Хэлпер: "view" в терминах MVC. Класс, реализующий отрисовку интерфейса списка или детальной страницы.
*
Роутер: "controller" в терминах MVC. Файл, принимающий все запросы к админке данного модуля, создающий нужные
* хэлперы с нужными настройками. С ним напрямую работать не придётся.
*
Виджеты: "delegate" в терминах MVC. Классы, отвечающие за отрисовку элементов управления для отдельных полей
* сущностей. В списке и на детальной.
*
*
* Схема работы с модулем следующая:
*
*
Реализация класса AdminListHelper - для управления страницей списка элементов
*
Реализация класса AdminEditHelper - для управления страницей просмотра/редактирования элемента
*
Реализация класса AdminInterface - для описания конфигурации полей админки и классы интерфейсов
*
Реализация класса AdminSectionListHelper - для описания странице списка разделов(если они используются)
*
Реализация класса AdminSectionEditHelper - для управления страницей просмотра/редактирования раздела(если они используются)
*
Если не хватает возможностей виджетов, идущих с модулем, можно реализовать свой виджет, унаследованный от любого
* другого готового виджета или от абстрактного класса HelperWidget
*
*
* Устаревший функционал:
*
*
Файл Interface.php с вызовом AdminBaseHelper::setInterfaceSettings(), в который передается
* конфигурация полей админки и классы.
*
* Рекомендуемая файловая структура для модулей, использующих данный функционал:
*
*
Каталог admin. Достаточно поместить в него файл menu.php, отдельные файлы для списка и детальной
* создавать не надо благодаря единому роутингу.
*
Каталог classes (или lib): содержит классы модли, представлений и делегатов.
*
-- classes/admininterface: каталог, содержащий классы "view", унаследованные от AdminListHelper,
* AdminEditHelper, AdminInterface, AdminSectionListHelper и AdminSectionEditHelper.
*
-- classes/widget: каталог, содержащий виджеты ("delegate"), если для модуля пришлось создавать
* свои.
*
-- classes/model: каталог с моделями, если пришлось переопределять поведение стандартынх функций getList
* и т.д.
*
*
* Использовать данную структуру не обязательно, это лишь рекомендация, основанная на успешном опыте применения модуля
* в ряде проектов.
*
* Единственное обязательное условие - расположение всех реализуемых классов админ хелперов и админ интерфейсов
* в одном неймспейсе
*
* При использовании разделов нужно обязательно прописать в модели элементов привязку к модели разделов, например:
*
* ```php
* [
* 'data_type' => 'Vendor\Module\CategoryTable',
* 'reference' => ['=this.CATEGORY_ID' => 'ref.ID'],
* ]
* ];
* }
* ```
*
* @see AdminInterface::fields()
* @package AdminHelper
*
* @author Nik Samokhvalov
* @author Artem Yarygin
*/
abstract class AdminBaseHelper
{
/**
* @internal
* @var string адрес обработчика запросов к админ. интерфейсу.
*/
static protected $routerUrl = '/bitrix/admin/admin_helper_route.php';
/**
* @var string
* Имя класса используемой модели. Используется для выполнения CRUD-операций.
* При наследовании класса необходимо переопределить эту переменную, указав полное имя класса модели.
*
* @see DataManager
* @api
*/
static protected $model;
/**
* @var string
* Имя класса используемого менеджера сущностей. Используется для выполнения CRUD-операций.
*
* @see DataManager
* @api
*/
static protected $entityManager = '\DigitalWand\AdminHelper\EntityManager';
/**
* @var string
* Назвние модуля данной модели.
* При наследовании класса необходимо указать нзвание модуля, в котором он находится.
* А можно и не указывать, в этому случае он определится автоматически по namespace класса
* Используется для избежания конфликтов между именами представлений.
*
* @api
*/
static public $module = array();
/**
* @var string[]
* Название представления.
* При наследовании класса необходимо указать название представления.
* А можно и не указывать, в этому случае оно определится автоматически по namespace класса.
* Оно будет использовано при построении URL к данному разделу админки.
* Не должно содержать пробелов и других символов, требующих преобразований для
* адресной строки браузера.
*
* @api
*/
static protected $viewName = array();
/**
* @var array
* Настройки интерфейса
* @see AdminBaseHelper::setInterfaceSettings()
* @internal
*/
static protected $interfaceSettings = array();
/**
* @var array
* Привязка класса интерфеса к классу хелпера
*/
static protected $interfaceClass = array();
/**
* @var array
* Хранит список отображаемых полей и настройки их отображения
* @see AdminBaseHelper::setInterfaceSettings()
*/
protected $fields = array();
/**
* @var \CMain
* Замена global $APPLICATION;
*/
protected $app;
protected $validationErrors = array();
/**
* @var string
* Позволяет непосредственно указать адрес страницы списка. Полезно, в случае, если такая станица реализована без
* использования данного модуля. В случае, если поле определено для класса, роутинг не используется.
*
* @see AdminBaseHelper::getListPageUrl
* @api
*/
static protected $listPageUrl;
/**
* @var string
* $viewName представления, отвечающего за страницу списка. Необходимо указывать только для классов, уналедованных
* от AdminEditHelper.
* Необязательное, сгенерируется автоматически если не определено
*
* @see AdminBaseHelper::getViewName()
* @see AdminBaseHelper::getListPageUrl
* @see AdminEditHelper
* @api
*/
static protected $listViewName;
/**
* @var string
* Позволяет непосредственно указать адрес страницы просмотра/редактирования элемента. Полезно, в случае, если
* такая станица реализована без использования данного модуля. В случае, если поле определено для класса,
* роутинг не используется.
*
* @see AdminBaseHelper::getEditPageUrl
* @api
*/
static protected $editPageUrl;
/**
* @var string
* $viewName представления, отвечающего за страницу редактирования/просмотра элемента. Необходимо указывать только
* для классов, уналедованных от AdminListHelper.
*
* @see AdminBaseHelper::getViewName()
* @see AdminBaseHelper::getEditPageUrl
* @see AdminListHelper
* @api
*/
static protected $editViewName;
/**
* @var string
* Позволяет непосредственно указать адрес страницы просмотра/редактирования раздела. Полезно, в случае, если
* такая станица реализована без использования данного модуля. В случае, если поле определено для класса,
* роутинг не используется.
*
* @see AdminBaseHelper::getEditPageUrl
* @api
*/
static protected $sectionsEditPageUrl;
/**
* @var string
* $viewName представления, отвечающего за страницу редактирования/просмотра раздела. Необходимо указывать только
* для классов, уналедованных от AdminListHelper.
* Необязательное, сгенерируется автоматически если не определено
*
* @see AdminBaseHelper::getViewName()
* @see AdminBaseHelper::getEditPageUrl
* @see AdminListHelper
* @api
*/
static protected $sectionsEditViewName;
/**
* @var array
* Дополнительные параметры URL, которые будут добавлены к параметрам по-умолчанию, генерируемым автоматически
* @api
*/
protected $additionalUrlParams = array();
/**
* @var string контекст выполнения. Полезен для информирования виджетов о том, какая операция в настоящий момент
* производится.
*/
protected $context = '';
/**
* Флаг использования разделов, необходимо переопределять в дочернем классе
* @var bool
*/
static protected $useSections = false;
/**
* Правило именования хелперов для разделов по умолчанию
* @var string
*/
static protected $sectionSuffix = 'Sections';
/**
* @param array $fields список используемых полей и виджетов для них
* @param array $tabs список вкладок для детальной страницы
* @param string $module название модуля
*/
public function __construct(array $fields, array $tabs = array(), $module = "")
{
global $APPLICATION;
$this->app = $APPLICATION;
$settings = array(
'FIELDS' => $fields,
'TABS' => $tabs
);
if (static::setInterfaceSettings($settings)) {
$this->fields = $fields;
}
else {
$settings = static::getInterfaceSettings();
$this->fields = $settings['FIELDS'];
}
}
/**
* @param string $viewName Имя вьюхи, для которой мы хотим получить натсройки
*
* @return array Возвращает настройки интерфейса для данного класса.
*
* @see AdminBaseHelper::setInterfaceSettings()
* @api
*/
public static function getInterfaceSettings($viewName = '')
{
if (empty($viewName)) {
$viewName = static::getViewName();
}
return self::$interfaceSettings[static::getModule()][$viewName]['interface'];
}
/**
* Основная функция для конфигурации всего административного интерфейса.
*
* @param array $settings настройки полей и вкладок
* @param array $helpers список классов-хэлперов, используемых для отрисовки админки
* @param string $module название модуля
*
* @return bool false, если для данного класса уже были утановлены настройки
*
* @api
*/
public static function setInterfaceSettings(array $settings, array $helpers = array(), $module = '')
{
foreach ($helpers as $helperClass => $helperSettings) {
if (!is_array($helperSettings)) { // поддержка старого формата описания хелперов
$helperClass = $helperSettings; // в значении передается класс хелпера а не настройки
$helperSettings = array(); // настроек в старом формате нет
}
$success = $helperClass::registerInterfaceSettings($module, array_merge($settings, $helperSettings));
if (!$success) return false;
}
return true;
}
/**
* Привязывает класса хелпера из которого вызывается к интерфесу, используется при получении
* данных об элементах управления из интерфейса.
*
* @param $class
*/
public static function setInterfaceClass($class)
{
static::$interfaceClass[get_called_class()] = $class;
}
/**
* Возвращает класс интерфейса к которому привязан хелпер из которого вызван метод.
*
* @return array
*/
public static function getInterfaceClass()
{
return isset(static::$interfaceClass[get_called_class()]) ? static::$interfaceClass[get_called_class()] : false;
}
/**
* Регистрирует настройки интерфейса для текущего хелпера
*
* @param string $module имя текущего модуля
* @param $interfaceSettings
*
* @return bool
* @internal
*/
public static function registerInterfaceSettings($module, $interfaceSettings)
{
if (isset(self::$interfaceSettings[$module][static::getViewName()]) || empty($module)
|| empty($interfaceSettings)
) {
return false;
}
self::$interfaceSettings[$module][static::getViewName()] = array(
'helper' => get_called_class(),
'interface' => $interfaceSettings
);
return true;
}
/**
* Получает настройки интерфейса для данного модуля и представления. Используется при роутинге.
* Возвращается массив со следующими ключами:
*
*
*
helper - название класса-хэлпера, который будет рисовать страницу
*
interface - настройки интерфейса для хелпера
*
*
* @param string $module Модуль, для которого нужно получить настройки.
* @param string $view Название представления.
*
* @return array
* @internal
*/
public static function getGlobalInterfaceSettings($module, $view)
{
if (!isset(self::$interfaceSettings[$module][$view])) {
return false;
}
return array(
self::$interfaceSettings[$module][$view]['helper'],
self::$interfaceSettings[$module][$view]['interface'],
);
}
/**
* Возвращает имя текущего представления.
*
* @return string
* @api
*/
public static function getViewName()
{
if (!is_array(static::$viewName)) {
return static::$viewName;
}
$className = get_called_class();
if (!isset(static::$viewName[$className])) {
$classNameParts = explode('\\', trim($className, '\\'));
if (count($classNameParts) > 2) {
$classCaption = array_pop($classNameParts); // название класса без namespace
preg_match_all('/((?:^|[A-Z])[a-z]+)/', $classCaption, $matches);
$classCaptionParts = $matches[0];
if (end($classCaptionParts) == 'Helper') {
array_pop($classCaptionParts);
}
static::$viewName[$className] = strtolower(implode('_', $classCaptionParts));
}
}
return static::$viewName[$className];
}
/**
* Возвращает поле модели которое используется для привязки к разделу из поля с типом совпадающим с классом модели
* раздела.
* @return string
* @throws Exception
*/
public static function getSectionField()
{
$sectionListHelper = static::getHelperClass(AdminSectionListHelper::className());
if (empty($sectionListHelper))
{
return null;
}
$sectionModelClass = $sectionListHelper::getModel();
$modelClass = static::getModel();
foreach ($modelClass::getMap() as $field => $data) {
if ($data instanceof ReferenceField && $data->getDataType() . 'Table' === $sectionModelClass) {
return str_replace('=this.', '', reset($data->getReference()));
}
if (is_array($data) && $data['data_type'] === $sectionModelClass) {
return str_replace('=this.', '', key($data['reference']));
}
}
throw new Exception('References to section model not found');
}
/**
* Возвращает имя класса используемой модели.
*
* @return \Bitrix\Main\Entity\DataManager|string
*
* @throws \Bitrix\Main\ArgumentException
* @throws \Bitrix\Main\SystemException
* @throws \Exception
* @api
*/
public static function getModel()
{
if (static::$model) {
return static::getHLEntity(static::$model);
}
return null;
}
/**
* Возвращает имя модуля. Если оно не задано, то определяет автоматически из namespace класса.
*
* @return string
*
* @throws LoaderException
* @api
*/
public static function getModule()
{
if (!is_array(static::$module)) {
return static::$module;
}
$className = get_called_class();
if (!isset(static::$module[$className])) {
$classNameParts = explode('\\', trim($className, '\\'));
$moduleNameParts = array();
$moduleName = false;
while (count($classNameParts)) {
$moduleNameParts[] = strtolower(array_shift($classNameParts));
$moduleName = implode('.', $moduleNameParts);
if (ModuleManager::isModuleInstalled($moduleName)) {
static::$module[$className] = $moduleName;
break;
}
}
if (empty($moduleName)) {
throw new LoaderException('Module name not found');
}
}
return static::$module[$className];
}
/**
* Возвращает модифцированный массив с описанием элемента управления по его коду. Берет название и настройки
* из админ-интерфейса, если они не заданы — используются значения по умолчанию.
*
* Если элемент управления описан в админ-интерфейсе, то дефолтные настройки и описанные в классе интерфейса
* будут совмещены (смержены).
*
* @param $code
* @param $params
* @param array $keys
*
* @return array|bool
*/
protected function getButton($code, $params, $keys = array('name', 'TEXT'))
{
$interfaceClass = static::getInterfaceClass();
$interfaceSettings = static::getInterfaceSettings();
if ($interfaceClass && !empty($interfaceSettings['BUTTONS'])) {
$buttons = $interfaceSettings['BUTTONS'];
if (is_array($buttons) && isset($buttons[$code])) {
if ($buttons[$code]['VISIBLE'] == 'N') {
return false;
}
$params = array_merge($params, $buttons[$code]);
return $params;
}
}
$text = Loc::getMessage('DIGITALWAND_ADMIN_HELPER_' . $code);
foreach ($keys as $key) {
$params[$key] = $text;
}
return $params;
}
/**
* Возвращает список полей интерфейса.
*
* @see AdminBaseHelper::setInterfaceSettings()
*
* @return array
*
* @api
*/
public function getFields()
{
return $this->fields;
}
/**
* Окончательно выводит административную страницу.
*/
abstract public function show();
/**
* Получает название таблицы используемой модели.
*
* @return mixed
*/
public function table()
{
/**
* @var DataManager $className
*/
$className = static::getModel();
return $className::getTableName();
}
/**
* Возвращает первичный ключ таблицы используемой модели
* Для HL-инфоблоков битрикс - всегда ID. Но может поменяться для какой-либо другой сущности.
* @return string
* @api
*/
public static function pk()
{
return 'ID';
}
/**
* Возвращает значение первичного ключа таблицы используемой модели
* @return array|int|null
*
* @api
*/
public function getPk()
{
return isset($_REQUEST['FIELDS'][$this->pk()]) ? $_REQUEST['FIELDS'][$this->pk()] : $_REQUEST[$this->pk()];
}
/**
* Возвращает первичный ключ таблицы используемой модели разделов. Для HL-инфоблоков битрикс - всегда ID.
* Но может поменяться для какой-либо другой сущности.
*
* @return string
*
* @api
*/
public function sectionPk()
{
return 'ID';
}
/**
* Устанавливает заголовок раздела в админке.
*
* @param string $title
*
* @api
*/
public function setTitle($title)
{
$this->app->SetTitle($title);
}
/**
* Функция для обработки дополнительных операций над элементами в админке. Как правило, должно оканчиваться
* LocalRedirect после внесения изменений.
*
* @param string $action Название действия.
* @param null|int $id ID элемента.
*
* @api
*/
protected function customActions($action, $id = null)
{
return;
}
/**
* Выполняется проверка прав на доступ к сущности.
*
* @return bool
*
* @api
*/
protected function hasRights()
{
return true;
}
/**
* Выполняется проверка прав на выполнение операций чтения элементов.
*
* @return bool
*
* @api
*/
protected function hasReadRights()
{
return true;
}
/**
* Выполняется проверка прав на выполнение операций редактирования элементов.
*
* @return bool
*
* @api
*/
protected function hasWriteRights()
{
return true;
}
/**
* Проверка прав на изменение определенного элемента.
*
* @param array $element Массив данных элемента.
*
* @return bool
*
* @api
*/
protected function hasWriteRightsElement($element = array())
{
if (!$this->hasWriteRights()) {
return false;
}
return true;
}
/**
* Выполняется проверка прав на выполнение опреаций удаления элементов.
*
* @return bool
*
* @api
*/
protected function hasDeleteRights()
{
return true;
}
/**
* Выводит сообщения об ошибках.
*
* @internal
*/
protected function showMessages()
{
$allErrors = $this->getErrors();
$notes = $this->getNotes();
if (!empty($allErrors)) {
$errorList[] = implode("\n", $allErrors);
}
if ($e = $this->getLastException()) {
$errorList[] = trim($e->GetString());
}
if (!empty($errorList)) {
$errorText = implode("\n\n", $errorList);
\CAdminMessage::ShowOldStyleError($errorText);
}
else {
if (!empty($notes)) {
$noteText = implode("\n\n", $notes);
\CAdminMessage::ShowNote($noteText);
}
}
}
/**
* @return bool|\CApplicationException
*
* @internal
*/
protected function getLastException()
{
if (isset($_SESSION['APPLICATION_EXCEPTION']) AND !empty($_SESSION['APPLICATION_EXCEPTION'])) {
/** @var CApplicationException $e */
$e = $_SESSION['APPLICATION_EXCEPTION'];
unset($_SESSION['APPLICATION_EXCEPTION']);
return $e;
}
else {
return false;
}
}
/**
* @param $e
*/
protected function setAppException($e)
{
$_SESSION['APPLICATION_EXCEPTION'] = $e;
}
/**
* Добавляет ошибку или массив ошибок для показа пользователю.
*
* @param array|string $errors
*
* @api
*/
public function addErrors($errors)
{
if (!is_array($errors)) {
$errors = array($errors);
}
if (isset($_SESSION['ELEMENT_SAVE_ERRORS']) AND !empty($_SESSION['ELEMENT_SAVE_ERRORS'])) {
$_SESSION['ELEMENT_SAVE_ERRORS'] = array_merge($_SESSION['ELEMENT_SAVE_ERRORS'], $errors);
}
else {
$_SESSION['ELEMENT_SAVE_ERRORS'] = $errors;
}
}
/**
* Добавляет уведомление или список уведомлений для показа пользователю.
*
* @param array|string $notes
*
* @api
*/
public function addNotes($notes)
{
if (!is_array($notes)) {
$notes = array($notes);
}
if (isset($_SESSION['ELEMENT_SAVE_NOTES']) AND !empty($_SESSION['ELEMENT_SAVE_NOTES'])) {
$_SESSION['ELEMENT_SAVE_NOTES'] = array_merge($_SESSION['ELEMENT_SAVE_NOTES'],
$notes);
}
else {
$_SESSION['ELEMENT_SAVE_NOTES'] = $notes;
}
}
/**
* @return bool|array
*
* @api
*/
protected function getErrors()
{
if (isset($_SESSION['ELEMENT_SAVE_ERRORS']) AND !empty($_SESSION['ELEMENT_SAVE_ERRORS'])) {
$errors = $_SESSION['ELEMENT_SAVE_ERRORS'];
unset($_SESSION['ELEMENT_SAVE_ERRORS']);
return $errors;
}
else {
return false;
}
}
/**
* @return bool
*
* @api
*/
protected function getNotes()
{
if (isset($_SESSION['ELEMENT_SAVE_NOTES']) AND !empty($_SESSION['ELEMENT_SAVE_NOTES'])) {
$notes = $_SESSION['ELEMENT_SAVE_NOTES'];
unset($_SESSION['ELEMENT_SAVE_NOTES']);
return $notes;
}
else {
return false;
}
}
/**
* Возвращает класс хелпера нужного типа из всех зарегистрированных хелперов в модуле и находящихся
* в том же неймспейсе что класс хелпера из которого вызван этот метод
*
* Под типом понимается ближайший родитель из модуля AdminHelper.
*
* Например если нам нужно получить ListHelper для формирования ссылки на список из EditHelper,
* то это будет вглядеть так $listHelperClass = static::getHelperClass(AdminListHelper::getClass())
*
* @param $class
*
* @return string|bool
*/
public static function getHelperClass($class)
{
$interfaceSettings = self::$interfaceSettings[static::getModule()];
foreach ($interfaceSettings as $viewName => $settings) {
$parentClasses = class_parents($settings['helper']);
array_pop($parentClasses); // AdminBaseHelper
$parentClass = array_pop($parentClasses);
$thirdClass = array_pop($parentClasses);
if (in_array($thirdClass, array(AdminSectionListHelper::className(), AdminSectionEditHelper::className()))) {
$parentClass = $thirdClass;
}
if ($parentClass == $class && class_exists($settings['helper'])) {
$helperClassParts = explode('\\', $settings['helper']);
array_pop($helperClassParts);
$helperNamespace = implode('\\', $helperClassParts);
$сlassParts = explode('\\', get_called_class());
array_pop($сlassParts);
$classNamespace = implode('\\', $сlassParts);
if ($helperNamespace == $classNamespace) {
return $settings['helper'];
}
}
}
return false;
}
/**
* Возвращает относительный namespace до хелперов в виде URL параметра.
*
* @return string
*/
public static function getEntityCode()
{
$namespaceParts = explode('\\', get_called_class());
array_pop($namespaceParts);
array_shift($namespaceParts);
array_shift($namespaceParts);
if (end($namespaceParts) == 'AdminInterface') {
array_pop($namespaceParts);
}
return str_replace(
'\\',
'_',
implode(
'\\',
array_map('lcfirst', $namespaceParts)
)
);
}
/**
* Возвращает URL страницы редактирования класса данного представления.
*
* @param array $params
*
* @return string
*
* @api
*/
public static function getEditPageURL($params = array())
{
$editHelperClass = str_replace('List', 'Edit', get_called_class());
if (empty(static::$editViewName) && class_exists($editHelperClass)) {
return $editHelperClass::getViewURL($editHelperClass::getViewName(), static::$editPageUrl, $params);
}
else {
return static::getViewURL(static::$editViewName, static::$editPageUrl, $params);
}
}
/**
* Возвращает URL страницы редактирования класса данного представления.
*
* @param array $params
*
* @return string
*
* @api
*/
public static function getSectionsEditPageURL($params = array())
{
$sectionEditHelperClass = str_replace('List', 'SectionsEdit', get_called_class());
if (empty(static::$sectionsEditViewName) && class_exists($sectionEditHelperClass)) {
return $sectionEditHelperClass::getViewURL($sectionEditHelperClass::getViewName(), static::$sectionsEditPageUrl, $params);
}
else {
return static::getViewURL(static::$sectionsEditViewName, static::$sectionsEditPageUrl, $params);
}
}
/**
* Возвращает URL страницы списка класса данного представления.
*
* @param array $params
*
* @return string
*
* @api
*/
public static function getListPageURL($params = array())
{
$listHelperClass = str_replace('Edit', 'List', get_called_class());
if (empty(static::$listViewName) && class_exists($listHelperClass)) {
return $listHelperClass::getViewURL($listHelperClass::getViewName(), static::$listPageUrl, $params);
}
else {
return static::getViewURL(static::$listViewName, static::$listPageUrl, $params);
}
}
/**
* Получает URL для указанного представления
*
* @param string $viewName Название представления.
* @param string $defaultURL Позволяет указать URL напрямую. Если указано, то будет использовано это значение.
* @param array $params Дополнительные query-параметры в URL.
*
* @return string
*
* @internal
*/
public static function getViewURL($viewName, $defaultURL, $params = array())
{
$params['entity'] = static::getEntityCode();
if (isset($defaultURL)) {
$url = $defaultURL . "?lang=" . LANGUAGE_ID;
}
else {
$url = static::getRouterURL() . '?lang=' . LANGUAGE_ID . '&module=' . static::getModule() . '&view=' . $viewName;
}
if (!empty($params)) {
unset($params['lang']);
unset($params['module']);
unset($params['view']);
$query = http_build_query($params);
$url .= '&' . $query;
}
return $url;
}
/**
* Возвращает адрес обработчика запросов к админ. интерфейсу.
*
* @return string
*
* @api
*/
public static function getRouterURL()
{
return static::$routerUrl;
}
/**
* Возвращает URL страницы с хелпером. Как правило, метод вызывается при генерации административного
* меню (`menu.php`).
*
* @param array $params Дополнительные GET-параметры для подстановки в URL.
*
* @return string
*/
public static function getUrl(array $params = array())
{
return static::getViewURL(static::getViewName(), null, $params);
}
/**
* Получает виджет для текущего поля, выполняет базовую инициализацию.
*
* @param string $code Ключ поля для данного виджета (должен быть в массиве $data).
* @param array $data Данные объекта в виде массива.
*
* @return bool|\DigitalWand\AdminHelper\Widget\HelperWidget
*
* @throws \DigitalWand\AdminHelper\Helper\Exception
*
* @internal
*/
public function createWidgetForField($code, &$data = array())
{
if (!isset($this->fields[$code]['WIDGET'])) {
$error = str_replace('#CODE#', $code, 'Can\'t create widget for the code "#CODE#"');
throw new Exception($error, Exception::CODE_NO_WIDGET);
}
/** @var HelperWidget $widget */
$widget = $this->fields[$code]['WIDGET'];
$widget->setHelper($this);
$widget->setCode($code);
$widget->setData($data);
$widget->setEntityName($this->getModel());
$this->onCreateWidgetForField($widget, $data);
if (!$this->hasWriteRightsElement($data)) {
$widget->setSetting('READONLY', true);
}
return $widget;
}
/**
* Метод вызывается при создании виджета для текущего поля. Может быть использован для изменения настроек виджета
* на основе передаваемых данных.
*
* @param \DigitalWand\AdminHelper\Widget\HelperWidget $widget
* @param array $data
*/
protected function onCreateWidgetForField(&$widget, $data = array())
{
}
/**
* Если класс не объявлен, то битрикс генерирует новый класс в рантайме. Если класс уже есть, то возвращаем имя
* как есть.
*
* @param $className
* @return \Bitrix\Highloadblock\DataManager
*
* @throws \Bitrix\Main\ArgumentException
* @throws \Bitrix\Main\SystemException
* @throws Exception
*/
public static function getHLEntity($className)
{
if (!class_exists($className)) {
$info = static::getHLEntityInfo($className);
if ($info) {
$entity = HL\HighloadBlockTable::compileEntity($info);
return $entity->getDataClass();
}
else {
$error = Loc::getMessage('DIGITALWAND_ADMIN_HELPER_GETMODEL_EXCEPTION', array('#CLASS#' => $className));
$exception = new Exception($error, Exception::CODE_NO_HL_ENTITY_INFORMATION);
throw $exception;
}
}
return $className;
}
/**
* Получает запись из БД с информацией об HL.
*
* @param string $className Название класса, обязательно без Table в конце и без указания неймспейса.
*
* @return array|false
*
* @throws \Bitrix\Main\ArgumentException
*/
public static function getHLEntityInfo($className)
{
$className = str_replace('\\', '', $className);
$pos = strripos($className, 'Table', -5);
if ($pos !== false) {
$className = substr($className, 0, $pos);
}
$parameters = array(
'filter' => array(
'NAME' => $className,
),
'limit' => 1
);
return HL\HighloadBlockTable::getList($parameters)->fetch();
}
/**
* Отобразить страницу 404 ошибка
*/
protected function show404()
{
// инициализация глобальных переменных, необходимых для вывода страницы административного раздела в
// текущей области видимости
global $APPLICATION, $adminPage, $adminMenu, $USER;
\CHTTP::SetStatus(404);
include $_SERVER['DOCUMENT_ROOT'] . BX_ROOT . '/admin/404.php';
die();
}
/**
* Выставляет текущий контекст исполнения.
*
* @param $context
*
* @see $context
*/
protected function setContext($context)
{
$this->context = $context;
}
public function getContext()
{
return $this->context;
}
public static function className()
{
return get_called_class();
}
}
================================================
FILE: lib/helper/AdminEditHelper.php
================================================
*
static protected $model
*
*
* Этого будет дастаточно для получения минимальной функциональности.
*
* @package AdminHelper
*
* @see AdminBaseHelper::$model
* @see AdminBaseHelper::$module
* @see AdminBaseHelper::$listViewName
* @see AdminBaseHelper::$viewName
*
* @author Nik Samokhvalov
* @author Artem Yarygin
*/
abstract class AdminEditHelper extends AdminBaseHelper
{
const OP_SHOW_TAB_ELEMENTS = 'AdminEditHelper::showTabElements';
const OP_EDIT_ACTION_BEFORE = 'AdminEditHelper::editAction_before';
const OP_EDIT_ACTION_AFTER = 'AdminEditHelper::editAction_after';
/**
* @var array Данные сущности, редактируемой в данный момент. Ключи ассива — названия полей в БД.
* @api
*/
protected $data;
/**
* @var array Вкладки страницы редактирования.
*/
protected $tabs = array();
/**
* @var array Элементы верхнего меню страницы.
* @see AdminEditHelper::fillMenu()
*/
protected $menu = array();
/**
* @var \CAdminForm
*/
protected $tabControl;
/**
* Производится инициализация переменных, обработка запросов на редактирование
*
* @param array $fields
* @param array $tabs
*/
public function __construct(array $fields, array $tabs = array())
{
$this->tabs = $tabs;
if (empty($this->tabs)) {
$this->tabs = array(
array(
'DIV' => 'DEFAULT_TAB',
'TAB' => Loc::getMessage('DEFAULT_TAB'),
'ICON' => 'main_user_edit',
'TITLE' => Loc::getMessage('DEFAULT_TAB'),
'VISIBLE' => true,
)
);
}
else {
if (!is_array(reset($this->tabs))) {
$converted = array();
foreach ($this->tabs as $tabCode => $tabName) {
$tabVisible = true;
if (is_array($tabName)) {
$tabVisible = isset($tabName['VISIBLE']) ? $tabName['VISIBLE'] : $tabVisible;
$tabName = $tabName['TITLE'];
}
$converted[] = array(
'DIV' => $tabCode,
'TAB' => $tabName,
'ICON' => '',
'TITLE' => $tabName,
'VISIBLE' => $tabVisible,
);
}
$this->tabs = $converted;
}
}
parent::__construct($fields, $tabs);
$this->tabControl = new \CAdminForm(str_replace("\\", "", get_called_class()), $this->tabs);
if (isset($_REQUEST['apply']) OR isset($_REQUEST['save'])) {
if (
isset($_SERVER["HTTP_BX_AJAX"])
||
isset($_SERVER["HTTP_X_REQUESTED_WITH"]) && $_SERVER["HTTP_X_REQUESTED_WITH"] === "XMLHttpRequest"
) {
\CUtil::JSPostUnescape();
}
$this->data = $_REQUEST['FIELDS'];
if (isset($_REQUEST[$this->pk()])) {
//Первичный ключ проставляем отдельно, чтобы не вынуждать всегда указывать его в настройках интерфейса.
$this->data[$this->pk()] = $_REQUEST[$this->pk()];
}
foreach ($fields as $code => $settings) {
if (isset($_REQUEST[$code])) {
$this->data[$code] = $_REQUEST[$code];
}
}
if ($this->editAction()) {
if (isset($_REQUEST['apply'])) {
$id = $this->data[$this->pk()];
$url = $this->app->GetCurPageParam($this->pk() . '=' . (is_array($id) ? $id[$this->pk()] : $id), array('ID'));
}
else {
if (isset($_REQUEST['save'])) {
$listHelperClass = static::getHelperClass(AdminListHelper::className());
$url = $listHelperClass::getUrl(array_merge($this->additionalUrlParams,
array(
'restore_query' => 'Y'
)));
}
}
}
else {
if (isset($this->data[$this->pk()])) {
$id = $this->data[$this->pk()];
$url = $this->app->GetCurPageParam($this->pk() . '=' . $id);
}
else {
unset($this->data);
$this->data = $_REQUEST['FIELDS']; //Заполняем, чтобы в случае ошибки сохранения поля не были пустыми
}
}
if (isset($url)) {
if (defined('BX_PUBLIC_MODE') && BX_PUBLIC_MODE === 1 && ($errors = $this->getErrors())) {
ob_end_clean();
$jsMessage = \CUtil::JSEscape(implode("\n", $errors));
echo '';
die();
}
$this->setAppException($this->app->GetException());
LocalRedirect($url);
}
}
else {
$helperFields = $this->getFields();
$select = array_keys($helperFields);
foreach ($select as $key => $field) {
if (isset($helperFields[$field]['VIRTUAL'])
AND $helperFields[$field]['VIRTUAL'] == true
AND (!isset($helperFields[$field]['FORCE_SELECT']) OR $helperFields[$field]['FORCE_SELECT'] = false)
) {
unset($select[$key]);
}
}
$this->data = $this->loadElement($select);
$id = isset($_REQUEST[$this->pk()]) ? $_REQUEST[$this->pk()] : null;
if ($this->data === false && !is_null($id)) {
$this->show404();
}
if (isset($_REQUEST['action']) || isset($_REQUEST['action_button'])) {
$action = isset($_REQUEST['action']) ? $_REQUEST['action'] : $_REQUEST['action_button'];
$this->customActions($action, $this->getPk());
}
}
$this->setElementTitle();
}
/**
* Возвращает верхнее меню страницы.
* По-умолчанию две кнопки:
*
*
Возврат в список
*
Удаление элемента
*
*
* Добавляя новые кнопки, нужно указывать параметр URl "action", который будет обрабатываться в
* AdminEditHelper::customActions()
*
* @param bool $showDeleteButton Управляет видимостью кнопки удаления элемента.
*
* @return array
*
* @see AdminEditHelper::$menu
* @see AdminEditHelper::customActions()
*
* @api
*/
protected function getMenu($showDeleteButton = true)
{
$listHelper = static::getHelperClass(AdminListHelper::className());
$menu = array(
$this->getButton('RETURN_TO_LIST', array(
'LINK' => $listHelper::getUrl(array_merge($this->additionalUrlParams,
array('restore_query' => 'Y')
)),
'ICON' => 'btn_list',
))
);
$arSubMenu = array();
if (isset($this->data[$this->pk()]) && $this->hasWriteRights()) {
$arSubMenu[] = $this->getButton('ADD_ELEMENT', array(
'LINK' => static::getUrl(array_merge($this->additionalUrlParams,
array(
'action' => 'add',
'lang' => LANGUAGE_ID,
'restore_query' => 'Y',
))),
'ICON' => 'edit'
));
}
if ($showDeleteButton && isset($this->data[$this->pk()]) && $this->hasDeleteRights()) {
$arSubMenu[] = $this->getButton('DELETE_ELEMENT', array(
'ONCLICK' => "if(confirm('" . Loc::getMessage('DIGITALWAND_ADMIN_HELPER_EDIT_DELETE_CONFIRM') . "')) location.href='" .
static::getUrl(array_merge($this->additionalUrlParams,
array(
'ID' => $this->data[$this->pk()],
'action' => 'delete',
'lang' => LANGUAGE_ID,
'restore_query' => 'Y',
))) . "'",
'ICON' => 'delete'
));
}
if (count($arSubMenu)) {
$menu[] = array('SEPARATOR' => 'Y');
$menu[] = $this->getButton('ACTIONS', array(
'MENU' => $arSubMenu,
'ICON' => 'btn_new'
));
}
return $menu;
}
/**
* {@inheritdoc}
*/
public function show()
{
if (!$this->hasReadRights()) {
$this->addErrors(Loc::getMessage('DIGITALWAND_ADMIN_HELPER_ACCESS_FORBIDDEN'));
$this->showMessages();
return false;
}
$context = new \CAdminContextMenu($this->getMenu());
$context->Show();
$this->tabControl->BeginPrologContent();
$this->showMessages();
$this->showProlog();
$this->tabControl->EndPrologContent();
$this->tabControl->BeginEpilogContent();
$this->showEpilog();
$this->tabControl->EndEpilogContent();
$query = $this->additionalUrlParams;
if (isset($_REQUEST[$this->pk()])) {
$query[$this->pk()] = $_REQUEST[$this->pk()];
}
elseif (isset($_REQUEST['SECTION_ID']) && $_REQUEST['SECTION_ID']) {
$this->data[static::getSectionField()] = $_REQUEST['SECTION_ID'];
}
$this->tabControl->Begin(array(
'FORM_ACTION' => static::getUrl($query)
));
foreach ($this->tabs as $tabSettings) {
if ($tabSettings['VISIBLE']) {
$this->showTabElements($tabSettings);
}
}
$this->showEditPageButtons();
$this->tabControl->ShowWarnings('editform', array()); //TODO: дописать
$this->tabControl->Show();
}
/**
* Отображение кнопок для управления элементом на странице редактирования.
*/
protected function showEditPageButtons()
{
$listHelper = static::getHelperClass(AdminListHelper::className());
$this->tabControl->Buttons(array(
'back_url' => $listHelper::getUrl(array_merge($this->additionalUrlParams,
array(
'lang' => LANGUAGE_ID,
'restore_query' => 'Y',
)))
));
}
/**
* Отрисовка верхней части страницы.
*
* @api
*/
protected function showProlog()
{
}
/**
* Отрисовка нижней части страницы. По-умолчанию рисует все поля, которые не попали в вывод, как input hidden.
*
* @api
*/
protected function showEpilog()
{
echo bitrix_sessid_post();
$interfaceSettings = static::getInterfaceSettings();
foreach ($interfaceSettings['FIELDS'] as $code => $settings) {
if (!isset($settings['TAB']) AND isset($settings['FORCE_SELECT']) AND $settings['FORCE_SELECT'] == true) {
print '';
}
}
}
/**
* Отрисовывает вкладку со всеми привязанными к ней полями.
*
* @param $tabSettings
*
* @internal
*/
private function showTabElements($tabSettings)
{
$this->setContext(AdminEditHelper::OP_SHOW_TAB_ELEMENTS);
$this->tabControl->BeginNextFormTab();
foreach ($this->getFields() as $code => $fieldSettings) {
$widget = $this->createWidgetForField($code, $this->data);
$fieldTab = $widget->getSettings('TAB');
$fieldOnCurrentTab = ($fieldTab == $tabSettings['DIV'] OR $tabSettings['DIV'] == 'DEFAULT_TAB');
if (!$fieldOnCurrentTab) {
continue;
}
$fieldSettings = $widget->getSettings();
if (isset($fieldSettings['VISIBLE']) && $fieldSettings['VISIBLE'] === false) {
continue;
}
$this->tabControl->BeginCustomField($code, $widget->getSettings('TITLE'));
$pkField = ($code == $this->pk());
$widget->showBasicEditField($pkField);
$this->tabControl->EndCustomField($code);
}
}
/**
* Обработка запроса редактирования страницы. Этапы:
*
*
Проверка прав пользователя
*
Создание виджетов для каждого поля
*
Удаление значений для READONLY и HIDE_WHEN_CREATE полей
*
Изменение данных модели каждым виджетом (исходя из его внутренней логики)
*
Валидация значений каждого поля соответствующим виджетом
*
Проверка на ошибики валидации
*
В случае неудачи - выход из функции
*
В случае успеха - обновление или добавление элемента в БД
*
Постобработка данных модели каждым виджетом
*
*
* @return bool
*
* @see HelperWidget::processEditAction();
* @see HelperWidget::processAfterSaveAction();
*
* @internal
*/
protected function editAction()
{
$this->setContext(AdminEditHelper::OP_EDIT_ACTION_BEFORE);
if (!$this->hasWriteRights()) {
$this->addErrors(Loc::getMessage('DIGITALWAND_ADMIN_HELPER_EDIT_WRITE_FORBIDDEN'));
return false;
}
$allWidgets = array();
foreach ($this->getFields() as $code => $settings) {
if ($settings['READONLY'] && $code !== $this->pk()) {
unset($this->data[$code]);
}
}
foreach ($this->getFields() as $code => $settings) {
$widget = $this->createWidgetForField($code, $this->data);
$widget->processEditAction();
$this->validationErrors = array_merge($this->validationErrors, $widget->getValidationErrors());
$allWidgets[] = $widget;
if ($widget->getSettings('READONLY') || empty($this->data[$this->pk()])
&& $widget->getSettings('HIDE_WHEN_CREATE')) {
unset($this->data[$code]);
}
}
$this->addErrors($this->validationErrors);
$success = empty($this->validationErrors);
if ($success) {
$this->setContext(AdminEditHelper::OP_EDIT_ACTION_AFTER);
$existing = false;
$id = $this->getPk();
if ($id) {
$existing = $this->loadElement();
}
if ($existing) {
$result = $this->saveElement($id);
}
else {
$result = $this->saveElement();
}
if ($result) {
if (!$result->isSuccess()) {
$this->addErrors($result->getErrorMessages());
return false;
}
}
else {
// TODO Вывод ошибки
return false;
}
$this->data[$this->pk()] = $result->getId();
foreach ($allWidgets as $widget) {
/** @var HelperWidget $widget */
$widget->setData($this->data);
$widget->processAfterSaveAction();
}
return true;
}
return false;
}
/**
* Функция загрузки элемента из БД. Можно переопределить, если требуется сложная логика и нет возможности
* определить её в модели.
*
* @param array $select
*
* @return bool
* @api
*/
protected function loadElement($select = array())
{
if ($this->getPk() !== null) {
$className = static::getModel();
$result = $className::getList(array(
'filter' => array(
$this->pk() => $this->getPk()
),
'select' => $select ?: array('*')
));
return $result->fetch();
}
return false;
}
/**
* Сохранение элемента. Можно переопределить, если требуется сложная логика и нет возможности определить её
* в модели.
*
* Операциями сохранения модели занимается EntityManager.
*
* @param bool $id
*
* @return \Bitrix\Main\Entity\AddResult|\Bitrix\Main\Entity\UpdateResult
*
* @throws \Exception
*
* @see EntityManager
*
* @api
*/
protected function saveElement($id = null)
{
/** @var EntityManager $entityManager */
$entityManager = new static::$entityManager(static::getModel(), empty($this->data) ? array() : $this->data, $id, $this);
$saveResult = $entityManager->save();
$this->addNotes($entityManager->getNotes());
return $saveResult;
}
/**
* Удаление элемента. Можно переопределить, если требуется сложная логика и нет возможности определить её в модели.
*
* @param $id
*
* @return bool|\Bitrix\Main\Entity\DeleteResult
*
* @throws \Exception
*
* @api
*/
protected function deleteElement($id)
{
if (!$this->hasDeleteRights()) {
$this->addErrors(Loc::getMessage('DIGITALWAND_ADMIN_HELPER_EDIT_DELETE_FORBIDDEN'));
return false;
}
/** @var EntityManager $entityManager */
$entityManager = new static::$entityManager(static::getModel(), empty($this->data) ? array() : $this->data, $id, $this);
$deleteResult = $entityManager->delete();
$this->addNotes($entityManager->getNotes());
return $deleteResult;
}
/**
* Выполнение кастомных операций над объектом в процессе редактирования.
*
* @param string $action Название операции.
* @param int|null $id ID элемента.
*
* @see AdminEditHelper::fillMenu()
*
* @api
*/
protected function customActions($action, $id = null)
{
if ($action == 'delete' AND !is_null($id)) {
$result = $this->deleteElement($id);
if(!$result->isSuccess()){
$this->addErrors($result->getErrorMessages());
}
$listHelper = static::getHelperClass(AdminListHelper::className());
$redirectUrl = $listHelper::getUrl(array_merge(
$this->additionalUrlParams,
array('restore_query' => 'Y')
));
LocalRedirect($redirectUrl);
}
}
/**
* Устанавливает заголовок исходя из данных текущего элемента.
*
* @see $data
* @see AdminBaseHelper::setTitle()
*
* @api
*/
protected function setElementTitle()
{
if (!empty($this->data)) {
$title = Loc::getMessage('DIGITALWAND_ADMIN_HELPER_EDIT_TITLE', array('#ID#' => $this->data[$this->pk()]));
}
else {
$title = Loc::getMessage('DIGITALWAND_ADMIN_HELPER_NEW_ELEMENT');
}
$this->setTitle($title);
}
/**
* @return \CAdminForm
*/
public function getTabControl()
{
return $this->tabControl;
}
/**
* @inheritdoc
*/
public static function getUrl(array $params = array())
{
return static::getViewURL(static::getViewName(), static::$editPageUrl, $params);
}
}
================================================
FILE: lib/helper/AdminInterface.php
================================================
* @author Artem Yarygin
*/
abstract class AdminInterface
{
/**
* Список зарегистрированных интерфейсов
* @var string
*/
public static $registeredInterfaces = array();
/**
* Описание интерфейса админки: списка табов и полей. Метод должен вернуть массив вида:
*
* ```
* array(
* 'TAB_1' => array(
* 'NAME' => Loc::getMessage('VENDOR_MODULE_ENTITY_TAB_1_NAME'),
* 'FIELDS' => array(
* 'FIELD_1' => array(
* 'WIDGET' => new StringWidget(),
* 'TITLE' => Loc::getMessage('VENDOR_MODULE_ENTITY_FIELD_1_TITLE'),
* ...
* ),
* 'FIELD_2' => array(
* 'WIDGET' => new NumberWidget(),
* 'TITLE' => Loc::getMessage('VENDOR_MODULE_ENTITY_FIELD_2_TITLE'),
* ...
* ),
* ...
* )
* ),
* 'TAB_2' => array(
* 'NAME' => Loc::getMessage('VENDOR_MODULE_ENTITY_TAB_2_NAME'),
* 'FIELDS' => array(
* 'FIELD_3' => array(
* 'WIDGET' => new DateTimeWidget(),
* 'TITLE' => Loc::getMessage('VENDOR_MODULE_ENTITY_FIELD_3_TITLE'),
* ...
* ),
* 'FIELD_4' => array(
* 'WIDGET' => new UserWidget(),
* 'TITLE' => Loc::getMessage('VENDOR_MODULE_ENTITY_FIELD_4_TITLE'),
* ...
* ),
* ...
* )
* ),
* ...
* )
* ```
*
* Где TAB_1..2 - символьные коды табов, FIELD_1..4 - название столбцов в таблице сущности. TITLE для поля задавать
* не обязательно, в этому случае он будет запрашиваться из модели.
*
* Более подробную информацию о формате описания настроек виджетов см. в классе HelperWidget.
*
* @see DigitalWand\AdminHelper\Widget\HelperWidget
*
* @return array[]
*/
abstract public function fields();
/**
* Список классов хелперов с настройками. Метод должен вернуть массив вида:
*
* ```
* array(
* '\Vendor\Module\Entity\AdminInterface\EntityListHelper' => array(
* 'BUTTONS' => array(
* 'RETURN_TO_LIST' => array('TEXT' => Loc::getMessage('VENDOR_MODULE_ENTITY_RETURN_TO_LIST')),
* 'ADD_ELEMENT' => array('TEXT' => Loc::getMessage('VENDOR_MODULE_ENTITY_ADD_ELEMENT'),
* ...
* )
* ),
* '\Vendor\Module\Entity\AdminInterface\EntityEditHelper' => array(
* 'BUTTONS' => array(
* 'LIST_CREATE_NEW' => array('TEXT' => Loc::getMessage('VENDOR_MODULE_ENTITY_LIST_CREATE_NEW')),
* 'LIST_CREATE_NEW_SECTION' => array('TEXT' => Loc::getMessage('VENDOR_MODULE_ENTITY_LIST_CREATE_NEW_SECTION'),
* ...
* )
* )
* )
* ```
*
* или
*
* ```
* array(
* '\Vendor\Module\Entity\AdminInterface\EntityListHelper',
* '\Vendor\Module\Entity\AdminInterface\EntityEditHelper'
* )
* ```
*
* Где:
*
*
`Vendor\Module\Entity\AdminInterface` - namespace до реализованных классов AdminHelper.
*
`BUTTONS` - ключ для массива с описанием элементов управления (подробнее в методе getButton()
* класса AdminBaseHelper).
*
`LIST_CREATE_NEW`, `LIST_CREATE_NEW_SECTION`, `RETURN_TO_LIST`, `ADD_ELEMENT` - символьные код элементов
* управления.
*
`EntityListHelper` и `EntityEditHelper` - реализованные классы хелперов.
*
* Оба формата могут сочетаться друг с другом.
*
* @see \DigitalWand\AdminHelper\Helper\AdminBaseHelper::getButton()
*
* @return string[]
*/
abstract public function helpers();
/**
* Список зависимых админских интерфейсов, которые будут зарегистрированы при регистраци админского интерфейса,
* например, админские интерфейсы разделов.
*
* @return string[]
*/
public function dependencies()
{
return array();
}
/**
* Регистрируем поля, табы и кнопки.
*/
public function registerData()
{
$fieldsAndTabs = array('FIELDS' => array(), 'TABS' => array());
$tabsWithFields = $this->fields();
// приводим массив хелперов к формату класс => настройки
$helpers = array();
foreach ($this->helpers() as $key => $value) {
if (is_array($value)) {
$helpers[$key] = $value;
}
else {
$helpers[$value] = array();
}
}
$helperClasses = array_keys($helpers);
/**
* @var \Bitrix\Main\Entity\DataManager
*/
$model = $helperClasses[0]::getModel();
foreach ($tabsWithFields as $tabCode => $tab) {
$fieldsAndTabs['TABS'][$tabCode] = $tab['NAME'];
foreach ($tab['FIELDS'] as $fieldCode => $field) {
if (empty($field['TITLE']) && $model) {
//Битрикс не использует параметр title при создании экземпляра ReferenceField.
if (is_a($model::getEntity()->getField($fieldCode), 'Bitrix\Main\Entity\ReferenceField')) {
$map = $model::getMap();
if(isset($map[$fieldCode]['title'])){
$field['TITLE'] = $map[$fieldCode]['title'];
}
} else {
$field['TITLE'] = $model::getEntity()->getField($fieldCode)->getTitle();
}
}
$field['TAB'] = $tabCode;
$fieldsAndTabs['FIELDS'][$fieldCode] = $field;
}
}
AdminBaseHelper::setInterfaceSettings($fieldsAndTabs, $helpers, $helperClasses[0]::getModule());
foreach ($helperClasses as $helperClass) {
/**
* @var AdminBaseHelper $helperClass
*/
$helperClass::setInterfaceClass(get_called_class());
}
}
/**
* Регистрация интерфейса и его зависимостей.
*/
public static function register()
{
if (!in_array(get_called_class(), static::$registeredInterfaces)) {
static::$registeredInterfaces[] = get_called_class();
$adminInterface = new static();
$adminInterface->registerData();
foreach ($adminInterface->dependencies() as $adminInterfaceClass) {
$adminInterfaceClass::register();
}
}
}
}
================================================
FILE: lib/helper/AdminListHelper.php
================================================
*
static protected $model
*
*
* Этого будет дастаточно для получения минимальной функциональности
* Также данный класс может использоваться для отображения всплывающих окон с возможностью выбора элемента из списка
*
* @see AdminBaseHelper::$model
* @see AdminBaseHelper::$module
* @see AdminBaseHelper::$editViewName
* @see AdminBaseHelper::$viewName
* @package AdminHelper
*
* @author Nik Samokhvalov
* @author Artem Yarygin
*/
abstract class AdminListHelper extends AdminBaseHelper
{
const OP_GROUP_ACTION = 'AdminListHelper::__construct_groupAction';
const OP_ADMIN_VARIABLES_FILTER = 'AdminListHelper::prepareAdminVariables_filter';
const OP_ADMIN_VARIABLES_HEADER = 'AdminListHelper::prepareAdminVariables_header';
const OP_GET_DATA_BEFORE = 'AdminListHelper::getData_before';
const OP_ADD_ROW_CELL = 'AdminListHelper::addRowCell';
const OP_CREATE_FILTER_FORM = 'AdminListHelper::createFilterForm';
const OP_CHECK_FILTER = 'AdminListHelper::checkFilter';
const OP_EDIT_ACTION = 'AdminListHelper::editAction';
/**
* @var bool
* Показывать ли кнопки добавления раздела и элемента в списке
*/
protected $showAdd = true;
/**
* @var bool
* Выводить кнопку экспорта в Excel
* @api
*/
protected $exportExcel = true;
/**
* @var bool
* Выводить в списке кол-во элементов пункт Все
*/
protected $showAll = true;
/**
* @var bool
* Является ли список всплывающим окном для выбора элементов из списка.
* В этой версии не должно быть операций удаления/перехода к редактированию.
*/
protected $isPopup = false;
/**
* @var string
* Название поля, в котором хранится результат выбора во всплывающем окне
*/
protected $fieldPopupResultName = '';
/**
* @var string
* Уникальный индекс поля, в котором хранится результат выбора во всплывающем окне
*/
protected $fieldPopupResultIndex = '';
protected $sectionFields = array();
/**
* @var string
* Название столбца, в котором хранится название элемента
*/
protected $fieldPopupResultElTitle = '';
/**
* @var string
* Название функции, вызываемой при даблклике на строке списка, в случае, если список выводится в режиме
* всплывающего окна
*/
protected $popupClickFunctionName = 'selectRow';
/**
* @var string
* Код функции, вызываемой при клике на строке списка
* @see AdminListHelper::genPopupActionJS()
*/
protected $popupClickFunctionCode;
/**
* @var array
* Массив с заголовками таблицы
* @see \CAdminList::AddHeaders()
*/
protected $arHeader = array();
/**
* @var array
* параметры фильтрации списка в классическим битриксовом формате
*/
protected $arFilter = array();
/**
* @var array
* Массив, хранящий тип фильтра для данного поля. Позволяет избежать лишнего парсинга строк.
*/
protected $filterTypes = array();
/**
* @var array
* Поля, предназначенные для фильтрации
* @see \CAdminList::InitFilter();
*/
protected $arFilterFields = array();
/**
* Список полей, для которых доступна фильтрация
* @var array
* @see \CAdminFilter::__construct();
*/
protected $arFilterOpts = array();
/**
* @var \CAdminList
*/
protected $list;
/**
* @var string
* Префикс таблицы. Нужен, чтобы обеспечить уникальность относительно других админ. интерфейсов.
* Без его добавления к конструктору таблицы повычается вероятность, что возникнет конфликт с таблицей из другого
* административного интерфейса, в результате чего неправильно будет работать паджинация, фильтрация. Вероятны
* ошибки запросов к БД.
*/
static protected $tablePrefix = "digitalwand_admin_helper_";
/**
* @var array
* @see \CAdminList::AddGroupActionTable()
*/
protected $groupActionsParams = array();
/**
* Текущие параметры пагинации,
* требуются для составления смешанного списка разделов и элементов
* @var array
*/
protected $navParams = array();
/**
* Количество элементов смешанном списке
* @see AdminListHelper::CustomNavStart
* @var int
*/
protected $totalRowsCount = 0;
/**
* Массив для слияния столбцов элементов и разделов
* @var array
*/
protected $tableColumnsMap = array();
/**
* @var string
* HTML верхней части таблицы
* @api
*/
public $prologHtml;
/**
* @var string
* HTML нижней части таблицы
* @api
*/
public $epilogHtml;
/**
* Производится инициализация переменных, обработка запросов на редактирование
*
* @param array $fields
* @param bool $isPopup
* @throws \Bitrix\Main\ArgumentException
*/
public function __construct(array $fields, $isPopup = false)
{
$this->isPopup = $isPopup;
if ($this->isPopup) {
$this->fieldPopupResultName = preg_replace("/[^a-zA-Z0-9_:\\[\\]]/", "", $_REQUEST['n']);
$this->fieldPopupResultIndex = preg_replace("/[^a-zA-Z0-9_:]/", "", $_REQUEST['k']);
$this->fieldPopupResultElTitle = $_REQUEST['eltitle'];
}
parent::__construct($fields);
$this->restoreLastGetQuery();
$this->prepareAdminVariables();
$className = static::getModel();
$oSort = $this->initSortingParameters();
$this->list = new \CAdminList($this->getListTableID(), $oSort);
$this->list->InitFilter($this->arFilterFields);
if ($this->list->EditAction() AND $this->hasWriteRights()) {
global $FIELDS;
foreach ($FIELDS as $id => $fields) {
if (!$this->list->IsUpdated($id)) {
continue;
}
$this->editAction($id, $fields);
}
}
if ($IDs = $this->list->GroupAction() AND $this->hasWriteRights()) {
//Элементы выбраны галочкой "Для всех". Нужно собрать все элементы и разделы,
//попадающие под текущий фильтр, и передать их ID на удаление
if ($_REQUEST['action_target'] == 'selected') {
$this->setContext(AdminListHelper::OP_GROUP_ACTION);
//Если находимся в подразделе, то его нужно учесть при фильтрации
if (isset($_GET['SECTION_ID'])) {
$sectionField = static::getSectionField();
$this->arFilter[$sectionField] = $_GET['SECTION_ID'];
}
$IDs = array();
//Текущий фильтр должен быть модифицирован виждтами
//для соответствия результатов фильтрации тому, что видит пользователь в интерфейсе.
$raw = array(
'SELECT' => $this->pk(),
'FILTER' => $this->arFilter,
'SORT' => array()
);
foreach ($this->fields as $code => $settings) {
$widget = $this->createWidgetForField($code);
$widget->changeGetListOptions($this->arFilter, $raw['SELECT'], $raw['SORT'], $raw);
}
$res = $className::getList(array(
'filter' => $this->arFilter,
'select' => array($this->pk()),
));
while ($el = $res->Fetch()) {
$IDs[] = $el[$this->pk()];
}
//Собираем ID разделов, если они используются
$sectionEditHelperClass = $this->getHelperClass(AdminSectionEditHelper::className());
if ($sectionEditHelperClass) {
$sectionFilter = $this->arFilter;
$sectionsInterfaceSettings = static::getInterfaceSettings($sectionEditHelperClass::getViewName());
if ($sectionEditHelperClass && !isset($_REQUEST['model-section'])) {
$sectionClassName = $sectionEditHelperClass::getModel();
} else {
$sectionClassName = $_REQUEST['model-section'];
}
foreach ($sectionFilter as $elementFieldName => $elementFilterValue) {
$elementFieldNameEscaped = $this->escapeFilterFieldName($elementFieldName);
if (!isset($sectionsInterfaceSettings['FIELDS'][$elementFieldNameEscaped])) {
unset($sectionFilter[$elementFieldName]);
}
}
if (isset($_GET['SECTION_ID'])) {
$sectionField = $sectionEditHelperClass::getSectionField();
$sectionFilter[$sectionField] = $_GET['SECTION_ID'];
}
$res = $sectionClassName::getList(array(
'filter' => $sectionFilter,
'select' => array($this->pk()),
));
while ($el = $res->Fetch()) {
$IDs[] = 's' . $el[$this->pk()];
}
}
}
$filteredIDs = array();
foreach ($IDs as $id) {
if (strlen($id) <= 0) {
continue;
}
$filteredIDs[] = IntVal($id);
}
$this->groupActions($IDs, $_REQUEST['action']);
}
if (isset($_REQUEST['action']) || isset($_REQUEST['action_button']) && count($this->getErrors()) == 0) {
$listHelperClass = $this->getHelperClass(AdminListHelper::className());
$id = isset($_GET['ID']) ? $_GET['ID'] : null;
$action = isset($_REQUEST['action']) ? $_REQUEST['action'] : $_REQUEST['action_button'];
if ($action != 'edit' && $_REQUEST['cancel'] != 'Y') {
$params = $_GET;
unset($params['action']);
unset($params['action_button']);
$this->customActions($action, $id);
LocalRedirect($listHelperClass::getUrl($params));
}
}
if ($this->isPopup()) {
$this->genPopupActionJS();
}
// Получаем параметры навигации
$navUniqSettings = array(
'nPageSize' => 20,
'sNavID' => $this->getListTableID()
);
$this->navParams = array(
'nPageSize' => \CAdminResult::GetNavSize($this->getListTableID(), $navUniqSettings),
'navParams' => \CAdminResult::GetNavParams($navUniqSettings)
);
}
/**
* Инициализирует параметры сортировки на основании запроса
* @return \CAdminSorting
*/
protected function initSortingParameters()
{
$sortByParameter = 'by';
$sortOrderParameter = 'order';
return new Sorting($this->getListTableID(), $this->pk(), 'desc', $sortByParameter, $sortOrderParameter, $this);
}
/**
* Подготавливает переменные, используемые для инициализации списка.
*
* - добавляет поля в список фильтра только если FILTER не задано false по умолчанию для виджета и поле не является
* полем связи сущностью разделов
*/
protected function prepareAdminVariables()
{
$this->arHeader = array();
$this->arFilter = array();
$this->arFilterFields = array();
$arFilter = array();
$this->filterTypes = array();
$this->arFilterOpts = array();
$sectionField = static::getSectionField();
foreach ($this->fields as $code => $settings) {
$widget = $this->createWidgetForField($code);
if (
($sectionField != $code && $widget->getSettings('FILTER') !==false)
&&
((isset($settings['FILTER']) AND $settings['FILTER'] != false) OR !isset($settings['FILTER']))
) {
$this->setContext(AdminListHelper::OP_ADMIN_VARIABLES_FILTER);
$filterVarName = 'find_' . $code;
$this->arFilterFields[] = $filterVarName;
$filterType = '';
if (is_string($settings['FILTER'])) {
$filterType = $settings['FILTER'];
}
if (isset($_REQUEST[$filterVarName])
AND !isset($_REQUEST['del_filter'])
AND $_REQUEST['del_filter'] != 'Y'
) {
$arFilter[$filterType . $code] = $_REQUEST[$filterVarName];
$this->filterTypes[$code] = $filterType;
}
$this->arFilterOpts[$code] = $widget->getSettings('TITLE');
}
if (!isset($settings['LIST']) || $settings['LIST'] === true) {
$this->setContext(AdminListHelper::OP_ADMIN_VARIABLES_HEADER);
$mergedColumn = false;
// проверяем есть ли столбец раздела с таким названием
if ($widget->getSettings('LIST_TITLE')) {
$sectionHeader = $this->getSectionsHeader();
foreach ($sectionHeader as $sectionColumn) {
if ($sectionColumn['content'] == $widget->getSettings('LIST_TITLE')) {
// добавляем столбец элементов в карту столбцов
$this->tableColumnsMap[$code] = $sectionColumn['id'];
$mergedColumn = true;
break;
}
}
}
if (!$mergedColumn) {
$this->arHeader[] = array(
"id" => $code,
"content" => $widget->getSettings('LIST_TITLE') ? $widget->getSettings('LIST_TITLE') : $widget->getSettings('TITLE'),
"sort" => $code,
"default" => !isset($settings['HEADER']) || $settings['HEADER'] === true,
'admin_list_helper_sort' => $widget->getSettings('LIST_COLUMN_SORT') ? $widget->getSettings('LIST_COLUMN_SORT') : 100
);
}
}
}
if ($this->checkFilter($arFilter)) {
$this->arFilter = $arFilter;
}
if (static::getHelperClass(AdminSectionEditHelper::className())) {
$this->arFilter[static::getSectionField()] = $_GET['ID'];
}
}
/**
* Возвращает список столбцов для разделов
* @return array
*/
public function getSectionsHeader()
{
$arSectionsHeaders = array();
$sectionHelper = static::getHelperClass(AdminSectionEditHelper::className());
$sectionsInterfaceSettings = static::getInterfaceSettings($sectionHelper::getViewName());
$this->sectionFields = $sectionsInterfaceSettings['FIELDS'];
foreach ($sectionsInterfaceSettings['FIELDS'] as $code => $settings) {
if (!isset($settings['LIST']) || $settings['LIST'] === true) {
$arSectionsHeaders[] = array(
"id" => $code,
"content" => isset($settings['LIST_TITLE']) ? $settings['LIST_TITLE'] : $settings['TITLE'],
"sort" => $code,
"default" => !isset($settings['HEADER']) || $settings['HEADER'] === true,
'admin_list_helper_sort' => isset($settings['LIST_COLUMN_SORT']) ? $settings['LIST_COLUMN_SORT'] : 100
);
}
unset($settings['WIDGET']);
foreach ($settings as $c => $v) {
$sectionsInterfaceSettings['FIELDS'][$code]['WIDGET']->setSetting($c, $v);
}
}
return $arSectionsHeaders;
}
/**
* Производит проверку корректности данных (в массиве $_REQUEST), переданных в фильтр
* @TODO: нужно сделать вывод сообщений об ошибке фильтрации.
* @param $arFilter
* @return bool
*/
protected function checkFilter($arFilter)
{
$this->setContext(AdminListHelper::OP_CHECK_FILTER);
$filterValidationErrors = array();
foreach ($this->filterTypes as $code => $type) {
$widget = $this->createWidgetForField($code);
$value = $arFilter[$type . $code];
if (!$widget->checkFilter($type, $value)) {
$filterValidationErrors = array_merge($filterValidationErrors,
$widget->getValidationErrors());
}
}
return empty($filterValidationErrors);
}
/**
* Подготавливает массив с настройками контекстного меню. По-умолчанию добавлена кнопка "создать элемент".
*
* @see $contextMenu
*
* @api
*/
protected function getContextMenu()
{
$contextMenu = array();
/** @var AdminSectionEditHelper $sectionEditHelper */
$sectionEditHelper = static::getHelperClass(AdminSectionEditHelper::className());
if ($sectionEditHelper) {
$sectionId = $_GET['SECTION_ID'] ?: $_GET['ID'] ?: null;
$this->additionalUrlParams['SECTION_ID'] = $sectionId = $sectionId > 0 ? (int)$sectionId : null;
}
/**
* Если задан для разделов добавляем кнопку создать раздел и
* кнопку на уровень вверх если это не корневой раздел
*/
if (isset($sectionId)) {
$params = $this->additionalUrlParams;
$sectionModel = $sectionEditHelper::getModel();
$sectionField = $sectionEditHelper::getSectionField();
$section = $sectionModel::getById(
$this->getCommonPrimaryFilterById($sectionModel, null, $sectionId)
)->Fetch();
if ($this->isPopup()) {
$params = array_merge($_GET);
}
if ($section[$sectionField]) {
$params['ID'] = $section[$sectionField];
}
else {
unset($params['ID']);
}
unset($params['SECTION_ID']);
$contextMenu[] = $this->getButton('LIST_SECTION_UP', array(
'LINK' => static::getUrl($params),
'ICON' => 'btn_list'
));
}
/**
* Добавляем кнопку создать элемент и создать раздел
*/
if (!$this->isPopup() && $this->hasWriteRights() && $this->showAdd) {
$editHelperClass = static::getHelperClass(AdminEditHelper::className());
if ($editHelperClass) {
$contextMenu[] = $this->getButton('LIST_CREATE_NEW', array(
'LINK' => $editHelperClass::getUrl($this->additionalUrlParams),
'ICON' => 'btn_new'
));
}
$sectionsHelperClass = static::getHelperClass(AdminSectionEditHelper::className());
if ($sectionsHelperClass) {
$contextMenu[] = $this->getButton('LIST_CREATE_NEW_SECTION', array(
'LINK' => $sectionsHelperClass::getUrl($this->additionalUrlParams),
'ICON' => 'btn_new'
));
}
}
return $contextMenu;
}
/**
* Возвращает массив с настройками групповых действий над списком.
*
* @return array
*
* @api
*/
protected function getGroupActions()
{
$result = array();
if (!$this->isPopup()) {
if ($this->hasDeleteRights()) {
$result = array('delete' => Loc::getMessage("DIGITALWAND_ADMIN_HELPER_LIST_DELETE"));
}
}
return $result;
}
/**
* Обработчик групповых операций. По-умолчанию прописаны операции активации / деактивации и удаления.
*
* @param array $IDs
* @param string $action
*
* @api
*/
protected function groupActions($IDs, $action)
{
$sectionEditHelperClass = $this->getHelperClass(AdminSectionEditHelper::className());
$listHelperClass = $this->getHelperClass(AdminListHelper::className());
$className = static::getModel();
if (isset($_REQUEST['model'])) {
$className = $_REQUEST['model'];
}
if ($sectionEditHelperClass && !isset($_REQUEST['model-section'])) {
$sectionClassName = $sectionEditHelperClass::getModel();
}
else {
$sectionClassName = $_REQUEST['model-section'];
}
if ($action == 'delete') {
if ($this->hasDeleteRights()) {
$complexPrimaryKey = is_array($className::getEntity()->getPrimary());
if ($complexPrimaryKey) {
$IDs = $this->getIds();
}
// ищем правильный урл для перехода
if (!empty($IDs[0])) {
$id = $complexPrimaryKey ? $IDs[0][$this->pk()] : $IDs[0];
$model = $className;
if (strpos($id, 's') === 0) {
$model = $sectionClassName;
$listHelper = $this->getHelperClass(AdminSectionListHelper::className());
if (!$listHelper) {
$this->addErrors(Loc::getMessage('DIGITALWAND_ADMIN_HELPER_LIST_SECTION_HELPER_NOT_FOUND'));
unset($_GET['ID']);
return;
}
$id = substr($id, 1);
} else {
$listHelper = $listHelperClass;
}
if ($listHelper) {
$id = $this->getCommonPrimaryFilterById($model, null, $id);
$element = $model::getById($id)->Fetch();
$sectionField = $listHelper::getSectionField();
if ($element[$sectionField]) {
$_GET[$this->pk()] = $element[$sectionField];
} else {
unset($_GET['ID']);
}
}
}
foreach ($IDs as $id) {
$model = $className;
$id = $complexPrimaryKey ? $id[$this->pk()] : $id;
if (strpos($id, 's') === 0) {
$model = $sectionClassName;
$id = substr($id, 1);
}
/** @var EntityManager $entityManager */
$entityManager = new static::$entityManager($model, empty($this->data) ? array() : $this->data, $id,
$this);
$result = $entityManager->delete();
$this->addNotes($entityManager->getNotes());
if (!$result->isSuccess()) {
$this->addErrors($result->getErrorMessages());
break;
}
}
}
else {
$this->addErrors(Loc::getMessage('DIGITALWAND_ADMIN_HELPER_LIST_DELETE_FORBIDDEN'));
}
}
if ($action == 'delete-section') {
if ($this->hasDeleteRights()) {
// ищем правильный урл для перехода
if (!empty($IDs[0])) {
$id = $this->getCommonPrimaryFilterById($sectionClassName, null, $IDs[0]);
$sectionListHelperClass = $this->getHelperClass(AdminSectionListHelper::className());
if ($sectionListHelperClass) {
$element = $sectionClassName::getById($id)->Fetch();
$sectionField = $sectionListHelperClass::getSectionField();
if ($element[$sectionField]) {
$_GET[$this->pk()] = $element[$sectionField];
} else {
unset($_GET['ID']);
}
} else {
unset($_GET['ID']);
$this->addErrors(Loc::getMessage('DIGITALWAND_ADMIN_HELPER_LIST_SECTION_HELPER_NOT_FOUND'));
return;
}
}
foreach ($IDs as $id) {
$id = $this->getCommonPrimaryFilterById($sectionClassName, null, $id);
$entityManager = new static::$entityManager($sectionClassName, array(), $id, $this);
$result = $entityManager->delete();
$this->addNotes($entityManager->getNotes());
if(!$result->isSuccess()){
$this->addErrors($result->getErrorMessages());
break;
}
}
}
else {
$this->addErrors(Loc::getMessage('DIGITALWAND_ADMIN_HELPER_LIST_DELETE_FORBIDDEN'));
}
}
}
/**
* Сохранение полей для отной записи, отредактированной в списке.
* Этапы:
*
*
Выборка элемента по ID, чтобы удостовериться, что он существует. В противном случае возвращается
* ошибка
*
Создание виджета для каждой ячейки, валидация значений поля
*
TODO: вывод ошибок валидации
*
Сохранение записи
*
Вывод ошибок сохранения, если таковые появились
*
Модификация данных сроки виджетами.
*
*
* @param int $id ID записи в БД
* @param array $fields Поля с изменениями
*
* @see HelperWidget::processEditAction();
* @see HelperWidget::processAfterSaveAction();
*/
protected function editAction($id, $fields)
{
$this->setContext(AdminListHelper::OP_EDIT_ACTION);
if(strpos($id, 's')===0){ // для раделов другой класс модели
$editHelperClass = $this->getHelperClass(AdminSectionEditHelper::className());
$sectionsInterfaceSettings = static::getInterfaceSettings($editHelperClass::getViewName());
$className = $editHelperClass::getModel();
$id = str_replace('s','',$id);
}else{
$className = static::getModel();
$sectionsInterfaceSettings = false;
}
$idForLog = $id;
$complexPrimaryKey = is_array($className::getEntity()->getPrimary());
if ($complexPrimaryKey) {
$oldRequest = $_REQUEST;
$_REQUEST = array($this->pk() => $id);
$id = $this->getCommonPrimaryFilterById($className, null, $id);
$idForLog = json_encode($id);
$_REQUEST = $oldRequest;
}
$el = $className::getById($id);
if ($el->getSelectedRowsCount() == 0) {
$this->list->AddGroupError(Loc::getMessage("MAIN_ADMIN_SAVE_ERROR"), $idForLog);
return;
}
// замена кодов для столбцов элементов соединенных со столбцами разделов
if($sectionsInterfaceSettings==false){
$tableColumnsMap = array_flip($this->tableColumnsMap);
$replacedFields = array();
foreach($fields as $key => $value){
if(!empty($tableColumnsMap[$key])) {
$key = $tableColumnsMap[$key];
}
$replacedFields[$key] = $value;
}
$fields = $replacedFields;
}
$allWidgets = array();
foreach ($fields as $key => $value) {
if($sectionsInterfaceSettings!==false){ // для разделов свои виджеты
$widget = $sectionsInterfaceSettings['FIELDS'][$key]['WIDGET'];
}else{
$widget = $this->createWidgetForField($key, $fields); // для элементов свои
}
$widget->processEditAction();
$this->validationErrors = array_merge($this->validationErrors, $widget->getValidationErrors());
$allWidgets[] = $widget;
}
//FIXME: может, надо добавить вывод ошибок ДО сохранения?..
$this->addErrors($this->validationErrors);
$result = $className::update($id, $fields);
$errors = $result->getErrorMessages();
if (empty($this->validationErrors) AND !empty($errors)) {
$fieldList = implode("\n", $errors);
$this->list->AddGroupError(Loc::getMessage("MAIN_ADMIN_SAVE_ERROR") . " " . $fieldList, $idForLog);
}
if (!empty($errors)) {
foreach ($allWidgets as $widget) {
/** @var \DigitalWand\AdminHelper\Widget\HelperWidget $widget */
$widget->setData($fields);
$widget->processAfterSaveAction();
}
}
}
/**
* Является ли список всплывающим окном для выбора элементов из списка.
* В этой версии не должно быть операций удаления/перехода к редактированию.
*
* @return boolean
*/
public function isPopup()
{
return $this->isPopup;
}
/**
* Функция определяет js-функцию для двойонго клика по строке.
* Вызывается в том случае, если окно открыто в режиме попапа.
*
* @api
*/
protected function genPopupActionJS()
{
$this->popupClickFunctionCode = '';
}
/**
* Основной цикл отображения списка. Этапы:
*
*
Вывод заголовков страницы
*
Определение списка видимых колонок и колонок, участвующих в выборке.
*
Создание виджета для каждого поля выборки
*
Модификация параметров запроса каждым из виджетов
*
Выборка данных
*
Вывод строк таблицы. Во время итерации по строкам возможна модификация данных строки.
*
* @param array $sort Настройки сортировки.
*
* @see AdminListHelper::getList();
* @see AdminListHelper::getMixedData();
* @see AdminListHelper::modifyRowData();
* @see AdminListHelper::addRowCell();
* @see AdminListHelper::addRow();
* @see HelperWidget::changeGetListOptions();
*/
public function buildList($sort)
{
$this->setContext(AdminListHelper::OP_GET_DATA_BEFORE);
$headers = $this->arHeader;
$sectionEditHelper = static::getHelperClass(AdminSectionEditHelper::className());
if ($sectionEditHelper) { // если есть реализация класса AdminSectionEditHelper, значит используются разделы
$sectionHeaders = $this->getSectionsHeader();
foreach ($sectionHeaders as $sectionHeader) {
$found = false;
foreach ($headers as $i => $elementHeader) {
if ($sectionHeader['content'] == $elementHeader['content'] || $sectionHeader['id'] == $elementHeader['id']) {
if (!$elementHeader['default'] && $sectionHeader['default']) {
$headers[$i] = $sectionHeader;
} else {
$found = true;
}
break;
}
}
if (!$found) {
$headers[] = $sectionHeader;
}
}
}
// сортировка столбцов с сохранением исходной позиции в
// массиве для развнозначных элементов
// массив $headers модифицируется
$this->mergeSortHeader($headers);
$this->list->AddHeaders($headers);
$visibleColumns = $this->list->GetVisibleHeaderColumns();
$modelClass = $this->getModel();
$elementFields = array_keys($modelClass::getEntity()->getFields());
if ($sectionEditHelper) {
$sectionsVisibleColumns = array();
foreach ($visibleColumns as $k => $v) {
if (isset($this->sectionFields[$v])) {
if(!in_array($v, $elementFields)){
unset($visibleColumns[$k]);
}
if (!isset($this->sectionFields[$v]['LIST']) || $this->sectionFields[$v]['LIST'] !== false) {
$sectionsVisibleColumns[] = $v;
}
}
}
$visibleColumns = array_values($visibleColumns);
$visibleColumns = array_merge($visibleColumns, array_keys($this->tableColumnsMap));
}
$className = static::getModel();
$visibleColumns[] = $this->pk();
$sectionsVisibleColumns[] = $this->sectionPk();
$raw = array(
'SELECT' => $visibleColumns,
'FILTER' => $this->arFilter,
'SORT' => $sort
);
foreach ($this->fields as $name => $settings) {
$key = array_search($name, $visibleColumns);
if ((isset($settings['VIRTUAL']) AND $settings['VIRTUAL'] == true)) {
unset($visibleColumns[$key]);
unset($this->arFilter[$name]);
unset($sort[$name]);
}
if (isset($settings['LIST']) && $settings['LIST'] === false) {
unset($visibleColumns[$key]);
}
if (isset($settings['FORCE_SELECT']) AND $settings['FORCE_SELECT'] == true) {
$visibleColumns[] = $name;
}
}
$visibleColumns = array_unique($visibleColumns);
$sectionsVisibleColumns = array_unique($sectionsVisibleColumns);
foreach ($this->fields as $code => $settings) {
if($_REQUEST['del_filter'] !== 'Y') {
$widget = $this->createWidgetForField($code);
$widget->changeGetListOptions($this->arFilter, $visibleColumns, $sort, $raw);
}
// Множественные поля не должны быть в селекте
if (!empty($settings['MULTIPLE'])) {
$visibleColumns = array_diff($visibleColumns, array($code));
}
}
if ($sectionEditHelper) // Вывод разделов и элементов в одном списке
{
$mixedData = $this->getMixedData($sectionsVisibleColumns, $visibleColumns, $sort, $raw);
$res = new \CDbResult;
$res->InitFromArray($mixedData);
$res = new \CAdminResult($res, $this->getListTableID());
$res->nSelectedCount = $this->totalRowsCount;
// используем кастомный NavStart что бы определить правильное количество страниц и элементов в списке
$this->customNavStart($res);
$this->list->NavText($res->GetNavPrint(Loc::getMessage("PAGES")));
while ($data = $res->NavNext(false)) {
$this->modifyRowData($data);
if ($data['IS_SECTION']) // для разделов своя обработка
{
list($link, $name) = $this->getRow($data, $this->getHelperClass(AdminSectionEditHelper::className()));
$row = $this->list->AddRow('s' . $data[$this->pk()], $data, $link, $name);
foreach ($this->sectionFields as $code => $settings) {
if (in_array($code, $sectionsVisibleColumns)) {
$this->addRowSectionCell($row, $code, $data);
}
}
$row->AddActions($this->getRowActions($data, true));
}
else // для элементов своя
{
$this->modifyRowData($data);
list($link, $name) = $this->getRow($data);
// объединение полей элемента с полями раздела
foreach ($this->tableColumnsMap as $elementCode => $sectionCode) {
if (isset($data[$elementCode])) {
$data[$sectionCode] = $data[$elementCode];
}
}
$row = $this->list->AddRow($data[$this->pk()], $data, $link, $name);
foreach ($this->fields as $code => $settings) {
if(in_array($code, $visibleColumns)) {
$this->addRowCell($row, $code, $data,
isset($this->tableColumnsMap[$code]) ? $this->tableColumnsMap[$code] : false);
}
}
$row->AddActions($this->getRowActions($data));
}
}
}
else // Обычный вывод элементов без использования разделов
{
$this->totalRowsCount = $className::getCount($this->getElementsFilter($this->arFilter));
$res = $this->getData($className, $this->arFilter, $visibleColumns, $sort, $raw);
$res = new \CAdminResult($res, $this->getListTableID());
$this->customNavStart($res);
// отключаем отображение всех элементов, если установлено св-во
$res->bShowAll = $this->showAll;
$this->list->NavText($res->GetNavPrint(Loc::getMessage("PAGES")));
while ($data = $res->NavNext(false)) {
$this->modifyRowData($data);
list($link, $name) = $this->getRow($data);
$row = $this->list->AddRow($data[$this->pk()], $data, $link, $name);
foreach ($this->fields as $code => $settings) {
if(in_array($code, $visibleColumns)) {
$this->addRowCell($row, $code, $data);
}
}
$row->AddActions($this->getRowActions($data));
}
}
$this->list->AddFooter($this->getFooter($res));
$this->list->AddGroupActionTable($this->getGroupActions(), $this->groupActionsParams);
$this->list->AddAdminContextMenu($this->getContextMenu(), $this->exportExcel);
$this->list->BeginPrologContent();
echo $this->prologHtml;
$this->list->EndPrologContent();
$this->list->BeginEpilogContent();
echo $this->epilogHtml;
$this->list->EndEpilogContent();
// добавляем ошибки в CAdminList для режимов list и frame
$errors = $this->getErrors();
if(in_array($_GET['mode'], array('list','frame')) && is_array($errors)) {
foreach($errors as $error) {
$this->list->addGroupError($error);
}
}
$this->list->CheckListMode();
}
/**
* Функция сортировки столбцов c сохранением порядка равнозначных элементов
* @param $array
*/
protected function mergeSortHeader(&$array)
{
// для сортировки нужно хотя бы 2 элемента
if (count($array) < 2) return;
// делим массив пополам
$halfway = count($array) / 2;
$array1 = array_slice($array, 0, $halfway);
$array2 = array_slice($array, $halfway);
// реукрсивно сортируем каждую половину
$this->mergeSortHeader($array1);
$this->mergeSortHeader($array2);
// если последний элемент первой половины меньше или равен первому элементу
// второй половины, то просто соединяем массивы
if ($this->mergeSortHeaderCompare(end($array1), $array2[0]) < 1) {
$array = array_merge($array1, $array2);
return;
}
// соединяем 2 отсортированных половины в один отсортированный массив
$array = array();
$ptr1 = $ptr2 = 0;
while ($ptr1 < count($array1) && $ptr2 < count($array2)) {
// собираем в 1 массив последовательную цепочку
// элементов из 2-х отсортированных половинок
if ($this->mergeSortHeaderCompare($array1[$ptr1], $array2[$ptr2]) < 1) {
$array[] = $array1[$ptr1++];
}
else {
$array[] = $array2[$ptr2++];
}
}
// если в исходных массивах что-то осталось забираем в основной массив
while ($ptr1 < count($array1)) $array[] = $array1[$ptr1++];
while ($ptr2 < count($array2)) $array[] = $array2[$ptr2++];
return;
}
/**
* Функция сравнения столбцов по их весу в сортировке
* @param $a
* @param $b
* @return int
*/
public function mergeSortHeaderCompare($a, $b)
{
$a = $a['admin_list_helper_sort'];
$b = $b['admin_list_helper_sort'];
if ($a == $b) {
return 0;
}
return ($a < $b) ? -1 : 1;
}
/**
* Получение смешанного списка из разделов и элементов.
*
* @param $sectionsVisibleColumns
* @param $elementVisibleColumns
* @param $sort
* @param $raw
* @return array
*/
protected function getMixedData($sectionsVisibleColumns, $elementVisibleColumns, $sort, $raw)
{
$sectionEditHelperClass = $this->getHelperClass(AdminSectionEditHelper::className());
$elementEditHelperClass = $this->getHelperClass(AdminEditHelper::className());
$sectionField = $sectionEditHelperClass::getSectionField();
$sectionId = $_GET['SECTION_ID'] ? $_GET['SECTION_ID'] : $_GET['ID'];
$returnData = array();
/**
* @var DataManager $sectionModel
*/
$sectionModel = $sectionEditHelperClass::getModel();
$sectionFilter = array();
// добавляем из фильтра те поля которые есть у разделов
foreach ($this->arFilter as $field => $value) {
$fieldName = $this->escapeFilterFieldName($field);
if(!empty($this->tableColumnsMap[$fieldName])) {
$field = str_replace($fieldName, $this->tableColumnsMap[$fieldName], $field);
$fieldName = $this->tableColumnsMap[$fieldName];
}
if (isset($this->sectionFields[$fieldName])) {
$sectionFilter[$field] = $value;
}
}
$sectionFilter[$sectionField] = $sectionId;
$raw['SELECT'] = array_unique($raw['SELECT']);
// при использовании в качестве popup окна исключаем раздел из выборке
// что бы не было возможности сделать раздел родителем самого себя
if (!empty($_REQUEST['self_id'])) {
$sectionFilter['!' . $this->sectionPk()] = $_REQUEST['self_id'];
}
$sectionSort = array();
$limitData = $this->getLimits();
// добавляем к общему количеству элементов количество разделов
$this->totalRowsCount = $sectionModel::getCount($this->getSectionsFilter($sectionFilter));
foreach ($sort as $field => $direction) {
if (in_array($field, $sectionsVisibleColumns)) {
$sectionSort[$field] = $direction;
}
}
// добавляем к выборке разделы
$rsSections = $sectionModel::getList(array(
'filter' => $this->getSectionsFilter($sectionFilter),
'select' => $sectionsVisibleColumns,
'order' => $sectionSort,
'limit' => $limitData[1],
'offset' => $limitData[0],
));
while ($section = $rsSections->fetch()) {
$section['IS_SECTION'] = true;
$returnData[] = $section;
}
// расчитываем offset и limit для элементов
if (count($returnData) > 0) {
$elementOffset = 0;
}
else {
$elementOffset = $limitData[0] - $this->totalRowsCount;
}
// для списка разделов элементы не нужны
if (static::getHelperClass(AdminSectionListHelper::className()) == static::className()) {
return $returnData;
}
$elementLimit = $limitData[1] - count($returnData);
$elementModel = static::$model;
$elementFilter = $this->arFilter;
if(get_called_class() != static::getHelperClass(AdminSectionListHelper::className())) {
$elementFilter[$elementEditHelperClass::getSectionField()] = $sectionId;
}
// добавляем к общему количеству элементов количество элементов
$this->totalRowsCount += $elementModel::getCount($this->getElementsFilter($elementFilter));
// возвращае данные без элементов если разделы занимают всю страницу выборки
if (!empty($returnData) && $limitData[0] == 0 && $limitData[1] == $this->totalRowsCount) {
return $returnData;
}
$elementSort = array();
foreach ($sort as $field => $direction) {
if (in_array($field, $elementVisibleColumns)) {
$elementSort[$field] = $direction;
}
}
$elementParams = array(
'filter' => $this->getElementsFilter($elementFilter),
'select' => $elementVisibleColumns,
'order' => $elementSort,
);
if ($elementLimit > 0 && $elementOffset >= 0) {
$elementParams['limit'] = $elementLimit;
$elementParams['offset'] = $elementOffset;
// добавляем к выборке элементы
$rsSections = $elementModel::getList($elementParams);
while ($element = $rsSections->fetch()) {
$element['IS_SECTION'] = false;
$returnData[] = $element;
}
}
/**
* Вернем результат с первой страницы если на текущей нет элементов.
* Для списка элементов аналогичная проверка есть в $this->getLimits()
*/
if (!count($returnData) && $this->totalRowsCount > 0)
{
$this->navParams['navParams']['PAGEN'] = 1;
return $this->getMixedData($sectionsVisibleColumns, $elementVisibleColumns, $sort, $raw);
}
return $returnData;
}
/**
* Огранчения выборки из CAdminResult
* @return array
*/
protected function getLimits()
{
if ($this->navParams['navParams']['SHOW_ALL']) {
return array();
}
else {
if (!intval($this->navParams['navParams']['PAGEN']) OR !isset($this->navParams['navParams']['PAGEN'])) {
$this->navParams['navParams']['PAGEN'] = 1;
}
$from = $this->navParams['nPageSize'] * ((int)$this->navParams['navParams']['PAGEN'] - 1);
/**
* Вернем результат с первой страницы если на текущей нет элементов.
*
* $this->totalRowsCount еще не заполнен при смешанном отображении элементов и разделов,
* в $this->>getMixedData() есть отдельная проверка на этот счет
*/
if ($this->totalRowsCount && $from >= $this->totalRowsCount)
{
$this->navParams['navParams']['PAGEN'] = 1;
$from = 0;
}
return array($from, $this->navParams['nPageSize']);
}
}
/**
* Очищает название поля от операторов фильтра
* @param string $fieldName названия поля из фильтра
* @return string название поля без без операторов фильтра
*/
protected function escapeFilterFieldName($fieldName)
{
return str_replace(array('!','<', '<=', '>', '>=', '><', '=', '%'), '', $fieldName);
}
/**
* Выполняет CDBResult::NavNext с той разницей, что общее количество элементов берется не из count($arResult),
* а из нашего параметра, полученного из SQL-запроса.
* array_slice также не делается.
*
* @param \CAdminResult $res
*/
protected function customNavStart(&$res)
{
$res->NavStart($this->navParams['nPageSize'],
$this->navParams['navParams']['SHOW_ALL'],
(int)$this->navParams['navParams']['PAGEN']
);
// отключаем отображение всех элементов
$res->bShowAll = $this->showAll;
$res->NavRecordCount = $this->totalRowsCount;
if ($res->NavRecordCount < 1)
return;
if ($res->NavShowAll)
$res->NavPageSize = $res->NavRecordCount;
$res->NavPageCount = floor($res->NavRecordCount / $res->NavPageSize);
if ($res->NavRecordCount % $res->NavPageSize > 0)
$res->NavPageCount++;
$res->NavPageNomer =
($res->PAGEN < 1 || $res->PAGEN > $res->NavPageCount
?
(\CPageOption::GetOptionString("main", "nav_page_in_session", "Y") != "Y"
|| $_SESSION[$res->SESS_PAGEN] < 1
|| $_SESSION[$res->SESS_PAGEN] > $res->NavPageCount
?
1
:
$_SESSION[$res->SESS_PAGEN]
)
:
$res->PAGEN
);
}
/**
* Преобразует данные строки, перед тем как добавлять их в список.
*
* @param $data
*
* @see AdminListHelper::getList()
*
* @api
*/
protected function modifyRowData(&$data)
{
}
/**
* Настройки строки таблицы.
*
* @param array $data Данные текущей строки БД.
* @param bool|string $class Класс хелпера через метод getUrl которого идет получение ссылки.
*
* @return array Возвращает ссылку на детальную страницу и её название.
*
* @api
*/
protected function getRow($data, $class = false)
{
if (empty($class)) {
$class = static::getHelperClass(AdminEditHelper::className());
}
if ($this->isPopup()) {
return array();
}
else {
$query = array_merge($this->additionalUrlParams, array(
'lang' => LANGUAGE_ID,
$this->pk() => $data[$this->pk()]
));
return array($class::getUrl($query));
}
}
/**
* Для каждой ячейки(раздела) таблицы создаёт виджет соответствующего типа.
* Виджет подготавливает необходимый HTML для списка.
*
* @param \CAdminListRow $row
* @param $code Сивольный код поля.
* @param $data Данные текущей строки.
*
* @throws Exception
*
* @see HelperWidget::generateRow()
*/
protected function addRowSectionCell($row, $code, $data)
{
$sectionEditHelper = $this->getHelperClass(AdminSectionEditHelper::className());
if (!isset($this->sectionFields[$code]['WIDGET'])) {
$error = str_replace('#CODE#', $code, 'Can\'t create widget for the code "#CODE#"');
throw new Exception($error, Exception::CODE_NO_WIDGET);
}
/**
* @var \DigitalWand\AdminHelper\Widget\HelperWidget $widget
*/
$widget = $this->sectionFields[$code]['WIDGET'];
$widget->setHelper($this);
$widget->setCode($code);
$widget->setData($data);
$widget->setEntityName($sectionEditHelper::getModel());
$this->setContext(AdminListHelper::OP_ADD_ROW_CELL);
$widget->generateRow($row, $data);
}
/**
* Возвращает массив со списком действий при клике правой клавишей мыши на строке таблицы
* По-умолчанию:
*
*
Редактировать элемент
*
Удалить элемент
*
Если это всплывающее окно - запустить кастомную JS-функцию.
*
*
* @param $data Данные текущей строки.
* @param $section Признак списка для раздела.
*
* @return array
*
* @see CAdminListRow::AddActions
*
* @api
*/
protected function getRowActions($data, $section = false)
{
$actions = array();
if ($this->isPopup()) {
$jsData = \CUtil::PhpToJSObject($data);
$actions['select'] = array(
'ICON' => 'select',
'DEFAULT' => true,
'TEXT' => Loc::getMessage('DIGITALWAND_ADMIN_HELPER_LIST_SELECT'),
"ACTION" => 'javascript:' . $this->popupClickFunctionName . '(' . $jsData . ')'
);
}
else {
$viewQueryString = 'module=' . static::getModule() . '&view=' . static::getViewName() . '&entity=' . static::getEntityCode();
$query = array_merge($this->additionalUrlParams,
array($this->pk() => $data[$this->pk()]));
if ($this->hasWriteRights()) {
$sectionHelperClass = static::getHelperClass(AdminSectionEditHelper::className());
$editHelperClass = static::getHelperClass(AdminEditHelper::className());
$actions['edit'] = array(
'ICON' => 'edit',
'DEFAULT' => true,
'TEXT' => Loc::getMessage('DIGITALWAND_ADMIN_HELPER_LIST_EDIT'),
'ACTION' => $this->list->ActionRedirect($section ? $sectionHelperClass::getUrl($query) : $editHelperClass::getUrl($query))
);
}
if ($this->hasDeleteRights()) {
$actions['delete'] = array(
'ICON' => 'delete',
'TEXT' => Loc::getMessage("DIGITALWAND_ADMIN_HELPER_LIST_DELETE"),
'ACTION' => "if(confirm('" . Loc::getMessage('DIGITALWAND_ADMIN_HELPER_LIST_DELETE_CONFIRM') . "')) " . $this->list->ActionDoGroup($data[$this->pk()],
$section ? "delete-section" : "delete", $viewQueryString)
);
}
}
return $actions;
}
/**
* Для каждой ячейки таблицы создаёт виджет соответствующего типа. Виджет подготавливает необходимый HTML-код
* для списка.
*
* @param \CAdminListRow $row Объект строки списка записей.
* @param string $code Сивольный код поля.
* @param array $data Данные текущей строки.
* @param bool $virtualCode
*
* @throws Exception
*
* @see HelperWidget::generateRow()
*/
protected function addRowCell($row, $code, $data, $virtualCode = false)
{
$widget = $this->createWidgetForField($code, $data);
$this->setContext(AdminListHelper::OP_ADD_ROW_CELL);
// устанавливаем виртуальный код ячейки, используется при слиянии столбцов
if ($virtualCode) {
$widget->setCode($virtualCode);
}
$widget->generateRow($row, $data);
if ($virtualCode) {
$widget->setCode($code);
}
}
/**
* Производит выборку данных. Функцию стоит переопределить в случае, если необходима своя логика, и её нельзя
* вынести в класс модели.
*
* @param DataManager $className
* @param array $filter
* @param array $select
* @param array $sort
* @param array $raw
*
* @return Result
*
* @api
*/
protected function getData($className, $filter, $select, $sort, $raw)
{
$limits = $this->getLimits();
$parameters = array(
'filter' => $this->getElementsFilter($filter),
'select' => $select,
'order' => $sort,
'offset' => $limits[0],
'limit' => $limits[1],
);
/** @var Result $res */
$res = $className::getList($parameters);
return $res;
}
/**
* Подготавливает массив с настройками футера таблицы Bitrix
* @param \CAdminResult $res - результат выборки данных
* @see \CAdminList::AddFooter()
* @return array[]
*/
protected function getFooter($res)
{
return array(
$this->getButton('MAIN_ADMIN_LIST_SELECTED', array("value" => $res->SelectedRowsCount())),
$this->getButton('MAIN_ADMIN_LIST_CHECKED', array("value" => $res->SelectedRowsCount()), array(
"counter" => true,
"value" => "0",
)),
);
}
/**
* Выводит форму фильтрации списка
*/
public function createFilterForm()
{
//нужно пробрасывать параметр popup в форму, если она является таковой
if($this->isPopup())
{
$this->additionalUrlParams['popup'] = 'Y';
}
$this->setContext(AdminListHelper::OP_CREATE_FILTER_FORM);
print ' ';
}
/**
* Возвращает ID таблицы, который не должен конфликтовать с ID в других разделах админки, а также нормально
* парситься в JS
*
* @return string
*/
protected function getListTableID()
{
return str_replace('.', '', static::$tablePrefix . $this->table());
}
/**
* Выводит сформированный список.
* Сохраняет обработанный GET-запрос в сессию
*/
public function show()
{
if (!$this->hasReadRights()) {
$this->addErrors(Loc::getMessage('DIGITALWAND_ADMIN_HELPER_ACCESS_FORBIDDEN'));
$this->showMessages();
return false;
}
$this->showMessages();
$this->list->DisplayList();
if ($this->isPopup()) {
print $this->popupClickFunctionCode;
}
$this->saveGetQuery();
}
/**
* Сохраняет параметры запроса для поторного использования после возврата с других страниц (к примеру, после
* перехода с детальной обратно в список - чтобы вернуться в точности в тот раздел, с которого ранее ушли)
*/
private function saveGetQuery()
{
$_SESSION['LAST_GET_QUERY'][get_called_class()] = $_GET;
}
/**
* Восстанавливает последний GET-запрос, если в текущем задан параметр restore_query=Y
*/
private function restoreLastGetQuery()
{
if (!isset($_SESSION['LAST_GET_QUERY'][get_called_class()])
OR !isset($_REQUEST['restore_query'])
OR $_REQUEST['restore_query'] != 'Y'
) {
return;
}
$_GET = array_merge($_GET, $_SESSION['LAST_GET_QUERY'][get_called_class()]);
$_REQUEST = array_merge($_REQUEST, $_SESSION['LAST_GET_QUERY'][get_called_class()]);
}
/**
* @inheritdoc
*/
public static function getUrl(array $params = array())
{
return static::getViewURL(static::getViewName(), static::$listPageUrl, $params);
}
/**
* Кастомизация фильтра разделов
* @param $filter
* @return mixed
*/
protected function getSectionsFilter(array $filter)
{
return $filter;
}
/**
* Кастомизация фильтра элементов
* @param $filter
* @return mixed
*/
protected function getElementsFilter($filter)
{
return $filter;
}
/**
* Список идентификаторов для групповых операций
*
* @return array
*/
protected function getIds()
{
$className = static::getModel();
if (isset($_REQUEST['model'])) {
$className = $_REQUEST['model'];
}
$sectionEditHelperClass = $this->getHelperClass(AdminSectionEditHelper::className());
if ($sectionEditHelperClass && !isset($_REQUEST['model-section'])) {
$sectionClassName = $sectionEditHelperClass::getModel();
}
else {
$sectionClassName = $_REQUEST['model-section'];
}
$pkValue = $this->getPk();
if (isset($pkValue[$this->pk()]) && is_array($pkValue[$this->pk()])) {
foreach ($pkValue[$this->pk()] as $id) {
$class = strpos($id, 's') === 0 ? $sectionClassName : $className;
$ids[] = $this->getCommonPrimaryFilterById($class, null, $id);
}
} else {
$ids = array($this->getPk());
}
return $ids;
}
/**
* Получить оставшуюся часть составного первичного ключа
*
* @param $className
* @param null $sectionClassName
* @param $id
* @return array
*/
protected function getCommonPrimaryFilterById($className, $sectionClassName = null, $id)
{
if ($this->getHelperClass($sectionClassName) && strpos($id, 's') === 0) {
$primary = $sectionClassName::getEntity()->getPrimaryArray();
} else {
$primary = $className::getEntity()->getPrimaryArray();
}
if (count($primary) === 1) {
return array($this->pk() => $id);
}
$key = $this->getPk();
$key[$this->pk()] = $id;
return $key;
}
}
================================================
FILE: lib/helper/AdminSectionEditHelper.php
================================================
* @author Artem Yarygin
*/
class AdminSectionEditHelper extends AdminEditHelper
{
}
================================================
FILE: lib/helper/AdminSectionListHelper.php
================================================
* @author Artem Yarygin
*/
class AdminSectionListHelper extends AdminListHelper
{
}
================================================
FILE: lib/helper/Exception.php
================================================
*
FIELD_TYPE - Тип данных для хранения булевых значений (строка, целые числа, булево)
*
*/
class CheckboxWidget extends HelperWidget
{
/**
* Строковый тип чекбокса (Y/N)
* FIXME: если верить битриксу, мжет быть ещё и экзотичный случай со строками "true" и "false"!
*/
const TYPE_STRING = 'string';
/**
* Целочисленный тип чекбокса (1/0)
*/
const TYPE_INT = 'integer';
/**
* Булевый тип чекбокса
*/
const TYPE_BOOLEAN = 'boolean';
/**
* Значение положительного варианта для строкового чекбокса
*/
const TYPE_STRING_YES = 'Y';
/**
* Значение отрицательного варианта для строкового чекбокса
*/
const TYPE_STRING_NO = 'N';
/**
* Значение положительного варианта для целочисленного чекбокса
*/
const TYPE_INT_YES = 1;
/**
* Значение отрицательного варианта для целочисленного чекбокса
*/
const TYPE_INT_NO = 0;
protected static $defaults = array(
'EDIT_IN_LIST' => true
);
/**
* @inheritdoc
*/
public function generateRow(&$row, $data)
{
$modeType = $this->getCheckboxType();
$globalYes = '';
$globalNo = '';
switch ($modeType) {
case self::TYPE_STRING: {
$globalYes = self::TYPE_STRING_YES;
$globalNo = self::TYPE_STRING_NO;
break;
}
case self::TYPE_INT:
case self::TYPE_BOOLEAN: {
$globalYes = self::TYPE_INT_YES;
$globalNo = self::TYPE_INT_NO;
break;
}
}
if ($this->getSettings('EDIT_IN_LIST') AND !$this->getSettings('READONLY')) {
$checked = intval($this->getValue() == $globalYes) ? 'checked' : '';
$js = 'var input = document.getElementsByName(\'' . $this->getEditableListInputName() . '\')[0];
input.value = this.checked ? \'' . $globalYes . '\' : \'' . $globalNo . '\';';
$editHtml = '
';
$row->AddEditField($this->getCode(), $editHtml);
}
if (intval($this->getValue() == $globalYes)) {
$value = Loc::getMessage('DIGITALWAND_AH_CHECKBOX_YES');
} else {
$value = Loc::getMessage('DIGITALWAND_AH_CHECKBOX_NO');
}
$row->AddViewField($this->getCode(), $value);
}
/**
* @inheritdoc
*/
public function showFilterHtml()
{
$filterHtml = '
';
$filterHtml .= '
' . $this->getSettings('TITLE') . '
';
$filterHtml .= '
';
$filterHtml .= '
';
print $filterHtml;
}
/**
* @inheritdoc
*/
public function getValueReadonly()
{
$code = $this->getCode();
$value = isset($this->data[$code]) ? $this->data[$code] : null;
$modeType = $this->getCheckboxType();
switch ($modeType) {
case static::TYPE_STRING: {
$value = $value == 'Y' ? Loc::getMessage('DIGITALWAND_AH_CHECKBOX_YES') : Loc::getMessage('DIGITALWAND_AH_CHECKBOX_NO');
break;
}
case static::TYPE_INT:
case static::TYPE_BOOLEAN: {
$value = $value ? Loc::getMessage('DIGITALWAND_AH_CHECKBOX_YES') : Loc::getMessage('DIGITALWAND_AH_CHECKBOX_NO');
break;
}
}
return static::prepareToOutput($value);
}
/**
* @inheritdoc
*/
public function processEditAction()
{
parent::processEditAction();
if ($this->getCheckboxType() === static::TYPE_BOOLEAN) {
$this->data[$this->getCode()] = (bool)$this->data[$this->getCode()];
}
}
/**
* @inheritdoc
*/
protected function getEditHtml()
{
$html = '';
$modeType = $this->getCheckboxType();
switch ($modeType) {
case static::TYPE_STRING: {
$checked = $this->getValue() == self::TYPE_STRING_YES ? 'checked' : '';
$html = '';
$html .= '';
break;
}
case static::TYPE_INT:
case static::TYPE_BOOLEAN: {
$checked = $this->getValue() == self::TYPE_INT_YES ? 'checked' : '';
$html = '';
$html .= '';
break;
}
}
return $html;
}
/**
* Получить тип чекбокса по типу поля.
* По-умолчанию возвращает TYPE_STRING
* @return mixed
*/
public function getCheckboxType()
{
$settingsFieldType = $this->getSettings('FIELD_TYPE');
$checkTypes = array(static::TYPE_STRING, static::TYPE_BOOLEAN, static::TYPE_INT);
$columnName = $this->getCode();
if ($settingsFieldType AND in_array($settingsFieldType, $checkTypes)) {
return $settingsFieldType;
} else {
$entity = $this->getEntityName();
$entityMap = $entity::getMap();
if (!isset($entityMap[$columnName])) {
foreach ($entityMap as $field/** @var \Bitrix\Main\Entity\ScalarField $field */) {
if($field instanceof \Bitrix\Main\Entity\ReferenceField)
continue;
if (is_object($field) AND $field->getColumnName() === $columnName) {
return $field->getDataType(); //FIXME: deprecated? На что нужно заменить?
}
}
} elseif (isset($entityMap[$columnName]['values']) AND
is_array($entityMap[$columnName]['values']) AND
count($entityMap[$columnName]['values']) == 2
) {
$value = reset($entityMap[$columnName]['values']);
if (is_string($value)) {
return static::TYPE_STRING;
} elseif (is_bool($value) OR is_integer($value)) {
return static::TYPE_BOOLEAN;
}
} elseif (isset($entityMap[$columnName]['data_type'])) {
return $entityMap[$columnName]['data_type'];
}
/**
* Теоретически, рзработчик мог ввести полную хрень, указывая варианты значений для сущности
* В этом случае ни одна проверка выше не сработает.
* FIXME: а нужен ли эксепшн?
*/
// throw new \Bitrix\Main\ArgumentTypeException("Unknown checkbox type");
}
return static::TYPE_STRING;
}
}
================================================
FILE: lib/widget/ComboBoxWidget.php
================================================
*
STYLE - inline-стили
*
VARIANTS - массив с вариантами значений или функция для их получения в формате ключ=>заголовок
* Например:
* [
* 1=>'Первый пункт',
* 2=>'Второй пункт'
* ]
*
*
DEFAULT_VARIANT - ID варианта по-умолчанию
*
*/
class ComboBoxWidget extends HelperWidget
{
static protected $defaults = array(
'EDIT_IN_LIST' => true
);
/**
* @inheritdoc
*
* @see AdminEditHelper::showField();
*
* @param bool $forFilter
*
* @return mixed
*/
protected function getEditHtml()
{
return $this->getComboBox();
}
/**
* @inheritdoc
*/
protected function getMultipleEditHtml()
{
return $this->getComboBox(true);
}
/**
* Возвращает ХТМЛ-код с комбобоксом.
*
* @param bool $multiple Множественный режим.
* @param bool $forFilter Комбобокс будет выводиться в блоке с фильтром.
*
* @return string
*/
protected function getComboBox($multiple = false, $forFilter = false)
{
if ($multiple) {
$value = $this->getMultipleValue();
} else {
$value = $this->getValue();
}
$style = $this->getSettings('STYLE');
$variants = $this->getVariants();
if (!$multiple)
{
array_unshift($variants, array(
'ID' => null,
'TITLE' => null
));
}
if (empty($variants)) {
$comboBox = Loc::getMessage('DIGITALWAND_AH_MISSING_VARIANTS');
} else {
$name = $forFilter ? $this->getFilterInputName() : $this->getEditInputName();
$comboBox = '';
}
return $comboBox;
}
/**
* @inheritdoc
*/
protected function getValueReadonly()
{
$variants = $this->getVariants();
$value = $variants[$this->getValue()]['TITLE'];
return static::prepareToOutput($value);
}
/**
* @inheritdoc
*/
protected function getMultipleValueReadonly()
{
$variants = $this->getVariants();
$values = $this->getMultipleValue();
$result = '';
if (empty($variants)) {
$result = Loc::getMessage('DIGITALWAND_AH_MISSING_VARIANTS');
} else {
foreach ($variants as $id => $data) {
$name = strlen($data["TITLE"]) > 0 ? $data["TITLE"] : "";
if (in_array($id, $values)) {
$result .= static::prepareToOutput($name) . ' ';
}
}
}
return $result;
}
/**
* Возвращает массив в следующем формате:
*
* array(
* '123' => array('ID' => 123, 'TITLE' => 'ololo'),
* '456' => array('ID' => 456, 'TITLE' => 'blablabla'),
* '789' => array('ID' => 789, 'TITLE' => 'pish-pish'),
* )
*
*
* Результат будет выводиться в комбобоксе.
* @return array
*/
protected function getVariants()
{
$variants = $this->getSettings('VARIANTS');
if (is_callable($variants)) {
$var = $variants();
if (is_array($var)) {
return $this->formatVariants($var);
}
}elseif (is_array($variants) AND !empty($variants)) {
return $this->formatVariants($variants);
}
return array();
}
/**
* Приводит варианты к нужному формату, если они заданы в виде одномерного массива.
*
* @param $variants
*
* @return array
*/
protected function formatVariants($variants)
{
$formatted = array();
foreach ($variants as $id => $data) {
if (!is_array($data)) {
$formatted[$id] = array(
'ID' => $id,
'TITLE' => $data
);
}
}
return $formatted;
}
/**
* @inheritdoc
*/
public function generateRow(&$row, $data)
{
if ($this->settings['EDIT_IN_LIST'] AND !$this->settings['READONLY']) {
$row->AddInputField($this->getCode(), array('style' => 'width:90%'));
} else {
$row->AddViewField($this->getCode(), $this->getValueReadonly());
}
}
/**
* @inheritdoc
*/
public function showFilterHtml()
{
print '
';
print '
' . $this->getSettings('TITLE') . '
';
print '
' . $this->getComboBox(false, true) . '
';
print '
';
}
/**
* @inheritdoc
*/
public function processEditAction()
{
if ($this->getSettings('MULTIPLE')) {
$sphere = $this->data[$this->getCode()];
unset($this->data[$this->getCode()]);
foreach ($sphere as $sphereKey) {
$this->data[$this->getCode()][] = array('VALUE' => $sphereKey);
}
}
parent::processEditAction();
}
}
================================================
FILE: lib/widget/DateTimeWidget.php
================================================
'BETWEEN',
);
/**
* Генерирует HTML для редактирования поля
* @see AdminEditHelper::showField();
* @return mixed
*/
protected function getEditHtml()
{
return \CAdminCalendar::CalendarDate($this->getEditInputName(), ConvertTimeStamp(strtotime($this->getValue()), "FULL"), 10, true);
}
/**
* Генерирует HTML для поля в списке
* @see AdminListHelper::addRowCell();
* @param CAdminListRow $row
* @param array $data - данные текущей строки
* @return mixed
*/
public function generateRow(&$row, $data)
{
if (isset($this->settings['EDIT_IN_LIST']) AND $this->settings['EDIT_IN_LIST'])
{
$row->AddCalendarField($this->getCode());
}
else
{
$arDate = ParseDateTime($this->getValue());
if ($arDate['YYYY'] < 10)
{
$stDate = '-';
}
else
{
$stDate = ConvertDateTime($this->getValue(), "DD.MM.YYYY HH:MI:SS", "ru");
}
$row->AddViewField($this->getCode(), $stDate);
}
}
/**
* Генерирует HTML для поля фильтрации
* @see AdminListHelper::createFilterForm();
* @return mixed
*/
public function showFilterHtml()
{
list($inputNameFrom, $inputNameTo) = $this->getFilterInputName();
print '
MODEL - Название модели, из которой будет производиться выборка данных. По-умолчанию - модель текущего
* хэлпера
*
* Class HLIBlockFieldWidget
* @package DigitalWand\AdminHelper\Widget
*/
class HLIBlockFieldWidget extends HelperWidget
{
static protected $userFieldsCache = array();
static protected $defaults = array(
'USE_BX_API' => true
);
/**
* Генерирует HTML для редактирования поля
*
* @see \CAdminForm::ShowUserFieldsWithReadyData
* @return mixed
*/
protected function getEditHtml()
{
$info = $this->getUserFieldInfo();
if ($info) {
/** @var \CAllUserTypeManager $USER_FIELD_MANAGER */
global $USER_FIELD_MANAGER;
$GLOBALS[$this->getCode()] = isset($GLOBALS[$this->getCode()]) ? $GLOBALS[$this->getCode()] : $this->data[$this->getCode()];
$bVarsFromForm = false;
$info["VALUE_ID"] = intval($this->data['ID']);
$info['EDIT_FORM_LABEL'] = $this->getSettings('TITLE');
if (isset($_REQUEST['def_' . $this->getCode()])) {
$info['SETTINGS']['DEFAULT_VALUE'] = $_REQUEST['def_' . $this->getCode()];
}
print $USER_FIELD_MANAGER->GetEditFormHTML($bVarsFromForm, $GLOBALS[$this->getCode()], $info);
}
}
/**
* Конвертирует данные при сохранении так, как это делали бы пользовательские свойства битрикса.
* Выполняет валидацию с помощью CheckFields() пользовательских полей.
*
* @see Bitrix\Highloadblock\DataManager
* @see /bitrix/modules/highloadblock/admin/highloadblock_row_edit.php
*
* @throws \Bitrix\Main\ArgumentException
* @throws \Bitrix\Main\SystemException
*/
public function processEditAction()
{
/** @var \CAllUserTypeManager $USER_FIELD_MANAGER */
global $USER_FIELD_MANAGER;
$iblockId = 'HLBLOCK_' . $this->getHLId();
//Чтобы не терялись старые данные
if (!isset($this->data[$this->getCode()]) AND isset($_REQUEST[$this->getCode() . '_old_id'])) {
$this->data[$this->getCode()] = $_REQUEST[$this->getCode() . '_old_id'];
}
//Функция работает для всех полей, так что запускаем её только один раз, результат кешируем.
static $data = array();
if (empty($data)) {
$data = $this->data;
$USER_FIELD_MANAGER->EditFormAddFields($iblockId, $data);
}
$value = $data[$this->getCode()];
$entity_data_class = AdminBaseHelper::getHLEntity($this->getSettings('MODEL'));
$oldData = $this->getOldFieldData($entity_data_class);
$fieldsInfo = $USER_FIELD_MANAGER->getUserFieldsWithReadyData($iblockId, $oldData, LANGUAGE_ID, false, 'ID');
$fieldInfo = $fieldsInfo[$this->getCode()];
$className = $fieldInfo['USER_TYPE']['CLASS_NAME'];
if (is_callable(array($className, 'CheckFields'))) {
$errors = $className::CheckFields($fieldInfo, $value);
if (!empty($errors)) {
$this->addError($errors);
return;
}
}
// use save modifiers
$field = $entity_data_class::getEntity()->getField($this->getCode());
$value = $field->modifyValueBeforeSave($value, $data);
//Типоспецифичные хаки
if ($unserialized = unserialize($value)) {
//Список значений прилетает сериализованным
$this->data[$this->getCode()] = $unserialized;
} else if ($className == 'CUserTypeFile' AND !is_array($value)) {
//Если не сделать intval, то при сохранении с ранее добавленным файлом будет выскакивать ошибка
$this->data[$this->getCode()] = intval($value);
} else {
//Все остальные поля - сохраняем как есть.
$this->data[$this->getCode()] = $value;
}
}
/**
* Битриксу надо получить поля, кторые сохранены в базе для этого пользовательского свойства.
* Иначе множественные свойства он затрёт.
* Проблема в том, что пользовательские свойства могут браться из связанной сущности.
* @param HL\DataManager $entity_data_class
*
* @return mixed
*/
protected function getOldFieldData($entity_data_class)
{
if (is_null($this->data) OR !isset($this->data[$this->helper->pk()])) return false;
return $entity_data_class::getByPrimary($this->data[$this->helper->pk()])->fetch();
}
/**
* Если запрашивается модель, и если модель явно не указана, то берется модель текущего хэлпера, сохраняется для
* последующего использования и возарвщвется пользователю.
*
* @param string $name
* @return array|\Bitrix\Main\Entity\DataManager|mixed|string
*/
public function getSettings($name = '')
{
$value = parent::getSettings($name);
if (!$value) {
if ($name == 'MODEL') {
$value = $this->helper->getModel();
$this->setSetting($name, $value);
} else if ($name == 'TITLE') {
$context = $this->helper->getContext();
$info = $this->getUserFieldInfo();
if (($context == AdminListHelper::OP_ADMIN_VARIABLES_FILTER OR $context == AdminListHelper::OP_CREATE_FILTER_FORM)
AND (isset($info['LIST_FILTER_LABEL']) AND !empty($info['LIST_FILTER_LABEL']))
) {
$value = $info['LIST_FILTER_LABEL'];
} else if ($context == AdminListHelper::OP_ADMIN_VARIABLES_HEADER
AND isset($info['LIST_COLUMN_LABEL'])
AND !empty($info['LIST_COLUMN_LABEL'])
) {
$value = $info['LIST_COLUMN_LABEL'];
} else if ($context == AdminEditHelper::OP_SHOW_TAB_ELEMENTS
AND isset($info['EDIT_FORM_LABEL'])
AND !empty($info['EDIT_FORM_LABEL'])
) {
$value = $info['EDIT_FORM_LABEL'];
} else {
$value = $info['FIELD_NAME'];
}
}
}
return $value;
}
/**
* Генерирует HTML для поля в списке
* Копипаст из API Битрикса, бессмысленного и беспощадного...
*
* @see AdminListHelper::addRowCell();
*
* @param \CAdminListRow $row
* @param array $data - данные текущей строки
*
* @return mixed
*/
public function generateRow(&$row, $data)
{
$info = $this->getUserFieldInfo();
if ($info) {
/** @var \CAllUserTypeManager $USER_FIELD_MANAGER */
global $USER_FIELD_MANAGER;
$FIELD_NAME = $this->getCode();
$GLOBALS[$FIELD_NAME] = isset($GLOBALS[$FIELD_NAME]) ? $GLOBALS[$FIELD_NAME] : $this->data[$this->getCode()];
$info["VALUE_ID"] = intval($this->data['ID']);
if (isset($_REQUEST['def_' . $FIELD_NAME])) {
$info['SETTINGS']['DEFAULT_VALUE'] = $_REQUEST['def_' . $FIELD_NAME];
}
$USER_FIELD_MANAGER->AddUserField($info, $data[$this->getCode()], $row);
}
}
/**
* Генерирует HTML для поля фильтрации
*
* @see AdminListHelper::createFilterForm();
* @return mixed
*/
public function showFilterHtml()
{
$info = $this->getUserFieldInfo();
if ($info) {
/** @var \CAllUserTypeManager $USER_FIELD_MANAGER */
global $USER_FIELD_MANAGER;
$FIELD_NAME = $this->getCode();
$GLOBALS[$FIELD_NAME] = isset($GLOBALS[$FIELD_NAME]) ? $GLOBALS[$FIELD_NAME] : $this->data[$this->getCode()];
$info["VALUE_ID"] = intval($this->data['ID']);
$info['LIST_FILTER_LABEL'] = $this->getSettings('TITLE');
print $USER_FIELD_MANAGER->GetFilterHTML($info, $this->getFilterInputName(), $this->getCurrentFilterValue());
}
}
public function getUserFieldInfo()
{
$id = $this->getHLId();
$fields = static::getUserFields($id, $this->data);
if (isset($fields[$this->getCode()])) {
return $fields[$this->getCode()];
}
return false;
}
/**
* Получаем ID HL-инфоблока по имени его класса
* @return mixed
*/
protected function getHLId()
{
static $id = false;
if ($id === false) {
$model = $this->getSettings('MODEL');
$info = AdminBaseHelper::getHLEntityInfo($model);
if ($info AND isset($info['ID'])) {
$id = $info['ID'];
}
}
return $id;
}
static public function getUserFields($iblockId, $data)
{
/** @var \CAllUserTypeManager $USER_FIELD_MANAGER */
global $USER_FIELD_MANAGER;
$iblockId = 'HLBLOCK_' . $iblockId;
if (!isset(static::$userFieldsCache[$iblockId][$data['ID']])) {
$fields = $USER_FIELD_MANAGER->getUserFieldsWithReadyData($iblockId, $data, LANGUAGE_ID, false, 'ID');
self::$userFieldsCache[$iblockId][$data['ID']] = $fields;
}
return self::$userFieldsCache[$iblockId][$data['ID']];
}
/**
* Заменяем оригинальную функцию, т.к. текст ошибки приходит от битрикса, причем название поля там почему-то не
* проставлено
*
*@param string $messageId
*/
protected function addError($messageId)
{
if (is_array($messageId)) {
foreach ($messageId as $key => $error) {
if (isset($error['text'])) {
//FIXME: почему-то битрикс не подхватывает корректное название поля, поэтому запихиваем его сами.
if (isset($error['id']) AND strpos($error['text'], '""')) {
$messageId[$key] = str_replace('""', '"' . $this->getSettings('TITLE') . '"', $error['text']);
} else {
$messageId[$key] = $error['text'];
}
}
}
}
$messageId = implode("\n", $messageId);
$this->validationErrors[$this->getCode()] = $messageId;
}
}
================================================
FILE: lib/widget/HelperWidget.php
================================================
*
Отображение поля на странице редактирования
*
Отображение ячейки поля в таблице списка - при просмотре и редактировании
*
Отображение фильтра по данному полю
*
Валидацию значения поля
*
*
* Также виджетами осуществляется предварительная обработка данных:
*
*
Перед сохранением значения поля в БД
*
После получения значения поля из БД
*
Модификация запроса перед фильтрацией
*
Модификация пуеДшые перед выборкой данных
*
*
* Для получения минимальной функциональности достаточно переопределить основные методы, отвечающие за отображение
* виджета в списке и на детальной.
*
* Каждый виджет имеет ряд специфических настроек, некоторые из которых обязательны для заполнения. Подробную
* документацию по настройкам стоит искать в документации к конкретному виджету. Настройки могут быть переданы в
* виджет как при описании всего интерфейса в файле Interface.php, так и непосредственно во время исполнения,
* внутри Helper-классов.
*
* При указании настроек типа "да"/"нет", нельзя использовать строковые обозначения "Y"/"N":
* для этого есть булевы true и false.
*
* Настройки базового класса:
*
*
HIDE_WHEN_CREATE - скрывает поле в форме редактирования, если создаётся новый элемент, а не открыт
* существующий на редактирование.
*
TITLE - название поля. Если не задано то возьмется значение title из DataManager::getMap()
* через getField($code)->getTitle(). Будет использовано в фильтре, заголовке таблицы и в качестве подписи поля
* на
* странице редактирования.
*
REQUIRED - является ли поле обязательным.
*
READONLY - поле нельзя редактировать, предназначено только для чтения
*
FILTER - позволяет указать способ фильтрации по полю. В базовом классе возможен только вариант "BETWEEN"
* или "><". И в том и в другом случае это будет означать фильтрацию по диапазону значений. Количество возможных
* вариантов этого параметра может быть расширено в наследуемых классах
*
UNIQUE - поле должно содержать только уникальные значения
*
VIRTUAL - особая настройка, отражается как на поведении виджета, так и на поведении хэлперов. Поле,
* объявленное виртуальным, отображается в графическом интерфейче, однако не участвоует в запросах к БД. Опция
* может быть полезной при реализации нестандартной логики, когда, к примеру, под именем одного поля могут
* выводиться данные из нескольких полей сразу.
*
EDIT_IN_LIST - параметр не обрабатывается непосредственно виджетом, однако используется хэлпером.
* Указывает, можно ли редактировать данное поле в спискке
*
MULTIPLE - bool является ли поле множественным
*
MULTIPLE_FIELDS - array поля используемые в хранилище множественных значений и их алиасы
*
LIST - отображать ли поле в списке доступных в настройках столбцов таблицы (по-умолчанию true)
*
HEADER - является ли столбец отображаемым по-умолчанию, если вывод столбцов таблицы не настроен (по-умолчанию true)
*
*
* Как сделать виджет множественным?
*
*
Реализуйте метод genMultipleEditHTML(). Метод должен выводить множественную форму ввода. Для реализации формы
* ввода есть JS хелпер HelperWidget::jsHelper()
*
Опишите поля, которые будут переданы связи в EntityManager. Поля описываются в настройке "MULTIPLE_FIELDS"
* виджета. По умолчанию множественный виджет использует поля ID, ENTITY_ID, VALUE
*
Полученные от виджета данные будут переданы в EntityManager и сохранены как связанные данные
*
* Пример реализации можно увидеть в виджете StringWidget
*
* Как использовать множественный виджет?
*
*
* Создайте таблицу и модель, которая будет хранить данные поля
* - Таблица обязательно должна иметь поля, которые требует виджет.
* Обязательные поля виджета по умолчанию описаны в: HelperWidget::$settings['MULTIPLE_FIELDS']
* Если у виджета нестандартный набор полей, то они хранятся в: SomeWidget::$settings['MULTIPLE_FIELDS']
* - Если поля, которые требует виджет есть в вашей таблице, но они имеют другие названия,
* можно настроить виджет для работы с вашими полями.
* Для этого переопределите настройку MULTIPLE_FIELDS при объявлении поля в интерфейсе следующим способом:
* ```
* 'RELATED_LINKS' => array(
* 'WIDGET' => new StringWidget(),
* 'TITLE' => 'Ссылки',
* 'MULTIPLE' => true,
* // Обратите внимание, именно тут переопределяются поля виджета
* 'MULTIPLE_FIELDS' => array(
* 'ID', // Должны быть прописаны все поля, даже те, которые не нужно переопределять
* 'ENTITY_ID' => 'NEWS_ID', // ENTITY_ID - поле, которое требует виджет, NEWS_ID - пример поля, которое
* будет использоваться вместо ENTITY_ID
* 'VALUE' => 'LINK', // VALUE - поле, которое требует виджет, LINK - пример поля, которое будет
* использоваться вместо VALUE
* )
* ),
* ```
*
*
*
* Далее в основной модели (та, которая указана в AdminBaseHelper::$model) нужно прописать связь с моделью,
* в которой вы хотите хранить данные поля
* Пример объявления связи:
* ```
* new Entity\ReferenceField(
* 'RELATED_LINKS',
* 'namespace\NewsLinksTable',
* array('=this.ID' => 'ref.NEWS_ID'),
* // Условия FIELD и ENTITY не обязательны, подробности смотрите в комментариях к классу @see EntityManager
* 'ref.FIELD' => new DB\SqlExpression('?s', 'NEWS_LINKS'),
* 'ref.ENTITY' => new DB\SqlExpression('?s', 'news'),
* ),
* ```
*
*
*
* Чтобы виджет работал во множественном режиме, нужно при описании интерфейса поля указать параметр MULTIPLE => true
* ```
* 'RELATED_LINKS' => array(
* 'WIDGET' => new StringWidget(),
* 'TITLE' => 'Ссылки',
* // Включаем режим множественного ввода
* 'MULTIPLE' => true,
* )
* ```
*
*
*
* Готово :)
*
*
*
* О том как сохраняются данные множественных виджетов можно узнать из комментариев
* класса \DigitalWand\AdminHelper\EntityManager.
*
* @see EntityManager
* @see HelperWidget::getEditHtml()
* @see HelperWidget::generateRow()
* @see showFilterHtml::showFilterHTML()
* @see HelperWidget::setSetting()
*
* @author Nik Samokhvalov
* @author Dmitriy Baibuhtin
*/
abstract class HelperWidget
{
const LIST_HELPER = 1;
const EDIT_HELPER = 2;
/**
* @var string Код поля.
*/
protected $code;
/**
* @var array $settings Настройки виджета для данной модели.
*/
protected $settings = array(
// Поля множественного виджета по умолчанию (array('ОРИГИНАЛЬНОЕ НАЗВАНИЕ', 'ОРИГИНАЛЬНОЕ НАЗВАНИЕ' => 'АЛИАС'))
'MULTIPLE_FIELDS' => array('ID', 'VALUE', 'ENTITY_ID')
);
/**
* @var array Настройки "по-умолчанию" для модели.
*/
static protected $defaults;
/**
* @var DataManager Название класса модели.
*/
protected $entityName;
/**
* @var array Данные модели.
*/
protected $data;
/** @var AdminBaseHelper|AdminListHelper|AdminEditHelper $helper Экземпляр хэлпера, вызывающий данный виджет.
*/
protected $helper;
/**
* @var bool Статус отображения JS хелпера. Используется для исключения дублирования JS-кода.
*/
protected $jsHelper = false;
/**
* @var array $validationErrors Ошибки валидации поля.
*/
protected $validationErrors = array();
/**
* @var string Строка, добавляемая к полю name полей фильтра.
*/
protected $filterFieldPrefix = 'find_';
/**
* Эксемпляр виджета создаётся всего один раз, при описании настроек интерфейса. При создании есть возможность
* сразу указать для него необходимые настройки.
*
* @param array $settings
*/
public function __construct(array $settings = array())
{
Loc::loadMessages(__FILE__);
$this->settings = $settings;
}
/**
* Генерирует HTML для редактирования поля.
*
* @return string
*
* @api
*/
abstract protected function getEditHtml();
/**
* Генерирует HTML для редактирования поля в мульти-режиме.
*
* @return string
*
* @api
*/
protected function getMultipleEditHtml()
{
return Loc::getMessage('DIGITALWAND_AH_MULTI_NOT_SUPPORT');
}
/**
* Оборачивает поле в HTML код, который в большинстве случаев менять не придется. Далее вызывается
* кастомизируемая часть.
*
* @param bool $isPKField Является ли поле первичным ключом модели.
*
* @see HelperWidget::getEditHtml();
*/
public function showBasicEditField($isPKField)
{
if ($this->getSettings('HIDE_WHEN_CREATE') AND !isset($this->data[$this->helper->pk()])) {
return;
}
// JS хелперы
$this->jsHelper();
if ($this->getSettings('USE_BX_API')) {
$this->getEditHtml();
} else {
print '