БесплатноБазовая интеграция с 1С в подарок при заказе интернет-магазина под ключ

Разработка своего модуля для 1С-Битрикс: структура и установка

Разработка своего модуля для 1С-Битрикс: структура папок, установщик install/index.php, свои таблицы через ORM и регистрация обработчиков

Рано или поздно у любого, кто серьёзно пишет на 1С-Битрикс, накапливается «золотой фонд» кода: обработчики скидок, интеграция с внешним сервисом, свой калькулятор доставки, набор агентов и служебных таблиц. Сначала всё живёт в init.php и в папке /local, потом переезжает копипастой на следующий проект, и в какой-то момент вы ловите себя на том, что заново собираете одну и ту же логику из десятка файлов, боясь что-нибудь забыть. Ответ на это — собственный модуль: упакованная, переносимая единица кода со своим установщиком, версией и чистым удалением. Один клик «Установить» — и таблицы созданы, обработчики зарегистрированы, настройки на месте; один клик «Удалить» — и система вернулась в исходное состояние.

Эта статья — практическая карта разработки модуля для Битрикс от первой папки до публикации. Разберём, зачем вообще нужен свой модуль и чем он отличается от кода в /local и от компонента; как устроена структура модуля (install/index.php с классом установки, lib/ с классами и namespace, options.php, lang/, include.php, version.php, .description.php); как работает регистрация через RegisterModule, установка и удаление (InstallDB/UnInstallDB, InstallEvents), как создавать свои таблицы через ORM, регистрировать обработчики событий, делать страницу настроек и права, класть внутрь компоненты и агенты, вести версии и обновления и, обзорно, публиковать модуль в Маркетплейсе. Материал — выжимка из нашей практики разработки кастомных модулей 1С-Битрикс, где аккуратный установщик и чистое удаление — часть стандарта качества.

Коротко

  • Модуль — это переиспользуемая логика с установщиком, версией и чистым удалением; в отличие от /local он ставится и снимается как единое целое и изолирован от ядра.
  • Структура: install/index.php (класс установки), install/version.php, lib/ (классы с namespace), include.php, options.php, lang/, .description.php.
  • Установка: RegisterModule → InstallDB (свои таблицы) → InstallEvents (обработчики) → копирование файлов; удаление — точное зеркало.
  • Свои таблицы удобнее описывать сущностью ORM (DataManager + getMap) и создавать через createDbTable; обработчики регистрируют через RegisterModuleDependences.
  • Внутри модуля живут настройки (options.php, Option::get/set), права, компоненты и агенты; публикация в Маркетплейсе — отдельный, необязательный шаг.

Зачем разработчику свой модуль

Технически всё, что делает модуль, можно сделать и без него: положить классы в /local/php_interface, обработчики — в init.php, таблицы создать руками через миграцию, настройки хранить в своём файле. Так почему модуль? Потому что модуль превращает разрозненный код в управляемую единицу с жизненным циклом. У него есть три ключевых свойства, которых нет у «просто файлов».

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

Модуль, /local и компонент: в чём разница

Три сущности часто путают, потому что все они «добавляют функциональность». Но они решают разные задачи и находятся на разных уровнях.

Папка /local

/local — это «проектная надстройка»: место для кода конкретного сайта, который имеет приоритет над /bitrix и не затирается обновлениями. Здесь живут проектные компоненты и шаблоны, php_interface/init.php с обработчиками, вспомогательные классы. /local не устанавливается и не удаляется как единое целое, не имеет версии и установщика. Это идеально для того, что нужно только этому проекту и никуда не переедет.

Компонент

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

Модуль

Модуль — это единица функциональности с жизненным циклом: установщик, версия, свои таблицы, обработчики, настройки, права, а также может содержать внутри себя компоненты и агенты. Модуль — самый «тяжёлый» и самый переносимый из трёх. Он изолирует крупную логику и умеет ставиться/сниматься целиком.

Практическое правило. Нужен один блок на странице — делайте компонент. Нужна проектная правка «только здесь» — кладите в /local. Нужна переиспользуемая логика с таблицами, обработчиками и настройками, которую вы будете переносить между проектами или отдавать заказчику — делайте модуль (а компоненты положите внутрь него). Эти подходы не конкурируют: часто ядро логики — модуль, а мелкие проектные хвосты — в /local.

Таблица: модуль vs /local vs ядро

Чтобы граница была наглядной, соберём отличия в одну таблицу. Под «ядром» здесь понимается штатный код /bitrix/modules/, который вы не должны править напрямую.

КритерийСвой модульКод в /localПравка ядра /bitrix
Установщик и удалениеЕсть (DoInstall/DoUninstall)НетНет
Версия и обновленияЕсть (version.php)НетНе ваше
Переносимость между проектамиВысокая, целикомКопированием файловНельзя
Свои таблицы БДДа, в установщикеВручную/миграциейОпасно
Обработчики событийRegisterModuleDependencesinit.php на каждый хитНельзя
Страница настроекoptions.phpНет штатнойНельзя
Переживает обновление платформыДаДаНет (затрётся)
Публикация в МаркетплейсеВозможнаНетНет
Порог входаВышеНиже—

Главный вывод из таблицы: правка ядра — всегда нет (её затрёт обновление, и это классический источник «сломалось после апдейта»); /local — для проектного и быстрого; модуль — для переиспользуемого и управляемого. Ниже — схема этих трёх уровней.

Три уровня кода на 1С-Битрикс Ядро /bitrix штатные модули main, iblock, sale не трогать правки затрёт обновление обновляется платформой Проект /local свои компоненты шаблоны, init.php классы проекта без установщика только этот сайт быстро и просто Свой модуль установщик версия, таблицы обработчики настройки, права переносится целиком изолирован от ядра ставится / снимается Правка ядра — никогда. /local — проектное. Модуль — переиспользуемое.
Схема: ядро править нельзя, проектную логику держат в /local, а переиспользуемую — в собственном модуле, который ставится и снимается целиком и переживает обновления платформы.

Идентификатор и место модуля в системе

Каждый модуль в Битриксе имеет уникальный идентификатор вида vendor.modulename — вендор (латиницей, ваша компания или ник) и имя модуля через точку. Например, b2bsite.delivery, vendor.shop. Этот идентификатор — имя папки модуля, значение MODULE_ID, префикс языковых сообщений и ключ настроек. Он должен быть глобально узнаваемым и не пересекаться с чужими модулями, поэтому в вендоре используют своё, а не общие слова.

Физически модуль лежит в одной из двух папок:

Оба варианта равноправны для установщика: Битрикс находит модуль по идентификатору в обеих папках. Для распространяемых модулей (Маркетплейс) используют /bitrix/modules/; для внутренних проектных — часто /local/modules/, чтобы всё «своё» лежало под /local. Важно: имя класса установки получается из идентификатора заменой точки на подчёркивание — для vendor.shop класс называется vendor_shop. Это соглашение жёсткое, ошибётесь в имени — модуль не установится.

Про namespace. Пространство имён классов модуля обычно строят из идентификатора: у vendor.shop классы живут в Vendor\Shop\... и лежат в lib/. Такое соответствие имени и пути делает автозагрузку предсказуемой — как это устроено в ядре, мы разбирали в статье про D7 и ORM в 1С-Битрикс. Здесь тот же принцип применяется к вашему коду.

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

У модуля есть каноническая структура. Не все папки обязательны — минимальный модуль состоит буквально из установщика и include.php, — но чем богаче функциональность, тем полнее раскладка. Вот типичная структура рабочего модуля:

/local/modules/vendor.shop/
├── install/
│   ├── index.php          # класс установки (DoInstall / DoUninstall)
│   ├── version.php        # версия и дата версии
│   ├── db/                # SQL-скрипты (install.sql / uninstall.sql) — опц.
│   ├── components/        # компоненты, копируемые при установке
│   ├── admin/             # админ-страницы модуля
│   └── themes/            # иконки/картинки модуля — опц.
├── lib/                   # классы модуля (namespace Vendor\Shop)
│   ├── deliverycalculator.php
│   └── eventhandlers.php
├── lang/                  # языковые файлы (ru, en)
│   └── ru/
│       ├── install/index.php
│       ├── options.php
│       └── include.php
├── include.php           # загружается при подключении модуля
├── options.php           # страница настроек модуля
├── prolog.php            # общий пролог модуля — опц.
└── .description.php      # метаданные для Маркетплейса — опц.

Разберём логику этой раскладки. Всё, что должно скопироваться в систему при установке (компоненты, админ-страницы), лежит в install/ — установщик переносит это в нужные системные папки, а при удалении убирает. Всё, что работает из самого модуля (классы, настройки, языки, точка входа), лежит в корне модуля и не копируется никуда. Это ключевое разделение: install/ — «дистрибутив», корень модуля — «рантайм».

Структура модуля vendor.shop install/ — дистрибутив копируется в систему при установке index.php — класс установки version.php — версия модуля db/ — SQL install / uninstall components/ — компоненты модуля admin/ — админ-страницы DoInstall переносит это в систему, DoUninstall — убирает обратно корень — рантайм модуля работает из папки модуля, не копируется include.php — точка входа lib/ — классы (namespace) options.php — настройки lang/ — языковые файлы .description.php — метаданные подключается через Loader::includeModule()
Схема: папка install/ — это «дистрибутив», содержимое которого установщик переносит в систему; корень модуля — «рантайм», который работает прямо из папки модуля и подключается через Loader::includeModule().

Таблица: файлы модуля и их роль

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

Файл / папкаРольОбязателен
install/index.phpКласс установки: DoInstall, DoUninstall, свойства модуляДа
install/version.phpМассив с VERSION и VERSION_DATEДа
include.phpЗагружается при includeModule: константы, автозагрузкаПрактически да
lib/Классы модуля с пространством имён Vendor\ShopПо необходимости
lang/ru/...Языковые сообщения (тексты интерфейса)Желательно
options.phpСтраница настроек модуля в админкеЕсли есть настройки
install/db/*.sqlSQL создания/удаления таблиц (классический путь)Альтернатива ORM
install/components/Компоненты, копируемые в систему при установкеЕсли есть компоненты
install/admin/Административные страницы модуляЕсли есть админка
.description.phpМетаданные для каталога/МаркетплейсаДля Маркетплейса
default_option.phpЗначения настроек по умолчаниюОпционально

Минимально жизнеспособный модуль — это install/index.php + install/version.php + include.php. Всё остальное подключается, когда появляется соответствующая потребность: таблицы, настройки, компоненты, языки.

version.php и .description.php

Начнём с самого простого — файлов метаданных. install/version.php хранит версию модуля и дату версии. Это обычный PHP-файл, возвращающий массив в переменную $arModuleVersion:

<?php
// /local/modules/vendor.shop/install/version.php
$arModuleVersion = [
    'VERSION'      => '1.0.0',
    'VERSION_DATE' => '2024-04-30 12:00:00',
];

Этот файл читает конструктор класса установки, чтобы заполнить MODULE_VERSION и MODULE_VERSION_DATE. При выпуске новой версии вы меняете здесь VERSION и синхронно VERSION_DATE — по ним система понимает, что появилось обновление.

Файл .description.php (с точкой в начале) описывает модуль для каталога решений и Маркетплейса: категория, теги, ссылки, партнёрская информация. Для внутренних модулей он необязателен, но если планируете публикацию — заполните его аккуратно:

<?php
// /local/modules/vendor.shop/.description.php
$arModuleDescription = [
    'NAME'        => 'Vendor: калькулятор доставки',
    'DESCRIPTION' => 'Расчёт стоимости доставки по своим правилам',
    'ICON'        => '/images/icon.gif',
    'PATH'        => '/vendor/modules/', // категория в каталоге решений
];

Отдельно стоит упомянуть default_option.php — файл со значениями настроек по умолчанию, которые применяются при установке, если явные значения ещё не заданы. Он не обязателен, но удобен, чтобы модуль сразу работал с разумными дефолтами.

install/index.php: класс установки

Сердце модуля — класс установки в install/index.php. Его имя формируется из идентификатора модуля заменой точки на подчёркивание (vendor.shop → vendor_shop), и он наследуется от CModule. В конструкторе класс читает версию и заполняет свойства, а два обязательных метода — DoInstall и DoUninstall — описывают установку и удаление.

<?php
// /local/modules/vendor.shop/install/index.php
use Bitrix\Main\Localization\Loc;

Loc::loadMessages(__FILE__);

class vendor_shop extends CModule
{
    public $MODULE_ID = 'vendor.shop';
    public $MODULE_VERSION;
    public $MODULE_VERSION_DATE;
    public $MODULE_NAME;
    public $MODULE_DESCRIPTION;
    public $PARTNER_NAME;
    public $PARTNER_URI;

    public function __construct()
    {
        $arModuleVersion = [];
        include __DIR__ . '/version.php';

        $this->MODULE_VERSION      = $arModuleVersion['VERSION'];
        $this->MODULE_VERSION_DATE = $arModuleVersion['VERSION_DATE'];
        $this->MODULE_NAME         = Loc::getMessage('VENDOR_SHOP_MODULE_NAME');
        $this->MODULE_DESCRIPTION  = Loc::getMessage('VENDOR_SHOP_MODULE_DESC');
        $this->PARTNER_NAME        = Loc::getMessage('VENDOR_SHOP_PARTNER_NAME');
        $this->PARTNER_URI         = 'https://example.com';
    }

    public function DoInstall()
    {
        global $APPLICATION;

        \Bitrix\Main\ModuleManager::registerModule($this->MODULE_ID);

        $this->InstallDB();
        $this->InstallEvents();
        $this->InstallFiles();

        $APPLICATION->IncludeAdminFile(
            Loc::getMessage('VENDOR_SHOP_INSTALL_TITLE'),
            __DIR__ . '/step_final.php'
        );
    }

    public function DoUninstall()
    {
        global $APPLICATION;
        $request = \Bitrix\Main\Context::getCurrent()->getRequest();

        $this->UnInstallEvents();
        $this->UnInstallFiles();

        // Таблицы удаляем только по явному согласию администратора
        if ($request->get('savedata') !== 'Y') {
            $this->UnInstallDB();
        }

        \Bitrix\Main\ModuleManager::unRegisterModule($this->MODULE_ID);

        $APPLICATION->IncludeAdminFile(
            Loc::getMessage('VENDOR_SHOP_UNINSTALL_TITLE'),
            __DIR__ . '/unstep_final.php'
        );
    }
}

Обратите внимание на структуру: тяжёлые шаги вынесены в отдельные методы (InstallDB, InstallEvents, InstallFiles и их зеркала), а DoInstall/DoUninstall лишь оркеструют их в правильном порядке. Такой код читается и, что важнее, удаление точно зеркалит установку — на каждый Install* есть свой UnInstall*. Здесь же используется ModuleManager::registerModule — современный D7-эквивалент старого RegisterModule (о нём — в следующем разделе).

Регистрация модуля: RegisterModule

Регистрация — это то, что делает модуль «видимым» для системы: он появляется в списке установленных модулей, и Loader::includeModule('vendor.shop') начинает возвращать true. Исторически для этого использовали пару функций RegisterModule / UnRegisterModule; в D7 те же действия выполняет Bitrix\Main\ModuleManager:

<?php
use Bitrix\Main\ModuleManager;

// Установка — модуль появляется в списке установленных
ModuleManager::registerModule('vendor.shop');

// Удаление — модуль исчезает из списка
ModuleManager::unRegisterModule('vendor.shop');

// Проверка, установлен ли модуль
if (ModuleManager::isModuleInstalled('vendor.shop')) {
    // ...
}

Физически регистрация — это запись идентификатора модуля в таблицу установленных модулей. После неё система знает про модуль и разрешает его подключение. Порядок в установщике важен: сначала регистрируем модуль, а уже потом создаём таблицы и обработчики — часть из них может ссылаться на модуль по идентификатору. При удалении порядок обратный: сначала снимаем всё, что модуль создал, и только в самом конце unRegisterModule. Если снять регистрацию раньше времени, зеркальные операции могут «не найти» модуль.

Тонкость про Loader и ModuleManager. Не путайте две вещи: ModuleManager::registerModule — это факт установки (модуль записан в список установленных), а Loader::includeModule('vendor.shop') — это подключение модуля на конкретном хите (загрузка его include.php и автозагрузки классов). Первое делают один раз при установке, второе — в каждом скрипте, где нужен модуль. Проверять результат includeModule — обязательная привычка.

InstallDB и свои таблицы

Если модуль хранит собственные данные, ему нужны свои таблицы. Классический путь — SQL-скрипты в install/db/: файл install.sql с командами создания таблиц и uninstall.sql с их удалением. Метод установщика выполняет их через соединение с базой:

<?php
public function InstallDB()
{
    $connection = \Bitrix\Main\Application::getConnection();

    // Выполнить пакет SQL-команд из файла (классический путь)
    $errors = $connection->getSqlHelper() ? null : null;
    $GLOBALS['DB']->RunSQLBatch(__DIR__ . '/db/install.sql');

    return true;
}

public function UnInstallDB()
{
    $GLOBALS['DB']->RunSQLBatch(__DIR__ . '/db/uninstall.sql');
    return true;
}

Сам install.sql — обычные CREATE TABLE под нужную СУБД:

CREATE TABLE IF NOT EXISTS vendor_shop_delivery_log (
    ID          INT(18) NOT NULL AUTO_INCREMENT,
    ORDER_ID    INT(18) NOT NULL,
    RATE        DECIMAL(18,2) NOT NULL DEFAULT 0,
    CREATED_AT  DATETIME NULL,
    PRIMARY KEY (ID),
    INDEX ix_order (ORDER_ID)
);

Этот путь рабочий, но у него есть минус: SQL живёт отдельно от кода, и структуру таблицы приходится описывать дважды — в .sql для создания и в классе-сущности для работы с данными. Поэтому на современном ядре предпочитают создавать таблицы прямо из описания ORM — одно описание сущности служит обеим целям. Об этом — следующий раздел.

Свои таблицы через ORM (DataManager)

Современный подход: описать таблицу классом-наследником DataManager в lib/, а в установщике создать таблицу по этому описанию. Так у вас единый источник правды о структуре — метод getMap(), — который используется и для создания таблицы, и для дальнейшей работы с данными через getList/add/update/delete.

Сначала — сущность в lib/:

<?php
// /local/modules/vendor.shop/lib/DeliveryLogTable.php
namespace Vendor\Shop;

use Bitrix\Main\ORM\Data\DataManager;
use Bitrix\Main\ORM\Fields\IntegerField;
use Bitrix\Main\ORM\Fields\FloatField;
use Bitrix\Main\ORM\Fields\DatetimeField;

class DeliveryLogTable extends DataManager
{
    public static function getTableName()
    {
        return 'vendor_shop_delivery_log';
    }

    public static function getMap()
    {
        return [
            new IntegerField('ID', ['primary' => true, 'autocomplete' => true]),
            new IntegerField('ORDER_ID', ['required' => true]),
            new FloatField('RATE', ['default_value' => 0]),
            new DatetimeField('CREATED_AT'),
        ];
    }
}

Теперь в InstallDB создаём таблицу из этого описания, а в UnInstallDB — удаляем:

<?php
use Bitrix\Main\Loader;
use Bitrix\Main\Application;
use Vendor\Shop\DeliveryLogTable;

public function InstallDB()
{
    Loader::includeModule($this->MODULE_ID);

    $connection = Application::getConnection();
    if (!$connection->isTableExists(DeliveryLogTable::getTableName())) {
        // Создаём таблицу по описанию getMap() сущности ORM
        DeliveryLogTable::getEntity()->createDbTable();
    }
    return true;
}

public function UnInstallDB()
{
    Loader::includeModule($this->MODULE_ID);

    $connection = Application::getConnection();
    if ($connection->isTableExists(DeliveryLogTable::getTableName())) {
        $connection->dropTable(DeliveryLogTable::getTableName());
    }
    return true;
}

Одно описание — обе операции. Дальше эта же сущность используется в бизнес-логике модуля: DeliveryLogTable::add([...]), ::getList([...]). Если вы ещё не работали с ORM, разбор DataManager, getMap, полей и связей — в отдельной статье про D7 и ORM в 1С-Битрикс.

Про удаление данных. Обратите внимание, что в DoUninstall мы вызываем UnInstallDB только если администратор не попросил сохранить данные (savedata != 'Y'). Это важная страховка: снятие модуля не должно молча уничтожать накопленные данные. Штатная форма удаления модуля предлагает чекбокс «сохранить таблицы» — уважайте этот выбор, иначе одно неосторожное удаление сотрёт логи или справочники безвозвратно.

include.php и автозагрузка классов

Файл include.php в корне модуля подключается автоматически при каждом Loader::includeModule('vendor.shop'). Его задача — подготовить рантайм модуля: объявить константы, зарегистрировать автозагрузку классов, подключить что-то общее. Чаще всего он регистрирует соответствие пространства имён и папки lib/, чтобы классы модуля грузились сами:

<?php
// /local/modules/vendor.shop/include.php
use Bitrix\Main\Loader;

// Полезные константы модуля
define('VENDOR_SHOP_MODULE_ID', 'vendor.shop');

// Регистрируем пространство имён модуля на папку lib/
Loader::registerAutoLoadClasses('vendor.shop', [
    // Полное имя класса => путь к файлу относительно модуля
    '\\Vendor\\Shop\\DeliveryLogTable'    => 'lib/DeliveryLogTable.php',
    '\\Vendor\\Shop\\DeliveryCalculator'  => 'lib/DeliveryCalculator.php',
    '\\Vendor\\Shop\\EventHandlers'       => 'lib/EventHandlers.php',
]);

Есть и более удобный способ, если вы придерживаетесь соглашения «класс Vendor\Shop\Foo лежит в lib/foo.php»: современное ядро умеет автозагружать классы модуля по namespace без ручного перечисления, если пространство имён совпадает с идентификатором модуля. Тогда include.php может быть совсем коротким. Но явная регистрация через registerAutoLoadClasses надёжна и понятна, особенно когда имена файлов не совпадают с классами один в один.

Итог: после includeModule все классы модуля доступны без единого require — точно так же, как классы ядра. Это и есть та изоляция и предсказуемость, ради которой оформляют модуль.

Языковые файлы lang/

Все тексты интерфейса модуля — названия, подписи полей настроек, сообщения установщика — выносят в языковые файлы, чтобы модуль поддерживал несколько языков и чтобы тексты не были «зашиты» в код. Языковые файлы лежат в lang/{язык}/ и повторяют структуру самого модуля: сообщения для options.php — в lang/ru/options.php, для установщика — в lang/ru/install/index.php.

<?php
// /local/modules/vendor.shop/lang/ru/install/index.php
$MESS['VENDOR_SHOP_MODULE_NAME']    = 'Vendor: калькулятор доставки';
$MESS['VENDOR_SHOP_MODULE_DESC']    = 'Расчёт доставки по своим правилам';
$MESS['VENDOR_SHOP_PARTNER_NAME']   = 'Vendor';
$MESS['VENDOR_SHOP_INSTALL_TITLE']  = 'Установка модуля';
$MESS['VENDOR_SHOP_UNINSTALL_TITLE']= 'Удаление модуля';

В коде эти сообщения читают через локализацию: Bitrix\Main\Localization\Loc::getMessage('VENDOR_SHOP_MODULE_NAME'), предварительно вызвав Loc::loadMessages(__FILE__). Ключевое соглашение — Битрикс сам подбирает языковой файл по пути к текущему файлу и активному языку: если код лежит в options.php, сообщения ищутся в lang/ru/options.php. Поэтому имена и структуру файлов важно держать параллельными.

Хорошая привычка — префиксовать ключи идентификатором модуля (VENDOR_SHOP_...). Языковые сообщения складываются в общий массив $MESS, и без префикса легко перезаписать чужой ключ и получить «съехавшие» подписи в чужом интерфейсе.

Регистрация обработчиков событий

Одна из главных причин делать именно модуль — надёжная регистрация обработчиков событий. В /local обработчики вешают в init.php через AddEventHandler, и это выполняется на каждом хите. Модуль регистрирует зависимость один раз при установке через RegisterModuleDependences — связка сохраняется в базе и работает, пока модуль установлен, без ежехитового объявления.

Классы-обработчики кладут в lib/, чтобы они подхватывались автозагрузкой:

<?php
// /local/modules/vendor.shop/lib/EventHandlers.php
namespace Vendor\Shop;

class EventHandlers
{
    // Обработчик события «после сохранения заказа»
    public static function onSaleOrderSaved(\Bitrix\Main\Event $event)
    {
        $order = $event->getParameter('ENTITY');
        // ... своя логика: лог, уведомление, интеграция
    }
}

Регистрация и снятие — в установщике, зеркальными методами:

<?php
public function InstallEvents()
{
    $em = \Bitrix\Main\EventManager::getInstance();

    $em->registerEventHandler(
        'sale',                              // модуль-источник события
        'OnSaleOrderSaved',                  // имя события
        $this->MODULE_ID,                    // наш модуль
        '\\Vendor\\Shop\\EventHandlers',     // класс обработчика
        'onSaleOrderSaved'                   // метод
    );
    return true;
}

public function UnInstallEvents()
{
    $em = \Bitrix\Main\EventManager::getInstance();

    $em->unRegisterEventHandler(
        'sale',
        'OnSaleOrderSaved',
        $this->MODULE_ID,
        '\\Vendor\\Shop\\EventHandlers',
        'onSaleOrderSaved'
    );
    return true;
}

Здесь EventManager::registerEventHandler — D7-эквивалент старого RegisterModuleDependences. Параметры зеркальны при установке и удалении — иначе в системе останется «висячий» обработчик, ссылающийся на удалённый модуль, и при событии вы получите ошибку «класс не найден». Как устроена событийная модель в целом, какие бывают события и чем отличается onBefore от onAfter — подробно разобрано в статье про события и обработчики в 1С-Битрикс.

options.php: страница настроек

Если у модуля есть настройки — ключи API, лимиты, флаги поведения, — им нужна страница в админке. Битрикс предоставляет для этого штатный механизм: файл options.php в корне модуля подключается в разделе «Настройки → Настройки продукта → Настройки модулей» при выборе вашего модуля. Вам не нужно верстать интерфейс с нуля — вы описываете вкладки и поля, а рендерит их платформа.

Значения хранятся в общей таблице настроек, привязанные к идентификатору модуля, и читаются/пишутся через Bitrix\Main\Config\Option:

<?php
// /local/modules/vendor.shop/options.php
use Bitrix\Main\Localization\Loc;
use Bitrix\Main\Config\Option;

Loc::loadMessages(__FILE__);

$moduleId = 'vendor.shop';
global $APPLICATION;

// Проверка прав текущего пользователя к модулю
$rights = $APPLICATION->GetGroupRight($moduleId);
if ($rights < 'R') {
    $APPLICATION->AuthForm(Loc::getMessage('ACCESS_DENIED'));
}

// Сохранение формы
if (
    $_SERVER['REQUEST_METHOD'] === 'POST'
    && check_bitrix_sessid()
    && $rights >= 'W'
) {
    Option::set($moduleId, 'API_KEY', trim($_POST['API_KEY'] ?? ''));
    Option::set($moduleId, 'MAX_RATE', (int)($_POST['MAX_RATE'] ?? 0));
}

// Текущие значения (с дефолтами)
$apiKey  = Option::get($moduleId, 'API_KEY', '');
$maxRate = Option::get($moduleId, 'MAX_RATE', 1000);
?>
<form method="post" action="<?= $APPLICATION->GetCurPage() ?>?mid=<?= htmlspecialcharsbx($moduleId) ?>&lang=<?= LANGUAGE_ID ?>">
    <?= bitrix_sessid_post() ?>
    <tr>
        <td><?= Loc::getMessage('VENDOR_SHOP_API_KEY') ?></td>
        <td><input type="text" name="API_KEY" value="<?= htmlspecialcharsbx($apiKey) ?>"></td>
    </tr>
    <input type="submit" name="apply" value="Сохранить">
</form>

В боевом коде настройки читают там, где они нужны, тем же Option::get('vendor.shop', 'API_KEY', ''). Значение по умолчанию в третьем аргументе — важная деталь: пока настройка не задана явно, модуль работает на дефолте, а не падает. Отдельную таблицу под настройки заводить не нужно — общая таблица b_option уже решает эту задачу.

Права модуля

Модуль может участвовать в общей системе прав доступа Битрикса — той самой, где группам пользователей выдаются уровни доступа к модулям (нет доступа, чтение, запись и т. д.). Чтобы система знала о ваших уровнях, модуль объявляет метод GetModuleRightList в классе установки:

<?php
// В классе установки vendor_shop
public function GetModuleRightList()
{
    return [
        'reference_id' => ['D', 'R', 'W'],
        'reference'    => [
            '[D] Доступ закрыт',
            '[R] Чтение настроек и данных',
            '[W] Полный доступ (запись, настройки)',
        ],
    ];
}

После этого в разделе прав доступа модуля администратор сможет назначать группам пользователей нужный уровень. В коде текущий уровень проверяют через $APPLICATION->GetGroupRight('vendor.shop') и сравнивают со строковыми константами ('D' < 'R' < 'W') — именно это мы делали выше в options.php, ограничивая сохранение настроек уровнем W.

Зачем это нужно. Права модуля — не формальность, а способ безопасно разграничить, кто может менять настройки и данные вашего модуля. Без них любой администратор с доступом к разделу получает полный контроль. Для модулей, которые работают с ключами интеграций, платёжными данными или важными справочниками, объявление уровней доступа — часть базовой гигиены безопасности.

Компоненты внутри модуля

Модуль может нести в себе компоненты — и это правильное место для компонентов, которые логически относятся к функциональности модуля (форма расчёта доставки, вывод данных из своих таблиц). Компоненты кладут в install/components/vendor.shop/, повторяя стандартную структуру компонента (class.php, .parameters.php, шаблоны), а установщик копирует их в систему.

Копирование делают в методе InstallFiles через файловые утилиты Битрикса:

<?php
public function InstallFiles()
{
    // Копируем компоненты модуля в системную папку компонентов
    CopyDirFiles(
        __DIR__ . '/components',
        \Bitrix\Main\Application::getDocumentRoot() . '/local/components',
        true, // перезаписывать
        true  // рекурсивно
    );
    return true;
}

public function UnInstallFiles()
{
    DeleteDirFiles(
        __DIR__ . '/components/vendor.shop',
        \Bitrix\Main\Application::getDocumentRoot() . '/local/components/vendor.shop'
    );
    return true;
}

Компонент из модуля вызывается по своему полному имени vendor.shop:delivery.form, как любой компонент. Внутри class.php он подключает модуль (Loader::includeModule('vendor.shop')) и пользуется его классами и настройками. Так функциональность и её визуальная часть путешествуют вместе, единым модулем. Как устроены сами компоненты, их параметры и шаблоны — в статье про шаблоны и компоненты 1С-Битрикс.

Альтернативный подход — не копировать компоненты в систему, а держать их прямо в модуле и регистрировать путь поиска компонентов. Оба варианта рабочие; копирование в install/components — более традиционный и понятный для установщика.

Агенты внутри модуля

Агент — это отложенная или периодическая задача Битрикса (аналог cron внутри платформы): «раз в час пересчитать остатки», «ежедневно чистить лог». Агенты удобно регистрировать в установщике модуля, чтобы фоновая логика ставилась и снималась вместе с модулем.

Метод-агент — статический метод класса модуля, который возвращает строку своего повторного вызова (так Битрикс переустанавливает агента на следующий запуск):

<?php
// В классе модуля, например lib/Agent.php
namespace Vendor\Shop;

class Agent
{
    public static function cleanupLog()
    {
        // ... периодическая работа: чистка старых записей лога
        DeliveryLogTable::deleteByFilter([
            '<CREATED_AT' => (new \Bitrix\Main\Type\DateTime())->add('-90 days'),
        ]);

        // Обязательно вернуть вызов самого себя — иначе агент не повторится
        return '\\Vendor\\Shop\\Agent::cleanupLog();';
    }
}

Регистрируют агента в установщике (или в InstallEvents-подобном шаге), а снимают при удалении:

<?php
use Bitrix\Main\Loader;

public function InstallAgents()
{
    Loader::includeModule($this->MODULE_ID);
    \CAgent::AddAgent(
        '\\Vendor\\Shop\\Agent::cleanupLog();', // функция агента
        $this->MODULE_ID,                        // модуль
        'N',                                     // не критичный к периоду
        86400                                    // интервал, секунд (сутки)
    );
    return true;
}

public function UnInstallAgents()
{
    \CAgent::RemoveModuleAgents($this->MODULE_ID); // снять все агенты модуля
    return true;
}

Удобная деталь: CAgent::RemoveModuleAgents снимает все агенты, привязанные к идентификатору модуля, одним вызовом — при удалении не нужно перечислять каждый. Ключевое правило агента — возвращать строку своего вызова; забудете return — агент отработает один раз и больше не запустится.

Полный цикл: установка и удаление

Соберём всё вместе. Установка модуля — это строгая последовательность шагов, и удаление — её точное зеркало в обратном порядке. Держать эту последовательность в голове важно: перепутаете порядок — что-то не найдёт модуль или таблицу.

Порядок установки (DoInstall):

Порядок удаления (DoUninstall) — обратный:

DoInstall: строгий порядок шагов 1. Register registerModule 2. InstallDB свои таблицы 3. InstallEvents обработчики 4. InstallFiles компоненты, agents DoUninstall — зеркало в обратном порядке Files → Events → DB (с согласия) → unRegisterModule снятие регистрации — всегда последним шагом
Схема: установка идёт по строгому порядку — регистрация модуля, затем таблицы, обработчики и файлы; удаление выполняет те же шаги зеркально в обратном порядке, а снятие регистрации всегда завершает процесс.

Создание модуля: пошагово

Теперь — практический порядок, как собрать рабочий модуль с нуля. Это тот маршрут, по которому удобно идти, чтобы ничего не забыть и получить корректную установку и удаление.

  1. Выберите идентификатор. Придумайте vendor.modulename с узнаваемым вендором. От него зависят имя папки, класс установки (vendor_modulename), namespace и префикс языковых ключей.
  2. Создайте структуру папок. Заведите install/, lib/, lang/ru/ и корневые файлы. Начните с минимума: install/index.php, install/version.php, include.php.
  3. Опишите версию и метаданные. Заполните version.php (VERSION, VERSION_DATE) и языковые сообщения для имени и описания модуля.
  4. Напишите класс установки. Наследник CModule с конструктором (чтение версии) и методами DoInstall/DoUninstall, оркеструющими шаги.
  5. Опишите свои сущности. Классы-наследники DataManager в lib/, а в InstallDB создайте таблицы через createDbTable (и dropTable в UnInstallDB).
  6. Настройте автозагрузку. В include.php зарегистрируйте классы модуля, чтобы они грузились без require.
  7. Добавьте обработчики, настройки, права. Классы обработчиков в lib/ и их регистрация в InstallEvents; options.php и GetModuleRightList, если модулю нужны настройки и разграничение доступа.
  8. Положите компоненты и агенты. Компоненты — в install/components/ с копированием в InstallFiles; агенты — с регистрацией и снятием по идентификатору модуля.
  9. Проверьте цикл установка → удаление → установка. Поставьте модуль, убедитесь, что всё создано; снимите — убедитесь, что всё откатилось; поставьте снова. Именно этот цикл выявляет незеркальные шаги.

Последний шаг — не формальность, а главный тест качества модуля. Если после удаления в базе остались таблицы или в системе «висят» обработчики, значит установка и удаление рассинхронизированы. Хороший модуль проходит цикл «поставил — снял — поставил» без следов и ошибок.

Версии и обновления модуля

Модуль живёт: вы добавляете поля в таблицы, новые обработчики, меняете настройки. Всё это нужно доставлять на уже установленные копии, не ломая данные. За это отвечает система версий и обновлений.

Базовая версия — в install/version.php. Поднимая её, вы сигнализируете системе о наличии обновления. Для распространяемых модулей обновления оформляют как апдейты: набор файлов и, при необходимости, скрипт updater.php, который донакатывает изменения структуры (новые таблицы, поля, обработчики) поверх текущей версии.

<?php
// Пример updater.php апдейта — донакатка нового поля и обработчика
if ($updater->CanUpdateKernel()) {
    // Копирование новых файлов модуля выполнит система обновлений;
    // здесь — только доработка структуры уже установленной версии.
}

use Bitrix\Main\Loader;
use Bitrix\Main\Application;

if (Loader::includeModule('vendor.shop')) {
    $connection = Application::getConnection();

    // Идемпотентно: добавляем поле только если его ещё нет
    if (!$connection->getTableField('vendor_shop_delivery_log', 'ZONE_ID')) {
        $connection->queryExecute(
            'ALTER TABLE vendor_shop_delivery_log ADD ZONE_ID INT(18) NULL'
        );
    }
}

Главное требование к логике обновления — идемпотентность: апдейт должен безопасно применяться поверх любой предыдущей версии и не ломаться при повторном запуске. Поэтому перед каждым изменением проверяют, не применено ли оно уже (поле существует? обработчик зарегистрирован?). Никогда не удаляйте данные в апдейтах молча и не полагайтесь на то, что пользователь стоит ровно на предыдущей версии — он может обновляться «через несколько».

Дисциплина версий. Меняете структуру или поведение — поднимайте версию и синхронно VERSION_DATE, а изменения фиксируйте в описании. Это не бюрократия: по версии администратор понимает, что установлено, а система обновлений — что накатывать. Рассинхрон версии и реального кода — источник самых запутанных багов «у меня работает, у него нет». Как встроить накатку модулей в процесс выкладки, мы затрагиваем в материале про CI/CD и деплой 1С-Битрикс.

Публикация в Маркетплейс

Если модуль задуман как продукт для распространения, финальный шаг — публикация в Маркетплейсе 1С-Битрикс. Это необязательный путь: множество модулей никогда не покидают периметр своих проектов и прекрасно живут как внутренние решения. Но если публикуете — держите в голове, что требований становится больше.

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

Модуль в работе: настройки и обработчики vendor.shop lib/ классы + логика Loader::includeModule options.php Option::get / set события ядра OnSaleOrderSaved свои таблицы DataManager (ORM) агенты периодические задачи настройки и обработчики регистрируются при установке, работают из модуля
Схема: во время работы модуль читает свои настройки через Option, реагирует на события ядра зарегистрированными обработчиками, а его классы из lib/ обслуживают собственные таблицы и агенты — всё в едином изолированном пространстве.

Частые ошибки

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

Чек-лист модуля

Короткий список для самопроверки и код-ревью нового модуля на Битриксе:

Вывод

Собственный модуль — это способ превратить разрозненный код в управляемую, переносимую единицу с понятным жизненным циклом. В отличие от правок в /local у него есть установщик, версия и чистое удаление; в отличие от компонента он изолирует крупную фоновую логику, а компоненты может носить внутри себя. Каноническая структура (install/index.php с классом установки, lib/ с классами и namespace, include.php, options.php, lang/, version.php, .description.php) и строгий порядок установки — регистрация, таблицы, обработчики, файлы — делают модуль предсказуемым, а зеркальное удаление возвращает систему в исходное состояние.

Практический стержень — дисциплина установщика: свои таблицы через ORM с единым описанием, обработчики через RegisterModuleDependences, настройки через Option, права через GetModuleRightList, агенты и компоненты — с корректной установкой и снятием, идемпотентные обновления и честная работа с данными пользователя. Модуль, прошедший цикл «поставил — снял — поставил» без следов, надёжен и внутри команды, и в Маркетплейсе. Если нужен такой модуль под вашу задачу — мы проектируем и пишем их в рамках разработки кастомных модулей 1С-Битрикс и разработки REST API для 1С-Битрикс. Смежные материалы по архитектуре — D7 и ORM, события и обработчики, шаблоны и компоненты и вся рубрика «Разработка и архитектура».

Частые вопросы

Что такое модуль в 1С-Битрикс простыми словами?

Модуль в 1С-Битрикс — это самостоятельная единица кода со своей структурой папок, установщиком и удалителем, которая регистрируется в системе под уникальным идентификатором вида vendor.modulename. Модуль умеет создавать свои таблицы в базе, регистрировать обработчики событий, добавлять страницу настроек, хранить собственные компоненты и агенты, объявлять права доступа. Ключевое отличие модуля от просто «файлов в /local» — наличие класса установки install/index.php с методами DoInstall и DoUninstall: модуль можно поставить и полностью снять одним действием в административной панели, а всё, что он создал (таблицы, обработчики, настройки), корректно откатывается. Именно поэтому переиспользуемую логику, которую вы хотите переносить между проектами или публиковать в Маркетплейсе, оформляют модулем, а не россыпью подключаемых файлов.

Чем свой модуль отличается от кода в /local и когда что выбирать?

Папка /local — это место для правок конкретного проекта: свои компоненты, шаблоны, обработчики в init.php, вспомогательные классы. Она не устанавливается и не удаляется как единое целое, не имеет версий и установщика, живёт только внутри одного сайта. Модуль — это упакованная, переносимая единица с установщиком, версией, языковыми файлами и чистым удалением. Правило простое: если логика нужна только этому проекту и никуда не переедет — держите её в /local, это быстрее и проще. Если логику нужно переиспользовать на нескольких проектах, передавать заказчику как готовый продукт, публиковать в Маркетплейсе или просто изолировать от ядра с понятным жизненным циклом установки и обновления — делайте модуль. Часто оба подхода сосуществуют: базовая логика в модуле, а мелкие проектные правки в /local.

Что обязательно должно быть в install/index.php модуля?

install/index.php содержит класс с именем вида vendor_modulename (идентификатор модуля, где точка заменена на подчёркивание), унаследованный от CModule. В конструкторе класс читает version.php и заполняет свойства: MODULE_ID, MODULE_VERSION, MODULE_VERSION_DATE, MODULE_NAME, MODULE_DESCRIPTION, а также данные партнёра. Обязательны два метода: DoInstall — он вызывается при установке и обычно делает RegisterModule, создаёт таблицы (InstallDB), регистрирует обработчики (InstallEvents) и копирует файлы (компоненты, админ-страницы); и DoUninstall — зеркальный метод, который снимает обработчики (UnInstallEvents), удаляет файлы, при необходимости удаляет таблицы (UnInstallDB) и в конце вызывает UnRegisterModule. Хорошая практика — выносить шаги в отдельные методы InstallDB/UnInstallDB, InstallEvents/UnInstallEvents, InstallFiles/UnInstallFiles, чтобы установщик читался и чтобы удаление было точным зеркалом установки.

Как модуль создаёт свои таблицы в базе данных?

Есть два подхода. Классический — держать SQL-скрипты в install/db/ (файлы install.sql и uninstall.sql под нужную СУБД) и выполнять их методом RunSQLBatch соединения с базой в InstallDB и UnInstallDB. Современный подход на ядре D7 — описать таблицу классом-наследником DataManager с методами getTableName и getMap, а в InstallDB получить сущность и вызвать createDbTable, который построит таблицу по описанию полей; в UnInstallDB — dropTable. ORM-подход предпочтителен: одно описание сущности служит и для создания таблицы, и для дальнейшей работы с данными через getList, add, update, delete. Важно: свои таблицы удаляйте в UnInstallDB только по явному подтверждению администратора — потеря пользовательских данных при снятии модуля должна быть осознанным выбором, поэтому обычно спрашивают согласие через параметр savedata.

Как модуль регистрирует обработчики событий и куда девать их при удалении?

Обработчики регистрируют в методе InstallEvents установщика через RegisterModuleDependences: указывают модуль-источник события, имя события, свой модуль, класс обработчика и метод. Эта связка сохраняется в базе и переживает перезагрузки — в отличие от AddEventHandler в init.php, который надо объявлять на каждом хите. Классы обработчиков кладут в lib/ модуля, чтобы они подхватывались автозагрузкой. При удалении в UnInstallEvents вызывают зеркальный UnRegisterModuleDependences с теми же параметрами — иначе в системе останутся «висячие» обработчики, ссылающиеся на удалённый модуль, и это приведёт к ошибкам. Регистрация обработчиков в установщике — как раз одна из причин делать именно модуль: события подключаются один раз при установке и снимаются при удалении, без ручного сопровождения init.php.

Как сделать страницу настроек модуля?

Страница настроек описывается файлом options.php в корне модуля. Битрикс подключает его в разделе «Настройки → Настройки продукта → Настройки модулей», выбрав ваш модуль по MODULE_ID. В options.php описывают массив вкладок и полей, читают и сохраняют значения через Option::get и Option::set (класс Bitrix\Main\Config\Option), обрабатывают отправку формы и проверяют права доступа текущего пользователя к модулю. Значения хранятся в общей таблице настроек b_option, привязанные к идентификатору модуля, поэтому отдельную таблицу под настройки заводить не нужно. В коде модуля эти значения затем читаются тем же Option::get(MODULE_ID, ключ, значение по умолчанию). Это стандартный, знакомый администраторам интерфейс, и делать собственную страницу настроек «с нуля» не требуется.

Как устроены версии и обновления модуля?

Версия модуля хранится в install/version.php в виде массива с ключами VERSION и VERSION_DATE. Конструктор класса установки читает этот файл и подставляет версию в MODULE_VERSION. Для распространяемых модулей (в том числе через Маркетплейс) обновления оформляют в папке install/updater или через систему обновлений: каждый апдейт — это набор файлов и, при необходимости, скрипт updater.php, который донакатывает изменения структуры (новые таблицы, поля, обработчики) на уже установленную версию. Логика обновления должна быть идемпотентной и аккуратной: проверять, не применён ли шаг ранее, не терять данные, уметь донакатываться поверх любой предыдущей версии. Поднимая версию, синхронно обновляйте VERSION_DATE и описание изменений, чтобы администратор в списке модулей видел актуальную информацию.

Обязательно ли публиковать модуль в Маркетплейсе?

Нет. Модуль — это в первую очередь способ организации кода, а не обязательно продукт для продажи. Многие студии и команды делают внутренние модули, которые никогда не попадают в Маркетплейс: они просто переносятся между своими проектами, ставятся из папки /bitrix/modules/ или /local/modules/ и снимаются штатным установщиком. Публикация в Маркетплейсе нужна, только если вы хотите распространять модуль публично — тогда добавляются требования к оформлению, безопасности, совместимости, проверке партнёрским аккаунтом и модерации. Для приватного или проектного использования достаточно корректного установщика и структуры. То есть освоить разработку модуля полезно всем, кто пишет на Битриксе серьёзно, независимо от планов на Маркетплейс.

Поделиться:

Нужен свой модуль для 1С-Битрикс?

Спроектируем и разработаем модуль под вашу задачу: аккуратный установщик и чистое удаление, свои таблицы через ORM, обработчики событий, настройки и права, компоненты и агенты внутри модуля. Переносимый, поддерживаемый код вместо россыпи правок в /local.

Игорь Воскресенский

Команда студии B2Bsite. С 2014 года разрабатываем, ускоряем и поддерживаем проекты на 1С-Битрикс: кастомные модули с аккуратными установщиками, современное ядро D7 и ORM, обработчики событий, REST API, кэш и обмен с 1С для среднего и крупного бизнеса.

← Все статьи блога