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

Ложка меда и бочка дегтя

Кстати, в маркетплейсе есть модуль, который помогает создавать модули - 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 > Установленные решения) и смотрим, что там имеется:

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

Можно сделать первый коммит 🙂

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

К следующей статье цикла.