Генератор админок «Битрикса»

Несколько лет назад я начал делать сайты на «Битриксе». У меня были проекты СМИ, которым требовались нестандартные интерфейсы, но не было инструментов для их разработки. Тут и там нужно было сделать страницу в админке: то для простого редактирования данных, то с навороченным интерфейсом и функционалом документооборота. Матёрый читатель наверняка уже досадно хмыкнул, читая эти строки, ведь в «Битриксе» это делается с помощью большой портянки в виде ПХП-скрипта, содержащего логику всех КРУД-операций страницы, вперемежку с ХТМЛ-кодом, ЦСС и ЯС. Пример — любая админская страница любого модуля «Битрикса».

ООП?

Генерация административного интерфейса — головная боль битриксоида. Кто-то мирится с этим досадным фактом и идёт по пути написания скриптов-портянок. Другие выносят «генератор» админских страниц в компоненты. Третьи разрабатывают различные обёртки, позволяющие хоть немного привести доработку панели управления «Битрикса» к ООП. Но тем не менее, каким бы решением вы не пользовались, речи об ООП быть не может, и вам так же придётся повсюду вставлять типовые куски кода: подключение ядра, связанных классов и т. п.

Автоматизация?

В «Битриксе» в принципе отсутствует роутинг страниц, основанный на единой точке входа. Под каждую новую страницу на сайте нужно создавать новый ПХП-файл. Тоже самое касается и админки. Сделали новую страницу для админки? Даже не думайте, что этим всё закончится! «Битрикс» не умеет забирать админские страницы из модуля. Ваш модуль при установке или обновлении должен создать эту страницу в каталоге bitrix/admin.

Решение есть!

Мой товарищ — Алексей Волков из «Цифровой палки» — летом прошлого года опубликовал бета-версию генератора админских страниц с немудрёным названием «Админ-хелпер».

Модуль реализует подход шаблона проектирования МВЦ в создании страниц для панели управления «Битрикса». Если у вас есть модель, написанная на ОРМ «Битрикса», вы за считанные минуты сможете сделать раздел для управления данными этой модели, ничем не уступающему по своим возможностям и внешнему виду интерфейсу управления элементами инфоблоков.

Стоит сразу же оговориться: единственный недостаток генератора на данный момент — отсутствие бизнес-процессов и документооборота. А теперь перечислю его преимущества:

  • объектно-ориентированный подход,
  • пара минут уходит на создание админки под КРУД-операции с моделью,
  • данные могут храниться где угодно: в отдельной таблице или другой БД,
  • неограниченные возможности по кастомизации интерфейса,
  • нет необходимости копировать ПХП-скрипты в bitrix/admin.

С тех пор я применил на нескольких проектах этот модуль и весьма неплохо доработал его вместе со своей командой в «Нотамедии». Мы сделали версию 2.0, в которую было добавлено много различных фич, приведу некоторые из них:

  • менеджер сущностей, сохраняющий данные в связанные модели,
  • совместный вывод и управление записями и категориями этих записей,
  • новые виджеты (файлы, элементы инфоблоков, записи из ОРМ, УРЛ, пользователи, визуальный редактор),
  • возможность создания интерфейсов, не завязанных на ОРМ-моделях,
  • объектно-ориентированное описание интерфейса админки.

Ближе к телу

Сейчас я покажу вам, на сколько просто и быстро вы теперь можете создавать прекрасные интерфейсы в панели управления. Сделаем раздел по управлению новостями.

Подключите «Админ-хелпер» к своему проекту через «Композер»:

Подключение генератора.

composer require digitalwand/digitalwand.admin_helper

Примера ради, представим, что у вас есть модуль: local/modules/demo.adminhelper. Как вы помните, все классы должны начинаться с неймспейса Demo\AdminHelper, что бы работала автозагрузка классов.

Структура модуля.

В каталоге lib вашего модуля создайте каталог под сущность новостей: demo.adminhelper/lib/news. В нём мы будем размещать всё, что так или иначе связано с сущностью новостей. А всё относящееся к интерфейсу панели управления «Битрикса» — в каталог demo.adminhelper/lib/news/admininterface.

Создадим модель. Подразумевается, что с ОРМ «Битрикса» вы знакомы. Модель предельно простая:

Модель новостей.

<?php

namespace Demo\AdminHelper\News;

use Bitrix\Main\Entity\DataManager;
use Bitrix\Main\Localization\Loc;
use Bitrix\Main\Type\DateTime;

Loc::loadMessages(__FILE__);

/**
 * Модель новостей.
 */
class NewsTable extends DataManager
{
    /**
     * {@inheritdoc}
     */
    public static function getTableName()
    {
        return 'd_ah_news';
    }

    /**
     * {@inheritdoc}
     */
    public static function getMap()
    {
        return array(
            'ID' => array(
                'data_type' => 'integer',
                'primary' => true,
                'autocomplete' => true,
            ),
            'DATE_CREATE' => array(
                'data_type' => 'datetime',
                'title' => Loc::getMessage('DEMO_AH_NEWS_DATE_CREATE'),
                'default_value' => new DateTime()
            ),
            'CREATED_BY' => array(
                'data_type' => 'integer',
                'title' => Loc::getMessage('DEMO_AH_NEWS_CREATED_BY'),
                'default_value' => static::getUserId()
            ),
            'MODIFIED_BY' => array(
                'data_type' => 'integer',
                'title' => Loc::getMessage('DEMO_AH_NEWS_MODIFIED_BY'),
                'default_value' => static::getUserId()
            ),
            'TITLE' => array(
                'data_type' => 'string',
                'title' => Loc::getMessage('DEMO_AH_NEWS_TITLE')
            ),
            'TEXT' => array(
                'data_type' => 'text',
                'title' => Loc::getMessage('DEMO_AH_NEWS_TEXT')
            ),
            // Для всех полей, используемых визивигом, нужно создавать в таблице атрибут с суффиксом _TEXT_TYPE.
            // В нём будет храниться информация о типе сохранённого контента (ХТМЛ или обычный текст).
            'TEXT_TEXT_TYPE' => array(
                'data_type' => 'string'
            ),
            'SOURCE' => array(
                'data_type' => 'string',
                'title' => Loc::getMessage('DEMO_AH_NEWS_SOURCE')
            ),
            // Хранением файлов занимается Битрикс (хотя это вовсе необязательно, вы можете описать свою логику).
            // В атрибуте таблицы будет хранится идентификатор файла.
            'IMAGE' => array(
                'data_type' => 'integer',
                'title' => Loc::getMessage('DEMO_AH_NEWS_IMAGE')
            ),
        );
    }

    /**
     * {@inheritdoc}
     */
    public static function update($primary, array $data)
    {
        $data['MODIFIED_BY'] = static::getUserId();

        return parent::update($primary, $data);
    }

    /**
     * Возвращает идентификатор пользователя.
     *
     * @return int|null
     */
    public static function getUserId()
    {
        global $USER;

        return $USER->GetID();
    }
}

Теперь займёмся интерфейсом админки новостей. Давайте опишем его, что бы генератор знал, какие поля он должен отображать, какие табы должны присутствовать в форме редактирования новости:

Описание интерфейса.

<?php

namespace Demo\AdminHelper\News\AdminInterface;

use Bitrix\Main\Localization\Loc;
use DigitalWand\AdminHelper\Helper\AdminInterface;
use DigitalWand\AdminHelper\Widget\DateTimeWidget;
use DigitalWand\AdminHelper\Widget\FileWidget;
use DigitalWand\AdminHelper\Widget\NumberWidget;
use DigitalWand\AdminHelper\Widget\StringWidget;
use DigitalWand\AdminHelper\Widget\UrlWidget;
use DigitalWand\AdminHelper\Widget\UserWidget;
use DigitalWand\AdminHelper\Widget\VisualEditorWidget;

Loc::loadMessages(__FILE__);

/**
 * Описание интерфейса (табок и полей) админки новостей.
 *
 * {@inheritdoc}
 */
class NewsAdminInterface extends AdminInterface
{
    /**
     * {@inheritdoc}
     */
    public function fields()
    {
        return array(
            'MAIN' => array(
                'NAME' => Loc::getMessage('DEMO_AH_NEWS'),
                'FIELDS' => array(
                    'ID' => array(
                        'WIDGET' => new NumberWidget(),
                        'READONLY' => true,
                        'FILTER' => true,
                        'HIDE_WHEN_CREATE' => true
                    ),
                    'TITLE' => array(
                        'WIDGET' => new StringWidget(),
                        'SIZE' => '80',
                        'FILTER' => '%',
                        'REQUIRED' => true
                    ),
                    'TEXT' => array(
                        'WIDGET' => new VisualEditorWidget(),
                        'HEADER' => false
                    ),
                    'SOURCE' => array(
                        'WIDGET' => new UrlWidget(),
                        'HEADER' => false
                    ),
                    'IMAGE' => array(
                        'WIDGET' => new FileWidget(),
                        'IMAGE' => true,
                        'HEADER' => false
                    ),
                    'DATE_CREATE' => array(
                        'WIDGET' => new DateTimeWidget(),
                        'READONLY' => true,
                        'HIDE_WHEN_CREATE' => true
                    ),
                    'CREATED_BY' => array(
                        'WIDGET' => new UserWidget(),
                        'READONLY' => true,
                        'HIDE_WHEN_CREATE' => true
                    ),
                    'MODIFIED_BY' => array(
                        'WIDGET' => new UserWidget(),
                        'READONLY' => true,
                        'HIDE_WHEN_CREATE' => true
                    ),
                )
            )
        );
    }

    /**
     * {@inheritdoc}
     */
    public function helpers()
    {
        return array(
            '\Demo\AdminHelper\News\AdminInterface\NewsListHelper',
            '\Demo\AdminHelper\News\AdminInterface\NewsEditHelper'
        );
    }
}

Обратите внимание на описание полей (секция FIELDS). Каждое поле — это какой-то виджет. Виджеты представляет собой класс, занимающийся выводом представления поля, а так же предобработкой данных перед сохранением модели (но увлекаться этим не стоит, управлением данных должна заниматься модель). Виджеты имеют настройки, с помощью которых ими можно управлять. А если настроек недостаточно, вы без труда можете создать свой или расширить существующий виджет.

Далее нужно создать хелперы, которые будут заниматься «отрисовкой» страниц в панели управления. Хелпер списка новостей:

Страницы в панели управления.

<?php

namespace Demo\AdminHelper\News\AdminInterface;

use DigitalWand\AdminHelper\Helper\AdminListHelper;

/**
 * Хелпер описывает интерфейс, выводящий список новостей.
 *
 * {@inheritdoc}
 */
class NewsListHelper extends AdminListHelper
{
	protected static $model = '\Demo\AdminHelper\News\NewsTable';
}

И хелпер формы редактирования новости:

<?php

namespace Demo\AdminHelper\News\AdminInterface;

use DigitalWand\AdminHelper\Helper\AdminEditHelper;

/**
 * Хелпер описывает интерфейс, выводящий форму редактирования новости.
 *
 * {@inheritdoc}
 */
class NewsEditHelper extends AdminEditHelper
{
    protected static $model = '\Demo\AdminHelper\News\NewsTable';
}

Как видите, всего за пару минут мы написали простую админку управления новостями.

Пример рассмотренного модуля вы можете найти у меня на «Гитхабе». Помните о редакторах, делайте хорошие, удобные интерфейсы в админке, ведь теперь это просто!

Поделиться
Отправить
24 комментария
Артем

Добрый день!
Установил модуль https://github.com/niksamokhvalov/demo.adminhelper (по видео)
Но не вижу его в «Управление модулями» также нет в меню ссылки

Подскажите куда копать

Никита Самохвалов

Привет, на странице «Управление модулями» отображаются системные модули. А партнёрские ищете в разделе Marketplace → Установленные решения.

Александр

При такой конструкции
’PROGRAM_FILES’ => array(
’WIDGET’ => new FileWidget(),
’IMAGE’ => false,
’HEADER’ => false,
’MULTIPLE’ => true
),
при сохранение элемента вылетает ошибка. Если MULTIPLE убрать, то всё хорошо.
Normal fields can be only the last in chain, `PROGRAM_FILES` Bitrix\Main\Entity\IntegerField is not the last. (0)
/home/bitrix/www/bitrix/modules/main/lib/entity/querychain.php:166
#0: Bitrix\Main\Entity\QueryChain::getChainByDefinition(object, string)
/home/bitrix/www/bitrix/modules/main/lib/entity/query.php:1699
#1: Bitrix\Main\Entity\Query->getRegisteredChain(string, boolean)
/home/bitrix/www/bitrix/modules/main/lib/entity/query.php:494
#2: Bitrix\Main\Entity\Query->addToSelectChain(string, string)
/home/bitrix/www/bitrix/modules/main/lib/entity/query.php:1188
#3: Bitrix\Main\Entity\Query->buildQuery()
/home/bitrix/www/bitrix/modules/main/lib/entity/query.php:452
#4: Bitrix\Main\Entity\Query->exec()
/home/bitrix/www/bitrix/modules/main/lib/entity/datamanager.php:230
#5: Bitrix\Main\Entity\DataManager::getList(array)
/home/bitrix/ext_www/i.eksmo.ru/local/modules/digitalwand.admin_helper/lib/widget/FileWidget.php:99
#6: DigitalWand\AdminHelper\Widget\FileWidget->getMultipleEditHtml()
/home/bitrix/ext_www/i.eksmo.ru/local/modules/digitalwand.admin_helper/lib/widget/HelperWidget.php:260
#7: DigitalWand\AdminHelper\Widget\HelperWidget->showBasicEditField(boolean)
/home/bitrix/ext_www/i.eksmo.ru/local/modules/digitalwand.admin_helper/lib/helper/AdminEditHelper.php:371
#8: DigitalWand\AdminHelper\Helper\AdminEditHelper->showTabElements(array)
/home/bitrix/ext_www/i.eksmo.ru/local/modules/digitalwand.admin_helper/lib/helper/AdminEditHelper.php:290
#9: DigitalWand\AdminHelper\Helper\AdminEditHelper->show()
/home/bitrix/ext_www/i.eksmo.ru/local/modules/digitalwand.admin_helper/admin/route.php:130
#10: include_once(string)
/home/bitrix/www/bitrix/admin/admin_helper_route.php:3

Никита Самохвалов

Если вы хотите загружать несколько файлов в одно свойство, у вас должна быть отдельная модель для хранение файлов, а в основной модели поле PROGRAM_FILES должно быть полем-связью (ReferenceField).

Александр

У меня прописано так
new Entity\IntegerField(’PROGRAM_FILES’, array(
’title’ => Loc::getMessage(’DEMO_AH_NEWS_PROGRAM_FILES’)
)),
new Entity\ReferenceField(
’PROGRAM_FILES_FILE’, ’Bitrix\File\File’, array(’=this.PROGRAM_FILES’ => ’ref.ID’), array(’join_type’ => ’LEFT’)
),

Никита Самохвалов

Что это за модель такая Bitrix\File\File? Такого в Битриксе не существует.

Виджет файлов в описании интерфейса привязан к полю PROGRAM_FILES, но оно у вас является числовым. Виджет, если он работает в множественном режиме, должен быть привязан к полю-связке с другой таблицей. Тогда Админ-хелпер будет сохранять файлы в связанную таблицу.

Александр

Спасибо, а Bitrix\File\File взял из файла module\iblock\lib\element.php там так описано
’DETAIL_PICTURE_FILE’ => new Main\Entity\ReferenceField(
’DETAIL_PICTURE_FILE’,
’Bitrix\File\File’,
array(’=this.DETAIL_PICTURE’ => ’ref.ID’),
array(’join_type’ => ’LEFT’)
),

Никита Самохвалов

Это какая-то ошибка ядра, класса Bitrix\File\File не существует.

Александр

И ещё при использование виджета HLIBlockFieldWidget, когда я создаю поле Дата со временем и при создании элемента пытаюсь его сохранить с пустым или заполненным значением из календаря, появляется ошибка
Значение поля «UF_DATE_TIME» не является корректной датой/временем.

Никита Самохвалов

Нужно больше подробностей: описание интерфейса, модель. Можете описать свою проблему на «Гитхабе» и приложить эти сведения, рассмотрим там: https://github.com/DigitalWand/digitalwand.admin_helper

Александр

написал.

Никита Самохвалов

За вами уже выехали :)

Виталий

Большое дело делаете! Будем пробовать.

Козлов Дмитрий

Добрый день. Все отлично работает, но вот только указание наименования «NAME» в массиве возвращаемом AdminInterface->fields() не имеет никакого действия. Всегда выводится заголовок «Административный интерфейс».
В чем может быть ошибка?

Никита Самохвалов

Фраза «Административный интерфейс» где выводится? Таб или название поля?
Покажите вашу конфигурацию.

Козлов Дмитрий

Фраза «Административный интерфейс» где выводится?

Название страницы. Я нашел по коду метод setTitle, но его просто негде вызвать. Решил пока, переопределив метод show. В нем вызываю SetTitle, а потом parent::show()

Никита Самохвалов

Понятно. Обычно мы устанавливаем подобные настройки в конструкторе класса.

Козлов Дмитрий

Понятно. Обычно мы устанавливаем подобные настройки в конструкторе класса.

Просто для простых админ. страниц с одним ОРМ-классом требуется только унаследовать классы без переопределения методов и в итоге setTitle негде вызывать. Может стоит сделать публичное свойство title, и если оно задано вызывать setTitle?

Никита Самохвалов

Справедливое замечание, особенно учитывая метод AdminInterface::helpers: https://github.com/DigitalWand/digitalwand.admin_helper/blob/2.1.0/lib/helper/AdminInterface.php#L87-L131. В нём уже можно настраивать заголовки кнопок управления. Похоже, что нужно сюда же добавить настройку заголовка страницы. Заводите проблему на Гитхабе, а если хватит сил, то и пул-реквест ;-)

Козлов Дмитрий

Можно ли добавить в демку работу с секциями?
В модели добавил поле PARENT, связывающее с моделью разделов по
’reference’ => array(’=this.PARENT_ID’ => ’ref.ID’)

создал потомков от AdminSectionEditHelper, AdminSectionListHelper, указал в них модель разделов

добавил их в helpers. В итоге вижу лист корневых разделов и кнопки «Создать элемент», «Создать раздел». Разделы отображаются как элементы, двойной клик вызывает редактирование раздела. Как добраться до элементов?

Никита Самохвалов

Так же вам нужно указать в интерфейсе зависимость: https://github.com/DigitalWand/digitalwand.admin_helper/blob/2.x/lib/helper/AdminInterface.php#L135-L140.

Козлов Дмитрий

Разделы отображаются как элементы

Понятно )))
заглянул в код
class AdminSectionListHelper extends AdminListHelper
{
}

Козлов Дмитрий

Так же вам нужно указать в интерфейсе зависимость

А примерчик можно с разделами в демке добавить?

Никита Самохвалов

Постараюсь сделать, но не уверен, что в ближайшее время получится. Завёл задачку: https://github.com/niksamokhvalov/demo.adminhelper/issues/2

Максим

А есть возможность у модуля создавать так называемые карточки? То есть допустим два множественных поля, связаных. То есть, допустим товар, цена — я хочу, что бы эти два поля выглядели целостно, то что бы рапологались вместе и нажатие на «+» сразу же добавляло новую картку(связку этих полей, а не одно отдельное поле).

Никита Самохвалов

Да, конечно, сделать можно абсолютно любой интерфейс и функционал. Создайте свой виджет, в котором вы сможете сделать необходимую вьюху и логику сохранения данных.

Ирина

Доброго дня!
Есть ли в модуле возможность создавать взаимосвязанные поля? Например есть выпадашка со списком инфоблоков и чтобы при выборе инфоблока в другом поле отображался список разделов выбранного инфоблока?
Если да, то подскажите, пожалуйста, как подобное реализовать)

Никита Самохвалов

Здравствуйте, такой возможности из коробки нет. Сделайте джаваскриптом связь между двумя полями, что бы динамически их синхронизировать.

Дмитрий

Попробовал создать свой модуль на основе демки, получил сообщение об ошибке:
[DigitalWand\AdminHelper\Helper\Exception]
References to section model not found (0)

Подскажите, куда копать дальше?

Дмитрий

Разобрался... поспешил и не от того класса наследовался

Никита Липилин

Здравствуйте! Возникла необходимость реализации связи многих ко многим. В моей версии Битрикса это реализуется путём создания связывающей сущности, у которой в качестве полей выступают ссылки на связанные элементы сущностей 1 и 2. К примеру, нужно сделать связь типа «Автор» -> «Книга». Можно ли как-то добавить возможность в админке при добавлении, скажем, книги, указывать авторов, чтобы при добавлении элементов формировались соответствующие связи, а недостающие элементы добавлялись?
Метод «add» у моделей уже перегружен соответствующим образом, то есть если при добавлении книги в таблицу этим методом в массиве полей будет «AUTHORS», то будет добавлена и книга, и её авторы(если их ещё нет в таблице авторов), и созданы необходимые связи. Осталось лишь понять, как реализовать функционал, чтобы подобное мог выполнить пользователь через админку

Никита Липилин

Переформулирую вопрос.
Можно ли сделать так, чтобы в форме редактирования или создания элемента добавлялось поле, которого в сущности нет и значение которого обрабатывалось бы особым образом?

Никита Липилин

Прошу прощения за беспокойство, похоже, всё реализуется довольно просто. Большое спасибо за проделанную по созданию этого чуда работу.

Никита Липилин

А нет, всё-таки не понимаю, как это сделать)

Никита Самохвалов

Посмотрите метод saveElement в хелпере: https://github.com/DigitalWand/digitalwand.admin_helper/blob/2.x/lib/helper/AdminEditHelper.php#L512-L536. Если я правильно понял, подмешивать требуемые параметры и значения вы можете в нём (нужно всего лишь переопределить этот метод в своём хелпере).

Никита Липилин

Спасибо большое, очень выручили!

Александр

Можно ли каким-то образом использовать ваш генератор внутри своего модуля? Я имею ввиду без необходимости устанавливать и включать ваш модуль на сайте.

Никита Самохвалов

Нельзя.

Aleksandr

Подскажите, пожалуйста.
Как можно реализовать свой в виджет в рамках модуля, чтобы он сохранял данные не только в текущую таблицу, но и в таблицу связанной сущности.
Например, у меня есть элементы, есть свойства и значения свойств. Мне нужна возможность при создании элемента заполнять его свойства, чтобы значения этих свойств сохранялось в таблицу «значения свойств».

Популярное