Перед тем, как начать реализовывать эту часть, я решил все-таки проверить, насколько битрикс трепетно относится к структуре тех таблиц, которые сам создает. И, кажется, не зря.
Я в посте про архитектуру писал, что битрикс будет ругаться, если в одну из его таблиц добавить собственную колонку. Однако тут я немного обманул. На самом деле он проверяет наличие тех колонок, которые перечислены в инсталляторе модуля, а также их типы и настройки каждой колонки. А вот если в таблице больше колонок, чем надо, а все колонки из инсталлятора присутствуют, то битрикс никак не реагирует. Поэтому я попробую воспользоваться этой ситуацией исходя из собственных нужд. Заодно и узнаем, как к этому отнесется битрикс 🙂 А если отнесется плохо, то переделаю на пользовательские свойства, или вообще откажусь от этого механизма до поры до времени. По этому поводу добавил UPD в статью по архитектуре.
Таблица процессов
Нам понадобится таблица для хранения процессов (поначалу только для процессов генерации, но позже и для прочих процессов). Назовем таблицу maximaster_coupanda_process. Для работы с этой таблицей нам понадобится ORM сущность. Можно пойти двумя путями - сначала сделать таблицу в БД, а потом сгенерировать php код сущности, а можно наоборот, сначала написать код сущности, а потом сгенерировать таблицу. Я пойду вторым путем, т.к. битрикс будет генерировать неактуальный php-код, который сам же считает устаревшим (битрикс такой битрикс … ).
У битриксового автозагрузчика есть огромный недостаток (он не использует PSR-4). Имя файла для таблиц ORM отличается от имени класса сущности. Поэтому все классы сущностей ORM я буду складировать в отдельный неймспейс и, соответственно, в отдельную директорию. Опишем класс сущности ProcessTable и положим его в файл /lib/orm/process.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(). Для того, чтобы битрикс смог найти этот класс, нужно предварительно зарегистрировать этот модуль и подключить его:
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, его нужно также добавить в инсталлятор. Создаем новый метод, который добавит колонку:
protected function addPIDColumn(\Bitrix\Main\DB\Connection $connection)
{
$connection->queryExecute('ALTER TABLE `b_sale_discount_coupon` ADD `MAXIMASTER_COUPANDA_PID` INT NULL DEFAULT NULL');
}
И добавляем его вызов в инсталлятор.
Но тут сразу кроется засада - что если запрос на создание таблицы пройдет успешно, а запрос на добавление колонки - нет? Или наоборот, запрос на создание таблицы не пройдет, а на создание колонки - прокатит? Добавлять кучу проверок на этот счет? Нафиг, сразу лучше использовать транзакции.
Кроме этого нужно какую-то ошибку показывать пользователю, если вдруг инсталляция не удалась. Сделаем пока простую заглушку, которая тупо будет транслировать сообщение из словленного исключения в процессе инсталляции БД.
В результате наш метод инсталляции БД превращается в нечто такое:
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;
}
Примерно то же самое опишем в методе деинсталляции БД, но в немного другом порядке (снятие регистрации модуля оставляем в конце, после того как все данные удалены успешно):
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, а значит отложим это до момента запуска проекта, т.к. частота появления ошибки будет слишком низкой, а полученной информации чаще всего будет достаточно для того, чтобы разработчики смогли разобраться с проблемой инсталляции.
Проверяем
После инсталляции модуля с нуля тестируем наличие новой таблицы:
show create table maximaster_coupanda_process;
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
Добавим запись:
\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
Проверим, корректно ли была выполнена связь между таблицами:
$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);
Array
(
[MAXIMASTER_COUPANDA_PID] => 1
[MAXIMASTER_COUPANDA_ORM_DISCOUNT_COUPON_MAXIMASTER_COUPANDA_PROCESS_SETTINGS] => Array
(
[setting1] => 123
[setting2] => 123123
)
)
Данные из связанной таблицы выводятся корректно.
В процессе написания этой статьи стало понятно, что для работы с колонками SETTINGS и REPORT будет гораздо удобнее использовать объекты. Для этого потребуется добавить в класс сущности валидацию этих полей с помощью механизов ORM. В методе getMap для каждого из этих полей добавляем настройку validation, которая должна содержать коллбек, возвращающий массив валидаторов:
'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')
]),
а также добавим методы валидации:
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;
}
Теперь, при создании и обновлении записей в БД битрикс будет использовать эти валидаторы, у благодаря сериализации/десериализации мы сможем хранить эти данные в БД и получать из БД готовый заполненный инстанс нужного класса.
Подводим итоги.
Я не претендую на экспертность по части БД, возможно я тут накосячил. Напишите мне в комментах, если вдруг где-то тут есть серьезные огрехи, буду исправлять.
В этой статье я создал небольшой реальный пример связки модуля Битрикс с БД и все минимально необходимое для этого. Осталось узнать, как на эти изменения отреагирует сам битрикс при проверке модуля 🙂
В следующей статье я начну реализовывать основную часть логики - сами классы генераторов, последовательностей, проверку зависимостей и может быть начну заниматься внешним видом.