Пора начинать наконец заниматься делом и приступать к созданию модуля. Первым делом нужно реализовать базовую файловую структуру и механизм инсталляции. Каждый раз так влом создавать эту структуру всю, лень писать инсталлятор, но надо перебороть себя и сделать.
Ложка меда и бочка дегтя
Кстати, в маркетплейсе есть модуль, который помогает создавать модули - bitrix.mpbuilder. Разработал его Битрикс, видимо, в помощь партнерам. Но, судя по комментариям, этот модуль уже 2 года не получал обновлений и вообще работает не полностью. Меня это поначалу не смутило, поэтому я все равно поставил этот модуль и попытался сгенерировать из него структуру файлов.
Модуль предлагает разработчику пройти 5 шагов: создать структуру модуля, выделить языковые фразы, редактор языковых фраз (который не работает), создать архив с модулем и собрать новую версию для обновления модуля в маркете. Иду на первый шаг, заполняю там все интересующие меня на данный момент поля:

Вот что получилось из этого:

Он создал директорию install, в которой лежит готовый инсталлятор и файл с описанием версии. Все языковые фразы, которые я вводил (описание модуля, название и т.д.) он выделил в необходимый для этого языковой файл. Также он создал необходимый для работы модуль include.php. Все бы хорошо, но …
Все это не годится и придется переделывать 🙂 Почему? Причины:
- описание класса инсталлятора ну вообще не годится. Обилие устаревших конструкций, конструктор в формате php4, отсутствие модификаторов доступа к методам класса, табы вместо пробелов, куча лишнего кода …
- закрывающие теги в php файлах! Мало того, что они есть, так после них есть еще и переносы строк!что будет расцениваться как вывод в стандартный поток вывода и в некоторых случаях может привести к очень трудноуловимым ошибкам (на эту тему напишу отдельный пост)

- часть логики вынесено в include.php, а именно создание пункта меню. Я понимаю, что битрикс рекомендует это делать, нужно это для того, чтобы обеспечить работу бесплатного пробного периода (весь код include.php обфусцируется, и туда добавляется проверка на пробный режим). Но я бы выносил в эту часть что-то более существенное, наверно. Кстати, надо бы подумать, что вынести туда 🙂
В довесок к этому, в модуле не работает третий шаг (редактор языковых файлов). В общем у битрикса как всегда - подзабили …
Придется создавать самому. Кстати, вот неплохая идея для опенсорса - запилить конструктор модулей с помощью symfony/console и composer create-project, думаю будет быстрее, удобнее, и более гибко, а соответственно расширяемо, по сравнению с этим bitrix.mpbuilder. Туда же можно будет прикрутить сборку пакета с модулем, а также сборку файлов обновлений на основании версионного контроля. Найти бы время … Или может быть уже есть что-то подобное?
Для начала нам понадобится раздобыть инсталляцию битрикса. Можно взять официальную демку, и развернуть чистую битру по мануалу. Но я не буду заморачиваться и просто начну работу по модулю в одном из своих существующих проектов, чтобы не тратить время на развертывание.
Кроме этого нам понадобится версионный контроль. Думаю, в этом блоге никому не надо объяснять целесообразность его использования даже для одного разработчика.
Создадим новый проект в PhpStorm и создадим там структуру файлов и папок по мануалу.

Жутковато, правда? Но ничего, справимся. Тут я создал полную версию всей структуры, за исключением устаревших возможностей и с учетом того, что модуль будет работать на mysql-подобной базе. Я сохраню всю эту структуру на будущее для того, чтобы может быть потом взяться за конструктор модулей, а сейчас пока удалю все лишнее. Исходя из ограничений, принятых на проекте, я буду использовать только d7 для разработки своих классов, а значит директория /classes мне не понадобится. Также мне не понадобится файл /admin/menu.php, т.к. меню у меня будет добавляться с помощью обработчика события. /install/images и /install/js мне тоже не понадобятся, т.к. все механизмы, связанные с интерактивом я буду выносить в компоненты. Может быть потом появится какой-то красивый логотип, тогда добавлю его в пункт меню, но не более. Кастомизировать внешний вид админки мы тоже не будем, поэтому директория /panel тоже не нужна. Поскольку я буду работать с таблицами d7, то и создание таблиц будет происходить с помощью ORM, а значит мне не понадобится (по крайней мере пока), директория /install/db. Еще я хочу удалить директорию /install/admin (битрикс называет скрипты из этой папки «вызывающими»). Вот и у меня эти скрипты вызывают много вопросов и из-за них я впадаю в уныние, поэтому не буду хранить их в проекте а просто буду генерировать на лету в процессе инсталляции. Ну и напоследок, можно удалить файл /prolog.php, который имеет весьма сомнительное предназначение. Если он мне понадобится, то заведу, но сейчас не вижу в нем никакого смысла. Итого получаем следующую структуру:

Уже не так страшно.
В .gitignore добавим директорию /vendor, которая скорее всего у меня тут появится для хранения скриптов, нужных для разработки.
Для того, чтобы иметь отдельный проект, но при этом иметь возможность работать с этим кодом в другом проекте, я просто создал симлинк из одного проекта в другой. В проекте, где используется битрикс я просто создал симлинк /local/modules/maximaster.coupanda, который ссылается на мой проект. Но тут можно придумать 100500 схем работы с модулем внутри другого проекта, дело вкуса.
Код
Наконец-то, добрался я до разработки. Инсталлятор должен находиться в директории /install/index.php. Это должен быть класс, наследник от \CModule с именем maximaster_coupanda. Данный класс должен присвоить значения свойствам:
public $MODULE_NAME;
public $MODULE_DESCRIPTION;
public $MODULE_VERSION;
public $MODULE_VERSION_DATE;
public $MODULE_ID;
public $MODULE_SORT;
public $SHOW_SUPER_ADMIN_GROUP_RIGHTS;
public $MODULE_GROUP_RIGHTS;
public $PARTNER_NAME;
public $PARTNER_URI;
Часть из них обязательны, часть нет. Все свойства в родителе объявлены через var, поэтому являются публичными. В конструкторе создадим набор методов, который будет инициализировать значения этих свойств:
public function __construct()
{
$this->initModuleId();
$this->initModuleVersionDefinition();
$this->initModuleName();
$this->initModuleDescription();
$this->initModulePartnerInfo();
$this->initModuleGroupRights();
}
Тут все довольно тривиально, за исключением версии модуля. Номер версии модуля должен храниться в файле /install/version.php в виде массива. Плюс к этому нужно в классе модуля записывать эти значения. Зачем так сделано? Да хз … Но в связи с этим нужно налепить небольшую городушку, которая будет брать значение из этого файла и записывать его в модуль. Получаем нечто такое для какой-то … версии … :
protected function initModuleVersionDefinition()
{
$versionDefinition = $this->getModuleVersionDefinition();
$this->MODULE_VERSION = $versionDefinition['VERSION'];
$this->MODULE_VERSION_DATE = $versionDefinition['VERSION_DATE'];
}
protected function getDefaultVersionDefinition()
{
return [
'VERSION' => '1.0.0',
'VERSION_DATE' => '1970-01-01 00:00:00'
];
}
protected function getModuleVersionDefinition()
{
$arModuleVersion = [];
include __DIR__ . '/version.php';
$defaultVersionDefinition = $this->getDefaultVersionDefinition();
if (!\is_array($arModuleVersion) || empty($arModuleVersion)) {
return $defaultVersionDefinition;
}
$version = isset($arModuleVersion['VERSION']) ? $arModuleVersion['VERSION']
: $defaultVersionDefinition['VERSION'];
$versionDate = isset($arModuleVersion['VERSION_DATE']) ? $arModuleVersion['VERSION_DATE']
: $defaultVersionDefinition['VERSION_DATE'];
return [
'VERSION' => $version,
'VERSION_DATE' => $versionDate
];
}
Тут я в некоторых местах использовал венгерскую нотацию (которую, к слову, не очень то воспринимаю в условиях современного процесса разработки), но это обусловлено необходимостью работать с битриксом.
Инсталляция/деинсталляция
Для процесса инсталлирования в битриксе предусмотрен метод DoInstall(). А для деинсталлирования DoUninstall() соответственно. Кроме этого битриксом заложен заранее предустановленный набор методов, которые должны проинсталлировать файлы модуля, почтовые события и данные для БД - InstallFiles, InstallEvents и InstallDB соответственно, в автоматическом режиме. Для деинсталляции предусмотрены аналогичные методы. Пока что модуль у нас пустой, инсталлировать кроме него самого нечего, поэтому методы инсталляции файлов и почтовых событий оставлю пока пустыми, а в методе инсталляции БД пропишем регистрацию модуля в системе, а сам метод инсталляции БД добавим в DoInstall. Для остальных методов инсталляции добавим заглушки и тоже добавим их в DoInstall. Получается вот такая штука:
public function DoInstall()
{
$this->InstallDB();
$this->InstallEvents();
$this->InstallFiles();
}
public function InstallDB()
{
\Bitrix\Main\ModuleManager::registerModule($this->MODULE_ID);
return true;
}
public function InstallFiles()
{
return true;
}
public function InstallEvents()
{
return true;
}
Ровно то же самое получается для деинсталляции.
Система прав доступа
В битриксе есть несколько уровней доступа - права на файлы/директории, права модулей (на основе ролей или «просто права»). Права на файлы нас не интересуют, ими управляет модуль управления структурой. По условиям задачи мне потребуется только 2 роли - администратор модуля (который будет иметь права на все) и все остальные (которые не будут иметь доступа).
Я думаю, что для реализации этих возможностей можно не использовать систему ролей, чтобы не усложнять, а обойтись системой обычных прав доступа. Создам 2 права доступа пока - «[D] Доступ запрещен» и «[W] Полный доступ». Буквенные обозначения будут использоваться в коде для проверки прав. Данные буквы выбраны из соображения «привычности» для мира битрикса, но можно использовать любые, главное чтобы вы с оператором сравнения могли их нормально обрабатывать. Тут понятно, что W (Write) больше чем D (Denied), главное не напутать.
Для того, чтобы обозначить список прав доступа, нужно в классе модуля добавить метод GetModuleRightList. Этот метод должен возвращать массив описания прав доступа, доступных для вашего модуля. В моем случае получился вот такой простой метод:
public function GetModuleRightList()
{
$rightsReferenceIds = ['D', 'W'];
$references = [];
foreach ($rightsReferenceIds as $referenceId) {
$references[] = "[{$referenceId}] " . \GetMessage('MAXIMASTER.COUPANDA:MODULE_RIGHTS_REFERENCE_' . $referenceId);
}
return [
'reference_id' => $rightsReferenceIds,
'reference' => $references,
];
}
Если же нужно использовать систему ролей, то необходимо вместо этого метода добавить метод GetModuleTasks, где описать массив чуть более сложной структуры, но суть его проста - добавить строковые имена всем возможным операциям и привязать эти операции к ролям и также дать буквенные обозначения. Благодаря этому администратор главного модуля сможет добавлять собственные роли к вашему модулю в разделе «Настройки > Пользователи > Уровни доступа». Но мне это не нужно, поэтому пока пропустим. Надо бы статейку про права накатать, а то инфы мало по этому поводу в интернете …
Остается только создать набор языковых файлов и вынести туда всякие тексты - названия прав доступа, название организации-разработчика, название и описание модуля, etc.
Первый пуск
Итак, что мы имеем. В /local/modules/ у нас лежит директория maximaster.coupanda, в которой есть минимально необходимая структура для работы модуля, инсталлятор модуля и языковые файлы модуля.
Идем в админку (Marketplace > Установленные решения) и смотрим, что там имеется:

Проверяем, что модуль действительно устанавливается и удаляется:


Можно сделать первый коммит 🙂
В следующей «серии» будем делать функционал по части БД и прикручивать его к инсталлятору.