Свои номера документов и заявок - шаблон, счётчики, уникальность
Выдаём номера заявкам, счетам и своим документам штатным нумератором ядра: шаблон, независимые счётчики, правка конфигурации и свой генератор.
Механика
Нумератор ядра выдаёт последовательные номера по шаблону и гарантирует их уникальность. Гарантия действует и при одновременных запросах, поэтому две заявки не получат один номер даже в пиковую минуту распродажи.
Шаблон собирается из служебных слов, которые заменяются на значения при выдаче номера. Порядковый номер, год, случайная часть и постоянный префикс - это отдельные слова, и каждое обрабатывает свой генератор.
Набор доступных слов зависит от типа нумератора, а не выбирается произвольно. Список слов запрашивают у ядра по типу и показывают редактору, вместо того чтобы жёстко прописывать варианты в своём коде.
Конфигурация нумератора - это ассоциативный массив «тип генератора - его параметры». Начальное значение счётчика, шаг, длина случайной части и текст префикса задаются здесь же, рядом с самим шаблоном.
Созданный нумератор сохраняют один раз, а дальше правят отдельным методом обновления. Повторное сохранение существующего нумератора приводит к конфликтам при параллельных запросах и портит последовательность номеров.
Один нумератор умеет держать несколько независимых последовательностей по хешу. Хеш - произвольная строка вроде идентификатора менеджера или года, и каждая такая строка получает собственный счётчик с первого номера.
Номер заказа интернет-магазина формирует сам модуль продаж по своим настройкам. Нумератор ядра берут для остальных сущностей: заявок, счетов, обращений в поддержку и своих документов внутри проекта.
Свой генератор регистрируют событием сбора генераторов при инициализации ядра. Так в шаблон добавляют собственное служебное слово - например, код города или букву склада, откуда уходит документ.
Шаги
- Решить, из чего состоит номер: префикс, год, счётчик и случайная часть.
- Создать нумератор с шаблоном и параметрами генераторов один раз при установке.
- Загружать нумератор по идентификатору или по типу перед выдачей номера.
- Брать следующий номер и сохранять его вместе с самим документом.
- Развести независимые последовательности по хешу, если счётчиков нужно несколько.
- Править конфигурацию методом обновления, а не повторным сохранением нумератора.
Код
Создаём нумератор с шаблоном:
use Bitrix\Main\Numerator\Numerator;use Bitrix\Main\Numerator\Generator;
$numerator = Numerator::create();$numerator->setConfig([ Numerator::getType() => ['name' => 'Заявки с сайта', 'template' => '{PREFIX}-{YEAR}/{NUMBER}'], Generator\SequentNumberGenerator::getType() => ['start' => 1, 'step' => 1], Generator\PrefixNumberGenerator::getType() => ['prefix' => 'REQ'],]);$result = $numerator->save(); // создание выполняют один раз, при установкеИмя и шаблон - обязательные параметры самого нумератора. Остальные ключи массива описывают генераторы: каждый отвечает за своё служебное слово и получает собственные параметры вроде начального значения или длины.
Берём следующий номер:
$numerator = Numerator::load($numeratorId);$number = $numerator->getNext(); // например REQ-2026/000042$request->setNumber($number); // номер сохраняют вместе с документомНомер выдаётся один раз и сохраняется вместе с записью. Повторный вызов вернёт следующее значение, поэтому номер получают в момент создания документа, а не при каждом его показе на странице.
Разводим независимые счётчики по хешу:
echo $numerator->getNext('MANAGER_42'); // своя последовательность менеджераecho $numerator->getNext('MANAGER_17'); // независимый счётчик, снова с единицы// один нумератор держит сколько угодно параллельных последовательностейХеш решает частую задачу «свой счётчик на подразделение». Вместо десятка нумераторов с одинаковыми шаблонами держат один, а разделение обеспечивает строка хеша, собранная из идентификатора менеджера или города.
Правим конфигурацию существующего нумератора:
Numerator::update($numeratorId, [ Numerator::getType() => ['name' => 'Заявки с сайта', 'template' => '{PREFIX}-{YEAR}/{NUMBER}'], Generator\PrefixNumberGenerator::getType() => ['prefix' => 'ORD'],]);// повторный save() существующего нумератора ломает последовательность номеровПравка через метод обновления защищена от одновременных запросов. Это единственный поддерживаемый способ поменять шаблон или префикс у нумератора, который уже выдаёт номера в бою.
Показываем редактору доступные слова шаблона:
$words = Numerator::getTemplateWordsForType('DOCUMENT'); // список слов для типаprint_r($words);// набор слов зависит от типа нумератора и не совпадает у заказов и документовСписок слов запрашивают у ядра, а не переписывают в свой интерфейс руками. Тогда форма настройки шаблона не устаревает после обновления платформы и не предлагает редактору слово, которого в его типе нет.
Находим нумератор по типу при сохранении документа:
$numerator = Numerator::getOneByType('DOCUMENT');if ($numerator === null) { throw new \RuntimeException('нумератор документов не создан'); // защита от пустой установки}$document['NUMBER'] = $numerator->getNext();Поиск по типу избавляет от хранения идентификатора нумератора в коде. Проверка на отсутствие нужна всегда: на свежем стенде нумератор может быть не создан, и документ иначе сохранится без номера.
Сохраняем номер вместе с самим документом:
$connection = \Bitrix\Main\Application::getConnection();$connection->startTransaction();try { $number = $numerator->getNext(); // номер выдаётся ровно один раз RequestTable::add(['NUMBER' => $number, 'TITLE' => $title]); $connection->commitTransaction();} catch (\Throwable $e) { $connection->rollbackTransaction(); // документа нет, а номер уже потрачен}Откат транзакции возвращает данные, но не возвращает выданный номер. Пропуск в ряду номеров - нормальная плата за уникальность, и объяснить его бухгалтерии проще, чем разбирать два документа с одним номером.
Регистрируем свой генератор служебного слова:
$em = \Bitrix\Main\EventManager::getInstance();$em->addEventHandler('main', 'onNumberGeneratorsClassesCollect', static function () { return new \Bitrix\Main\EventResult( \Bitrix\Main\EventResult::SUCCESS, ['class' => \Vendor\Module\CityNumberGenerator::class]);});Свой генератор наследуют от базового класса генераторов номера. Он объявляет собственное служебное слово и подставляет значение, которого в поставке нет: код города, букву склада или обозначение канала продаж.
Ограничения
Нумератор выдаёт номер, но не хранит связь номера с документом. Сохранение номера в поле своей сущности - ваша задача, и потерянный номер в последовательности уже не вернётся.
Смена шаблона не переписывает выданные раньше номера. В базе останутся записи старого вида, и отчёты по номерам придётся строить с учётом обоих форматов сразу.
Номер заказа магазина живёт по своим правилам модуля продаж. Подменять его нумератором ядра не стоит: обмен с учётной системой и печатные формы рассчитывают на штатное поле номера.
Случайная часть в шаблоне не заменяет уникальности счётчика. Она затрудняет подбор чужого номера в публичной ссылке, но сама по себе не гарантирует отсутствия совпадений.
Печатные формы и обмен с учётной системой знают только тот номер, который лежит в поле документа. Поэтому номер сохраняют сразу при создании записи, а не собирают заново при выводе на печать или выгрузке.
Типичные проблемы
Два документа получили одинаковый номер.
Номер собирается своим кодом по количеству записей в таблице. Такой подход ломается при одновременных запросах, а нумератор ядра защищён от этого.
После правки шаблона счётчик начался заново.
Существующий нумератор сохранили повторно вместо вызова обновления. Конфигурацию правят методом обновления, который не создаёт нумератор заново.
В шаблоне не работает нужное служебное слово.
Слово недоступно для этого типа нумератора или не подключён его генератор. Список слов запрашивают у ядра по типу нумератора.
Номера у разных менеджеров идут вперемешку.
Все документы берут номер из общей последовательности без хеша. Отдельные счётчики получают передачей строки хеша при выдаче номера.
Документ сохранился с пустым номером.
Нумератор не создан на этом стенде, а результат поиска по типу не проверен. Отсутствие нумератора обрабатывают явной ошибкой при сохранении.
Частые вопросы
Можно ли поменять формат номера заказа магазина?
Номер заказа формирует модуль продаж по своим настройкам, и его меняют там. Нумератор ядра берут для своих сущностей: заявок, счетов, обращений.
Как сделать сквозную нумерацию по годам?
Добавить слово года в шаблон и завести отдельный счётчик по хешу для каждого года. Тогда с началом года последовательность стартует заново.
Гарантируется ли уникальность при большой нагрузке?
Да, последовательные номера уникальны и при одновременных запросах. Самодельный счётчик по числу записей такой гарантии не даёт вовсе.
Где хранится состояние счётчика?
В таблицах ядра вместе с конфигурацией нумератора. Отдельного файла или своей таблицы для этого заводить не нужно.
Можно ли добавить своё слово в шаблон?
Да, своим генератором, зарегистрированным на событии сбора генераторов. Он объявляет слово и подставляет нужное значение при выдаче номера.
Смежное
- Заказы и статусы - оглавление подтемы
- Заказ из кода: создание, оплата, отгрузка - где пригодится свой номер документа
- Свойства заказа: свои поля, вывод, поиск - куда сохраняют такой номер
- Своя таблица на ORM: сущность, запросы, изменение структуры - хранилище своих документов
- Транзакции: когда нужны и как не сломать данные - сохранение номера вместе с записью
- Сервисы ядра D7 - устройство подсистем ядра
- Каталог и продажи - устройство магазина целиком