Магазин держит в базе десятки справочников: реестр настроек, опции товаров и заказов, поля покупателя, валюты, поставщиков, языки, операции программы, веб-модули, сотрудников и выданные им права. Всё это модуль читает постоянно и почти никогда не меняет.
Методы Sys* двух родов. Двери читают
эти справочники, каждая свой источник одним и тем же
запросом — разделы 3–8. Утилиты делают
системную работу, которую иначе каждый модуль писал бы сам: спускаются
по ответу двери, смотрят схему таблицы, перестраивают дерево, считают и
подметают зависимости — разделы 9–13. Две утилиты стоят среди дверей
своих разделов: запись настройки и проверка входа.
Зачем дверь, если запрос пишется в три строки.
Валюты и правда достаются обычным SELECT — но написать его
самому значит завести свой текст запроса. Статический кеш
адресуется по тексту: чужой текст — вторая запись, своя копия тех же
данных и свой поход в базу при промахе. Причём хватит переставить пробел
или перенос строки, чтобы текст стал другим, — и записей станет три.
Дверь даёт то, чего запросом не добиться: все модули магазина попадают в одну и ту же запись кеша. Кто первым спросил валюты, оплатил запрос за всех остальных — за соседний модуль, за следующую страницу, за весь срок жизни записи. Поэтому системную таблицу читают только через её дверь, а если двери нет — её стоит завести, а не обойти.
$param_set = MELBIS()->SysParamValues();
$param = array_column($param_set, null, 'skey');
$ban_id = $param['BAN']['id'];Дверь отдаёт список полных строк. Ключа у ответа нет и аргумента-селектора тоже: ключ выбирает тот, кому он нужен, одной строкой.
$lang_set = MELBIS()->SysLanguages();
$lang = array_column($lang_set, null, 'skey');
$ua = $lang['UA'] ?? [];Строка самодостаточна. В ней все колонки таблицы,
включая id, skey и pos. Потому
ответ и список, а не карта: ключ карты — колонка, вынутая из строки, и
стоит достать строку перебором или array_column, как имя
потеряно и взять его неоткуда.
Порядок строк — договор. Список приходит в порядке
программы: pos у плоских справочников,
absindex у деревьев, и id вторым ключом —
чтобы одинаковые pos не давали разный ответ от запроса к
запросу. У сотрудников своей колонки порядка нет вовсе, там порядок
появления. Вложенные списки упорядочены по тому же правилу, каждый по
своей колонке.
Дверь отдаёт семью целиком. Где у справочника есть
подчинённые таблицы, они приходят вложенным списком под своим ключом, и
так до самого низа: у группы поставщиков provider, у
поставщика stock; у опции value; у области
налога rule, а у правила rate. Имя ключа — имя
того, что в нём лежит.
Плоской двери на каждый уровень нет и не будет. Справочник читается одним кешируемым запросом; второй, выбирающий из тех же таблиц по-другому, — это вторая запись кеша с теми же данными, и они начнут расходиться, как только таблица поменяется. Свой запрос оправдан лишь там, где перебор в PHP дороже похода в базу, а справочники для этого малы.
Нужный уровень достаёт SysPickBranch — одной строкой, на
любую глубину:
$group_set = MELBIS()->SysProviderGroups();
$provider = MELBIS()->SysPickBranch($group_set, 'provider', 'id');
$mine = $provider[$provider_id] ?? [];Строка при этом ничего не теряет: у поставщика внутри лежит
group_id, у склада provider_id — дорога наверх
известна из самой строки. Подробно — раздел 9.
Производное поле — сверх колонок, а не вместо. Дверь
вправе добавить в строку то, чего в таблице нет, если это избавляет
каждый магазин от одного и того же расчёта: так у валюты появился
rate, а у сотрудника group. Настоящие колонки
при этом остаются на месте.
Строка без символьного имени тоже в списке. Дверь не
отбирает «только с skey»: набор ведёт владелец магазина,
безымянная строка там обычное дело, а данные ссылаются на неё по
id. Валюта без skey стоит в
price_curr_id товара, и без неё в списке цену не посчитать.
Кому нужны только именованные — отбирает сам.
Три двери отвечают картой, и это решение, а не
отставание. SysKeyValues отдаёт пары «ключ →
значение», где строка вырождена в саму пару;
SysEntityValues — проекцию «что у сущности проставлено», а
не строки таблицы; SysTableSkey — адресацию по имени, ради
которой она и заведена. У этих трёх есть аргумент-срез.
Утилиты отвечают каждая по-своему. Уровень семьи, схема, номер нового узла, счёт зависимостей, отчёт о подметании — это ответы другого рода, и каждая утилита говорит о своей форме на месте: разделы 9–13.
Поля называются как колонки таблицы. Что написано в
install.sql и в описании таблицы — то и в строке:
kind_key, tindex, is_blocked,
modify_sum. Второго словаря учить не нужно, а
grep по имени колонки находит и запросы, и модули.
Адресация двойная, и граница проходит по смыслу.
Символьным именем адресуется то, на что ссылается код: команда операции,
skey модуля, ключ настройки. Идентификатором — то, что
завёл человек в магазине: сотрудник, бренд, раздел, значение опции. У
таких строк символьного имени может не быть вовсе, и это нормально.
$mLine ни двери, ни утилиты не
принимают. Запрос у них свой, и строка модуля к нему отношения
не имеет. В отказах и в журнале вместо номера строки стоит имя
метода.
Правило одно: дверь читает статическим кешем, утилита не кеширует ничего. Справочники, сотрудники, права — всё это двери, и все они идут через кеш.
Свежесть от этого не страдает. Адрес статической записи собран из метки изменения таблицы, а метку двигает всякая запись через движок — и модуль на PHP, и нативный клиент, который пишет теми же скриптами обмена. Изменилась таблица — у записи стал другой адрес, и следующий же запрос собирает её заново. Сутки в сроке жизни записи это не задержка изменения, а срок для записи, которую никто не отменил.
Метку не сдвинет только запись мимо движка: сырой запрос из SQL-инструмента, внешний скрипт, правка руками в базе. Но такая правка не видна ни в правах, ни в ценах, ни в товарах — это свойство обхода, а не отдельных таблиц.
Утилиты в базу по большей части не ходят вовсе: обе
SysPick* перебирают готовый массив, который дверь уже
принесла, а двери дерева и SysSelfKeyModify пишут. Но трое
читают, и каждая без статического кеша — по своей причине.
SysUserLoginCheck — глагол в имени
говорит, что это проверка, а не список. Пароль в общую память не
кладётся: статический кеш хранит параметры запроса внутри самой записи,
и пароль лежал бы там открытым. Вход всегда идёт в базу.
SysTableColumns — в тексте запроса нет
ни одной таблицы магазина, а значит нет и метки, по которой запись можно
было бы отменить. Схема запоминается на время запроса: внутри одного
запроса колонки не меняются.
SysDependCount считает по живой базе:
запрос собирается под вызов, а ответ нужен точный на сейчас — по нему
решают, можно ли удалять.
В памяти запроса дверь ничего не держит: каждое обращение идёт в статический кеш заново, а адрес там сдвигает любая запись. Поэтому правка, сделанная раньше на этой же странице, видна всем, кто спросит после неё.
Зависимость модуля от таблицы движок находит сам — по запросу, который дверь выполнила. Отмечать её в среде не нужно и негде: с точки зрения учёта запрос двери ничем не отличается от запроса, написанного в модуле руками. Как список собирается и когда пополняется — в разделе «Кеширование».
А вот страница, собранная по правам, в кеш модуля не годится. Дверь ответит свежим, но страница, попавшая в кеш, достанется и тому, у кого этих прав нет. Модули, которые спрашивают права, работают под конкретным сотрудником, и кешировать их незачем.
SysSelfKeyValues() — пользовательские
настройки магазина, те самые, что DefineSelfConst
превращает в константы. Список строк: код, значение, префикс, подпись с
описанием и маской ввода, и key_id — ветка реестра, на
которой выдаётся право.
$self_set = MELBIS()->SysSelfKeyValues();
$self = array_column($self_set, null, 'code');
$phone = $self['SHOP_PHONE']['value_txt'] ?? '';Префикс лежит в самой настройке: он группирует их для показа, а
адресуются они кодом. Разложить целую группу по глобальному массиву
умеет DefineSelfVars — регистр префикса ему
безразличен.
SysSelfKeyModify($mUserId, $mCode, $mValue, $mTries = 10, $mPause = 500)
— утилита раздела, она пишет. Проверяет, что настройка существует и что
человеку разрешено её править, берёт таблицу в блокировку, пишет и
снимает блокировку. Занятую таблицу пересиживает как
SqlTableLock: попытки и пауза в миллисекундах уходят туда,
и BUSY приходит только после них. Отвечает словом:
| Ответ | Что случилось |
|---|---|
ACCEPT |
записано |
ABSENT |
такого кода нет |
DENIED |
права нет |
BUSY |
таблицу держит программа, повторить позже |
Слово, а не true/false, потому что «нельзя»
и «сейчас занято» — разные новости: первая окончательная, вторая просит
повторить.
Константы, уже определённые
DefineSelfConstв этом запросе, останутся со старым значением: переопределить константу PHP не умеет. После записи читайте черезSysSelfKeyValues.
SysKeyValues($mKeyCode = '') — реестр
ключей: разделы вроде STORE_STATUS_KEY и пары «ключ →
значение» внутри них. Именно им подписывается статус товара, тип бренда,
вид веб-опции.
$status_set = MELBIS()->SysKeyValues('STORE_STATUS_KEY');
$name = $status_set[$store['status_key']] ?? '';Шесть дверей одной формы. У каждой сущности своя пара таблиц —
«опция» и «значения опции», — и отвечают все одинаково: список
опций, у каждой свой список значений под ключом
value. Ключа нет ни на одном уровне.
| Дверь | Что описывает |
|---|---|
SysEntityKeyValues($mEntity) |
доп. опции сущности: см. список имён ниже |
SysWebKeyValues() |
веб-опции |
SysOrderOptionValues() |
опции заказа: статус, оплата, доставка |
SysOrderStoreOptionValues() |
опции товара внутри заказа |
SysParamValues() |
параметры товаров |
SysFieldValues() |
регистрационные поля покупателя |
Имя сущности — это и есть имя её таблиц.
<сущность>_key держит опции,
<сущность>_key_value их значения,
<сущность>_key_set — что кому проставлено. Отдельной
двери на каждую сущность нет: заводится тройка таблиц по этому образцу —
и обе двери начинают её понимать.
$mEntity у первой — одно из двенадцати:
advert |
brand |
info |
param |
param_value |
provider |
provider_stock |
tax_area |
topic |
topic_filter |
user |
user_group |
Незнакомое имя останавливает разбор со списком принимаемых.
Пары param и param_value в этом списке
разные: у параметра товара свои опции, у каждого его
значения — свои. Так же различаются
provider и provider_stock, user и
user_group.
$param_set = MELBIS()->SysParamValues();
$param = array_column($param_set, null, 'skey');
foreach ( $param['COLOR']['value'] as $value )
{
echo $value['id'].' '.$value['name'];
}Строка значения полная. В ней и id, и
skey, и pos, и своя сумма — поэтому вынутое из
списка значение остаётся собой. Ключ ставится тем, кому он нужен, и на
любом уровне:
$value = array_column($param['COLOR']['value'], null, 'skey');
$red_id = $value['RED']['id'] ?? 0;Ключевать значения по skey можно только там, где вы
знаете свои данные: набор ведёт владелец магазина, и значение без
символьного имени — обычное дело, а все безымянные схлопнутся в одну
ячейку. По id такого не бывает.
Папки дерева опций тоже в списке. У них поднят
folder — по нему они и отличаются от опций.
Суммы приходят сырыми. У значений опций заказа и
параметров есть своя сумма и своя валюта (modify_sum или
set_sum плюс sum_curr_id). Пересчёт в валюту
витрины дверь не делает — курс берётся из
SysCurrencies.
SysEntityValues($mEntity, $mId = 0) —
то, что реально проставлено у сущности, а не список допустимого:
$option = MELBIS()->SysEntityValues('provider', $provider_id);
$min_buy = $option['MIN_BUY'] ?? '';Ответом идёт skey выбранного значения, а если у значения
символьного имени нет — его название; у опции со свободным вводом — сам
текст. Если на одной опции заведено несколько строк, отвечает заведённая
последней.
Ключ ответа — skey опции, поэтому опция без него
в ответ не попадает. Это единственное место, где символьное имя
обязательно: адресоваться к опции по id здесь нечем, а
свести все безымянные в одну ячейку значило бы отдать вместо них одну
случайную. Если дверь молчит на сущности, где значения заведены, —
первым делом смотреть skey у самих опций в
SysEntityKeyValues.
Правила, которыми владелец ограничивает выбор опций заказа. Три
таблицы, три двери, у всех порядок по id — колонки
pos у правил нет.
| Дверь | Что отдаёт |
|---|---|
SysOrderOptionBlocks() |
несовместимые значения опций заказа: value1_id,
value2_id, message |
SysOrderStoreOptionBlocks() |
то же для опций товара внутри заказа |
SysOrderOptionLimits() |
запрещённые переходы значения опции: option_id,
was_value_id, set_value_id,
message, и под ключом right — кого правило
касается |
Проверка — пересечение множеств в PHP, а не условие в запросе. Подставить выбранные значения в текст запроса значит завести запись кеша на каждую комбинацию, какую способен выбрать покупатель; дверь читает таблицу целиком, и запись одна:
$chosen = array_flip($value_ids);
foreach ( MELBIS()->SysOrderOptionBlocks() as $block )
{
if ( !isset($chosen[$block['value1_id']]) ) continue;
if ( !isset($chosen[$block['value2_id']]) ) continue;
// The first rule by id is the one shown, as the order of the list promises
$message = $block['message'];
break;
}right у лимита — не право, а адресат.
Строки order_option_limit_right говорят, на кого
действует запрет: user_id — на сотрудника,
group_id — на группу; пустое приходит нулём. Это не семья
прав (раздел 8): там дверь отвечает «можно ли», а здесь отдаёт правило с
текстом, и владелец магазина под него попадает наравне со всеми, если
его назвали. Касается ли правило человека, модуль решает сам — по
group из SysUsers:
$user_set = MELBIS()->SysUsers();
$person = array_column($user_set, null, 'id');
$group = $person[$user_id]['group'] ?? [];
foreach ( MELBIS()->SysOrderOptionLimits() as $limit )
{
if ( $limit['was_value_id'] !== $was_id ) continue;
if ( $limit['set_value_id'] !== $set_id ) continue;
foreach ( $limit['right'] as $right )
{
$mine = ( $right['user_id'] === $user_id || in_array($right['group_id'], $group, true) );
if ( !$mine ) continue;
$message = $limit['message'];
break 2;
}
}Правило без адресата в списке есть — с пустым right — и
не касается никого.
| Дверь | Что отдаёт |
|---|---|
SysCurrencies() |
валюты с multiplex, division и поставщиком
курса, списком |
SysClientGroups() |
группы покупателей с колонкой цены и скидкой |
SysLanguages() |
языки магазина, is_origin отмечает исходный |
SysProviderGroups() |
группы поставщиков, в каждой provider, а в поставщике
stock |
SysDiscGroups() |
группы скидок, в каждой rate — правила скидки |
SysTaxGroups() |
группы налогов, в каждой rate — ставки |
SysTaxAreaRules() |
области налогов, в каждой rule, а в правиле
rate |
SysTableSkey($mTableName) |
карта skey → id по любой справочной таблице |
Четыре последние двери отдают семью целиком, до самого нижнего уровня — почему так и как достать нужный уровень, в разделе 1.
rate у валюты — колонка производная. В
базе её нет: она считается из multiplex и
division по тому правилу, которое иначе переписывал бы
каждый магазин, — вместе с защитой от нуля в multiplex.
Сумму на неё умножают и получают сумму в базовой валюте, делить не нужно
нигде и никогда. Ключ для валюты почти всегда id: именно по
нему на неё ссылаются данные (price_curr_id,
sum_curr_id, pprice_curr_id).
$currency_set = MELBIS()->SysCurrencies();
$currency = array_column($currency_set, null, 'id');
$rate = $currency[$store['price_curr_id']]['rate'] ?? 1;
$price = round($sum * $rate, 2);Исходные multiplex и division остаются в
строке рядом. Если у валюты в магазине проставлен нулевой курс,
rate тоже выйдет нулём — это видно, и это данные магазина,
а не догадка движка.
Для таблиц без своей двери есть
SysTableSkey($mTableName). Условие в запросе пишется по
идентификатору, а разработчик знает символьное имя: id в
каждом магазине свой, skey один и тот же.
$advert = MELBIS()->SysTableSkey('advert');
$param_ban = [
'advert_id' => $advert['NEWYEAR']
];
$store = MELBIS()->SqlSelect(__LINE__, $command, $param_ban);Имя таблицы пишется без префикса, как всё в этом
разделе: ник подставляет сама дверь. Незнакомая таблица останавливает
разбор. Строки с пустым skey в карту не попадают — по нему
она и адресуется. Читается запрос статическим кешем, так что повторный
вызов в базу не идёт.
Двери несут id в каждой строке, так что для их таблиц
этот метод не нужен — он для редких справочников, до которых двери не
дошли.
SysOperations() — операции программы,
те, на которые выдаются права. Кроме имени и команды несут окно времени
(allow_from, allow_to) и пороги нагрузки. В
списке есть и папки дерева — они помечены folder, команды у
них нет.
SysWebInKeys() и
SysWebOutKeys() — встроенные и внешние
веб-модули: адрес, место, порядок и флаг auth. Нулевой
auth означает, что модуль не требует авторизации.
$menu = MELBIS()->SysWebInKeys();
foreach ( $menu as $module )
{
$skey = $module['skey'];
if ( $skey === '' ) continue;
if ( !MELBIS()->SysWebInRight($user_id, $skey) ) continue;
echo '<a href="'.$module['url'].'">'.$module['name'].'</a>';
}SysUsers() — сотрудники магазина. Кроме
своих колонок group_id и add_group_id каждый
несёт производное поле group — обе группы одним списком,
пустая выброшена.
SysUserGroups() — группы, и в каждой
под ключом users идентификаторы её людей,
а не их строки: у людей есть своя дверь, и группа называет их, а не
копирует. Разворачивается в две строки:
$user_set = MELBIS()->SysUsers();
$user = array_column($user_set, null, 'id');
foreach ( $group['users'] as $user_id )
{
$person = $user[$user_id];
}Группа, в которую ещё никого не перевели, в списке есть — просто с
пустым users.
SysUserLoginCheck($mLogin, $mPassCode)
— вход: возвращает идентификатор сотрудника или ноль. Ноль означает всё
сразу — нет такого логина, не подошёл пароль, человек заблокирован, — и
это правильно: снаружи эти случаи различать нельзя.
Глагол в имени не для красоты: это утилита, а не дверь — читает без
кеша, и одна на весь раздел принимает секрет. Пароль ни одна дверь не
читает, в списке колонок SysUsers его нет, а сама проверка
в общую память не попадает.
Десять дверей, по одной на семью прав. Первый аргумент везде — сотрудник, о котором спрашивают.
| Дверь | О чём спрашивают |
|---|---|
SysOperRight($mUserId, $mCommand = '') |
операция программы |
SysWebInRight($mUserId, $mSkey = '') |
встроенный веб-модуль |
SysWebOutRight($mUserId, $mSkey = '') |
внешний веб-модуль |
SysWebKeyRight($mUserId, $mSkey = '', $mFor = 'read') |
веб-опция: read, write,
remove |
SysTopicRight($mUserId, $mTopicId = 0, $mFor = 'frame') |
раздел: frame, price, ctrl,
browse |
SysInfoRight($mUserId, $mInfoId = 0, $mFor = 'info') |
инфо-элемент: info, value |
SysOrderRight($mUserId, $mValueId = 0) |
значение опции заказа: какие заказы видны |
SysOrderOptionRight($mUserId, $mOptionId = 0) |
опция заказа: можно ли править её в заказе |
SysOrderStoreOptionRight($mUserId, $mOptionId = 0) |
опция товара внутри заказа: то же |
SysSelfKeyRight($mUserId, $mCode = '') |
настройка магазина |
Три двери заказа — три разных вопроса об одних и тех же опциях.
SysOrderRight спрашивает про значение:
заказ виден тому, кому выданы значения его опций, статус среди них. Две
другие — про опцию: вправе ли сотрудник сам поставить
ей значение в заказе. Блокировки и лимиты (раздел 4.1) — не права, а
правила: они ограничивают выбор, а не доступ.
Ответ бывает трёх видов, и это стоит прочитать внимательно:
$allow = MELBIS()->SysWebKeyRight($user_id, 'FIN_ACCOUNT', 'read'); // true | false
$allow = MELBIS()->SysWebKeyRight($user_id, '', 'read'); // true | массивС названным объектом дверь отвечает «да» или «нет». Без объекта —
отдаёт множество разрешённого: карту имя => id. А если
человеку разрешено всё — он владелец или держит
супер-право, — вместо карты приходит true.
Отсюда правило: проверять надо === true, а не
«истинность». Непустой массив тоже истинный.
$allow = MELBIS()->SysTopicRight($user_id, 0, 'price');
if ( $allow !== true )
{
$id_line = implode(',', $allow);
$command .= " AND t.id IN ( $id_line )";
}Почему true, а не полный список. Дверь
без объекта нужна не для показа, а для фильтра — сузить выборку до
разрешённого. И «разрешено всё» здесь не список, в который попали все
строки, а другой факт: сужать не надо. Собери дверь
владельцу полный набор, в запрос ушло бы
AND t.id IN ( все разделы ) — лишняя работа, да ещё и
снимок, который устареет прямо в этом запросе.
Так что true несёт больше карты, а не меньше, и
!== true в примере выше — это не защита от неудобного типа,
а развилка по существу: фильтровать или нет.
Отсюда же и то, чего делать не стоит: перебирать этот ответ как
готовый список для показа. У владельца перебирать будет нечего. Нужен
именно список — соберите его из справочника
(SysWebKeyValues, SysOperations и прочие) и
оставьте в нём разрешённое.
Три правила комплекса двери соблюдают сами:
id = 1) получает
«да» на любой вопрос, без похода в базу.PUT_OUTSIDE_RIGHT открывает все веб-модули и веб-опции,
PUT_TOPIC_RIGHT — все разделы, PUT_INFO_RIGHT
— все инфо-элементы, PUT_ORDER_OPTION — все три двери
заказа разом, PUT_KEY_VALUE — все настройки.Идентификатор сотрудника обязателен и обязан быть числом больше нуля: право — системный вопрос, и спрашивать его не про кого нельзя. Ноль или чужая строка останавливают разбор.
Две утилиты SysPick* работают с тем, что дверь уже
принесла: одна спускается по семье, другая снимает с неё вложенное. В
базу не ходят ни та ни другая.
SysPickBranch($mSet, $mPath, $mKey = '', $mDefault = [])
— спуститься по ответу семейной двери до нужного уровня и получить его
плоским списком или картой.
$group_set = MELBIS()->SysProviderGroups();
$provider = MELBIS()->SysPickBranch($group_set, 'provider', 'id');
$mine = $provider[$provider_id] ?? [];Путь пишется через слэш — шаг вниз по ключу вложенности, на любую глубину:
$stock = MELBIS()->SysPickBranch($group_set, 'provider/stock', 'id');
$rate = MELBIS()->SysPickBranch(MELBIS()->SysTaxAreaRules(), 'rule/rate', 'id');Дважды встреченная строка становится одной: сотрудник в двух группах
придёт один раз, потому что карта ключуется по id.
Без $mKey возвращается список без ключа. Незнакомый ключ
пути останавливает разбор со списком того, что в строке есть, и отдаёт
$mDefault — по умолчанию пустой массив, так что
isset по ключу работает и после промаха.
SysPickFlat($mSet, $mKey = '', $mDefault = [])
— те же строки, но без всего вложенного. Обе утилиты обычно идут парой:
одна берёт нижний уровень, вторая — верхний без него.
$keys = MELBIS()->SysEntityKeyValues('provider');
$value_set = MELBIS()->SysPickBranch($keys, 'value');
$key_set = MELBIS()->SysPickFlat($keys);$mKey работает так же, как у соседки: пустой — список,
заданный — карта.
$key = MELBIS()->SysPickFlat($keys, 'skey');
$min = $key['MIN_BUY'] ?? [];Выбрасывается всё, что массив, — и вложенный
уровень, и производное поле-список. У сотрудника вместе с
provider уйдёт и group, но
group_id с add_group_id остаются на месте, так
что данные не теряются: пропадает только удобная выжимка.
SysTableColumns($mTableName) — колонки
таблицы и их типы, картой колонка => тип. Имя без
префикса, как всё в этом разделе.
$column = MELBIS()->SysTableColumns('topic');
$has_folder = isset($column['folder']);Утилита нужна там, где имя колонки приходит со стороны — из настройки магазина, из параметров модуля, из чужого кода. Схема сама себе белый список: чего в ней нет, то и не колонка. На ней стоят обе следующие семьи — и дерево, и зависимости сверяют по ней каждое имя, прежде чем взять его в запрос.
Дерево в базе — это три колонки: tindex (родитель),
tlevel (уровень), absindex (номер в обходе).
Так устроены разделы каталога, инфо-элементы, поля покупателя, операции
программы, веб-опции и другие. Таблицу без этих трёх колонок утилиты
деревом не считают и отвечают отказом.
Одна таблица может держать несколько деревьев — тогда их различает
область: колонка и её значение. Так устроены
альтернативные каталоги в topic_alt: все в одной таблице, а
к какому из них принадлежит строка, говорит kind_key со
значением из реестра TOPIC_ALT_KIND_KEY. Область сверяется
со схемой и уходит в запрос параметром, а не текстом.
$id = MELBIS()->SysTreeAdd('topic_alt', $parent_id, ['kind_key' => 'kTree']);Утилиты дерева только пишут. Читающих среди них нет,
и заводить их незачем: дерево лежит в обычной таблице, и обычный
SELECT по ней даёт больше — ваши колонки, ваш фильтр, ваш
порядок. Дверь отдавала бы чужой набор полей, и за остальными пришлось
бы идти вторым запросом.
Порядок задаёт сама таблица: absindex — номер в обходе
сверху вниз, tlevel — глубина.
ORDER BY absindex рисует дерево как оно есть,
tlevel даёт отступ:
$command = "SELECT id, name, tlevel, seo_psu
FROM {DBNICK}_topic
WHERE no_visible = 0
ORDER BY absindex
";
$topic_set = MELBIS()->SqlSelectStatic(__LINE__, $command);Из того же порядка берётся и ветвь: узел и всё, что под ним, лежат по
absindex непрерывным куском — от номера
узла до первого следующего узла, чей tlevel не глубже. Так
«раздел со всеми подразделами» отбирается условием в запросе, без обхода
в PHP.
Сами утилиты меняют структуру — где узел стоит, в каком порядке и кому виден:
| Утилита | Что делает | Что отвечает |
|---|---|---|
SysTreeAdd($mTable, $mParentId = 0, $mScope = []) |
заводит узел под родителем | id нового узла или 0 |
SysTreeRightCopy($mTable, $mParentId, $mId) |
копирует узлу гранты родителя | число скопированных строк |
SysTreeMove($mTable, $mId, $mParentId = 0, $mScope = []) |
переносит узел вместе с ветвью | true или false |
SysTreeShift($mTable, $mId, $mDown = false, $mScope = []) |
меняет узел местами с соседом | true или false |
SysTreeDelete($mTable, $mId, $mScope = []) |
сносит узел вместе с ветвью | число снесённых строк |
Три вещи о записи стоит знать заранее.
Строка рождается голой. SysTreeAdd
пишет только id и колонки области — имя, тип и всё
остальное дописывает вызывающий своим SqlUpdate. Права
родителя узел тоже не наследует сам: пока их не скопировали, новый
раздел или инфо-элемент видит один владелец. Второй акт —
SysTreeRightCopy: она находит у таблицы двойник
<таблица>_right и переписывает узлу гранты родителя.
У таблицы без двойника копировать нечего, ответ ноль — и это не
ошибка.
Блокировка на вызывающем. Изменённой таблицу утилиты
метят сами, так что кеш этого же запроса правку увидит. А вот
SqlTableLock они не берут — берите его вокруг своей работы
целиком, а не вокруг каждого вызова.
Перенос внутрь собственной ветви не пишет ничего и
отвечает false: узел не может стать потомком самого себя.
Так же отвечает сдвиг узла, у которого с той стороны нет соседа: менять
не с кем — не ошибка, но и не сдвиг.
Там, где дерева нет, порядок держит колонка pos:
справочники, значения опций, ставки, языки. По ней двери и сортируют.
Таблицу без pos эти утилиты списком не считают и отвечают
отказом.
Одна таблица держит несколько списков — тогда их различает
область, та же, что у дерева: колонка и её значение. У
ставок налога это ['group_id' => 3], у значений опции —
['option_id' => 7].
| Утилита | Что делает | Что отвечает |
|---|---|---|
SysPosShift($mTable, $mId, $mDown = false, $mScope = []) |
меняет строку местами с соседом | true или false |
SysPosOrder($mTable, $mIdSet, $mScope = []) |
ставит названные строки в начало в заданном порядке | число переписанных строк |
$taken = MELBIS()->SqlTableLock(__LINE__, '{DBNICK}_tax_rate');
if ( $taken )
{
$scope_rate = [
'group_id' => $group_id
];
MELBIS()->SysPosOrder('tax_rate', [$third_id, $first_id], $scope_rate);
MELBIS()->SqlTableUnlock(__LINE__, '{DBNICK}_tax_rate');
}Три вещи стоит знать заранее — те же, что у дерева.
pos идёт подряд, 1..N. После любого
вызова дырок в нумерации не остаётся: утилита перенумеровывает весь
список области. Пишутся при этом только сдвинувшиеся
строки, остальные не трогаются вовсе.
Названные идут вперёд, остальные держат свой порядок за ними. Полный список и частичный работают одинаково: назвали все строки — получили ровно тот порядок, назвали две — они встали первыми. Строка, которой в этой области нет, останавливает разбор, и до записи дело не доходит: половина порядка хуже, чем никакого.
Блокировка на вызывающем. Изменённой таблицу утилиты
метят сами, а SqlTableLock берите вокруг своей работы
целиком.
Сдвиг строки, у которой с той стороны нет соседа, отвечает
false и ничего не пишет — как и у дерева.
Внешних ключей база не держит, поэтому что на чём висит движок знает списком, написанным вручную. Модуль может пополнить этот список на время своего запроса и спросить, кто повиснет в воздухе, если строку убрать.
SysDependAdd($mMain, $mTable, $mKey, $mKeyNullable = false)
— объявить связь: в таблице $mTable колонка
$mKey называет строку таблицы $mMain. Обе
таблицы и колонка сверяются со схемой сразу: связь, взятая на веру,
удаляла бы по имени, которого нет, а подметание — не то место, где это
выясняют.
$mKeyNullable говорит, что колонка может стоять
пустой. Тогда пустая строка не сирота: она висит ни на ком по
своей воле, и подметание её щадит. Так устроены гранты —
topic_right.user_id пуст, когда право выдано группе.
Связь живёт до конца запроса. Связь, которую движок уже держит —
встроенную или объявленную раньше, — второй раз не ложится: ответ
false, и счёт не меняется.
SysDepends($mTable) — что висит на
таблице, списком строк main, table,
key, nullable.
SysDependCount($mTable, $mIds) —
сколько строк повиснет, если убрать названные. Ответ — карта
таблица => число, только ненулевые. Спрашивать надо
до удаления, пока строки ещё на месте:
$count = MELBIS()->SysDependCount('topic', $topic_id);
if ( !empty($count) )
{
// сказать человеку, что именно уйдёт следом
}SysDependSweep($mTable, $mTries = 10, $mPause = 500)
— убрать то, что уже повисло. Обходит всю цепочку: у зависимого бывают
свои зависимые. Отвечает строкой на связь, а не на
таблицу:
[
['main' => 'user', 'table' => 'user_chat', 'key' => 'user_id', 'gone' => 12, 'busy' => false],
['main' => 'user', 'table' => 'user_chat', 'key' => 'to_user_id', 'gone' => 0, 'busy' => true],
]Одна таблица бывает привязана дважды разными колонками — как
user_chat, где человек стоит и отправителем, и получателем.
Строка на связь потому и выбрана: иначе один проход затирал бы итог
другого.
Замок берётся только там, где сироты нашлись: сначала связь
проверяется дешёвым счётом, и где подметать нечего — а это обычный
случай, — строка отвечает gone 0 без блокировки. Само
удаление находит своих сирот заново уже под замком, так что
gone говорит о том, что ушло на самом деле, а не о счёте
предпроверки.
busy значит, что таблицу держал кто-то другой и её
пропустили — уже пересидев чужую работу: попытки и пауза в миллисекундах
уходят в SqlTableLock, и с умолчаниями каждая занятая
таблица цепочки ждётся до пяти секунд. Подметание берёт каждую таблицу в
работу само, поэтому зовите его до того, как возьмёте в
работу свой набор, или после того, как отпустите.
Не всё, что ссылается, зависит: товар ссылается на валюту и бренд, но
умирать с ними не должен — строка остаётся, обнуляется ссылка. Такие
связи живут своей картой (у валюты — пять ценовых
колонок товара, у бренда — brand_id), и жёсткое подметание
их не видит вовсе. Двери — зеркало жёстких:
| Дверь | Что делает |
|---|---|
SysRelates($mTable) |
мягкие связи таблицы списком: main, table,
key |
SysRelateCount($mTable, $mIds) |
сколько строк останутся без ссылки, если убрать названные — спрашивать до удаления |
SysRelateSweep($mTable, $mTries = false, $mPause = false) |
обнулить ссылки на уже удалённые строки |
Проход устроен как жёсткий — предпроверка счётом, потабличный замок,
метка, busy после пересиживания, — но с двумя отличиями по
смыслу. Цепочки нет: обнулённая ссылка никого не осиротила, идти вглубь
не за чем. И щадящего вида нет: обнуляются только непустые ссылки,
пустая и так пуста. В строке отчёта вместо gone стоит
cleared:
[
['main' => 'currency', 'table' => 'store', 'key' => 'price_curr_id', 'cleared' => 8, 'busy' => false],
]Обнуление — дело по крайней надобности, само оно нигде не зовётся: жёсткое подметание строк не трогает, и решение отвязать товары от удалённой валюты принимает тот, кто валюту удалил.
Справочные двери читают справочники, а не данные
магазина. Товары, заказы, покупатели, разделы каталога и привязки к ним
читаются обычными Sql*-методами: их объём растёт вместе с
магазином, и список целиком там не окупится.
Утилиты дерева и зависимостей — исключение по смыслу: они работают со структурой любой таблицы, каталога в том числе. Но строк они не отдают: дерево пишет, зависимости считают.
Граница видна на примере опций: список допустимых значений параметра
отдаёт SysParamValues, а то, какие значения проставлены
конкретному товару, лежит в store_param и читается
запросом.