Разработка архитектуры и составление технического задания - это, пожалуй, самый сложный этап работ во всем процессе. Особенно важным этот этап становится при разработке особо крупных и сложных проектов, т.к. на нем закладываются базовые принципы взаимодействия подмодулей системы, гибкость и расширяемость, структуры данных; если что-то на этом этапе не додумать и не доработать, то в будущем это может аукнуться серьезными переработками и массовыми вливаниями средств.
Но, у нас фан-проект 🙂 сурового энтерпрайза тут нет и не предвидится. Да и команды разработки у нас нет, у нас всего один человек в штате 🙂 Так и хочется сказать, что «мы забьем на этот этап», но нет, все же надо заложить какой-то фундамент, чтобы потом от него уже строить код.
Сперва - очевидное.
Раз уж мы разрабатываем модуль для 1С-Битрикс, да который еще и на маркетплейсе будет размещаться, значит мы волей-неволей должны реализовывать ту структуру модуля, которую нам навязывает Битрикс. А значит мы должны построить файловую структуру модуля, описанную в документации, и реализовать инсталлятор/деинсталлятор средствами платформы.
Модуль «стороннего разработчика» по правилам cms должен иметь вендорный префикс. В нашем случае это будет maximaster (по названию компании, в которой я работаю)
Кроме этого, модуль должен иметь собственное название. Один товарищ как-то ляпнул:
В программировании есть две сложных вещи: инвалидация кэша и именование.
Выбрать хорошее название для такого модуля - реально сложно. Можно не заморачиваться и назвать модуль, основываясь на его функционале - coupon_generator или coupon_extension. Но я такие сухие названия не люблю, люблю что-нибудь прикольное и забавное.
А еще меня забавляют панды 🙂 Кого не забавляют панды?
Получается, с одной стороны у нас есть сухие и всем надоевшие купоны, а с другой - милые и смешные панды. Тупо мешаем два слова и получается крутой бренд - CouPanda! И на русском забавно - КуПанда, сразу можно много значений придать 🙂 Пускай будет купанда, а потом может и переиграем, но мне уже нравится 🙂 Логотипчик сам собой напрашивается ))
Казалось бы, нахрена нам название в архитектуре? А все просто - все названия классов нужно строить от вендорного префикса. Все пространства имен должны называться с вендора и имени библиотеки (как в битриксе, так и вообще в принципе), поэтому выбранные слова будут сопровождать нас везде.
Обозначаем базовый вендорный префикс для всех пространств имен - \Maximaster.
Неймспейс имени модуля - \Coupanda.
Значит имена всех классов должны начинаться с \Maximaster\Coupanda 🙂 все просто
Сам модуль с точки зрения битрикса будет именоваться maximaster.coupanda. Директорию с модулем здесь и далее я буду называть сокращенно %module_root%. По правилам Битрикса эта директория может находиться как в /bitrix/modules, так и в /local/modules (после установки из маркетплейса тут её быть физически не может, но мы в теории можем туда руками его залить). Поэтому важно, чтобы на работу модуля не влияло его местоположение в системе.
Погнали все это дело формализовывать.
Схема БД
Внизу поста добавил небольшой UPD к ниже написанному.
Данный модуль будет расширять существующие возможности системы, так что определенная структура таблиц для хранения купонов, пулов с ними у нас уже есть. Однако не все так радужно, как хотелось бы. Данный модуль будет давать возможность генерировать купоны, и по хорошему, надо бы каждый запуск процесса генерации запоминать в БД, чтобы была возможность обратиться к истории. Что я хочу хранить:
- id, автоинкремент
- дата запуска процесса, datetime not null
- дата окончания генерации, datetime null (процесс может не завершиться)
- настройки процесса (выбранные настройки). Наверно пока text, хранить будем в json (да да, я слышал про нормализацию)
- статистика и прогресс (количество обработанных купонов, успешных, неуспешных)
Эти данные буду хранить в таблице maximaster_coupanda_process. Называю я её так, потому что в будущем еще хочу в этой же таблице хранить данные не только о процессах генерации, но и импорта, а может быть потом и о еще каких-нибудь процессах обработки купонов.
Также есть еще один важный момент - нужно каждый сгенерированный купон привязывать к записи из таблицы процессов. Но вот засада - в битриксе нельзя расширять таблицы уже существующих сущностей, при проверке система будет ругаться на то, что описание таблицы в модуле не соответствует структуре таблицы в БД. Поэтому придется для хранения связи между купоном и процессом генерации использовать промежуточную таблицу. Это не очень-то хорошо, и не удобно совсем, но мы сознательно идем на этот шаг, потому что система не предоставляет нам выбора. Благо в битриксе существует возможность для любой сущности создавать произвольные поля, и битрикс сам будет заботиться о связи таблиц при построении запросов. Воспользуемся этой возможностью, и добавим к сущности купона свойство UF_COUPANDA_PID (идентификатор процесса). Код свойства именно такой, т.к. битрикс обязывает ставить префикс UF_ и не дает делать коды длиннее 20 символов.
Для работы с этой таблицей нужно создать ORM сущность - ProcessTable. Данный класс должен содержать описание всех полей таблицы, логику для валидации и подготовки данных при работе с БД.
При работе с купонами нам потребуется расширенная ORM сущность купона. Нужно создать класс DiscountCoupon, наследующийся от \Bitrix\Sale\Internals\DiscountCouponTable и добавляющий в него привязку к новому UF_ свойству.
Набор подсистем
Инсталлятор/деинсталлятор
Как я писал выше - это подсистема, необходимость которой определяет битрикс для модуля. Это простой класс, наследник от \CModule. Имя класса тоже строго определено правилами битрикса, а значит класс должен именоваться \maximaster_coupanda, и должен находиться в файле %module_root%/install/index.php
Интерфейс работы с этим классом тоже заранее определен битриксом (интерфейс не в терминах php, а как набор соглашений), поэтому описывать его не буду.
Печальный факт. Вся эта модульная система тянется с момента зарождения этой модульной системы, и рефакторить её пока не хотят, поэтому тут будет много анти-паттернов. Но куда деваться … мы же бабло пришли зарабатывать, а не ныть о том, как все плохо в битриксе 🙂
Обработчики событий
В Битриксе есть механизм, который позволяет влиять на ход работы других модулей, все его знают - это обработчики событий. В моем модуле будет всего один обработчик:
- Регистрация в меню - OnBuildGlobalMenu. По заданию, нам необходимо встроить собственные пункты меню в уже существующее меню модуля «sale», поэтому будем использовать данный обработчик
Менеджмент ограничений
Мы изначально задали набор ограничений (зависимость от версии модулей, зависимость от версии php и зависимость от наличия других модулей). В битриксе есть класс \Bitrix\Main\ModuleManager, который умеет следить за зависимостью от модулей. Но у него есть пара недостатков - он состоит из статических методов и он final. Поэтому мы создадим небольшую обертку над ним. Зачем? Для того, чтобы инкапсулировать зависимость от ModuleManager в одном месте. Для нашего класса, проверяющего ограничения, мы зададим интерфейс, и будем работать только с ним. Так, все остальные классы, работающие с классом ограничений, всегда смогут опираться на интерфейс при работе с этим классом, и не будут зависеть от ModuleManager. С ростом версий битрикса класс ModuleManager может измениться (и он скорее всего изменится, либо появится новый, более эффективный метод проверки зависимостей), и тогда мы сможем поменять код только в одном месте, чтобы изменить всю систему проверки зависимостей. И наоборот, если нам надо будет в нашем модуле поддержать более древние версии битры, где еще не было ModuleManager, мы сможем это сделать в одном классе.
По большому счету, нужно лишь проверить, совместим ли наш модуль с действующей установкой битрикс, а также иметь возможность узнать причины несовместимостей, если таковые будут.
interface CompabilityCheckerInterface
{
public function isCompatible();
public function getCompabilityInfo();
}
Внутреннюю кухню этого класса описывать не буду, оставлю на откуп «разработчику» 🙂
Генератор последовательностей
Каждый промокод будет состоять из нескольких частей, и в каждом промокоде может быть генерируемая часть. Пример промокода:
PROMO-ASJ&D173!-CODE
Здесь видно, что центральная часть между символами «-» является генерируемой автоматически. На основании этого можно сформулировать частную подзадачу.
Генератор последовательностей должен уметь:
- генерировать строковые последовательности заданной длины
- генерировать заданное количество строковых последовательностей
- генерировать последовательность на основании заданной коллекции символов
- просчитать количество возможных комбинаций
- генерировать строковые последовательности на основании шаблонной строки
На последнем пункте я хотел бы поподробнее остановиться. Мне хочется добавить сразу некий запас гибкости модулю, и не просто генерировать последовательность из набора, а давать возможность влиять на эту последовательность. Поэтому я хочу добавить микроскопический «шаблонный» язык. Хотя это будет не язык, а простая автозамена символов:
- # - заменяем на цифру от 0 до 9
- @ - заменяем на букву русского алфавита
- $ - заменяем на букву английского алфавита
- заменяем на любой символ из заданных последовательностей
Я сознательно пока резервирую эти символы под свои нужды. Чуть позже сделаю какой-нибудь парсер шаблона и может быть более продвинутый язык шаблонов с возможностью управления через интерфейс битрикса, но пока так.
Скорее всего, благодаря этому шаблонному языку, можно будет избавиться от указания префикса и суффикса при генерации промокода, т.к. их можно будет задать непосредственно в шаблоне. Единственный минус тут - это отсутствие возможности задать переменную длину для генерируемой части купона, но это не такое уж важное ограничение и наверно пока очень редкий кейс. Да и при желании можно будет прикрутить и его.
Основываясь на этих рассуждениях становится ясно, что нам понадобятся:
- классы, которые описывают последовательности разных символов
- класс, который описывает шаблон генерируемой части и соответствие между символами шаблона и последовательностями
- класс, который будет заниматься генерацией последовательностей из шаблонов, он же и будет нашим генератором купонов
- класс, который будет отвечать за настройку генерации битриксовых купонов
- класс, который будет выполнять генерацию в соответствии с настройками и сохранение купонов в БД
- ну и для всего этого нужна будет коллекция интерфейсов, исключений
Итак:
interface SymbolsCollectionInterface
{
public function getOne($number);
public function getRandomOne();
public function getCount();
public function getSymbols();
}
Этот интерфейс будут реализовывать классы:
- SymbolsCollection - базовый класс
- DigitsCollection - класс с коллекцией цифр
- SpecialCharsCollection - класс со спец. символами
- EnglishLettersUppercaseCollection - класс с английскими буквами в верхнем регистре
- EnglishLettersLowercaseCollection - в нижнем регистре
- RussianLettersUppercaseCollection и RussianLettersLowercaseCollection - то же самое для русских символов
- UnionCollection - класс, который позволит объединить несколько последовательностей в одну
interface SequenceTemplateInterface
{
public function setTemplate($template);
public function setPlaceholder($placeholder, array $collections);
public function addPlaceholder($placeholder, SymbolsCollectionInterface $collection);
public function removePlaceholder($placeholder);
public function getPlaceholders();
}
Пока что нам понадобится только одна частная реализация этого интерфейса - SequenceTemplate
interface SequenceGeneratorInterface
{
public function setTemplate(SequenceTemplate $template);
public function generateOne();
public function generateUniqueOne();
public function generateSeveral($count);
public function generateUniqueSeveral($count);
public function getGeneratedCount();
public function calculateCombinationsCount();
public function setPrefix($prefix);
public function setPostfix($postfix);
}
Тут тоже пока будет достаточно одной реализации - SequenceGenerator
interface CouponGeneratorConfigurationInterface
{
public function setCodeTemplate(SequenceTemplateInterface $template);
public function setDiscountId($discountId);
public function setActiveFrom(\DateTime $activeFrom = null);
public function setActiveTo(\DateTime $activeTo = null);
public function setUserId($userId = null);
public function setActiveFlag($active = true);
public function setType($type = null);
public function setMaxUse($maxUse = null);
public function setDescription($description = null);
}
У данного интерфейса также будет всего одна реализация - CouponGeneratorConfiguration.
interface CouponGeneratorInterface
{
public function setConfiguration(CouponGeneratorConfigurationInterface $config);
public function generate();
}
Ну и одна реализация генератора - CouponGenerator.
Страница генератора
На начальном этапе у нас будет всего один контроллер с тремя разными экранами, который будет отвечать за функциональность всего модуля. Данный контроллер имеет смысл реализовать с помощью обычного компонента битрикс. Данный компонент должен иметь возможность работы в трех режимах (все в соответствии с макетами интерфейса из прошлой статьи):
- Настройка генерации
- Процесс генерации
- Отчет о генерации
Естественно компонент должен иметь возможность работы в режиме ajax, должен быть оформлен в виде класса.
Логирование
Нельзя забывать о логировании, т.к. в случае возникновения проблемных ситуаций нужно будет какое-то основание для поиска ошибки. В идеале, данная система должна следовать PSR-3 рекомендациям. Но в битриксе нет его нативной поддержки, а подключить наикрутейший monolog в битрикс можно только с помощью composer, который недоступен в коробке при инсталляции модулей. Поэтому, думаю, будет достаточно подвязаться к существующей системе логирования в битрикс.
Система прав доступа
Нужно реализовать механизм прав доступа к модулю на основе битриксовых возможностей. Нужно добавить всего 2 вида прав - запрет на работу с этим модулем, и разрешение на осуществление всех операций этого модуля.
Summary
Вот такое техническое заданьице получилось на такой крошечный модуль, несмотря даже на очень низкий уровень детализации. Но я думаю, что этого уровня детализации будет вполне достаточно среднему разработчику для того, чтобы начать непосредственно реализацию этого модуля.
Хочу заметить, что я пока еще не написал ни единой строчки кода этого модуля, но уже около половины дела сделано. Остается дело за малым - открыть любимую IDE и набросать функционал, подумав немного о частных проблемах реализации. Этим и займемся в следующих статьях.
upd
Обманул я вас немного по части структуры БД. Проверил опытным путем, добавил в таблицу b_sale_discount_coupon свою колонку, после чего прогнал полную проверку системы. Битрикс даже не шелохнулся. Поэтому буду добавлять колонку и строить функционал модуля исходя из этого. И уже если на проверке завернут, буду рефакторить.