Pull to refresh

BitrixFramework: берем все в свои руки

Reading time 14 min
Views 16K
Здрасте!

Очередная суицидальная статья от меня на тему Битрикс, надеюсь в этот раз хабраобщество будет более снисходительно, т.к. здесь все по факту, с кодом, схемками, никакого холивара и все по-честному.

В статье я рассмотрю альтернативу BitrixFramework, которая призвана облегчить жизнь разработчика и как-нибудь повлиять на развитие CMS Битрикс в нужном направлении.

Акция для хейтеров: если напишите комментарий с нормальной критикой и по теме + к карме лично отправлю ;-). Вот вам Вольфыча для затравки, все интересное внутри…



Для начала расставим все точки над ‘i’


Все что написано ниже лично мое мнение и может быть спокойно закидано тапками, но я уверен, что я прав:

  1. «У Битрикс миллион строк говнокода» — да, бесспорно. Вся проблема заключается в том, что Битрикс поддерживает обратную совместимость (якобы обновив версию 12.0 до 16.5 все будет нормально работать). Зачем они это делают я не знаю (мне кажется никто не знает). Если же говорить про исходники стандартных компонентов (2К строк кода для вывода элементов инфоблока – в порядке вещей), то здесь ребята решили облегчить работу конечным юзерам и предусмотрели все что можно было предусмотреть (и то не факт), при чем все полностью это очень редко когда нужно. Ну и в догонку, на недавнем семинаре разработчик Битрикс сообщил свое отношение к PSR: «Ну что этот PSR, читал я его, собрались какие-то ребята и написали какую-то фигню» (не точная цитата). Так что код будет пахнуть всегда.
  2. «У Битрикс ужасная структура» — не совсем. Битрикс основан на файлах, и понимание MVC отличается от общепринятого, а для многих «не MVC» = «ужас-ужас какая структура». Так что это весьма спорный и спорный вопрос. И с Битриксовой структурой можно жить.
  3. «BitrixFramework никогда не будет развиваться» — это уже мое мнение и вот почему: с каждым релизом Битрикс дорабатывает только модуль «Магазин», делают какие-то правки, но все они направлены на магазин. На остальное им откровенно наплевать. Развитие BF начнется, когда они откажутся от обратной совместимости и начнут заниматься не только модулем «Магазин».

Знакомьтесь, Juggernaut!


Наверняка многие знакомы с этим персонажем (из Marvel, не из Dota), которого «невозможно остановить». Слегка пафосное название, на самом деле отражает суть данного проекта: абсолютно безразлично как развивается Битрикс, какие новшества он вводит и что он делает, все равно библиотека будет жить и процветать.

Bitrix нацелен на пользователей. Juggernaut нацелен на разработчиков.

Зачем это надо?


Потому что это надо! Все на самом деле очень логично:
  • Битрикс разрабатывают новое ядро (good), но документировать вообще не хотят (bad);
  • Битрикс разрабатывают новый функционал (good), но только для магазина (bad);
  • Битрикс разрабатывают новые компоненты (good), но от их кода кровь из глаз (bad);
  • Битрикс запатентовали «новую» технологию «Композитный сайт» (bad).


Битрикс нужно было с версии 14 просто закончить поддержку старого ядра и сделать основной упор на новом, но нет, «заботятся о клиентах». Бред. Это тоже самое если бы Yii2 поддерживал и обратно совмещал Yii1.

Раз Битрикс никакие подвижки не делает, то их будет делать сообщество (вместо того чтобы ныть, писать в сервис «Идея», и как-то выкручивать используя стандартные компоненты).

Поругали Bitrix, теперь можно приступить и к обзору Juggernaut. Далее начнется обзор составляющих частей библиотеки и краткое описание их использования.

Компоненты


Компоненты – это кирпичи из которых строиться сайт на Битрикс. Компоненты условно разделены на 2 категории: виджеты и роутеры (в нотации Битрикс: «обычный» и «комплексный»).

Виджет


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

Порядок выполнения компонента по умолчанию:


По порядку:
  • init — инициализирует начальные данные. Преобразует входные параметры ($arParams) в свойства класса;
  • onBefore — проводит проверку возможности проведения действия;
  • isCachedTemplate — флаг, определяющий есть ли кешированная копия. Если есть — выводит данные кеша, если нет — формирует их (в коде это выглядит несколько иначе, на схеме указано так для простоты);
  • initResult — формирует данные для представления ($arResult);
  • run — функция непосредственного исполнения виджета. В ней определяется что необходимо сделать с данными ($arResult);
  • onBeforeRender — проводит проверку возможности вывода шаблона и выполняет какие либо преобразования (аналог result_modifier.php, хотя можно и им пользоваться);
  • render — непосредственный вывод шаблона компонента;
  • onAfter — выполнение действия после отработки виджета (аналог component_epilog.php).


Чаще всего достаточно переопределить метод initResult и накидать шаблон компонента.

Ниже представлен пример класса компонента (class.php), который выводит список элементов инфоблока. На вход он получает массив параметров ($params), которые используются для фильтра и сортировки данных.

Код
<?php
namespace Widget\Iblock\Element\List_;

use Jugger\Db\Orm\Ib\IblockElement;
use Jugger\Component\WidgetComponent;

class Component extends WidgetComponent
{
    /*
     * при выполнения метода 'init', 
     * все переменные из $arParams присваиваются существующим свойствам класса компонента,
     * в данном случае: $this->params = $arParams['params']
     */
    public $params = [];
    /*
     * по умолчанию, кеширование компонентов отключено
     * в данном методе, мы его включаем
     */
    protected function init() {
        parent::init();
        $this->isCachingTemplate = true;
    }
    /*
     * инициализируются элементы для отображения
     */
    protected function initResult() {
        $this->arResult['elements'] = IblockElement::getList($this->params);
    }
}


Роутер


Задача роутера – это сбор виджетов воедино. Роутер — представляет из себя контроллер, который на основе запроса пользователя (REQUEST_URI), вызывает соответствующее действие. Действие может быть либо страницей с информацией (в том числе виджетами), либо содержать какую-либо логику.

Порядок выполнения компонента по умолчанию:


По порядку:
  • init — инициализирует начальные данные. Преобразует входные параметры ($arParams) в свойства класса;
  • initUrlManager — заполняет UrlManager данные маршрутов (aliases). Это действие необходимо для выполнения маршрутизации по действием и дальнейшей генерацией URL адресов;
  • parseRequest — производится разбор запроса UrlManager и определяется какое действие запрошено пользователем;
  • existBeforeAction — проверка наличия персонального обработчика onBefore. Если есть действие 'index' и есть метод 'onBeforeIndex', то будет вызван именно он, иначе будет вызван общий 'onBefore';
  • onBefore — проводит проверку возможности проведения действия;
  • run — функция непосредственного исполнения компонентв. В ней определяется что необходимо сделать с данными ($arResult);
  • existMethodAction — проверка на наличие обработчика действия. Если запрошено действие 'index' и есть метод 'actionIndex', то будет вызван этот метод, иначе роутер попытается вывести представление с именем 'index';
  • onBeforeRender — проводит проверку возможности вывода шаблона и выполняет какие либо преобразования (в параметрах передается имя действия, поэтому можно настроить персональную проверку);
  • render — непосредственный вывод шаблона компонента;
  • onAfter — выполнение действия после отработки виджета (аналог component_epilog.php). Работает аналогично с методом 'onBefore': если для действия 'index' существует метод 'onAfterIndex', то будет вызван он, иначе общий 'onAfter'.


Ниже представлен пример компонента, которые реализует каталог:
  • список элементов,
  • список разделов
  • детальная карточка элемента.

Код
<?php
namespace Widget\Iblock\Element\Catalog;

use Jugger\Db\Orm\Ib\IblockElement;
use Jugger\Db\Orm\Ib\IblockSection;
use Jugger\Component\RouteComponent;

class Component extends RouteComponent
{
    /*
     * ID инфоблока, который отображается
     */
    public $iblockId;
    /*
     * Маршруты действий, в которые будут транслироваться адреса и по которым будер производиться маршрутизация
     * по умолчанию, маршруты беруться из параметров компонента из свойства 'aliases'
     */
    protected function getAliases() {
        return [
            "sectionList" =>    "index.php",
            "elementList" =>    "#SECTION_CODE#/",
            "elementView" =>    "#SECTION_CODE#/#ELEMENT_CODE#/"
        ];
    }
    /*
     * Если инфоблок не указан, то выходим
     */
    protected function onBefore($action) {
        if (!$this->iblockId) {
            throw new \Exception("Не указан 'iblockId' ". get_called_class());
        }
        return parent::onBefore($action);
    }
    /*
     * Получаем раздел по его символьному коду
     */
    protected function getSection($sectionCode) {
        return IblockSection::getRow([
            "filter" => [
                "IBLOCK_ID" => $this->iblockId,
                "CODE" => $sectionCode
            ],
        ]);
    }
    /**
     * Список разделов инфоблока
     */
    public function actionSectionList() {
        $sectionList = IblockSection::getListByField(
            "=IBLOCK_ID",
            $this->iblockId,
            [
                "order" => ["SORT" => "ASC"]
            ]
        );
        $this->arResult['sectionList'] = $sectionList;
        $this->render('list');
    }
    /**
     * Список элементов указанного раздела
     * Параметр $sectionCode содержит данные из URL
     */
    public function actionElementList($sectionCode) {
        $section = $this->getSection($sectionCode);
        if (!$section) {
            $this->error404();
        }
        //
        $this->arResult['section'] = $section;
        $this->arResult['elementList'] = $section->getElements();
        $this->render('section');
    }
    /**
     * Отображение карточки товара
     * Параметры передаются в том же порядке, в каком они указаны в методе 'aliases'
     */
    public function actionElementView($sectionCode, $elementCode) {
        $section = $this->getSection($sectionCode);
        if (!$section) {
            $this->error404();
        }
        //
        $element = IblockElement::getRow([
            "filter" => [
                "IBLOCK_ID" => $this->iblockId,
                "IBLOCK_SECTION_ID" => $section->ID,
                "CODE" => $elementCode,
            ],
        ]);
        if (!$element) {
            $this->error404();
        }
        $this->arResult['element'] = $element;
        $this->arResult['section'] = $section;
        $this->render('view');
    }
}



Автозагрузка классов


По данному вопросу много говорить не буду, потому что и так ясно что это очень нужная вещь, просто опишу все работает.

Как реализовано в Juggernaut:

В папке «lib» вы должны соблюдать следующую структуру: имена файлов классов, идентичны именам пространства имен, не включая расширение и верхнего пространства имен. Например, классу «Iblock\Property\Table» будет соответствовать файл «…/modules/Iblock/lib/Property/Table.php».

Вызывать «includeModule» больше не нужно, т.к. при необходимости все классы подгрузятся автоматически из нужных директорий.
Если директория модуля отличается от названия пространства имен, или в любой другой ситуации, можно вручную задать соответствие пространства имен и директории:

// класс "Jugger\D7\Iblock" доступен по адресу "./lib/D7/Iblock.php" – по умолчанию так и работает 
\Jugger\Psr\Psr4\Autoloader::addNamespace('Jugger', __DIR__.'/lib');

// класс "Jugger\D7\Iblock" доступен по адресу "./classes/Iblock.php"
\Jugger\Psr\Psr4\Autoloader::addNamespace('Jugger\D7', __DIR__.'/classes'); 


У Битрикс тоже реализована автозагрузка, но формирует она путь несколько иначе:

Класс «Olof\Catalog\Tools\File» транслируется как «/Olof.Catalog/lib/Tools/File.php».

Если Вам нужен класс «Olof\Catalog» — то извините, руками указывайте его наличие (см.ниже). Директория модуля у Вас должна быть именно с разделителем «.» иначе гуляйте лесом. При чем директория «olof.catalog.iblock» — является некорректной.

Господа из Битрикс на самом деле сделали нормальную штуку: позаботились об указании вендора в имени модуля, но я считаю это лишнее условие именования директории.

Автозагрузка неявно реагирует на классы вида «ElementTable» удаляя постфикс, транслируя их в файлы «element.php». Собственно, из-за этого, вы не можете создать класс с именем Table.

Также загрузить классы из модулей, которые в данный момент не подключены (includeModule) – нельзя.

Рассмотрим пример работы Битриксового варианта: имеем модуль «olof.iblock» и соответствующий файл include.php:

namespace Olof\Iblock;

use Bitrix\Main\Loader;

// подключаем модуль
Loader::includeModule("Olof.Iblock");

// переопределяем стандартное поведение для класса Api
Loader::registerAutoLoadClasses("Olof.Iblock", [
    "\Olof\Classes\Api" => ". /modules/Olof.Iblock/classes/api.php",
]);

// Примеры доступа к классам:
// Olof\Iblock\Element  -> ./modules/Olof.Iblock/lib/Element.php
// Olof\Classes\Api     -> ./modules/Olof.Iblock/classes/api.php
// Olof\Classes\Help    -> ./modules/Olof.Classes/lib/Help.php


Слишком много неявностей и условий на мой взгляд. Да и никто не знает, какую глупость Битрикс завтра придумают. А придумать им стоит указание директории для префикса пространства имен (как в PSR-4) и тогда будет круто. А пока есть Juggernaut ;-)

ActiveRecord


Для удобства работы с сущностями, а в частности с инфоблоками, реализован шаблон ActiveRecord. На данный момент AR базируется (по факту является надстройкой) на битриксовых DataMapper’ах, в дальнейшем планируется полный перенос на независимый ORM / DAO.

Ниже представлен пример работы с инфоблоками через AR, охвачены практически все, имеющиеся на данный момент, методы.

Код
use Jugger\Db\Orm\Ib\Iblock;
use Jugger\Db\Orm\Ib\IblockSection;
use Jugger\Db\Orm\Ib\IblockElement;

/*
 * Получаем инфоблок
 */
$iblock = Iblock::getByPrimary(1);
$iblock = Iblock::getRowByField("=ID", 1);
$iblock = Iblock::getRow([
    "filter" => [
        "=ID" => 1,
    ],
]);
$iblock = new Iblock($iblock);
/**
 * Доступ к полям таблицы, возможен как к свойствам класса
 */
$iblock->NAME;
$iblock->IBLOCK_TYPE_ID;
/**
 * Дочерние элементы и разделы
 */
$iblock->getElements();
$iblock->getSections();
/*
 * Получить разделы инфоблока
 */
$sectionList = $iblock->getSections();
$sectionList = $iblock->getSections([
    "order" => [
        "NAME" => "ASC",
    ],
]);
$sectionList = IblockSection::getListByField("=IBLOCK_ID", $iblock->ID);
$sectionList = IblockSection::getList([
    "filter" => [
        "=IBLOCK_ID" => $iblock->ID,
    ],
]);
/*
 * Получить дочерние разделы
 */
$section = new IblockSection($sectionList->fetch());
$section->getChilds();  // получить детей 1-ого уровня вложенности
$section->getChilds(2); // получить детей до 2-ого уровня вложенности (вернется массив, в порядке вложенности потомков)
$section->getChilds(0); // получить всех детей
$section->getIblock();  // родительский инфоблок
/*
 * Работа с элементами
 */
$elementList = $section->getElements();
while($element = $elementList->fetch()) {
    /*
     * Массив преобразуется в AR без дополнительного запроса к базе
     */
    $element = new IblockElement($element);
    $element->getProperties(); // свойства элемента
    /*
     * Работа со свойствами
     */
    $elementProperty = $element->getProperty(1);
    /*
     * значение свойства (оба вызова равносильны)
     */
    $value = $elementProperty->VALUE;
    $value = $elementProperty->getValue();
    /*
     * при получении значения любым из способов выше,
     * значение автоматически приводится к типу свойства одним из методов ниже
     */
    $elementProperty->getValueRaw();  // значение без преобразования
    $elementProperty->getValueEnum(); // IblockPropertyEnum - значение элемента списка (L)
    $elementProperty->getValueFile(); // CFile::GetFileArray
    $elementProperty->getValueHtml(); // (string) HTML код
    $elementProperty->getValueElement(); // IblockElement - связный элемент (E)
    $elementProperty->getValueSection(); // IblockSection - связный раздел (G)
    $elementProperty->getValueNumber();  // (float) или (int) в зависимости от значения
    /*
     * Получить объект свойства
     */
    $property = $elementProperty->getMeta();
    $property->NAME;
    $property->HINT;
}



Методы: getPrimary, getRow, getRowByField, getList, getListByField — идентичны для всех ActiveRecord.

Функционал AR на данный момент достаточно беден (например, нет перекрестного поиска по таблицам), но т. к. они являются оберткой над стандартными функциями, в методах «getList» и «getRow» можно использовать Битриксовые плюшки. После создания / заимствования нормального DAO, этот момент будет допилен.

Hermitage


Сильной стороной Битрикс, и я думаю многие согласятся, является его пользовательский интерфейс a.k.a. «Эрмитаж». Он очень удобен и гибок.

Ниже представлен пример работы с Эрмитажем:

Код
use Jugger\Db\Orm\Ib\IblockSection;
use Jugger\Db\Orm\Ib\IblockElement;
use Jugger\Ui\Hermitage;
use Jugger\Ui\Hermitage\Icon;
use Jugger\Context\UrlManager\Iblock;

/* @var $this CBitrixComponentTemplate */
/* @var $component CBitrixComponent */

/*
 * Добавление кнопок "редактировать" и "удалить" для элементов и разделов
 */
$element = IblockSection::getByPrimary(1);
Hermitage::addButtonEditIblockElement($this, $element);
Hermitage::addButtonDeleteIblockElement($this, $element);

$section = IblockSection::getByPrimary(1);
Hermitage::addButtonEditIblockSection($this, $section);
Hermitage::addButtonDeleteIblockSection($this, $section);
/*
 * Добавление кнопок в тулбар компонента
 */
Hermitage::addButton(
    $component,
    Iblock::getElementCreateUrl(1),
    "Добавить элемент",
    [
        "ICON" => Icon::TOOLBAR_CREATE,
    ]
);
/*
 * Добавление кнопок в верхнюю панель
 */
Hermitage::addPanelButton("#", "Надпись", [
    "ICON" => Icon::PANEL_TRANSLATE,
]);



Так похвалил и так мало написал)) На самом деле этого достаточно для взаимодействия с пользователем. Очень много нужно реализовать касаемо административного интерфейса, но это уже не Эрмитаж, и это все в планах.

Безопасность


В Битрикс на сколько я знаю (а в данном вопросе, скрывать не буду, я особо не ковырялся), с безопасностью сайта (именно в коде) вообще грустно (только защита от SI). В будущем данный раздел будет содержать в себе инструменты для защиты от различных атак и вредоносных действий (XSS, генерация случайных данных, различные крипто-функции, валидация форм, работа с паролями, …). На данный момент реализован только инструментарий для защиты от CSRF:

use Jugger\Security\Csrf;

/*
 * "автоматический" режим
 */
if (Csrf::validateTokenByPost()) {
    // ok
}
else {
    // error
}
echo Csrf::printInput();

/*
 * "ручной" режим
 */
$nameField = "csrf";
$token = Bitrix\Main\Context::getCurrent()->getRequest()->getPost($nameField);

if (Csrf::validateToken($token)) {
    // ok
}
else {
    // error
}

$token = Csrf::createToken();
echo "<input type='hidden' name='{$nameField}' value='{$token}'>";


После каждой проверки (удачно или неудачной) – токен из сессии удаляется, таким образом проверить токен можно только один раз.

UrlManager


Маршрутизация в Битрикс, не сказал бы что на высоте, поэтому и эта область затронута в Juggernaut. Данный класс позволяет динамически создавать и использовать URL маршруты (используется в компонентах-роутерах).

Рассмотрим пример парсинга и генерирования URL:

Код
use Jugger\Context\UrlManager;

/*
 * Установка базового URL и маршрутов
 */
UrlManager::setBaseUrl("/catalog");
UrlManager::addAlias("sectionList", "index.php");
UrlManager::addAlias("elementList", "#SECTION_CODE#/");
UrlManager::addAlias("elementView", "#SECTION_CODE#/#ELEMENT_CODE#/");
/*
 * Получаем запрашиваемый 'alias' на основе запроса
 * Например, для запроса '/catalog/section1/element1/' будет получен маршрут 'elementView'
 */
$alias = UrlManager::parseRequest();
/*
 * Аналог используя старые фукнции (хотя по сути UrlManager тоже их использует, просто это сокрыто в недрах)
 */
$folder404 = "/catalog";
$arUrlTemplates = [
    "sectionList" => "index.php",
    "elementList" => "#SECTION_CODE#/",
    "elementView" => "#SECTION_CODE#/#ELEMENT_CODE#/",
];
$arVariables = []; // UrlManager::$params
CComponentEngine::parseComponentPath($folder404, $arUrlTemplates, $arVariables);
/*
 * Добавляем параметры маршрутов в систему
 */
UrlManager::addParams([
    "param1" => "value1",
    "param2" => "value2",
]);
/*
 * Получаем сгенерированый URL.
 * Параметры указанные в данном методе будут использоваться локально.
 * После выполнения метода параметр "ELEMENT_CODE" - не будет доступен
 */
UrlManager::addParam("SECTION_CODE", "section1");
$url = UrlManager::build("elementView", [
    "ELEMENT_CODE" => "element1",
]);
// $url: /catalog/section1/element1/



В дальнейшем планируется также подвязаться и к urlRewrite.php.

События


Данный класс является просто оберткой над функциями D7, с более удобным использованием.

use Jugger\Helper\Event;

/*
 * добавление обработчика
 */
Event::on("имя события", function(){
    // обработчик
});
Event::on("имя события", "\ClassName::MethodName", "moduleName");
/*
 * удаление обработчиков
 */
Event::off("имя события");
Event::off("имя события", 3); // удалить 4-ий по счету (с нуля) обработчик
/*
 * Вызов события
 */
Event::trigger("имя события");
/*
 * Вызов события с указанием сендера (вызывателя)
 */
Event::trigger("имя события", $this);


Что дальше?


Планы на ближайшее будущее:
  • нормальный QueryBuilder
  • нормальное кеширование
  • нормальный AssetsManager
  • набор компонентов для создания и работы с административным интерфейсом
  • нормальная маршрутизация (завязанная на HttpException)


Заключение


Много чего задумано, много чего не сделано. Библиотека развивается по мере моей необходимости, поэтому очень зависит от текущих заказов (которое очень часто однотипны) и свободного времени.

Как я уже сказал вначале, проект будет развиваться несмотря ни на что, от количества ее авторов и заинтересованных лиц зависит лишь скорость развития. Так что выбор только за вами:
  • ныть, ждать и подстраиваться под Bitrix (а развитие BitrixFramework явно не в приоритете);
  • взять все в свои руки и помочь в развитии Juggernaut.

Помочь может каждый желающий филантроп (а иначе никак), для этого нужно:
  • поделиться идеей
  • рефакторить то что есть
  • сделать что-нибудь своими ручками

Проект лежит на GitHub, так что править, добавлять, комментировать и спрашивать может любой желающий.

Спасибо за внимание! Конструктивная критика очень даже приветствуется :-)

P.S. комментарии типа «да на фиг Битрикс» огромная просьба не писать. Я в курсе какое у людей отношение к этой системе, и данный проект как раз направлен на ее облагораживание. Поэтому если вы считаете что «лучше и проще сделать проект на любом фреймворке» — то я это знаю и очень рад за вас, поэтому оставьте свое мнение при себе. Спасибо!

Репозиторий: github.com/irpsv/juggernaut.bitrix_release
Маркетплейс: скоро будет
Tags:
Hubs:
-5
Comments 85
Comments Comments 85

Articles