Модуль для маркетплейс — от идеи до старта продаж. Часть 10 — Бизнес логика

На этот раз речь пойдет о бизнес-логике модуля. Это, пожалуй, самая важная часть. Это то, без чего смысла в модуле вообще никакого нет. Бизнес-логика должна решать реальную задачу, что в свою очередь нацелено на извлечение прибыли. Ей и займемся, но сначала ….

Инсталлятор

Инсталлятор, черт побери! Я уже писал ранее, что к нему скорее всего придется возвращаться время от времени. И вот время снова пришло 🙂 Я тут в процессе изысканий обнаружил очередное неочевидное поведение в ядре (почему меня это не удивляет?). При описании ORM сущности мы должны создать метод getMap, который должен вернуть массив с описанием всех полей таблицы сущности. В моем модуле используется две сущности — DiscountCoupon и Process. Последняя сущность содержит поля с датами, одно из которых, в соответствии с описанием, должно по дефолту иметь значение null. Когда я задаю сущности дефолтное значение null, я ожидаю, что ORM сгенерирует мне таблицу с колонкой, в которую можно записать NULL’able значения. И каково было моё удивление, когда я попытался создать с помощью ORM запись в этой таблице со значением NULL для этого поля, но ORM вывалила мне Exception.

А дело вот в чем. Для создания таблицы в d7 используется метод \Bitrix\Main\DB\Connection::createTable. Этот метод на основании массива полей сущности создает таблицу. Если заглянуть в исходники, то можно увидеть, что там захардкодили запрет на создание NULL’able значений для всех полей. Возможно в будущем это изменится, поэтому вставлю скрином:

Если кто-то знает причины, которыми руководствовались разработчики при написании этого кода — подскажите. Причем тут этот комментарий в коде — я тоже не понял. На звание гуру в теории реляционных СУБД я ну никак не претендую, да и с mysql изнутри тоже знаком не особо. Чем тут может помешать наличие NULL значений? Не понятно ..

Если посмотреть тот же метод в реализации для других СУБД, то для MSSQL наблюдается аналогичная картина, а вот для Oracle выбор между NULL и NOT NULL осуществляется в зависимости от того, является ли поле первичным ключом. В общем — мне не понятно. Если кто-то может истолковать — будьте добры, в комментах.

В связи с этой ерундистикой пришлось изящный createTable заменить на прямой запрос CREATE TABLE … Ну да ладно …

Коллекции

Для того, чтобы генерировать какие-то последовательности символов, нам нужно определить наборы символов, которые будут использоваться в будущем для генерации. На этапе создания ТЗ я описал несколько разных классов последовательностей. Был определен базовый интерфейс SymbolsCollectionInterface, реализовать который должны все коллекции. Есть еще дополнительный интерфейс LettersCollectionInterface, который описывает коллекцию букв, необходимость которого заключается в том, чтобы отличать буквы от других последовательностей, а также иметь возможность работать с регистром букв. В итоге были реализованы коллекции для чисел, букв русского и английского алфавита, спец-символов, а также специальная коллекция, в которую можно собрать несколько других коллекций. Вся иерархия реализованных коллекций может быть представлена на диаграмме в следующем виде:

Реализация этих классов довольно тривиальна.

Шаблон последовательности

Помимо коллекций символов нам понадобится шаблон. Можно было бы пойти по более простому пути — указать в настройках количество купонов, количество символов в каждом купоне и формировать все купоны на основании этой настройки, но эта штука не является достаточно гибкой. Поэтому я решил сразу ввести понятие шаблона. В будущем эту концепцию можно будет развить (и у меня уже есть планы), но на старте этого будет более чем достаточно.

Шаблон купона будет представлять из себя набор символов, где часть символов является статическими, а часть — генерируемыми. Если в шаблоне будет присутствовать какой-то заранее предопределенный символ, являющийся генерируемым, то он будет заменен на рандомный символ из ассоциированной этому символу коллекции. В остальном же это обычная строка.

Класс для составления шаблона принимает в конструкторе строку, а также предоставляет интерфейс, который позволяет добавить к шаблону соответствие между символом и коллекцией. Это позволит составлять шаблоны программно на лету.

Структура классов и реализацию можно посмотреть в неймспейсе \Maximaster\Coupanda\Generator\Template

Генератор последовательностей

Задача генератора сводится к тому, чтобы сформировать одну или несколько последовательностей на основании настроек шаблона. При генерации необходимо предусмотреть обработку уникальных и не уникальных значений. Если шаблон содержит всего 3 генерируемых цифры, то количество последовательностей не будет превышать 1000 штук (основная формула комбинаторики — 10*10*10). И если генератору дать задание генерить более 1000 значений, генератор должен адекватно реагировать на это.

Но тут встает другая проблема — если ввести достаточно длинный шаблон, то количество комбинаций запросто перевалит за PHP_INT_MAX. К счастью, php легко справится с достаточно большими числами, просто переведя количество во float. Но генератор должен давать возможность генерировать уникальные значения в рамках одного хита, а поэтому все сгенерированные купоны придется хранить в оперативке в виде хеш таблицы (читай — массива). Но на этапе запуска нам не нужно особо думать о том, чтобы давать возможность в рамках одного хита формировать огромные количества купонов. Поэтому просто ограничим максимальное количество последовательностей, сгенерированных за одно обращение к генератору каким-то достаточно большим числом.

На самом деле, проблем тут куда больше, и если копнуть достаточно глубоко, то можно ооочень сильно и ооочень надолго закопаться. Но мы просто не будем об этом думать. Вероятность, что мы наткнемся на проблемы — слишком мала (хоть по закону Мёрфи, это когда-нибудь и случится). Но я пока решил этим всем пренебречь и просто ввести ограничения.

Генератор купонов

На основании генератора последовательностей уже можно сделать и генератор купонов. Задача генератора купонов — обратиться к генератору последовательностей за набором последовательностей и добавить их в БД, а также проконтролировать уникальность добавляемых в БД записей. Все это нужно делать на основании настроек, которые указал пользователь в форме. Класс совсем простой и маленький получается.

Для общения с таблицей купонов у нас есть ORM сущность DiscountCoupon, которая наследуется от битриксовой одноименной сущности.

Процесс

На прошлых этапах я выделил 3 сущности, которые будут описывать сам процесс и все вспомогательные для него данные — ProcessSettings, ProcessProgress и ProcessReport. Начав реализовывать первый концепт, я сделал работу с ProcessProgress с помощью сессии, а ProcessSettings являлся одним из полей ProcessProgress. Однако немного поработав с этим стало понятно, что будет удобнее в качестве хранилища для прогресса использовать СУБД, а сама сущность Process, которую я уже заранее определил, по сути и является этим самым хранилищем для прогресса. В связи с этим я просто избавился от ProcessProgress в пользу класса Process, о котором чуть подробнее.

Процесс в понятиях моего модуля — это как некая книга учета событий. Если нам нужно запланировать какое-то событие, то мы создаем запись в календаре, что нам нужно начать его 15го числа такого-то месяца в 12 часов дня. Событию сопутствуют некоторые параметры — в моем случае это параметры генерации купона, но могут быть и другие параметры. Кроме этого событие имеет в себе флаг состояния (в моем случае — количество обработанных процессом купонов).

Каждый новый процесс создается в тот момент, когда администратор модуля нажимает кнопку «Начать генерацию». В БД заводится запись о новом процессе, которая хранит в себе все заданные пользователем настройки. А компонент, который работает с этим процессом, обращается к БД и обновляет процесс в БД по мере его выполнения.

Сам по себе процесс и является неким отчетом — он содержит данные о начале и окончании процесса, а также о количестве обработанных купонов.

Для удобства работы с процессом нужно представить его в виде класса. Однако класс этот связан с БД напрямую, значит я должен использовать ORM сущность. Но использовать ORM сущности неудобно в битриксе для хранения бизнес-логики из-за их статической природы и связи с БД, поэтому я решил создать прослойку, которая позволит более-менее удобно обращаться к данным из БД.

Репозиторий процессов

Смысл в том, что работать с массивами менее удобно, чем с объектами. Поэтому нужен какой-то слой, который будет преобразовывать массивы с описанием процессов в объекты процессов.

Кроме этого, перед преобразованием в объекты, данные нужно выбрать из БД. Возложим это все на наш «репозиторий», который будет являться реализацией паттерна DataMapper.

Задачи репозитория процессов сводятся к тому, чтобы:

  • искать сущности в БД
  • преобразовывать ORM сущности в объекты процессов (и отдавать их после поиска), и наоборот
  • сохранять объекты процессов в БД

Класс репозитория тоже получается довольно простым:

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

Настройки процесса — это дополнительный, пока не особо структурированный объект, поэтому он хранится в БД в виде сериализованной строки (если у вас нет на то реальных причин — не делайте так в РСУБД!).

Ну и класс ProcessReport я удалил пока за ненадобностью. Возможно в будущем еще вернусь к нему, если мне понадобятся расширенные отчеты.

Таким образом, удалось уменьшить количество кода и разделить обязанности между классами, увеличив читабельность и удобство в поддержке.

Компонент

Для того, чтобы воплотить все наработки в жизнь, нужно лишь в компонент генератора добавить обращение к разработанным классам. В обработчике действия generation_start — создаем новый процесс, сохраняем туда дату начала и настройки.

В generation_process готовим инстанс шаблона, созданный из настроек, готовим генератор последовательностей на основе этого шаблона и достаем инстанс процесса, который был создан на предыдущем шаге. Все это скармливаем в CouponGenerator и в цикле генерируем по несколько купонов до достижения лимита.

В generation_finish сохраняем процесс в БД с датой окончания.

Естественно все это сопровождается валидацией и обработкой ошибок.

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

Превью

Ну и осталось сделать только функцию превью. Тут достаточно обратиться к генератору последовательностей и сформировать несколько последовательностей (можно даже за уникальностью не следить) на основании заданного шаблона купона. Кода вообще минимум. Результат генерации выводится на фронтенде в отдельное всплывающее окно.

Выводы

На данный момент у нас уже есть полностью работоспособная версия модуля, которая уже может быть установлена и использована на проектах с кодировкой utf-8. На 100% уверен, что данная версия не является стабильной, и содержит набор багов. Но это еще совсем не конец разработки, еще много работы — это и управление правами доступа, логирование, локализация, документация, тестирование и исправление косяков. Кроме этого нужно еще разработать какой-то инструментарий для автоматической сборки обновлений в маркетплейс и запуска автотестов, сборки документации. А еще надо логотип нарисовать:) Так что впереди еще много интересного, следите за обновлениями в RSS и подписывайтесь на email рассылку! Исходники, как обычно, на гитхабе.