Компоненты Битрикс: пишем свой с нуля

Штатные компоненты Битрикс закрывают 80% задач. Но когда нужна нестандартная логика — вывод данных из нескольких инфоблоков с фильтрацией, кастомная пагинация, интеграция с внешним API — приходится писать свой компонент. Показываем пошаговый процесс создания компонента с нуля: от файловой структуры до кеширования.

Файловая структура компонента

Компонент создаём в папке /local/components/ — не в /bitrix/components/, чтобы не потерять при обновлении:

/local/components/mycompany/news.list/
├── .description.php     # Описание для визуального редактора
├── .parameters.php      # Настраиваемые параметры
├── class.php            # Основная логика (ООП)
├── templates/
│   └── .default/
│       ├── template.php         # Шаблон вывода
│       ├── result_modifier.php  # Модификация данных перед выводом
│       ├── component_epilog.php # Код после вывода (вне кеша)
│       └── style.css
└── lang/
    └── ru/
        ├── .description.php
        └── .parameters.php

Пространство имён mycompany — обязательно. Без него Битрикс не найдёт компонент.

.description.php — регистрация компонента

<?php
if (!defined('B_PROLOG_INCLUDED') || B_PROLOG_INCLUDED !== true) die();

$arComponentDescription = [
    'NAME' => 'Список новостей',
    'DESCRIPTION' => 'Выводит список новостей из инфоблока с фильтрацией и пагинацией',
    'ICON' => '/images/icon.gif',
    'SORT' => 10,
    'CACHE_PATH' => 'Y',
    'PATH' => [
        'ID' => 'mycompany',
        'NAME' => 'Мои компоненты',
        'CHILD' => [
            'ID' => 'content',
            'NAME' => 'Контент',
        ],
    ],
];

.parameters.php — параметры компонента

Параметры — это то, что редактор сайта настраивает в визуальном редакторе:

<?php
if (!defined('B_PROLOG_INCLUDED') || B_PROLOG_INCLUDED !== true) die();

use Bitrix\Main\Loader;

Loader::includeModule('iblock');

$arIBlockTypes = CIBlockParameters::GetIBlockTypes();

$arIBlocks = [];
$rsIBlocks = \Bitrix\Iblock\IblockTable::getList([
    'filter' => ['ACTIVE' => 'Y'],
    'select' => ['ID', 'NAME'],
    'order' => ['SORT' => 'ASC'],
]);
while ($row = $rsIBlocks->fetch()) {
    $arIBlocks[$row['ID']] = '[' . $row['ID'] . '] ' . $row['NAME'];
}

$arComponentParameters = [
    'GROUPS' => [
        'SETTINGS' => [
            'NAME' => 'Основные настройки',
            'SORT' => 100,
        ],
        'PAGER' => [
            'NAME' => 'Пагинация',
            'SORT' => 200,
        ],
    ],
    'PARAMETERS' => [
        'IBLOCK_TYPE' => [
            'PARENT' => 'SETTINGS',
            'NAME' => 'Тип инфоблока',
            'TYPE' => 'LIST',
            'VALUES' => $arIBlockTypes,
            'REFRESH' => 'Y',
        ],
        'IBLOCK_ID' => [
            'PARENT' => 'SETTINGS',
            'NAME' => 'Инфоблок',
            'TYPE' => 'LIST',
            'VALUES' => $arIBlocks,
            'REFRESH' => 'Y',
        ],
        'NEWS_COUNT' => [
            'PARENT' => 'PAGER',
            'NAME' => 'Количество на странице',
            'TYPE' => 'STRING',
            'DEFAULT' => '10',
        ],
        'SORT_BY' => [
            'PARENT' => 'SETTINGS',
            'NAME' => 'Сортировка по',
            'TYPE' => 'LIST',
            'VALUES' => [
                'ACTIVE_FROM' => 'Дате активности',
                'SORT' => 'Индексу сортировки',
                'NAME' => 'Названию',
            ],
            'DEFAULT' => 'ACTIVE_FROM',
        ],
        'SORT_ORDER' => [
            'PARENT' => 'SETTINGS',
            'NAME' => 'Направление сортировки',
            'TYPE' => 'LIST',
            'VALUES' => [
                'DESC' => 'По убыванию',
                'ASC' => 'По возрастанию',
            ],
            'DEFAULT' => 'DESC',
        ],
        'CACHE_TIME' => ['DEFAULT' => 3600],
    ],
];

class.php — основная логика

Современный подход — ООП через наследование от CBitrixComponent:

<?php
if (!defined('B_PROLOG_INCLUDED') || B_PROLOG_INCLUDED !== true) die();

use Bitrix\Main\Loader;
use Bitrix\Main\ArgumentException;
use Bitrix\Iblock\ElementTable;

class MyCompanyNewsList extends CBitrixComponent
{
    private int $iblockId;
    private int $pageSize;

    public function onPrepareComponentParams($arParams): array
    {
        $arParams['IBLOCK_ID'] = (int)($arParams['IBLOCK_ID'] ?? 0);
        $arParams['NEWS_COUNT'] = (int)($arParams['NEWS_COUNT'] ?? 10);
        $arParams['SORT_BY'] = $arParams['SORT_BY'] ?? 'ACTIVE_FROM';
        $arParams['SORT_ORDER'] = $arParams['SORT_ORDER'] ?? 'DESC';
        $arParams['CACHE_TIME'] = (int)($arParams['CACHE_TIME'] ?? 3600);

        return $arParams;
    }

    public function executeComponent(): void
    {
        if (!Loader::includeModule('iblock')) {
            ShowError('Модуль iblock не установлен');
            return;
        }

        $this->iblockId = $this->arParams['IBLOCK_ID'];
        $this->pageSize = $this->arParams['NEWS_COUNT'];

        if ($this->iblockId <= 0) {
            ShowError('Не указан инфоблок');
            return;
        }

        if ($this->startResultCache()) {
            $this->fetchData();

            if (empty($this->arResult['ITEMS'])) {
                $this->abortResultCache();
            }

            $this->includeComponentTemplate();
        }
    }

    private function fetchData(): void
    {
        $navParams = \CDBResult::GetNavParams([
            'nPageSize' => $this->pageSize,
            'bDescPageNumbering' => false,
        ]);

        $arFilter = [
            'IBLOCK_ID' => $this->iblockId,
            'ACTIVE' => 'Y',
        ];

        $arSort = [
            $this->arParams['SORT_BY'] => $this->arParams['SORT_ORDER'],
        ];

        $rsElements = \CIBlockElement::GetList(
            $arSort,
            $arFilter,
            false,
            ['nPageSize' => $this->pageSize, 'iNumPage' => $navParams['PAGEN']],
            ['ID', 'NAME', 'DETAIL_PAGE_URL', 'PREVIEW_TEXT', 'PREVIEW_PICTURE', 'ACTIVE_FROM']
        );

        $this->arResult['ITEMS'] = [];

        while ($arElement = $rsElements->GetNext()) {
            if ($arElement['PREVIEW_PICTURE']) {
                $arElement['PREVIEW_PICTURE'] = \CFile::GetFileArray($arElement['PREVIEW_PICTURE']);
            }
            $this->arResult['ITEMS'][] = $arElement;
        }

        $this->arResult['NAV_STRING'] = $rsElements->GetPageNavString(
            'Новости',
            'default',
            true
        );
        $this->arResult['NAV_RESULT'] = $rsElements;
    }
}

Важные моменты:

  • onPrepareComponentParams — валидация и приведение типов параметров. Вызывается до кеширования.
  • startResultCache — проверяет, есть ли кеш. Если да — сразу отдаёт шаблон из кеша, fetchData не вызывается.
  • abortResultCache — если данных нет, не кешируем пустой результат.

template.php — шаблон вывода

<?php
if (!defined('B_PROLOG_INCLUDED') || B_PROLOG_INCLUDED !== true) die();

/** @var array $arResult */
/** @var array $arParams */
?>

<div class="news-list">
    <?php foreach ($arResult['ITEMS'] as $arItem): ?>
        <article class="news-list__item">
            <?php if (!empty($arItem['PREVIEW_PICTURE'])): ?>
                <div class="news-list__image">
                    <img
                        src="<?= $arItem['PREVIEW_PICTURE']['SRC'] ?>"
                        alt="<?= htmlspecialchars($arItem['NAME']) ?>"
                        loading="lazy"
                        width="<?= $arItem['PREVIEW_PICTURE']['WIDTH'] ?>"
                        height="<?= $arItem['PREVIEW_PICTURE']['HEIGHT'] ?>"
                    >
                </div>
            <?php endif; ?>

            <div class="news-list__content">
                <?php if ($arItem['ACTIVE_FROM']): ?>
                    <time class="news-list__date" datetime="<?= $arItem['ACTIVE_FROM'] ?>">
                        <?= FormatDate('d F Y', MakeTimeStamp($arItem['ACTIVE_FROM'])) ?>
                    </time>
                <?php endif; ?>

                <h3 class="news-list__title">
                    <a href="<?= $arItem['DETAIL_PAGE_URL'] ?>">
                        <?= $arItem['NAME'] ?>
                    </a>
                </h3>

                <?php if ($arItem['PREVIEW_TEXT']): ?>
                    <div class="news-list__text">
                        <?= $arItem['PREVIEW_TEXT'] ?>
                    </div>
                <?php endif; ?>
            </div>
        </article>
    <?php endforeach; ?>

    <?php if ($arResult['NAV_STRING']): ?>
        <div class="news-list__pagination">
            <?= $arResult['NAV_STRING'] ?>
        </div>
    <?php endif; ?>
</div>

result_modifier.php — модификация данных

Этот файл выполняется после class.php, но до вывода шаблона. Работает внутри кеша. Идеально для трансформации данных без изменения логики компонента:

<?php
if (!defined('B_PROLOG_INCLUDED') || B_PROLOG_INCLUDED !== true) die();

// Добавляем ресайзы картинок
foreach ($arResult['ITEMS'] as &$arItem) {
    if (!empty($arItem['PREVIEW_PICTURE'])) {
        $arItem['PREVIEW_PICTURE_RESIZE'] = CFile::ResizeImageGet(
            $arItem['PREVIEW_PICTURE']['ID'],
            ['width' => 400, 'height' => 300],
            BX_RESIZE_IMAGE_PROPORTIONAL,
            true
        );
    }

    // Форматируем дату
    if ($arItem['ACTIVE_FROM']) {
        $arItem['DATE_FORMATTED'] = FormatDate(
            'd F Y',
            MakeTimeStamp($arItem['ACTIVE_FROM'])
        );
    }
}
unset($arItem);

component_epilog.php — код вне кеша

Выполняется всегда, даже когда компонент отдаёт данные из кеша. Нужен для:

  • Счётчиков просмотров
  • Добавления в навигационную цепочку
  • Логики, зависящей от текущего пользователя
<?php
if (!defined('B_PROLOG_INCLUDED') || B_PROLOG_INCLUDED !== true) die();

// Навигационная цепочка — вне кеша, т.к. зависит от текущей страницы
$APPLICATION->AddChainItem('Новости', '/news/');

// Заголовок страницы
$APPLICATION->SetTitle('Новости компании');

Вызов компонента на странице

<?php $APPLICATION->IncludeComponent(
    'mycompany:news.list',
    '.default',
    [
        'IBLOCK_ID' => 5,
        'NEWS_COUNT' => 12,
        'SORT_BY' => 'ACTIVE_FROM',
        'SORT_ORDER' => 'DESC',
        'CACHE_TIME' => 3600,
        'CACHE_TYPE' => 'A',
    ]
); ?>

Частые ошибки

  • Логика в template.php — запросы к базе в шаблоне замедляют отдачу кеша. Вся работа с данными — в class.php.
  • Нет abortResultCache — без этого закешируется пустой результат, и даже после появления данных будет показываться пустота.
  • Забыли unset после foreach по ссылке — классический PHP-баг, когда &$arItem в цикле портит последний элемент массива.
  • Кастомные CSS/JS подключают в template.php — лучше использовать \Bitrix\Main\Page\Asset в component_epilog.php.

Есть идея? Реализуем

Разрабатываем проекты, которые решают задачи бизнеса — от лендинга до сложного сервиса. Расскажите о своей задаче, подберём решение.

Написать в Telegram

30.03.2026

Нужна консультация?

Оставьте свои контактные данные, или свяжитесь с нами удобным для вас способом

Привет! Меня зовут Багира. Пишите, я все передам хозяевам!

Привет! Меня зовут Багира. Пишите, я все передам хозяевам!

Нажимая кнопку «Принять», вы соглашаетесь на сбор cookie. Мы используем их для обеспечения функционирования веб-сайта, аналитики действий и улучшения качества обслуживания. Если Вы не хотите, чтобы эти данные обрабатывались, отключите cookie в настройках браузера или прекратите использовать сайт.
Принять