Пора бы уже начать решать поставленную задачу, а то уже на протяжении 7ми последних частей — один трёп 🙂 Но перед этим снова придется немного повозиться с инсталлятором, т.к. появились дополнительные потребности, а также нужно исправить недочеты, сделанные ранее. Чувствую, что с инсталлятором придется возиться до момента самого запуска …
Благодаря информации из обсуждения в предыдущей статье стало ясно, что оказывается в битриксе предусмотрено два способа инсталляции модуля — автоматический и интерактивный. Автоматический способ используется при инсталляции системы и не подразумевает взаимодействия с пользователем в момент установки, тогда как интерактивный способ вызывается пользователем вручную в момент установки модуля из админки. В связи с этим есть две точки входа для установки модуля — комплекс методов InstallDB, InstallFiles и InstallEvents (для автоматической установки), а также метод DoInstall (для интерактивной).
В момент автоматической установки битрикс вызывает методы в следующем порядке: InstallDB > InstallEvents > InstallFiles. Значит мы должны со своей стороны обеспечить корректную работу инсталлятора при вызове этих методов последовательно именно в этом порядке. В связи с этими умозаключениями пришлось немного поменять порядок вызова методов автоматической установки внутри метода интерактивной установки. В моем случае это не сыграло ровно никакой роли, т.к. метод установки почтовых событый у меня пустой. Ну да ладно ..
Также я слегка подкрутил инсталлятор для БД. В прошлый раз я слишком сильно положился на механизм транзакций, совсем забыв, что в MyISAM транзакций-то и нет. Переделывать инсталлятор в связи с этим смысла нет, MyISAM будет просто игнорировать инструкции транзаций при попытке их вызова, поэтому к ошибкам это не приведет. Но нужно немного обезопаситься на случай, если все же в процессе инсталляции произойдет какой-то косяк. На этот случай я после поимки исключения в процессе инсталляции помимо rollback’а также вызываю еще и снятие регистрации модуля. Метод инсталляции теперь выглядит примерно так
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 |
public function install() { try { $this->connection->startTransaction(); ModuleManager::registerModule($this->moduleId); Loader::includeModule($this->moduleId); $this->createProcessTable(); $this->addPIDColumn(); $this->connection->commitTransaction(); } catch (\Exception $e) { $this->connection->rollbackTransaction(); ModuleManager::unRegisterModule($this->moduleId); // Тут добавил unregister throw new \Exception($e->getMessage(), $e->getCode()); } return true; } |
Я посчитал, что если уж на каком-то из этапов произойдет сбой, который приведет к исключению, то этого будет достаточно. В частности — если вдруг исключение произойдет в момент регистрации модуля, то регистрацию модуля вообще не надо будет снимать (да и сам вызов снятия регистрации в этом случае также приведет к исключению скорее всего, которое будет поймано уровнем выше). Если исключение произойдет в процессе подключения модуля (что маловероятно), то снятие регистрации произойдет успешно и никакие данные модуля не будут установлены. Если исключение произойдет в момент добавления таблицы, то я хз, вообще сам битрикс не будет работать в случае, если прав на создание таблиц не хватает, например. Точно также битрикс не будет работать, если не хватает прав на создание колонок.
В общем, я посчитал что этого будет достаточно.
Меню
Ну и последнее нововведение, которое появилось в инсталляторе — это установка обработчиков событий. Я уже упоминал, что нужно будет добавлять собственные пункты меню, и что делать я это буду с помощью события OnBuildGlobalMenu. Еще я упоминал, что регистрировать события я буду в runtime, т.к. это несколько удобнее. Но чтобы регистрировать события в runtime, например в файле %module_root%/include.php , нужно чтобы кто-то подключил наш модуль извне. Это неудобно, т.к. работа модуля начинает зависеть от внешнего кода, а нам это не надо. Для решения этой проблемы в битриксе есть функционал, который позволяет добавить обработчики событий для модулей в БД, и эти события будут регистрироваться автоматически, если модуль установлен. Для добавления таких обработчиков событий существует функция RegisterModuleDependencies, или её новый аналог из d7 — \Bitrix\Main\EventManager::registerEventHandlerCompatible. Данный метод добавляет событие в табличку b_module_to_module, если его там нет, и прописывает там все необходимые параметры для регистрации события. Ну а при возникновении первого события в жизненном цикле страницы (OnPageStart, вероятно), все эти события запрашиваются из БД и регистрируются в рантайме.
На текущий момент у меня есть лишь одно событие. Что если мне понадобится в будущем завести несколько событий? Если использовать функцию \RegisterModuleDependencies, то мне придется делать регистрацию всех событий частью инсталлятора, а это менее удобно в долгосрочной поддержке. Поэтому я решил прибегнуть к следующему, комбинированному способу:
- в момент инсталляции модуля я зарегистрирую обработчик только для одного события — OnPageStart
- в обработчике этого события я буду регистрировать всё то, что мне необходимо для работы, включая нужные мне обработчики событий
Для регистрации обработчиков мне потребуется какой-то класс, в котором будет храниться вся логика, связанная с регистрацией обработчиков событий. Пусть это будет \Maximaster\Coupanda\EventHandlersRegistry. В нем будет всего один метод — register, задачей которого будет регистрация всех обработчиков событий, которые мне необходимы. Ну и поскольку обработчик мне нужен всего один, то и регистрироваться будет только он один. Код такой:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 |
<?php namespace Maximaster\Coupanda; use Bitrix\Main\Context; use Bitrix\Main\EventManager; use Maximaster\Coupanda\EventHandlers\OnBuildGlobalMenu; class EventHandlersRegistry { public static function register() { $manager = EventManager::getInstance(); $context = Context::getCurrent(); if ($context->getRequest()->isAdminSection()) { $manager->addEventHandler('main', 'OnBuildGlobalMenu', array( OnBuildGlobalMenu::class, 'addGeneratorToMenu' )); } } } |
Все просто. Если мы находимся в админке, то регистрируем событие OnBuildGlobalMenu, которое добавит пункт меню для генератора. Тут я пока не заморачиваюсь с правами доступа, т.к. ими буду заниматься в последнюю очередь.
Код класса обработчика события выкладывать не буду, кому нужно — посмотрит на гитхабе. Смысл этого обработчика события заключается в том, чтобы во всем дереве пунктов меню найти нужную ветвь и внедрить туда свой пункт меню со своей ссылкой. Результат не заставляет себя ждать после переинсталляции:
Вот он, родимый.
Вызывающий скрипт
Наконец-то! Добрались до визуала, интерактива и логики, теперь работать должно стать поинтереснее 🙂
Итак, опираясь на функциональные требования, где мы составили набросок элементов интерфейса, можно теперь набросать форму настроек процесса генерации. Но для того, чтобы создавать какой-то интерфейс, нужно сначала добавить URL, по которому этот интерфейс будет отображаться. Ранее я уже решил, что вся логика, связанная с отображением формы настроек, а также с рендерингом процесса генерации у меня будет располагаться в моем собственном компоненте. Компонент этот будет подключаться в скрипте %module_root%/admin/register.php. Создаем этот скрипт:
1 2 3 4 5 6 7 8 9 10 11 12 |
<?php require_once $_SERVER['DOCUMENT_ROOT'].'/bitrix/modules/main/include/prolog_admin_before.php'; require_once $_SERVER['DOCUMENT_ROOT'].'/bitrix/modules/main/include/prolog_admin_after.php'; global $APPLICATION; // Вызываем пока еще несуществующий компонент maximaster:coupanda.coupon.generator // с шаблоном admin $APPLICATION->IncludeComponent('maximaster:coupanda.coupon.generator', 'admin'); require_once $_SERVER['DOCUMENT_ROOT'].'/bitrix/modules/main/include/epilog_admin.php'; |
Осталось переустановить модуль, чтобы создался вызывающий скрипт, а также добавить на него ссылку из обработчика события, добавляющего пункт меню. После переустановки тыкаем на наш новый пункт меню и видим что-то такое:
Видно, что вызов компонента работает (но компонента нет, поэтому и ошибка), а также видим корректно сформированную цепочку навигации, что автоматически означает правильность работы зарегистрированного пункта меню.
Форма настроек
Теперь можно приступать к разработке компонента, который будет контролировать процесс генерации. Для начала создадим просто форму настроек, чтобы хоть что-то у нас появилось материальное.
Исходя из документации на структуру модулей битрикс, все компоненты должны располагаться в директории %module_root%/install/components, а уже оттуда должны копироваться в /bitrix/components. Так и сделаем — добавляем директорию %module_root%/install/components/maximaster/coupanda.coupon.generator, переинсталлируем и видим, что создалась директория /bitrix/components/maximaster/coupanda.coupon.generator
Дальше предполагается, что вся работа по созданию компонента будет вестись в этой директории, но для меня такой подход не удобен, поэтому я просто удаляю эту директорию и создаю симлинк на %module_root%/install/components/maximaster/coupanda.coupon.generator, что позволяет мне работать внутри своего репозитория и исключает неудобства, связанные с необходимостью заливки измененного компонента в инсталлятор обратно (правда в этом случае при переустановке надо следить за этим, но меня теперь уже переустановка не интересует пока).
Компонент, ясен пень, будем писать в виде класса. На данном этапе задачей этого класса будет выборка всех тех данных, которые необходимы для отображения формы генерации. В нашем случае это:
- список всех скидок
- список типов купонов
- всякие системные вещи вроде инициализации компонента, подключения модулей и подключения шаблона компонента
- сам шаблон с формой
- ссылка на страницу со списком битриксовых скидок
Список всех скидок получить просто:
1 2 3 4 5 6 7 8 9 |
$q = DiscountTable::query() ->addOrder('ID', 'desc') ->setSelect(['ID', 'NAME', 'ACTIVE']); $discounts = []; $discountList = $q->exec(); while ($discount = $discountList->fetch()) { $discounts[] = $discount; } |
Список всех типов купонов — в общем-то тоже:
1 |
$types = DiscountCouponTable::getCouponTypes(true); |
Писать о том, как подключить модули — я не буду 🙂 Банально
Остается завернуть это все в компонент и написать шаблон компонента.
В соответствии с функциональными требованиями, процесс генерации должен состоять из трех разных экранов. Визуально с помощью админки можно разделить экраны на табы. Для построения страницы с табами в админке есть несколько способов:
- \CAdminTabControl — это класс, который имеет набор методов для формирования административной страницы, разбитой на табы
- \CAdminForm — наследник предыдущего класса, используется для построения форм со стандартными элементами управления
- \CAdminTabControlDrag — новый класс, который позволяет строить страницы вида той, что используется для просмотра информации о заказе с возможностью перетаскивания блоков
Я буду использовать первый, т.к. моя страница не является целиком формой (поэтому мне не подойдет \CAdminForm), а использовать \CAdminTabControlDrag я не хочу в виду того, что данный способ построения интерфейса не очень подходит для целей данной страницы.
Ок, делаем шаблон компонента:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 |
<?=BeginNote();?> Данная страница осуществляет процесс генерации новых купонов. Сначала нужно произвести настройку на вкладке "Настройка генерации", после подтверждения которых сразу же будет запущен процесс генерации, с которым можно ознакомиться на вкладке "Процесс генерации". После окончания генерации вся сводная информация о процессе будет доступна во вкладке "Отчет о генерации" <?=EndNote();?> <? $buttons = []; $tabs = [ [ 'DIV' => 'configuration', 'TAB' => 'Настройка генерации', 'ICON' => '', 'TITLE' => 'Настройка генерации' ], [ 'DIV' => 'progress', 'TAB' => 'Процесс генерации', 'ICON' => '', 'TITLE' => 'Процесс генерации' ], [ 'DIV' => 'report', 'TAB' => 'Отчет о генерации', 'ICON' => '', 'TITLE' => 'Отчет о генерации' ], ]; $tabControl = new \CAdminTabControl('coupanda_generator', $tabs, false, true); $tabControl->Begin(); $tabControl->End(); |
И получаем на выходе рабочую страницу с рабочими табами (правда пустую пока :))
После этого с помощью обычных таблиц и элементов форм рендерим всю страницу с настройками. Из не очень часто встречающегося, нам потребуется пара инпутов для ввода периода с датами. Для их рендеринга приходится функция \CalendarPeriod. Также у нас будет инпут с выбором пользователя, который можно отрендерить с помощью функции \FindUserID. Небольшие пояснения выводятся с помощью функций \BeginNote и \EndNote.
Не знаю, есть ли более современные аналоги для этих функций, поделитесь в комментариях, если вдруг знаете.
Кроме этого в макетах я предусмотрел вывод небольшого хинта для каждого поля в формах настройки. Во многих формах в битриксе встречается этот элемент управления, но для его быстрого использования функции не нашлось, поэтому пришлось завернуть его в свою функцию (пока разместил её в шаблоне данного компонента, т.к. пока больше нигде и не нужно). Вот, может кому пригодится:
1 2 3 4 5 6 7 8 9 |
function getHint($id, $hint) { $id = 'hint_' . $id; $hint = \CUtil::JSEscape($hint); return <<<HTML <span id="{$id}"></span> <script>BX.hint_replace(BX('{$id}'), '{$hint}');</script> HTML; } |
Функция рендерит span и script теги, которые в паре работают как вопросик с тултипом в нативном битриксовом интерфейсе.
После того, как были запилены все элементы формы, получился вот такой монстрик:
По-моему норм.
Все последние изменения модуля — тут.
В следующей статье будем оживлять эту форму с помощью битриксовой адской js-библиотеки и ванильного javascript’а (а может и jq, там видно будет).