Здрасте! Поговорим про самое спорное телодвижение компании Битрикс, а именно технология «Композитный сайт». Спорное оно потому, что ребята запатентовали технологию, которая по моему мнению, не тянет даже на курсовую 3 курса профильной специальности. Ну да ладно, это ж маркетологи. В статье рассмотрены:
сама технология «Композитный сайт»
альтернатива данной технологии «CompoJax» (через ДЖ)
примеры кода, для той и другой технологии
INTRO
Честно, не понимаю какой был смысл патентовать технологию на 1-2 сотни строк кода, да к тому же и совсем не инновационную, это правда уже вопросы к «патентному бюро», так что по фиг. Погнали!
Технология композитный сайт
Громко конечно называть это технологией, но все же пусть будет так. Что же это такое? Цитата с сайта Битрикс: Уникальная технология производства сайтов объединяет в себе высокую скорость загрузки статического сайта и все возможности динамического сайта. Пользователь мгновенно получает контент страницы.
Обещают даже ускорение в 100 раз, правда бенчмарки не предоставили. Ну это опять вопрос к маркетологам, а это, честно, не по моей части.
Алгоритм работы следующий:
То, что выделено красным, выполняется параллельно. Пояснять не буду, тут все понятно. Пример использования
/*
* включение в компоненте и в шаблоне
*/
$component->setFrameMode(true);
$template->setFrameMode(true);
/*
* динамический фрейм
*/
$frameBuffer = new Bitrix\Main\Page\FrameBuffered("id_container_buffer");
$frameBuffer->begin("сообщение, которое будет выведено вместо контента");
echo "динамические данные";
$frameBuffer->beginStub();
echo "статические данные";
$frameBuffer->end();
/*
* статический фрейм
*/
$frame = new \Bitrix\Main\Page\FrameStatic("id_container_static");
$frame->setStub("сообщение, которое будет выведено вместо контента");
$frame->startDynamicArea();
echo "динамические и статические данные";
$frame->finishDynamicArea();
Код снят с документации и подглядками в исходники. Сильно не углублялся, да это мне и не нужно, есть же CompoJax))). Запускать в компонентах не пробовал, скажу лишь, что на страницах и шаблоне сайта он не завелся. Кому интересно может почитать документацию, но на самом деле не советую, т.к. после 9-ти страничного теоретического петтинга, вы увидите почти те же самые строчки кода.
CompoJax
Собственно, «технология» делающая практически тоже самое, только без пафоса, патента, проще, прозрачнее + PJAX. Разделить технологию можно на 2 части:
Композит
PJAX
Для первой ничего не требуется, можно смело юзать. Для второй составляющей, необходимо наличие библиотек jQuery и PJAX.
Алгоритм работы следующий:
Собственно, вот и все. Алгоритмически «технологии» не одинаковы, так что нарушения патента, в принципе, нет. В патент не углублялся, но если придет повестка в суд сделаю в статье update Ниже представлены все возможности CompoJax, параметры PJAX и способы его кастомизации. Данный код можно использовать везде, в компонентах, шаблонах, своих классах, неважно. Пример использования
use Jugger\Context\CompoJax;
/*
* простой вызов, без параметров
*/
if (CompoJax::begin("id_container")) {
// ...content...
CompoJax::end();
}
/*
* указываем картинку лоадер
*/
$params = [
"loading" => "http://dev.1c-bitrix.ru/community/webdev/%3Ca%20href=%22http://loader.gif%22%3Ehttp://loader.gif%3C/a%3E",
];
if (CompoJax::begin("id_container", $params)) {
// ...content...
CompoJax::end();
}
/*
* указываем URL к которому делается запрос на вывод данных
* таким образом Вы сможете динамически подгружать данные откуда угодно
*/
$params = [
"url" => "/api/methodName?param1=...",
];
CompoJax::begin("id_out_container", $params);
/*
* можно перегрузить логику обработки ответа
*/
$js = <<<JS
function(xhr, params) {
// xhr - объект XMLHttpRequest
// params - объект содержащий параметры блока: id - индектификатор контейнера, url - адрес на который отправлялся запрос
if (xhr.status == 200) {
document.getElementById(params.id).innerHTML = xhr.responseText;
}
else {
// обработка ошибки
}
};
JS;
$params = [
"callback" => $js,
];
if (CompoJax::begin("id_container", $params)) {
// ...content...
CompoJax::end();
}
/*
* для работы с PJAX необходимо добавить соответствующий пункт в параметрах,
* структура представлена на примере ниже,
* добавлять можно несколько вариантов привязки
*/
$params = [
"pjax" => [
// по умолчанию контейнер самого compoJax блока
// "container" => "#id_container"
[
"selector" => "a", // обязательный параметр
],
[
"selector" => "form#pjax-form",
"container" => "#pjax-form-contaner",
// параметры самого PJAX (подробнее: [URL=https://github.com/defunkt/jquery-pjax#pjax-options]https://github.com/defunkt/jquery-pjax#pjax-options[/URL] )
"options" => [
"push" => false,
"scrollTop" => false,
],
],
// правило ниже не примениться, т.к. не указан 'selector'
[
"container" => ".fail-container"
]
],
];
if (CompoJax::begin("id_container", $params)) {
// ...content...
CompoJax::end();
}
CompoJax включен в либу Juggernaut, однако, вы можете скачать его отдельно (всего 1 класс), ссылки ниже.
Ну вот и все В принципе, все готово, все круто, все работает. Осталось только добавить кеширование и вообще будет замечательно. Это будет добавлено в скором времени, возможно.
Юлиана Присяжнюк, а если композит включен зачем тогда CompoJax? Только ради PJAX. Они делают одно и тоже, только слегка разными способами. В статье приведен пример как управлять тем, что отображается при загрузке.
Продолжаем лупить статьи на тему «Битрикс не так уж и плох, если его доработать». На этот раз разговор пойдет на тему «url_rewrite», потому как я считаю, что текущий вариант вообще не идеален. А идеальным я считаю вариант маршрутизации в микрофреймворках, например Slim (или тот же Lumen), вообщем тех, которые дружат с PSR-7.
INTRO
На самом деле мои предыдущие статьи носили более менее абстрактный характер (ну кроме статьи про Juggernaut пожалуй), поэтому в данном посте постараюсь меньше писать теории и побольше кода.
Кстати про Juggernaut
Документация будет в скором времени, к сожалению есть некоторые преграды:
время
рефакторинг
мне полюбился TDD, так что рефакторинг остановился до тех пор пока не напишу тесты
направление развитие библиотеки как оказалось я не совсем еще до конца определил
Но это как говорить «совсем другая история», поэтому вернемся к тому, о чем собственно данная статья: роутинг.
UrlRewrite by Bitrix
Порядок маршрутизации я думаю лучше изобразить схемкой (и понятно, и наглядно):
Что это все значит:
include bitrix/urlRewrite.php Подключаем файл который занимается маршрутизацией (ну это я думаю и так все поняли). Вообще данный пункт (и все что выше на блок схеме) — это заслуги .htaccess:
RewriteCond %{REQUEST_FILENAME} !-f # не файл
RewriteCond %{REQUEST_FILENAME} !-l # не символьная ссылка
RewriteCond %{REQUEST_FILENAME} !-d # не директория
RewriteCond %{REQUEST_FILENAME} !/bitrix/urlrewrite.php$ # не файл маршрутизации
RewriteRule ^(.*)$ /bitrix/urlrewrite.php [L]
fix request_uri for IIS Данный пункт, судя по комменту в коде, ответственен за какие то косяки IIS (бедняга MS), за какие я не в курсе, но логика следующая: если QUERY_STRING имеет вид «wtf=404;http(s)://wtf.ru», то все GET параметры запроса чистятся и данная конструкция убирается из адреса. На вопрос «что проиходит?» я не могу дать ответа, так что едем дальше.
include dbconn.php Подключаем базу. Зачем? Непонятно, т. к. запросов к базе дальше нет и работа идет только с файловой системой. Я конечно не опускался в реализацию классов для работы с файлами, но если им нужно что-либо от базы, то это не иначе как печально
decode request page (for UTF-8 ) Все понятно из названия, кодирование REQUEST_URI. Зачем? Зачем Битрикс любит Windows-1251? Без понятия. Но это будет продолжаться вечно (и это инсайдерская информация).
include /urlRewrite.php Собственно подключаем сами правила маршрутизации.
process Url Немного странные действия, но все же происходит следующее: Если есть GET параметр SEF_APPLICATION_CUR_PAGE_URL, то приравниваем REQUEST_URI к его значению, а затем переписываем все зависимые переменные и глобальные массивы ($_GET, $_SERVER, …)
process urlRewrite О, да! Мы до него добрались. Собственно что происходит:
Парсим параметр CONDITION.
Заменяем параметр CONDITION на RULE в REQUEST_URI
Добавляет в $_GET и $_REQUEST переменные из правила маршрутизации.
Проверяем существует ли указанный файл, валидный ли у него путь и не является ли он административным (upload, bitrix, bitrix/services, bitrix/groupdavphp).
Если все ок, то подключаем.
Меня одного смущает что проверка идет после того как мы уже сунули все параметры в глобальные переменные?
Много неясностей, зачем сделано так, а не иначе? Ну и много неясностей, зачем вообще это сделано? Так что теперь перейдем к идеальному варианту Slim’a.
Легко и непринужденно цепляемся к нужному действию, с нужным маршрутом, параметрами и реализацией.
UrlRewrite by Juhhernaut
Ну, а теперь пробуем все это миксануть. Выкидываем из Slim'a указание метода и собственно реализацию действия, вместо нее будет путь до файла. Для начала обозначим синтаксис привязки маршрутов к реальным физически файлам (по факту это является мануалом использования):
/*
* подключаем файлы для роутрера
*/
include_once __DIR__.'/modules/olof.juggernaut/includeRewrite.php';
use Jugger\Context\Router;
/**
* создаем роутер
* в отличии от Slim'a маршруты не добавляются, а выполняются
* таким образом как только маршрут найден,
* остальной код не будет выполняться
*/
$r = new Router();
/*
* поиск файла с комнца маршрута
* например, URL запроса выглядит так: "/catalog/section1/section2/element1/",
* То поиск поочередно будет перебирать директории в поисках файла 'index.php':
* - /catalog/section1/section2/element1/index.php
* - /catalog/section1/section2/index.php
* - /catalog/section1/index.php
* - /catalog/index.php
*
* в корень сайта опускаться поиск не будет
* также никакие параметры не будут добавлены в переменные GET и REQUEST,
* т.к. нет шаблона маршрута
* данный способ хорошо подходит для стандартной ситуации Битрикс
* когда маршрутизация ложится на плечи компонентов
*/
$r->runRecursive();
/*
* добавляем маршрут
* формат записи:
* {nameParam},
* {nameParam:regExp}
* где 'regExp' - регулярное выражение. Например, '\d+' или '[0-9]+'
*/
$r->run(
"/page/",
"/page/index1.php"
);
$r->run(
"/page/{p1:[0-9]+}/{p2}",
"/page/index2.php"
);
$r->run(
"/catalog/",
"/catalog/index1.php"
);
/*
* добавляем сразу несколько маршрутов
*/
$r->run(
[
"/catalog/{sectionCode}/",
"/catalog/{sectionCode}/{elementId:\d+}/",
],
"/catalog/index2.php"
);
/*
* маршрутизация в один файл с параметром ?r=path/to/file
*/
$r->run("{r:.+}", "index.php");
/*
* окончание роутига
* если никакой маршрут не подошел
* то подключается /bitrix/urlrewrite.php
*/
$r->end();
По факту, если реализацию маршрутов оставить на совести компонентов, то достаточно будет прописать следующую конструкцию (да, так тоже можно ):
(new Router())
->runRecursive()
->end();
Данный файл нужно (можно) назвать urlrewrite.php, кинуть его в папку /local/ и внести правки в .htaccess файл, который лежит в корне.
Олег Постоев, пока на одном сайте, и только runRecursive (на самом деле она мне и нужна была по большей части). Сайт: olof.ru. Не глюкает, ровно работает. Потихоньку буду втыкать в другие проекты.
кстати вопрос, если у тебя должно использоваться чпу, и ты используешь какой-нибудь компонент на главной, в котором шаблон url не указан, типа должен быть взят из инфоблока, как твой модуль это обработает?
Юлиана Присяжнюк, вообще "по логике", данные маршрутизации должны быть в одном отдельном месте, а не размазаны по моделям и компонентам как в Битрикс. Теперь к вопросу:
Юлиана Присяжнюк написал: если у тебя должно использоваться чпу, и ты используешь какой-нибудь компонент на главной, в котором шаблон url не указан, типа должен быть взят из инфоблока, как твой модуль это обработает?
Никак, потому что это уже зона ответственности компонента (в рамках Битрикс). Роутер срабатывает до загрузки какой либо страницы, он по факту и определяет какая страница будет загружена.
В статье рассматривается альтернативный способ загрузки страниц т. н. «Единая точка входа» (иначе FrontController). Его сравнение с текущим способом загрузки ну и конечно реализация данного подхода.
INTRO
Все что описано ниже, это мое личное мнение и виденье, если оно вас не устраивает, если вы считаете меня не компетентным, или недостаточно опытным — это сугубо ваше личное мнение. Если говорить про объективную критику и советы — то милости прошу, это очень даже полезные штуки, тем более что именно для них и написана статья. Ну, а теперь поехали
AFTER_INTRO
Так уж случилось, что по каким-то причинам Битрикс не имеет единую точку входа. Не то, чтобы это было прям необходимо, но ДА - это прям необходимо! Почему же единая точка прям таки нужна (прям таки для Битрикс):
Вызов функции Cmain::ShowSpreadCookieHTML (данная функция выводит набор невидимых IFRAME'ов используемых в Технология переноса посетителей)
Событие OnEpilog
Завершение буферизации страницы
Отправка E-Mail писем
Событие OnAfterEpilog
Завершение соединения с базой данных
Ну а теперь небольшой разбор всего того что выше (если какие то пункты опущены, то значит по ним нет комментариев): 2-3. а зачем подключаться сразу? Если мы хотим вернуть статику (кеш) это действие будет лишним; 7. проверка, то есть запуск? Вообще немного странно, но про агенты будет далее; 8. опять лишнее действие если данные в сессии не хранятся (хотя по факту Битрикс ее юзает в любом случае, но по идее стартовать должен модуль ее использующий, и это не должно быть заложено априори); 16-21. самое непонятное что может только быть. То есть если нам нужно вывести при обработке страницы, что-либо в шаблоне (title, breadcrumbs, установка заголовков, ...) мы прекращаем текущую буферизацию, запоминаем данные, а затем начинаем буферизацию по новой и по сути загрузка страницы может выглядит примерно так:
// блок 1
ob_start();
$APPLICATION→AddBufferContent(…);
// записываем в переменную то, что вывели в блоке 1
ob_end_clean();
// блок 2
ob_start();
$APPLICATION→AddBufferContent(…);
// записываем в переменную то, что вывели в блоке 2
ob_end_clean();
// …
// записывает в переменную то, что вывели в блоке 100500
// находится компонент который возвращает JSON пакет (для AJAX запроса)
// собственно чистим буфер, т. к. то что вывелось выше нам не нужно
$APPLICATION→RestartBuffer();
22. почему отдельно, а не в пункте 23?
Собственно опущены очевидные вещи и все то что касается многосайтовости.
ЗАГРУЗКА СТРАНИЦЫ (by Juggernaut)
Для начала нужно составить порядок загрузки страницы:
Событие OnInit (конструктор)
Событие OnBeforeRequest
Обработка запроса
Событие OnAfterRequest
Отправка ответа (вместе с заголовками, куками и данными)
Теперь конкретно по пунктам:
OnInit – в данном событии определяются константы, обработчики событий и какой сайт запрашивается (собственно сначала нужно делать это, а потом уже все остальное).
OnBeforeRequest – сюда можно посадить например выполнение агентов.
Обработка запроса — определение нужно действия, обработка ошибок и прочие полезности.
OnAfterRequest – сюда можно посадить например логирование, выполнение агентов (хотя лучше не стоит, ниже подробнее).
Отправка ответа — собственно вот
Чем проще тем лучше господа!
Прежде чем я перейду к профитам данного порядка загрузки, представлю псевдокод (концепт) который собственно данный порядок формализует и добавляет некоторые возможности:
class Main
{
const EVENT_INIT = 'mainOnInit';
const EVENT_BEFORE_REQUEST = 'mainOnBeforeRequest';
const EVENT_AFTER_REQUEST = 'mainOnAfterRequest';
const EVENT_ERROR = 'mainOnError';
/**
* Функция обработки ошибки
* @var callable
*/
protected $errorHandler;
/**
* Инициализация предварительных данных
*/
public function __construct() {
$this->initAutoloader();
$this->initListeners();
$this->initErrorHandler();
//
Event::trigger(self::EVENT_INIT);
}
/**
* инициализация автозагрузчика,
* для возможности неявно подключать классы модулей, без ручного подключения,
* для возможности вызова класса компонентов напрямую (не через APPLICATION)
*/
public function initAutoloader() {}
/**
* загружает файлы инициализации всех активных модулей.
* вместо того чтобы вносить какие-либо изменения в файл 'php_interface/init.php'
* нужно в каждом модуле, если это требуется,
* создать файл 'init.php', который будет подгружаться в данном событии.
* в файле 'init.php' можно вешать слушателей на любые события,
* а также производить любые необходимые действия с учетом текущего этапа загрузки
*/
public function initListeners() {}
/**
* Точка входа
*/
public function run() {
try {
$this->onBeforeRequest();
$response = $this->handleRequest();
$this->onAfterRequest();
$response->send();
return $response->status;
}
catch (Exception $error) {
call_user_func($this->errorHandler, $error);
return $error->getCode();
}
}
/**
* событие ПЕРЕД обработкой запроса,
* сюда можно повешать выполнение агентов
*/
public function onBeforeRequest() {
$this->initSite();
$this->initUrlRewrite();
//
Event::trigger(self::EVENT_BEFORE_REQUEST);
}
/**
* инициализируются параметры запрошеного сайта:
* - id
* - локализация (по-умолчанию)
* - шаблон (по-умолчанию)
* - ...
*/
public function initSite() {}
/**
* инициализация URL менеджера
*/
public function initUrlRewrite() {}
/**
* непосредственная обработка запроса,
* определяет запрошеную страницу,
* формирует объект ответа,
* обработка запрошенной страницы,
* подключает запрошенную страницу на основе полученного маршрута,
* инициализирует контроллер страницы которые отвечает:
* - за вывод данных
* - за тип данных
* - за подключение шаблона
* - за атрибуты страницы
* - др.
*/
public function handleRequest() {
list($page, $params) = $this->parseRequest();
$pageController = new PageController($page);
$pageController->params = $params;
$pageController->run();
return $pageController->response;
}
/**
* событие ПОСЛЕ обработки запроса,
* сюда можно повешать различные логгирования
*/
public function onAfterRequest() {
Event::trigger(self::EVENT_AFTER_REQUEST);
}
/**
* вещает обработчика ошибок,
* если не был установлен никакой кастомный
*/
public function initErrorHandler() {
if ($this->errorHandler === null) {
/**
* подключает страницы с соответствующими страницами
*/
$this->errorHandler = function(Exception $error) {
if ($error instanceof NotFoundHttpException) {
include '404.php';
}
elseif ($error instanceof BadRequestHttpException) {
include '400.php';
}
else {
include '500.php';
}
};
}
}
/**
* выполняет разбор запроса, на основе URL менеджера и текущего сайта
*/
public function parseRequest() {}
}
Реализация классов Request, Response и др. опущена, потому что не имеет особо значения (на данный момент по крайней мере).
Какие плюсы вытекают из «нового» варианта загрузки:
это проще и прозрачнее
избавляет от глобальных переменных типа APPLICATION, USER, …
упрощает подключение файлов и модулей
все страницы, можно вынести из публичной части
избавляет от AddBufferContent и ее логики коллбэков
нет include (вообще)
…
На самом деле это все, что пришло в голову, но мне достаточно 1 пункта.
Вот такой небольшой вариант альтернативной загрузки, ваши вопросы, предложения, пожелания и гневные комменты жду ниже
P.S. небольшой опросик напоследок, который меня дико волнует
Здрасте! В статье я рассмотрю альтернативу 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С-Битрикс».