Перед тем, как начать реализовывать эту часть, я решил все-таки проверить, насколько битрикс трепетно относится к структуре тех таблиц, которые сам создает. И, кажется, не зря.
Я в посте про архитектуру писал, что битрикс будет ругаться, если в одну из его таблиц добавить собственную колонку. Однако тут я немного обманул. На самом деле он проверяет наличие тех колонок, которые перечислены в инсталляторе модуля, а также их типы и настройки каждой колонки. А вот если в таблице больше колонок, чем надо, а все колонки из инсталлятора присутствуют, то битрикс никак не реагирует. Поэтому я попробую воспользоваться этой ситуацией исходя из собственных нужд. Заодно и узнаем, как к этому отнесется битрикс 🙂 А если отнесется плохо, то переделаю на пользовательские свойства, или вообще откажусь от этого механизма до поры до времени. По этому поводу добавил UPD в статью по архитектуре.
Таблица процессов
Нам понадобится таблица для хранения процессов (поначалу только для процессов генерации, но позже и для прочих процессов). Назовем таблицу maximaster_coupanda_process. Для работы с этой таблицей нам понадобится ORM сущность. Можно пойти двумя путями — сначала сделать таблицу в БД, а потом сгенерировать php код сущности, а можно наоборот, сначала написать код сущности, а потом сгенерировать таблицу. Я пойду вторым путем, т.к. битрикс будет генерировать неактуальный php-код, который сам же считает устаревшим (битрикс такой битрикс … ).
У битриксового автозагрузчика есть огромный недостаток (он не использует PSR-4). Имя файла для таблиц ORM отличается от имени класса сущности. Поэтому все классы сущностей ORM я буду складировать в отдельный неймспейс и, соответственно, в отдельную директорию. Опишем класс сущности ProcessTable и положим его в файл /lib/orm/process.php:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 |
<?php namespace Maximaster\Coupanda\Orm; use Bitrix\Main\Entity\DataManager; use Bitrix\Main\Entity\DatetimeField; use Bitrix\Main\Entity\IntegerField; use Bitrix\Main\Entity\TextField; use Bitrix\Main\Localization\Loc; Loc::loadMessages(__FILE__); class ProcessTable extends DataManager { public static function getTableName() { return 'maximaster_coupanda_process'; } public static function getMap() { return [ 'ID' => new IntegerField('ID', [ 'primary' => true, 'autocomplete' => true, 'title' => Loc::getMessage('PROCESS_ENTITY_ID_FIELD') ]), 'STARTED_AT' => new DatetimeField('STARTED_AT', [ 'required' => true, 'title' => Loc::getMessage('PROCESS_ENTITY_STARTED_AT_FIELD') ]), 'FINISHED_AT' => new DatetimeField('FINISHED_AT', [ 'default_value' => null, 'title' => Loc::getMessage('PROCESS_ENTITY_FINISHED_AT_FIELD') ]), 'SETTINGS' => new TextField('SETTINGS', [ 'required' => true, 'serialized' => true, 'title' => Loc::getMessage('PROCESS_ENTITY_SETTINGS_FIELD') ]), 'REPORT' => new TextField('REPORT', [ 'required' => true, 'serialized' => true, 'title' => Loc::getMessage('PROCESS_ENTITY_REPORT_FIELD') ]), ]; } } |
Все просто, понятно, и по документации. Единственное, я отошел тут от архитектуры в плане сериализации значений колонок REPORT и SETTINGS. Я планировал делать json, но битрикс довольно часто устанавливается с windows-1251 кодировкой, в которой кириллица сериализоваться в json нативным для php способом не будет. Да и к тому же в битриксе для полей сущности есть специальный флаг, который будет сам следить за необходимостью сериализации/десериализации значений. Поэтому не вижу смысла не воспользоваться нативным функционалом на данном этапе.
Но описания самой сущности недостаточно, нужно ещё эту таблицу инсталлировать/деинсталлировать. Выделим создание этой таблицы в отдельный метод и добавим вызов этого метода в InstallDB(). Для того, чтобы битрикс смог найти этот класс, нужно предварительно зарегистрировать этот модуль и подключить его:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 |
public function InstallDB() { \Bitrix\Main\ModuleManager::registerModule($this->MODULE_ID); \Bitrix\Main\Loader::includeModule($this->MODULE_ID); $connection = \Bitrix\Main\Application::getConnection(); $this->createProcessTable($connection); return true; } protected function createProcessTable(\Bitrix\Main\DB\Connection $connection) { $tableName = \Maximaster\Coupanda\Orm\ProcessTable::getTableName(); if (!$connection->isTableExists($tableName)) { $connection->createTable(\Maximaster\Coupanda\Orm\ProcessTable::getTableName(), \Maximaster\Coupanda\Orm\ProcessTable::getMap(), ['ID'], ['ID']); } } |
Примерно то же самое делаем и для деинсталляции таблицы (писать код сюда не буду, оно итак понятно).
Хранение связи с ProcessTable
Теперь нам нужно добавить колонку в таблицу с купонами, которая будет ссылаться на данную таблицу. Добавить колонку в существующую таблицу довольно просто, делается это с помощью запроса ALTER_TABLE, его нужно также добавить в инсталлятор. Создаем новый метод, который добавит колонку:
1 2 3 4 |
protected function addPIDColumn(\Bitrix\Main\DB\Connection $connection) { $connection->queryExecute('ALTER TABLE `b_sale_discount_coupon` ADD `MAXIMASTER_COUPANDA_PID` INT NULL DEFAULT NULL'); } |
И добавляем его вызов в инсталлятор.
Но тут сразу кроется засада — что если запрос на создание таблицы пройдет успешно, а запрос на добавление колонки — нет? Или наоборот, запрос на создание таблицы не пройдет, а на создание колонки — прокатит? Добавлять кучу проверок на этот счет? Нафиг, сразу лучше использовать транзакции.
Кроме этого нужно какую-то ошибку показывать пользователю, если вдруг инсталляция не удалась. Сделаем пока простую заглушку, которая тупо будет транслировать сообщение из словленного исключения в процессе инсталляции БД.
В результате наш метод инсталляции БД превращается в нечто такое:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 |
public function InstallDB() { $connection = \Bitrix\Main\Application::getConnection(); try { $connection->startTransaction(); \Bitrix\Main\ModuleManager::registerModule($this->MODULE_ID); \Bitrix\Main\Loader::includeModule($this->MODULE_ID); $this->createProcessTable($connection); $this->addPIDColumn($connection); $connection->commitTransaction(); } catch (\Exception $e) { $connection->rollbackTransaction(); global $APPLICATION; $APPLICATION->ResetException(); $APPLICATION->ThrowException($e->getMessage()); return false; } return true; } |
Примерно то же самое опишем в методе деинсталляции БД, но в немного другом порядке (снятие регистрации модуля оставляем в конце, после того как все данные удалены успешно):
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 |
public function UninstallDB() { $connection = \Bitrix\Main\Application::getConnection(); try { \Bitrix\Main\Loader::includeModule($this->MODULE_ID); $connection->startTransaction(); $this->dropProcessTable($connection); $this->dropPIDColumn($connection); \Bitrix\Main\ModuleManager::unRegisterModule($this->MODULE_ID); $connection->commitTransaction(); } catch (\Exception $e) { $connection->rollbackTransaction(); global $APPLICATION; $APPLICATION->ResetException(); $APPLICATION->ThrowException($e->getMessage()); return false; } return true; } |
По хорошему, нужно обработать каждый тип исключения, который выбрасывается битриксом при выполнении работы с БД, и обработать каждую ошибку по разному. Но мы же делаем MVP, а значит отложим это до момента запуска проекта, т.к. частота появления ошибки будет слишком низкой, а полученной информации чаще всего будет достаточно для того, чтобы разработчики смогли разобраться с проблемой инсталляции.
Проверяем
После инсталляции модуля с нуля тестируем наличие новой таблицы:
1 |
show create table maximaster_coupanda_process; |
1 2 3 4 5 6 7 8 |
CREATE TABLE `maximaster_coupanda_process` ( `ID` int(11) NOT NULL AUTO_INCREMENT, `STARTED_AT` datetime NOT NULL, `FINISHED_AT` datetime NOT NULL, `SETTINGS` text COLLATE utf8_unicode_ci NOT NULL, `REPORT` text COLLATE utf8_unicode_ci NOT NULL, PRIMARY KEY (`ID`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8 COLLATE=utf8_unicode_ci |
Добавим запись:
1 2 3 4 5 6 7 8 9 |
\Bitrix\Main\Loader::includeModule('maximaster.coupanda'); $addedResult = \Maximaster\Coupanda\Orm\ProcessTable::add([ 'STARTED_AT' => new \Bitrix\Main\Type\DateTime(), 'REPORT' => ['add' => 1, 'all' => 100], 'SETTINGS' => ['setting1' => '1', 'setting2' => '2'] ]); var_dump($addedRsult->isSuccess()); // true |
Проверим, корректно ли была выполнена связь между таблицами:
1 2 3 4 5 6 7 8 9 10 |
$updateResult = \Maximaster\Coupanda\Orm\DiscountCouponTable::update(2, ['MAXIMASTER_COUPANDA_PID' => 1]); value($updateResult->isSuccess()); //true $selectedCoupon = \Maximaster\Coupanda\Orm\DiscountCouponTable::query() ->addFilter('ID', 2) ->addSelect('MAXIMASTER_COUPANDA_PID') ->addSelect('MAXIMASTER_COUPANDA_PROCESS.SETTINGS') ->exec()->fetch(); print_r($selectedCoupon); |
1 2 3 4 5 6 7 8 9 10 |
Array ( [MAXIMASTER_COUPANDA_PID] => 1 [MAXIMASTER_COUPANDA_ORM_DISCOUNT_COUPON_MAXIMASTER_COUPANDA_PROCESS_SETTINGS] => Array ( [setting1] => 123 [setting2] => 123123 ) ) |
Данные из связанной таблицы выводятся корректно.
В процессе написания этой статьи стало понятно, что для работы с колонками SETTINGS и REPORT будет гораздо удобнее использовать объекты. Для этого потребуется добавить в класс сущности валидацию этих полей с помощью механизов ORM. В методе getMap для каждого из этих полей добавляем настройку validation, которая должна содержать коллбек, возвращающий массив валидаторов:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 |
'SETTINGS' => new TextField('SETTINGS', [ 'required' => true, 'serialized' => true, 'validation' => function() { return [[__CLASS__, 'validateSettingsInstance']]; }, 'title' => Loc::getMessage('PROCESS_ENTITY_SETTINGS_FIELD') ]), 'REPORT' => new TextField('REPORT', [ 'required' => true, 'serialized' => true, 'validation' => function() { return [[__CLASS__, 'validateReportInstance']]; }, 'title' => Loc::getMessage('PROCESS_ENTITY_REPORT_FIELD') ]), |
а также добавим методы валидации:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 |
public static function validateSettingsInstance($value, $primary, array $row, Field $field) { if (!$value instanceof ProcessSettings) { return new FieldError($field, Loc::getMessage('PROCESS_ENTITY_SETTINGS_FIELD_NOT_VALID'), 'VALIDATION_ERROR'); } return true; } public static function validateReportInstance($value, $primary, array $row, Field $field) { if (!$value instanceof ProcessReport) { return new FieldError($field, Loc::getMessage('PROCESS_ENTITY_REPORT_FIELD_NOT_VALID'), 'VALIDATION_ERROR'); } return true; } |
Теперь, при создании и обновлении записей в БД битрикс будет использовать эти валидаторы, у благодаря сериализации/десериализации мы сможем хранить эти данные в БД и получать из БД готовый заполненный инстанс нужного класса.
Подводим итоги.
Я не претендую на экспертность по части БД, возможно я тут накосячил. Напишите мне в комментах, если вдруг где-то тут есть серьезные огрехи, буду исправлять.
В этой статье я создал небольшой реальный пример связки модуля Битрикс с БД и все минимально необходимое для этого. Осталось узнать, как на эти изменения отреагирует сам битрикс при проверке модуля 🙂
В следующей статье я начну реализовывать основную часть логики — сами классы генераторов, последовательностей, проверку зависимостей и может быть начну заниматься внешним видом.