Рано или поздно у любого, кто серьёзно пишет на 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) | Нет | Не ваше |
| Переносимость между проектами | Высокая, целиком | Копированием файлов | Нельзя |
| Свои таблицы БД | Да, в установщике | Вручную/миграцией | Опасно |
| Обработчики событий | RegisterModuleDependences | init.php на каждый хит | Нельзя |
| Страница настроек | options.php | Нет штатной | Нельзя |
| Переживает обновление платформы | Да | Да | Нет (затрётся) |
| Публикация в Маркетплейсе | Возможна | Нет | Нет |
| Порог входа | Выше | Ниже | — |
Главный вывод из таблицы: правка ядра — всегда нет (её затрёт обновление, и это классический источник «сломалось после апдейта»); /local — для проектного и быстрого; модуль — для переиспользуемого и управляемого. Ниже — схема этих трёх уровней.
Идентификатор и место модуля в системе
Каждый модуль в Битриксе имеет уникальный идентификатор вида vendor.modulename — вендор (латиницей, ваша компания или ник) и имя модуля через точку. Например, b2bsite.delivery, vendor.shop. Этот идентификатор — имя папки модуля, значение MODULE_ID, префикс языковых сообщений и ключ настроек. Он должен быть глобально узнаваемым и не пересекаться с чужими модулями, поэтому в вендоре используют своё, а не общие слова.
Физически модуль лежит в одной из двух папок:
/bitrix/modules/vendor.modulename/— «системное» размещение, как у штатных модулей;/local/modules/vendor.modulename/— «проектное» размещение с приоритетом; удобно, когда модуль относится к конкретному сайту и вы не хотите класть его в/bitrix.
Оба варианта равноправны для установщика: Битрикс находит модуль по идентификатору в обеих папках. Для распространяемых модулей (Маркетплейс) используют /bitrix/modules/; для внутренних проектных — часто /local/modules/, чтобы всё «своё» лежало под /local. Важно: имя класса установки получается из идентификатора заменой точки на подчёркивание — для vendor.shop класс называется vendor_shop. Это соглашение жёсткое, ошибётесь в имени — модуль не установится.
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/ — «дистрибутив», корень модуля — «рантайм».
Таблица: файлы модуля и их роль
Чтобы держать раскладку в голове, вот назначение ключевых файлов. Обязательные помечены отдельно — остальное добавляется по мере роста функциональности.
| Файл / папка | Роль | Обязателен |
|---|---|---|
install/index.php | Класс установки: DoInstall, DoUninstall, свойства модуля | Да |
install/version.php | Массив с VERSION и VERSION_DATE | Да |
include.php | Загружается при includeModule: константы, автозагрузка | Практически да |
lib/ | Классы модуля с пространством имён Vendor\Shop | По необходимости |
lang/ru/... | Языковые сообщения (тексты интерфейса) | Желательно |
options.php | Страница настроек модуля в админке | Если есть настройки |
install/db/*.sql | SQL создания/удаления таблиц (классический путь) | Альтернатива 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. Если снять регистрацию раньше времени, зеркальные операции могут «не найти» модуль.
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):
- 1. Регистрация модуля —
ModuleManager::registerModule. Модуль появляется в списке установленных, дальше его можно подключать. - 2. Создание таблиц —
InstallDB. Свои таблицы через ORM (createDbTable) или SQL. - 3. Регистрация обработчиков —
InstallEvents. Связки событий черезEventManager. - 4. Агенты —
InstallAgents. Периодические задачи модуля. - 5. Копирование файлов —
InstallFiles. Компоненты и админ-страницы в систему.
Порядок удаления (DoUninstall) — обратный:
- 1. Файлы —
UnInstallFiles(убрать компоненты и админку). - 2. Агенты —
UnInstallAgents. - 3. Обработчики —
UnInstallEvents. - 4. Таблицы —
UnInstallDB, но только с согласия администратора (savedata). - 5. Снятие регистрации —
ModuleManager::unRegisterModuleв самом конце.
Создание модуля: пошагово
Теперь — практический порядок, как собрать рабочий модуль с нуля. Это тот маршрут, по которому удобно идти, чтобы ничего не забыть и получить корректную установку и удаление.
- Выберите идентификатор. Придумайте
vendor.modulenameс узнаваемым вендором. От него зависят имя папки, класс установки (vendor_modulename), namespace и префикс языковых ключей. - Создайте структуру папок. Заведите
install/,lib/,lang/ru/и корневые файлы. Начните с минимума:install/index.php,install/version.php,include.php. - Опишите версию и метаданные. Заполните
version.php(VERSION,VERSION_DATE) и языковые сообщения для имени и описания модуля. - Напишите класс установки. Наследник
CModuleс конструктором (чтение версии) и методамиDoInstall/DoUninstall, оркеструющими шаги. - Опишите свои сущности. Классы-наследники
DataManagerвlib/, а вInstallDBсоздайте таблицы черезcreateDbTable(иdropTableвUnInstallDB). - Настройте автозагрузку. В
include.phpзарегистрируйте классы модуля, чтобы они грузились безrequire. - Добавьте обработчики, настройки, права. Классы обработчиков в
lib/и их регистрация вInstallEvents;options.phpиGetModuleRightList, если модулю нужны настройки и разграничение доступа. - Положите компоненты и агенты. Компоненты — в
install/components/с копированием вInstallFiles; агенты — с регистрацией и снятием по идентификатору модуля. - Проверьте цикл установка → удаление → установка. Поставьте модуль, убедитесь, что всё создано; снимите — убедитесь, что всё откатилось; поставьте снова. Именно этот цикл выявляет незеркальные шаги.
Последний шаг — не формальность, а главный тест качества модуля. Если после удаления в базе остались таблицы или в системе «висят» обработчики, значит установка и удаление рассинхронизированы. Хороший модуль проходит цикл «поставил — снял — поставил» без следов и ошибок.
Версии и обновления модуля
Модуль живёт: вы добавляете поля в таблицы, новые обработчики, меняете настройки. Всё это нужно доставлять на уже установленные копии, не ломая данные. За это отвечает система версий и обновлений.
Базовая версия — в 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С-Битрикс. Это необязательный путь: множество модулей никогда не покидают периметр своих проектов и прекрасно живут как внутренние решения. Но если публикуете — держите в голове, что требований становится больше.
- Оформление. Заполненный
.description.php, корректные названия и описания в языковых файлах, иконка, скриншоты, документация. - Совместимость. Модуль должен корректно ставиться и сниматься на поддерживаемых редакциях и версиях платформы, не конфликтовать с ядром и типовыми решениями.
- Безопасность. Экранирование ввода/вывода, проверка прав и
sessidв формах, отсутствие произвольного выполнения кода и SQL-инъекций. Это проверяется при модерации. - Обновления. Работающая система апдейтов, идемпотентные скрипты, аккуратное обращение с данными пользователя.
- Партнёрский аккаунт и модерация. Публикация идёт через партнёрский кабинет, а решение проходит проверку перед появлением в каталоге.
Даже если публикация не в планах, полезно проектировать модуль так, будто его увидят чужие глаза: чистое удаление, безопасные формы, понятные настройки, идемпотентные обновления. Такой модуль надёжен и внутри вашей команды. Публикация же — это скорее про упаковку и соответствие требованиям каталога, чем про переписывание архитектуры. Если хотите превратить внутреннее решение в продукт для Маркетплейса, это отдельная работа по доводке, которую мы делаем в рамках разработки кастомных модулей.
Частые ошибки
Разработка модуля порождает свой набор типовых граблей. Зная их заранее, вы сэкономите часы отладки, особенно на этапе установки и удаления.
- Имя класса установки не совпадает с идентификатором. Для
vendor.shopкласс обязан называтьсяvendor_shop(точка → подчёркивание). Ошиблись — модуль не установится, и система не подскажет внятно почему. - Удаление не зеркалит установку. Зарегистрировали обработчик, но не сняли; создали таблицу, но не удалили. После снятия модуля остаются «висячие» обработчики и осиротевшие таблицы. На каждый
Install*— свойUnInstall*. - Снятие регистрации раньше времени. Вызвали
unRegisterModuleв началеDoUninstall— и последующие шаги не могут подключить модуль и найти его классы. Регистрацию снимают последней. - Молчаливое удаление данных.
UnInstallDBбез проверки согласия администратора стирает накопленные данные при снятии модуля. Всегда уважайте флаг «сохранить таблицы». - Забытый
returnв агенте. Метод-агент не вернул строку своего вызова — агент отработал один раз и больше не запускается. Всегда возвращайте'\\Vendor\\Shop\\Agent::method();'. - Тексты «зашиты» в код. Подписи и сообщения прямо в PHP вместо языковых файлов — модуль не локализуется, а ключи без префикса модуля затирают чужие. Выносите в
lang/с префиксом. - Обработчики в init.php вместо установщика. Смысл модуля теряется, если его события вешаются в
init.phpпроекта. Регистрируйте зависимости вInstallEvents— тогда они ставятся и снимаются вместе с модулем. - Настройки в своей таблице. Заводить таблицу под пару настроек — лишнее. Используйте
Option::get/setи общую таблицу настроек. - Неидемпотентные обновления. Апдейт, который падает при повторном запуске или предполагает строго предыдущую версию. Проверяйте состояние перед каждым изменением структуры.
- Правка ядра вместо модуля. Дописать логику прямо в
/bitrix/modules/— значит потерять её при обновлении платформы. Всё своё — в свой модуль или в/local.
Чек-лист модуля
Короткий список для самопроверки и код-ревью нового модуля на Битриксе:
- Идентификатор вида
vendor.modulenameс узнаваемым вендором; класс установки названvendor_modulename. - Есть
install/index.php(класс сDoInstall/DoUninstall) иinstall/version.phpс версией и датой. - Установка регистрирует модуль (
registerModule) до создания таблиц и обработчиков; снятие регистрации — последним шагом. - Свои таблицы описаны сущностью ORM (
DataManager+getMap) и создаются черезcreateDbTable, удаляются черезdropTable. - Удаление точно зеркалит установку: на каждый
Install*естьUnInstall*. - Удаление таблиц спрашивает согласие администратора (
savedata) — данные не стираются молча. - Обработчики зарегистрированы в
InstallEvents(EventManager) и сняты вUnInstallEventsзеркально. include.phpрегистрирует автозагрузку классов модуля; ручныхrequireнет.- Тексты вынесены в
lang/с префиксом идентификатора модуля; интерфейс локализуем. - Настройки — через
options.phpиOption::get/set; формы проверяют права иsessid. - Права модуля объявлены через
GetModuleRightList, если модуль работает с чувствительными данными. - Агенты снимаются при удалении (
RemoveModuleAgents); методы-агенты возвращают строку своего вызова. - Обновления идемпотентны и не теряют данные; версия и
VERSION_DATEсинхронны с реальным кодом. - Пройден цикл «поставил → снял → поставил» без остатков в базе и ошибок.
Вывод
Собственный модуль — это способ превратить разрозненный код в управляемую, переносимую единицу с понятным жизненным циклом. В отличие от правок в /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, события и обработчики, шаблоны и компоненты и вся рубрика «Разработка и архитектура».