Системные двери и утилиты

Магазин держит в базе десятки справочников: реестр настроек, опции товаров и заказов, поля покупателя, валюты, поставщиков, языки, операции программы, веб-модули, сотрудников и выданные им права. Всё это модуль читает постоянно и почти никогда не меняет.

Методы Sys* двух родов. Двери читают эти справочники, каждая свой источник одним и тем же запросом — разделы 3–8. Утилиты делают системную работу, которую иначе каждый модуль писал бы сам: спускаются по ответу двери, смотрят схему таблицы, перестраивают дерево, считают и подметают зависимости — разделы 9–13. Две утилиты стоят среди дверей своих разделов: запись настройки и проверка входа.

Зачем дверь, если запрос пишется в три строки. Валюты и правда достаются обычным SELECT — но написать его самому значит завести свой текст запроса. Статический кеш адресуется по тексту: чужой текст — вторая запись, своя копия тех же данных и свой поход в базу при промахе. Причём хватит переставить пробел или перенос строки, чтобы текст стал другим, — и записей станет три.

Дверь даёт то, чего запросом не добиться: все модули магазина попадают в одну и ту же запись кеша. Кто первым спросил валюты, оплатил запрос за всех остальных — за соседний модуль, за следующую страницу, за весь срок жизни записи. Поэтому системную таблицу читают только через её дверь, а если двери нет — её стоит завести, а не обойти.

$param_set = MELBIS()->SysParamValues();
$param = array_column($param_set, null, 'skey');

$ban_id = $param['BAN']['id'];

1. Как устроены двери

Дверь отдаёт список полных строк. Ключа у ответа нет и аргумента-селектора тоже: ключ выбирает тот, кому он нужен, одной строкой.

$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 ни двери, ни утилиты не принимают. Запрос у них свой, и строка модуля к нему отношения не имеет. В отказах и в журнале вместо номера строки стоит имя метода.

2. Что кешируется, а что читается живьём

Правило одно: дверь читает статическим кешем, утилита не кеширует ничего. Справочники, сотрудники, права — всё это двери, и все они идут через кеш.

Свежесть от этого не страдает. Адрес статической записи собран из метки изменения таблицы, а метку двигает всякая запись через движок — и модуль на PHP, и нативный клиент, который пишет теми же скриптами обмена. Изменилась таблица — у записи стал другой адрес, и следующий же запрос собирает её заново. Сутки в сроке жизни записи это не задержка изменения, а срок для записи, которую никто не отменил.

Метку не сдвинет только запись мимо движка: сырой запрос из SQL-инструмента, внешний скрипт, правка руками в базе. Но такая правка не видна ни в правах, ни в ценах, ни в товарах — это свойство обхода, а не отдельных таблиц.

Утилиты в базу по большей части не ходят вовсе: обе SysPick* перебирают готовый массив, который дверь уже принесла, а двери дерева и SysSelfKeyModify пишут. Но трое читают, и каждая без статического кеша — по своей причине.

SysUserLoginCheck — глагол в имени говорит, что это проверка, а не список. Пароль в общую память не кладётся: статический кеш хранит параметры запроса внутри самой записи, и пароль лежал бы там открытым. Вход всегда идёт в базу.

SysTableColumns — в тексте запроса нет ни одной таблицы магазина, а значит нет и метки, по которой запись можно было бы отменить. Схема запоминается на время запроса: внутри одного запроса колонки не меняются.

SysDependCount считает по живой базе: запрос собирается под вызов, а ответ нужен точный на сейчас — по нему решают, можно ли удалять.

В памяти запроса дверь ничего не держит: каждое обращение идёт в статический кеш заново, а адрес там сдвигает любая запись. Поэтому правка, сделанная раньше на этой же странице, видна всем, кто спросит после неё.

2.1. Дверь и кеш модуля

Зависимость модуля от таблицы движок находит сам — по запросу, который дверь выполнила. Отмечать её в среде не нужно и негде: с точки зрения учёта запрос двери ничем не отличается от запроса, написанного в модуле руками. Как список собирается и когда пополняется — в разделе «Кеширование».

А вот страница, собранная по правам, в кеш модуля не годится. Дверь ответит свежим, но страница, попавшая в кеш, достанется и тому, у кого этих прав нет. Модули, которые спрашивают права, работают под конкретным сотрудником, и кешировать их незачем.

3. Реестр настроек

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']] ?? '';

4. Опции и их значения

Шесть дверей одной формы. У каждой сущности своя пара таблиц — «опция» и «значения опции», — и отвечают все одинаково: список опций, у каждой свой список значений под ключом 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.

4.1. Блокировки и лимиты заказа

Правила, которыми владелец ограничивает выбор опций заказа. Три таблицы, три двери, у всех порядок по 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 — и не касается никого.

5. Справочники магазина

Дверь Что отдаёт
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 в каждой строке, так что для их таблиц этот метод не нужен — он для редких справочников, до которых двери не дошли.

6. Реестры программы

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>';
}

7. Люди

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 его нет, а сама проверка в общую память не попадает.

8. Права

Десять дверей, по одной на семью прав. Первый аргумент везде — сотрудник, о котором спрашивают.

Дверь О чём спрашивают
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 и прочие) и оставьте в нём разрешённое.

Три правила комплекса двери соблюдают сами:

Идентификатор сотрудника обязателен и обязан быть числом больше нуля: право — системный вопрос, и спрашивать его не про кого нельзя. Ноль или чужая строка останавливают разбор.

9. Разбор ответа двери

Две утилиты 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 остаются на месте, так что данные не теряются: пропадает только удобная выжимка.

10. Схема таблицы

SysTableColumns($mTableName) — колонки таблицы и их типы, картой колонка => тип. Имя без префикса, как всё в этом разделе.

$column = MELBIS()->SysTableColumns('topic');
$has_folder = isset($column['folder']);

Утилита нужна там, где имя колонки приходит со стороны — из настройки магазина, из параметров модуля, из чужого кода. Схема сама себе белый список: чего в ней нет, то и не колонка. На ней стоят обе следующие семьи — и дерево, и зависимости сверяют по ней каждое имя, прежде чем взять его в запрос.

11. Дерево

Дерево в базе — это три колонки: 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: узел не может стать потомком самого себя. Так же отвечает сдвиг узла, у которого с той стороны нет соседа: менять не с кем — не ошибка, но и не сдвиг.

12. Порядок плоского списка

Там, где дерева нет, порядок держит колонка 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 и ничего не пишет — как и у дерева.

13. Зависимости

Внешних ключей база не держит, поэтому что на чём висит движок знает списком, написанным вручную. Модуль может пополнить этот список на время своего запроса и спросить, кто повиснет в воздухе, если строку убрать.

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, и с умолчаниями каждая занятая таблица цепочки ждётся до пяти секунд. Подметание берёт каждую таблицу в работу само, поэтому зовите его до того, как возьмёте в работу свой набор, или после того, как отпустите.

13.1. Мягкие связи

Не всё, что ссылается, зависит: товар ссылается на валюту и бренд, но умирать с ними не должен — строка остаётся, обнуляется ссылка. Такие связи живут своей картой (у валюты — пять ценовых колонок товара, у бренда — 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],
    ]

Обнуление — дело по крайней надобности, само оно нигде не зовётся: жёсткое подметание строк не трогает, и решение отвязать товары от удалённой валюты принимает тот, кто валюту удалил.

14. Что сюда не входит

Справочные двери читают справочники, а не данные магазина. Товары, заказы, покупатели, разделы каталога и привязки к ним читаются обычными Sql*-методами: их объём растёт вместе с магазином, и список целиком там не окупится.

Утилиты дерева и зависимостей — исключение по смыслу: они работают со структурой любой таблицы, каталога в том числе. Но строк они не отдают: дерево пишет, зависимости считают.

Граница видна на примере опций: список допустимых значений параметра отдаёт SysParamValues, а то, какие значения проставлены конкретному товару, лежит в store_param и читается запросом.