Здрасте! В статье я рассмотрю альтернативу BitrixFramework, которая призвана облегчить жизнь разработчика и как-нибудь повлиять на развитие CMS Битрикс в нужном направлении. Для начала расставим все точки над ‘i’ Все что написано ниже лично мое мнение и может быть спокойно закидано тапками, но я уверен, что я прав:
«У Битрикс миллион строк говнокода» — да, бесспорно. Вся проблема заключается в том, что Битрикс поддерживает обратную совместимость (якобы обновив версию 12.0 до 16.5 все будет нормально работать). Зачем они это делают я не знаю (мне кажется никто не знает). Если же говорить про исходники стандартных компонентов (2К строк кода для вывода элементов инфоблока – в порядке вещей), то здесь ребята решили облегчить работу конечным юзерам и предусмотрели все что можно было предусмотреть (и то не факт), при чем все полностью это очень редко когда нужно. Ну и в догонку, на недавнем семинаре разработчик Битрикс сообщил свое отношение к PSR: «Ну что этот PSR, читал я его, собрались какие-то ребята и написали какую-то фигню» (не точная цитата). Так что код будет пахнуть всегда.
«У Битрикс ужасная структура» — не совсем. Битрикс основан на файлах, и понимание MVC отличается от общепринятого, а для многих «не MVC» = «ужас-ужас какая структура». Так что это весьма спорный и спорный вопрос. И с Битриксовой структурой можно жить.
«BitrixFramework никогда не будет развиваться» — это уже мое мнение и вот почему: с каждым релизом Битрикс дорабатывает только модуль «Магазин», делают какие-то правки, но все они направлены на магазин. На остальное им откровенно наплевать. Развитие BF начнется, когда они откажутся от обратной совместимости и начнут заниматься не только модулем «Магазин».
Знакомьтесь, Juggernaut! Наверняка многие знакомы с этим персонажем (из Marvel, не из Dota), которого «невозможно остановить». Слегка пафосное название, на самом деле отражает суть данного проекта: абсолютно безразлично как развивается Битрикс, какие новшества он вводит и что он делает, все равно библиотека будет жить и процветать. Bitrix нацелен на пользователей. Juggernaut нацелен на разработчиков. Зачем это надо? Потому что это надо! Все на самом деле очень логично:
Битрикс разрабатывают новое ядро (good), но документировать вообще не хотят (bad);
Битрикс разрабатывают новый функционал (good), но только для магазина (bad);
Битрикс разрабатывают новые компоненты (good), но от их кода кровь из глаз (bad);
Битрикс нужно было с версии 14 просто закончить поддержку старого ядра и сделать основной упор на новом, но нет, «заботятся о клиентах». Бред. Это тоже самое если бы Yii2 поддерживал и обратно совмещал Yii1. Раз Битрикс никакие подвижки не делает, то их будет делать сообщество (вместо того чтобы ныть, писать в сервис «Идея», и как-то выкручивать используя стандартные компоненты). Поругали Bitrix, теперь можно приступить и к обзору Juggernaut. Далее начнется обзор составляющих частей библиотеки и краткое описание их использования. Компоненты Компоненты условно разделены на 2 категории: виджеты и роутеры (в нотации Битрикс: «обычный» и «комплексный»). Виджет – это компонент, который тупо делает одну элементарную задачу (выводит форму, список, информацию). Виджет получает на вход данные и каким-либо образом их преобразует. Больше делать он ничего не должен. Виджеты не управляют маршрутизацией, но могут ее использовать. Порядок выполнения компонента по умолчанию:
По порядку:
init — инициализирует начальные данные. Преобразует входные параметры ($arParams) в свойства класса;
onBefore — проводит проверку возможности проведения действия;
isCachedTemplate — флаг, определяющий есть ли кешированная копия. Если есть — выводит данные кеша, если нет — формирует их (в коде это выглядит несколько иначе, на схеме указано так для простоты);
initResult — формирует данные для представления ($arResult);
run — функция непосредственного исполнения виджета. В ней определяется что необходимо сделать с данными ($arResult);
onBeforeRender — проводит проверку возможности вывода шаблона и выполняет какие либо преобразования (аналог result_modifier.php, хотя можно и им пользоваться);
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 — проводит проверку возможности вывода шаблона и выполняет какие либо преобразования (в параметрах передается имя действия, поэтому можно настроить персональную проверку);
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, так что править, добавлять, комментировать и спрашивать может любой желающий.
Спасибо за внимание! Конструктивная критика очень даже приветствуется
Группы на сайте создаются не только сотрудниками «1С-Битрикс», но и партнерами компании. Поэтому мнения участников групп могут не совпадать с позицией компании «1С-Битрикс».