BITRIX Простой компонент

С собеседования пришла задача реализовать компонент на CMS 1-С Битрикс. Сразу скажу, до этого с данной системой дела не имел, поэтому могу в чем-то и ошибаться. Система состоит из ядра и модулей, расширяющих его функционал до уровня корпоративного портала с мессенджерами, таскерами и даже распознаванием лиц.  Основная идея битрикс, как CMS, строится на основе разделения контента на статический и динамический. Все страницы сайта являются статическими, то есть физическими файлами на сервере. Зачастую структура папок этих страниц совпадает со структурой меню сайта. Динамика сайта реализована через универсальные типы – инфоблоки(подключаемые отдельным модулем). Они позволяют добавлять в систему бизнес-сущности (например, продукты, статьи и т.д.), а затем уже контент менеджер создает экземпляры этих сущностей (заполняя каталог продуктов). Инфоблоки подразделяются по типу предоставляя большую гибкость по созданию контента. Сами инфоблоки на страницы выводятся с помощью компонентов — мини-приложений, отвечающих за формат вывода инфоблоков. В битрикс из коробки уже заложено большое количество компонентов, при необходимости их можно кастомизировать или создавать свои собственные. Компоненты бывают простыми и комплексными. Например, простой компонент выводит краткую информацию о товаре на карточку. Комплексный компонент позволяет связать несколько простых, например, вывести карточки товаров и компоненты верхней и нижней пагинации.

В итоге, получаем следующую структуру:

  • Страницы — статический контент, оболочка для динамического контента.
  • Инфоблоки — сущности хранения информации с возможностью разделения по типам.
  • Компоненты — мини-приложения, выводящие данные инфоблока на страницу.

Тестовое задание заключалось в создании из скрипта php инфоблока «экзамен» и пяти его элементов, а также, в создании компонента для вывода этих элементов на странице в виде таблицы.

Еще важный момент, все что находится в папке bitrix на сервере является ядром системы. Разработчики не рекомендуют использовать эту папку для своих шаблонов и компонентов, а выделить для этого отдельную папку local в корне сайта.

Первая часть задания.

Первым делом, после понимания принципа работы битрикса идем на man Bitrix Framework API и открытый курс битрикс для разработчиков, там представлена вся необходимая информация. Битрикс имеет модульную структуру. Каждый отдельный модуль реализует какую-то функциональность, например, форум, блог и т.д. Для работы с частями модуля он должен быть подключен на странице. Инфоблоки также являются модулем в ядре битрикса.

Для тестов создаем отдельную страницу и располагаем на ней код.

CModule::IncludeModule("iblock");

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

Для нашего инфоблока потребуется создать новый тип (этого в задании не было), для этих целей создадим следующую функцию.

function createIBlockType($arFields)
{
    global $DB;
    $blockType = $arFields['ID'];
    // проверяем наличие нужного типа инфоблока
    $dbIblockType = CIBlockType::GetList(array(), array("ID" => $blockType));
    // если его нет - создаём
    if (!$dbIblockType->Fetch()) {

        $obBlocktype = new CIBlockType;
        $DB->StartTransaction();
        $res = $obBlocktype->Add($arFields);
        if (!$res) {
            $DB->Rollback();
            echo 'Error: ' . $obBlocktype->LAST_ERROR . '<br>';
        } else
            $DB->Commit();
            echo "Создан тип инфоблока расписание<br>";
            return $blockType;
    } else
        echo "Такой тип инфоблока уже создан<br>";
}

Функция принимает массив со значениями полей создаваемого типа инфоблоков. Названия полей даны в документации.  При запуске каждой страницы в битриксе создается глобальный объект $DB класса CDatabase, через который нужно выполнять все операции с базой данных. Тип блока должен иметь уникальный идентификатор строкового типа (в нижнем регистре). Возвращаем его из функции, он еще понадобится.  Статический метод CIBlockType::GetList возвращает список типов по фильтру, если такой тип уже есть, то функция завершит работу. Далее, создаем экземпляр CIBlockType и вызываем метод $DB->StartTransaction(), для запуска транзакции. Вызываем не статический метод Add, в случае ошибки он возвращает LAST_ERROR, соответственно, тогда откатываем транзакцию, иначе завершаем ее. Довольно странное решение (для меня), по явному открытию транзакции для добавления типа (можно было скрыть это в методе Add), но дальше этот подход не требуется.

Создание инфоблоков выносим в следующую функцию

function createIBlock($iBlockType, $SiteId, $arFields)
{
    // Проверяем наличие блока, чтобы не дублировать
    $rsBlock = CIBlock::GetList(array(), array('CODE' => 'examines'), false);
    if ($arItem = $rsBlock->GetNext()) {
        return;
    } else {
        $ib = new CIBlock;

        $arFields['IBLOCK_TYPE_ID'] = $iBlockType;
        $arFields['SITE_ID'] = $SiteId;

        $ID = $ib->Add($arFields);
        if ($ID > 0) {
            echo $ID , "&mdash; инфоблок \"Расписание экзаменов\" успешно создан<br />";
            return $ID;
        } else {
            echo $ID , "&mdash; ошибка создания инфоблока \"Расписание экзаменов\"<br />";
        }
    }
}

Для создания инфоблока он обязательно должен передавать значения ‘IBLOCK_TYPE_ID’ и ‘SITE_ID’ мы передаем их как параметры функции  и добавляем в массив со значениями. Для добавления вызываем не статический метод CIBlock::Add. Метод возвращает ID созданного инфоблока, его мы передаем из функции для дальнейшего использования.

Для добавления пользовательских свойств в инфоблок пишем еще одну функцию.

function createIBlockProperties($ID, $arFieldsIBProp)
{
    // Определяем, есть ли у инфоблока свойства
    $dbProperties = CIBlockProperty::GetList(array(), array("IBLOCK_ID" => $ID));
    if ($dbProperties->SelectedRowsCount() <= 0) {
        $ibp = new CIBlockProperty;

        foreach ($arFieldsIBProp as $item) {
            $item['IBLOCK_ID'] = $ID;
            $propId = $ibp->Add($item); // функция возвращает ID
            if ($propId > 0) {
                echo "&mdash; Добавлено свойство " . $item["NAME"] . '<br>';
            } else
                echo "&mdash; Ошибка добавления свойства " . $item["NAME"] . '<br>';
        }
    }
}

Работа этой функции похожа на предыдущую, только использует метод CIBlockProperty::Add.

Далее добавляем элементы инфоблоков.

function createIBlockElements($ID, $arFields){
    $ibe = new CIBlockElement;
    foreach ($arFields as $item) {
        $item["IBLOCK_ID"] = $ID;
        if ($PRODUCT_ID = $ibe->Add($item))
            echo "Создан элемент с ID: " . $PRODUCT_ID . "<br>";
        else
            echo "Ошибка: " . $ibe->LAST_ERROR . "<br>";
    }
}

Все аналогично, передаем ID инфоблока и массив значений, вызываем функцию CIBlockElement::Add.

Вторая часть задания.

Вторая часть задания.

Компоненты в битриксе являются реализацией паттерна CRM (Carrier Rider Mapper) и используют разделение логики и представления. Компонент обращается к API различных модулей и обрабатывает вывод полученной информации. Вообще, если речь идет о создании компонента, то разработчики берут готовый компонент (news:list — подчерпнул на форуме), и изменяют его шаблон. News:list является очень универсальным компонентом, и может отображать любые инфоблоки, но в условиях задачи указывалось, что использовать его нельзя. Дальнейшие изыскания привели к следующему, реализация стандартных компонентов взята из предыдущей версии битрикс, на текущий момент компоненты могут реализовываться через класс, это не сильно меняет весь принцип работы, но все же.

Начнем разработку с файловой структуры. Битрикс позволяет хранить все файлы компонента в отдельной папке (в нашем случае, в соответствии с заданием это папка local/components/lvlsv/exam:list).

exams.list
│   .description.php
│   .parameters.php
│   class.php
│
├───lang
│   └───ru
│           .description.php
│           .parameters.php
│
└───templates
    └───.default
        │   style.css
        │   template.php
        │
        └───lang
            └───ru
                    template.php

В папке lang содержится перевод свойств файлов description.php  и parameters.php компонента для отображения в админке.  В папке templates обязательно должен быть шаблон по умолчанию (.default). Он может содержать дополнительный файл стилей (style.css), который битрикс цепляет автоматически, ну, и сам шаблон (template.php). Логика компонента строится в файлах parameters.php и class.php (если реализация не через класс, то файл называется component.php).

$arComponentParameters = array(
	"GROUPS" => array(
	),
	"PARAMETERS" => array(
		"IBLOCK_TYPE" => array(
			"PARENT" => "BASE",
			"NAME" => GetMessage("IBLOCK_TYPE"),
			"TYPE" => "STRING",
			"VALUE" => "shedule",
			"DEFAULT" => "shedule",
			"REFRESH" => "Y",
		),
        "IBLOCK_NUMBER_ID" => array(
            "PARENT" => "BASE",
            "NAME" => GetMessage("IBLOCK_NUMBER_ID"),
            "TYPE" => "STRING",
        ),
		"CACHE_TIME"  =>  Array("DEFAULT"=>36000000),
	),
);

Parameters.php хранит массив значений, которые определяют, какие параметры компонента доступны для редактирования из админки. Можно, например, сделать список с множественным выбором или выпадающий список (в моем случае  строка). Заданные в параметрах значения хранятся в массиве arParams, в классе компонента мы используем эти значения для вывода в шаблон нужных данных.

class examsComponent extends CBitrixComponent
{
    /**
     * Возвращаем массив элементов
     * @param $arParams
     * @return array
     */
    function setarResult($arParams){
        $arFields = [];

        $arParams['IBLOCK_NUMBER_ID'] = (int)trim($arParams['IBLOCK_NUMBER_ID']);

        $arSelect = Array("ID", "NAME", "DATE_ACTIVE_FROM", "PROPERTY_ATT_TEACHER", "PROPERTY_ATT_CABINET");
        $arFilter = Array("IBLOCK_ID"=>$arParams['IBLOCK_NUMBER_ID'], "ACTIVE_DATE"=>"Y", "ACTIVE"=>"Y");
        $res = CIBlockElement::GetList(array("DATE_ACTIVE_FROM"=>"ASC"), $arFilter, false, array("nPageSize"=>50), $arSelect);

        while($ob = $res->GetNextElement())
        {
            $arFields['ITEMS'][] = $ob->GetFields();
        }
//        echo "<pre>".print_r($arFields, true)."</pre>";

        return $arFields;
    }

    /**
     *  Функция инициализации компонента.
     */
    public function executeComponent(){
        $this->arResult = array_merge($this->arResult, $this->setarResult($this->arParams));

        $this->includeComponentTemplate();
    }
}

Здесь важно следующее, executeComponent() — перегрузка метода класса родителя CBitrixComponent. Этот метод должен подключать к компоненту шаблон для вывода данных (вызовом includeComponentTemplate()). Данные в шаблон передаются в массиве arResult, мы добавили в него искомые значения из вызова функции setarResult(). В шаблоне просто выводятся значения массива в цикле.

Весь код решения доступен на github, надеюсь, он будет кому-то полезен.