Своя сущность от таблицы до админки - слои, права, интерфейс
Собираем свою сущность целиком: от карты полей и таблицы до страницы в административной части, с правами и слоем правил между ними.
Механика
У любой своей сущности три слоя, и путать их дорого. Хранение - это карта полей и таблица; правила - это код, который знает, что можно менять и при каких условиях; интерфейс - страницы, через которые с ней работает сотрудник. Смешение слоёв начинается с первой же проверки, написанной прямо в форме, и заканчивается тем, что то же правило приходится повторять в обмене и в задании по расписанию.
Карта полей - единственный источник правды о структуре этой сущности. По ней платформа создаёт таблицу, проверяет значения и строит запросы; из неё же удобно собирать описание полей для формы. Всё, что описано в карте, не приходится описывать второй раз.
Правила изменения данных живут в своём отдельном классе проекта. Он принимает данные, проверяет их и возвращает результат с ошибками, а вызывать его могут и форма админки, и обмен, и своё API. Именно этот класс делает сущность пригодной больше чем для одного лишь интерфейса.
Интерфейс собирают штатными классами административной части, а не своей вёрсткой. Список с фильтром, сортировкой и групповыми действиями, форма с вкладками и сообщениями об ошибках - всё это уже есть, и сотрудник получает привычное поведение вместо новой для него страницы.
Права - это отдельный слой, который забывают чаще всех остальных. Административная часть доступна всем, кто в неё вошёл, поэтому доступ к своим данным описывают своей операцией и проверяют её в каждой точке входа, а не в одной.
Наконец, сквозная задача редко бывает одноразовой. За первой сущностью почти всегда приходит вторая и третья, поэтому первую стоит собрать так, чтобы её устройство можно было повторить: одинаковые слои, одинаковые имена, одинаковый способ проверки прав. Тогда третья сущность делается за день, а не за неделю.
Шаги
- Описать карту полей сущности: типы, обязательность и значения по умолчанию.
- Создать таблицу из карты в установке модуля и добавить ей нужные индексы.
- Вынести правила изменения данных в отдельный класс с понятным результатом.
- Собрать страницу списка с фильтром, сортировкой и групповыми действиями.
- Собрать форму правки, вызывающую ровно тот же класс правил, что и обмен.
- Завести свою операцию прав и проверить её на каждой странице.
Код
Описываем карту полей:
public static function getMap(): array{ return [ new IntegerField('ID', ['primary' => true, 'autocomplete' => true]), new StringField('NUMBER', ['required' => true, 'validation' => fn() => [new LengthValidator(1, 32)]]), new DatetimeField('DATE_SIGN'), new EnumField('STATUS', ['values' => ['draft', 'active', 'closed'], 'default_value' => 'draft']), new IntegerField('USER_ID'), ];}Сущность описывают картой полей, а таблицу создают из неё. Обязательность, длина и допустимые значения заданы здесь один раз и работают одинаково в форме, в обмене и в любом своём скрипте.
Создаём таблицу при установке:
public function InstallDB(){ \Vendor\Shop\ContractTable::getEntity()->createDbTable(); $c = \Bitrix\Main\Application::getConnection(); $c->queryExecute('CREATE INDEX ix_contract_user ON b_vendor_contract (USER_ID)'); $c->queryExecute('CREATE INDEX ix_contract_status ON b_vendor_contract (STATUS)'); return true;}// индексы создаются отдельно: карта полей о них не знаетТаблицу создают из карты, а индексы добавляют отдельными запросами. Про них вспоминают обычно позже, когда список в админке начинает открываться секундами, и дешевле завести их сразу.
Выносим правила в свой класс:
public function close(int $id, string $reason): Result{ $result = new \Bitrix\Main\Result(); $row = ContractTable::getById($id)->fetch(); if ($row['STATUS'] === 'closed') { return $result->addError(new Error('Договор уже закрыт')); } ContractTable::update($id, ['STATUS' => 'closed']); return $result;}Бизнес-правила живут в своём классе, а не в форме. Тот же метод вызывают обмен, задание по расписанию и своё API, и правило «нельзя закрыть дважды» не приходится повторять трижды.
Собираем страницу списка:
require $_SERVER['DOCUMENT_ROOT'] . '/bitrix/modules/main/include/prolog_admin_before.php';if (!$USER->CanDoOperation('vendor_contract_read')) { $APPLICATION->AuthForm('Доступ запрещён'); }
$list = new CAdminUiList('vendor_contract', new CAdminUiSorting('vendor_contract', 'ID', 'desc'));$list->AddHeaders([ ['id' => 'NUMBER', 'content' => 'Номер', 'sort' => 'NUMBER', 'default' => true], ['id' => 'STATUS', 'content' => 'Статус', 'default' => true],]);Список и форма собираются штатными классами админки. Фильтр, сортировка, постраничность и групповые действия приходят вместе с ними, а сотрудник видит привычную страницу, а не самодельную таблицу.
Проверяем свои права:
// в установке модуля: заводим свои операции и связываем их с уровнями доступа$APPLICATION->SetGroupRight('vendor.shop', $groupId, 'W');// в коде страницы и в формеif (!$USER->CanDoOperation('vendor_contract_write')) { ShowError('Недостаточно прав для правки договора'); return;}Доступ к своим данным описывают своей операцией. Проверять её нужно и на странице списка, и в форме, и в обработчике сохранения: обойти одну проверку проще, чем кажется.
Пишем важные изменения в журнал:
CEventLog::Add([ 'SEVERITY' => 'SECURITY', 'AUDIT_TYPE_ID' => 'VENDOR_CONTRACT_CLOSE', 'MODULE_ID' => 'vendor.shop', 'ITEM_ID' => $id, 'DESCRIPTION' => 'закрыл ' . $USER->GetID() . ', причина: ' . $reason,]);// записи журнала переживают правку данных: по ним и разбирают спорные случаиВажные изменения пишут в журнал сразу, а не после первого спора. Вопрос «кто закрыл этот договор в пятницу» задают редко, но ответ на него должен быть, и взять его больше неоткуда.
Ограничения
У своей таблицы нет никаких штатных прав на её отдельные записи. Права инфоблоков и заказов сюда не переносятся: если сотруднику можно видеть только свои договоры, это условие пишут в самом запросе, и забыть его легко.
Изменение структуры на живом проекте - это отдельная и вполне заметная работа. Карта полей меняется в коде, а таблица - командой на боевом сайте, и синхронность этих двух шагов обеспечивает миграция, а не память разработчика.
Интерфейс административной части вообще не рассчитан на сотни тысяч записей. Список с фильтром на таком объёме требует индексов под каждый частый фильтр, а иногда и отдельной страницы отчёта вместо универсального списка.
Названия полей и таблиц стоит выбирать так, будто их будет читать посторонний.
Через год этим посторонним окажетесь вы сами, а FIELD_2 и b_data не расскажут
ни о смысле поля, ни о том, откуда в нём берутся значения.
Своя сущность не даёт бесплатно ни поиска, ни бизнес-процессов, ни истории изменений. Всё это - отдельные задачи, и если они нужны с самого начала, универсальный список или инфоблок может оказаться дешевле.
Типичные проблемы
Одно и то же правило проверяется в трёх местах.
Проверки написаны прямо в форме, а обмен и задание проверяют всё по-своему. Правила выносят в один общий класс и вызывают его уже отовсюду одинаково.
Список в админке открывается несколько секунд.
У полей фильтра нет индексов, и база читает таблицу целиком. Индексы заводят под частые фильтры сразу, а не после жалоб.
Сотрудник видит чужие записи.
Права проверяются на странице списка, но не в форме и не при сохранении. Операцию проверяют в каждой точке входа, а не только на самом списке.
После выкладки код не совпал со структурой таблицы.
Карта полей уехала с кодом, а таблицу никто не менял. Структуру таблицы меняют миграцией вместе с выкладкой самого кода.
Никто не знает, кто закрыл договор.
Важные изменения этих данных нигде и никак не записываются. Журнал заводят вместе с самой операцией, а не после спора.
Частые вопросы
Когда своя таблица лучше инфоблока?
Когда нужны свои поля, скорость и нет нужды в разделах и правах на записи. Инфоблок даёт интерфейс, но берёт за это объёмом.
Где держать бизнес-правила?
В отдельном классе, который возвращает результат с ошибками. Форма и обмен вызывают его одинаково.
Как ограничить доступ к отдельным записям?
Условием в самом запросе: штатных прав на записи у своей таблицы нет. Забыть это условие - типовая ошибка.
Нужен ли свой модуль ради одной таблицы?
Не обязательно, но с ним установка, права и обновления получаются аккуратнее. Разовую таблицу заводят и проще.
Что делать при росте до миллионов записей?
Разделять данные и отчёты: интерфейс списка на такое не рассчитан. Часто помогает отдельная таблица с готовыми итогами.
Смежное
-
Свои таблицы на ORM - оглавление подтемы
-
Своя таблица на ORM: сущность, запросы, изменение структуры - слой хранения отдельно
-
Своя страница в админке: список, форма, пункт меню - слой интерфейса отдельно
-
Пользовательские поля у своей сущности: регистрация, запись, чтение - дополнительные поля без правки таблицы
-
Транзакции при записи: откат, границы, что не откатится - как писать связанное целиком
-
Миграции структуры: перенос инфоблоков и настроек между стендами - как правки структуры доезжают до боевого
-
Ядро D7 - устройство ядра целиком
-
История изменений своей таблицы: версии, автор, откат - вкладка истории в карточке записи