Yandex Metrika
sanches.free 1 просмотр

Сколько элементов в разделе инфоблока в D7: SectionElementTable и агент с UF-счётчиками

Зачем SectionElementTable

Элемент может быть привязан к нескольким разделам. Таблица \Bitrix\Iblock\SectionElementTable отражает именно связку «раздел ↔ элемент»: одна строка на пару. Агрегат COUNT(*) по фильтру по разделу даёт число активных элементов с учётом множественных привязок — без двойного подсчёта «по элементу», если нужна логика на уровне связей раздел–элемент.

Перед кодом всегда подключают модуль: \Bitrix\Main\Loader::includeModule('iblock');

Разовый подсчёт для одного раздела

Ниже — вымышленные идентификаторы: инфоблок $catalogPk = 10, раздел $leafSectionPk = 989. В фильтре оставлены только активные элементы этого инфоблока:

use Bitrix\Main\Loader;
use Bitrix\Main\ORM\Fields\ExpressionField;
use Bitrix\Iblock\SectionElementTable;

Loader::includeModule('iblock');

$catalogPk = 10;
$leafSectionPk = 989;

$aggregate = SectionElementTable::getList([
    'runtime' => [
        new ExpressionField('TALLY', 'COUNT(*)'),
    ],
    'filter' => [
        '=IBLOCK_SECTION_ID' => $leafSectionPk,
        'IBLOCK_ELEMENT.ACTIVE' => 'Y',
        '=IBLOCK_ELEMENT.IBLOCK_ID' => $catalogPk,
    ],
    'select' => ['TALLY'],
]);

$directQty = 0;
if ($row = $aggregate->fetch()) {
    $directQty = (int)$row['TALLY'];
}

Смысл runtime — объявить псевдополе с агрегатной функцией, которое затем можно вывести в select.

Хранить числа на разделе: задача для агента

Иногда в шаблоне каталога не хочется каждый раз гонять COUNT: проще два пользовательских поля раздела — сколько активных элементов «здесь» и сколько суммарно «здесь + подразделы». Названия полей в примере: UF_ELEMENT_COUNT и UF_ELEMENT_RECURSIVE_COUNT; их нужно завести в настройках инфоблока для разделов.

Идея обхода: сортировка разделов по DEPTH_LEVEL DESC, чтобы дочерние обрабатывались раньше родителей. Для каждого раздела считают прямой $directQty, кладут в стек по ID, затем один проход суммирует вклад каждого раздела в цепочку родителей. В конце через CIBlockSection::Update записывают только изменённые значения.

Полностью класс агента (исправление логики сравнения)

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

namespace Partner\IblockAssist;

use Bitrix\Main\Loader;
use Bitrix\Main\ORM\Fields\ExpressionField;
use Bitrix\Iblock\SectionElementTable;
use Bitrix\Iblock\Model\Section;
use CIBlockSection;

class SectionLeafAndSubtreeCounters
{
    public static function runAgent(): string
    {
        if (!Loader::includeModule('iblock')) {
            return '\\'.__METHOD__.'();';
        }

        $catalogPk = 10;

        /** @var class-string|\Bitrix\Main\ORM\Data\DataManager $entity */
        $entity = Section::compileEntityByIblock($catalogPk);

        $walker = $entity::getList([
            'order' => ['DEPTH_LEVEL' => 'DESC'],
            'filter' => [
                'IBLOCK_ID' => $catalogPk,
                'ACTIVE' => 'Y',
            ],
            'select' => [
                'ID',
                'DEPTH_LEVEL',
                'IBLOCK_SECTION_ID',
                'UF_ELEMENT_COUNT',
                'UF_ELEMENT_RECURSIVE_COUNT',
            ],
        ]);

        $stack = [];

        while ($sectionPayload = $walker->fetch()) {
            $aggregate = SectionElementTable::getList([
                'runtime' => [
                    new ExpressionField('TALLY', 'COUNT(*)'),
                ],
                'filter' => [
                    '=IBLOCK_SECTION_ID' => $sectionPayload['ID'],
                    'IBLOCK_ELEMENT.ACTIVE' => 'Y',
                    '=IBLOCK_ELEMENT.IBLOCK_ID' => $catalogPk,
                ],
                'select' => ['TALLY'],
            ]);

            $directQty = 0;
            if ($countRow = $aggregate->fetch()) {
                $directQty = (int)$countRow['TALLY'];
            }

            $parentEdge = (int)$sectionPayload['IBLOCK_SECTION_ID'];
            $selfEdge = (int)$sectionPayload['ID'];

            foreach ([$parentEdge, $selfEdge] as $edgeId) {
                if ($edgeId <= 0) {
                    continue;
                }
                if (!isset($stack[$edgeId])) {
                    $stack[$edgeId] = [
                        'directQty' => 0,
                        'subtreeQty' => 0,
                        'nestedChildren' => 0,
                    ];
                }
            }

            if ($parentEdge > 0) {
                $stack[$parentEdge]['nestedChildren']++;
            }

            $stack[$selfEdge]['directQty'] = $directQty;
            $stack[$selfEdge]['section'] = $sectionPayload;
        }

        foreach ($stack as $accumulatorPayload) {
            if (!isset($accumulatorPayload['section'])) {
                continue;
            }

            $trail = [];
            $cursor = (int)$accumulatorPayload['section']['IBLOCK_SECTION_ID'];
            $guard = 0;
            while (
                $cursor > 0
                && isset($stack[$cursor]['section'])
                && ++$guard <= 500
            ) {
                $trail[] = $cursor;
                $cursor = (int)$stack[$cursor]['section']['IBLOCK_SECTION_ID'];
            }

            $selfPk = (int)$accumulatorPayload['section']['ID'];
            $qty = $accumulatorPayload['directQty'];
            $stack[$selfPk]['subtreeQty'] += $qty;
            foreach ($trail as $ancestorPk) {
                $stack[$ancestorPk]['subtreeQty'] += $qty;
            }
        }

        foreach ($stack as $slicePk => $accumulatorPayload) {
            if (!isset($accumulatorPayload['section'])) {
                continue;
            }
            $fields = [];
            $storedDirect = (int)$accumulatorPayload['section']['UF_ELEMENT_COUNT'];
            $storedSubtree = (int)$accumulatorPayload['section']['UF_ELEMENT_RECURSIVE_COUNT'];

            if ($accumulatorPayload['directQty'] !== $storedDirect) {
                $fields['UF_ELEMENT_COUNT'] = $accumulatorPayload['directQty'];
            }
            if ($accumulatorPayload['subtreeQty'] !== $storedSubtree) {
                $fields['UF_ELEMENT_RECURSIVE_COUNT'] = $accumulatorPayload['subtreeQty'];
            }
            if (!$fields) {
                continue;
            }
            $broker = new CIBlockSection();
            $broker->Update((int)$slicePk, $fields);
        }

        return '\\'.__METHOD__.'();';
    }
}

Строка для записи агента: \Partner\IblockAssist\SectionLeafAndSubtreeCounters::runAgent();

Важные оговорки

  • Убедитесь, что торговые и основные инфоблоки вашей схемы не смешиваются в одном $catalogPk, если SKU вынесены отдельно.
  • Для огромных деревьев ночной cron предпочтительнее триггеров на каждом сохранении элемента — иначе пишущие транзакции будут упираться в пересчёт очередности.
  • Если в админке привязки меняются часто, добавьте обновление счётчиков в обработчики событий инфоблока или смешайте агент с инкрементальной логикой.

Итог

SectionElementTable + ExpressionField дают короткий ORM-подсчёт по связи раздела с элементами. Агент с обходом снизу вверх и двумя UF-полями закрывает вывод счётчиков в меню и карточках разделов без повторных тяжёлых запросов на каждый хит.

Не хотите копаться сами?

Починю за 1-3 дня. Без предоплаты — оплата по результату.

15+ лет опыта с 1С-Битрикс · Без предоплаты · 7 дней гарантии