0  /  380
Справочник

Разработчик Bitrix Framework

Содержание

Как отформатирован текст в курсе

Читать монотонный текст сложно. Форматирование – один из способов облегчить понимание смысла. Ознакомимся с правилами оформления текста в курсе.

Всплывающие подсказки

Используются в тексте в следующих случаях:

  • пояснить какой-то термин;
  • дать наглядное представление о команде интерфейса;
  • дать дополнительную, но не относящуюся напрямую к выполняемым работам, информацию;
  • показать описание и ссылку на страницу компонента в пользовательской документации.

Всплывающие подсказки обозначаются пунктирным подчёркиванием: [dw]пример подсказки[/dw][di]В окне отображается подробное содержимое подсказки. Подсказка может состоять из текста и/или изображения.[/di].

Табы (вкладки)

Текст каждого урока разбит на табы. Другими словами их можно назвать тематическими вкладками. Это сделано с целью избавить читателя от прокрутки экрана и быстро перейти к нужной части урока. Если вам неудобно пользоваться вкладками, то [dw]отключите их[/dw][di][/di], тогда весь урок будет выводиться в виде единого длинного текста.

Внимание! При использовании табов поиск в браузере ищет только по открытой вкладке, а не по всему уроку.

Спойлеры, скрывающие текст

В спойлерах размещается информация, которая имеет вторичное значение, но может быть полезна «для общего развития». Для открытия спойлера кликните на «+» в спойлере:

Ознакомьтесь с простым примером как работает продукт.

Чтобы избежать лишней прокрутки экрана, в спойлерах может быть размещена большая по объёму информация.

Выделение абзаца фоном

Форматирование целых абзацев текста используется для выделения какой-либо информации с целью указать её направленность:

  • Определения каких-либо сущностей, явлений, терминов и так далее выводятся в абзацах такого вида:

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

  • В Примечании размещается информация, поясняющая текст на странице, на которую стоит обратить внимание. Это могут быть примеры, расширенное объяснение, уточнение, ссылка на дополнительную информацию. Например:

    Примечание: подробное описание формы создания и редактирования теста представлено в документации по продукту.

  • Предупреждения – это информация, имеющая критическое значение для работы того или иного функционала продукта. Например:

    Важно! Если курс был деактивирован, то результаты тестов и сертификаты по этому курсу будут недоступны пользователям. Если курс был удален, то результаты тестов и сертификаты этого курса будут также удалены.

    В особо важных случаях в этом виде форматирования может использоваться красный шрифт.

Форматирование текста

Стиль Назначение Пример использования
Жирный шрифт Для выделения важных в смысловом значении слов, фраз, терминов, названий элементов интерфейса. В поле Идентификатор введите краткое название латинскими буквами.
Курсив и подчёркивание Для названий продуктов компании 1С-Битрикс, сторонних программ и платформ. Также для названий методов, событий, классов, пространств имён в D7, если нет их описания в API. После изучения теоретического курса будет полезным выполнить практические задания по работе с "1С-Битрикс: Управление сайтом".
Серый фонДля выделения путей в рамках файловой системы, атрибутов, HTML-тегов, параметров функций, переменных, значений полей и короткого кода. В Короткой ссылке рекомендуется использовать знак тильда: ~.
Синий курсив Для подсветки путей в продукте. Пути выглядят так же, как они оформлены в административном разделе системы. Создание опроса начинается со страницы Группы опросов (Сервисы > Опросы > Дополнительно > Группы опросов).
Синий шрифт Подсветка URL-адреса без создания собственно ссылки – псевдо УРЛ. Пример адреса страницы с контактами: https://mysite.ru/about/contacts/.

Что такое Bitrix Framework?

Цитатник веб-разработчиков.

Александр Сербул: БУС это не "компоненты перетаскивать и галочки в админке тыкать", а это - хардкорная разработка модулей, доработка ядра, жесткий ORM, последние новинки PHP.

Bitrix Framework - это созданная на основе PHP платформа для разработки веб-приложений. На этой платформе компанией «1C-Битрикс» созданы два популярных продукта: «1C-Битрикс: Управление сайтом», «Битрикс24 в коробке» и облачная версия "Битрикс24"..

Продукт «1C-Битрикс: Управление сайтом» представляет собой программное ядро для всестороннего управления веб-проектами любой сложности.

Продукт «Битрикс24» - готовый продукт, позволяющий создать корпоративный портал компании, с возможностью доработки штатного функционала под потребности компании (в коробочной версии), либо готовый облачный продукт, функционал которого можно расширять за счёт создания Rest-приложений.

В отличие от Zend Framework при разворачивании Bitrix Framework мы получаем не только набор классов, но и развитый интерфейс администрирования.

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

Продукты на базе Bitrix Framework выходят в нескольких редакциях. Изучая систему и повторяя уроки необходимо быть уверенным, что ваша локальная установка имеет модуль, с которым вы экспериментируете. Помодульное сравнение редакций: для 1С-Битрикс: Управление сайтом, для Битрикс24.

Примечание: В учебном курсе будут приводиться примеры из задач стоящих перед разработчиками разных продуктов. Механизмы решения этих задач можно применять в любом другом продукте, созданном на Bitrix Framework. В этом плане слова сайт и корпоративный портал в рамках этого курса можно рассматривать как синонимы.

Список ссылок по теме:

Как изучать Bitrix Framework?

Как построить обучение?

Цитатник веб-разработчиков.

Виктор Векслер:

Мы выбрали подход кейсов в обучении, стажеры получают 5-10 кейсов в течение первого месяца, которыми и занимаются, при этом выделяется куратор, который общается, поддерживает и направляет стажера. При этом стажер учится на реальных задачах и не сильно мешает куратору.

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

Любой соискатель при собеседовании получает тестовое задание, оно сложное (для начинающего). Он получает 2 psd шаблона, дистрибутив Битрикса с окружением и мануал по интеграции. Его задача разобраться и интегрировать данные шаблоны в Битрикс, если он не может этого делать, мы его не принимаем.

С первого дня, стажер получает первый кейс - это сайт турфирмы, основная задача здесь разобрать инфоблоки и немного познакомиться с Битриксом вообще. На данный проект в среднем уходит 1 неделя. Далее 2-ой кейс - это снять реальный сайт и сделать его полную копию на Битриксе (в среднем 3-4 дня). Далее пишет элементарный компонент - один день (может меньше). После, если у нас есть легкие проекты, он работает как подмастерье у программиста, если нет делает кейс интернет-магазина, если он осилил его, то он уже может переходить на сайты визитки.

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

Скачать кейсы

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

Идеальный вариант. Пошаговое изучение всех курсов в линейке сертификационных курсов от Контент-менеджера через курсы Администратор до курса Разработчик Bitrix Framework. С одновременным изучением API и пользовательской документации.

Реальный вариант. Как правило, - это неделя на изучение, и потом работа над новым проектом в рамках веб-студии или на фрилансе. В этом случае необходимо знать интерфейс программы (глава Элементы управления курса Контент-менеджер) и данный курс. При работе впоследствии с проектом - постоянное обращение к документации для разработчиков, пользовательской документации и другим учебным курсам.

Сколько времени уйдет?

Опыт подготовки разработчиков от одного из партнеров

У нас стажёры (типовой опыт: 5 лет института, возможно, фриланс и знание РНР) с обучением по курсам, внутренней документации и консультациями разработчика, примерно через месяц понимают как и что устроено. Общий курс обучения - порядка 2-3 месяцев.

То есть через 1,5 месяца стажёр сдаёт тестовое задание (собранный на типовых компонентах сайт), а через 3 уже вливается в команду.

Более короткие сроки обучения приводят к тому, что разработчики пишут крайне неоптимальные вещи с точки зрения Bitrix Framework или испытывают трудности при решении типовых задач. Студия выросла с 5 до 20 человек. Всех обучали сами.

Это зависит от уровня стартовой подготовки и от способностей обучаемого. И в немалой степени зависит не только от знаний, сколько от умения отдаляться от прошлого опыта и смотреть на все новыми глазами.

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

Оффлайновое обучение

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



Примечание: Bitrix Framework активно развивается. Его нельзя изучить один раз и жить на этом знании все оставшееся время. Надо постоянно быть в курсе событий. Все новинки документации (новые уроки и изменения в старых) можно отслеживать с помощью страницы Что нового? или канала в Телеграмме)

Внимание! Приведенные в курсе примеры работы с API могут устаревать в силу постоянного развития системы. Отдел документации отслеживает это развитие, но задержки с внесением изменений в курсы могут быть.

Где брать информацию?

Все продукты компании «1С-Битрикс» сопровождаются полной документацией, которая расположена на сайте в разделе Документация.

Документация

Цитатник веб-разработчиков.

Степан Овчинников: Битрикс огромен. Перечень вопросов, которые человек задает сразу, на первом проекте, относительно мал. Но вот объем знаний, нужных на втором этапе погружения, когда делается нетривиальное, действительно очень велик. Его нельзя наработать быстро.

Документация для продукта «1С-Битрикс: Управление сайтом» включает в себя:

  • Онлайн документация для пользователей

    Нажмите на рисунок, чтобы увеличить

    Справочная документация представляет собой описание интерфейса системы управления сайтом, ее основных модулей и наиболее типичных операций. Также в ней содержится полное описание прикладного программного интерфейса.

    Рекомендуется таким группам пользователей, как Контент-менеджер и Администратор.

  • Онлайн документация для разработчиков

    Документация, предназначенная для технических специалистов со знанием PHP и HTML. В ней содержатся сведения о технологиях и основных принципах, заложенных в систему, описание классов и функций.

    Рекомендуется таким группам пользователей, как Администратор и Разработчик.

  • Онлайн документация для разработчиков по Rest_API

    Документация, предназначенная для технических специалистов со знанием PHP и HTML. В ней содержатся сведения о технологиях и основных принципах, заложенных в систему, описание классов и функций, относящихся к сервису Битрикс24.

    Рекомендуется таким группам пользователей, как Администратор и Разработчик.

При работе с Bitrix Framework очень большое значение имеет описание API. К сожалению составление описаний API нового функционала никогда не выходит одновременно с функционалом. В этом случае рекомендуем воспользоваться уроком Если нет описания API.

Оффлайновые файлы

Документация в файлах формата .chm

Содержит копию онлайн документации в формате справки Windows (.chm). Данная документация обновляется реже, чем источник на сайте. Удобно использовать при отсутствии интернета.

Если не отображается содержимое файла CHM

Для изучения материалов учебных курсов в оффлайне на индексной странице каждого курса размещаются [dw]ссылки на файлы[/dw][di][/di] формата [dw]EPUB[/dw][di] Чем открыть файл на
Android:
EPUB Reader
CoolReader
FBReader
Moon+ Reader
eBoox

iPhone:
FBReader
CoolReader
iBook
Bookmate

Windows:
Calibre
FBReader
Icecream Ebook Reader
Плагины для браузеров:
EpuBReader – для Firefox
Readium – для Google Chrome

iOS
Marvin for iOS
ShortBook
[/di]. Через некоторое время и файлы документации тоже будут предоставляться в этом формате.

Рекомендуется таким группам пользователей, как Контент-менеджер, Администратор и Разработчик.

Учебные курсы

Онлайн Курсы

Нажмите на рисунок, чтобы увеличить

Методические пособия по работе с продуктом, которые включают в себя описание и примеры работы с системой.

Рекомендуется таким группам пользователей, как Контент-менеджер, Администратор и Разработчик.

FAQ

Частые вопросы

Нажмите на рисунок, чтобы увеличить

Подборка решений наиболее часто встречающихся проблем при работе с продуктом в удобном представлении.

Рекомендуется таким группам пользователей, как Администратор и Разработчик.

Видео

Учебные видеоролики

Видеоролики демонстрируют основные функциональные возможности продукта.

Внимание! Материалы роликов могут отставать от текущих версий продуктов.

Рекомендуется таким группам пользователей, как Контент-менеджер и Администратор.

Сообщество разработчиков

Цитатник веб-разработчиков.

Евгений Смолин: Ну а вам совет - не бойтесь Битрикса, Битрикс это всерьез и надолго. Начинайте работать с ним, появятся вопросы или не пожелаете нанимать разработчиков, то обращайтесь к форуму, в ТП. Почти всегда помощь оказывают, причем оказывают помощь такие монстры, что даже опытным разработчикам интересно смотреть на решения.

За время развития продукта сложилось свое сообщество разработчиков на Bitrix Framework. Это сообщество представлено на сайте компании «1C-Битрикс» и обменивается опытом работы и результатами работы на форуме компании, в группах социальной сети и на собственных сайтах.

Совет: Пока вы не овладеете в полной мере терминологией Bitrix Framework, рекомендуем при описании проблемы избегать терминологии, известной вам по другим CMS.

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

Внимание! Форум сайта не является службой техподдержки. Техподдержка работает согласно регламенту и в другом разделе сайта.

Рекомендуется познакомиться с опытом разработок сайтов на системе Bitrix Framework, который описывается в блогах разработчиков Bitrix Framework и в группах Социальной сети сайта компании 1С-Битрикс. Так же будет полезно заглядывать на хабр за статьями об 1С-Битрикс и на официальный Хабр компании Битрикс24.

Для тех, кто переходит на Bitrix Framework с других платформ

Цитатник веб-разработчиков.

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

Не сравнивать, а понять

Для программистов, переходящих на Bitrix Framework с других платформ и CMS возникают дополнительные сложности, вызванные «давлением» предыдущего опыта.

Чтобы научиться эффективно работать в Bitrix Framework, нужно не сравнивать то, что вы знаете по другим системам, а стараться понять как то или иное реализуется в этой системе. В плане обучения «сравнительный» подход не работает. Просто отвлекитесь от старых знаний и изучите новую систему используя только знания PHP и сайтостроения, а не сравнивая идеологии и технологии. Легче будет освоить. А сравнивать будете потом, когда освоите Bitrix Framework.

Зато у вас может возникнуть преимущество знания двух систем. В последнее время заказчики нередко ставят задачу миграции сайтов с других систем на Bitrix Framework. Настолько нередко, что партнеры компании «1C-Битрикс» даже разрабатывают специальные решения под эти задачи.

Прямое сравнение Bitrix Framework и других систем далеко не всегда корректно. Тем не менее, такие вопросы возникают и потому приведем некоторые мнения, высказанные партнерами компании «1C-Битрикс» и программистами, работающими на Bitrix Framework.

Дополнительно:

  • Пример "переезда" сайта с неизвестной CMS на "1С-Битрикс: Управление сайтом"

Bitrix Framework и Drupal

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

В Bitrix Framework реализована идеология инфоблоков, которые структурно можно уподобить таблице в базе данных. Инфоблок представляет собой совокупность объектов, обладающих одинаковым набором свойств.

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

Bitrix Framework и Joomla

  • Шаблоны сайта в Bitrix Framework примерно соответствуют по концепции шаблонам сайта в Joomla.
  • Создание шаблона сайта для Bitrix Framework по готовой верстке заключается в выделении блоков и размещении вместо этих блоков компонентов. Далее эти компоненты настраиваются на источник данных и для них редактируются шаблоны вывода в соответствии с версткой сайта.
  • В шаблоне сайта для Bitrix Framework нет позиций под модули с номером, как в Joomla. И программист не может из административной части указать какой модуль в какую позицию ставить, просто изменив число. Размещение компонентов в Bitrix Framework реализовано по-другому.
  • Модули в Bitrix Framework - это сущность для объединения необходимых программистам функций "в одном флаконе" и разделения по редакциям. Аналог в Joomla - расширения.
  • Модуль в Joomla - это компонент в Bitrix Framework. Компонент извлекает информацию из различных источников системы и выводит её в виде фрагментов web-страниц. Шаблон в составе компонента отвечает за вывод данных на страницу.

    У одного компонента может быть несколько шаблонов, по-разному выводящих информацию. Например, если вам нужно вывести на сайте новости и статьи, вы создаете две папки на диске, кидаете в каждую из них по комплексному компоненту Новости и настраиваете их на источник данных. Дальше вы можете перерабатывать шаблоны.

  • В Bitrix Framework разделяются динамические и статические данные. Есть чисто динамические блоки, например, каталог товаров. Есть статические. Есть смешанные. Однако, для того, чтобы вывести динамическую информацию, например, каталог товаров, вы должны сделать для него «домик» - папку на диске, в которой будет находиться комплексный компонент «каталог», обрабатывающий обращения к динамической информации.

Глоссарий

Что есть что?

Каждая платформа имеет свои термины, используемые в работе. Чтобы без проблем ориентироваться в документации, учебных курсах, при общении с разработчиками желательно объяснять ваши трудности не «на пальцах», а на языке понятном специалистам. Ниже в помощь вам приведен глоссарий терминов системы и несколько общих терминов, активно используемых в системе.


ТерминОписание
Административная частьРаздел системы, содержащий интерфейс для управления модулями системы, структурой, содержанием, посетителями и другими составляющими сайта. Страницы, в которых подключен административный пролог и эпилог. Эти страницы:
  • не принадлежат ни одному сайту;
  • принадлежат какому-либо модулю;
  • имеют строго определённый административный интерфейс;
  • отображаются в выбранном языке интерфейса;
  • дополнительно проверяются по правам, задаваемым в настройках соответствующего модуля.
Публичная частьРаздел системы, доступный для отображения посетителям сайта.На страницах раздела подключается пролог и эпилог одного из шаблонов сайта. Эти страницы:
  • принадлежат какому либо сайту;
  • имеют интерфейс текущего шаблона сайта;
  • отображаются на языке текущего сайта.
Режим Правка Режим, при котором текущая страница публичного раздела отображается в особом виде: выделены используемые компоненты, включаемые области, редактируемые области и т.д. Каждая такая область имеет набор кнопок для быстрого перехода к редактированию данного элемента страницы.
СайтСовокупность параметров:
  • учетная запись в базе данных - создаётся в административном меню Сайты, включает в себя:
    • идентификатор - набор символов, идентифицирующий сайт;
    • доменное имя - одно или более доменное имя сайта;
    • папка сайта - путь к каталогу в котором будет храниться публичная часть сайта;
    • язык сайта;
    • формат даты и времени;
    • URL - протокол и доменное имя по умолчанию (например, http://www.site.ru);
    • DocumentRoot - если многосайтовость организована на разных доменах, то в данном параметре должен храниться путь в файловой системе сервера к корню сайта;
    • условия подключения шаблонов - каждый сайт может иметь один или более шаблонов для отображения скриптов своей публичной части, каждый такой шаблон может быть подключен по тому или иному условию;
  • публичная часть - совокупность страниц, лежащих в папке сайта и принадлежащих этому сайту;
  • настройки - каждый модуль может иметь ряд настроек, связанных с сайтом, например у модуля Информационные блоки эти настройки представляют из себя привязку информационного блока к тому или иному сайту, у модуля Техподдержка - привязку статуса, категории и т.п. к сайту.
Шаблон сайтаСинонимы: дизайн сайта, скин сайта. Для показа одного сайта можно использовать несколько различных шаблонов.
Шаблон сайта - это набор файлов в каталоге /bitrix/templates/ID_шаблона/, где ID_шаблона - поле ID в [ds]форме редактирования[/ds][di]Управление шаблонами дизайна осуществляется в административном разделе на странице Шаблоны сайта (Настройки > Настройки продукта > Сайты > Шаблоны сайтов), где можно...

Подробнее ...[/di] шаблона сайта. Структура каталога:
  • /components/ - каталог с компонентами, принадлежащими тому или иному модулю;
  • /lang/ - языковые файлы, принадлежащие как шаблону в целом, так и отдельным компонентам;
  • /images/ - каталог с изображениями шаблона;
  • /page_templates/ - каталог с шаблонами страниц и их описанием, хранящимся в файле .content.php;
  • /include_areas/ - каталог с файлами - содержимым включаемых областей;
  • header.php - пролог шаблона;
  • footer.php - эпилог шаблона;
  • styles.css - CSS стили, используемые на страницах сайта, когда используется шаблон;
  • template_styles.css - CSS стили, используемые в самом шаблоне;
  • .тип_меню.menu_template.php - шаблон вывода меню соответствующего типа;
  • chain_template.php - шаблон по умолчанию для вывода навигационной цепочки;
  • а также ряд других вспомогательных произвольных файлов и папок, входящих в данный шаблон.
Раздел сайтаКаталог в файловой системе сервера. В Bitrix Framework структура сайта - это файловая структура сервера, поэтому страницы сайта - это файлы, а разделы сайта - соответственно каталоги.
КомпонентЧасть какого-либо модуля, логически завершенный код, хранящийся в одном файле. Принимает ряд параметров, выполняет ряд действий и выводит результат этих действий (например, в виде HTML кода). Использование компонента - предпочтительный способ вывода информации как в публичной, так и в административной частях. Компонент подключается методом IncludeComponent Рабочая версия компонентов - 2.0. Использование компонентов 1.0 не рекомендуется, но их ещё можно встретить на сайтах, построенных на ранних версиях системы.
Путь к компонентуУстаревшее. Используется в функции CMain::IncludeFile в качестве первого параметра и представляет из себя путь к основному файлу компонента 1.0.
Включаемые области Это специально выделенная область на странице сайта, которую можно редактировать отдельно от основного содержания страницы. Реализуется с помощью специального компонента.
Навигационная цепочкаЭто элемент дизайна, предназначенный для навигации по сайту. Выводится в визуальной части пролога и состоит из заголовков разделов сайта с соответствующими ссылками на них. Помимо заголовков разделов, добавляемых автоматически, вы также можете добавлять произвольные пункты в навигационную цепочку.
Индексная страница (файл)Это имя файла, который будет использован веб-сервером в случае, если запрашиваемый URL заканчивается на слэш и не содержит в себе имени файла. Порядок, в котором будут искаться индексные страницы для различных веб-серверов, задается по разному, например:
  • Apache - в файле httpd.conf, параметр DirectoryIndex;
  • IIS - в свойствах сайта, закладка Documents > Enable default content page;
К сожалению, в PHP значение данного параметра недоступно, поэтому для определения индексной страницы в коде необходимо пользоваться функцией GetDirIndex.
ПользовательЗапись в базе данных с параметрами зарегистрированного пользователя, обязательные поля:
  • логин;
  • пароль;
  • E-Mail.
А также ряд дополнительных полей, содержащих личную информацию о пользователе, информацию по его работе, административные заметки.

Регистрационные данные (логин и пароль) в дальнейшем используются для авторизации в системе. Пользователь привязывается к определенной группе и получает право на доступ к ресурсам портала в соответствии с правами данной группы.

Группа пользователейСовокупность пользователей, обладающих определенными правами на доступ и управление ресурсами (например, пользователи группы Модераторы обладают правом на чтение и редактирование сообщений форума). Управление группами пользователей осуществляется на странице Группы пользователей в административном разделе (Настройки > Пользователи > Группы пользователей).
Почтовое событиеЭто почтовое сообщение, имеющее свой тип и отправляемое по соответствующему почтовому шаблону. Почтовое событие инициализирует поля типа почтового события конкретными значениями. Порядок расположения этих полей в письме, а также текст письма, определяется почтовым шаблоном.
Для создания почтового события предназначен класс CEvent.
Почтовый шаблонОпределяет текст почтового сообщения, а также порядок расположения полей (placeholder'ов), заданных в типе почтового события.
Почтовые шаблоны доступны в административном разделе на странице Почтовые шаблоны (Настройки > Настройки продукта > Почтовые события > Почтовые шаблоны).
Для манипуляции почтовыми шаблонами предназначен класс CEventMessage.
Тип почтового событияОпределяет набор специальных полей (placeholder'ов), которые могут быть использованы в почтовом шаблоне. В момент создания почтового события эти поля будут инициализированы конкретными значениями.
Типы почтовых событий доступны в административном разделе на странице Типы почтовых событий (Настройки > Настройки продукта > Почтовые события > Типы почтовых событий).
Для манипуляции типами почтовых событий предназначен класс CEventType.
Путь относительно корняПуть к файлу, начинающийся от каталога, указанного в параметре DocumentRoot в настройках веб-сервера, заданный по правилам формирования URL-адресов. Пример:/ru/about/index.php
Полный путьВключает в себя протокол, домен и путь относительно корня к странице (каталогу). Пример: http://www.bitrixsoft.ru/ru/about/index.php
Абсолютный путьАбсолютный путь к файлу включает в себя DocumentRoot и путь относительно корня.
DocumentRootПуть к корню сайта в файловой системе сервера. Задается в настройках веб-сервера, например:
  • для Apache - в файле httpd.conf, параметр DocumentRoot;
  • для IIS - в свойствах сайта, закладка Home Directory > Local Path.
Система обновленийТехнология SiteUpdate позволяет:
  • скачивать обновления продукта;
  • загружать новые модули и обновления для имеющихся модулей, расширяющие их функциональные возможности;
  • загружать языковые файлы и устанавливать новые языки;
  • выполнять регистрацию лицензий на дополнительные сайты.
В процессе обновления выполняется модификация только ядра продукта (файлы папок /bitrix/modules/, /bitrix/tools/, /bitrix/admin/ и /bitrix/components/bitrix/). Обновление не затрагивает публичную часть портала, полностью исключая возможность потери данных.

Обновление системы осуществляется в несколько шагов:

  • система обновлений автоматически запрашивает лицензионный ключ продукта;
  • затем выполняется проверка наличия доступных обновлений;
  • далее пользователю предлагается выбрать обновления для загрузки;
  • после этого происходит загрузка выбранных обновлений.
ЯзыкЭто учетная запись в базе данных, доступная для редактирования в административном меню на странице Языки интерфейса, со следующими полями:
  • Идентификатор;
  • Название;
  • Формат даты;
  • Формат времени;
  • Кодировка;
  • и т. д..
Как в публичной, так и административной частях, язык в первую очередь используется в работе с языковыми файлами.
В административной части язык определяет формат времени, даты, кодировку страниц (в публичной - данные параметры определяются настройками сайта).
Языковой файлФайл, хранящий переводы языковых фраз на тот или иной язык. Данный скрипт состоит из массива $MESS, ключи которого - идентификаторы языковых фраз, а значения - переводы на соответствующий язык. Для каждого языка существует свой набор языковых файлов, хранящихся как правило в каталогах /lang/.
Языковые файлы, как правило, используются в административных скриптах модулей и в компонентах.
Для работы с языковыми файлами предназначен модуль Перевод.
Многоязычный интерфейсВозможность реализуется за счет использования языковых файлов, хранящих перевод фраз на соответствующие языки для:
  • административного раздела;
  • сообщений об ошибках;
  • визуальных компонентов;
  • соответствующих областей в шаблоне портала;
  • и т.д.
В соответствии с текущим языком интерфейса выполняется подключение необходимых языковых файлов. В результате сообщения отображаются пользователю на выбранном им языке. Переключение между языками административного интерфейса осуществляется на административной панели.
ЛокализацияПодразумевает представление информации в переводе на соответствующих языке, кодировке и форматов представления данных (дата, время, денежные единицы, числа и т.д.).
Мета-тэгМета-тэг это - элемент HTML, задающий информацию о странице: кодировку страницы, ключевые слова, автора, краткое описание. Как правило, содержимое мета-тэгов используется в служебных целях, например, роботами поисковых систем, индексирующих сайт. Мета-тэг задается внутри тэга <head>.
Доменное имя (домен)Одно из полей DNS таблицы (domain name service), содержащее в себе строго структурированное имя интернет сайта, заданное по определённым правилам. Основная задача DNS таблицы - это ассоциация доменных имен с IP адресами сайтов. Пример доменного имени: 1c-bitrix.ru
IP адресЭто "имя" компьютера в сети, заданное по правилам протоколов TCP/IP. IP адрес состоит из четырех октетов, часть из которых идентифицирует подсеть, в которой находится компьютер, а часть - непосредственно этот компьютер в рамках соответствующей подсети. Пример IP адреса: 198.63.210.79.
ХостВ применении к функциям главного модуля, хост - это доменное имя или IP адрес для обращения к тому или иному сайту.
СессияПод термином понимается сессия PHP. Сессия может открываться в момент захода на сайт и закрывается при закрытии окна браузера. Также новая сессия открывается при авторизации пользователя, если закончить сеанс авторизации (разлогиниться) - сессия закрывается. Синонимом термина сессия можно считать один "заход на сайт".
Время в Unix-форматеКоличество секунд, прошедшее с 1 января 1970 года, с точностью до микросекунды. На сегодняшний день, время в Unix-формате может фиксироваться только до 2038 года.
Права в Unix системахВ Unix-подобных операционных системах поддерживаются три вида прав - чтение, запись и выполнение, которые присваиваются каждому файлу или директории. Права эти повторяются три раза: для владельца файла, для группы пользователей, и для всех остальных пользователей. Как правило, права указываются в числовом формате:
  • 4 - чтение;
  • 2 - запись;
  • 1 - выполнение.

Сумма этих чисел дает окончательный набор прав, например, 6 - это чтение и запись, но без выполнения, 7 - все права, 5 - чтение и выполнение.

Таким образом, например, право 764 будет означать: 7 - все права для владельца файла, 6 - чтение и запись для группы пользователей, к которой принадлежит владелец файла и 4 - чтение для всех остальных пользователей.

В PHP все права задаются в виде восьмеричных чисел, поэтому их надо задавать с обязательным указанием префикса - 0. Пример: 0755.

Пролог

В общем случае, под данным термином понимается верхняя левая часть страницы.

Для публичной части, пролог соответствующего шаблона сайта хранится в файле /bitrix/templates/ID шаблона сайта/header.php.

Для административной части - пролог хранится в файле /bitrix/modules/main/interface/prolog_main_admin.php.

В свою очередь пролог может быть разделен на служебную и визуальную части. В служебной части подключаются все необходимые классы, создаётся соединения с базой, создаются ряд служебных экземпляров объектов, таких как $USER, $APPLICATION и т.д. В визуальной части выводится верхняя левая часть страницы.

Если в публичной части необходимо подключить неразделенный пролог, то используем следующий код:

require($_SERVER["DOCUMENT_ROOT"]."/bitrix/header.php");

Если по тем или иным причинам необходимо разделить пролог на служебную (prolog_before.php) и визуальную (prolog_after.php) части, то используем следующий код:

require($_SERVER["DOCUMENT_ROOT"].
"/bitrix/modules/main/include/prolog_before.php");
...
require($_SERVER["DOCUMENT_ROOT"].
"/bitrix/modules/main/include/prolog_after.php");
Эпилог

В общем случае под данным термином понимается нижняя правая часть страницы.

Для публичной части, эпилог соответствующего шаблона сайта хранится в файле /bitrix/templates/ID шаблона сайта/footer.php.

Для административной части - эпилог хранится в файле /bitrix/modules/main/interface/epilog_main_admin.php.

В свою очередь эпилог может быть разделен на служебную и визуальную части. В служебной части производится ряд таких действий, как: отсылка почтовых сообщений, отработка обработчиков события OnAfterEpilog и др. В визуальной части выводится нижняя правая часть страницы.

Если в публичной части необходимо подключить неразделенный эпилог, то используем следующий код:

require($_SERVER["DOCUMENT_ROOT"]."/bitrix/footer.php");

Если по тем или иным причинам необходимо разделить эпилог на визуальную (epilog_before.php) и служебную (epilog_after.php) части, то используем следующие коды:

require($_SERVER["DOCUMENT_ROOT"].
"/bitrix/modules/main/include/epilog_before.php");
...
require($_SERVER["DOCUMENT_ROOT"].
"/bitrix/modules/main/include/epilog_after.php");

Примечание: Обратите внимание, что в случае пролога файл *_before.php это служебная часть, а в случае эпилога *_before.php это визуальная часть. Такое отличие возникает потому, что пролог перед телом страницы, а эпилог - после.

Тело страницыТело страницы это часть PHP/HTML кода, расположенного в скрипте между подключениями пролога и эпилога. Тело страницы не является частью шаблона сайта и представляет из себя индивидуальное содержимое публичной либо административной страницы.
Шаблон страницыЭто файл, хранящийся в одном из каталогов:
  • /bitrix/templates/ID шаблона сайта/page_templates/
  • /bitrix/templates/.default/page_templates/
Данный файл представляет из себя заготовку публичной страницы. Как правило, используется при создании новой страницы в модуле Управление структурой. Для задания порядка сортировки шаблонов страниц используется файл .content.php находящийся в одном из вышеуказанных каталогов.
СабмитОтправка данных HTML формы на сервер.
HTML-безопасный видКак правило данный термин применяют к тексту, в котором произведены следующие замены:
ИсходноеРезультат
<&lt;
>&gt;
"&quot;
&&amp;

Подобные замены позволяют выводить текст внутри HTML кода, не опасаясь, что он будет интерпретирован браузером как часть этого HTML кода.
Связывание переменныхВ применении к SQL-запросам для Oracle версии, под данным термином подразумевается связывание имен переменных (либо полей таблицы) с их значениями. Как правило, подобная технология используется для полей типа BLOB, CLOB, LONG и т.п. предназначенных для хранения больших объемов данных.
ДампВ применении к базе данных это - выгрузка содержимого и, возможно, структуры таблиц в файл в определённом формате, для их возможной дальнейшей загрузки обратно в данную, либо любую другую БД. Для каждой базы данных существуют свои утилиты, позволяющие сделать дамп, например для MySQL утилита mysqldump позволяет выгрузить в формате обычных SQL запросов, для Oracle утилита exp позволяет выгрузить в своем внутреннем формате.
В применении к переменным, дамп подразумевает отображение структуры и содержимого переменной в текстовом виде.
cron (крон)В Unix-подобных операционных системах утилита cron позволяет организовать запуск скриптов по четко указанному расписанию.
БуферизацияРежим, при котором весь исходящий поток данных из PHP скрипта (например, HTML код) запоминается предварительно в памяти и не отдается браузеру пользователя. Буферизацию в PHP можно включить с помощью функции ob_start. В дальнейшем её можно отключить, например с использованием функции ob_end_flush, при этом все накопленные данные будут отосланы браузеру. Режим позволяет произвольно манипулировать исходящим потоком данных, на этом принципе основана технология отложенных функций.
Постоянное соединение (persistent)При создании соединения с базой, в памяти создаётся дескриптор данного соединения. Если соединение обычное, то после отработки скрипта этот дескриптор удаляется, если соединение постоянное, он остается и может быть использован другими процессами при необходимости. Достоинством постоянного соединения является то, что времени на его работу требуется меньше. Недостаток - количество открытых постоянных соединений ограничивается в настройках базы данных и при превышении этого лимита посетитель не сможет зайти на сайт пока не освободятся новые соединения.
Accept-LanguageНабор языков, установленных в браузере посетителя сайта. К примеру, для MS Internet Explorer их можно выставить в меню Сервис > Свойства обозревателя > Общие > Языки. Для Mozilla Firefox: Меню > Настройки > Содержимое > Языки.
КастомизацияИзменение логики работы компонента или шаблона компонента под частные задачи.
API (SDK)Каждый модуль системы содержит набор высокоуровневых функций для выборки данных в публичном разделе сайта и набор классов с низкоуровневыми методами для более специализированной работы с данными модуля. Подробная информация по API каждого модуля представлена в документации для разработчиков.
Экземпляр программыКопия какого-либо продукта «1C-Битрикс», включающая в себя исходный текст продукта и только одну копию структуры и таблиц базы данных, входящих в состав продукта, а также любую документацию по использованию продукта.
ПорталОдин набор файлов, хранящихся в каталоге /bitrix/modules/, и одна копия базы. Портал включает в себя один или более сайтов. Синонимом данного термина может служить: «экземпляр продукта», «одна инсталляция системы», «одна копия системы».

Золотые правила работы с Bitrix Framework

Цитатник веб-разработчиков.

Степан Овчинников: Неописуемы безобразия, которые можно увидеть в коде человека, знающего мало, а делающего много.

Перед тем как начать работать в Bitrix Framework, необходимо понять основные правила, следование которым поможет избежать многих и многих ошибок:

  • Что важно помнить при работе над сайтом:
    1. Нельзя править на "боевом" сайте. Необходимо вести разработку на копии сайта с использованием режима [ds]Установка для разработки[/ds][di]Начиная с версии 16.5.7 и старше, в продуктах «1С-Битрикс» можно пометить новую или существующую установку продукта специальным маркером Установка для разработки. Маркер позволяет проводить тестирование, не устанавливая продукт локально.

      Подробнее ...[/di]
    2. Если в силу каких-то причин [dw]приходится править[/dw][di]При должном уровне квалификации и обоснованных доводах почему именно так нужно.[/di] на живом сайте, то нужно всегда иметь доступ к FTP или SSH, так как административная часть из-за ошибок в коде может оказаться недоступной.
    3. Всегда должен быть наготове свежий бекап.
    4. Для случая, когда доработки значительны, когда работает команда из нескольких человек крайне рекомендуется использовать [ds]систему контроля версий[/ds][di]Организовать сопровождение проекта с помощью системы контроля версий не сложно, если ограничиваться файлами. Для этого можно использовать, например, Mercurial - кроссплатформенную распределённую систему управления версиями, разработанную для эффективной работы с очень большими репозиториями кода.

      Подробнее ...[/di].
  • Если вы хотите внести какие-то изменения в работе сайта, то:
    • Сначала формализуйте свои требования на листе бумаги, а не кидайтесь править код.
    • После этого заново просмотрите все случаи использования на сайте модифицируемого вами блока. Убедитесь, что всё логично. Очень часто делают небольшие, казалось бы, изменения, а потом оказывается, что страдает связанный функционал, о котором не подумали или забыли.
    • После того, как вы формализовали потребности в изменениях, посмотрите, какие сущности эти требования затрагивают.
    • Только после этого подумайте, какие средства использовать для достижения своих целей.
  • Способы внесения изменений и желательный порядок их применения:
    • сначала попытайтесь сделать это редактированием шаблона самого сайта и файлов CSS;
    • если предыдущее невозможно, то попытайтесь сделать это средствами редактирования страницы сайта;

      Примечание: К этому пункту нужно подходить крайне осторожно. Бездумное добавление кода может привести к тому, что компоненты будут обрамляться кодом, который должен быть в шаблоне компонента, а не на странице. Размещение php-кода на странице грозит проблемами. Если контент-менеджер откроет страницу через визуальный редактор, то легко можно "все сломать":
      • визуальный редактор на этапе разбора и визуализации содержимого страницы может допустить ошибки,
      • контент-менеджер может случайно внести правки "несовместимые с жизнью" в php-код и даже не поймет этого.

    • при невозможности реализации задачи с помощью первых вариантов переходите к редактированию шаблонов компонента и файлов CSS компонента, либо изменяйте вывод данных с помощью файлов result_modifier.php и component_epilog.php
    • используйте обработчики событий, которые позволяют решать очень широкий спектр задач.
    • кастомизация компонента или разработка собственного компонента или модуля - последний из возможных вариантов получения нештатного функционала.
  • Не рекомендуется писать код HTML в код PHP для изменения представления данных. В компонентах 2.0 разделены логика и представление. Логика - это сам компонент, представление - это шаблон вывода компонента. Шаблон существенно проще, чем компонент в целом. Нет необходимости изменять логику компонента для изменения особенностей показа его данных. Для одной логики может быть несколько представлений, в том числе зависящих от шаблона текущего сайта. Представление (шаблон вывода) может быть написано на любом шаблонном языке, который можно подключить из PHP. Например, шаблоны могут быть на PHP, Smarty, XSL и т.д. Использование стороннего шаблонизатора должно быть четко обосновано, его использование должно давать какие-то преимущества перед штатными средствами.
  • Собственные компоненты и шаблоны - в собственном пространстве имен. Кастомизируя штатные компоненты и шаблоны, разрабатывая собственные, размещайте их в собственном пространстве имен. При обновлении системы все внесенные изменения в пространстве bitrix затираются.
  • При работе с компонентами не надо обращаться к базе напрямую. Концепция работы с продуктом предполагает работу с данными через функции API. Структура данных может меняться от версии к версии, а функции сохраняют обратную совместимость. Мы настоятельно не рекомендуем использовать прямые запросы к БД, т.к. это может нарушить целостность данных и привести к неработоспособности сайта. В силу вышесказанного структура таблиц не афишируется.
  • [dw]Запрещается[/dw][di]Напрямую это не запрещено Лицензионным соглашением. Но при изменениях в ядре вы одновременно можете полностью нарушить работу системы и потерять право на техподдержку. То есть изменения станут необратимыми.[/di] править код ядра в силу нескольких причин:
    • при обновлении системы внесенные изменения затрутся;
    • при изменении ядра владелец лицензии теряет право на техническую поддержку;
    • при изменении ядра разработчиком сайта возможна некорректная работа системы, так как ядро - сложная система, требующая учета работы всех модулей.
    Ядро продукта - файлы, находящиеся в директории /bitrix/modules/ а так же файлы системных компонентов: /bitrix/components/bitrix/.

    Модификация ядра - это мера, которая возможна только в крайних случаях. Допустимо только разработчиками уровня Senior при полном понимании:
    - зачем это нужно и
    - последствий в виде потери возможности штатного обновления через систему обновлений и отказа вендора от технической поддержки такого сайта.

  • Если вы сопровождаете живой сайт и заказчик просит провести работы по доделке-расширению функционала, то сначала проверьте, а пользуются ли вообще этим функционалом. Проверить можно так: Если блок мёртвый, то лучше его вообще убрать или [dw]переформулировать ТЗ[/dw][di]В большинстве случаев это задача не разработки, а проектного отдела, проект менеджеров, руководства, в крайнем случае тим лида, если он контактирует с клиентами.
    Кроме того, зачастую заказчик хочет доделать функционал, вне зависимости от того, пользуются или нет им, он платит за это деньги. А экспертиза обычного разработчика может быть вовсе не достаточна для такой оценки. Возможно клиент планирует дальнейшие шаги, рекламу и т. п.[/di].

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

<?if(!defined("B_PROLOG_INCLUDED") || B_PROLOG_INCLUDED!==true)die();?>

К таким файлам относится все, что работает внутри продукта, например: шаблоны сайтов, шаблоны компонента, файлы .parameters.php и .description.php.


Junior, Как создать простой сайт

Самый первый и самый простой по сложности пример создания сайта на «1С-Битрикс: Управление сайтом» Освоив эту главу вы сможете назвать себя разработчиком начального уровня.

Чтобы уметь создавать сайты на начальном уровне необходимо освоить систему на уровне работы штатным функционалом. Практически без кодирования. Кодирование нужно только при создании шаблона и это кодирование - знание HTML.

К концу изучения главы вы должны уметь:

  1. Устанавливать систему
  2. Создавать простейший шаблон сайта и применять его к сайту в целом, отдельным разделам или страницам.
  3. Интегрировать компоненты в шаблон.
  4. Создавать, настраивать и применять информационные блоки.
  5. Управлять метаданными и заголовками.
  6. Управлять штатными инструментами кеширования.
  7. Создавать страницы и разделы.
  8. Задавать права доступа к страницам и разделам.

Типовой порядок действий

Цитатник веб-разработчиков.

Sergey Leshchenko: Сайты разрабатываются не для того, чтобы их было удобно потом поддерживать веб-разработчикам.

Примерный алгоритм действий по созданию простого сайта следующий:

  • Получение верстки и ознакомление с техническим заданием на сайт;
  • Определение необходимого числа шаблонов и их структуры;
  • [ds]Установка дистрибутива[/ds][di]Описание предварительных шагов для установки продукта, шагов мастера установки, а также выбора и первоначальной настройки решений для быстрого развертывания своего проекта.

    Подробнее ...[/di] «1C-Битрикс»;
  • Создание сущности «сайт» в Административном разделе;
  • Создание шаблонов и применение их;
  • Создание и настройка необходимых элементов шаблона для SEO-продвижения;
  • Создание структуры сайта;
  • Создание и настройка инфоблоков;
  • Создание шаблонов визуальных компонентов;
  • Тестирование.

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


Сайт в понятии Bitrix Framework

Сайт - это совокупность:

  • Учетной записи в базе данных, которая создаётся в административном меню на странице Список сайтов (Настройки > Настройки продукта > Сайты > Список сайтов) и включает в себя следующие основные параметры:
    • идентификатор - набор символов, идентифицирующий сайт;
    • доменное имя - одно или более доменных имён;
    • папка сайта - путь к каталогу, в котором будет храниться публичная часть сайта;
    • язык сайта;
    • Региональные настройки;
    • URL - протокол и доменное имя по умолчанию (например, http://www.site.ru)
    • DocumentRoot (Путь к корневой папке веб-сервера для этого сайта) - параметр заполняется при использовании многосайтовости системы.
    • условия подключения шаблонов: каждый сайт может иметь один или более шаблонов для отображения страниц публичной части, каждый такой шаблон может быть подключен по тому или иному условию.
  • Публичной части - совокупности скриптов (страниц) лежащих в папке сайта и принадлежащих этому сайту.
  • Настроек системы. Некоторые модули имеют ряд настроек связанных с сайтом, например, у модуля Информационные блоки эти настройки представляют из себя привязку информационного блока к тому или иному сайту, у модуля Техподдержка - привязку статуса, категории и т.п..

В Bitrix Framework имеется возможность на базе одного экземпляра продукта создавать и поддерживать неограниченное количество сайтов. Особенностями системы многосайтовости являются единые:

  • права на управление модулями;
  • набор аккаунтов пользователей;
  • система ведения статистики.

Детально эта функция системы описана в курсе Многосайтовость

Структура

Структура сайта в рамках Bitrix Framework:

  • Шаблон - определяет представление сайта пользователям. Существуют шаблоны компонентов и шаблоны сайта.
  • Компоненты - задают вывод данных.
  • Страница - элемент структуры сайта.

В этой главе будут описаны Страница и Шаблон сайта, как элементы структуры. Компоненты описаны в отдельной главе.

Что такое страница

Структура

Страница представляет из себя PHP файл, состоящий из пролога, тела страницы (основной рабочей области) и эпилога:
  • header
  • workarea
  • footer

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

В общем случае все страницы сайта имеют следующую структуру:

Нажмите на рисунок, чтобы увеличить

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

2 Основная рабочая область - work area. Рабочая область страницы, в которой размещаются собственно информационные материалы сайта. В качестве Основной рабочей области может подключаться как физический файл, так и создаваемый системой на основе комплексных компонентов, динамический код.

Если в качестве Основной рабочей области подключается физический файл, то такая страница называется статической. Если подключается динамический код, то такая страница называется динамической.

3 Нижняя - footer. Включает в себя, как правило, статическую информацию (контактная информация, сведения об авторе и владельце сайта и так далее), нижнее горизонтальное меню и правое меню (если они есть в дизайне). Может включать в себя информационные материалы.

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

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

Шаблоны и свойства

Шаблоны

Шаблон страницы - это PHP файл, содержимое которого соответствует правилам формирования структуры страницы. Шаблоны могут использоваться при создании новой страницы.

Шаблоны хранятся в каталогах:

  • /bitrix/templates/.default/page_templates/;
  • /bitrix/templates/ID_шаблона_сайта/page_templates/.

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


Свойства

Свойства раздела хранятся в файле .section.php соответствующего каталога (раздела сайта). Свойства страницы задаются, как правило, либо в теле страницы, либо между служебной частью и визуальной частью пролога.

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


Язык и языковые файлы

Язык - это учётная запись в базе данных, доступная для редактирования в административном меню на странице Настройки > Настройки продукта > Языковые параметры > Языки интерфейса, со следующими основными полями:
  • Идентификатор,
  • Название,
  • Региональные настройки.

Как в публичной, так и в административной частях, язык используется в первую очередь для выбора того или иного языкового файла.

В административной части язык определяет формат времени и даты, кодировку страниц. В публичной части - данные параметры определяются настройками сайта.

Цитатник веб-разработчиков.

Антон Долганин: В компонентах фразы выношу в ланг-файлы, просто потому что это системная часть и там хотелось бы видеть порядок.

Языковые файлы

Языковой файл - PHP скрипт, хранящий переводы языковых фраз на тот или иной язык.

Данный скрипт состоит из массива $MESS, ключи которого - идентификаторы языковых фраз, а значения - переводы на соответствующий язык.

Пример языкового файла для русского языка

Пример языкового файла для английского языка

Для каждого языка существует свой набор языковых файлов, хранящихся в подкаталогах /lang/ структуры файлов системы или модуля. Языковые файлы как правило используются в административных скриптах модулей или в компонентах.


Выбор кодировки сайта

Цитатник веб-разработчиков.

Зайцев Артемий: Если есть возможность делать в UTF, надо делать в UTF.

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

UTF-8 (от англ. Unicode Transformation Format) — в настоящее время распространённая кодировка, реализующая представление Юникода, совместимое с 8-битным кодированием текста.

и

Windows-1251 (или cp1251) — набор символов и кодировка, являющаяся стандартной 8-битной кодировкой для всех русских версий Microsoft Windows.

UTF-8 более перспективна. Но у любой вещи есть недостатки. И решение об использовании какой-то кодировки только потому, что она перспективна, без учета многих других факторов, не представляется правильным. Выбор будет оптимальным только тогда, когда он полностью учитывает все нюансы конкретного проекта. Другое дело, что предусмотреть все нюансы - само по себе весьма не просто.

Мы считаем, что использование UTF-8 предпочтительнее, но решать что выбрать - это дело разработчика проекта. А для облегчения этого выбора используйте сравнительную таблицу особенностей обеих кодировок.

Свойство UTF-8 Windows 1251
Общего характера
Многоязычность Кодировка позволяет использовать разные языки как в публичной, так и в административной части сайта. Смена кодировки действующего крупного сайта с Windows-1251 на UTF-8 может вызвать серьёзные дополнительные трудовые и финансовые издержки.
Большое число символов. Возможность использования спецсимволов. Есть. Но надо учитывать возможности браузеров. Штатно нет. Есть возможность замены спецсимволов на "костыли", например, © на &cорy; или × (знак умножения) на &timеs;. Однако это повышает требования к уровню подготовки контент-менеджера и создаёт проблемы при переносе данных из другой базы данных. Кроме того, в Bitrix Framework есть поля, которые не используют визуальный редактор, например, название страницы или название элемента инфоблока. Это также усложняет поддержку проекта силами низкоквалифицированных сотрудников.
Минимизация объема проекта. Проект на UTF-8 будет заведомо "тяжелее", в силу того что строки в этой кодировке занимают в два раза больше места, чем строки в однобайтной Windows-1251. Размер сайта и базы данных будет в 1,2 - 1,5 раз больше.
Поддержка большинством js-фреймворков Поддерживается без проблем. Сложности в реализации.
Импорт из 1С Сайты на UTF-8 работают без проблем при интеграции через SOAP с такими системами как, например, 1С.
Вебвизор Яндекс.Метрики Вебвизор корректно записывает действия посетителей. Возможны ошибки в записи.
Связанные с Bitrix Framework
Возможность сделать сайты в разной кодировке по системе многосайтовости. Невозможно. Все сайты на одном ядре должны быть в одной кодировке.
Поддержка на различных хостингах Работает на любых хостингах. С версии 20.100.0 Главного модуля (main) требуется удаление настройки PHP mbstring.func_overload. Эта опция более не требуется и не поддерживается платформой.
до версии 20.100.0
Работает на любых хостингах.
Размещение продуктов на виртуальной машине BitrixVM. По умолчанию. Требует дополнительных действий по настройке.
Разные мелочи
Взаимодействие с WordPress (блог-клиенты, trackback и ping'и) Есть Нет
Поддержка большинством редакторов Требуется редактор, который поддерживает кодировку UTF-8 без BOM. Нет проблем.

Список ссылок по теме:

Техническое задание на сайт

Цитатник веб-разработчиков.

Антипов Андрей: Как правило, плохо составленное ТЗ приводит к расходам со стороны исполнителя, разочарованию заказчика и, в худшем случае, бесконечным доработкам (на основе логики «так это же очевидно»).

Техническое задание - исходный документ на проектирование технического объекта. ТЗ устанавливает основное назначение разрабатываемого объекта, его технические и тактико-технические характеристики, показатели качества и технико-экономические требования, предписание по выполнению необходимых стадий создания документации (конструкторской, технологической, программной и т. д.) и её состав, а также специальные требования.

Составление технического задания - обязательный шаг для разработки качественного, удовлетворяющего пользователя сайта. Обычно клиенты обращаются за разработкой сайта, не имея ни постановки задачи, ни, тем более, технического задания. А лишь имея некое формальное описание того, чего бы им хотелось получить в итоге. Такое формальное описание называется "бриф".

Пример брифа.

Необходимо разработать сайт автосервиса с Интернет-магазином автозапчастей, с формами заказа запасных частей, заявки на ремонт или ТО, отзыва о сайте, с фото-галереей.

Бриф может быть таким как в примере, а может быть гораздо обширнее и походить на постановку задачи.

Чтобы начать разработку проекта, необходимо подготовить всю необходимую документацию:

  • Постановка задачи, составляется после анализа брифа от заказчика;
  • Техническое задание, составляется после того, как с постановкой задачи разобрались;
  • ER-диаграмма - инфологическая или даталогическая (т.к. в 1С-Битрикс для хранения информации используются инфоблоки, то эти модели как бы совмещены) модель. На этой диаграмме отображаются все ваши сущности (инфоблоки) и связи между ними.
  • Прототип будущего сайта. Для больших проектов рисуется подробный прототип в специализированных программах, например, Axure.

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

Как описать архитектуру будущего сайта: текст? схемы?

Иван Неслуховский: На самом первом этапе делаю майнд-карту всего сайта, на которой также размещаю сущности и основные атрибуты. Это даёт возможность, уже глядя на что-то, обсуждать с заказчиком ТЗ, заказчик видит, что вы вникаете в проблему. Плюс - скорость. Я пользуюсь бесплатной программой XMind.

Далее уже составляются схемы данных. Я предпочитаю делать модель предметной области и модель данных в нотации UML. Для этого существует множество средств. Бумага тоже подходит для черновика. Тип данных обозначаю в соответствии с типами данных свойств инфоблока, например:

NAME: S [1]
CODE: S [1]
PREVIEW_TEXT: S* - звёздочка у меня означает, что текст многострочный
DURATION_FROM: D
DURATION_TO: D
PRODUCT: E ----------------------------------------------------------------> здесь может выходить связь к другому блоку

Для небольших проектов достаточно "начертить" прототипы в графическом редакторе или на бумаге.

После готовности такого пакета документации можно смело приступать к реализации проекта:

  • разработка дизайна;
  • интеграция дизайна в систему 1С-Битрикс;
  • создание структуры сайта;
  • разработка функционала;
  • тестирование;
  • внедрение и сопровождение.

Список ссылок по теме:

Базовый шаблон

Цитатник веб-разработчиков.

Иван Левый: Самая лучшая верстка - своя верстка. Сколько ни заказывали у внешних подрядчиков дизайн с версткой, все равно приходилось после них править.

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

Когда дизайн готов, обычно применяется один из двух технологических процессов по интеграции дизайна в систему управления: либо разработчик сам верстает (т.е. переводит из графического эскиза в HTML) макет сайта, либо ему предоставляется уже готовая верстка, и он ее интегрирует в сайт. Вопросы создания верстки не входят в программу обучения Bitrix Framework, поэтому речь пойдет о готовом, сверстанном шаблоне.

  • Определение количества необходимых шаблонов
  • Редактирование шаблона
  • Структурное деление шаблона
  • Служебные директивы
  • Картинки и файлы стилей
  • Определение количества необходимых шаблонов

    Перед началом работы необходимо определить, сколько различных шаблонов сайта понадобится. Обычно при разработке сайта прорисовываются все различные страницы или основные элементы сайта.

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

    Редактирование шаблона

    Перейти к редактированию можно любым из способов:

    • Создав (открыв для редактирования) нужный шаблон (Настройки > Настройки продукта > Сайты > Шаблоны Сайтов);
    • Выбрав Шаблон в меню Пуск (Настройки > Настройки продукта > Сайты > Шаблоны Сайтов);
    • С помощью кнопки Шаблон сайта на Панели управления (Шаблон сайта > В Панели управления > Редактировать шаблон);
    • Прямое редактирование файлов header.php и footer.php в папке шаблона.

    Структурное деление шаблона

    Проанализируйте прототип дизайна и определите, какая часть кода должна относиться к Прологу (файл header.php), какая к Эпилогу (файл footer.php), а какая часть - к Рабочей области страницы. Выбранные части кода должны быть размещены в соответствующих файлах, а рабочая область должна быть отмечена тегом #WORK_AREA# в шаблоне сайта.

    Нажмите на рисунок, чтобы увеличить

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

    Служебные директивы

    Необходимо заменить некоторые части верстки на служебные директивы Bitrix Framework для создания шаблона:

    • Заменить подключение стилей и, возможно, javascript файлов на директиву <?$APPLICATION->ShowHead()?>
    • Заменить прописанный явно заголовок страницы на <title><?$APPLICATION->ShowTitle()?></title>
    • Сразу после тэга <body> добавить <?$APPLICATION->ShowPanel();?>. Если этого не сделать, Панель управления не появится.
    • Перед всеми картинками добавить путь к ним <? SITE_TEMPLATE_PATH?>/images/
    • Заменить контент на специальный разделитель - #WORK_AREA#

    Картинки и файлы стилей

    Все изображения, относящиеся к шаблону размещаются в папке /bitrix/templates/ID шаблона сайта/images/.

    Описания стилей из представленной верстки переносятся в файл: /bitrix/templates/ID шаблона сайта/styles.css.

    Описания стилей шаблона переносятся в файл /bitrix/templates/ID шаблона сайта/template_styles.css.

    Список ссылок по теме:

    Что такое Шаблон сайта

    Файлы и композиция

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

    В общем случае шаблон сайта задает «обрамление» страницы, а за вывод динамической информации отвечают Визуальные компоненты.

    Шаблон сайта определяет:

    • оформление сайта (дизайн, верстку страниц, набор основных каскадных стилей);
    • типы меню и их расположение;
    • наличие рекламных областей (областей для размещения баннеров);
    • наличие включаемых областей в шаблоне и страницах сайта;
    • наличие в дизайне сайта формы авторизации, оформления подписки и т.д.

    Файлы шаблона

    В шаблон сайта входят:

    • каталог /components – предназначен для шаблонов компонентов;
    • каталог /images – предназначен для картинок шаблона (которые не зависят от просматриваемой страницы), копируется из верстки сайта;
    • каталог /include_areas – содержит включаемые области шаблона;
    • каталог /lang – содержит файлы языковых сообщений;
    • каталог /page_templates – для шаблонов страниц и редактируемых областей;
    • каталог /snippets – содержит сниппеты – маленькие фрагменты html-кода для ускорения работы контент-менеджера по созданию часто встречающихся блоков кода;
    • каталог /themes – тема оформления шаблона;
    • файл header.php – часть шаблона ДО контента;
    • файл footer.php – часть шаблона ПОСЛЕ контента;
    • файл description.php – название и описание шаблона;
    • файл .styles.php – описания стилей для визуального редактора страниц;
    • файл template_styles.css – стили шаблона (стили применяемые в самом шаблоне дизайна сайта);
    • файл styles.css – стили для контента и включаемых областей. Эти стили можно применять в визуальном редакторе.

    Композиция шаблона

    Композицию шаблона сайта строят из трех основных частей:

    Header - верхняя часть дизайна, заголовок. Включает в себя, как правило, верхнюю и левую часть дизайна со статической информацией (логотипом, слоганом и так далее), верхним горизонтальным меню и левым меню (если они есть в дизайне). Может включать в себя информационные динамические материалы. Хранится в отдельном файле .../<идентификатор_шаблона>/header.php.

    Work area - рабочая область страницы, в которой размещаются собственно информационные материалы сайта. Рабочая область - это все создаваемые пользователями документы, хранящиеся в файлах <имя_документа>.php в соответствующих папках сайта. В шаблоне сайта рабочая область помечается разделителем #WORK_AREA#, который используется для указания границы между верхней и нижней частью дизайна. В этом месте будет выполняться подключение рабочей области страницы сайта. Сохранение шаблона без этого разделителя невозможно.

    Footer - нижняя часть дизайна со статической информацией (как правило: контактная информация, сведения об авторе и владельце сайта и так далее), нижним горизонтальным меню и правым меню (если они есть в дизайне). Может включать в себя информационные материалы. Хранится в отдельном файле .../<идентификатор_шаблона>/footer.php.

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

    Несколько типовых примеров композиций

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

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

    Помимо статической информации, в шаблоне и в рабочей области могут располагаться:

    • Визуальные компоненты
    • Включаемые области
    • Произвольный PHP-код

    Эти элементы сайта предназначены для вывода динамической информации.

    Хранение и подключение

    Где хранятся шаблоны

    Все используемые в системе шаблоны хранятся в отдельных папках каталога /bitrix/templates/ (например, /bitrix/templates/demo/ или /bitrix/templates/template1/), либо, начиная с версии 14.0.0, в /local/templates/. Также существует специальная папка .default, которая не является полноценным шаблоном сайта, а содержит шаблоны компонентов и файлы, общие для остальных шаблонов сайта.

    Для собственных, не штатных, шаблонов рекомендуется использовать папку /local/templates/.

    Внимание! Шаблон Битрикс24, используемый в дистрибутивах "1С-Битрикс: Корпоративный портал" является системным, то есть некастомизируемым шаблоном. Шаблон расположен в папке /bitrix/templates/bitrix24/.

    Подключение частей дизайна

    Сборка типовых страниц сайта выполняется путем подключения верхней и нижней частей дизайна для каждой страницы сайта. В общем случае структура страницы сайта выглядит так:

    <?
    require($_SERVER["DOCUMENT_ROOT"]."/bitrix/header.php");
    $APPLICATION->SetTitle("1C-Битрикс: Управление сайтом");
    ?>

    Тело документа. Содержательная часть.

    <?
    require($_SERVER["DOCUMENT_ROOT"]."/bitrix/footer.php");
    ?>

      Сколько может быть шаблонов

    Количество используемых на сайте шаблонов дизайна не ограничено. Для каждого шаблона определяется условие, при котором данный шаблон будет применяться к страницам сайта:

    Настройка условий применения того или иного шаблона определяется отдельно для каждого сайта (в форме создания и редактирования сайта: Настройки > Настройки продукта > Сайты > Список сайтов):

    Стили шаблона сайта подключаются последними - это крайняя возможность переопределить, например, стили стандартных компонентов. Иначе потребуется масштабная кастомизация шаблонов компонентов. Если есть необходимость подключить свои стили последними, то это можно сделать с помощью функции CMain::AddHeadString.

    Список ссылок по теме:


    Создание и управление шаблоном

    Управление шаблонами дизайна осуществляется в административном разделе на странице Шаблоны сайта (Настройки > Настройки продукта > Сайты > Шаблоны сайтов), где можно:

    Выбор шаблона

    Создание шаблона

    Шаблон дизайна сайта может быть создан непосредственно в системе с помощью формы Новый шаблон, для перехода к которой служит кнопка Добавить шаблон, расположенная на контекстной панели.

    При создании нового шаблона через интерфейс задается:

    • его идентификатор;
    • название;
    • описание для показа в списке;
    • порядок следования в общем списке;
    • тип;
    • код внешнего вида сайта;
    • таблицы стилей:

      • Закладка Стили сайта служит для описания таблиц каскадных стилей (CSS), используемых на страницах сайта. Описание стилей хранится в файле styles.css в папке шаблона сайта.
      • Закладка Стили шаблона служит для описания таблиц каскадных стилей (CSS), используемых в шаблоне. Описание стилей хранится в файле template_styles.css в папке шаблона сайта.
    • набор используемых включаемых компонентов и картинок.

    При сохранении шаблона автоматически создается поддиректория /bitrix/templates/<идентификатор_шаблона>.

    Все используемые графические элементы рекомендуется размещать в директории /bitrix/templates/<идентификатор_шаблона>/images/.

    Примечание: На время создания шаблона рекомендуется отключить кеширование.

    Для наглядного представления шаблона в списке может использоваться его скриншот. Скриншот размещается в папке соответствующего шаблона в файле с именем screen.gif (например, /bitrix/templates/books/screen.gif).

    Редактирование шаблона

    Чтобы просмотреть или поменять структуру и программный код шаблона, перейдите в режим редактирования, выбрав в меню действий пункт Изменить в списке шаблонов, либо используйте пункт [dw]Редактировать шаблон[/dw][di]Пункт меню "Редактировать шаблон"[/di] меню кнопки Шаблон сайта на административной панели в Публичном разделе.

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

    Внимание! В шаблоне дизайна сайта категорически не рекомендуется использовать комплексные компоненты.

    Экспорт/Импорт шаблона

    С помощью интерфейса системы используемый на сайте шаблон может быть выгружен в файл формата <имя_шаблона>.tar.gz. Для выгрузки шаблона служит пункт контекстного меню Скачать.

    Готовый шаблон можно импортировать в виде комплекта файлов с помощью менеджера файлов либо с помощью специального интерфейса системы. На странице со списком шаблонов имеется специальная кнопка контекстной панели Загрузить шаблон.

    При нажатии на кнопку открывается форма:

    • С помощью кнопки Обзор... укажите файл с шаблоном для загрузки.

      Примечание: Файлы шаблона должны быть в кодировке UTF-8.

    • При загрузке по умолчанию шаблон будет распакован и помещен в папку с именем, соответствующим имени загружаемого файла (/bitrix/templates/<идентификатор_шаблона>/). Например, если имя загружаемого файла template1.tar.gz, то шаблон будет автоматически помещен в папку .../template1/, а самому шаблону будет присвоен идентификатор (ID) template1.

    • Чтобы использовать другой идентификатор, а сам шаблон был размещён в папке с соответствующим именем, в поле Код шаблона указывается нужный код.
    • Также можно привязать загруженный шаблон как шаблон по умолчанию для выбранного сайта с помощью соответствующей опции.

    Разработка шаблона дизайна

    Процесс создания шаблона сайта включает два основных этапа:

    Разработка прототипа

    Цитатник веб-разработчиков.

    Роман Петров: Периодически возникает вопрос: продайте мне Битрикс с шаблоном... Ведь для Joomla шаблоны есть, а для Битрикс? Открываю небольшую тайну: для Битрикс все немного по другому.

    Справедливости ради следует отметить, что разработка простого шаблона для 1С-Битрикс на основе имеющейся верстки - достаточно несложное занятие. Сложность заключается в том, что мало кому требуется сделать простой шаблон. Везде нужно реализовать бизнес-логику заказчика. Ведь Битрикс - решение для бизнеса, а не для "поиграться".

    Прототип представляет собой сверстанный в html шаблон дизайн сайта. При верстке в шаблоне выделяются функциональные области, например:

    • заголовок страницы;
    • меню;
    • цепочка навигации;
    • форма авторизации;
    • форма поиска;
    • включаемые области и файлы;
    • рекламные области;
    • и т.д.

    Создание полнофункционального шаблона

    На втором этапе выполняется замена HTML элементов дизайна на соответствующие функциональные элементы: программный код и вызовы компонентов. В результате чего уже получается PHP-шаблон дизайна сайта.

    Примечание: При создании шаблона сайта возможно использование различных программных условий, влияющих на отображение тех или иных элементов шаблона для различных разделов сайта. В этом случае для раздела сайта нужно определить [ds]некоторое свойство[/ds][di]Управление свойствами страницы в Административном разделе, независимо от выбранного редактора, осуществляется в форме редактирования страницы...

    Подробнее ...[/di], значение которого будет проверяться в шаблоне сайта:
    <?if ($APPLICATION->GetProperty(“SECT_PROP”)=="Y"):?>
    

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

    Рекомендации по созданию шаблона

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

    • При подготовке графического дизайна следует заранее разметить линию раздела дизайна на пролог (header.php) и эпилог (footer.php).
    • Следует выделить основные элементы дизайна, для последующей модификации таблицы стилей: шрифты, цвета заливки и т.п.
    • Разрабатывая дизайн меню различных уровней, желательно выделять повторяющиеся элементы - для упрощения создания шаблона меню и дальнейшего управления этими меню.
    • Для облегчения сопровождения различных языковых версий сайта по возможности следует использовать вместо графических элементов текстовые.
    • При нарезке графического дизайна и подготовке HTML шаблона, необходимо заранее предусмотреть место расположения основных компонентов системы управления сайтом. Выделить области меню, рекламные области, области размещения дополнительных форм. Рекомендуется верстать компоненты независимо от основного шаблона. Чтобы в будущем, при перемещении компонента в другое место или другой шаблон, не поехала верстка.
    • Для удобства отладки верстки, рекомендуется отключить [ds]сжатие и объединение css и js файлов[/ds][di] Одним из способов повышения производительности является
      использование штатной функции объединения и сжатия
      css и js файлов, которое включается в настройках
      главного модуля:



      Подробнее ...[/di].
    • Размещать графические изображения, относящиеся к шаблону сайта, следует в папке /bitrix/templates/<имя_шаблона>/images.
    • Каскадные стили, используемые в шаблоне, рекомендуется разделять на две таблицы стилей, хранящиеся в двух разных файлах. Оба файла находятся в директории /bitrix/templates/<идентификатор_шаблона>/. Файл styles.css содержит стили для представления информационного содержания страницы на сайте. Файл template_styles.css содержит стили для отображения текстов в самом шаблоне дизайна.

      [dw]В крайнем случае[/dw][di]По "фен шую" это неправильно, так как ресурсы нужно подключать через API, чтобы для них работало объединение в один файл, подключение минифицированных версий, кэширование и т.д[/di] допускается подключение в <head> любое количество стилевых файлов, дополнительно к styles.css и template_styles.css, подключаемым через showhead(). Делается это обычными линками перед закрытием тэга </head>, а дополнительные стилевые файлы положите в любую папку. Эффект будет тот же самый, как если бы вы собрали все ваши дополнительные стили и дописали их в два файла шаблона сайта со стандартными наименованиями.

    Создание шаблона сайта рекомендуется выполнять на локальной демо-версии продукта. Готовый шаблон необходимо экспортировать средствами системы в виде комплекта файлов формата tar.gz на удаленный сервер и развернуть его.

    Примечание: Крайне не рекомендуется использование комплексных компонентов в шаблоне дизайна. Так как в этом случае правила переписывания адресов начинают работать для всего сайта. Это может отражаться на работе [ds]ЧПУ[/ds][di]Правила обработки адресов настраиваются отдельно для каждого сайта и каждое правило должно содержать уникальное в рамках сайта условие выполнения.

    Подробнее ...[/di] других компонентов и страницы /404.php. Комплексные компоненты должны находиться в #WORKAREA.

    Как правило, для сайта разрабатывается несколько шаблонов: Для главной, для раздела, для детальной и так далее. Если различия между шаблонами незначительны, то лучше использовать один шаблон:

    • либо с внедрённой в него проверкой того, какая страница сейчас открыта,
    • либо с использованием включаемых областей.

    Практика показывает, что это решение намного дешевле в сопровождении сайта.


    Опыт веб-разработчиков.

    Роман Петров: Мы, например, интегрируем готовую верстку в Битрикс и поэтому в большинстве случаев у нас стили задаются для всего сайта сразу, стили компонентов мы чаще всего удаляем, а сами шаблоны компонентов складываем в .default потому что это сильно экономит время при наличии нескольких шаблонов сайта.
    Мы складываем все шаблоны в .default потому, что если на сайте несколько шаблонов дизайна, очень тяжело вносить правки в каждый из них. Гораздо проще поменять в одном месте.

    Sergey Leshchenko: Значит, я храню в:
    .default - все шаблоны компонентов, все стили всех шаблонов сайтов, картинки/спрайты, js и прочее с преследованием цели минимизации обращений к веб-серверу, но без фанатизма.
    site_template_name/ - styles.css со стилями для визуального редактора, шаблоны общих компонентов специфичные только под данным шаблоном сайта.

    Евгений Малков: Все стили храню в папке шаблона сайта в одном файле styles.css. Картинки, используемые в css, в /images/ в корне сайта. Когда точно знаю, что будет 1 сайт то храню шаблоны компонент в .default, когда несколько сайтов - в шаблоне сайта. Иногда, при другом шаблоне для ряда страниц (например для главной), в styles.css этого шаблона подключаю стиль основного шаблона. От верстальщика приходит 1 файл стилей на все шаблоны и делить его, особенно для компонент, сложно.

    Включаемые области

    О работе с включаемыми областями смотрите курс Контент-менеджер

    Управление

    Включаемая область - это специально выделенная часть страницы сайта, которую можно редактировать отдельно от основного содержания страницы.

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

    Содержимое включаемых областей хранится в отдельных PHP или HTML файлах. Такие файлы для страниц или разделов сохраняются с некоторым суффиксом. Например, в поставляемых файлах продукта в качестве обозначения включаемой области для страницы используется суффикс _inc (например, index_inc.php), а включаемая область для раздела сайта сохраняется в файле с именем sect и добавлением к нему суффикса (например, sect_inc.php).

    Важно! Файл с включаемой областью должен быть сохранен в той же директории, что и страница, для которой он был создан. Включаемая область для раздела - в папке этого раздела.

    Подключение областей в шаблоне дизайна сайта выполняется с помощью компонента Вставка включаемой области.

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

    Один компонент может быть настроен только на один из вышеописанных вариантов отображения. Кроме того, они могут отображаться на страницах сайта в соответствии с любыми другими условиями. Например, только на главной странице сайта или только для авторизованных посетителей, и т.д.

    Примечание: Тип включаемой области определяется опцией Показывать включаемую область.

    Если компонент [comp include_main_include]Включаемая область[/comp] расположить в шаблоне дизайна сайта, то информация [dw]из файла[/dw][di]Установка параметра доступна только пользователю с правами на редактирование php файлов: edit_php.[/di] будет выводиться на всем сайте.

    Размещение

    Для размещения включаемой области выполните следующее:

    • Откройте для редактирования шаблон сайта или страницу в визуальном редакторе.
    • Добавьте компонент Вставка включаемой области (bitrix:main.include) в шаблон сайта (или в тело страницы) и настройте его параметры.

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

    Использование включаемых областей позволяет управлять не только текстом. Можно поместить в эту область картинку вместо текста (или компонент Случайное фото) и получить индивидуальный вид каждого раздела. При этом индивидуальность будет «динамическая», изменяемая.

    Создание и редактирование

    Создание включаемых областей может быть выполнено:

    • из административного раздела в Менеджере файлов (Контент > Структура сайта > Файлы и папки), создав файл с соответствующим именем;
    • из публичного раздела сайта в режиме правки. В тех местах, где предполагается вывод включаемых областей, будут показаны иконки для быстрого перехода к созданию этих областей.

      Примечание: Файл включаемой области будет создан и назван в соответствии указанным в настройках компонента суффиксом - для опции для раздела, или именем файла - для опции из файла.

      После выбора команды Добавить область будет запущен визуальный редактор для создания содержимого включаемой области. При выборе команды Добавить область как PHP станет возможным добавление области в режиме РНР кода

    Аналогично перейти к редактированию включаемых областей можно:

    • непосредственно [dw]из публичного раздела[/dw][di][/di] сайта в режиме правки;
    • либо из административного раздела, открыв для редактирования соответствующий файл в Менеджере файлов.


    Пример. Использование Включаемых областей

    Задача: Сайт разделен на несколько разделов. По замыслу у каждого из них должна быть своя «шапка» в дизайне. Более в дизайне ничего не меняется. Как лучше реализовать смену «шапок» разделов?

    Решение: В шаблон подключается компонент "Включаемая область (для раздела)":

    <div id="header">
    <?$APPLICATION->IncludeComponent("bitrix:main.include", ".default", array(
    	"AREA_FILE_SHOW" => "sect",
    	"AREA_FILE_SUFFIX" => "headerinc",
    	"AREA_FILE_RECURSIVE" => "Y",
    	"EDIT_TEMPLATE" => "sect_headerinc.php"
    	),
    	false
    );?>
    </div>

    Код шапки каждого из разделов будет храниться в файле sect_headerinc.php. Параметр "AREA_FILE_RECURSIVE" => "Y" означает, что такая же "шапка" появится у всех подразделов данного раздела, если родительский sect_headerinc.php не будет специально перекрыт у кого-то из нижележащих разделов.

    Цепочка навигации

    Цепочка навигации - последовательный список ссылок на разделы и страницы сайта, который показывает уровень "погружения" текущей страницы в структуру сайта.

    Значения, подставляемые в навигационную цепочку, могут быть заданы как для каждого раздела, так и для отдельного документа.

    Цепочка навигации помогает посетителю легко ориентироваться на сайте: быстро вернуться на главную страницу или подняться на один (или более) уровень вверх, т.е. перейти из подраздела в раздел.

    Описание построения простейшей цепочки навигации без кастомизации приведено в курсе [ds]Контент-менеджер[/ds][di]Сайты зачастую обладают сложной структурой и несколькими уровнями вложенности. Не потеряться на таком сайте поможет цепочка навигации.

    Подробнее ...[/di].

    В этом разделе приводится описание структуры шаблона и данных, на основе которых выполняется построение и отображение цепочки навигации, а также приемы управления её показом на страницах сайта.

    Управление навигационной цепочкой

    Управление пунктами

    По умолчанию в системе используется механизм управления названиями пунктов навигационной цепочки через свойства разделов с помощью поля Заголовок в форме настройки свойств раздела.

    Переход к форме настройки свойств раздела можно осуществить из:

    • [ds]публичного раздела[/ds][di]Сайты зачастую обладают сложной структурой и несколькими уровнями вложенности. Не потеряться на таком сайте поможет цепочка навигации .

      Подробнее ...[/di]
    • [ds]административного раздела[/ds][di]Управление свойствами страницы в Административном разделе, независимо от выбранного редактора, осуществляется в форме редактирования страницы в таблице

      Подробнее ...[/di].

    Чтобы изменить название ссылки на раздел в цепочке навигации, нужно отредактировать Заголовок папки. Название ссылки будет изменено сразу же после сохранения новых свойств раздела.

    Примечание: Чтобы ссылка на какой-либо раздел не выводилась в навигационной цепочке сайта, нужно удалить название раздела из поля Заголовок и сохранить внесенные изменения.

    Отдельные компоненты могут также добавлять в навигационную цепочку заголовок текущей страницы сайта или, например, заголовок текущей новости или товара каталога. Так, например, комплексный компонент Новости (bitrix:news) последовательно добавляет в навигационную цепочку названия каталогов и групп новостей по мере погружения вглубь по уровням, если это установлено в его настройках.

    Аналогичным образом в навигационную цепочку добавляются названия форумов и тем форумов. В этом случае значение пункта навигационной цепочки для данной страницы определяется непосредственно в сущности.


    Управление показом пунктов

    Показ навигационной цепочки может быть отключен на определенных страницах или в определенном разделе сайта. Управление отображением навигационной цепочки также осуществляется с помощью свойств страницы (раздела). Для этого необходимо:

    • На странице настроек модуля Управление структурой, секция Настройки для сайтов, создать свойство для страниц Не показывать навигационную цепочку с кодом not_show_nav_chain.

    • если навигационная цепочка не должна отображаться на определенной странице или страницах какого-либо раздела, то для этой страницы или раздела нужно установить значение данного свойства равным Y.

    Управление шаблоном

    Цепочка навигации подключается в шаблоне дизайна сайта с помощью компонента [comp include_breadcrumb]Навигационная цепочка[/comp]. Для него может быть создано любое количество шаблонов, т.е. внешних видов. Все они хранятся в папке компонента /bitrix/components/bitrix/breadcrumb/templates/<название шаблона>/. Все созданные шаблоны будут отображаться в настройках компонента. Таким образом для каждого шаблона сайта может быть установлен свой шаблон оформления компонента цепочки навигации. Структура шаблона показа навигационной цепочки аналогична структуре шаблона показа меню.

    Управление навигационной цепочкой и ее шаблоном оформления осуществляется также, как при работе с другими компонентами 2.0. В режиме правки сайта с помощью кнопки управления вы можете быстро перейти к форме изменения параметров компонента либо скопировать шаблон компонента, а затем отредактировать его.


    Примеры работы с навигационной цепочкой

  • Как добавить свой пункт в цепочку навигации?
  • На главной не отображается цепочка навигации
  • Повторение пунктов в цепочке навигации
  • Как сделать, чтобы в навигационной цепочке присутствовал только физический раздел?
  • Как добавить свой пункт в цепочку навигации?

    С помощью функции AddChainItem() :

    <?
    $APPLICATION->AddChainItem("Форум &quot;Отзывы&quot;", "/ru/forum/list.php?FID=3");
    ?>
    

    На главной не отображается цепочка навигации

    Цитата: В шаблоне сайта в цепочке навигации выводится название только инфоблоков. Заголовки страниц (допустим контакты) не выводит, заголовок "главная" тоже не отображается. Подскажите что не так?!

    Для главной страницы необходимо в свойствах страницы в поле NOT_SHOW_NAV_CHAIN установить значение N.

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

    Главная страница может также не отображаться по причине неправильно установленной опции Номер пункта, начиная с которого будет построена навигационная цепочка в параметрах компонента Навигационная цепочка: 0 (значение по умолчанию) означает, что построение навигационной цепочки начнется от корня сайта. В случае если заполнено поле Путь, для которого будет построена навигационная цепочка, то номер пункта считается в указанном пути.


    Повторение пунктов в цепочке навигации

    Цитата: Не подскажете, как так получается, что в каталоге в цепочке навигации повторяется дважды название каталога:

    Главная / Магазин / Видеонаблюдение / Видеонаблюдение

    Первая ссылка на каталог выглядит так: /catalog/video/

    а вторая: /catalog/video/section.php?SECTION_ID=0

    Первое название берется из свойств директории video (файл .section.php), а второе - компонентом, расположенном на странице (в данном случае адрес страницы section.php).

    Примечание: Например, у компонента Новости (bitrix:news) в его параметрах присутствуют соответствующие опции: Включать инфоблок в цепочку навигации и Включать раздел в цепочку навигации.

    Повторение элементов в цепочке навигации также может быть вызвано наличием нескольких компонентов на странице, которые настроены на добавление своих пунктов в цепочку.


    Как сделать, чтобы в навигационной цепочке присутствовал только физический раздел?

    Используется комплексный компонент "Новости" для вывода раздела из инфоблока. В навигационной цепочке помимо физического раздела, появляется некликабельное название раздела самого инфоблока. В настройках ни раздел, ни инфоблок не включен в НЦ.

    Решение: Добавьте в шаблон строку:

    "ADD_SECTIONS_CHAIN" => $arParams["ADD_SECTIONS_CHAIN"],

    Меню сайта

    Меню - это элемент дизайна, являющийся основным средством навигации по сайту.

    Общие понятия о [ds]работе с меню[/ds][di]Разработка эффективного, то есть заметного, красивого, понятного и логичного меню - важнейшая задача контент-менеджера.

    Подробнее ...[/di] даются в курсе Контент-менеджер. На основе меню строится карта сайта. Подробный пример создания карты сайта приведен в курсе [ds]Контент-менеджер[/ds][di]Карта сайта - это отдельная страница, на которой содержатся ссылки на все остальные разделы и страницы в иерархическом порядке. С неё возможен переход в любую точку сайта за минимальное число переходов.

    Подробнее ...[/di].

    Типы меню

    Тип меню – принцип организации меню. По умолчанию в дистрибутиве используется два типа меню: Верхнее (как главное) и Левое (для разделов).

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

    В самом общем случае на сайте существует одно «основное» меню (1), соответствующее самому верхнему уровню иерархии и отображаемое во всех разделах сайта. Также в системе часто используется «второстепенное» (или второго уровня) меню (2 и 3), включающее ссылки на подразделы и документы текущего раздела.

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

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

    Меню разделов, как правило, создается для каждого раздела свое и транслируется на все страницы раздела. При необходимости в подразделах можно создать свое собственное меню и применить к нему собственный тип.

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

    Например, пусть в системе используются два типа меню:

    • левое меню – тип left;
    • верхнее (основное) меню – тип top.

    Тип меню, заданный в настройках модуля Управления структурой, будет использован как префикс файла меню, а также для идентификации файлов с пунктами меню (например, .top.menu.php).

    Кроме того, имя типа меню используется для подключения меню в шаблоне дизайна.

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

    Типы задаются произвольно (только символами латинского алфавита). Однако для упрощения управления меню рекомендуется давать типам значимые имена. Например, top, left, bottom.

    Карта сайта

    Построение и показ меню

    В общем случае задача формирования меню включает:

    • выделение HTML элементов для построения меню;
    • создание шаблона меню (создание шаблона компонента Меню);
    • включение функции показа меню (вызов компонента Меню) в общем шаблоне ("прологе" и "эпилоге");
    • заполнение меню в соответствии со структурой сайта.

    Структура меню

    Любое меню на сайте строится на основе двух составляющих:

    • массива данных $aMenuLinks, определяющего состав меню, задает названия и ссылки для всех пунктов меню. Управление массивом данных осуществляется через административный интерфейс;
    • шаблона внешнего представления меню. Шаблон меню – это PHP код, определяющий внешний вид меню (шаблон компонента Меню). Шаблон меню обрабатывает массив данных, выдавая на выходе HTML-код.

    Массив данных меню

    Данные для каждого типа меню хранятся в отдельном файле, имя которого имеет следующий формат: .<тип меню>.menu.php. Например, для хранения данных меню типа left будет использоваться файл .left.menu.php, а для хранения данных меню типа top - файл .top.menu.php.

    Меню является иерархически наследуемым. Файлы меню размещаются в папках тех разделов сайта, где требуется показ соответствующих типов меню. Если для данного раздела не создан соответствующий файл меню, система производит поиск файла в каталоге уровнем выше.

    Например, т.к. основное меню (в демо-версии продукта, это меню типа top) должно выводиться во всех разделах, то файл данного меню помещается только в корневой каталог сайта.

    Соответственно меню второго уровня (в демо-версии продукта, это меню left) выводится отдельно для каждого раздела сайта. Поэтому в папке каждого раздела создается свой файл для данного типа меню.

    Ещё пример: посетитель находится в разделе /ru/company/about/. Для показа меню типа left файл меню ищется системой в следующей последовательности:

    1. /ru/company/about/.left.menu.php
    2. /ru/company/.left.menu.php
    3. /ru/.left.menu.php
    4. /.left.menu.php

    Если в одном из каталогов найдено меню, то поиск останавливается и в вышележаших каталогах уже не ищется.

    Система Bitrix Framework позволяет также создавать [dw]меню динамического типа[/dw][di]Для этого требуется в компоненте Меню включить опцию Подключать файлы с именами вида .тип_меню.menu_ext.php ("USE_EXT" => "Y"), которая по умолчанию выключена.[/di]. Т.е. массив данных таких меню генерируется автоматически на основании некоторых данных, получаемых с помощью программного кода. Данный код должен храниться в папке соответствующего раздела сайта в файле с именем .<тип меню>.menu_ext.php.

    Основная задача подобных файлов - это манипуляция массивом $aMenuLinks. Данные файлы не редактируются визуально в модуле Управление структурой, поэтому они не смогут быть случайно отредактированы при визуальном редактировании меню. При создании этого файла используйте компонент Пункты меню (bitrix:menu.sections).

    Примечание: В абзаце выше речь идёт только о дополнении созданного меню названиями разделов инфоблоков. Например, для дополнения меню названиями форумов этот вариант не годится.
    Внимание! Если в качестве пунктов меню используются разделы каталога без ЧПУ, необходимо указывать переменные в значимых переменных запроса.

    Примером такого меню может служить левое меню раздела Каталог книг, представленное в старой демо-версии продукта. Здесь первые два пункта меню Авторы и Рецензии созданы обычным способом, а остальные (Бизнес-литература, Детская литература и т.д.) формируются динамически.

    Динамическое меню

    В данном случае в качестве пунктов меню используются названия групп каталога Книги, созданного на основе информационных блоков. Программный код, на основе которого генерируется меню, хранится в файле .left.menu_ext.php в папке /e-store/books/.


    В файлах .<тип меню>.menu.php могут использоваться следующие стандартные переменные:

    • $sMenuTemplate - абсолютный путь к шаблону меню (данная переменная используется крайне редко);
    • $aMenuLinks - массив, каждый элемент которого описывает очередной пункт меню.

      Структура данного массива:

      Array
      (
      	[0] => пункт меню 1
      		Array
      			(
      				[0] => заголовок пункта
      				[1] => ссылка на пункте
      				[2] => массив дополнительных ссылок для подсветки пункта:
      					Array
      						(
      							[0] => ссылка 1
      							[1] => ссылка 2
      							...
      						)
      				[3] => массив дополнительных переменных передаваемых в шаблон меню:
      					Array
      						(
      							[имя переменной 1] => значение переменной 1
      							[имя переменной 2] => значение переменной 2
      							...
      						)
      				[4] => условие, при котором пункт появляется 
      					это PHP выражение, которое должно вернуть "true"
      			)
      	[1] => пункт 2
      	[2] => пункт 3
      	...
      )

    Примеры файлов меню

    <?
    // пример файла .left.menu.php
    
    $aMenuLinks = Array(
    	Array(
    		"Каталог курсов", 
    		"index.php",
    		Array(), 
    		Array(), 
    		"" 
    	),
    
    	Array(
    		"Мои курсы",
    		"mycourses.php",
    		Array(), 
    		Array(), 
    		"\$GLOBALS['USER']->IsAuthorized()" 
    	),
    
    	Array(
    		"Журнал обучения",
    		"gradebook.php",
    		Array(), 
    		Array(), 
    		"\$GLOBALS['USER']->IsAuthorized()"  
    	),
    
    	Array(
    		"Анкета специалиста",
    		"profile.php",
    		Array(), 
    		Array(), 
    		"\$GLOBALS['USER']->IsAuthorized()"  
    	),
    );
    ?>
    <?
    // пример файла .left.menu_ext.php
    
    if(!defined("B_PROLOG_INCLUDED") || B_PROLOG_INCLUDED!==true)die();
    
    global $APPLICATION;
    
    $aMenuLinksExt = $APPLICATION->IncludeComponent(
    	"bitrix:menu.sections",
    	"",
    	Array(
    		"ID" => $_REQUEST["ELEMENT_ID"], 
    		"IBLOCK_TYPE" => "books", 
    		"IBLOCK_ID" => "30", 
    		"SECTION_URL" => "/e-store/books/index.php?SECTION_ID=#ID#", 
    		"CACHE_TIME" => "3600" 
    	)
    );
    
    $aMenuLinks = array_merge($aMenuLinks, $aMenuLinksExt);
    ?>

    Организация показа меню

    Показ меню на страницах сайта выполняется с помощью компонента Меню (bitrix:menu). Например, вызов верхнего меню на демо-сайте имеет следующий вид:

    <?$APPLICATION->IncludeComponent(
    	"bitrix:menu",
    	"horizontal_multilevel",
    	Array(
    		"ROOT_MENU_TYPE" => "top",
    		"MAX_LEVEL" => "3",
    		"CHILD_MENU_TYPE" => "left",
    		"USE_EXT" => "Y"
    	)
    );?>

    Данный код помещается в предусмотренные для вывода меню области шаблона сайта.


    Построение меню сайта

    Построение меню для показа происходит следующим образом:

    • в общий шаблон показа включается вызов вывода меню на экран;
    • при загрузке компонент проверяет наличие в текущем разделе сайта файла, содержащего массив значений для меню;
    • затем компонент вызывает шаблон построения для данного типа меню и выводит HTML меню на экран.

    Шаблоны меню

    Вывод данных в компоненте Меню реализован с помощью шаблонов. Шаблоны могут создаваться пользователями самостоятельно.

    Системные шаблоны

    В дистрибутиве по умолчанию включены [dw]несколько шаблонов[/dw][di][/di]. Описания некоторых из них:

    • Default (Вертикальное) – шаблон для вертикального меню. Самый простой шаблон. При выборе в параметрах компонента глубины вложения более 1 вы увидите список страниц сайта в общей иерархии. То есть индексная страница сайта (раздела) будет размещена на одном уровне с вложенной страницей (разделом). Это затрудняет осмысление структуры сайта посетителем. Поэтому шаблон рекомендуется для простых видов меню и главных меню верхнего уровня. Шаблон достаточно прост для кастомизации под конкретный дизайн.

    • Tree (Древовидное) – шаблон для вертикального меню. Реализует меню в виде древовидной структуры аналогично Проводнику Windows. Вложенные страницы показаны в виде страниц в папке (разделе), что существенно облегчает понимание пользователем структуры сайта. Это меню не всегда удобно при разветвленной структуре интернет-проекта, так как существенно растягивает по вертикали колонку, где оно расположено.

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

    • Grey_tabs (Серое меню в виде закладок) и Blue_tabs (Голубое меню в виде закладок) – шаблоны для горизонтального меню. Отличаются только внешним видом. Это самые простые шаблоны, аналогичные шаблону по умолчанию (default) для вертикального меню.

    • Horizontal_multilevel (Горизонтальное многоуровневое выпадающее) – шаблон для горизонтального меню. Аналогично Vertical_multilevel (Вертикальному многоуровневому выпадающему меню) и реализует меню с выпадающими пунктами нижнего уровня.

    Создание шаблонов меню

    Выделение HTML элементов для построения меню

    Создание шаблонов меню начинается с выделения необходимых HTML областей в шаблоне сайта:

    • неизменной верхней и нижней части шаблона;
    • повторяющихся элементов. Например, для горизонтального меню – это ячейки таблицы, а для вертикального – строки.

    Создание шаблона меню

    Все шаблоны меню имеют одинаковую структуру:

    • область пролога шаблона;
    • область с описанием замен для различных условий обработки шаблона;
    • область тела шаблона;
    • область эпилога шаблона.

    В php шаблоне для вывода меню используется массив $arItem - копия массива пунктов меню, в котором каждый пункт в свою очередь представляет собой массив, использующий следующие параметры:

    • TEXT - заголовок пункта;
    • LINK - ссылка на пункте;
    • SELECTED - активен ли пункт меню в данный момент, возможны следующие значения:
      • true - пункт выбран;
      • false - пункт не выбран;
    • PERMISSION - право доступа на страницу, указанную в LINK для текущего пользователя. Возможны следующие значения:
      • D - доступ запрещён;
      • R - чтение (право просмотра содержимого файла);
      • U - документооборот (право на редактирование файла в режиме документооборота);
      • W - запись (право на прямое редактирование);
      • X - полный доступ (право на прямое редактирование файла и право на изменение прав доступа на данный файл);
    • ITEM_TYPE - флаг, указывающий на тип ссылки, указанной в LINK, возможны следующие значения:
      • D - каталог (LINK заканчивается на "/");
      • P - страница;
      • U - страница с параметрами;
    • ITEM_INDEX - порядковый номер пункта;
    • PARAMS - ассоциативный массив параметров пунктов. Параметры задаются в расширенном режиме редактирования меню.

    Рассмотрим построение шаблона меню на примере Левого меню, представленного в демо-версии продукта (шаблон .default компонента Меню):

    <?if (!defined("B_PROLOG_INCLUDED") || B_PROLOG_INCLUDED!==true)die();?>
       <?if (!empty($arResult)):?>
    	<ul class="left-menu"> 
    	<?foreach($arResult as $arItem):?>
    		<?if($arItem["SELECTED"]):?>
    			<li><a href="<?=$arItem["LINK"]?>" class="selected">
    			<?=$arItem["TEXT"]?></a></li>
    		<?else:?>
    			<li><a href="<?=$arItem["LINK"]?>">
    			<?=$arItem["TEXT"]?></a></li>
    		<?endif?>
    	<?endforeach?>
    	</ul>
    <?endif?>

    Повторяющаяся часть меню, выделенная на предыдущем шаге, выносится в тело шаблона.

    При создании шаблона меню потребуется также создать дополнительные стили в таблице стилей (CSS). Например, для текстового меню: цвет пункта меню и цвет текущего (активного) пункта меню.

    Отдельного представления в шаблоне могут потребовать заголовки разделов (например, название текущего раздела при просмотре подразделов). Также можно предусмотреть использование графических или текстовых обозначений, например, того, что данный пункт ссылается на подразделы или документ текущего раздела и т.д.

    Примечание: Все шаблоны меню хранятся в папке компонента: /bitrix/components/bitrix/menu/templates/.


    Быстрый доступ к редактированию шаблона каждого типа меню можно осуществить в режиме Правки с помощью пункта Редактировать шаблон компонента меню команд кнопки по управлению компонентом.

    Примечание: Шаблон меню, если он является системным, перед изменением необходимо скопировать в текущий шаблон сайта.

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

    Управление меню

    Управление меню выполняется как с помощью средств административного, так и публичного раздела. Про работу с меню из публичного раздела читайте в курсе Контент-менеджер.

    Создание меню

    Перейти к созданию меню раздела из административного раздела можно с помощью команды Добавить меню кнопки Добавить, расположенной на контекстной панели Менеджера файлов:

    Меню будет создано для раздела, папка которого открыта в данный момент в Менеджере файлов.

    Примечание: В результате данных операций создается файл данных меню с именем .<тип_ меню>.menu.php. Однако в Менеджере файлов имя файла данных автоматически представляется в виде ссылки Меню типа "<тип_меню>".

    Редактирование меню

    Примечание: При редактировании меню выполняется изменение файла .<тип_ меню>.menu.php (например, .top.menu.php). Однако работа с данным файлом ведется через специальный интерфейс системы. Это позволяет исключить необходимость работы непосредственно с программным кодом и дает возможность редактировать пункты меню в визуальном режиме.

    Перейти к редактированию из административного раздела можно открыв на редактирование файл соответствующего меню в Менеджере файлов.

    В системе предусмотрено два режима редактирования меню (переключение между ними выполняется с помощью соответствующей кнопки, расположенной на контекстной панели страницы редактирования):

    • упрощенный режим редактирования;

      Режим позволяет определить тип меню и указать название пункта меню, ссылку для перехода и значение индекса сортировки.
    • расширенный режим редактирования.

      В этом режиме для управления доступны следующие данные:
      • тип редактируемого меню;
      • шаблон, на основе которого будет генерироваться меню (поле используется в случае, если создаваемое меню должно генерироваться на основе шаблона, отличного от используемого по умолчанию);
      • название пункта;
      • ссылка для перехода;
      • индекс сортировки;
      • набор дополнительных ссылок, которые соответствуют этому же пункту меню. В данном поле задается набор ссылок на страницы, при переходе на которые будет также подсвечиваться данный пункт меню. Например, чтобы при просмотре любой страницы раздела Каталог книг подсвечивался пункт меню Каталог книг, в данном поле нужно указать ссылку на папку, содержащую все страницы раздела (или перечислить необходимые страницы): /e-store/books/;
      • условия показа. Например, позволяет внести ограничения на показ данного пункта меню пользователям с определенными правами доступа;
      • дополнительные параметры – набор произвольных параметров, которые могут быть обработаны в шаблоне показа меню и представлены соответствующим образом. Например, если пункт меню является заголовком секции, это может быть указано в параметрах пункта так: название параметра - SEPARATOR, значение - Y. При разработке шаблона можно проверять значение этого параметра и при показе выделять данный пункт меню разделителем.

        Параметры хранятся в ассоциированном массиве $PARAMS в виде пар имя => значение. При построении меню по шаблону, в самом шаблоне может быть добавлена проверка параметра, например:

        if ($PARAMS["MY_PARAM"]=="Y")
        

        Примечание: При желании количество дополнительных параметров в форме можно увеличить с помощью соответствующей опции в настройках модуля Управление структурой, секция Настройки для сайтов.

    Примечание: Подробное описание всех полей формы можно посмотреть на странице пользовательской документации.

    В курсе Администратор. Базовый есть пример настройки расширенного меню.


    Примеры создания меню

    Описанные ниже примеры можно повторить в Виртуальной лаборатории, выбрав при установке шаблон "Демо-сайт для разработчиков".

    Предварительные операции

    Для примера мы создадим в структуре сайта новый раздел test_menu, который также добавим в главное меню.

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

    В параметрах компонента, для примера, в опции Тип меню для первого уровня укажем Главное меню.

    Примечание: По умолчанию этот компонент уже настроен для вывода Меню раздела, поэтому операция по созданию меню и его настройка будет пропущена.

    Древовидное меню

    Древовидное меню – самое распространенное. Оно достаточно простое и вместе с этим информативное. Создается на базе статических и динамических элементов: разделов, страниц и инфоблоков.

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

    Одноуровневое древовидное меню

    Задача: создать древовидное меню.

    • Создайте дополнительный тип меню: Подменю (podmenu).
    • В параметрах компонента установите следующие параметры:
      • Шаблон компонента - tree (Встроенный шаблон);
      • Тип меню для первого уровня - Главное меню;
      • Тип меню для остальных уровней - Подменю;
      • Уровень вложенности меню - 2.

    • Перейдите в раздел test_menu и создайте в нем 2 страницы: novaya_stranitsa_1 и novaya_stranitsa_2. При их создании необходимо отметить опцию Добавить пункт меню и выбрать тип Подменю.

    Результат. Результатом этой работы будет меню такого вида:

    Файл .podmenu.menu.php будет иметь следующую структуру:

    <?
    $aMenuLinks = Array(
    	Array(
    		"Новая страница 1", 
    		"/test_menu/novaya_stranitsa_1.php", 
    		Array(), 
    		Array(), 
    		"" 
    	),
    	Array(
    		"Новая страница 2", 
    		"/test_menu/novaya_stranitsa_2.php", 
    		Array(), 
    		Array(), 
    		"" 
    	)
    );
    ?> 
    

    Многоуровневое древовидное меню

    Шаблоны компонента Меню поддерживают создание многоуровневого меню с глубиной вложения до 4-х уровней. Покажем это на примере. Выполнять работу на данном этапе удобнее в административной части.

    Задача: создать четырехуровневое меню.

    Решение. Решение осуществим на примере уже созданного выше меню с шаблоном tree.

    • Перейдите в административный раздел на страницу Управление структурой (Структура сайта > Файлы и папки)
    • В созданном ранее разделе /test_menu с помощью команды Добавить папку кнопки Добавить контекстной панели создайте новый каталог со следующими параметрами:
      • Имя папки - test_1;
      • Название раздела - test_1;
      • Тип меню - Подменю;
      • Название пункта - test_1.

      Примечание: Для полноты картины можно создать в папках по дополнительной странице (кроме индексной). При создании их не забывайте добавить их в тип меню – Подменю.

    • Перейдите во вновь созданный раздел и создайте по описанному выше алгоритму папку /test_2.

      Примечание: Если в последней по вложенности папке не создать файл меню, то в публичной части сайта она будет отображаться как страница, а не папка.

    • При необходимости добавления новых пунктов или редактирования существующих перейдите в нужный раздел (например, /test_menu) и на контекстной панели используйте команду Добавить меню кнопки Добавить. В открывшейся форме выберите тип меню равный Подменю, после чего отредактируйте нужные пункты меню.

      Примечание: При удалении страниц и разделов из административной части потребуется последующее редактирование пунктов меню вручную.

    Результат: результатом работы будет созданное древовидное меню в четыре уровня:

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

    Если требуется уровень вложенности более, чем по умолчанию

    Выпадающее меню

    Чтобы сделать многоуровневое выпадающее меню нужно использовать шаблон многоуровнего выпадающего меню, вертикального или горизонтального, в зависимости от дизайна проекта. Настройка компонента и основные правила создания этого меню абсолютно аналогичны древовидному меню. Приведем их в обобщенном виде:

    • Необходимо иметь два типа меню. Одно (первичное) будет применено как основное меню в разделе, другое (вторичное) – как источник формирования собственно выпадающего меню.
    • В каждом разделе должно быть обязательно создано вторичное меню с указанием его пунктов.
    • При указании путей до разделов, которые должны быть развернуты в выпадающем меню, необходимо указывать путь до папки подраздела, а не до индексного файла подраздела.
    • Выпадающее меню может быть построено не только на базе статических разделов и страниц, но и на основе инфоблоков.
    • Система допускает только четыре уровня вложения.

    Меню из информационных блоков

    Построение меню из динамических элементов - информационных блоков - позволяет снять с контент-менеджера часть нагрузки по поддержке сайта. Не нужно будет выполнять работы по актуализации меню в связи с появившимися новыми разделами и страницами. Для решения этой задачи необходимо использовать компонент Пункты меню (bitrix:menu.sections).

    Перед созданием такого меню у вас должен быть создан необходимый тип инфоблока и сам инфоблок. Желательно создать пару разделов и элементов для наглядности.

    • Выполните команду Редактировать параметры компонента из меню компонента Меню.
    • В разделе Дополнительные настройки установите флажок в поле Подключать файлы с именами вида .тип_меню.menu_ext.php.
    • Сохраните внесенные изменения.
    • Перейдите в административную часть сайта.
    • Перейдите в раздел, для которого планируется создавать меню с помощью инфоблоков.
    • Создайте пустой файл под именем .podmenu.menu_ext.php в директории, в меню которой должны подключаться пункты инфоблока (например, /test_menu).
    • Примечание: Первая часть имени файла должна совпадать с названием меню, для которого применяется данный компонент. То есть для верхнего меню первая часть должна называться .top, для любого другого меню первая часть должна называться .luboe_drugoe.

    • Откройте файл для редактирования в визуальном редакторе.
    • Добавьте в тело файла компонент Пункты меню (bitrix:menu.sections).
    • Настройте параметры компонента:

    • Выберите тип информационного блока и сам информационный блок (например, Каталог товаров).
    • Установите глубину вложений любую, более 1.
    • Сохраните внесенные изменения. Форма создания файла закроется, файл появится в общем списке файлов папки.
    • Откройте его вновь для редактирования, но уже в режиме Редактировать как PHP.
    • Допишите в файле код проверки включения кода из ядра:
      <? 
      if(!defined("B_PROLOG_INCLUDED") || B_PROLOG_INCLUDED!==true)die();
      global $APPLICATION; 
      $aMenuLinksExt = $APPLICATION->IncludeComponent(…
          
    • После вызова компонента допишите код подключения к меню:
      $aMenuLinks = array_merge($aMenuLinks, $aMenuLinksExt);
    • Конечный код указанного файла должен быть примерно таким:

      <?
      if(!defined("B_PROLOG_INCLUDED") || B_PROLOG_INCLUDED!==true)die();
      global $APPLICATION; 
      $aMenuLinksExt = $APPLICATION->IncludeComponent(
      	"bitrix:menu.sections",
      	"",
      	Array(
      		"ID" => $_REQUEST["ID"], 
      		"IBLOCK_TYPE" => "books", 
      		"IBLOCK_ID" => "5", 
      		"SECTION_URL" => "/catalog/phone/section.php?", 
      		"DEPTH_LEVEL" => "1", 
      		"CACHE_TYPE" => "A", 
      		"CACHE_TIME" => "3600" 
      	)
      );
      $aMenuLinks = array_merge($aMenuLinks, $aMenuLinksExt);
      ?>
          
    • Сохраните внесенные изменения.

    Результат: Перейдите в публичную часть в раздел, для которого создавался файл .тип_меню.menu_ext.php. [dw]Вы увидите[/dw][di]Если вы ранее не отключали кеш, то, возможно, нужно сбросить кеш, чтобы меню отобразилось.[/di] созданное новое меню:

    Красным цветом выделены пункты, созданные в рамках главного меню (массив $aMenuLinks, возвращающий пункты главного меню), синим цветом – полученные из инфоблоков (массив $aMenuLinksExt).

    Два меню рядом

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

    • Перейдите к редактированию шаблона сайта.
    • В режиме редактирования кода в левой колонке сайта создайте таблицу с одной строкой и двумя ячейками.
    • Переместите в левую ячейку компонент меню, использовавшийся в качестве верхнего меню, а в правую – компонент меню, использовавшийся в качестве левого меню.
    • Сохраните внесенные изменения.
    • Перейдите в публичную часть сайта.
    • Выполните команду Редактировать параметры компонента из меню компонента меню, стоящего в левой ячейке.
    • Задайте в качестве шаблона шаблон default.
    • В поле Уровень вложенности меню выставьте значение 1.
    • Сохраните внесенные изменения.
    • Создайте пункты в меню типа top (которое в левой ячейке).
    • Выполните команду Редактировать параметры компонента из меню компонента меню, стоящего справа.
    • Настройте его так, как мы настраивали в примере построения меню из инфоблоков.
    • Выполните команду Редактировать пункты меню для компонента меню, стоящего в правой ячейке.
    • Сохраните внесенные изменения.

    После перезагрузки вы увидите простое меню по умолчанию. При переходе во внутренние разделы справа от основного меню будут раскрываться меню активного раздела в зависимости от его настроек (статичное или динамическое, из инфоблоков).

    Графическое меню

    Иногда встает задача создания меню с графическими элементами в качестве его пунктов. Эту задачу можно решить разными способами.

    Самый простой – за счет редактирования файла CSS. Этот способ имеет преимущество в том плане, что текст, используемый в компонентах меню для разных пунктов, будет редактироваться обычным способом. Изображение, выводимое css, будет фоновым. Так как данный способ не относится непосредственно к Bitrix Framework, то он рассмотрен не будет.

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

    Задача: создать меню, где пункты оформляются выводом картинок. Отображение картинок должно быть различным: для активного пункта меню и пассивного пункта меню. Если по каким-то причинам картинки меню не загружены на сайт (либо для пункта меню не заданы параметры), должна отображаться картинка по умолчанию.

    • Создайте по паре картинок на каждый из пунктов меню и картинку для пунктов без параметров или без картинки.
    • Создайте папку /images/menu в рамках структуры сайта.
    • Загрузите в эту папку созданные картинки.
    • Примечание: Для одного из пунктов меню картинки не загрузите, чтобы проверить работу кода.

    • Перейдите в административной части сайта в раздел с меню, для которого хотите назначить картинки.
    • Откройте меню для редактирования в Расширенном режиме.

    • В строке Параметры каждого пункта меню в колонке Название введите ACT.
    • В строке Параметры каждого пункта меню в колонке Значение укажите путь до картинки, соответствующей активному состоянию пункта меню, от корня сайта.
    • Примените внесенные изменения. После перезагрузки добавится еще одна строка Параметры.

      Примечание: Увеличить количество доступных сразу дополнительных параметров меню можно в настройках модуля Управление структурой с помощью соответствующей опции.

    • Заполните для каждого пункта меню поля Название (NOACT) и Значение (укажите путь до картинки, соответствующей неактивному состоянию пункта меню, от корня сайта).
    • Сохраните внесенные изменения.

    Мы задали параметры для системы, по которым она будет определять какую картинку ей выводить. Теперь переходим к редактированию шаблона. Для простоты будем использовать шаблон default.

    • Перейдите в публичную часть сайта.
    • Выполните команду Редактировать параметры компонента из меню компонента Меню.
    • Назначьте для компонента шаблон default.
    • Перейдите к редактированию шаблона компонента, скопируйте его и откройте для редактирования.
    • Вам нужно заменить код вызова пункта меню со штатного на обращение к параметрам, заданным для данного меню. Делается это в этом участке кода шаблона:

      <?if($arItem["SELECTED"]):?>
      	<li><a href="<?=$arItem["LINK"]?>" class="selected"><?=$arItem["TEXT"]?></a></li>
      	<?else:?>
      	<li><a href="<?=$arItem["LINK"]?>"><?=$arItem["TEXT"]?></a></li>
      <?endif?>
          
    • Замените первую ссылку в коде на строку:
      <a href="<?=$arItem["LINK"]?>">
      <img src="<?=(array_key_exists("ACT", $arItem["PARAMS"]) 
      && file_exists($_SERVER["DOCUMENT_ROOT"].$arItem["PARAMS"]["ACT"]) ? 
      $arItem["PARAMS"]["ACT"] : "/images/menu/default.png")?>" /></a> 
    • Замените вторую ссылку в коде на строку:
      <a href="<?=$arItem["LINK"]?>">
      <img src="<?=(array_key_exists("NOACT", $arItem["PARAMS"]) 
      && file_exists($_SERVER["DOCUMENT_ROOT"].$arItem["PARAMS"]["NOACT"]) ?
      $arItem["PARAMS"]["NOACT"] : "/images/menu/default.png")?>" /></a> 
    • Строки различаются между собой только выбором параметра из расширенных настроек: ACT или NOACT.

      Примечание: В коде использован путь /images/menu/default.png для указания картинки, выводимой, если отсутствует картинка для пункта меню или если для него не указаны параметры. Вам нужно использовать собственное имя этой картинки. И другой путь, если вы сохраняете картинки в другой папке.

    • Сохраните внесенные изменения.

    При переходе в публичную часть мы увидим такое меню:


    Примеры решения частных задач в меню

  • Подсветка ссылок
  • Как сделать, чтобы отдельный пункт меню открывался в новом окне?
  • Отображение пункта меню для определенных групп пользователей
  • Отображение пункта меню неавторизованным пользователям
  • Отображение пункта меню при нахождении в определенном разделе сайта
  • Отображение определенных пунктов только на главной странице и еще в одном внутреннем разделе
  • Осуществление вывода пункта меню, ссылающегося на определенный товар, связанный с товаром, открытым на данной странице
  • Всплывающие подсказки для пунктов меню
  • Как поставить картинки рядом с пунктами меню?
  • Разные картинки-пункты меню для разных языков
  • Отображение собственного, не такого как в шаблоне, меню для определенных разделов сайта
  • Как сделать меню tree в постоянно открытом состоянии?
  • Как сделать так, чтобы при открытии страницы через древовидное меню, само меню не сворачивалось, а показывало, на какой странице ты находишься?
  • Скрытие бокового меню по свойству страницы
  • Меню и кеш

  • Подсветка ссылок

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

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


    Как сделать, чтобы отдельный пункт меню открывался в новом окне?

    Для этого нужно в расширенном режиме редактирования прописать у нужных пунктов следующие дополнительные параметры:

    Название: target 
    Значение: target="_blank"
    

    В шаблоне меню, после вывода элемента меню, необходимо заменить код:

    <a href="<?=$arItem["LINK"]?>"><?=$arItem["TEXT"]?></a>
    

    на следующий:

    <a href="<?=$arItem["LINK"]?>" <?=$arItem["PARAMS"]["target"]?>><?=$arItem["TEXT"]?></a>
    

    Отображение пункта меню для определенных групп пользователей

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

    Тип условия: Для групп пользователей
    Условие: требуемые_группы_пользователей
    

    Отображение пункта меню неавторизованным пользователям

    В расширенном режиме редактирования меню необходимо установить следующее условие:

    Тип условия: выражение PHP
    Условие: !$USER->IsAuthorized()
    

    Отображение пункта меню при нахождении в определенном разделе сайта

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

    Примечание: Условие Для папки и файла лучше всего применять для статических страниц и для динамических страниц с компонентами на ЧПУ. На динамических страницах без ЧПУ он не будет работать, так как производит проверку по части адреса, а в динамических страницах всегда будут присутствовать выбранные значения. Для динамических URL удобнее применять условие Параметр в URL.


    Отображение определенных пунктов только на главной странице и еще в одном внутреннем разделе

    В расширенном режиме в поле условие для нужных пунктов ввести выражение php:

    CSite::InDir('/about/') or CSite::InDir('/index.php')

    Осуществление вывода пункта меню, ссылающегося на определенный товар, связанный с товаром, открытым на данной странице

    Для этого можно использовать тип условия Параметр в URL. Пункт будет отображен на страницах с определенным параметром в URL. Параметр работает с URL, в которых есть символ ?. То есть, с динамическими страницами.

    Страницы, созданные на базе инфоблоков, имеют URL вида: http://сайт/раздел/index.php?SECTION_ID=***. Предположим, что на странице с SECTION_ID=123 должен быть отражен пункт меню, ведущий на страницу SECTION_ID=456.

    Создадим пункт меню, ведущий на страницу http://сайт/раздел/index.php?SECTION_ID=456. В поле Тип условия выберем Параметр в URL, в первом поле Условие укажем SECTION_ID, а во втором 123.


    Всплывающие подсказки для пунктов меню

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

    Название: A_TITLE
    Значение: текст_всплывающей_подсказки
    

    В шаблоне меню:

    <?if($arItem["SELECTED"]):?>
    		<li><a href="<?=$arItem["LINK"]?>" class="selected"><?=$arItem["TEXT"]?></a></li>
    	<?else:?>
    		<li><a href="<?=$arItem["LINK"]?>"><?=$arItem["TEXT"]?></a></li>
    <?endif?>
    
    нужно заменить первую ссылку в коде на строку:
    <a href="<?=$arItem["LINK"]?>" class="selected" title="<?=$arItem["PARAMS"]["A_TITLE"]?>"><?=$arItem["TEXT"]?></a>
    
    а вторую на:
    <a href="<?=$arItem["LINK"]?>" title="<?=$arItem["PARAMS"]["A_TITLE"]?>"><?=$arItem["TEXT"]?></a>
    

    Как поставить картинки рядом с пунктами меню?

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

    Название: IMG
    Значение: путь_к_картинке
    

    В шаблон меню, после вывода элемента меню, необходимо после строки (в зависимости от шаблона):

    <a href="<?=$arItem["LINK"]?>">
    

    добавить следующее:

    <img src="<?=$arItem["PARAMS"]["IMG"]?>" border="0" />
    

    Разные картинки-пункты меню для разных языков

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

    Решение:

    Укажите в шаблоне меню следующее:
    <body class="lang-<?=LANG?>"> 
    
    в CSS:
    .menu li.item1 { 
    background-image:url(title-en.gif); 
    } 
    .lang-ru .menu li.item1 { 
    background-image:url(title-ru.gif); 
    }
    

    Отображение собственного, не такого как в шаблоне, меню для определенных разделов сайта

    Организовать такое меню можно с помощью смены шаблона сайта (Настройки > Настройки продукта > Сайты > Список сайтов, редактирование сайта, секция Шаблон, условие для папки или файла). Причем, если шаблон не сложный, то можно прямо в коде шаблона поставить проверку и в одном случае выводить одно меню в другом - другое:
    if($APPLICATION->GetCurPage() == "/index.php") {
    вывод меню для главной страницы
    }
    else {
    вывод второго меню
    }
    

    Как сделать меню tree в постоянно открытом состоянии?

    Решение:

    Нужно взять стандартный шаблон меню tree, скопировать его в свой шаблон. После чего в файле template.php необходимо строчку:
    <li class="close">
    
    заменить на:
    <li>
    

    При этом развернутые пункты будут прятаться по щелчку на изображении.


    Как сделать так, чтобы при открытии страницы через древовидное меню, само меню не сворачивалось, а показывало, на какой странице ты находишься?

    Решение:

    Нужно взять стандартный шаблон меню tree, скопировать его в свой шаблон. После чего в файле template.php необходимо 14 строчку:
    <li class="close">
    
    заменить на:
    <li <?if (!$arItem["SELECTED"]):?>class="close"<?endif?>>
    

    Примечание: Предложенное решение актуально лишь до 2-го уровня вложенности меню.

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

    ...
    <?if (!empty($arResult)):?>
    
    <?
    //анализ открытых узлов дерева
    $lastLevel = 0;
    $selected = false;
    
    foreach(array_reverse($arResult) as $arItem){
    	if ($arItem["SELECTED"]) {
    		$lastLevel = $arItem["DEPTH_LEVEL"];
    		$selected = true;
    	}
    	if ($selected and $arItem["DEPTH_LEVEL"] < $lastLevel){
    		$arResult[ $arItem["ITEM_INDEX"] ]["SELECTED"] = true;
    		$lastLevel--;
    	}
    }
    ?>
    
    <div class="menu-sitemap-tree">
    <ul>
    <?$previousLevel = 0;foreach($arResult as $arItem):?>
    ...
    
    

    Скрытие бокового меню по свойству страницы

    Задача такая. В шаблоне сайта прописан вывод бокового меню. Нужно, чтобы оно не показывалось только на тех страницах, у которых прописано какое-нибудь свойство, отменяющее показ бокового меню.

    Решение:

    Если меню расположено в header'е, то функцию GetProperty использовать нельзя, потому что свойства страницы задаются после подключения header'a. Поэтому меню можно вывести "отложенно" следующим способом:

    • в header'е, где надо показать меню добавить следующий код:
    $APPLICATION->ShowProperty('menu');
    
    • в свойствах страницы, если надо запретить меню:
    $APPLICATION->SetPageProperty('hide_menu', 'Y');
    
    • в footer'е:
    if( 'Y' != $APPLICATION->GetPageProperty('hide_menu') ){
    	ob_start();
    	echo 'проверка отложенного меню!';
    	// ....здесь происходит вывод меню компонентом или другим способом.... //
    	$APPLICATION->SetPageProperty('menu', ob_get_clean() );
    }
    

    Само меню "выводится" в футере, если свойство hide_menu не установлено в значение Y. Но оно не выводится на самом деле в футере, а отправляется в свойство menu, которое "выше" по коду можно показать "отложенно" через ShowProperty. Если меню запрещено, то в значении свойства menu будет пусто и ничего не покажется в шаблоне. Если не запрещено, то для этого примера - там, где прописано $APPLICATION->ShowProperty('menu'), будет выведена фраза проверка отложенного меню!.


    Меню и кеш

    Цитата: Забился весь диск под завязку. Стал разбираться, и оказалось, что папка bitrix/managed_cache/MYSQL/menu весит 16Гб! На сайте присутствуют 2 меню горизонтальное - навигация по страницам сайта и вертикальное - по разделам каталога.

    Кеш меню зависит от адреса страницы. Если страниц много, то кеш может получаться большим. В этом случае может оказаться более эффективным отключить кеш в компоненте меню.

    Рекламные области

    На сайте может быть представлено несколько различных видов рекламы. Это может быть как стандартная баннерная реклама, так и текстовые рекламные области. Реклама может быть постоянной или показываться с определенной вероятностью, задаваемой администратором. Показ рекламы может быть регламентирован по определенным разделам сайта и т.д. Создание и управление рекламой осуществляется средствами модуля Рекламы.

    Показ рекламы выполняется в специально выделенных областях шаблона дизайна сайта – рекламных областях.

    Подключение баннера в рекламной области может выполняться с помощью компонента Баннер (bitrix:advertising.banner). Компонент выводит баннер заданного типа.

    Примечание: Данный компонент не учитывает таргетинг по ключевым словам. Если необходимо его учесть, то следует использовать функцию $APPLICATION->ShowBanner().

    Код подключения баннера в рекламной области с помощью PHP функции ShowBanner() имеет вид:
    <?
    //---Пример размещения рекламной области в левой части сайта.
    //---Аналогично производится выбор любого другого типа:
    //---TOP, BOTTOM, COUNTER,… (первый параметр функции)
    //---Могут быть использованы как предустановленные, так и задаваемые
    //---пользователем типы.
    //---В качестве двух дополнительных параметров может указываться
    //---HTML-код обрамляющий рекламную область сверху и снизу.
    $APPLICATION->ShowBanner("LEFT", '<div align="center">', '<br></div><br>');
    ?>

    Для каждой рекламной области определяется, реклама какого типа будет доступна для показа в данной области. На приведённой выше иллюстрации это рекламные баннеры типа LEFT и BOTTOM.

    Список ссылок по теме:

    • Описание модуля Реклама в курсе Администратор. Модули.

    Типы рекламы

    Тип рекламы - это условное название группы рекламных баннеров, обладающих некоторым общим признаком. Например:
    • место показа – все баннеры должны показываться в определенном месте страницы сайта;
    • тематика (например, рекламируется один товар);
    • рекламодатель (реклама определенной компании);
    • и т.д.

    Название типа рекламного блока задается администратором произвольно. Например, для верхней рекламы может быть установлен тип TOP, MAIN, BOTTOM и т.п.

    Важно! Рекомендуется, чтобы баннеры из одной группы имели одинаковый размер. Это позволит избежать деформации страницы при показе рекламы.

    Управление типами рекламы выполняется через административный интерфейс модуля Рекламы (Маркетинг > Баннерная реклама > Типы баннеров):

    Приступить к управлению рекламным баннером или списком баннеров выбранной рекламной области можно непосредственно из публичной части сайта. Для этого перейдите в режим правки и воспользуйтесь одной из кнопок управления рекламной областью:

    Механизм управления показом с помощью ключевых слов

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

    С помощью ключевых слов можно:

    • организовать показ рекламы, нацеленный на конкретную группу пользователей сайта. Т.е. показывать рекламу на страницах, посещаемых преимущественно этими пользователями, или страницах, тематика которых может заинтересовать данную группу пользователей.
    • ограничить показ рекламы на страницах сайта, например, исходя из того, насколько содержание рекламы связано с информацией, представленной на странице.

    Механизм управления

    В целях управления показом рекламы для страниц используются два вида специальных ключевых слов:

    • ключевые слова баннера;
    • ключевые слова страниц сайта:
      • желательные (desired): если для страницы сайта заданы желательные ключевые слова, то для показа на данной странице будут доступны все баннеры, в наборе ключевых слов которых содержится хотя бы одно желательное ключевое слово страницы.
      • обязательные (required): если для страницы сайта заданы обязательные ключевые слова, то для показа на данной странице будут доступны все баннеры, в наборе ключевых слов которых содержатся все обязательные ключевые слова страницы.

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

    Общий порядок действий по организации показа баннеров на нужных страницах такой:

    • Определяется, какая реклама будет доступна для показа на тех или иных страницах сайта.
    • В соответствии с поставленными задачами для страниц сайта задаются наборы специальных ключевых слов.
    • В настройках рекламных баннеров задается необходимый набор ключевых слов.

    Ключевые слова баннеров задаются в поле Ключевые слова на странице создания/редактирования баннера, вкладка Таргетинг (Маркетинг > Баннерная реклама > Баннеры).

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

    Желательные ключевые слова страницы могут быть заданы с помощью функции SetDesiredKeywords в коде страницы. Данная функция содержит следующие параметры:

    • keywords – одно или несколько желательных ключевых слов страницы;
    • TYPE_SID – тип рекламы, управление показом которой будет осуществляться с помощью указанных желательных ключевых слов. Если данный параметр оставить пустым, то ключевые слова будут заданы для всех типов рекламы.
    CAdvBanner::SetDesiredKeywords (array ("Partners", "Cooperation", "Company", "Contacts"), "RIGHT");
    Обратите внимание! По умолчанию, если в коде страницы не установлено каких-либо ключевых слов для рекламы, считается что функция SetDesiredKeywords использует в качестве параметра значение свойства страницы adv_desired_target_keywords. Если оно не задано, то в качестве параметра функции используется значение свойства keywords.

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

    Обязательные ключевые слова страницы задаются с помощью предустановленной функции системы SetRequiredKeywords. Данная функция содержит следующие параметры:

    • keywords – одно или несколько обязательных ключевых слов страницы;
    • TYPE_SID – тип рекламы, управление показом которой осуществляется с помощью указанных обязательных ключевых слов. Если данный параметр оставить пустым, то ключевые слова будут заданы для всех типов рекламы.
    CAdvBanner::SetRequiredKeywords (array("Partners", "Cooperation"), "RIGHT");

    В приведенном примере для показа на странице будет доступен баннер, в наборе ключевых слов которого содержатся все обязательные ключевые слова (partners и cooperation), заданные для страницы.



    Использование прав доступа

    При создании шаблона сайта достаточно часто возникает задача ограничения доступа к тем или иным элементам. Предусмотренный в системе механизм проверки прав доступа может быть использован в процессе создания шаблона сайта для управления:

    показом пунктов меню

    При редактировании меню в расширенном режиме для каждого пункта может быть задано условие показа. Например:

    Смотри также Настройка пунктов меню в курсе Администратор. Базовый.

    шаблоном меню

    Уровень прав доступа пользователей может влиять на структуру шаблона меню, используемые элементы, изображения и т.д. Пример проверки уровня прав доступа пользователя для шаблона меню приводится ниже:

    <?if (!defined("B_PROLOG_INCLUDED") || B_PROLOG_INCLUDED!==true)die();?>
    
    <?if (!empty($arResult)):?>
    <div class="blue-tabs-menu">
    	<ul>
    <?foreach($arResult as $arItem):?>
    
    	<?if ($arItem["PERMISSION"] > "D"):?>
    		<li><a href="<?=$arItem["LINK"]?>"><nobr><?=$arItem["TEXT"]?></nobr></a></li>
    	<?endif?>
    
    <?endforeach?>
    
    	</ul>
    </div>
    <div class="menu-clear-left"></div>
    <?endif?>
    Важно! Условия, включающие проверку значения ключа массива $PERMISSION у переменной $arItem, используются только для меню сайта.

    шаблоном сайта

    Для каждого шаблона дизайна может быть настроено условие его применения к сайту. Данная настройка выполняется на странице управление параметрами сайта (Настройки системы > Сайты > Изменить). Например:

    Нажмите на рисунок, чтобы увеличить

    В приведенном примере условие определяет, что шаблон Версия для печати будет применяться, если в URL параметр print=Y.

    Наиболее гибким инструментом настройки условий показа является Условие PHP. Примеры php-условий для показа шаблона сайта:

    $USER->IsAuthorized()Проверяется, является ли текущий пользователь авторизованным в системе.
    $USER->IsAdmin()Проверяется, является ли текущий пользователь администратором.
    in_array('5',$USER-> GetUserGroupArray())Проверяется, относится ли текущий пользователь к указанной группе (в данном случае к группе с ID равным 5).

    Смотрите также Настройка шаблона сайта в курсе Администратор. Базовый.

    отдельными элементами

    Для управления элементами шаблона дизайна

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

    Управление отдельными элементами сайта

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

    Разные языки сайта

    Система Bitrix Framework позволяет использовать один и тот же шаблон дизайна для нескольких сайтов на разных языках. Данная возможность реализуется с помощью механизма языковых сообщений:

    • в HTML верстке шаблона определяются области, в которых должен отображаться тот или иной текст;
    • затем в выделенные области вместо текстовых сообщений подставляется код, выполняющий подключение и показ соответствующих языковых сообщений на нужном языке.

    Например, с использованием языковых сообщений могут быть созданы заголовки таблиц, надписи на кнопках, сообщения компонентов и т.д.

    Перед изучением главы рекомендуется познакомится с [ds]Управлением языковыми сообщениями[/ds][di]Чтобы многоязычный интерфейс работал правильно, нужно где-то хранить весь текст, который используется в административном разделе. Он хранится в виде отдельных текстовых сообщений в языковых файлах соответствующих языков. Языковые файлы размещаются в папках тех модулей и шаблонов, для которых используются эти текстовые сообщения.

    Подробнее в курсе Администратор.Базовый[/di] курса Администратор. Базовый.


    Примечание: Механизм языковых сообщений используется, в том числе, для поддержки многоязычного интерфейса административного раздела системы.


    Ссылки по теме:

    Механизм реализации

    Языковое сообщение - группа фраз на разных языках, имеющая один смысл.


    • В папке шаблона сайта создается папка с именем /lang/:
      /bitrix/templates/< идентификатор шаблона >/lang/
    • В папке /lang/ создаются папки с идентификаторами используемых языков: /en/, /de/, /ru/ и т.д. Например:
      /bitrix/templates/<идентификатор шаблона>/lang/ru/
    • В созданных папках размещаются соответствующие файлы языковых сообщений. Они характеризуются следующими параметрами:

      • Имя файла языковых сообщений соответствует имени файла, в котором выполняется его вызов. Например, если предполагается, что вызов файла с языковыми сообщениями будет выполняться в прологе шаблона сайта (файл header.php), то файл языковых сообщений должен быть сохранен с именем header.php;
      • Список сообщений в файле хранится в следующем виде:
        <?
        $MESS ['COMPANY_NAME'] = "Company Name";
        $MESS ['MAIN_PAGE'] = "Home page";
        $MESS ['PRINT'] = "Print version";
        $MESS ['AUTH_LOGIN'] = "Authorization";
        $MESS ['RATES_HEADER'] = "Currency rates";
        $MESS ['SEARCH'] = "Site search";
        $MESS ['SUBSCR'] = "Subscription";
        ?>
        где выражение в скобках это код фразы (например, 'COMPANY_NAME')

        Примечание: Система собирает все языковые файлы и формирует единый массив $MESS при формировании страницы. И если языковые фразы в разных местах имеют одинаковый код, то во всех местах будет использоваться код из последнего подключенного языкового файла. Это приводит к выводу неверной фразы. Рекомендуется создавать максимально уникальные ключи.


    • Затем в начало файла, для которого предусмотрено использование языковых сообщений (например, header.php), добавляется функция:
      <?
      Loc::loadMessages(__FILE__).
      ?>
      Функция выполняет подключение файла языковых сообщений для текущего языка.
    • Далее все текстовые сообщения заменяются на функции вызова соответствующих языковых сообщений:
      <?echo Loc::getMessage("SEARCH");?>
      В качестве параметра функции GetMessage() используется код подключаемого сообщения. Функция проверяет наличие в подключенном языковом файле сообщения с соответствующим кодом и отображает его пользователю.

    Загрузка и выгрузка локализации

    Загрузка и выгрузка в CSV-файл

    Для ручной правки файлов локализации используется функционал выгрузки и загрузки в формате CSV.

    Для этого необходимо:

    • Открыть страницу Просмотра файлов (Настройки > Локализация > Просмотр файлов) и выбрать [dw]Выгрузить сообщения в CSV[/dw][di][/di].
    • Откроется форма настройки выгрузки. Указать, если требуется, выгрузку только непереведенных фраз и необходимость конвертации в кодировку UTF-8, а также какие языки выгрузить. Для выгрузки языковых сообщений из конкретных папок или файлов требуется внести пути к ним в поле Выгрузить сообщения только для файлов и папок:

    • Нажать кнопку Экспортировать для выгрузки.

      Для скачивания - нажать Скачать файл экспорта. Название CSV-файла по умолчанию будет включать путь до папки, в которой находится папка/файл и выбранные языки.

      Например, если экспортировать папку /bitrix/activities/bitrix/absenceactivity с языками ru и en, то название файла будет: bitrix_activities_bitrix_ru_en.csv.

      Примечание: Второй способ экспорта конкретных папок или файлов - из списка. Отметьте их в списке и на панели действий выберите действие [dw]Экспортировать выделенные[/dw][di][/di].
      Функция экспорта языковых сообщений также доступна в форме просмотра конкретного файла.

    • Отредактировать необходимые строки и сохранить все изменения в том же CSV-файле.
    • Далее перейти к импорту нажатием кнопки [dw]Импортировать сообщения из CSV[/dw][di][/di] на странице просмотра файлов. Выбрать файл для загрузки, кодировку и способ добавления локализации в систему: добавлять только новые переводы, обновить только фразы найденные в csv-файле или добавлять новые переводы и обновить фразы, найденные в csv-файле. Нажать кнопку Импортировать:

    Локализация из CSV-файла будет добавлена в систему.

    Сбор переводов

    После создания локализации системы можно создать полный пакет локализации для каждого языка системы.

    Для этого необходимо:

    • Перейти на страницу Сбор переводов (Настройки > Локализация > Выгрузка и загрузка).

    • Выбрать необходимые опции в закладке Сбор переводов:
      • язык для сборки;
      • дату сборки в формате YYYYMMDD;
      • выбрать кодировку, в которую будут сконвертированы все файлы локализации (Необязательно. Если не выбрать, то файлы будут собраны в текущей кодировке сайта);
      • по желанию упаковать файлы в архив tar.gz;
    • Для запуска процесса сбора пакета локализации нажать кнопку Собрать локализацию.

    После того, как пакет будет собран, будет предоставлена ссылка, где хранится архив (если была выбрана опция упаковки в архив tar.gz) или файлы всего пакета.


    Чтобы импортировать пакет локализации в систему, необходимо:

    • Перейти на страницу Выгрузка и загрузка (Настройки > Локализация > Выгрузка и загрузка):

    • Выбрать необходимые опции в закладке Загрузка переводов:
      • файл с архивом tar.gz (ограничение - 32 МБ);
      • язык для сборки (необходимый язык интерфейса должен быть уже установлен в системе, либо можно его создать по ссылке Добавить);
      • выбрать кодировку, в которую были ранее сконвертированы все файлы локализации;
    • Для запуска процесса импорта пакета локализации в систему нажать кнопку Загрузить локализацию.

    Изменение фраз в компонентах и модулях

    Иногда при разработке дизайна проекта, необходимо изменить несколько слов или фраз в стандартных компонентах или (что хуже) модулях. Если шаблон компонента можно скопировать и кастомизировать, то с самими компонентами и тем более модулями все плохо. Сложно что-то изменить, не отказавшись от обновлений.

    Тем не менее, есть технология замены любых языковых фраз продукта. Решение не идеальное, но оно дает довольно большой простор для полета фантазии разработчика. Суть технологии в том, что языковые фразы продукта после подключения языкового файла заменяются на фразы, определенные разработчиком. Список замен должен находиться в файле /bitrix/php_interface/user_lang/<код языка>/lang.php, либо в файле /local/php_interface/user_lang/<код языка>/lang.php.

    В этом файле должны определяться элементы массива $MESS в виде $MESS['языковой файл']['код фразы'] = 'новая фраза', например:

    <?
    $MESS["/bitrix/components/bitrix/system.auth.form/templates/.default/lang/ru/template.php"]["AUTH_PROFILE"] = "Мой любимый профиль";
    $MESS["/bitrix/modules/main/lang/ru/public/top_panel.php"]['top_panel_tab_view'] = "Смотрим";
    $MESS["/bitrix/modules/main/lang/ru/interface/index.php"]['admin_index_sec'] = "Проактивка";
    ?>

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

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

    Настройка дополнительных элементов

    Настройка сообщений об ошибках

    Часто возникает необходимость выполнить настройку сообщений об ошибках, чтобы сообщение об ошибке было аккуратно показано в дизайне сайта.

    Чтобы произвести настройку внешнего вида сообщения об ошибке соединения с базой данных, следует отредактировать файл: /bitrix/php_interface/dbconn_error.php.

    Для того чтобы произвести настройку внешнего вида сообщения об ошибке в запросе к базе данных, следует отредактировать файл: /bitrix/php_interface/dbquery_error.php.



    Настройка файла, подключаемого при закрытии сайта

    Чтобы произвести настройку внешнего вида файла, подключаемого при [ds]закрытии публичной части[/ds][di]Закрытие публичной части в настройках Главного модуля...[/di] сайта, следует скопировать файл: /bitrix/modules/main/include/site_closed.php и поместите его в /bitrix/php_interface/<язык>/ или в /bitrix/php_interface/include/.



    Настройка внешнего вида навигации постраничного просмотра

    Постраничный показ информации организуется с использованием PHP функции NavPrint() – функции вывода ссылок для постраничной навигации. Для управления внешним видом постраничной навигации могут быть использованы следующие параметры: NavPrint($title, $show_always=false, $StyleText="text", $template_path) где:

    $title – название выводимых элементов;
    $show_always – если значение параметра false, то функция не будет выводить навигационные ссылки, если все записи умещаются на одну страницу. Если true, то ссылки для постраничной навигации будут выводиться всегда;
    $StyleText – CSS класс шрифта для вывода навигационных ссылок;
    $template_path – путь к шаблону показа навигационных ссылок.

    Favicon

    Одна иконка для административной и публичной части сайта

    По умолчанию favicon располагается в корневом каталоге сайта (/favicon.ico).
    В этом случае для административной и публичной части сайта будет отображаться один и тот же значок.

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

    Существует вариант, позволяющий задать свою иконку для конкретного шаблона сайта. Для этого:

    • файл с иконкой следует поместить в папку с шаблоном (/bitrix/templates/#имя_шаблона#/favicon.ico).
    • в файле header.php (/bitrix/templates/#имя_шаблона#/header.php) между тегами <head> </head> необходимо добавить строку:
      <link rel="shortcut icon" type="image/x-icon" href="<?=SITE_TEMPLATE_PATH?>/favicon.ico" />

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

    Примечание: В ранних версиях продукта favicon был в виде специального PHP-скрипта. Такое решение использовалось для модуля статистики.
    Во всех актуальных версиях продукта такой вариант более не используется.



    Примеры работы и решения проблем

    Цитатник веб-разработчиков.

    Алексей Майдокин: Моё мнение - вёрстке и другим сложным штукам место в шаблонах, компонентах и тому подобном, короче там, куда простым смертным просто так не добраться. Сложная вёрстка в области контента - это как двигатель в салоне легковушки.

    Подробную информацию об условиях применения шаблонов можно посмотреть в следующих уроках:


  • Простые примеры применения шаблонов в зависимости от различных условий
  • Подключение favicon.ico для разных шаблонов
  • Отдельный шаблон для главной страницы сайта
  • Изменение шаблона сайта, в зависимости от временного параметра
  • Применение шаблона сразу по двум условиям
  • Применение шаблона только к файлам с определённым расширением
  • Изменение дизайна «шапки» сайта для разных разделов
  • Особенности работы с ajax
  • Простые примеры применения шаблонов в зависимости от различных условий

    Если свойство раздела phone равно Y

    $APPLICATION->GetDirProperty("phone")=="Y"

    Если текущий раздел равен /ru/catalog/phone/

    $APPLICATION->GetCurDir()=="/ru/catalog/phone/"

    Если текущий пользователь - администратор

    $USER->IsAdmin()

    Если нужно привязать шаблон к динамической странице (в примере - к детальной странице(карточке) товара.)

    preg_match("#/catalog/\?SECTION_ID=\d+&ELEMENT_ID=\d+#i",$_SERVER['REQUEST_URI']);

    Подключение favicon.ico для разных шаблонов

    Для того чтобы в разных шаблонах сайтов были разные значки, надо в шаблоне в header.php добавить вызов значка из шаблона:

    <link rel="icon" type="image/x-icon"
    href="<?=SITE_TEMPLATE_PATH;?>/images/favicon.ico" />
    

    Отдельный шаблон для главной страницы сайта

    Цитата: По умолчанию ставлю шаблон, который рассчитан для внутренних страниц сайта. На главной странице будет свой шаблон. Подскажите, как составить условие для отображения нужного шаблона на главной странице сайта?

    Вдобавок к основному шаблону для всех страниц сайта необходимо подключить требуемый шаблон через условие Для папки или файла, в котором указать /index.php с учетом индекса сортировки применения шаблона.


    Изменение шаблона сайта, в зависимости от временного параметра

    Цитата: Как реализовать изменение шаблона сайта, в зависимости от временного параметра (Всего два шаблона, но они должны меняться каждый час)?

    Для этого необходимо сделать изменение текущего шаблона по условию Выражение PHP.

    Первый вариант

    По нечетным часам: ((date("H")+6)%2) == 1
    По четным часам: ((date("H")+6)%2) == 0

    где +6 - указывает временную зону.

    Второй вариант

    date("H")%2
    или
    !date("H")%2



    Применение шаблона сразу по двум условиям

    Цитата: Подскажите, как задать шаблон одновременно по 2 условиям (для группы пользователей и для папки и файла) в настройках БУС.

    Для этого необходимо использовать изменение текущего шаблона по условию Выражение PHP:

    in_array($groupID, $USER->GetUserGroupArray()) || strpos($APPLICATION->GetCurDir(), "/dir/")!==false || $APPLICATION->GetCurPage()=="/dir/file.php"
    


    Применение шаблона только к файлам с определённым расширением

    Цитата: Подскажите, пожалуйста, выражение PHP, чтобы шаблон действовал на все страницы, заканчивающиеся на .php, а .html не трогал.

    Решение: изменение шаблона по условию Выражение PHP:

    substr($APPLICATION->GetCurPage(true), -4) == ".php" 
    

    Аналогично для файлов с расширением html:

    substr($APPLICATION->GetCurPage(true), -5) == ".html"

    Изменение дизайна «шапки» сайта для разных разделов

    Задача: Сайт разделен на несколько разделов. По замыслу у каждого раздела должна быть своя «шапка» в дизайне. Более в дизайне ничего не меняется. Как лучше реализовать смену «шапок» разделов?

    Решение: В шаблон подключается компонент Включаемая область (для раздела):

    <div id="header">
    <?$APPLICATION->IncludeComponent("bitrix:main.include", ".default", array(
    	"AREA_FILE_SHOW" => "sect",
    	"AREA_FILE_SUFFIX" => "headerinc",
    	"AREA_FILE_RECURSIVE" => "Y",
    	"EDIT_TEMPLATE" => "sect_headerinc.php"
    	),
    	false
    );?>
    </div>
    

    Код шапки каждого из разделов будет храниться в файле sect_headerinc.php. Параметр "AREA_FILE_RECURSIVE" => "Y" означает, что такая же "шапка" появится у всех подразделов данного раздела, если родительский sect_headerinc.php не будет специально перекрыт у кого-то из нижележащих разделов.


    Особенности работы с ajax

    Использование режима ajax имеет свои особенности. Чтобы строка навигации в открываемой по ajax странице имела в цепочке навигации своё название, необходимо, чтобы в шаблоне обязательно присутствовал элемент с id="navigation". Это необязательно должен быть div, это может быть span, h1, p и так далее.

    Аналогично, для заголовка обязательно наличие элемента с id="pagetitle".



    Как вывести произвольный контент в шаблоне сайта и компонента

    Усовершенствованные методы буферизации в шаблоне позволяют более не использовать CBitrixComponentTemplate::EndViewTarget() ввиду того, что конец шаблона вызывает завершение буферизации автоматически.

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

    Можно использовать как в шаблоне сайта, так и в шаблоне компонента.

    Теперь есть поддержка стандартного кеширования в компонентах.

    • template.php:
      <?$this->SetViewTarget("sidebar");?>
      
      	<div class="element-filter">
      		<!--вывод фильтра -->
      	</div>
      
      <?$this->EndViewTarget();?>
      
      <div class="element-list">
      	<!--вывод списка -->
      </div>
    • header.php:
      <div id="sidebar">
      	<?$APPLICATION->ShowViewContent("sidebar")?>
      </div>

    Методы, доступные в шаблоне (через $this)

    • CBitrixComponentTemplate::SetViewTarget($view, $pos)
    • CBitrixComponentTemplate::EndViewTarget()

    Методы глобального объекта $APPLICATION

    где:

    • $view – идентификатор буферизируемой области;
    • $content – буферизируемый контент;
    • $pos – сортировка вывода контента.

    Примечание: одному идентификатору $view может соответствовать несколько буферов. Последовательность вывода контента определяется сортировкой $pos.


    Разработка шаблонов страниц

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

    Шаблоны страниц и редактируемых областей хранятся в папке /page_templates/, находящейся в каталоге соответствующего шаблона сайта или в папке /bitrix/templates/.default/, если предполагается использование одинаковых шаблонов страниц для всех шаблонов сайта.

    При создании страницы в режиме визуального HTML-редактора достаточно выбрать из списка нужную заготовку, по которому будет создана страница, и добавить необходимую информацию.

    Детально о создании шаблонов страниц описано в курсе [ds]Администратор. Базовый[/ds][di]На проектах с большим содержанием статических страниц возникает задача оптимизации труда контент-менеджеров. Решить эту задачу можно с помощью создания шаблонов. Есть несколько вариантов шаблонизации контента статики.

    Подробнее ...[/di]


    Руководство по оформлению HTML/CSS кода

    Руководство - это правила для разработчиков компании 1С-Битрикс, которое является обязательным при разработке продуктов Bitrix Framework. Раздел описывает как надо оформлять и форматировать HTML и CSS код. Цель раздела - повысить качество кода и облегчить совместную работу и поддержку инфраструктуры разработчиками.

    Правила стандартизированы относительно недавно и поэтому в коде продуктов могут встречаться отклонения от них. Тем не менее, разработчикам проектов на основе Bitrix Framework рекомендуется их придерживаться - это облегчит дальнейшую поддержку проектов.

    Общие правила оформления кода

    Отступы

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

    В PHPStorm настройте поля Показ невидимых символов, Табуляция вместо пробелов.

    Кодировка

    UTF-8 фактически стал стандартом, хотя клиенты Bitrix Framework иногда установливают продукт в кодировке ANSI.

    Перевод строки

    Строки должны заканчиваться только символом перевода LF - стандарт для текстовых файлов в Unix-системах. Не используйте комбинацию символов возврата каретки/перевода строки (CRLF), как это принято в Windows.

    В PHPStorm настройте поле Перевод строки для новых файлов. В нижней статусной строке показывается, какой перевод строки используется для текущего файла (должен быть LF).

    Максимальная длина строки

    Длина не должна превышать 120 символов. Настройте редактор кода так, чтобы он отображал правую границу строк.

    В PHPStorm настройте поля Показ правой границы, Ширина правой границы.

    Регистр

    Названия тегов, атрибутов и названия классов должны быть всегда в нижнем регистре.

    Пробелы в конце строки

    Убирайте пробелы в конце. Пробелы в конце строк не обязательны и усложняют сравнение файлов в системе контроля версий.

    В PHPStorm настройте поле Удаление пробелов перед сохранением файла.

    Комментарии

    Все комментарии должны быть на английском языке.

    Правила оформления HTML

    Семантика

    Использование семантики облегчает восприятие html-кода страницы и понимание логического и функционального назначения каждого тега. В HTML существует довольно много тегов, которые в своем названии несут четкую семантику: <a>, <p>, <i>, <menu>. Не стоит с помощью CSS таким тегам менять их базовый смысл. Идеально для верстальщика было бы удобно использовать теги с произвольными названиями.

    <users>
    	<user>
    		<avatar></avatar>
    		<name></name>
    	</user>
    </users>

    HTML5 предлагает механизм для создания произвольных тегов (Web Components). Но реализация этого механизма не самая простая (нужно писать JavaScript) и поддержка в браузерах неполная. Единственный нормальный способ писать семантический код - использовать атрибут class. Он позволяет описать смысл тега естественными словами, не требователен к уникальности (в отличие от атрибута id) и поддерживает возможность указать несколько классов к одном тегу.

    Основы верстки

    • Используйте теги div (для блочных элементов) и span (для строчных и строчно-блочных элементов).
    • Остальные теги используйте только согласно их базовому смыслу.
    • Теги <b>, <i>, <s>, <p>, с <h1> по <h6>, <ul>, <li> - используются только для форматирования текста. Как правило, эти стили находятся в шаблоне сайта и в стилях компонентов не присутствуют.
    • Для привязки CSS-свойств к тегу используйте только атрибут class.
    • Если нажатие на элемент приводит к открытию новой страницы, используйте тег <a>. В противном случае используйте <span> или <div>.
    • Отступы между логическими блоками никогда не делайте пробелами или тегом <br>.
    • Структуру html-тегов необходимо делать по сетке дизайна. "Колонки" и логические блоки одного уровня обязательно помещайте в контейнер.

      Плохо:

      <span class="task-detail-sidebar-info-link">сменить</span>
      <div class="task-detail-sidebar-info-title">Ответственный</div>
      <div class="task-detail-sidebar-info-user">
      	<a href="#" class="task-detail-sidebar-info-user-photo"></a>
      	<a href="#" class="task-detail-sidebar-info-user-name">Иван Петров</a>
      	<div class="task-detail-sidebar-info-user-post">Бухгалтер</div>
      </div>

      Лучше:

      <div class="task-detail-sidebar-info">
      	<div class="task-detail-sidebar-info-head">
      		<div class="task-detail-sidebar-info-link">сменить</div>
      		<div class="task-detail-sidebar-info-title">Ответственный</div>
      	</div>
      	<div class="task-detail-sidebar-info-user">
      		<a href="#" class="task-detail-sidebar-info-user-photo"></a>
      		<div class="task-detail-sidebar-info-user-title">
      			<a href="#" class="task-detail-sidebar-info-user-name">Иван Петров</a>
      			<div class="task-detail-sidebar-info-user-post">Бухгалтер</div>
      		</div>
      	</div>
      </div>
    • Удаление логических блоков не должно рушить дизайн.
    • Всегда есть вероятность, что на реальном сайте количество текста будет больше, чем нарисовано в макете. Верстка должна быть адаптивной к появлению дополнительных строк и к длинным строкам без пробелов.
    • Текстовый элемент не может быть на одном уровне с тегом.

      Плохо:

          <div class="task-detail-group">Задача в проекте: <div class="task-detail-link">добавить</div></div>

      Хорошо:

      <div class="task-detail-group">
      		<span class="task-detail-group-label">Группа:</span>
      		<span class="task-detail-group-name">Название группы</span>
      </div>
    • Каждый компонент должен иметь родительский контейнер. Для этого элемента в CSS необходимо указать шрифт (font-family), а также размер текста (font-size), чтобы избежать конфликтов со стилями шаблона сайта.

    Особые случаи

    • Внимательно относитесь к элементам inline-block, перевод строки между ними будет отображен как пробел. Чтобы не писать теги в одну длинную строку, можно воспользоваться следующим приемом:
      <div class="inline-block-items"><?
      	?><span class="inline-block-item">Inline Item 1</span><?
      	?><span class="inline-block-item">Inline Item 2</span><?
      	?><span class="inline-block-item">Inline Item 3</span><?
      	?><span class="inline-block-item">Inline Item 4</span><?
      ?></div>

      Визуально теги разбиты и отделены отступами, а в результирующем HTML-коде будет одна строка. Другой прием с помощью HTML-комментариев:

      <div class="inline-block-items"><!--
      	--><span class="inline-block-item">Inline Item 1</span><!--
      	--><span class="inline-block-item">Inline Item 2</span><!--
      	--><span class="inline-block-item">Inline Item 3</span><!--
      	--><span class="inline-block-item">Inline Item 4</span><!--
      --></div>
    • Элементы с обтеканием (float)
      • Неприятной особенностью свойства float является то, что такой блок может "вывалиться" из блока-родителя. Это происходит из-за того, что float-элемент удаляется из потока и родитель не выделяет место под него. Решить эту проблему можно несколькими способами:
        • Поставить родителю overflow: hidden/auto.
        • Поставить родителю float.
        • Добавить в родителя элемент с clear. Чтобы не добавлять в HTML-код лишний элемент, можно задать его через :after.
      • Элементы с обтеканием обязательно должны находится в контейнерах, чтобы не влиять на соседние логические блоки.
    • Для повторяющихся элементов в строчку (пункты меню, список фотографий) отдавайте приоритет inline-block вместо float.

    Тип документа

    Создавайте новую верстку по стандарту HTML5. В старых шаблонах по возможности избавляйтесь от XHTML (слешей в конце тегов <br />, <img />).

    <!DOCTYPE html>

    Альтернативные атрибуты (alt, title)

    Очень часто разработчики забывают указывать альтернативные подписи к иконкам, кнопкам или другим графическим элементам. Наличие атрибутов title и alt в верстке поможет решить эту проблему.

    Разделение ответственности

    Держите структуру (разметка), оформление (стили) и поведение (скрипты) раздельно и постарайтесь свести взаимодействие между ними к минимуму. Убедитесь, что документы и шаблоны содержат только HTML, и что HTML служит только для задания структуры документа. Весь код, отвечающий за оформление, перенесите в файлы стилей, а код отвечающий за поведение - в скрипты.

    Отделение структуры от представления и поведения помогает облегчить поддержку кода. Изменение шаблонов и HTML-документов всегда занимает больше времени чем изменение файлов стилей или скриптов.

    Формы

    • В верстке всегда должен присутствовать тег <form>.
    • Подписи полей должны быть слинкованы к соответствующим элементам формы.
    • Должно быть показано, как выводится ошибка и/или сообщение об успешном выполнении операции.
    • Переключение между элементами формы по клавише Tab.
    • Интерактивность: смена фокуса должна визуально подсказывать пользователю активный элемент.

    Картинки

    • Используйте SVG вместо растровых изображений.
    • В случае, если SVG использовать затруднительно, используйте формат PNG.
    • Объединяйте картинки в спрайты. Это ускорит отображение страницы.
    • Называйте файлы с указанием имени модуля и блока, к которому относится изображение. Для разделения слов используйте дефис (-).

      Плохо:

      .crm-lead-history {
      	background-image: url(images/sprite.svg);
      }
      
      .crm-lead-log {
      	background-image: url(images/arrow_left.svg);
      }

      Хорошо:

      .crm-lead-history {
      	background-image: url(images/crm-lead-sprite.svg);
      }
      
      .crm-lead-log {
      	background-image: url(images/crm-lead-arrow-left.svg);
      }

    Правила форматирования HTML

    Новая строка для каждого блочного или табличного элемента

    Выделяйте новую строку для каждого блочного элемента, ставьте отступы для каждого дочернего элемента.

    Плохо:
    <div class="menu">
    <div class="menu-item"><span class="menu-item-title">Home</span></div><div class="menu-item"><span class="menu-item-title">Search</span></div>
    </div><div class="breadcrumb"></div>
    Хорошо:
    <div class="menu">
    	<div class="menu-item">
    		<span class="menu-item-title">Home</span>
    	</div>
    	<div class="menu-item">
    		<span class="menu-item-title">Search</span>
    	</div>
    </div>
    <div class="breadcrumb"></div>

    Двойные кавычки в атрибутах

    Значения HTML-атрибутов всегда пишутся в двойных кавычках.

    Плохо:
    <div class='navigation'>
        <div class=menu></div>
    </div>
    Хорошо:
    <div class="navigation">
        <div class="menu"></div>
    </div>

    Множественные атрибуты

    Теги с большим количеством атрибутов можно переносить на новые строки. Это улучшит читаемость кода, а также облегчит сравнивание файлов в системе контроля версий (diff).

    Атрибуты в одну строку:

    <a class="task-detail-special-link" id="task-detail-special-link-id" href="" title="Special Link" data-id="13">
        <span class="task-detail-special-text">Link</span>
    </a>

    Атрибуты с разбивкой на две строки:

    <a class="task-detail-special-link" id="task-detail-special-link-id"
       href="" title="Special Link" data-id="13">
          <span class="task-detail-special-text">Link</span>
    </a>

    Сортировка атрибутов

    HTML-атрибуты должны быть перечислены в следующем порядке:

    1. class
    2. id
    3. атрибуты тега (src, href, title и другие.)
    4. data-*

    Правила оформления CSS

    Каскадные селекторы

    Любой блок на странице должен описываться селектором класса. Не используйте каскадные селекторы так как они нарушают принцип независимости компонентов. Это же правило относится к селекторам прямого потомка (>) и соседнего элемента (+).

    Исключения:

    • Селекторы состояний (:visited, :active, :hover).
    • Селекторы контекста. Существуют случаи, когда компонент используется на разных страницах или в разном окружении. В этом случае удобно использовать селекторы контекста. Важно, чтобы эти контекстные стили содержались в родительском компоненте.
    • Верстка, в которой нельзя изменить структуру HTML-тегов.

    Именование классов

    • Имена классов записываются на английском языке в нижнем регистре. Если не уверены в написании английского слова, проверьте его по словарю. PHPStorm имеет встроенную проверку орфографии.
    • Желательно, чтобы терминология в названии класса совпадала с названиями, которыми оперирует разработчик (названия полей в базе данных, названия PHP-классов).
    • Для разделения слов в именах используется дефис (-).
    • Создавайте имена CSS-селекторов максимально информативными и понятными. Это поможет упростить разработку и отладку кода. Не стесняйтесь использовать длинные названия классов.
    • Название класса должно отражать суть блока, а не его внешний вид.
    • Первым словом в названии класса должно идти название модуля. На втором месте - название блока. Название блока может состоять из нескольких слов. Такой формат, с одной стороны, сразу показывает принадлежность стилей, с другой - гарантирует их уникальность. Уникальность стилей обеспечивает независимость верстки компонентов.
      <div class="task-detail">
      	<div class="task-detail-title"></div>
      	<div class="task-detail-description"></div>
      </div>
      <div class="crm-lead-form">
      	<div class="crm-lead-form-title"></div>
      	<div class="crm-lead-form-description"></div>
      </div>
    • Вложенные элементы должны сохранять контекст именования блока. Блок не обязательно должен быть прямым родителем элемента.

      Оба варианта именования допустимы:

      <div class="disk-user">
      	<div class="disk-user-info">
      		<div class="disk-user-avatar"></div>
      		<div class="disk-user-name"></div>
      	</div>
      </div>
      <div class="disk-user">
      	<div class="disk-user-info">
      		<div class="disk-user-info-avatar"></div>
      		<div class="disk-user-info-name"></div>
      	</div>
      </div>
    • Не допускайте сокращений. Сокращения могут привести к ситуации, когда одинаковые вещи будут названы по-разному (btn/button).
    • Не используйте старый префикс bx-.
    • Если к элементу происходит обращение по классу через JavaScript, то такой класс должен иметь префикс js-.
    • Для верстки шаблона сайта допускается опускать префикс модуля.

    Inline-стили

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

    Селектор тега и селектор на атрибут

    Избегайте использование имен классов с селектором тега и атрибута.

    Плохо:

    div.task-detail-title {
        margin: 20px;
    }
    
    input[name=user] {
        margin: 20px;
    }
    Хорошо:
    .task-detail-title {
    	margin: 20px;
    }
    
    .task-detail-user-field {
    	margin: 20px;
    }

    Сокращенная форма записи свойств

    CSS предлагает множество различных сокращенных форм записи (например, margin, padding, border и другие), которые рекомендуется использовать везде где это возможно, даже если задается только одно из значений. Использование сокращенной записи свойств полезно для большей эффективности и лучшего понимания кода.

    Плохо:

    .meeting-sidebar {
    	margin-top: 5px;
    	margin-bottom: 3px;
    	border-width: 1px;
    	border-style: solid;
    	border-color: #fff;
    }

    Хорошо:

    .meeting-sidebar {
    	margin: 5px 0 3px;
    	border: 1px solid #fff;
    }

    Сокращенные формы для шрифта (font) и фона (background) допускается разделять на несколько свойств.

    Единицы измерения

    Используйте значения в пикселях. Размеры, указанные в px, абсолютные, четкие, понятные и не зависят ни от чего. Допустимо использовать проценты % везде, кроме размера текста (font-size).

    0 и единицы измерения

    Не указывайте единицы измерения для нулевых значений.

    Плохо:
    .timeman-task-list {
        margin: 0px 12px 0px 13px;
    }
    Хорошо:
    .timeman-task-list {
    	margin: 0 12px 0 13px;}

    Кавычки в ссылках

    Не используйте кавычки с функцией url(), кроме случая, когда ссылка содержит пробел, либо формируется программно (обычно при использовании inline-стилей).

    Плохо:
    .im-user-status {
        background: url("images/im-sprite.png");
    }
    Хорошо:
    .im-user-status {
    	background: url(images/im-sprite.png);
    }

    Цвет

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

    Плохо:
    .disk-invitation-popup {
        background: #FFFFFF;
    }
    Хорошо:
    .disk-invitation-popup {
    	background: #fff;
    }

    Хаки

    Избегайте хаков в CSS-коде. Если все таки требуется сделать уникальные свойства для конкретного браузера, воспользуйтесь специальными классами .bx-chrome, .bx-firefox, .bx-ie10.

    Плохо:
    *+ html .vote-answers {
        margin: 5px;
    }
    Хорошо:
    .bx-ie7 .vote-answers {
    	margin: 8px;
    }

    Группировка правил

    Старайтесь, чтобы селекторы, описывающие определенный блок верстки, находились в одном месте и не были раскиданы по CSS-файлу.

    Псевдоселекторы (:before, :after)

    Псевдоселекторы позволяют не нагромождать HTML-код лишними элементами, имеющими декоративное назначение. Важно понимать, что этих элементов нет в DOM и обратиться к ним через JavaScript невозможно.

    Сброс стилей (CSS Reset)

    Не используйте CSS Reset в верстке. Компоненты являются независимыми блоками. На них не должны влиять CSS-правила, созданные для всей страницы. Это нарушает независимость блоков и затрудняет их повторное использование. Общий сброс стилей реализуется с помощью глобальных CSS-правил, которые в большинстве случаев пишутся к селекторам на тег, что нежелательно использовать в верстке. Кроме того, сброс стилей влияет на производительность рендеринга страницы и может конфликтовать с версткой клиентов.

    Браузерные префиксы

    Свойство без префикса всегда должно быть в конце.

    Плохо:
    .menu-item {
        -webkit-box-shadow:0 0 10px 20px #000;
        box-shadow:0 0 10px 20px #000;
        -moz-box-shadow:0 0 10px 20px #000;
    }
    Хорошо:
    .menu-item {
    	-webkit-box-shadow:0 0 10px 20px #000;
    	-moz-box-shadow:0 0 10px 20px #000;
    	box-shadow:0 0 10px 20px #000;
    }

    Селекторы состояний (selected, active и hover)

    Классы-модификаторы должны именоваться согласно общим правилам:

    <div class="menu">
    	<div class="menu-item"></div>
    	<div class="menu-item menu-item-selected"></div>
    	<div class="menu-item"></div>
    </div>

    Приоритеты селекторов и !important

    Избегайте использования модификатора приоритета !important.

    Шрифты, ссылки, цвета

    Для оформления текста всегда указывайте семейство шрифтов.

    Плохо:
    .disk-external-folder {
        font: 12px "Helvetica Neue";
    }
    Хорошо:
    .disk-external-folder {
    	font: 12px "Helvetica Neue", Helvetica, Arial, sans-serif;
    }

    Правила форматирования CSS

    • При создании правила для нескольких селекторов помещайте каждый селектор на отдельной строке.
    • Перед открывающей скобкой ставьте один пробел.
    • Внутри блока объявлений помещайте каждое объявление на отдельной строке.
    • Добавляйте один уровень отступов перед каждым объявлением.
    • Ставьте пробел после двоеточия внутри объявления.
    • Всегда ставьте точку с запятой после последнего объявления в блоке.
    • Ставьте закрывающую скобку на одной вертикальной линии с первым символом в селекторе.
    • Ставьте пробел после каждой запятой в объявлениях со множественным значением.
    • Разделяйте правила пустой строкой.
    • Сортируйте свойства по принципу: свойства, сильно влияющие на элемент, в начале, а самые незначительно влияющие - в конце:
      • Display
      • Позиционирование (position/float)
      • Боксовая модель (width/height/margin/padding/border/box-sizing)
      • Цвета и типографика
      • Остальное

      Плохо:

          .crm-lead-form { margin: 34px; color: #000; }
          .crm-lead-title, .crm-invoice-title, .crm-company-title
          {
              position: relative;
              background: #000;
              height: 15px;
              padding: 10px;
              border: 1px solid red;
              margin: 12px 0 17px;
              display: block;
              color: #fff;
              width: 15px;
          }
      Хорошо:
      	.crm-lead-form {
      		margin: 34px;
      		color: #000;
          }
      
      	.crm-lead-title,
      	.crm-invoice-title,
      	.crm-company-title {
      		display: block;
      		position: relative;
      		width: 15px;
      		height: 15px;
      		padding: 10px;
      		border: 1px solid red;
      		margin: 12px 0 17px;
      		color: #fff;
      		background: #000;
      	}
    • Для значений с пробелами (font-family, url()) и для свойства content используйте двойные кавычки.
      	.disk-user-profile {
      		font-family: "Helvetica Neue Light", Helvetica, Arial, sans-serif;
      	}
      
      	.disk-user-profile:after {
      		display: block;
      		content: "";
      	}
    • Исключения

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

      	.crm-column-title { width: 10%; }
      	.crm-column-author { width: 20%; }
      	.crm-column-actions { width: 30%; }
      
      	.menu-icon-create { background-position: 0 0; }
      	.menu-icon-delete { background-position: -15px -35px; }
      	.menu-icon-approve { background-position: -34px -35px; }
      Длинные значения свойств, разделяемые запятыми - как, например, набор градиентов или теней - могут быть помещены на отдельной строке каждое, чтобы повысить читабельность кода и сообщений в системе управления версиями. Формат записи может слегка различаться, один из вариантов приведён ниже.
      	.disk-info-popup {
      		box-shadow:
      			1px 1px 1px #000,
      			2px 2px 1px 1px #ccc inset;
      		background-image:
      			linear-gradient(#fff, #ccc),
      			linear-gradient(#f3c, #4ec);
      	}

    Тестирование верстки (чеклист)

    Следующие пункты являются обязательными перед отдачей верстки в производство.

    • Проверка верстки на соответствие макету (Pixel Perfect).
    • Браузеры:
      • Chrome
      • Firefox
      • IE Edge
      • Safari
    • Минимальное и максимальное разрешение экрана.
    • Переполнение блоков:
      • Длинный текст без пробелов
      • Длинный текст с пробелами
    • Удаление необязательных блоков, которые могут отсутствовать.
    • Retina-экран.
    • Картинки разных размеров и пропорций в допустимых пределах, которые будет допускать код.

    Особенности интеграции с Битрикс

    Шаблон сайта и компоненты

    В рамках продукта верстку можно разделить на два вида:

    • Верстка компонентов
    • Верстка шаблона сайта

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

    Шаблон сайта определяет внешний вид сайта. Включает в себя HTML и CSS для layout'а страницы (header, footer, колонки), оформление текста и другие элементы, общие для всех страниц сайта.

    Контекст

    Контексты продуктов и шаблонов сайтов

    Прежде чем приступать к работе, необходимо выяснить в каком продукте и в каком шаблоне (или шаблонах) сайта будет использоваться верстка. От контекста использования зависит следующее:

    • Стандарт HTML (HTML5, HTML4, XHTML)
    • Standard Mode и/или Quirks Mode
    • Гибкость верстки
    • Устойчивость к внешним стилям
    • Тестирование
    • Кросс-браузерность

    Существует следующие контексты использования верстки:

    • Корпоративный портал. Данный продукт поставляется с шаблоном Bitrix24. Компоненты должны хорошо отображаться в этом шаблоне.
    • Управление сайтом. Именно с помощью этого продукта клиенты создают новые сайты. Компоненты будут работать в абсолютно разных по дизайну и по верстке проектах. Как правило, партнер, интегрируя стандартный компонент на сайте, идет по пути наименьшего сопротивления. Сначала пробует изменить только CSS шаблона и если этого недостаточно - копирует шаблон и на его основе создает новый.
    • Публичные интерфейсы. Административная верхняя панель, диалоги создания и редактирования страницы - это все примеры публичных интерфейсов, которые должны хорошо отображаться в любом дизайне. Здесь важно, чтобы верстка была "пуленепробиваемой" - внешние стили шаблона не должны влиять на ее отображение.
    • Административный раздел. Представляет собой по сути отдельный сайт со своим шаблоном и интерфейсами. Однако часть этих интерфейсов могут отображаться в публичной части сайта.
    • Сайты готовых решений. Для данных сайтов создается отдельный набор шаблонов, использование которых предназначено только для соответствующего шаблона сайта.
    • Решение Интернет-магазин. Несмотря на то, что сайт интернет-магазина является готовым решением, его шаблоны используются в компонентах как стандартные (по умолчанию). Это значит, что такие шаблоны могут использоваться в любых других дизайнах.
    • Мобильное приложение. Данный продукт использует браузер на основе движка WebKit.

    Bootstrap

    Bootstrap в Bitrix Framework

    В Bitrix Framework для вёрстки шаблонов сайта и компонентов используется Bootstrap. При вёрстке шаблонов учитывайте что:

    1. В БУС подключается не собственный вариант Bootstrap, а оригинальная, не изменённая поставка фреймворка в 4-ой версии.
    2. Bootstrap подключается не в конкретном компоненте, а в целом на продукте из модуля main.
    3. В Bootstrap рекомендуют кастомизировать стили в отдельном файле, обычно он называется bootstrap-theme.css. В случае с Bitrix Framework он называется template_style.css - для основного шаблона и style.css в каждом отдельном компоненте. В этом случае выше перечисленные файлы подключатся после основной библиотеки.

    Если верстальщик не следует рекомендациям Bootstrap и нашей документации, то возможна ситуация, когда "плывёт" дизайн.


    Верстка для мобильных устройств

    Viewport

    Чтобы сообщить браузеру, что ваша страница адаптируется к любым устройствам, добавьте в заголовок документа такой метатег:

    <meta name="viewport" content="width=device-width, initial-scale=1.0">

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

    Retina-экран

    • Используйте изображения в формате SVG.
    • Для растровых изображений используйте двукратный размер и свойство background-size.

    Border в 1px

    Часто дизайнеры требуют сделать границу блока шириной в 1 физический пиксель. Если присвоить блоку свойство border: 1px solid #000, на Retin'е это будет выглядеть жирной полосой в 2 физических пикселя. Раньше эту проблему решали путем создания картинки, которой обтягивали бордер с помощью свойства border-image. А это лишнее обращение к серверу. Начиная с iOS 8 и Android 4.4, стандартные браузеры на базе WebKit поддерживают дробные свойства border: .5px solid #000. На данный момент поддержка этого способа на используемых девайсах равна 97% на iOS и 75% на Android.


    В рамках курса «BitrixMobile - создание кроссплатформенных мобильных приложений» подробно рассмотрены возможности библиотеки BXMobileApp, которая позволяет создавать сложные бизнес-приложения на BitrixMobile.

    Приёмы верстки

    В этом разделе собраны типовые подходы к решению частых задач верстки.

    Использование оптимальных изображений под разные браузеры и устройства

    WebKit поддерживает srcset атрибут изображений в img и image-set в стилях. Это позволяет вам, как разработчику, использовать картинки с высоким разрешением для пользователей использующих ретина-дисплей без ущерба для остальных пользователей.

    Обязательно задается изображение для браузеров не поддерживающих это свойство.

    .class {
    	/* задаем фоновое изображение, если браузер не поддерживает image-set */
    	background-image: url(image-set-not-supported.png);
    	/* задаем фоновые изображения, для различных разрешений */
    	background-image: -webkit-image-set(url(low-res-image.jpg) 1x, url(high-res-image.jpg) 2x);
    	background-size: cover;
    }

    Не забудьте использовать свойство background-size для того что бы большие изображения корректно отображались.

    Атрибут srcset работает аналогичным образом.

    <img src="srcset-not-supported.png" srcset="low-res-image.jpg 1x, high-res-image.jpg 2x">

    Как в первом так и во втором случае, после ссылки на изображение указывается условие при котором оно будет работать. В примерах выше в качестве условия ppi устройства. Также можно указывать разрешения экранов.


    Простейший пример внедрения дизайна с табличной вёрсткой

    Для повторения описанного примера, знакомящего с принципами внедрения шаблона дизайна в Bitrix Framework, рекомендуется использовать этот макет шаблона. Шаблон - тестовый, вопросы кроссбраузерности при его создании не рассматривались. Все работы выполнялись в Firefox.

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

    Примечание: Описанный пример использовался в опубликованных несколькими годами ранее книгах о создании сайта на "1С-Битрикс: Управление сайтом". Принципы работы за это время не изменились. Однако поменялись шаблоны в демодистрибутиве и, следовательно, описание их кастомизации.

    При создании примера подразумевалось, что пользователь знаком с PHP, HTML и CSS. Поэтому детальное описание причин удалений или добавления того или иного кода опускается. Считаем, что вы сами способны понять эти моменты. Перед началом работы настойчиво рекомендуется познакомиться с понятием о шаблоне дизайна сайта в рамках Bitrix Framework.

    Перед началом работ можно создать специальный сайт, который будет использоваться в качестве базового для шаблона, либо применять шаблон к одному из сайтов в вашей тестовой системе.

    В Примере не рассматриваются вопросы, связанные с кастомизацией ввода и обработки данных за счёт изменения логики работы системы и компонентов.

    Создание шаблона

    Создание шаблона – не самый сложный шаг в интеграции дизайна, но требует внимательности.

    Форма создания шаблона

    • Перейдите в раздел Настройки > Настройки продукта > Сайты > Шаблоны сайтов. В Рабочей области откроется форма Шаблоны сайтов.
    • Выполните команду Добавить шаблон на Контекстной панели. Откроется форма создания нового шаблона. В этой форме в поле:
      • ID - введите идентификатор. В нашем примере это будет test.
      • Название - введите название. (Может быть и на кириллице и на латинице), например, тоже test.
      • Описание - можно вставить комментарий.
      • Порядок - числовое значение определяет порядок отображения шаблона в общем списке шаблонов.

    Примечание: Без установки в шаблоне разделителя #WORK_AREA# его сохранение невозможно. Поэтому у нас сейчас последует этап работы, который вы не сможете прервать в произвольный момент.

    Назначение шаблона для сайта

    Назначьте созданный шаблон для тестового сайта.

    Добавление кода тестового дизайна в шаблон

    Служебная часть

    Задание внешнего вида шаблона происходит в поле Внешний вид шаблона сайта.

    Вставьте в поле следующий служебный код для шаблона:
    • Верхняя часть:
      <?if(!defined("B_PROLOG_INCLUDED") || B_PROLOG_INCLUDED!==true)die();
      ?><!DOCTYPE html PUBLIC "-//W3C//DTD HTML 4.01 Transitional//EN">
      <html>
      <head>
      <meta http-equiv="Content-Type" content="text/html; charset=<?echo LANG_CHARSET;?>">
      <?$APPLICATION->ShowMeta("keywords");?>
      <?$APPLICATION->ShowMeta("description");?>
      <title><?$APPLICATION->ShowTitle()?></title>
      <?$APPLICATION->ShowHead()?>
      </head>
      <body>
      
    • Нижняя часть:
      </body>
      </html>
      
    • Для того, чтобы в шаблоне подключалась Административная панель, введите код административной панели после открывающего тега body:
      <?$APPLICATION->ShowPanel();?>
    • Примечание: При редактировании уже существующего шаблона можно использовать кнопку Предпросмотр в низу формы создания шаблона.

    Добавление кода шаблона

    Код шаблона добавляем в то же поле - Внешний вид шаблона сайта.

    • Откройте в браузере файл index.html из папки с ранее скачанным архивом шаблона. В окне браузера откроется тестовый дизайн:

    Теперь перенесём html-код дизайна в поле Внешний вид шаблона сайта.

    • Откройте исходный код страницы, выделите весь код расположенный между тегами <body> </body> (без самих тегов) и скопируйте его в буфер.
    • Вставьте код в поле Внешний вид шаблона сайта перед закрывающим тегом </body> (аккуратно, не удалите служебный код показа административной панели)
    • В коде <body> шаблона нет заданных дизайнером параметров. Вставьте в этот тег параметры из аналогичного тега файла index.html: BGCOLOR="#FFFFFF" TEXT="#000000" leftmargin="0" topmargin="0" marginwidth="0" marginheight="0".

    Задание разделителя

    Установите разделитель #WORK_AREA# нажав на [dw]кнопку с соответствующим названием[/dw][di][/di]. Он должен стоять строго перед меткой <!-- #Begin_Article -->.

    Примечание: Найти тот или иной нужный участок кода на большой странице бывает достаточно сложно, но в исходном коде страницы нашего тестового шаблона расставлены специальные метки для облегчения поиска. Их общий вид:
    <!-- #Begin******** -->
    <!-- #End******** -->
    где под звездочками будет стоять название функции, которую имитирует данный код. В нашем конкретном случае это будет название _Article. То есть метка в данном случае будут иметь вид:
    <!-- #Begin_Article -->
    

    Теперь можно сохранять созданный шаблон. До сих пор это было невозможно, так как был не задан разделитель #WORK_AREA#.


    Добавление графики и стилей

    Для полного и правильного отображения дизайна нужно добавить графику и стили.

    Графика

    Теоретически картинки можно хранить в любой папке системы. Однако лучше размещать их в специальной папке самого шаблона.

    • Откройте папку, где расположен шаблон (/bitrix/templates/test).
    • Создайте в ней папку /images.

      Примечание: Поместить картинки можно и в корневой папке /images. Но размещение изображений по предлагаемому нами варианту имеет одно преимущество. При копировании (или экспорте) шаблона вам не придется заново импортировать картинки в новый шаблон. Копия создастся сразу с папкой с картинками.

    • Загрузите графические файлы из архива дизайна. Файлы размещены в папке Сайт\izo\

    Теперь нам осталось только изменить пути до картинок в кодах шаблона. В исходном дизайне пути до картинок прописаны как izo/. Поменяйте их на /bitrix/templates/test/images/.

    Примечание: Ручная замена утомительна. Произведите такую замену в текстовом редакторе с функцией автоматической замены (Bred2, AkelPad и подобных, но не в MS Word), либо в используемой вам среде разработки типа PHP storm.

    • Сохраните изменения.
    • Откройте сайт с помощью кнопки Предпросмотр.

    Вы увидите, что теперь все картинки, кроме картинок в колонках Блоги и Галерея, а так же картинки в теле статьи «О прикорме», отображаются. При этом не прорисованы границы, и на сайте отображен текст (перечисление разделов), видео и картинки не предусмотренные в дизайне.

    Картинки в колонках Блоги и Галерея, а так же картинка в теле статьи «О прикроме» не отображаются потому, что они не загружены на сайт. (Они были расположены не в папке izo.) На это можно не обращать внимание, так как все равно нам удалять участки кода с этими картинками и заменять их на компоненты «1С-Битрикс: Управление сайтом».

    К ненужному тексту, видео и картинкам мы еще вернемся позже. (Кстати, появившийся «самостоятельно» текст может быть совсем не таким, как на нашей иллюстрации – это зависит от того какому сайту с каким содержанием вы применили шаблон.)

    Стили

    • Откройте на редактирование шаблон сайта на закладке Стили шаблона.

      Вы увидите, что поле Файл стилей шаблона (template_styles.css) пустое, то есть не задано никаких стилей. Стили применяемые в проекте указаны в файле Сайт\izo\code.css.

    • Перенесите стили из файла в это поле.
    • Сохраните изменения.

    Результат:


    Интеграция дизайна в систему

    Шаблон сайта на четверть создан. «На четверть» - это потому что в имеющемся у нас на данном моменте шаблоне есть только html-код, но отсутствуют функции вызова программных компонентов. А их интеграция – это существенный объем работы.

    Статическая информация, которая не нуждается (либо редко нуждается) в замене, как правило, размещается в статических зонах Footer’а и Header’а. Заменить ее можно в кодах самих файлов, но делать это придется квалифицированному разработчику. Либо разработчик должен организовать эту замену с помощью компонентов системы силами контент-менеджеров сайта.

    Примечание: Хорошим тоном в последнее время считается размещение всей информации с помощью тех или иных компонентов, оставив на долю шаблона только графическое отображение сайта.

    Компоненты существенно облегчают работу по поддержке сайта. Однако работа по их внедрению в шаблон сайта требует квалификации. Чем шире применяются компоненты, тем более трудоемка и сложна работа по созданию сайта, но проще его поддержка.

    Интегрировать код любого компонента в код шаблона в Bitrix Framework можно следующими способами:

    • Вставкой кода из [dw]пользовательской документации[/dw][di]Нажмите на рисунок, чтобы увеличить

      Пользовательская документация .[/di] ;
    • С помощью визуального редактора напрямую (невозможно с версии 14.0, так как отключено использование редактора при редактировании шаблонов сайта).

    Внедрение компонентов в дизайн можно, в принципе, производить в любом порядке. Мы начнем с компонента авторизации. Но сначала разберёмся с тем, что же такое Рабочая область.

    #WORK_AREA# – Рабочая область

    #WORK_AREA# - основная рабочая область. Рабочая область страницы, в которой размещаются собственно информационные материалы сайта. В качестве Основной рабочей области может подключаться как физический файл, так и создаваемый системой на основе комплексных компонентов, динамический код.

    Поясним суть Рабочей области #WORK_AREA#. При создании шаблона мы загружали готовую нарезку дизайна, в которой предусмотрен текст в центральной части, колонка в правой части (фотогалерея), левая колонка с анонсами блогов. На данный момент все они: текст, левая и правая колонки входят что-то в Header, что-то в Footer. Если при формировании шаблона левая и правая колонки могут то включаться в Footer или Header, то исключаться из него, но основной текст всегда будет в #WORK_AREA#.

    Если вы создавали сайт для тестов на установке демо-дистрибутива по способу описанному здесь, то на вашем сайте отобразятся именно те лишние элементы, которые мы называли ранее. Это происходит потому, что используется та же папка для сайта, что и в демо-версии. Соответственно, отображаются элементы уже размещённые на странице index.php.

    • Скопируйте в буфер обмена текст статьи, которая у нас сейчас видна на шаблоне. Только текст, без картинки!
    • С Публичной части откройте для редактирования индексную страницу сайта в визуальном редакторе. (Либо откройте для редактирования файл index.php в корне сайта)
    • Удалите из страницы все тексты и ярлыки компонентов. Кстати, этим действием вы удалили весь тот «непрошенный» текст, видео и картинки, которые появились у нас на сайте после вставки дизайна в шаблон.
    • В самом начале страницы введите текст: Тестовый проект. Главная страница.
    • В поле редактора вставьте текст из буфера обмена.
    • Сохраните внесенные изменения.

    Вы увидите, что статья у нас теперь отражена дважды: один раз как часть #WORK_AREA# (то, что мы только что добавили), второй раз как часть Footer’а (текст, который стоял раньше):

    Вот из Footer’а нам и надо текст убрать.

    • Удалите текст из кода шаблона сайта, вместе со строкой и ячейкой. Метки:
      <!-- #Begin_Article -->
      <!-- #End_Article -->
    • Сохраните внесенные изменения.
    • Теперь при просмотре сайта с публички текст будет отображаться только один раз и он выводится из Рабочей области #WORK_AREA#.

    Авторизация

    Переход на страницу авторизации в «1С-Битрикс: Управление сайтом» можно реализовать как с помощью html, так и с помощью компонента авторизации. Мы опишем оба способа.

    Авторизация с помощью HTML кода

    За авторизацию в html-коде тестового дизайна отвечает код:

    <img width="4" height="4" src="/bitrix/templates/test/images/punkt_top.gif" /> 
           <b><a href="#" class="text"><font size="1" color="#6e6e6e">Регистрация</font></a> 
    <img width="4" height="4" src="/bitrix/templates/test/images/punkt_top.gif" />
           <b><a href="#" class="text"><font size="1" color="#6e6e6e">Вход</font></a></b>

    Найти этот код довольно просто по меткам:

    <!-- #Begin_Auth -->
    <!-- #End_Auth -->

    Чтобы реализовать авторизацию в html:

    • Замените знак # во втором теге <a href="#" class="text"> на ссылку на /auth.
    • Замените в первой ссылке знак # на /auth.php?register=yes
    • Сохраните внесенные изменения.

    Авторизация встроена. Можете закончить сессию и вновь авторизоваться для проверки работы.

    Компонент авторизации

    Интеграция компонента авторизации не сложна, но требует внимания.

    • Удалите описанный выше html-код из общего кода шаблона. Метки:
      <!-- #Begin_Auth -->
      <!-- #End_Auth -->
    • Откройте страницу [comp include_system_auth_form]Компонента авторизации[/comp] в пользовательской документации.
    • Скопируйте в буфер код вызова компонента.
    • Вставьте код между указанными метками.
    • Настройте компонент:
      • У параметра REGISTER_URL укажите значение /auth/ (системный путь к странице регистрации в дистрибутиве по умолчанию).
      • У параметра PROFILE_URL укажите значение /personal/profile/ (ссылка на персональный раздел в дистрибутиве по умолчанию).
    • Сохраните внесенные изменения.

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

    Несоответствия в выводе данных

    Дизайнером по умолчанию подразумевалось всплывающее окно авторизации. В текущем дистрибутиве нет такого шаблона. При желании можете создать собственный шаблон, но мы воспользуемся одним уже созданным. Он расположен в папке auth в архиве дизайна сайта.

    • Скопируйте папку auth в папку \bitrix\templates\test\components\bitrix\system.auth.form\.
    • Откройте на редактирование шаблон сайта.
    • В коде компонента system.auth.form установите название шаблона: auth (в кавычках после названия компонента):
      <?$APPLICATION->IncludeComponent("bitrix:system.auth.form","auth",Array(
      	"REGISTER_URL" => "/auth/",
      		"FORGOT_PASSWORD_URL" => "",
      	"PROFILE_URL" => "/personal/profile/",
      	"SHOW_ERRORS" => "Y" 
      	)
      );?>
    • Сохраните внесенные изменения.

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

    Какие расхождения мы видим?

    • Не соответствуют местами порядок ссылок. Сначала Войти, потом – Регистрация, вместо обратного, как в дизайне;
    • Само слово Войти не соответствует утвержденному Вход;
    • Не соответствуют шрифты и их форматирование в надписях;
    • Не соответствуют картинки.
    • всплывающее окно авторизации тоже «не вписывается» в дизайн.

    Кастомизация шаблона компонента

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

    Прежде всего, оговоримся, что не все неточности дизайна мы будем исправлять в этом примере. Порядок ссылок невозможно изменить, не модифицируя php-код самого компонента. Это не задача нашего примера и мы этим заниматься не будем.

    Для замены слова Войти на Вход необходимо найти файл в системе, где задается эта надпись и поменять ее.

    • Перейдите в раздел Контент > Структура сайта > Файлы и папки /bitrix/templates/test/components/bitrix/system.auth.form/auth/lang/ru/. В этой папке есть файл: template.php.

      Примечание: Обратите внимание на определенную логику расположения информации в файлах компонента. Вот она: system.auth.form/auth/lang/ru/. Эта структура типична для всех компонентов системы: имя_компонента/применяемый_шаблон/языковая_папка/конкретный_язык. В папке конкретного языка расположены все текстовые значения, используемые в этом шаблоне. Так вы можете поменять тексты в любом компоненте.

    • В колонке действий для строки этого файла выберите Редактировать как PHP. Откроется форма редактирования файла:

      В самой первой строке мы видим слово «Войти», которое прописано для параметра AUTH_LOGIN_BUTTON. Именно это слово и выводится в форме авторизации.
    • Замените Войти на Вход.

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

    • Сохраните внесенные изменения.

    Текст шаблона адаптирован к требованиям дизайна. Теперь займемся внешним видом.

    • Откройте для редактирования шаблон компонента. Нам нужно заменить картинки. Работа упрощается тем, что в тестовом дизайне использована одна картинка на все три позиции. Если бы картинки были разные, то нам бы пришлось повторять описываемую ниже операцию несколько раз, меняя названия картинок.

      Если посмотреть исходный html-код авторизации, то видно, что имя картинки в этом коде: punkt_top.gif. Эта картинка после загрузки на тестовый сайт расположена в папке /bitrix/templates/test/images. Значит, путь до картинок в рамках системы будет такой: /bitrix/templates/test/images/punkt_top.gif.

    • В диалоге с кодом шаблона /bitrix/templates/test/components/bitrix/system.auth.form/auth/template.php найдите ссылки на картинки, используемые в шаблоне. Так как в теги картинки включен php-код и найти их будет не просто, то приведем ссылки на картинки полностью.
      Первая и вторая:
      <img src="<?=$templateFolder?>/images/login.gif" width="10" height="11" border="0" alt="">
      Третья:
      <input type="image" src="<?=$templateFolder?>/images/login.gif" alt="<?=GetMessage("AUTH_LOGOUT_BUTTON")?>">
    • Замените в шаблоне пути до всех картинок с тех, что прописаны в коде, на указанный выше адрес картинки, то есть у вас должно получиться.
      Первая и вторая:
      <img src="<?=SITE_TEMPLATE_PATH?>/images/punkt_top.gif" border="0" alt="">
      Третья:
      <input type="image" src="<?=SITE_TEMPLATE_PATH?>/images/punkt_top.gif" alt="<?=GetMessage("AUTH_LOGOUT_BUTTON")?>">
    • Сохраните внесенные изменения.

    Завершите сессию и авторизуйтесь заново. Вы увидите, что картинки заменены. Теперь займемся остальным. Начнем с всплывающего окна авторизации. Что нас не устраивает в нем? Фон, отличающийся от фона сайта. Кому-то еще может показаться, что не соответствует размер окна или еще что. Эти вопросы – дело вкуса и мы оставляем на ваше усмотрение изменение этих параметров.

    • Откройте для редактирования шаблон компонента. Просматривая шаблон компонента, мы увидим, что за оформление всплывающего окна отвечает стиль login-form-window: код этого окна расположен в теге <div id="login-form-window"> </div>. В то время как на надписи Вход и Регистрация стиль не задан. Следовательно, нам надо задать стиль для этих надписей. А стиль login-form-window изменить так, чтобы он соответствовал дизайну.
    • Откройте для редактирования CSS файл шаблона компонента Авторизация.
    • Откройте исходные коды тестового сайта. Дизайнер задал фон таблиц в цвете #DEDEE2, а границы в цвете #bfbfbf. Например, ячейка:
      <td class="br" bgcolor="#DEDEE2" bordercolor="#bfbfbf">
      ...
      </td>
      Воспользуемся этими же цветами и мы.
    • Замените цвет в строке background:#F5F5ED на background: #DEDEE2.
    • Замените цвет в строке border:1px solid #000; на border:1px solid #bfbfbf;.
    • Сохраните внесенные изменения.

    Завершите сессию и авторизуйтесь заново. Вы увидите, что вид окна изменился и стал соответствовать общей цветовой гамме сайта.

    Теперь нам нужно изменить вид надписей Вход и Регистрация. Сделать это можно разными способами. Но мы воспользуемся опять же файлом стилей. Пусть стиль, отвечающий за оформление этих надписей называется login-info. Соответственно, стиль активной ссылки будет #login-info а.

    • Откройте для редактирования шаблон компонента. Установим теги для стиля login-info. Установить эти теги нам придется два раза, так как html-код не должен пересекаться с php-кодом.
    • Установите тег <div id="login-info"> первый раз сразу за тегом </div>, закрывающим предыдущий стиль login-form-window.
    • Установите тег </div>, закрывающий использование стиля перед php-кодом <?else:?>.
    • Установите тег <div id="login-info"> второй раз сразу за тегом <?else:?>.
    • Установите тег </div>, закрывающий использование стиля перед завершающим php-кодом <?endif?>.
    • Сохраните внесенные изменения.

    Мы прописали место, где должны будут использоваться стили для шрифта. Теперь надо создать сами стили.

    • Откройте для редактирования файл CSS компонента.
    • Добавьте в файл стилей строки:
      	#login-info {color:#3B3B3B; font-size:70%; font-weight:bold;}
      	#login-info a {color: #666666;}
    • Сохраните внесенные изменения.

    Завершите сеанс и авторизуйтесь вновь. Вы увидите, что надписи изменили шрифт, размер и цвет. Теперь они почти полностью соответствуют утвержденному дизайну.

    Мы закончили встраивание модуля авторизации и теперь можем двигаться дальше и заняться меню сайта.

    Меню сайта

    Меню бывают разные. Бывают горизонтальные и вертикальные, левые и правые, верхние и нижние. Чаще всего встречаются левые вертикальные меню и верхние горизонтальные. В нашем дизайне встречаются три вида меню:

    • Левое вертикальное, классическое;
    • Нижнее горизонтальное;
    • Верхнее горизонтальное – частичное.

    Для выполнения работ вам нужно будет создать два типа меню, которых нет в дистрибутиве по умолчанию. Эти типы меню будут использоваться в примере. В примере их назвали podmenu и down.

    Работу начнем с левого вертикального меню.

    Левое меню

    Откройте для редактирования шаблон сайта. Первым делом нужно определить часть html-кода, отвечающего за один элемент меню. В нашем тестовом дизайне код, отвечающий за меню расположен между:

    <!-- #Begin_Left_Menu -->
    <!-- #End_Left_Menu -->

    Добавление компонента

    • Скопируйте код, имитирующий меню в отдельный текстовый файлик. Он нам понадобится.
    • Удалите из кода шаблона код меню.
    • Откройте страницу документации с описанием компонента [comp include_menu]Меню[/comp] (bitrix:menu).
    • Перенесите код вызова компонента в шаблон сайта между указанными метками.
    • Сохраните внесенные изменения.

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

    Меню совершенно не подходит по оформлению. Слева что получилось, справа - что должно быть:

    Кастомизация шаблона

    • Скопируйте шаблон компонента Меню в папку тестового шаблона. Откроется форма для редактирования шаблона
    • В шаблоне компонента само меню строится через применение списка с помощью тегов <ul> <li>элемент маркированного списка</li> </ul>. Мы от списка откажемся, у нас само меню будет строиться через таблицы с помощью тегов таблиц. Поэтому удалите теги <ul>, </ul>. Позже в шаблоне компонента мы заменим и теги <li>, </li>.
    • Удалите атрибут с указанием на стиль class="left-menu", так как мы его применять не будем.
    • Откройте текстовый файл, где вы сохраняли вырезанный html-код таблиц меню оригинального дизайна. Каждый пункт меню - это отдельная таблица. Посмотрим код оформления пункта меню Главная:
      <table width="99%" cellspacing="0" cellpadding="0" border="0"> 
      	<tbody> 
      		<tr>
      			<td width="25" height="25" background="/bitrix/templates/test/images/fot_tabl_centr.jpg"><img src="/bitrix/templates/test/images/ukazatel_red.gif" width="25" height="25"  /></td> 
      			<td width="1" valign="top" height="25" background="/bitrix/templates/test/images/fot_tabl_c.gif"><img src="/bitrix/templates/test/images/fot_tabl_w.gif" width="2" height="2"  /></td> 
      			<td height="25" background="/bitrix/templates/test/images/fot_tabl_centr.jpg"><img src="/bitrix/templates/test/images/pix.gif" width="10" height="10"  /><b><a href="#" class="text" ><font size="2" color="#666666">Главная</font></a></b></td> 
      			<td width="1" valign="top" height="25" background="/bitrix/templates/test/images/fot_tabl_c.gif"><img src="/bitrix/templates/test/images/fot_tabl_w.gif" width="2" height="2"/></td> 
      		</tr>
                         
      		<tr>
      			<td height="1" background="/bitrix/templates/test/images/fot_tabl_c.gif"><img src="/bitrix/templates/test/images/fot_tabl_w.gif" width="2" height="2"/></td>
      			<td width="1" height="1"><img src="/bitrix/templates/test/images/fot_tabl_w.gif" width="2" height="2"/></td> 
      			<td height="1" background="/bitrix/templates/test/images/fot_tabl_c.gif"><img src="/bitrix/templates/test/images/fot_tabl_w.gif" width="2" height="2"/></td> 
      			<td width="1" height="1"><img src="/bitrix/templates/test/images/fot_tabl_c.gif" width="2" height="2"/></td> 
      		</tr>
      	</tbody>
      </table>
      HTML-код представляет собой таблицу с двумя строками. Вторая строка – декоративная. Сама ссылка должна формироваться в первой строке, если быть точным в третьей ячейке первой строки. В эту ячейку и надо нам отредактировать.
    • Удалите из третьей ячейки из строки <b><a href="#" class="text"><font size="2" color="#666666">Главная</font></a></b> теги <b></b>; а также часть кода <font size="2" color="#666666">Главная</font>. Мы удалили параметры форматирования текста, которые будут определяться теперь не шаблоном, а стилями.
    • Замените параметр class="text" на class="left-menu". Этим мы указали название стиля, который должен применяться для названий разделов в меню. Теперь нам нужно заменить # - символ адреса ссылки на php-код, объясняющий системе, где искать название раздела и ссылку на него.
    • Структура шаблона

      Просматривая структуру шаблона можно увидеть, как отображаются в php-коде шаблона ссылки, на которые происходит переход при выборе пункта меню. Они располагаются между html-тегами <LI>, которые определяют отдельный элемент списка. Элементов списка – два, так как в шаблоне предусмотрено выделение активной ссылки. В исходном дизайне такое выделение не предусмотрено. Это значит, что мы должны объединить представление выбранной и простой ссылки в php-коде.

      То есть, вместо
      <?if($arItem["SELECTED"]):?>
      <li><a href="<?=$arItem["LINK"]?>" class="selected"><?=$arItem["TEXT"]?></a></li>
      	<?else:?>
      <li><a href="<?=$arItem["LINK"]?>"><?=$arItem["TEXT"]?></a></li>
      	<?endif?>
      php-код, вызывающий ссылку и текст должен стать таким:
      <?=$arItem["LINK"]?>"<?if($arItem["SELECTED"]):?><?else:?><?endif?>"><?=$arItem["TEXT"]?>
      

      В коде исходного дизайна вместо # поставьте указанный php-код.

    Теперь можно переходить к переносу html-кодов, определяющих внешний вид строки меню. PHP-код и html-код не должны пересекаться между собой, а должны быть включены друг в друга. Потому нам нужно сейчас очень внимательно подходить к размещению тегов в шаблоне. Прежде всего, разместим теги таблицы. Так как меню должно формироваться внутри таблицы, значит php-код, запускающий цикл, должен лежать внутри таблицы.

    • Вставьте открывающий тег <table width="99%" cellspacing="0" cellpadding="0" border="0"> перед php функцией foreach, задающей цикл.
    • Вставьте тег </table>, закрывающий таблицу сразу за функцией endforeach, завершающей php-цикл.

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

    Так как мы уже интегрировали код собственно php-цикла в html-код, то нам осталось только вставить полученную html-конструкция его между тегами начала и конца php-цикла.

    • Выделите в исходном html-коде дизайна меню теги строк таблицы:
      <tr> 
      	<td width="25" height="25" background="/bitrix/templates/test/images/fot_tabl_centr.jpg"><img width="25" height="25" src="/bitrix/templates/test/images/ukazatel_red.gif" /></td> 
      	<td width="1" valign="top" height="25" background="/bitrix/templates/test/images/fot_tabl_c.gif"><img width="2" height="2" src="/bitrix/templates/test/images/fot_tabl_w.gif" /></td>
      	<td height="25" background="/bitrix/templates/test/images/fot_tabl_centr.jpg"><img width="10" height="10" src="/bitrix/templates/test/images/pix.gif" /><a class="left-menu" href="<?=$arItem["LINK"]?>"<?if($arItem["SELECTED"]):?><?else:?> <?endif?>"> <?=$arItem["TEXT"]?> </a></td>    
      	<td width="1" valign="top" height="25" background="/bitrix/templates/test/images/fot_tabl_c.gif"><img width="2" height="2" src="/bitrix/templates/test/images/fot_tabl_w.gif" /></td>
      </tr>
      <tr>
      	<td height="1" background="/bitrix/templates/test/images/fot_tabl_c.gif"><img width="2" height="2" src="/bitrix/templates/test/images/fot_tabl_w.gif" /></td>
      	<td width="1" height="1"><img width="2" height="2" src="/bitrix/templates/test/images/fot_tabl_w.gif" /></td>
      	<td height="1" background="/bitrix/templates/test/images/fot_tabl_c.gif"><img width="2" height="2" src="/bitrix/templates/test/images/fot_tabl_w.gif" /></td>
      	<td width="1" height="1"><img width="2" height="2" src="/bitrix/templates/test/images/fot_tabl_c.gif" /></td>
      </tr>
    • Замените этим кодом следующий код:
      <?if($arItem["SELECTED"]):?>
      <li><a href="<?=$arItem["LINK"]?>" class="selected"><?=$arItem["TEXT"]?></a></li>
      <?else:?>
      <li><a href="<?=$arItem["LINK"]?>"><?=$arItem["TEXT"]?></a></li>
      <?endif?>
    • Сохраните внесенные изменения.

    Задание стилей

    • Откройте для редактирования файл с CSS стилями шаблона. В этом файле видно для вывода текста используется стиль left-menu.
    • Переименуйте стиль ul.left-menu в просто .left-menu.
    • Вставьте в параметры стиля left-menu следующие строки:
      font-size:80%;
      color:#666666;
      font-weight:bold;
      text-decoration:none;
    • Так как у нас по дизайну не предусмотрены другие стили оформления (активной ссылки, посещенной ссылки, подсветки ссылки), то все остальные стили просто удалите.
    • Сохраните внесенные изменения.

    Результат работы:

    Нижнее горизонтальное меню

    Левое вертикальное меню у нас будет меняться в зависимости от открытого раздела. Но для упрощения перемещения между разделами заказчик пожелал сохранить возможность доступа к любому разделу с любой страницы сайта. С этой целью нужно иметь одно меню, где-нибудь в не очень мешающемся месте, в котором будут выводиться ссылки на все основные разделы сайта. Таким у нас будет нижнее горизонтальное меню.

    Добавление компонента

    • Откройте для редактирования шаблон тестового сайта.
    • Скопируйте в отдельный текстовый файл а затем удалите код таблицы, расположенной между метками:
      <!-- #Begin_down_Menu -->
      <!-- #End_down_Menu -->
    • Перейдите в режим визуального редактирования.
    • Добавьте на то место, где находилась эта таблица компонент Меню так, как мы это уже делали с левым меню.
    • Откройте для редактирования диалог настроек параметров компонента. В отличие от левого меню здесь нам нужно их настроить.
    • В поле Тип меню для первого уровня выберите down (Нижнее).

      Примечание: Такого типа меню в дистрибутиве по умолчанию нет. Мы предполагаем что вы его создали заранее.

    • Сохраните внесенные изменения.
    • Скопируйте в место между этими тегами код вызова компонента Меню, так же как и в случае с левым меню.
    • Параметру ROOT_MENU_TYPE установите значение down. Этим мы назначили новый тип меню для этого компонента.
    • Сохраните внесенные изменения.

    Создание меню

    Создайте нижнее меню. Оно должно повторять разделы левого.

    После этого вы увидите на месте нижнего горизонтального меню то же самое системное меню по умолчанию. Нам надо его модифицировать. Но опять же, системный шаблон лучше не трогать.

    Кастомизация шаблона компонента

    • Скопируйте шаблон компонента по умолчанию в текущий шаблон сайта, как мы это делали с левым меню.
    • Откройте для редактирования шаблон компонента.
    • Из скопированного исходного кода вставьте в шаблон вместо кода <ul class="left-menu"> открывающий код таблицы:
      <table width="1024" height="10" cellspacing="0" cellpadding="0" border="0" class="text"> 
    • Установите закрывающий тег таблицы вместо кода </ul>
    • Замените class="text" на class="down-menu". (Пусть такое имя будет у стилей для нижнего меню).
    • Перенесите этот параметр class="down-menu" в строку
      <a href="<?=$arItem["LINK"]?>"><?=$arItem["TEXT"]?></a>
      после открывающего тега <a>/
    • Повторите последние действия для строки
      <a href="<?=$arItem["LINK"]?>" class="selected"><?=$arItem["TEXT"]?></a>
    • В параметре class="down-menu" этой строки добавьте букву а для стиля активной ссылки.
    • Задайте открывающий тег строки таблицы сразу за открывающим тегом таблицы и закрывающий тег строки перед закрывающим тегом таблицы.
    • Задайте открывающий тег ячейки с параметром align="center" на месте открывающего тега <li> отдельного элемента списка. И задайте закрывающий тег ячейки на место закрывающего тега отдельного элемента списка.
    • Удалите лишнюю пару тегов отдельного элемента списка
    • Добавьте код картинки из текстового файла с исходным кодом (<img src="/bitrix/templates/test/images/punkt_top.gif"/>) в код шаблона после тега <td align="center">.
    • Сохраните внесенные изменения.

    Кастомизация стилей

    • Откройте для редактирования стили шаблона компонента.
    • Отредактируйте файл стилей по аналогии с тем, как это делалось в левом меню.

    Результат работы:

    Верхнее частичное меню

    Заказчиком проекта частичное меню задумано как способ быстрого доступа к внутренним разделам и страницам, не отображаемым на левом и нижнем меню. Такой способ удобен, когда у вас новостей настолько много, что анонс статьи довольно быстро уйдет с индексной страницы. Скажем, у вас появилась статья, которая может представлять интерес для многих читателей в течение длительного времени. Выносить в отдельный раздел ее нет смысла, но привлечь к ней внимание стоит. Вы можете сделать ссылку на эту статью из верхнего частичного меню и держать ее на индексной странице столько, сколько сочтете нужным.

    Механизм интеграции этого меню абсолютно идентичен механизму интеграции, описанному выше. За исключением пары моментов:необходимо использовать отдельный тип меню (part), используется другая картинка.

    • Разместите код вызова компонента меню между метками:
      <!-- #Begin_part_menu -->
      <!-- #End_part_menu -->
    • Скопируйте шаблон компонента и файл css из шаблона нижнего меню;
    • Замените в шаблоне название картинки (punkt_top на punkt_red.)
    • Сохраните внесенные изменения.
    • Создайте меню.

    Цепочка навигации

    Цепочка навигации позволяет пользователю ориентироваться, где на сайте он находится в данный момент. Она очень полезна при разветвленной структуре сайта.

    Размещение компонента

    • Откройте для редактирования шаблон тестового сайта.
    • Найдите и удалите код, имитирующий цепочку навигации на шаблоне сайта, расположенную между
      <!-- #Begin_nav -->
      <!-- #End_nav -->
    • Перенесите на место между метками код вызова компонента [comp include_breadcrumb]Навигационная цепочка[/comp] (breadcrumb) из документации.

    Кастомизация шаблона

    Цепочка отображена, но ее вид не соответствует оформлению сайта. Повторим процесс «подгонки». Правда, оговоримся сразу, активную ссылку красным (как это у нас в дизайне) можно сделать только в том случае, если в шаблоне написать условие php: если ссылка последняя, то выделить цветом. Мы этим заниматься не будем, пусть это будет домашним заданием. Поэтому последняя ссылка останется у нас серой.

    Примечание: последнюю ссылку можно выделять с помощью CSS, используя псевдокласс: last-child.

    • Скопируйте шаблон Цепочки навигации.
    • Откройте для редактирования файл CSS.
    • Используя за основу файл CSS левого меню, отредактируйте отображение Цепочки навигации.

    Компонент Новые сообщения блогов

    Предполагается, что сайт предназначен прежде всего для общения единомышленников. В качестве основной формы такого общения выбран блог – сетевой журнал. К настройкам самих блогов мы еще вернемся. А пока нам нужно внедрить в шаблон компонент Новые сообщения блога. С его помощью будут выводиться на всех страницах тестового шаблона новые сообщения в блогах.

    Размещение и настройка компонента

    Размещение компонента

    • Откройте для редактирования шаблон тестового сайта.
    • Найдите и удалите код, имитирующий вывод новых сообщений блога, расположенный между:
      <!-- #Begin_blog -->
      <!-- #End_blog -->
    • Разместите между метками код вызова компонента[comp include_blog_new_posts]Новые сообщения[/comp].
    • Сохраните внесенные изменения.

    Настройка компонента

    Перед настройкой компонента необходимо проверить настройки (и, при необходимости изменить) групп блогов и блога администратора, на который мы будем настраивать компонент.

    • Перейдите в Панель управления на страницу Сервисы > Блоги > Группы блогов. Откроется форма Группы блогов.
    • Переопределите какую-нибудь группу блогов на ваш тестовый сайт.
    • Сохраните внесенные изменения.
    • Перейдите в Панель управления на страницу Сервисы > Блоги > Блоги. Откроется список блогов, имеющихся в дистрибутиве.
    • Привяжите один или несколько блогов к выбранной выше группе.

    Перейдя в публичку, вы увидите, что блоги появились:

    • Вызовите диалог настройки свойств компонента.
    • В поле Шаблон пути к странице с сообщением блога группы необходимо настроить путь до страницы с сообщениями. Если используется дистрибутив по умолчанию, то путь будет вида:/blog/group/#group_id#/post/#post_id#/
    • В исходном дизайне у нас предусмотрен вывод только трех последних сообщений. Потом всегда можно эти настройки «переиграть». В поле Количество результатов, выводимых на страницу поставьте значение 3.

      Примечание: Есть еще важный параметр, существенно влияющий на внешний вид проекта: Длина выводимого текста сообщения. Но его значение вам надо будет подбирать экспериментально на реальном проекте. Пока это поле оставим без изменения.

    • Сохраните внесенные изменения.

    Кастомизация вывода данных

    В исходном варианте дизайна в качестве новых сообщений выводится только текст. Без авторства и даты, но с аватаром. Задумка красивая, но есть одно «Но». Эта идея с аватарами накладывает ограничения на их размер, что не совсем удобно. Это – мелочи, в общем-то, можно потерпеть мелкие аватары в самих блогах. Серьезнее другое: для реализации этого варианта нам придется модифицировать код самого компонента. Это выходит за рамки примера. Потому отойдем от дизайна и вместо аватара выведем картинку с блога с параметрами, заданными в исходном шаблоне.

    Значит нам нужно:

    • Удалить картинку по умолчанию;
    • Удалить дату;
    • Удалить ссылку на комментарии;
    • Перевести ссылку на сообщение на картинку квадратика со стрелкой, как в исходном дизайне.
    • Вместо аватара пользователя вывести картинку из блога.

    Займемся этими изменениями.

    • Скопируйте шаблон компонента в текущий шаблон сайта и откройте его для редактирования.
    • Найдите и удалите из шаблона код:
      <div class="blog-author">
      	<?if($arParams["SEO_USER"] == "Y"):?>
      		<noindex>
      		<a class="blog-author-icon" href="<?=$arPost["urlToAuthor"]?> title="<?=GetMessage("BLOG_BLOG_M_TITLE_BLOG")?>" rel="nofollow"></a>
      		</noindex>
      	<?else:?>
      		<a class="blog-author-icon" href="<?=$arPost["urlToAuthor"]?>" title="<?=GetMessage("BLOG_BLOG_M_TITLE_BLOG")?>"></a>
      	<?endif;?>
      	<?
      	if (COption::GetOptionString("blog", "allow_alias", "Y") == "Y" && (strlen($arPost["urlToBlog"]) > 0 || strlen($arPost["urlToAuthor"]) > 0) && array_key_exists("BLOG_USER_ALIAS", $arPost) && strlen($arPost["BLOG_USER_ALIAS"]) > 0)
      		$arTmpUser = array(
      			"NAME" => "",
      			"LAST_NAME" => "",
      			"SECOND_NAME" => "",
      			"LOGIN" => "",
      			"NAME_LIST_FORMATTED" => $arPost["~BLOG_USER_ALIAS"],
      		);
      	elseif (strlen($arPost["urlToBlog"]) > 0 || strlen($arPost["urlToAuthor"]) > 0)
      		$arTmpUser = array(
      			"NAME" => $arPost["~AUTHOR_NAME"],
      			"LAST_NAME" => $arPost["~AUTHOR_LAST_NAME"],
      			"SECOND_NAME" => $arPost["~AUTHOR_SECOND_NAME"],
      			"LOGIN" => $arPost["~AUTHOR_LOGIN"],
      			"NAME_LIST_FORMATTED" => "",
      		);	
      	?>
      	<?
      	$GLOBALS["APPLICATION"]->IncludeComponent("bitrix:main.user.link",
      		'',
      		array(
      			"ID" => $arPost["AUTHOR_ID"],
      			"HTML_ID" => "blog_new_posts_".$arPost["AUTHOR_ID"],
      			"NAME" => $arTmpUser["NAME"],
      			"LAST_NAME" => $arTmpUser["LAST_NAME"],
      			"SECOND_NAME" => $arTmpUser["SECOND_NAME"],
      			"LOGIN" => $arTmpUser["LOGIN"],
      			"NAME_LIST_FORMATTED" => $arTmpUser["NAME_LIST_FORMATTED"],
      			"USE_THUMBNAIL_LIST" => "N",
      			"PROFILE_URL" => $arPost["urlToAuthor"],
      			"PROFILE_URL_LIST" => $arPost["urlToBlog"],							
      			"PATH_TO_SONET_MESSAGES_CHAT" => $arParams["~PATH_TO_MESSAGES_CHAT"],
      			"PATH_TO_VIDEO_CALL" => $arParams["~PATH_TO_VIDEO_CALL"],
      			"DATE_TIME_FORMAT" => $arParams["DATE_TIME_FORMAT"],
      			"SHOW_YEAR" => $arParams["SHOW_YEAR"],
      			"CACHE_TYPE" => $arParams["CACHE_TYPE"],
      			"CACHE_TIME" => $arParams["CACHE_TIME"],
      			"NAME_TEMPLATE" => $arParams["NAME_TEMPLATE"],
      			"SHOW_LOGIN" => $arParams["SHOW_LOGIN"],
      			"PATH_TO_CONPANY_DEPARTMENT" => $arParams["~PATH_TO_CONPANY_DEPARTMENT"],
      			"PATH_TO_SONET_USER_PROFILE" => ($arParams["USE_SOCNET"] == "Y" ? 
                                         $arParams["~PATH_TO_USER"] : $arParams["~PATH_TO_SONET_USER_PROFILE"]),
      			"INLINE" => "Y",
      			"SEO_USER" => $arParams["SEO_USER"],
      		),
      		false,
      		array("HIDE_ICONS" => "Y")
      	);
      	?>	
      	</div>

    Этим мы удалили картинку по умолчанию, имя владельца блога, выводимое через компонент.

    • Найдите и удалите в коде шаблона код:
      <div class="blog-mainpage-meta">
      		<a href="<?=$arPost["urlToPost"]?>
                      " title="<?=GetMessage("BLOG_BLOG_M_DATE")?>"><?=$arPost["DATE_PUBLISH_FORMATED"]?></a>
      		<?if(IntVal($arPost["VIEWS"]) > 0):?>
      			<span class="blog-vert-separator"></span> <a href="<?=$arPost["urlToPost"]?>"
                              title="<?=GetMessage("BLOG_BLOG_M_VIEWS")?>"><?=GetMessage("BLOG_BLOG_M_VIEWS")?>:
                               <?=$arPost["VIEWS"]?></a>
      		<?endif;?>
      		<?if(IntVal($arPost["NUM_COMMENTS"]) > 0):?>
      			<span class="blog-vert-separator"></span> 
                              <a href="<?=$arPost["urlToPost"]?>#comments" title="<?=GetMessage("BLOG_BLOG_M_NUM_COMMENTS")?>">
                              <?=GetMessage("BLOG_BLOG_M_NUM_COMMENTS")?>: <?=$arPost["NUM_COMMENTS"]?></a>
      		<?endif;?>
      		<?if ($arParams["SHOW_RATING"] == "Y"):?>
      		<span class="rating_vote_text">
      		<span class="blog-vert-separator"></span>
      		<?
      		$APPLICATION->IncludeComponent(
      			"bitrix:rating.vote", $arParams["RATING_TYPE"],
      			Array(
      				"ENTITY_TYPE_ID" => "BLOG_POST",
      				"ENTITY_ID" => $arPost["ID"],
      				"OWNER_ID" => $arPost["AUTHOR_ID"],
      				"USER_VOTE" => $arResult[0]["RATING"][$arPost["ID"]]["USER_VOTE"],
      				"USER_HAS_VOTED" => $arResult[0]["RATING"][$arPost["ID"]]["USER_HAS_VOTED"],
      				"TOTAL_VOTES" => $arResult[0]["RATING"][$arPost["ID"]]["TOTAL_VOTES"],
      				"TOTAL_POSITIVE_VOTES" => $arResult[0]["RATING"][$arPost["ID"]]["TOTAL_POSITIVE_VOTES"],
      				"TOTAL_NEGATIVE_VOTES" => $arResult[0]["RATING"][$arPost["ID"]]["TOTAL_NEGATIVE_VOTES"],
      				"TOTAL_VALUE" => $arResult[0]["RATING"][$arPost["ID"]]["TOTAL_VALUE"],
      				"PATH_TO_USER_PROFILE" => $arParams["~PATH_TO_USER"],
      			),
      			$component,
      			array("HIDE_ICONS" => "Y")
      		);?>
      		</span>
      		<?endif;?>
      	</div>

    Этим мы удалили вывод даты сообщения, комментариев и компонент рейтинга.

    • Удалите из шаблона код:
      <div class="blog-mainpage-title"><a href="<?=$arPost["urlToPost"]?>"><?echo $arPost["TITLE"]; ?></a></div>

    Этим мы удалили название сообщения (правда, одновременно со ссылкой на само сообщение и её нам ещё предстоит вернуть).

    • Удалим из кода проверку первого сообщения и линию, разделяющие сообщения. Эта линия нам не нужна, так как в исходном дизайне сообщения разделяются рамкой.
      if($arPost["FIRST"]!="Y")
      	{
      		?><div class="blog-line"></div><?
      	}
    • Удалите блоки </div> с классами blog-mainpage-item и blog-clear-float. Они нам не нужны, так как форматирование в исходном дизайне задаётся таблицей.

    Из шаблона компонента удалено всё не нужное, но теперь надо привести вид в соответствие с задумками дизайнера.

    • Вставьте код:
      <?
        if(strlen($arPost["IMG"]) > 0)
        echo $arPost["IMG"];
          ?>
      Перед вызовом текста сообщения (<?=$arPost["TEXT_FORMATED"]?>)
    • Сохраните внесенные изменения.
    • Откройте для редактирования диалог настройки параметров компонента.
    • Посмотрите в исходном шаблоне дизайна сайта параметры картинки предпросмотра и установите эти параметры в поля Ширина картинки предпросмотра и Высота картинки предпросмотра.

      Примечание: Учтите, что компонент blog.new_posts не может производить изменение пропорций изображений. При этом параметр высоты имеет преимущество перед шириной.

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

    • Добавьте из исходного кода дизайна код таблицы сообщения блога
      <table width="99%" border="0" cellspacing="3" cellpadding="5">
                   <tr> 
                      <td class="br" bgcolor="#DEDEE2" bordercolor="#bfbfbf">
      Перед блоком <div class="blog-mainpage-content">
    • Закройте ячейку, строку и саму таблицу после закрывающего тега </div>
    • Сохраните внесенные изменения.

    Теперь нам нужно добавить картинку перехода к сообщению и установить ссылку на сообщение на эту картинку. Ссылка на сообщение у нас в исходном шаблоне была в таком виде:

    <div class="blog-mainpage-title"><a href="<?=$arPost["urlToPost"]?>"><?echo $arPost["TITLE"]; ?></a></div>

    Если из этого кода удалить код блока и вызов названия, то получим только ссылку:

    <a href="<?=$arPost["urlToPost"]?>"></a>
    • Вставьте код ссылки в код шаблона после кода вызова текста сообщения (<?=$arPost["TEXT_FORMATED"]?>)
    • Скопируйте из исходного кода код картинки (<img width="20" height="20" border="0" align="right" src="/bitrix/templates/test/images/ukaz_inf.jpg" />) и вставьте его в код ссылки.
    • Сохраните внесенные изменения.

    Теперь переход к сообщению у нас происходит с картинки. Однако пока ещё отображение текста не соответствует нужному.

    Файл CSS

    В коде шаблона есть указание на расположение файлов стилей:

    <?
    if (!$this->__component->__parent || empty($this->__component->__parent->__name) || $this->__component->__parent->__name != "bitrix:blog"):
    	$GLOBALS['APPLICATION']->SetAdditionalCSS('/bitrix/components/bitrix/blog/templates/.default/style.css');
    	$GLOBALS['APPLICATION']->SetAdditionalCSS('/bitrix/components/bitrix/blog/templates/.default/themes/blue/style.css');
    endif;
    ?>

    Можно отредактировать эти файлы, а можно добавить свой файл CSS в папке C:\www\bitrix\templates\test\components\bitrix\blog.new_posts\template1\, что мы и сделаем.

    • Добавьте в указанную папку файл style.css.
    • Пропишите в файле:
       .blog-mainpage-content
      {
              font-size:80%;
              text-decoration:none;
      }
      
    • Сохраните внесенные изменения.

    Результат работы практически полностью соответствует исходному дизайну:

    Примечание: Маловероятно, что пользователи буду грузить картинки в пропорциях нужных для правильного отображения картинки на главной странице сайта. Значит разработчик должен будет предусмотреть модификацию вывода компонента с помощью файла result_modifier.php с целью формирования картинки в нужном размере.

    Кроме того, необходимо предусмотреть ситуацию, когда в блоге не загружена картинка. Это можно сделать в самом шаблоне компонента.


    Счетчик посещений

    Для любого проекта – посещаемость очень важна. Посещения страниц регистрируются счетчиками. Счетчики можно установить внешние, скажем, от Yandex’а. А можно поставить счетчик внутренний, системный.

    Примечание: Если быть точным, то системный счетчик устанавливать не надо. Он начинает работу сразу по инсталляции 1С-Битрикс: Управление сайтом на ваш компьютер и всю информацию по посещениям вы можете посмотреть в разделе Веб-аналитика Административного раздела (Аналитика > Сводная статистика). А вот вывести данные счетчика для просмотра посетителями сайта можно, если установить соответствующий компонент.

    • Откройте для редактирования шаблон тестового сайта.
    • Скопируйте код вызова компонента Таблица статистики из документации.
    • Разместите его сразу за меткой <!-- #End_blog -->
    • Сохраните внесенные изменения.

    Под Новыми сообщениями блогов появилась Таблица статистики. Далее нам надо совершить стандартные действия по адаптации таблицы под дизайн. В случае с Таблицей статистики все изменения внешнего вида можно сделать с помощью одного файла стилей, но копировать шаблон компонента всё равно придётся.

    Поскольку вид счетчика нам не задан дизайнером, возможностей для фантазий у нас больше. Но оставим эти фантазии на ваше усмотрение, сами же просто подгоним дизайн таблицы под дизайн сайта по стилям и под размер левой колонки:.

    Баннер

    Для установки баннера в 1С-Битрикс: Управление сайтом предусмотрен специальный компонент, который так и называется Баннер. Место для него в шаблоне тегами предусмотрено, хотя зрительно в «подвале» оно не видно. Ячейка для баннера расположена между ячейками с копирайтом автора дизайна и ячейкой с логотипом клуба.

    • Откройте для редактирования шаблон тестового сайта.
    • После метки <!-- #Begin_banner --> введите любой символ, например число 123. Это нужно для того, чтобы стал виден компонент. Ненастроенный компонент не выводит баннер и поэтому он не будет отображаться при просмотре сайта с публички.
    • Скопируйте из документации код вызова компонента [comp include_advertising_banner]Баннер[/comp] и разместите между метками:
      <!-- #Begin_banner -->
      <!-- #End_banner -->

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

    Включаемые области

    Правая колонка

    Тестовый шаблон рисовался в тот момент, когда в 1С-Битрикс: Управление сайтом не было показа фотогалереи во всплывающем окне. Предложенный дизайнером на тот момент вариант сегодня - не лучший вариант. Кроме того, использование правой колонки можно не ограничивать фотографиями. Ведь в разных разделах сайта может потребоваться вывод разных данных в этой колонке. Если такая потребность возникнет, то придётся создавать новый шаблон с новым компонентом (и соответственно впоследствии поддерживать его). Мы поступим по-другому. Создадим в этом месте Включаемую область и при необходимости будем наполнять её нужным контентом в нужном разделе.

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

    • Откройте для редактирования шаблон сайта.
    • Удалите коды, имитирующие фотогалерею в правой таблице, расположенные между
      <!-- #Begin_right_Column -->
      <!-- #End_right_Column -->
    • Добавьте в это место код вызова компонента [comp include_main_include]Включаемая область[/comp] из документации.
    • Сохраните внесенные изменения.
    • Скопируйте и откройте для редактирования шаблон области.
    • Из исходного кода дизайна возьмите код заголовка области между метками
      <!-- #Begin_right_Column -->
      <!-- #Begin_Foto -->
    • Сохраните внесенные изменения.

    Теперь правая колонка у нас выглядит так:

    Внешне ничего не изменилось, так как мы перенесли в шаблон компонента часть оформления сайта. Сделано это для того, чтобы простым копированием шаблона можно было назначать его для каждой новой включаемой области с новым названием. В текущем варианте у нас правая колонка называется Галерея. Если мы создадим в другом разделе новую включаемую область, то скопировав шаблон мы получим нужное название колонки простой заменой этой надписи в новом шаблоне компонента.

    На главной странице должна быть фотогалерея. Значит нужно разместить во включаемой области соответствующий компонент: Список фото (со слайдером).

    • Включаемая область - это файл с именем sect_inc.php, расположенный в соответствующей директории сайта. В нашем случае - в корне сайта. Откройте этот файл для редактирования.
    • Перенесите в этот файл код вызова компонента [comp include_photogallery_detail_list_ex]Список фото (со слайдером)[/comp] (photogallery.detail.list.ex)
    • Настройте параметры:
      • THUMBNAIL_SIZE - размер фотографии-анонса (значение нужно взять из исходного дизайна);
      • IBLOCK_TYPE - тип информационного блока, используемого в качестве фотогалереи
      • IBLOCK_ID - инфоблок, используемый в качестве фотогалереи
      • PAGE_ELEMENTS - число выводимых фото на странице, например - 5.
    • Сохраните внесенные изменения.

    Результат действий:

    Кастомизация шаблона

    • Скопируйте и откройте для редактирования шаблон компонента photogallery.detail.list.ex.
    • Для начала сделаем фоновую таблицу для всех фотографий. И в качестве основы возьмём таблицу, которую использовали при кастомизации компонента blog.new_posts. Перед этим нужно определить какая часть шаблона отвечает за вывод списка превью фотографий. Этот код отмечен комментарием /* Used to show 'More photos' in js*/ и выделен в блок <div> с классом photo-items-list photo-photo-list. Вот этот блок и нужно включить в таблицу.

      Поместите блок с указанным классом в ячейку таблицы:

      <table width="99%" border="0" cellspacing="3" cellpadding="5">
      	<tr> 
      		<td class="br" bgcolor="#DEDEE2" bordercolor="#bfbfbf">
      		</td>
      	</tr>
      </table>
    • Если мы остановимся на этом, то ссылка на открытие других фотографий у нас окажется вне таблицы, что не верно с точки зрения исходного дизайна. Надо включить вывод этой надписи в таблицу тоже. Найти часть кода, отвечающую за эту надпись, просто:
      • Откройте файл \bitrix\templates\test\components\bitrix\photogallery.detail.list.ex\test_1\lang\ru\.
      • Через поиск найдите фразу Еще фотографии. Это значение установлено у параметра P_SLIDER_MORE_PHOTOS.
      • Через поиск найдите в шаблоне код с этим параметром. Этот код в блоке с классом photo-show-more расположен внутри PHP-условия. Значит нужно перенести закрывающий код таблицы после тега, закрывающего это PHP-условие.
    • Теперь нам нужно сделать темные рамочки у каждой выводимой превью. Перед этим нужно определить какая часть шаблона выводит непосредственно превью. Эту часть кода легко найти через поиск по img src. Вызов картинки обрамлён в тег a href с классом photo-item-inner. Заключите этот тег в ячейку таблицы, взятой из исходного кода дизайна:
      <table border="0" cellspacing="0" cellpadding="10">
      	<tr> 
      		<td bgcolor="#333333" align="center" valign="top">
      		</td>
      	</tr>
      </table>
    • Сохраните внесенные изменения.

    Осталось только привести к нужному виду фразу Ещё фотографии. Подгоним стили вывода текста и добавим стрелки, как на исходном дизайне.

    • Как писали выше фраза Ещё фотографии размещена в блоке с классом photo-show-more. Значит надо задать ему параметры, чтобы текст отображался в соответствии с дизайном. Воспользуйтесь вашими знаниями CSS.
    • Для размещения картинок стрелок посмотрите в исходном коде коды картинок и разместите их вокруг вызова сообщения <?= GetMessage("P_SLIDER_MORE_PHOTOS")?>
    • Сохраните внесенные изменения.

    Результат:

    Примечание: Можно поиграться настройками компонента, параметрами таблиц, чтобы получить более "красивое" положение и оформление.

    Work Area и разные шаблоны

    Bitrix Framework позволяет создавать и использовать несколько шаблонов для разных условий просмотра. Условия могут быть самыми разными: для разных групп пользователей, для разных разделов, даже для отдельных страниц. Это очень удобно в самых разных случаях. Например, при смене дизайна.

    Все шаблоны создаются за счет того или иного размещения в пределах шаблона сайта Рабочей области #WORK_AREA#, включения или исключения в/из нее элементов входящих на данный момент в Header или Footer.

    Создадим три шаблона:

    • С зоной #WORK_AREA# занимающей всю среднюю часть сайта, без левой и правой колонки (для блогов и форумов).
    • С левой колонкой в Header’е и без правой колонки (для статей).
    • С левой колонкой в Header’е и правой колонкой в Footer’е (для страниц, где нужно организовать предварительный показ картинок).

    Собственно третий у нас уже есть, он создался после удаления текста статьи из Footer’а. Логичнее всего будет далее последовательным удалением элементов создать оставшиеся два шаблона. Детально приводить удаляемый код в данном случае мы не будем, здесь вам надо положиться на собственные знания html. Для грамотного выполнения этой операции вам надо очень внимательно следить за тем, что вы модифицируете в шаблоне и почаще пользоваться кнопкой Предпросмотр перед сохранением результатов.

    Шаблоны можно создать двумя способами. Первый: простым копированием. Второй – созданием нового и добавлением в него кода из старого шаблона. Преимущество первого способа – в простоте выполнения операции, преимущество второго в том, что можно задать произвольное имя шаблона (в первом случае копия будет называться test_copy). Второй вариант несколько более сложный, так как требует дополнительных действий по настройке:

    • Откройте для редактирования шаблон тестового сайта.
    • Скопируйте в буфер обмена весь код.
    • Нажмите кнопку Добавить шаблон на Контекстной панели. Откроется форма создания нового шаблона.
    • Вставьте скопированный код шаблона в поле Внешний вид шаблона сайта.
    • В поле ID введите название test2.
    • В поле Название введите test_article.
    • В поле Описание введите: Шаблон для статей.
    • Скопируйте в шаблон стили сайта и стили шаблона.
    • Скопируйте в папку шаблона папку components из папки bitrix\templates\test\

    Такие названия и пояснения мы сделали для того, чтобы не путаться в шаблонах. Если вы будете создавать их довольно много, то логически осмысленные названия и описания вам не помешают.

    • Удалите код, включающий в себя правую колонку:

      Метки:

      <!-- #Begin_right_Column -->
      <!-- #End_right_Column -->
    • Сохраните внесенные изменения.
    Результат:

    Шаблон создан. Следующий вариант мы создадим по первому варианту, копированием.

    • Выполните команду Настройки > Настройки продукта > Сайты > Шаблоны сайтов. Откроется список шаблонов.
    • В колонке команд в строке нашего второго шаблона выберите команду Копировать. Система выполнит копирование, в списке появится шаблон под именем test_copy.
    • Откройте его для редактирования.
    • В поле Название смените название на test_blogs.
    • В поле Описание смените текст на Шаблон для раздела Блоги.
    • Удалите из него левую колонку. Метки:
      <!-- #Begin_left_Column -->
      <!-- #End_left_Column -->
    • Сохраните внесенные изменения.
    Результат:

    Число шаблонов, которые вы можете использовать на сайте – не ограничено, хоть для каждой отдельной страницы создавайте свой собственный вид. Есть один нюанс. При создании шаблонов всегда учитывайте функционал удаляемых частей, и, при необходимости, изменяйте размещение компонентов. Например, при создании последнего шаблона вместе с левой колонкой мы удалили и компонент Таблица статистики. Если она вам нужна на страницах, использующих этот шаблон, то таблицу придется добавлять в #WORK_AREA#.


    Простейший пример внедрения дизайна с блочной вёрсткой (div)

    В данной главе представлен пример внедрения в Bitrix Framework шаблона дизайна с [dw]блочной вёрсткой.[/dw][di] Блочная верстка — это подход, при котором сайт строят на основе блоков,
    в качестве которых выступают теги div.

    Особенности тегов div:

    - это блочный элемент, и если у него не задана ширина, то
    растягивается на всю ширину окна браузера;

    - высота блока равна содержимому, если она не задана. Высота пустого
    блока 0 px, поэтому не отображается на странице;

    - не имеет оформления. Нужно задать ему стили в CSS, чтобы увидеть его;

    - не несет смысловой нагрузки, это просто способ структурировать сайт;

    - может содержать любое количество вложенных элементов (можно
    вкладывать другие блоки div, заголовки, параграфы,
    изображения и многие другие элементы). [/di]

    С примером внедрения шаблона с табличной вёрсткой можно ознакомиться в [ds]соответствующей главе.[/ds][di] Описанный пример использовался в опубликованных несколькими годами ранее книгах о создании сайта на "1С-Битрикс: Управление сайтом". Принципы работы за это время не изменились. Однако поменялись шаблоны в демодистрибутиве и, следовательно, описание их кастомизации.

    Подробнее...[/di]

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

    Скачать тестовые материалы.

    Примечание: Шаблон - тестовый, вопросы кроссбраузерности при его создании не рассматривались. Все работы выполнялись в Google Chrome.

    Повторение примера рекомендуется на [ds]локальной установке[/ds][di] Ознакомление с продуктом на локальном компьютере:

    Для ознакомления с продуктом на локальном компьютере используйте установку "1С-Битрикс: Управление сайтом" или "Битрикс24 в коробке" на Виртуальную машину.

    Подробнее...[/di] с демоверсией сайта для разработчиков (например, на [ds]виртуальной машине[/ds][di] Запуск виртуальной машины BitrixVM:

    Загрузите подходящий вам дистрибутив настроенной виртуальной машины BitrixVM.

    Загруженный архив распакуйте в любую папку, например, С:\BitrixVM\, и запустите виртуальную машину с помощью подходящего ПО.

    Подробнее...[/di]).

    Подразумевается, что пользователь знаком с PHP, HTML и CSS, поэтому детальное описание причин удаления или добавления того или иного кода опускается. Считаем, что Вы сами способны понять эти моменты. Перед началом работы настойчиво рекомендуем ознакомиться с понятием [ds]шаблона дизайна сайта[/ds][di]Шаблон дизайна - это внешний вид сайта, в котором определяется расположение различных элементов на сайте, художественный стиль и способ отображения страниц. Включает в себя программный html-код, графические элементы, таблицы стилей, дополнительные файлы для отображения контента. Может также включать в себя шаблоны компонентов, шаблоны готовых страниц и сниппеты.

    Подробнее...[/di] в рамках Bitrix Framework.

    Для повторения примера можно [ds]создать специальный сайт,[/ds][di] Как создать новый сайт:

    Перейдите в Административный раздел на страницу Настройки > Настройки продукта > Сайты. В Рабочей области откроется Список сайтов.

    Выполните команду Добавить сайт на Контекстной панели. В Рабочей области откроется форма Добавление сайта.

    Подробнее...[/di] который будет использоваться в качестве базового для шаблона, либо применять шаблон к одному из сайтов в Вашей тестовой системе.

    В примере не рассматриваются вопросы, связанные с кастомизацией ввода и обработки данных за счёт изменения логики работы системы и компонентов.

    Подготовка к работе

    Итак, нам необходимо внедрить заранее подготовленный макет сайта с [dw]блочной вёрсткой.[/dw][di] Блочная верстка — это подход, при котором сайт строят на основе блоков,
    в качестве которых выступают теги div.

    Особенности тегов div:

    - это блочный элемент, и если у него не задана ширина, то
    растягивается на всю ширину окна браузера;

    - высота блока равна содержимому, если она не задана. Высота пустого
    блока 0 px, поэтому он не виден;

    - не имеет оформления. Нужно задать ему стили в CSS, чтобы увидеть его;

    - не несет смысловой нагрузки, это просто способ структурировать сайт;

    - может содержать любое количество вложенных элементов (можно
    вкладывать другие блоки div, заголовки, параграфы,
    изображения и многие другие элементы). [/di]

    Скачать тестовые материалы.

    В архиве макета сайта [dw]содержатся:[/dw][di] [/di]

    • папки со стилями шаблона;
    • папка с js-файлами;
    • папка с картинками;
    • html-макет индексной страницы сайта (index.html);
    • файл с главными стилями шаблона (styles.css);
    • csv-файл с демоданными для импорта (demo.csv).

    Разархивируйте скачанный архив с тестовыми материалами (doc.zip).

    Откройте в браузере файл index.html из папки со скачанным архивом шаблона. В окне браузера откроется тестовый дизайн.


    Это дизайн главной страницы [ds]сайта-лендинга.[/ds][di] Целевая страница (англ. landing page, также «посадочная страница») — веб-страница, основной задачей которой является сбор контактных данных целевой аудитории. Используется для усиления эффективности рекламы, увеличения аудитории. Целевая страница обычно содержит информацию о товаре или услуге.

    Подробнее...[/di] Она состоит из основной заставки с названием сайта, навигации по странице (верхнее горизонтальное меню, кнопки РЫБЫ.НЕТ и Узнать больше), фотогалереи, блока новостей, контактной информации и копирайта.

    Пропишем требования, которым должен соответствовать сайт с внедрённым шаблоном.

    Техническое задание:

    • Основная заставка и навигация должны располагаться в шапке сайта (header).
    • Копирайт должен располагаться в подвале сайта (footer).
    • Новости сайта нужно выводить из инфоблока новостей с помощью компонентов. Для страницы детального вывода новости создать упрощенный шаблон.
    • Остальную информацию считать статической и разместить на индексной странице (тогда контент-менеджер сможет её изменять в публичном разделе, включив режим Правки).
    • В качестве дополнительного задания: создать нижнее меню.


    Разделение шаблона на header, footer и work_area

    Первым делом обычно определяют, какие части вёрстки главной страницы могут присутствовать и на других страницах сайта. Другими словами, разбивают страницу на три части:

    1. header (шапка, хедер, пролог) - верхняя часть дизайна; хранится в файле header.php шаблона сайта (Контент > Структура сайта > Файлы и папки > bitrix > templates > [ваш шаблон] );
    2. WORK_AREA - рабочая область, содержащая основной контент; при создании страниц заполняется как раз эта область (например, Контент > Структура сайта > Файлы и папки > index.php );
    3. footer (подвал, футер, эпилог) - нижняя часть дизайна; хранится в файле footer.php шаблона сайта (Контент > Структура сайта > Файлы и папки > bitrix > templates > [ваш шаблон] ).

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

    Рассмотрим часть возможных вариантов разделения сайта на зоны, обозначенные цветами:

    • красный - header (шапка сайта)
    • зелёный - WORK_AREA (рабочая зона, т.е. основной контент)
    • синий - footer (подвал сайта)

    Описание приведённых вариантов:

    1
    • в шапке - основная заставка с названием сайта и навигация по странице (верхнее горизонтальное меню, кнопки РЫБЫ.НЕТ и Узнать больше)
    • в подвале - копирайт
    2
    • в шапке - горизонтальное верхнее меню и кнопка РЫБЫ.НЕТ
    • в подвале - блок с контактами и копирайт
    3
    • в шапке сайта нет видимой части кода (то есть в файле header.php есть только служебный код)
    • в подвале - блок новостей, контакты и копирайт

    В примере будем использовать разбивку 1.



    Шаблон сайта (создание папки шаблона)

    Создание шаблона – первый шаг в интеграции дизайна. Рассмотрим подробнее на примере, как это сделать.

    Добавление кода шаблона / создание папки шаблона

    • Откроем исходный код страницы index.html, выделим весь код и скопируем его в буфер обмена.
    • На сайте перейдём в раздел Настройки > Настройки продукта > Сайты > Шаблоны сайтов. Кликнем по кнопке Добавить шаблон, и в Рабочей области откроется форма Шаблоны сайтов.
    • Выполним команду Добавить шаблон на Контекстной панели. В открывшейся [dw]форме создания[/dw][di] [/di] нового шаблона укажем [dw]идентификатор шаблона[/dw][di] Можно использовать латинские буквы и цифры. [/di] и остальные параметры (описание понятно интуитивно).
    • Вставим скопированный код в поле Внешний вид шаблона сайта.
    • Теперь нужно определить шапку сайта (header) и подвал (footer) - это те области, которые будут неизменны на каждой странице сайта.

      В нашем примере шапка сайта будет содержать основную заставку с названием сайта и навигацию по странице (верхнее горизонтальное меню, кнопки РЫБЫ.НЕТ и Узнать больше) - т.е. заканчиваться на теге </header>. Подвал сайта будет содержать только копирайт, т.е. начинаться с блока <!-- Footer -->.

      Выделим и удалим весь код между шапкой (header) и подвалом сайта (footer).

      Таким образом, мы удалили весь код (контент сайта) между тегом </header> и блоком <!-- Footer -->. Вместо удаленного кода пропишем #WORK_AREA#:

    • Теперь нужно заполнить вкладки [dw]Стили сайта[/dw][di] Стили, используемые при оформлении контента страниц (стили сайта), хранятся
      в файле styles.css.

      [/di] и [dw]Стили шаблона.[/dw][di] Стили, используемые в шаблоне дизайна, хранятся в файле template_styles.css. Это - основной
      CSS-файл шаблона.

      [/di] Скопируйте туда содержимое файлов styles.css и creative.css (если же эти вкладки оставить пустыми, то придется дополнительно подключать файлы в шапке сайта).
    • Сохраним изменения.

    После выполнения вышеперечисленных действий будет создана папка этого шаблона, включающая отдельные файлы шапки сайта (header.php), подвала сайта (footer.php) и файлы с автоматически подключаемыми скриптами и стилями (Контент > Структура сайта > Файлы и папки > bitrix > templates):

    Нажмите на рисунок, чтобы увеличить

    Загрузка файлов шаблона

    [dw]Загрузите[/dw][di] Например, по протоколу FTP. Или же можно создать папки вручную и загрузить в них содержимое. [/di] в папку созданного шаблона следующие папки из архива (стили шаблона, картинки и js-файлы):

    • css
    • img
    • js
    • vendor

    Новый шаблон создан (точнее, создана папка этого шаблона со всем необходимым содержимым). Теперь нужно отредактировать шапку и подвал сайта (файлы header.php и footer.php), а также настроить их подключение. Об этом читайте [ds]в следующем уроке.[/ds][di] В папке шаблона откроем файл header.php в режиме редактирования как PHP.

    Первой строкой пропишем служебный код (защита от подключения файла напрямую без подключения ядра).

    Подробнее...[/di]

    Примечание: вместо папки bitrix можно использовать папку local (подробнее [ds]в уроке[/ds][di] Чтобы сделать жизнь разработчиков проектов удобнее, в рамках работ по новому ядру D7 с версии главного модуля 14.0.1 основные файлы проекта вынесены из папки /bitrix в папку /local. Это позволит изолировать изменяющиеся файлы проекта от папки продукта. По сути, в исключения достаточно будет добавить одну папку /bitrix.

    Подробнее...[/di]).



    Шаблон сайта (настройка и подключение)

    [ds]Ранее[/ds][di] Создание шаблона – первый шаг в интеграции дизайна.

    Примечание: Ранее можно было задавать внешний вид шаблона сайта с помощью визуального редактора. С версии 14.0 работа выполняется без визуального редактора.

    Подробнее...[/di] мы создали папку шаблона сайта. Однако для корректного отображения шаблона на сайте нужно еще адаптировать вёрстку, подключив все стили и js-файлы в шапке и подвале сайта, а также прописав новые пути до картинок и объектов.

    Необходимо выполнить три важных шага:

    1. Редактирование шапки сайта (header.php)
    2. Редактирование подвала сайта (footer.php)
    3. Подключение шапки и подвала (index.php), добавление контента

    1. Редактирование шапки сайта (header.php)

    • В папке шаблона откроем файл header.php в режиме редактирования как PHP.
    • Первой строкой пропишем служебный код (защита от подключения файла напрямую без подключения ядра):

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

    • Внутри тегов <head> добавим следующие строки:
      • Код отображения заголовка страницы:

        <title><? $APPLICATION->ShowTitle(); ?></title>
        

      • Код вывода в шаблоне сайта основных полей тега <head> (мета-теги Content-Type, robots, keywords, description; стили CSS; скрипты):

        <? $APPLICATION->ShowHead();  ?>
        

    • После открывающего тега <body> пропишем код для отображения административной панели <? $APPLICATION->ShowPanel(); ?> . В связи с особенностями данного html-макета заключим код отображения панели в теги <div> (панель будет отображаться внизу страницы), иначе административная панель "наедет" на меню:

      <div id="panel">
      	<? $APPLICATION->ShowPanel(); ?> 
      </div>
      

    • Во все строках, где есть обращение к файлам, добавим в начале пути константу [dw]<?=SITE_TEMPLATE_PATH?>[/dw][di] Возвращает путь до текущего подключённого шаблона сайта без последнего слеша. [/di] и слеш /.

      Рассмотрим на примере блока <!-- Bootstrap core JavaScript -->:

        <!-- Bootstrap core JavaScript -->
        <script src="<?=SITE_TEMPLATE_PATH?>/vendor/jquery/jquery.min.js"></script>
        <script src="<?=SITE_TEMPLATE_PATH?>/vendor/bootstrap/js/bootstrap.bundle.min.js"></script>
      

    • Сохраним изменения.

    Пример отредактированного файла header.php с комментариями

    Примечание: css-стили в шапке сайта предпочтительнее подключить с помощью метода Asset::addCss() ядра D7.

    Пример файла header.php с комментариями

    2. Редактирование подвала сайта (footer.php)

    Подвал сайта почти [dw]не нуждается в изменениях[/dw][di]Проверьте обязательно наличие строки <?if(!defined("B_PROLOG_INCLUDED") || B_PROLOG_INCLUDED!==true) die();?> в начале файла.[/di]. Необходимо только в строках, обращающихся к файлам, добавить в начале пути константу [dw]<?=SITE_TEMPLATE_PATH?>[/dw][di] Возвращает путь до текущего подключённого шаблона сайта без последнего слеша. [/di] со слешем / и сохранить изменения.

    Пример отредактированного файла footer.php

    3. Подключение шапки и подвала (index.php), добавление контента

    • Необходимо удостовериться в подключении на странице вывода файлов header.php и footer.php.

      Для этого в файле главной страницы сайта [dw]index.php[/dw][di] (Контент > Структура сайта > Файлы и папки > index.php) [/di] сверим следующие конструкции:

      • первая строка: <?require($_SERVER["DOCUMENT_ROOT"]."/bitrix/header.php");?>
      • последняя строка: <?require($_SERVER["DOCUMENT_ROOT"]."/bitrix/footer.php");?>
    • Добавим [dw]статический[/dw][di] Т.е. для изменения контента страницы нужно будет вновь отредактировать вручную код страницы index.php. [/di] контент.

      Откроем файл с подготовленной вёрсткой (index.html) и скопируем код между шапкой (header) и подвалом сайта (footer) - тот, который мы в пункте 1 заменяли на #WORK_AREA#, то есть контент сайта.

      Настроим пути к картинкам. Пропишем в существующих путях уже знакомую константу <?=SITE_TEMPLATE_PATH?>/.

    Пример отредактированного файла index.php с комментариями

    Удобнее, разумеется, настроить вывод динамической информации и работать с ним. Подробнее об этом в [ds]следующем уроке.[/ds][di] В предыдущем уроке мы создали шаблон главной страницы сайта. Однако пока вся информация на сайте - статическая, и для внесения любых изменений придется править главную страницу index.php. Это не очень удобно, если информация на сайте должна постоянно обновляться. И при этом слишком громоздко.

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

    Подробнее...[/di]

    Готово! Теперь осталось только [ds]назначить[/ds][di] Откройте в Административном разделе страницу Настройки > Настройки продукта > Сайты > Список сайтов. С помощью меню действий откройте для редактирования нужный сайт.

    Выберите шаблон, который вы хотите применить и условия.

    Подробнее...[/di] созданный шаблон для тестового сайта и посмотреть результат.


    Примечание: Всегда можно изменить шапку или подвал сайта, перенеся требуемую часть кода из индексной страницы в файл header.php или footer.php.

    Пример: перенос блока "Связаться с нами" в footer.php

    Список новостей: кастомизация шаблона компонента

    В [ds]предыдущем уроке[/ds][di] Создание шаблона – первый шаг в интеграции дизайна.

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

    Подробнее...[/di] мы создали шаблон главной страницы сайта. Однако пока вся информация на сайте - статическая, и для внесения любых изменений придется править главную страницу index.php. Это не очень удобно, если информация на сайте должна постоянно обновляться. И при этом слишком громоздко.

    Поэтому однородную информацию лучше заносить в [ds]инфоблоки,[/ds][di] Информационные блоки - модуль, позволяющий каталогизировать и управлять различными типами (блоками) однородной информации. С помощью информационных блоков может быть реализована публикация различных типов динамической информации: каталоги товаров, блоки новостей, справочники и т.д.

    Подробнее...[/di] а потом с помощью [ds]компонентов[/ds][di] Компонент - это логически завершённый код, предназначенный для извлечения информации из инфоблоков и других источников и преобразования её в HTML-код для отображения в виде фрагментов web-страниц. Состоит из собственно компонента (контроллер) и шаблона (представление). Компонент, с помощью API одного или нескольких модулей, манипулирует данными. Шаблон компонента выводит данные на страницу.

    Подробнее...[/di] выводить её на сайте. А чтобы информация отображалась именно так, как задумал дизайнер, необходимо [ds]кастомизировать[/ds][di] Кастомизация шаблона компонента, как правило, преследует две цели:

    1. Приведение формы вывода данных компонента в соответствие с дизайном сайта;

    2. Организация вывода данных компонента в виде, недоступном в стандартном варианте.

    Подробнее...[/di] шаблон компонента.

    Новости будем выводить с использованием двух простых компонентов:

    • Список новостей [comp include_62968](news.list)[/comp] - его разместим на главной странице сайта для вывода [dw]анонсов новостей;[/dw][di] [/di]
    • Новость детально [comp include_62969](news.detail)[/comp] - для вывода [dw]детальной новости[/dw][di] [/di] нужно будет создать новую страницу со своим шаблоном.

    В этом уроке изменим дизайн компонента Список новостей. Рассмотрим пошагово:

    1. Копирование шаблона компонента

    Скопируйте шаблон компонента Список новостей. Это можно сделать двумя способами:

    • В рамках файловой системы копированием папки Контент > Структура сайта > Файлы и папки > bitrix > templates > [ваш шаблон] > components > bitrix > [название компонента] > [название шаблона компонента].
    • Средствами интерфейса системы, разместив на странице компонент и скопировав его с помощью команды [dw]Копировать шаблон компонента[/dw][di] [/di] (при включённом режиме Правка). Копировать лучше в [dw]текущий шаблон[/dw][di] [/di] сайта.

    В данном примере скопирован шаблон default.

    2. Кастомизация шаблона компонента

    • В исходном файле вёрстки index.html скопируйте блок, выводящий новости (в нашем случае блок Call to Action Section).
    • В папке скопированного шаблона компонента news.list откройте в режиме редактирования файл template.php.
    • После первой строки <? if (!defined('B_PROLOG_INCLUDED') || B_PROLOG_INCLUDED !== true) die(); ?> (защита от подключения файла напрямую без подключения ядра) вставьте скопированный ранее код вёрстки.
    • Теперь нужно разобраться в общей структуре кода вёрстки (какая часть кода за что отвечает):

      1 - стиль заголовка блока и сам заголовок;

      2 - стиль блока;

      3 - три элемента блока (т.е. три новости) со своими стилями.

      Код 1 и 2 оставим без изменений, а работать будем с 3:

    Примечание: Весь нижеприведённый код есть в штатном шаблоне компонента, который мы копировали в пункте 1.

    • Код трёх новостей заменим на конструкцию foreach, которая будет перебирать массив элементов и выводить найденные новости:

      <? foreach ($arResult["ITEMS"] as $arItem): ?>
      	<?
      	$this->AddEditAction($arItem['ID'], $arItem['EDIT_LINK'], CIBlock::GetArrayByID($arItem["IBLOCK_ID"], "ELEMENT_EDIT"));
      	$this->AddDeleteAction($arItem['ID'], $arItem['DELETE_LINK'], CIBlock::GetArrayByID($arItem["IBLOCK_ID"], "ELEMENT_DELETE"), array("CONFIRM" => GetMessage('CT_BNL_ELEMENT_DELETE_CONFIRM')));
      	?>
      
      	<!--  Код одной новости из исходного файла index.html  -->
      		<div class="col-lg-4 text-center">
      			<div class="card bg-secondary border border-dark">
      				<img class="card-img-top" src="img/2.jpg" alt="News image cap">
      				<div class="card-body">
      					<h5 class="card-title">News title</h5>
      					<p class="card-text">Some quick example text to build on the card title and make up the bulk of the card's content.</p>
      					<a href="#" class="btn btn-primary">Go somewhere</a>
      				</div>
      			</div>
      		</div>
      
      <? endforeach; ?>
      

    • Пропишем вызов пути картинки анонса src="<?= $arItem["PREVIEW_PICTURE"]["SRC"] ?>". Вместо статического названия элемента укажем <? echo $arItem["NAME"] ?> (будут подтягиваться названия элементов инфоблока):

      <img class="card-img-top" src="<?= $arItem["PREVIEW_PICTURE"]["SRC"] ?>"
      	alt="<? echo $arItem["NAME"] ?>">
      <div class="card-body ">
      		<h5 class="card-title"><? echo $arItem["NAME"] ?></h5>
      

    • Выводить текст анонса новостей будем конструкцией:

      <? if ($arParams["DISPLAY_PREVIEW_TEXT"] != "N" && $arItem["PREVIEW_TEXT"]): ?>
      	<p class="card-text"><? echo $arItem["PREVIEW_TEXT"]; ?></p>
      <? endif; ?>
      

    • В коде, отвечающем за вывод кнопки перехода на страницу детального просмотра элемента, пропишем <? echo $arItem["DETAIL_PAGE_URL"] ?>:

      <a href="<? echo $arItem["DETAIL_PAGE_URL"] ?>" class="btn btn-primary">Подробнее</a>
      

    Итоговый код кастомизированного шаблона компонента news.list



    Детальный просмотр новости: шаблон страницы и компонента

    В [ds]предыдущем уроке[/ds][di] Однородную информацию лучше заносить в инфоблоки, а потом с помощью компонентов выводить её на сайте. А чтобы информация отображалась именно так, как задумал дизайнер, необходимо кастомизировать шаблон компонента.

    Новости будем выводить с использованием двух простых компонентов:

    Список новостей (news.list) - его разместим на главной странице сайта для вывода анонсов новостей;

    Новость детально (news.detail) - для вывода детальной новости нужно будет создать новую страницу со своим шаблоном.

    В этом уроке изменим дизайн компонента Список новостей. Рассмотрим пошагово:

    Подробнее...[/di] мы настроили вывод списка новостей, а в этом уроке займемся страницей детального просмотра новости:

    • создадим шаблон страницы;
    • разместим и кастомизируем компонент Новость детально [comp include_news_detail](news.detail)[/comp].

    1. Создание шаблона страниц для вывода детальной информации

    Шаблон главной страницы уже подготовлен. Теперь нужно подготовить шаблон страницы вывода детальной информации.

    Можно, конечно, создать шаблон по ранее описанному пути, но в этом случае можно сделать гораздо легче: [dw]копировать папку[/dw][di] [/di] шаблона, [dw]переименовать[/dw][di] [/di] и откорректировать файлы.

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

    В нашем примере сделаем шаблон для страниц с подробной информацией минималистичным, оставив только [dw]копирайт[/dw][di] [/di] в подвале сайта. Поэтому файл footer.php (в котором и содержится копирайт) оставим без изменений, отредактировав только файл header.php:

    <? if (!defined('B_PROLOG_INCLUDED') || B_PROLOG_INCLUDED !== true) die(); ?> <!-- Служебный код, необходим для защиты подключения этого файла без подключения ядра -->
    
    <!DOCTYPE html>
    <html lang="ru">
    
    	<head>
    	<meta charset="utf-8">
    	<meta name="viewport" content="width=device-width, initial-scale=1, shrink-to-fit=no">
    	<meta name="description" content="">
    	<meta name="author" content="">
    
    	<title><? $APPLICATION->ShowTitle(); ?></title> <!-- Отображение заголовка страницы -->
    	<? $APPLICATION->ShowHead();  ?> <!--  Вывод в шаблоне сайта основных полей тега head (мета-теги Content-Type, robots, keywords, description; стили CSS; скрипты) -->
    
    	<!-- Font Awesome Icons -->
    	<link href="<?=SITE_TEMPLATE_PATH?>/vendor/fontawesome-free/css/all.min.css" rel="stylesheet" type="text/css">
    
    	<!-- Google Fonts -->
    	<link href="https://fonts.googleapis.com/css?family=Merriweather+Sans:400,700&selection.subset=cyrillic" rel="stylesheet">
    	<link href='https://fonts.googleapis.com/css?family=Merriweather:300,300i,400,400i,700,700i&display=swap&subset=cyrillic' rel='stylesheet' type='text/css'>
    
    </head>
    
    <body id="page-top">
    <div id="panel">
        <? $APPLICATION->ShowPanel(); ?> <!-- Отображение административной панели внизу страницы -->
    </div>
    

    В header.php мы оставили только служебный код, а также подключение стилей оформления текста.

    2. Применение шаблона к странице

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

    Изменим шаблон этой страницы (Настройки > Настройки продукта > Сайты > Список сайтов):

    [ds]Значение Сортировки[/ds][di]Обязательно проработайте порядок сортировки для шаблонов (колонка Сорт.). Указанные цифры определят порядок применения шаблонов.

    Подробнее ...[/di] для этого шаблона должно быть меньше, чем значение для основного шаблона.

    3. Размещение и настройка компонента

    • На созданной странице [dw]разместим и настроим[/dw][di] [/di] компонент Новость детально (news.detail). Не вдаваясь, пока, в объяснения почему так, проверьте чтобы в поле ID новости стояло значение ={$_REQUEST["ID"]}
    • Теперь на главной странице сайта откроем настройки размещенного ранее компонента Список новостей. В разделе Шаблоны ссылок пропишем [dw]URL страницы детального просмотра.[/dw][di] [/di]

    В результате при клике по кнопке [dw]Подробнее[/dw][di] [/di] в разделе Новости главной страницы сайта перейдем на страницу подробного описания новости, однако [dw]отображение новости[/dw][di] [/di] не совсем соответствует дизайну нашего сайта. Исправим это на следующем этапе.

    4. Кастомизация компонента

    • Скопируем шаблон компонента Новость детально по аналогии с [ds]уроком.[/ds][di] Это можно сделать двумя способами:

      1. В рамках файловой системы копированием папки /bitrix/components/bitrix/_нужный_компонент_/templates/ в папку /local/templates/шаблон_сайта/components/namespace/название_компонента/_название_шаблона.

      2. Средствами интерфейса системы, разместив на странице компонент и скопировав его с помощью команды Копировать шаблон компонента (при включённом режиме Правка).

      Подробнее...[/di]
    • В папке скопированного шаблона компонента news.detail откроем в режиме редактирования файл template.php.
    • Оформим детальный показ новости в том же стиле, что и показ анонса новостей. Для этого в файле исходной вёрстки страницы index.html скопируем [dw]часть кода,[/dw][di] [/di] отвечающую за стиль блока Новости.
    • После первой строки <? if (!defined('B_PROLOG_INCLUDED') || B_PROLOG_INCLUDED !== true) die(); ?> (защита от подключения файла напрямую без подключения ядра) вставим скопированный ранее код вёрстки. Не забудьте поставить закрывающий тег </section>.
    • В коде шаблона компонента, который скопировали, оставим только те части, которые отвечают за вывод картинки и текста.
    • Итоговый код кастомизированного шаблона компонента news.detail

    Теперь осталось в настройках компонента news.detail установить подготовленный нами [dw]шаблон[/dw][di] [/di] и проверить [dw]результат.[/dw][di] [/di]



    Меню: кастомизация шаблона компонента

    До этого момента навигация на нашем тестовом сайте-лендинге осуществлялась с помощью [ds]html-якорей,[/ds][di] Якорем называется закладка с уникальным именем на определенном месте веб-страницы, предназначенная для создания перехода к ней по ссылке. Якоря удобно применять в документах большого объема, чтобы можно было быстро переходить к нужному разделу.

    Для создания якоря следует вначале сделать закладку в соответствующем месте и дать ей имя при помощи атрибута name тега <a>. В качестве значения href для перехода к этому якорю используется имя закладки с символом решетки (#) впереди.

    Подробнее...[/di] что довольно удобно для переходов на "длинной" странице.

    Однако для более масштабных сайтов, имеющих несколько разделов, появляется необходимость создать [ds]меню[/ds][di] В общем случае задача формирования меню включает:



    - выделение HTML элементов для построения меню;

    -создание шаблона меню (создание шаблона компонента Меню);

    -включение функции показа меню (вызов компонента Меню) в общем шаблоне ("прологе" и "эпилоге");

    -заполнение меню в соответствии со структурой сайта.

    Подробнее...[/di] в качестве средства навигации по сайту.

    В этом уроке разберём создание [ds]статического [/ds][di] Как вы уже знаете, в "1С-Битрикс: Управление сайтом" информация делится на два вида - статическую и динамическую . Статическая вводится непосредственно на странице сайта, меняется редко и вручную. Динамическая размещается с помощью программного кода, меняется автоматически в зависимости от изменений в источнике данных - инфоблоке. Кроме того, эти два типа информации отличаются способами хранения, вывода и обработки.

    Подробнее...[/di] нижнего меню, выводящегося компонентом [comp include_133381]Меню[/comp] (понятия "нижнее", "верхнее" - условны, и влияют только на названия файлов и отображение при создании нового пункта; т.е. рассмотренное в данном уроке меню можно расположить в любой части сайта: и в шапке, и в подвале).

    Итак, для создания меню выполним следующие шаги:

    1. Кастомизация шаблона компонента Меню

    • Скопируйте шаблон компонента Меню. Это можно сделать двумя способами:
      • В рамках файловой системы копированием папки Контент > Структура сайта > Файлы и папки > bitrix > templates > [ваш шаблон] > components > bitrix > [название компонента] > [название шаблона компонента].
      • Средствами интерфейса системы, разместив на странице компонент и скопировав его с помощью команды [dw]Копировать шаблон компонента[/dw][di] [/di] (при включённом режиме Правка).
    • Задайте скопированному шаблону новое название (переименуйте папку шаблона). Например, bottom_menu.
    • В папке скопированного шаблона компонента Меню откройте в режиме редактирования файл template.php. Теперь нужно отредактировать этот шаблон в соответствии с исходным html-макетом сайта (т.е. согласно исходному файлу index.html):

      <?if (!defined("B_PROLOG_INCLUDED") || B_PROLOG_INCLUDED!==true)die();?>
      
      <?if (!empty($arResult)):?>
      <nav class="navbar navbar-expand-lg navbar-light">
      	<ul class="navbar-nav mx-auto"> 
      
      <?
      foreach($arResult as $arItem):
      	if($arParams["MAX_LEVEL"] == 1 && $arItem["DEPTH_LEVEL"] > 1) 
      		continue;
      ?>
      	<?if($arItem["SELECTED"]):?>
      		<li class="nav-item"><a href="<?=$arItem["LINK"]?>" class="selected nav-link"><?=$arItem["TEXT"]?></a></li>
      	<?else:?>
      		<li class="nav-item"><a href="<?=$arItem["LINK"]?>" class="nav-link"><?=$arItem["TEXT"]?></a></li>
      	<?endif?>
      
      <?endforeach?>
      
      </ul>
      </nav>
      <?endif?>
      

      Добавлены служебные классы навигационной панели navbar, а также классы оформления:

      • navbar-expand-lg - горизонтальное расположение элементов на больших экранах (при уменьшении экрана элементы автоматически станут отображаться вертикальным списком);
      • navbar-light - светлая тема оформления;
      • mx-auto - выравнивание блока по центру по оси ОХ.

      Все используемые классы и стили можно посмотреть в документации используемой библиотеки шаблонов (в нашем примере - [ds]Bootstrap[/ds][di] Навигационная панель.

      Документация и примеры для мощного и отзывчивого навигационного заголовка Bootstrap и навигационной панели. Включает поддержку брендинга, навигации и прочего, включая поддержку нашего плагина для сворачивания.

      Подробнее...[/di]).

    • Кроме того, у нас нет необходимости в специализированных стилях и картинках исходного шаблона компонента Меню. Поэтому можно удалить из папки нового шаблона файлы style.css, style.min.css и папку images.

    2. Размещение компонента Меню в подвале сайта.

    • Скопируем код вызова компонента. Это можно сделать следующими способами:
      • скопировать вызов из документации;
      • разместить на странице компонент Меню, и в [dw]совмещенном режиме просмотра[/dw][di] [/di] скопировать код вызова.
    • Добавим скопированный код вызова в файл footer.php (Контент > Структура сайта > Файлы и папки > bitrix > templates > [шаблон вашего сайта] ):

      В кавычках указывается название используемого шаблона компонента. В нашем случае bottom_menu.

    • В результате этого этапа меню будет выводиться в подвале сайта [dw]кастомизированным шаблоном.[/dw][di] [/di]

    Примечание: Подробнее о создании самого меню читайте в соответствующей [ds]главе.[/ds][di] В данном разделе приводятся основные принципы и правила создания и управления меню на сайте, в том числе описание файла данных и структуры шаблона меню. Также в разделе описывается возможность использования динамических меню.

    Подробнее...[/di]



    Управление служебными данными шаблона

    Кодировка страниц и формат отображения дат

    Bitrix Framework поддерживает кодировку UTF-8, в которой на одной странице могут сочетаться различные языки – от русского до иероглифов, что позволяет не заботиться о кодировке страниц. Рекомендуем вам разработку сайтов именно в кодировке UTF-8, что позволит вам избежать проблем с настройками однобайтных кодировок.

    Настройка кодировки выполняется отдельно для административного и публичного раздела.

    Управление метаданными

    Основной целью использования метаданных является оптимизация сайта для поисковых систем. Поисковые системы используют метаданные для индексации документов сайта.

    Примером управления метаданными в продукте может служить механизм задания ключевых слов и описаний для страниц и разделов сайта. По умолчанию в дистрибутиве продукта настроено управление именно этими двумя типами метаданных (по аналогичной схеме список возможных вариантов может быть дополнен).

    Набор свойств может быть задан отдельно для каждого сайта, работающего под управлением системы.

    Управление заголовком документа

    Одним из самых важных элементов сайта на сегодняшний день с позиции SEO-продвижения является заголовок окна браузера (тэг <title>). Практически повсеместно требуется, чтобы заголовок окна браузера отличался от заголовка страницы (тэг <H1>). В «1C-Битрикс» включена поддержка этого требования.

    Установка заголовка производится как с помощью свойств страницы, так и по настройкам компонентов, размещенных на странице. Установка дополнительного заголовка окна веб-браузера осуществляется с помощью зарезервированого в продукте свойства title.

    Управление кодировкой страниц

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

    Использование кодировок

    Одной из важных особенностей Bitrix Framework является поддержка произвольного количества языков. Система позволяет:

    • использовать многоязычный интерфейс в административном разделе;
    • создавать произвольное количество сайтов (в зависимости от лицензии) на различных языках в рамках одной системы.
    Примечание: Количество используемых в системе языков не зависит от количества сайтов.

    Для корректного отображения национальных символов используются соответствующие кодировки. При показе страницы браузер распознает используемую кодировку и на ее основе выполняет отображение символов.

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

    ЯзыкКодировка
    Russian (ru)windows-1251, koi8-r, iso-8859-5
    English (en)windows-1252, iso-8859-1, latin1
    German (de)windows-1252, iso-8859-1, latin1

    Полный список кодировок, используемых для различных языков, приводится в документации продукта.

    Примечание: Начиная с версии 7.0, в продукте (для баз данных MySql и Oracle) поддерживается универсальная кодировка UTF-8. С ее помощью содержимое сайта может быть одновременно представлено на разных языках.
    Если UTF-8 не используется, но в системе необходимо сочетание разных языков, то для каждого языка нужно определить кодовую таблицу, с использованием которой будут отображаться текстовые сообщения.

    Внимание! Кодировка страниц и кодировка таблиц базы данных должны совпадать.

    Настройка кодировок

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

    • Настройка кодировки, используемой в публичном разделе, выполняется для каждого сайта (Настройки > Настройки продукта > Сайты > Список сайтов):

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

      Параметры

    • Настройка кодировки для административного раздела сайта выполняется через форму управления параметрами языков, используемых в системе (Настройки > Настройки продукта > Языковые параметры > Языки интерфейса).

      Также при настройке параметров языка можно определить формат времени и даты.

      Добавление языка

      Указанный формат будет использоваться при отображении даты и времени в административном разделе сайта.

    Определение текущей кодировки

    Текущая кодировка, используемая в публичном разделе сайта, определяется с помощью php-константы LANG_CHARSET, подставленной в область заголовка шаблона сайта.

    При применении шаблона к сайту запрашивается значение параметра кодировка, заданное в настройках сайта. Константе LANG_CHARSET присваивается значение, равное значению параметра кодировка.

    Пример кода, с помощью которого выполняется установка кодировки страниц, приводится ниже:

    <head>
    …
    <meta http-equiv="Content-Type" content="text/html; charset=<?echo LANG_CHARSET?>">
    …
    <head> 

    Управление заголовком документа

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

    Управление

    Создание и изменение заголовка страницы, а также окна веб-браузера может быть выполнено как с помощью средств административного, так и публичного раздела.

    Примечание: Установка дополнительного заголовка окна веб-браузера осуществляется с помощью зарезервированного в продукте свойства title.

    Для удобного использования свойства следует задать его название (например, Дополнительный заголовок (заголовок окна веб-браузера)) в настройках модуля Управление структурой.

    Подробнее про установку нескольких заголовков смотрите на странице Примеры работы.

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

    Заголовок страницы

    Он может быть установлен следующими способами.

    • При редактировании документа с помощью встроенного редактора (в режиме «Текст», «PHP» или «HTML»). В этом случае заголовок страницы задается путем подстановки в код документа следующей функции:
      <?
      $APPLICATION->SetTitle("О компании");
      ?>
    • Может устанавливаться динамически одним из компонентов, расположенный на странице. В приведенном ниже примере в качестве заголовка страницы используется имя текущего информационного блока:
      <?
      $arIBlock = GetIBlock($ID, "news")
      $APPLICATION->SetTitle($arIBlock["NAME"]);
      …
      ?>

    Вывод заголовка документа выполняется с помощью размещения функции ShowTitle() в месте показа заголовка страницы:

    <H1><?$APPLICATION->ShowTitle()?></H1>

    Если при выводе заголовка страницы функция ShowTitle() использует параметр false, это означает, что для установки не будет проверяться значение свойства title (например, Дополнительный заголовок).

    <H1><?$APPLICATION->ShowTitle(false)?></H1>

    Т.е. в качестве заголовка страницы будет использовано значение, установленное функцией SetTitle().

    Примечание: Подробнее про работу функции ShowTitle(false) смотрите на странице Примеры работы.

    Заголовок окна веб-браузера

    Его вывод выполняется с помощью кода ShowTitle(), размещенного в области <head> шаблона дизайна сайта:

    <head><title><?$APPLICATION->ShowTitle()?></title></head>

    Заголовок окна веб-браузера может быть установлен с использованием различных механизмов. По умолчанию он устанавливается в свойстве страницы title (например, Дополнительный заголовок). Если значение данного свойства не указано, то заголовок окна браузера будет установлен равным текущему заголовку страницы.

    Примечание: Заголовок окна браузера может также устанавливаться с помощью функции SetPageProperty() или задаваться в публичной части сайта. Подробнее смотрите на странице Примеры работы.

    Внимание! Если на странице располагается несколько одинаковых функций или компонентов, которые могут устанавливать заголовок, то использоваться будет то значение, которое задается в самой последней (самой нижней на странице) функции/компоненте.


    Примеры работы

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

    В результате мы должны получить примерно следующий результат:

    Разные заголовки


    Выполним следующее:

    • Зададим заголовок страницы из интерфейса или с помощью функции $APPLICATION->SetTitle("Заголовок страницы"):
      <? require($_SERVER["DOCUMENT_ROOT"]."/bitrix/header.php");
      $APPLICATION->SetTitle("Заголовок страницы"); ?>
      
      <? require($_SERVER["DOCUMENT_ROOT"]."/bitrix/footer.php"); ?>
      

    • Добавим дополнительный заголовок через интерфейс или с помощью функции $APPLICATION->SetPageProperty('title','Альтернативный заголовок'):
      <? require($_SERVER["DOCUMENT_ROOT"]."/bitrix/header.php");
      $APPLICATION->SetTitle("Заголовок страницы"); ?>
      
      <? $APPLICATION->SetPageProperty('title','Альтернативный заголовок'); ?>
      
      <? require($_SERVER["DOCUMENT_ROOT"]."/bitrix/footer.php"); ?>
      

      В системе функция $APPLICATION->SetPageProperty() имеет приоритет над функцией $APPLICATION->SetTitle() , поэтому в заголовке браузера и страницы будет отображено именно ее содержимое:

    • Изменим шаблон страницы, если необходимо, так, чтобы вместо функции $APPLICATION->ShowTitle() заголовок страницы (не браузера) задавала $APPLICATION->ShowTitle(false).

      В таком случае будет игнорироваться значение свойства страницы SetPageProperty('title','Альтернативный заголовок') и в качестве заголовка страницы будет использовано значение, установленное функцией SetTitle().

      Пример работы кода из предыдущего пункта, но с измененной функцией $APPLICATION->ShowTitle() на $APPLICATION->ShowTitle(false) в шаблоне сайта:


      Отдельно продемонстрируем разницу работы функции $APPLICATION->ShowTitle() с параметром false на следующем примере, без изменения кода шаблона:

      <? require($_SERVER["DOCUMENT_ROOT"]."/bitrix/header.php");
      $APPLICATION->SetTitle("Заголовок страницы"); ?>
      <? $APPLICATION->SetPageProperty('title','Альтернативный заголовок'); ?>
      
      <? $APPLICATION->ShowTitle(); ?>
      <br>
      <? $APPLICATION->ShowTitle(false); ?>
      
      <?require($_SERVER["DOCUMENT_ROOT"]."/bitrix/footer.php");?>
      

    • Дополнительный пример работы нескольких функций по установке заголовка:
      <? require($_SERVER["DOCUMENT_ROOT"]."/bitrix/header.php");
      $APPLICATION->SetTitle("Заголовок страницы"); ?>
      
      <?
      $APPLICATION->SetPageProperty('title','Альтернативный заголовок');
      //......
      $APPLICATION->SetTitle("Заголовок страницы 2"); // заголовок установлен не будет т.к. у этой функции приоритет меньше 
      //.....
      $APPLICATION->SetPageProperty('title','Альтернативный заголовок 2'); // будет установлен именно этот заголовок т.к функция расположена ниже по коду
      ?>
      
      <? require($_SERVER["DOCUMENT_ROOT"]."/bitrix/footer.php"); ?>
      

    Управление стилями

    Таблицы стилей

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

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

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

    .forumborder {background-color:#CACBBD;}
    .forumhead
     {
          background-color:#EAEBE2;
     }
    .forumbody
     {
          background-color:#FBFBF9;
     }
    .forumbodytext
     {
          font-family: Arial, Helvetica, sans-serif; 
          font-size:smaller; 
          color:#000000;
     }
    .forumheadtext
     {
          font-family: Arial, Helvetica, sans-serif;
          font-size:smaller; 
          color:#000000;
     }
    .forumtitletext
     {
          font-family: Arial, Helvetica, sans-serif;
          font-size:smaller;
          color:#000000;
     }
    
    .postsep
     {
          background-color: #9C9A9C; 
          height: 1px
     }
    
    .forumquote
     {
          font-family: Arial, Helvetica, sans-serif; 
          font-size:8pt;
          color: #000000; 
          background-color: #FBFBF9; 
          border : 1px solid Black;
          padding-top: 2px; 
          padding-right: 2px; 
          padding-bottom: 2px; 
          padding-left: 2px; 
          text-indent: 2pt;
     }
    .forumcode
     {
          font-family: Arial, Helvetica, sans-serif;
          font-size:8pt; 
          color: #333333; 
          background-color: #FBFBF9; 
          border : 1px solid Black; 
          padding-top: 2px; 
          padding-right: 2px; 
          padding-bottom: 2px; 
          padding-left: 2px; 
          text-indent: 2pt;
     }
    …

    .forumborder {background-color:#96C0FA;}
    .forumhead {background-color:#A9CAF7;}
    .forumbody {background-color:#D7E6FB;}
    .forumbodytext {
    	font-family: Tahoma, Arial, Verdana, sans-serif;
    	font-size: 80%;
    	color:#042A69;
    }
    .forumheadtext {
    	font-family: Tahoma, Arial, Verdana, sans-serif;
    	font-size: 80%;
    	color:#011B46;
    …

    Таблицы стилей настраиваются отдельно для каждого шаблона сайта, используемого в системе, и размещаются в папках соответствующих шаблонов сайта. Эти таблицы хранятся в файлах вида styles.css папки соответствующего шаблона. Дополнительные таблицы стилей, например, форума, могут иметь имена вида: forum.css.

    Раздельное хранение стилей

    В продукте добавлен механизм, позволяющий осуществлять раздельное хранение стилей:

    • Стили, используемые в шаблоне дизайна, хранятся отдельно в файле template_styles.css. Это - основной CSS-файл шаблона.
    • стили, используемые при оформлении контента страниц (стили сайта), хранятся в файле styles.css. Стили из этого файла выводятся в выпадающем списке стилей при редактировании страниц в визуальном редакторе.

    Разграничивать назначение этих файлов можно условно, исходя из контекста. Точнее в файле styles.css возможно располагать те стили, которые должны формировать отображение контента не только для всего шаблона, но и для визуального редактора.

    К примеру: на сайте для всех заголовков определены соответствующие стили отображения, т.е. они используются как для оформления контента страниц, так и для оформления контента блоков, которые находятся вне #WORK_AREA#. Таким образом, мы должны были бы поместить стили этих заголовков как в файл template_styles.css так и в styles.css. Но так как оба этих файла подключаются к шаблону, то в данном случае все стили для оформления заголовков следует поместить только в файл styles.css, так как он тоже подключается на странице.

    Если же нам потребуется иное оформление заголовков при редактировании страницы в визуальном редакторе, то соответственно, те стили, которые будут отвечать за оформление шаблона сайта, следует поместить в файле template_styles.css, а для визуального редактора в файл styles.css.

    К разнесению стилей по этим двум файлам следует подходить внимательно. К примеру, если необходимо сделать цвет фона сайта серым, а цвет фона в визуальном редакторе — красным, то в файле template_styles.css для тега body необходимо определить background-color:#ccc;, а в файле styles.css для этого же тега: background-color:#ff0000;.

    Файлы на странице сайта подключаются в таком порядке:

    1. styles.css
    2. template_styles.css

    В результате на сайте фон body станет серого цвета, т.к. стиль в последнем подключенном файле template_styles.css «перебьет» стиль, определенный в styles.css. А в визуальном редакторе фон станет красным, так как содержимое визуального редактора представляет собой iframe в который подключаются стили только из файла styles.css причем вставляются они непосредственно в область head с помощью тега <style>.

    Но если в файле styles.css к определению цвета добавить повышение приоритета !important, то стиль из этого файла «перебьет» стиль, определенный в template_styles.css, фон сайта станет также красного цвета, несмотря на то, что файл стилей шаблона подключается последним.

    Стили сайта

    Формирование таблицы стилей сайта (файл styles.css) выполняется на странице редактирования шаблона дизайна (Настройки > Настройки продукта > Сайты > Шаблоны сайтов) на закладке Стили сайта. Важным элементом при создании таблицы стилей страниц является создание названий стилей. Названия следует создавать только для тех стилей, которые планируется [ds]использовать при редактировании страниц[/ds][di]Достаточно часто в работе нам приходится изменить цвет, размер шрифта, выравнивание текста. Чуть реже мы добавляем специальные символы и разрывы страниц. Для всех этих операций мы будем использовать панель инструментов визуального редактора "1С-Битрикс: Управление сайтом":

    Подробнее ...[/di] в режиме визуального HTML-редактора (секция Описания стилей).

    Стили будут доступны в визуальном редакторе [dw]из выпадающего списка[/dw][di]][/di] под именами, определенными в данной форме. Заданные здесь названия будут храниться в файле идентификатор_шаблона>/.styles.php (файл с именами стилей).

    Создание таблицы стилей шаблона дизайна (файл template_styles.css) выполняется на закладке Стили шаблона формы редактирования шаблона сайта:

    Настройка таблицы стилей для шаблона сайта

    Ссылки по теме:

    Механизм реализации

    Таблицы стилей подключаются к шаблону сайта в области пролога с помощью функции ShowCSS().

    <?
    $APPLICATION->ShowCSS(); 
    ?>

    Функция ShowCSS() выполняет подключение файла стилей из текущего шаблона сайта, а также всех дополнительных стилей определенных для данной страницы функцией SetAdditionalCSS().

    <?	
    $APPLICATION->SetAdditionalCSS("/bitrix/templates/demo/additional.css");
    ?>

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


    При использовании функции ShowCSS() без параметров подключение стилей будет выполнено в виде ссылки на CSS файл:

    <LINK href="/bitrix/templates/demo/styles.css" type="text/css" rel="STYLESHEET">

    При этом стили, подключаемые с использованием SetAdditionalCSS(), будут включены в код страницы с использованием PHP функции require() (т.е. будут полностью включены в итоговый код страницы).


    В случае использования функции ShowCSS() с параметром false файл стилей для текущего дизайна будет также включен в код страницы с использованием require():

    <?
    $APPLICATION->ShowCSS(false);
    ?>

    Работа со стилями в визуальном HTML-редакторе

    Для визуального редактора присутствует возможность к уже имеющимся стандартным стилям добавлять собственные и помещать их в соответствующую панель редактора. Чтобы стили отображались в меню, файл styles.css в шаблоне сайта должен существовать и быть непустым.

    Чтобы подключать сторонние стили в визуальном редакторе, например Bootstrap и Font Awesome, нужно прописать данную возможность в файле description.php, находящемся в папке шаблона сайта:

    "EDITOR_STYLES" => array (
    		'/bitrix/css/main/bootstrap.css',
    		'/bitrix/css/main/font-awesome.css',
    

    Внимание! Ошибки при создании стилей могут вызвать серьезные проблемы в работе сайта, поэтому данную задачу лучше предоставить разработчику с необходимой квалификацией.

    Для примера создадим простой стиль, добавляющий тексту рамку и меняющий его цвет на красный. Для начала добавим в файл styles.css описание стиля.

    .example {
    	border: 2px solid red; 
    	color: red;
    	padding: 20px; 
    }
    

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

    Чтобы созданный стиль отображался в меню, нужно добавить соответствующую строку в файл .styles.php.

    <?
    return array (
    "example" => array(
            "tag" => 'p', /*в какой тег будет помещен текст в данном стиле*/
            "title" => "Это тестовый стиль", /*название стиля*/
            "html" => '<span style="border: 2px solid red;color: #ff0000; padding: 4px;">Пример</span>')  /*оформление текста с помощью html*/
    );
    ?>
    

    Примечание: Поля "tag" и "title" являются обязательными для заполнения. Также поле "title" по умолчанию используется как название пункта в списке, соответствующему стилю, и всплывающая подсказка при наведении курсора на него. Однако при использовании поля "html" можно также указать название, которое и будет отображаться в выпадающем списке.

    Также можно разбивать стили по категориям с помощью поля "section". Пока что собственные стили можно помещать только в уже существующие категории.

    Теперь созданный нами стиль отобразится в меню.

    И его можно применять:


    Пример таблицы стилей для шаблона сайта

    Стили для левого меню:

    .leftmenu, .leftmenuact {font-family: Arial, Helvetica, sans-serif;
     font-size:12px; font-weight: bold; text-decoration: none;}
    .leftmenu {color: #3F3F3F;}
    .leftmenuact  {color:#FF5235;}

    Стили для верхнего меню:

    .topmenu  {font-family: Arial, Helvetica, sans-serif; font-size:12px;
     font-weight: bold; color: #FFFFFF; text-decoration: none;}
    .topmenuact  {font-family: Arial, Helvetica, sans-serif; font-size:12px; 
     font-weight: bold; color: #FAC535; text-decoration: none;}

    Стили для таблиц:

    .tableborder  {background-color:#9C9A9C;}
    .tablehead  {background-color:#D8D9DA;}
    .tablebody  {background-color:#F8F8F8;}
    .tablebodytext, .tableheadtext {font-family: Arial, Helvetica, sans-serif;
     font-size:12px;}
    .tablebodytext {color:#000000;}
    .tableheadtext {color:#000066;}

    Управление значениями метаданных

    Как правило, основной целью использования метаданных является оптимизация сайта для поисковых систем. Поисковые системы используют метаданные для индексации документов сайта.

    Примером управления метаданными в продукте может служить механизм задания ключевых слов и описаний для страниц и разделов сайта. По умолчанию в дистрибутиве продукта настроено управление именно этими двумя типами метаданных (по аналогичной схеме список возможных вариантов может быть дополнен).

    Управление значениями метаданных через визуальный интерфейс

    Чтобы иметь возможность управлять значениями метаданных, предварительно необходимо создать соответствующие свойства в настройках модуля Управление структурой (Настройки > Настройки продукта > Настройки модулей >Управление структурой):

    Важно! Названия типов свойств, используемых для управления метаданными страниц, должны совпадать с названиями мета-тегов в языке HTML. Например, типы свойств ключевые слова и описание должны совпадать именами (name) соответствующих мета-тегов: keywords и description.

    Примечание: Набор свойств может быть задан отдельно для каждого сайта, работающего под управлением системы.

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

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

    Управление метаданными в коде

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

    <head>
    …
    <?$APPLICATION->ShowMeta("keywords")?>
    <?$APPLICATION->ShowMeta("description")?>
    …
    </head>

    Предположим, что для страницы заданы следующие значения свойств ключевые слова и описание:

    C помощью функции SetPageProperty() значения данных свойств будут применены к странице:

    <?
    $APPLICATION->SetPageProperty("keywords","веб,разработка,программирование");
    $APPLICATION->SetPageProperty("description","Система управления сайтом");
    ?>
    Примечание: Настройка свойств раздела может быть выполнена с помощью функции SetDirProperty() (например, в коде файла .section.php):
    <?
    …
    $APPLICATION->SetDirProperty("keywords","дизайн, веб, сайт");
    …
    ?>

    В коде файла за это отвечает массив $arDirProperties:

    $arDirProperties = array(
    	"description" => "",
    	"keywords" => "дизайн, веб, сайт",
    	"title" => "",
    );

    Тогда в результате работы функции ShowMeta() в код страницы будет подставлен следующий HTML-код:

    <meta name="keywords" content="веб,разработка,программирование">
    <meta name="description" content="Система управления сайтом">

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

     $APPLICATION->SetPageProperty("description",$arIBlockElement["PROPERTIES"][$META_DESCRIPTION]["VALUE"]);

    В данном случае в качестве значения свойства страницы description будет использовано значение свойства элемента информационного блока с кодом meta_description. Таким образом, можно создавать свойства keywords и description для элементов каталога и динамически подставлять их в код страницы.


    Ссылки по теме:

    Интеграция компонентов

    Интеграция компонентов

    Цитатник веб-разработчиков.

    Антон Долганин:

    Я для себя выделил примерно так градацию создания магазинов (не учитывая супер-пупер уникальные):

    • 50-70 часов на создание "можно торговать", дизайн подключен, все работает.
    • 30 часов - тюнинг, комментарии клиента, которые не учел в начале.
    • Хитрые SKU или сложное оформление накидывают еще 50 часов.
    Это именно боевые часы, сел и работаешь, а не офисные часы.

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

    Пример выделения функциональных областей:

    Компоненты, которые должны выполнять функции в этих областях:

    В зависимости от того, где должен быть расположен компонент в файле header.php или footer.php, удаляется html код, отображающий данную функциональную область и вставляется код компонента.

    Если шаблон редактируется без визуального редактора системы (а рекомендуется именно такой вариант), то возможны 2 варианта вставки кода визуальных компонентов:

    • Из Пользовательской документации. В ней описан формат вызова всех компонентов.
    • Вставкой кода в визуальном редакторе на какой-нибудь тестовой странице. Полученный код копируется в нужное место шаблона.

      Примечание: Рекомендуется этот способ в силу того, что:
      • документация, хоть и редко, но может отставать от актуальной версии,
      • заказчиком может использоваться устаревшая версия продукта.

    Примеры размещения компонентов

    Компоненты версии 2.0 подключаются в коде страницы с помощью функции IncludeComponent(). В качестве параметров функции используется:

    • название компонента в форме <пространство_имен>:<название_компонента>. Причем название компонента рекомендуется строить иерархически, начиная с общего понятия и заканчивая конкретным назначением компонента. Например, catalog, catalog.element, catalog.section.list и т.п.
    • название шаблона. Шаблон компонента "по умолчанию" можно задавать пустой строкой, также можно явно определять .default
    • массив параметров компонента Array(...)

    Добавление включаемых областей

    Код для вставки компонента Включаемая область (bitrix:main.include), с настройкой на вывод из файла, выглядит так:

    <?$APPLICATION->IncludeComponent(
    	"bitrix:main.include",
    	"",
    	Array(
    	"AREA_FILE_SHOW" => "file",
    	"PATH" => $APPLICATION->GetTemplatePath("include_areas/company_name.php"),
    	"EDIT_TEMPLATE" => ""
    	)
    );?>

    В этом примере include_areas/company_name.php - файл с включаемой областью:

    <?if(!defined("B_PROLOG_INCLUDED") || B_PROLOG_INCLUDED!==true)die();?>
      
    World Book
    Все книги мира

    В файле включаемой области не требуется подключать пролог и эпилог (файлы header.php и footer.php). Необходимо лишь проверить, что файл включаемой области подключен из системы, а не вызван напрямую. Это делает строка:

    <?if(!defined("B_PROLOG_INCLUDED") || B_PROLOG_INCLUDED!==true)die();?>

    Меню

    Для подключения меню необходимо вставить в нужное место шаблона вызов компонента:

    <?$APPLICATION->IncludeComponent(
    	"bitrix:menu",
    	"horizontal_multilevel",
    	Array(
    		"ROOT_MENU_TYPE" => "top", 
    		"MAX_LEVEL" => "3", 
    		"CHILD_MENU_TYPE" => "left", 
    		"USE_EXT" => "Y", 
    		"MENU_CACHE_TYPE" => "A",
    		"MENU_CACHE_TIME" => "3600",
    		"MENU_CACHE_USE_GROUPS" => "Y",
    		"MENU_CACHE_GET_VARS" => Array()
    	)
    );?>

    Это можно сделать как через визуальный редактор, так и через PHP-код шаблона.

    Список ссылок по теме:

    Что такое компонент

    Компоненты

    Цитатник веб-разработчиков.

    Роман Петров: Начиная работать с 1С-Битрикс, ребята говорили «здесь надо свой компонент», «свое свойство» и т.д. Сейчас познали ДАО стандартных компонентов и счастливы.

    Компоненты - это основной инструмент разработчика при работе с проектами, созданными на Bitrix Framework. От умения владеть этим инструментом во многом зависит профессионализм разработчика.

    Компонент - это логически завершённый код, предназначенный для извлечения информации из инфоблоков и других источников и преобразования её в HTML-код для отображения в виде фрагментов web-страниц. Состоит из собственно компонента (контроллер) и шаблона (представление). Компонент, с помощью API одного или нескольких модулей, манипулирует данными. Шаблон компонента выводит данные на страницу.

    Классическая схема работы компонента:

    Carrier Rider Mapper

    Компоненты в полной мере реализуют паттерн проектирования Carrier Rider Mapper.

    • Carrier. Носитель любой информации к которой могут иметь доступ несколько клиентов одновременно.
    • Rider (Reader либо Writer) - объекты, посредством которых Carrier предоставляет доступ к хранимой в нём информации. Клиенты считывают и записывают информацию хранимую в Carrier исключительно только посредством объектов типа Reader и Writer. Таким образом, Reader и Writer - интерфейсы доступа к информации.
    • Mapper (Scanner либо Formatter) - объекты обёртки над Reader либо Writer соответственно. Мапперы отвечают за преобразование форматов данных в удобные для клиентов форматы.

    Поток информации от носителя к клиенту (считывание): Carrier > Reader > Scanner > Client.

    Поток информации от клиента к носителю (запись): Carrier < Writer < Formatter < Client.

    Введение прослойки мапперов между Carrier-Rider и клиентами позволяет соединять один и тот же Carrier-Rider с разными типами клиентов посредством соответствующих (разных) мапперов.

    Использование

    Компоненты используются для:

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

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

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

    Компоненты могут быть простыми и комплексными.

    Особенности технологии

    • В компонентах разделена логика и визуальное представление. Логика - это сам компонент, представление - это шаблон для вывода данных. Для одной логики может быть создано несколько представлений, в том числе зависящих от шаблона текущего сайта. Визуальное представление (шаблон вывода) может быть написано на любом шаблонном языке, который можно подключить из PHP. Например, шаблоны могут быть на PHP, Smarty, XSL и т.д.
    • В компонентах нет необходимости изменять его логику для изменения особенностей его показа. Поэтому управлять внешним видом выводимой информации значительно легче. Шаблон вывода существенно проще, чем компонент в целом.
    • Компоненты централизованно хранятся в одной папке. Это обеспечивает большую целостность и понятность структуры сайта. Папка доступна для обращений, а значит компонент и его шаблоны, могут легко подключать свои дополнительные ресурсы.

    Примечание: Эволюция развития Bitrix Framework через компоненты обычного стандарта привела к действующим сейчас компонентам в стандарте 2.0. Обычный стандарт (компоненты версии 1.0) в данное время не поддерживается. Но в некоторых случаях возможно встретить устаревшие компоненты в проектах, работающих на старых версиях 1С-Битрикс: Управление сайтом. Если встретился такой анахронизм, обратитесь к документации.

    Простые и комплексные компоненты

    Цитатник веб-разработчиков.

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

    Компоненты делятся на простые (одностраничные) и комплексные (многостраничные). Простой компонент реализует вывод на одной физической странице, доступной под конкретным URL. Комплексный же заменяет собой набор простых компонентов. Например, создание новостного раздела можно реализовать несколькими простыми компонентами, размещаемыми каждый на отдельной физической странице, а можно - одним комплексным, размещенным на одной физической странице.

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

    Простые компоненты

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

    Комплексные компоненты

    Комплексные (сложные, многостраничные) компоненты создают разделы сайта. Например, компонент каталога создает на сайте весь раздел каталога: список каталогов, список групп и страницы товаров. То есть, комплексный компонент состоит из набора динамических страниц при просмотре сайта, но из одной статической страницы на физическом уровне. Комплексные компоненты строятся на основе простых, используя их логику.

    Преимущество комплексных компонентов состоит в автоматической компоновке параметров одностраничных компонентов и отсутствии необходимости их связывать.

    Комплексные компоненты разрешают следующие проблемы:

    • Отпадает необходимость создания большого числа статических страниц для размещения всех требуемых простых компонентов. Отпадает необходимость отдельно настраивать для каждого из них общие (пересекающиеся) свойства (например, тип инфоблока и инфоблок).
    • Происходит установление сложных взаимосвязей между простыми компонентами. Например, нет необходимости для страницы со списком сообщений темы форума настраивать, как эта страница может указать на страницу списка тем форума, а как на страницу профиля посетителя.
    • В компонент (даже с кастомизированными шаблонами вывода) можно добавить новую страницу. Например, если на форуме появится страница (простой компонент) по выводу 10 посетителей с самым высоким рейтингом, то эта страница станет доступной и в публичной части.
    • Можно сменить шаблон вывода всего комплексного компонента одним действием, а не настраивать вывод каждого из простых компонентов.

    Алгоритм работы комплексного компонента таков:

    1. на основании действий посетителя сайта (например, переход по пунктам меню) комплексный компонент определяет, какая страница должна быть показана пользователю, и подключает свой шаблон компонента для этой динамической страницы;
    2. шаблон страницы подключает обычные компоненты, автоматически настраивая необходимым образом их свойства;
    3. обычные компоненты выполняют свою работу: запрашивают данные у ядра системы, форматируют их и выводят посетителю, а также предоставляют пользователю различные элементы управления (ссылки, формы, кнопки и т.п.);
    4. пользователь с помощью каких-либо элементов управления, посылает новый запрос комплексному компоненту.

    Пример

    Рассмотрим упрощённый пример работы комплексного компонента новостей. Пусть у нас есть обычные компоненты: список новостей и детальной новости (последний принимает во входных параметрах код новости, которую нужно показать).

    Раздел новостей можно организовать, например, разместив на странице index.php компонент списка новостей, а на странице news.php - компонент детальной новости. При этом у компонента списка новостей нужно настроить входные параметры так, чтобы он мог формировать ссылки на страницу детальной новости (с кодом новости), а у компонента детальной новости нужно настроить входные параметры так, чтобы он мог формировать ссылку на страницу списка новостей.

    Чтобы задать ссылку на страницу детальной новости, нужно задать путь к этой странице, а так же название параметра, в котором будет передаваться код новости для показа. То же название параметра нужно задать и во входных параметрах компонента детальной новости, чтобы он знал, где брать код новости для показа. Даже в данном максимально упрощённом случае настройки не так просты. А если это набор из десятков компонентов форума?

    Более удобной альтернативой для сборщика сайта будет использование комплексного компонента новостей. Его, например, можно просто установить на страницу index.php. Согласованием ссылок и параметров будет заниматься сам комплексный компонент. От разработчика сайта никаких дополнительных действий не потребуется.

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

    Таким образом реализуется паттерн MVC:

    • на комплексный компонент новостей (controller) приходит HTTP запрос (действия пользователя);
    • controller проверяет, установлен ли через HTTP запрос код новости и подключает из своего шаблона страницу списка новостей или страницу детальной новости (view);
    • эта страница, в свою очередь, подключает соответствующий обычный компонент, устанавливая при этом его входные параметры необходимым образом;
    • обычный компонент выполняет свою работу: запрашивает данные у ядра (model), форматирует их и выводит посетителю, а также отображает элементы управления (ссылки, формы, кнопки и т.п.);
    • пользователь с помощью элементов управления посылает новый HTTP запрос на controller;
    • процедура повторяется по мере надобности.

    Список ссылок по теме:

    Структура компонента

    Компонент хранит все, что ему нужно для работы, в своей папке. Поэтому их можно легко переносить между проектами.

    Структура

    Важно: файлы компонентов нельзя использовать по отдельности. Компонент - это единое целое, он обладает свойством неделимости.

    Структура файлов

    Папка компонента может содержать следующие подпапки и файлы:

    • подпапку /lang, в которой расположены файлы языковых сообщений (переводов) компонента. В ней также могут размещаться папки помощи /help.
    • подпапку /templates, в которой расположены шаблоны вывода (отображения) компонента. Эта подпапка может отсутствовать, если у компонента нет шаблонов вывода.
    • файл component.php, который содержит логику (код) компонента. Задача этого файла - сформировать из полученных параметров ($arParams) массив $arResult, который впоследствии попадет в шаблон компонента. Этот файл должен присутствовать в папке компонента, если только логика компонента не размещена в файле class.php.
    • файл .description.php, который содержит название, описание компонента и его положение в дереве логического размещения (для редактора). Этот файл должен всегда присутствовать в папке компонента. Его отсутствие не скажется на работе компонента, но размещение компонента через визуальный редактор станет невозможным.
    • файл .parameters.php, который содержит описание входных параметров компонента для редактора. Если у компонента есть входные параметры, то этот файл должен присутствовать в папке компонента.
    • файл class.php для поддержки ООП-компонентов. В этом файле так же может размещаться логика (код) компонента.
    • файл script.js, может подключаться из шаблона, а может из [dw]кода компонента[/dw][di]$APPLICATION->AddHeadScript($this->GetPath().'/script.js');[/di].
    • любые другие папки и файлы с ресурсами, необходимыми компоненту, например, папка /images.

    Общая структура компонента

    <?if(!defined("B_PROLOG_INCLUDED") || B_PROLOG_INCLUDED!==true)die();?> 
    <? 
    	//Проверяем и инициализируем входящие параметры компонента 
    	if(вывод компонента находится в валидном кеше) 
    		{ 
    		//Вывод данных из кеша
    		} 
    	else 
    		{ 
    		//Запрос данных и формирование массива $arResult в соответствии со 
    		//структурой,описанной в файле помощи компонента
    		$this->IncludeComponentTemplate(); 
    		//Кеширование вывода 
    		} 
    ?>

    Папки локализаций

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

    Для реализации этой функции при выводе строковых констант выводится не сама константа, а вызывается спецфункция, а в качестве аргумента ей передаётся идентификатор этой константы.

    Пример:

    В файле template.php без локализации:
    <?echo “Торговый каталог”?>
    В файле template.php с локализацией:
    <?echo GetMessage(“CATALOG”)?>
    В этом случае в файле /lang/ru/template.php пишем:
    <?$MESS[“CATALOG”] = “Торговый каталог”;?>

    В папке /lang для каждой языковой версии создаётся папка с названием языка (например, /ru, /en и т.п.), в которой и размещаются файлы языковых сообщений. Рекомендуется называть файл языковых сообщений для какого-либо файла компонента так же, как называется этот файл. В этих файлах находятся массивы, ключами для которых являются идентификаторы констант, а значениями - сами константы, переведенными на соответствующий язык.

    Рекомендуется располагать файлы в той же иерархии относительно папки /lang/код_языка/, в которой файл располагается относительно папки компонента. Например, языковой файл с английскими фразами для файла /install/uninstall.php рекомендуется располагать по пути /lang/en/install/uninstall.php. Подпапка /lang может отсутствовать, если в компоненте нет зависящих от языка фраз.

    Папки локализаций создаются отдельно для компонентов и для каждого из шаблонов.

    Как поменять стандартные надписи в интерфейсе.

    Скопировать шаблон компонента для того, чтобы его можно было кастомизировать (если он еще не кастомизирован). Затем либо:

    • С помощью модуля Перевод: Настройки > Локализация.
    • Либо же вручную редактируете языковые файлы /bitrix/templates/имя_шаблона_сайта/components/имя­_компонента/templates/имя_шаблона/lang/ru/template­.php.

    Подключение языкового файла (файла перевода) компонента

    Языковые файлы в компоненте и всех его стандартных файлах (component.php, .description.php, .parameters.php) подключаются автоматически. В других файлах компонента языковые файлы можно подключить командой:

    $this->IncludeComponentLang($relativePath = "", $lang = False)

    где:
    $relativePath - путь к файлу относительно папки компонента,
    $lang - язык. Если передается False, то используется текущий язык.

    Пример: $this->IncludeComponentLang("myfile.php");


    Подсказки в компонентах

    Bitrix Framework позволяет создать подсказки к параметрам компонента:

    Файл создаётся в языковой папке /lang. В файле - массив $MESS, в котором ключами являются параметры компонента c добавлением суффикса _TIP, значениями - подсказки. В качестве примера:

    $MESS["IBLOCK_TYPE_TIP"] = "Это подсказка для типа инфоблока";
    $MESS["IBLOCK_ID_TIP"] = "Это подсказка для ID инфоблока";
    $MESS["SORT_BY1_TIP"] = "Это подсказка для первой сортировки";

    Нет необходимости создавать отдельный lang-файл с подсказками. Их достаточно сохранить в lang-файле .parameters.php (как для компонента, так и для шаблона компонента). Формат подсказок прежний.

    Подсказки для параметров компонента IBLOCK_TYPE, IBLOCK_ID, SORT_BY1 и SORT_ORDER1.

    Ряд стандартных параметров (CACHE_TIME, AJAX_MODE и другие) имеет несколько подсказок. Для CACHE_TIME:

    • CACHE_TIME - время кеширования;
    • CACHE_TYPE - тип кеширования.

    Для страничной адресации:

    • DISPLAY_TOP_PAGER - показывать постраничку над списком;
    • DISPLAY_BOTTOM_PAGER - показывать постраничку под списком;
    • PAGER_TITLE - название элементов в постраничке;
    • PAGER_SHOW_ALWAYS - показывать постраничку всегда;
    • PAGER_TEMPLATE - имя шаблона постранички;
    • PAGER_DESC_NUMBERING - обратная адресация;
    • PAGER_DESC_NUMBERING_CACHE_TIME - время кеширования обратной адресации.

    Внимание! Нельзя делать подсказки про запас (создать, но оставить пустыми): перестанут показываться настройки компонента.


    Структура комплексного компонента

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

    Пример кода комплексного компонента:

    <?
    if (!defined('B_PROLOG_INCLUDED') || B_PROLOG_INCLUDED !== true) {
        die();
    }
    
    $arDefaultUrlTemplates404 = [
    	'list'    => 'index.php',
    	'element' => '#ELEMENT_ID#.php',
    ];
    
    $arDefaultVariableAliases404 = [];
    $arDefaultVariableAliases    = [];
    $arComponentVariables        = ['IBLOCK_ID', 'ELEMENT_ID'];
    $SEF_FOLDER                  = '';
    $arUrlTemplates              = [];
    
    if ($arParams['SEF_MODE'] == 'Y') {
        
    	$arVariables = [];
    
    	$arUrlTemplates = CComponentEngine::MakeComponentUrlTemplates(
    		$arDefaultUrlTemplates404,
    		$arParams['SEF_URL_TEMPLATES']
    	);
        
    	$arVariableAliases = CComponentEngine::MakeComponentVariableAliases(
    		$arDefaultVariableAliases404,
    	$arParams['VARIABLE_ALIASES']
    	);
    
    	$componentPage = CComponentEngine::ParseComponentPath(
    		$arParams['SEF_FOLDER'],
    		$arUrlTemplates,
    		$arVariables
    	);
    
    	if (strlen($componentPage) <= 0) {
    		$componentPage = 'list';
    	}
    
    	CComponentEngine::InitComponentVariables(
    		$componentPage,
    		$arComponentVariables,
    		$arVariableAliases,
    		$arVariables);
    
    	$SEF_FOLDER = $arParams['SEF_FOLDER'];
    } else {
    	$arVariables = [];
    
    	$arVariableAliases = CComponentEngine::MakeComponentVariableAliases(
    		$arDefaultVariableAliases,
    		$arParams['VARIABLE_ALIASES']
    	);
        
    	CComponentEngine::InitComponentVariables(
    		false,
    		$arComponentVariables,
    		$arVariableAliases,
    		$arVariables
    	);
    
    	$componentPage = '';
        
    	if (intval($arVariables['ELEMENT_ID']) > 0) {
    		$componentPage = 'element';
    	} else {
    		$componentPage = 'list';
    	}
    
    }
    
    $arResult = [
    	'FOLDER'        => $SEF_FOLDER,
    	'URL_TEMPLATES' => $arUrlTemplates,
    	'VARIABLES'     => $arVariables,
    	'ALIASES'       => $arVariableAliases,
    ];
    
    $this->IncludeComponentTemplate($componentPage);
    
    ?>

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

    • $arDefaultUrlTemplates404 - для задания путей по умолчанию для работы в ЧПУ режиме. Каждый элемент массива является шаблоном пути и задается в виде:
      "код шаблона пути" => "шаблон пути"
      В шаблоне пути могут быть использованы конструкции вида "#слово#", которые при формировании реального пути заменяются на значения соответствующих переменных. Например, для шаблона пути:
      "element" => "#ELEMENT_ID#.php"
      реальный путь будет иметь вид 195.php или 7453.php. Шаблоны путей могут иметь параметры, например:
      "element" => "#IBLOCK_ID#/#ELEMENT_ID#.php?SECTION_ID=#SECTION_ID#"
      Должны быть заданы все шаблоны путей, с которыми работает компонент.
    • $arDefaultVariableAliases404 - для задания псевдонимов по умолчанию переменных в режиме ЧПУ. Как правило, этот массив пуст (используются реальные имена переменных). В случае, если необходимо, чтобы в HTTP запросе (в адресе) переменная называлась по другому, можно задать псевдоним этой переменной, а при работе компонента восстанавливать значение переменной из псевдонима. Если для какого-либо шаблона пути нужно задать псевдоним для одной или более переменных, то в этом массиве должен появиться элемент вида:
      "код шаблона пути" => array(
      	"название переменной 1" => "псевдоним переменной 1",
      	"название переменной 2" => "псевдоним переменной 2",
          * * *
          )
      Например, если требуется, чтобы ссылка на страницу детальной информации об элементе инфоблока (например, карточки товара) имела вид:
      "/<мнемонический код инфоблока>/.php?SID=<код группы элементов>"
      то шаблон пути можно задать в виде:
      "element" => "#IBLOCK_ID#/#ELEMENT_ID#.php?SID=#SECTION_ID#"
      а в массиве $arDefaultVariableAliases404 задать псевдоним для переменной SECTION_ID в виде:
      "element" => array(
      	"SECTION_ID" => "SID"
      	)
      В этом случае ссылки (адреса) будут формироваться с параметром SID, а в компонентах будет установлена переменная SECTION_ID.
    • $arDefaultVariableAliases - для задания псевдонимов по умолчанию переменных в режиме не ЧПУ. Как правило, этот массив пуст, то есть используются реальные имена переменных. В случае, если необходимо, чтобы в HTTP запросе (в адресе) переменная называлась по другому, можно задать псевдоним этой переменной, а при работе компонента восстанавливать значение переменной из псевдонима. Если для какой-либо переменной нужно задать псевдоним, то в этом массиве должен появиться элемент вида:
      "название переменной" => "псевдоним переменной"
      Например, если название переменной в компоненте SECTION_ID, но требуется, чтобы в ссылках использовалась переменная SID, то псевдоним для SECTION_ID можно задать в виде
      "SECTION_ID" => "SID"
      В этом случае, ссылки (адреса) будут формироваться с параметром SID, а в компонентах будет установлена переменная SECTION_ID. Все эти массивы или их части могут быть переопределены с помощью входных параметров компонента (при вызове компонента). Например, во входном параметре SEF_URL_TEMPLATES в ЧПУ режиме может быть задан массив:
      "SEF_URL_TEMPLATES" => array(
      	"element" => "#IBLOCK_CODE#/#ELEMENT_ID#.php?GID=#SECTION_ID#"
      	)
      а во входном параметре VARIABLE_ALIASES может быть задан параметр:
      "VARIABLE_ALIASES" => array(
      	"element" => array(
      	"SECTION_ID" => "GID",
      	),
      )
      Тогда в адресах (ссылках) пути будут иметь вид типа /phone/3425.php?GID=28, а в компоненте из них будут восстанавливаться переменные IBLOCK_CODE = phone, ELEMENT_ID = 3425 и SECTION_ID = 28.
    • $arComponentVariables - для задания списка переменных, которые компонент может принимать в HTTP запросе и которые могут иметь псевдонимы. Каждый элемент массива является именем переменной.

    Входной параметр с предопределённым именем SEF_MODE может иметь два значения: Y и N. Если $arParams["SEF_MODE"] равен Y, значит компонент работает в режиме ЧПУ, иначе - нет.

    Входной параметр с предопределённым именем SEF_FOLDER имеет смысл в том случае, если компонент работает в режиме ЧПУ. В этом случае он содержит путь, по которому работает компонент. Путь может быть виртуальным (т.е. физически он может не существовать). Например, компонент из примера может лежать в файле /fld/n.php, при этом он работает в режиме ЧПУ и входной параметр SEF_FOLDER равен /company/news/. Тогда компонент будет откликаться на запросы по адресам /company/news/index.php, /company/news/25.php и т.п.

    Для определения, какую страницу должен показать комплексный компонент, а так же для восстановления переменных компонента из пути и из псевдонимов используются следующие методы.

    • CComponentEngine::MakeComponentUrlTemplates
      CComponentEngine::MakeComponentUrlTemplates(
      	$arDefaultUrlTemplates404, 
      	$arParams['SEF_URL_TEMPLATES']
      );
      Метод объединяет массив по умолчанию шаблонов путей и шаблоны путей, которые были переданы во входных параметрах компонента в один массив. При этом, если в $arParams["SEF_URL_TEMPLATES"] определён шаблон какого-либо пути, то он переопределяет шаблон по умолчанию этого пути.
    • CComponentEngine::MakeComponentVariableAliases
      CComponentEngine::MakeComponentVariableAliases(
      	$arDefaultVariableAliases404, 
      	$arParams['VARIABLE_ALIASES']
      );
      Метод объединяет массив по умолчанию псевдонимов переменных и псевдонимы переменных, которые были переданы во входных параметрах компонента в один массив. При этом, если псевдоним некоторой переменной определён и в массиве по умолчанию, и во входных параметрах, то возвращается псевдоним из входных параметров.
    • CComponentEngine::ParseComponentPath
      CComponentEngine::ParseComponentPath(
      	$arParams['SEF_FOLDER'],
      	$arUrlTemplates,
      	$arVariables
      );
      Метод на основании параметра $arParams["SEF_FOLDER"] и массива шаблонов путей (который вернул метод MakeComponentUrlTemplates) определяет, какому шаблону пути соответствует запрошенный адрес. Если шаблон был найден, возвращается его код, иначе возвращается пустая строка. Кроме того, в переменной $arVariables возвращается массив переменных компонента, который был восстановлен из шаблона пути без параметров. Например, если массив шаблонов путей (который получился из массива $arDefaultUrlTemplates404 после переопределения всех или части шаблонов через входные параметры компонента) имеет вид:
      $arUrlTemplates = array(
      	"list" => "index.php",
      	"element" => "#IBLOCK_ID#/#ELEMENT_ID#.php?SID=#SECTION_ID#"
      );
      Если входной параметр SEF_FOLDER равен /company/news/, а запрошенный адрес равен /company/news/15/7653.php?SID=28, то метод ParseComponentPath вернет строку "element" (код соответствующего шаблона), а массив $arVariables будет иметь вид:
      $arVariables = array(
      	"IBLOCK_ID" => 15,
      	"ELEMENT_ID" => 7653
      )
    • CComponentEngine::InitComponentVariables
      CComponentEngine::InitComponentVariables(
      	$componentPage,
      	$arComponentVariables,
      	$arVariableAliases,
      	$arVariables
      );
      где:
      • $componentPage - код шаблона, который вернул метод ParseComponentPath и которому соответствует запрошенный адрес;
      • $arComponentVariables - массив переменных, которые компонент может принимать в HTTP запросе и которые могут иметь псевдонимы;
      • $arVariableAliases - массив псевдонимов (который вернул метод MakeComponentVariableAliases).

      Метод восстанавливает переменные из $_REQUEST c учётом их возможных псевдонимов и возвращает их в переменной $arVariables. Например, если для кода, приведенного выше, в массиве $arVariableAliases есть запись вида

      "element" => array(
      	"SECTION_ID" => "SID",
      )
      то метод InitComponentVariables в параметре $arVariables вернет массив вида
      $arVariables = array(
      	"IBLOCK_ID" => 15,
      	"ELEMENT_ID" => 7653,
      	"SECTION_ID" => 28
      )
      Здесь методом InitComponentVariables был инициализирован третий элемент массива. Первые два были инициализированы методом ParseComponentPath в примере выше. В случае работы компонента не в режиме ЧПУ, в метод InitComponentVariables первым параметром передается значение False.

    В случае работы компонента в режиме ЧПУ на основании кода шаблона пути и переменных из HTTP запроса (а в случае не-ЧПУ адресов, только на основании переменных из HTTP запроса) компонент определяет, какая страница из шаблона компонента должна подключиться, и передает ее имя в вызов метода:

    $this->IncludeComponentTemplate($componentPage);

    На страницах шаблона комплексного компонента подключаются обычные компоненты и настраиваются их входные параметры на основании входных параметров комплексного компонента, некоторых вычисляемых значений и констант. Например, страница "element" шаблона компонента из примера (файл типа /templates/.default/element.php относительно папки компонента) может иметь вид типа:

    <?
    if (!defined('B_PROLOG_INCLUDED') || B_PROLOG_INCLUDED !== true) {
    	die();
    }
    
    $APPLICATION->IncludeComponent(
    	'bitrix:news.detail',
    	'',
    	[
    		'IBLOCK_ID'  => $arParams['IBLOCK_ID'],
    		'ELEMENT_ID' => $arResult['VARIABLES']['ELEMENT_ID'],
    		'SECTION_ID' => $arResult['VARIABLES']['SECTION_ID'],
    		'CACHE_TIME' => $arParams['CACHE_TIME'],
    	],
    	$component
    );
    ?>

    Последний параметр $component в подключении компонента - объект, представляющий текущий компонент. Он передается в вызов подключения компонента. Таким образом, подключаемый компонент будет знать, что он подключается из комплексного компонента. Соответственно, он сможет пользоваться ресурсами комплексного компонента, вызывать его методы и т.п.


    Размещение в системе и подключение компонента

    Размещение

    Компоненты в Bitrix Framework должны храниться только в определенных местах:

    • в папке /bitrix/components/bitrix (по умолчанию);
    • в папке /bitrix/components/собственное пространство имен.
    • в папке /local/components/ (рекомендуется для собственных компонентов сторонних разработчиков)

    Системные компоненты находятся в папке /bitrix/components/bitrix/. Содержимое этой папки обновляется системой обновлений и не может изменяться пользователями.

    Внимание! Изменение содержимого папки /bitrix/components/bitrix/ может привести к непредсказуемым последствиям.

    Пользовательские компоненты могут находиться в любых других подпапках папки /bitrix/components/ или прямо в папке /bitrix/components/, но рекомендуется папка /local/components/.

    Именование

    Название подпапки директории /bitrix/components/ образует пространство имен (namespace) компонентов. Например, все системные компоненты расположены в пространстве имен bitrix. При создании пользовательских компонентов рекомендуется создать какое-либо пространство имен и размещать свои наработки в нем.

    Например, существует системный компонент news, который расположен в пространстве имен bitrix. Если необходим пользовательский компонент news, который имеет другую функциональность, не реализуемую с помощью шаблона системного компонента, то рекомендуется создать некоторое пространство имен (подпапку) в папке /bitrix/components/ (например, demo) и расположить пользовательский компонент news в этом пространстве имен. Таким образом будут доступны два компонента news, но лежащие в разных пространствах имен: bitrix:news и demo:news.

       

    Имена компонентов имеют вид идентификатор1.идентификатор2.... Например, catalog, catalog.list, catalog.section.element и т.п. Рекомендуется строить имена иерархически, начиная с общего понятия и заканчивая конкретным назначением компонента. Например, компонент, показывающий список товаров данной группы, может называться catalog.section.elements. Полное имя компонента - это имя с указанием пространства имен. Полное имя имеет вид пространство_имен:имя_компонента. Например, bitrix:catalog.list. Если компонент лежит вне пространства имен, то пространство имен не указывается. Например, catalog.section.

    Подключение

    В самом общем виде подключение компонента осуществляется следующим образом:

    <?$APPLICATION->IncludeComponent(
       $componentName,         // имя компонента
       $componentTemplate,     //его шаблон, пустая строка если шаблон по умолчанию
       $arParams=array(),      // параметры
       $parentComponent=null,  // null или объект родительского компонента
       $arFunctionParams=array()
    );?>

    Внутри компонента (файл component.php) доступны следующие предопределенные переменные:

    $componentName – полное название компонента (например: bitrix:news.list).
    $componentTemplate – шаблон, с которым он вызывается.
    $arParams – входные параметры (т.е. параметры с которыми вызывается компонент). Параметры доступны так же по их именам.
    $componentPath – путь к компоненту относительно корня сайта (пример: /bitrix/components/bitrix/news.list).
    $parentComponentName – название родительского компонента (пустое, если нет родителя).
    $parentComponentPath – путь к родительскому компоненту относительно корня сайта (пустой, если нет родителя).
    $parentComponentTemplate – шаблон родительского компонента (пустой, если нет родителя).
    $arResult — результат, чтение/изменение. Затрагивает одноименный член класса компонента.
    $this — естественно ссылка на текущий вызванный компонент (объект класса CBitrixComponent), можно использовать все методы класса.
    Кроме того, в компоненте объявлены глобальными переменные $APPLICATION, $USER, $DB.

    Примечание: Компонент получает все параметры вызова следующим образом:
    1. В ключах, начинающихся с ~, данные содержатся в исходном виде (т.е. без всякой обработки).
      Если это комплексный компонент или в шаблоне компонента вызывается другой и часть параметров передается ему, то необходимо передавать значение ключей с ~.
    2. В ключах без ~ данные приведены к безопасному виду с помощью метода htmlspecialcharsEx. Если ключ содержит массив, то будут обработаны строковые ключи массива (тоже с помощью htmlspecialcharsEx).

    Описание компонента

    В файле .description.php содержится описание компонента. Это описание применяется для работы с компонентом (например, в визуальном редакторе), а также при работе в режиме редактирования сайта. При работе самого компонента (при обращении к странице, на которой он расположен) описание не используется и файл .description.php не подключается.

    Файл .description.php должен находиться в папке компонента. Языковой файл подключается автоматически (должен лежать в папке /lang/<язык>/.description.php относительно папки компонента).

    Структура типичного файла .description.php такова:

    <?
    $arComponentDescription = array(
    	"NAME" => GetMessage("COMP_NAME"),
    	"DESCRIPTION" => GetMessage("COMP_DESCR"),
    	"ICON" => "/images/icon.gif",
    	"PATH" => array(
    		"ID" => "content",
    		"CHILD" => array(
    			"ID" => "catalog",
    			"NAME" => "Каталог товаров"
    		)
    	),
    	"AREA_BUTTONS" => array(
    		array(
    			'URL' => "javascript:alert('Это кнопка!!!');",
    			'SRC' => '/images/button.jpg',
    			'TITLE' => "Это кнопка!"
    		),
    	),
    	"CACHE_PATH" => "Y",
    	"COMPLEX" => "Y"
    );
    ?>

    Как видно, в файле определяется массив $arComponentDescription, который описывает компонент. Этот массив может иметь следующие ключи:

    • "NAME" - название компонента;
    • "DESCRIPTION" - описание;
    • "ICON" - путь к пиктограмме относительно папки компонента. Значок компонента используется в разных частях системы, например: в визуальном редакторе. (Параметр устарел, его можно не использовать. При создании собственных компонентов можно создавать .descripton.php без ICON, несмотря на то, что во многих старых компонентах ICON присутствует.)
    • "PATH" - расположение компонента в виртуальном дереве компонентов в визуальном редакторе. Значением этого элемента должен быть массив, имеющий ключи:
      • "ID" - код ветки дерева. ID узла должен быть уникальным в пределах всего дерева компонентов (включая стандартные). Если у узлов будут два одинаковых ID, то оба не будут открываться. Например, для компонента собственной разработки выбран узел ID = "news", а такой ID уже есть для стандартных компонентов.
      • "NAME" - название ветки дерева. Необходимо обязательно указать. NAME берется из первого попавшегося компонента в узле. Если его не оказалось либо нет нужной языковой константы - в качестве NAME используется ID.
      • "CHILD" - дочерняя или подчиненная ветка. В элементе с ключом "CHILD" может быть задана подчиненная ветка дерева с той же структурой, что и родительская ветка.
      Дерево ограничено тремя уровнями. Как правило, строится двухуровневое дерево и компоненты располагаются на втором уровне. Следующие служебные названия первого уровня зарезервированы и не могут быть использованы: "content" (контент), "service" (сервисы), "communication" (общение), "e-store" (магазин), "utility" (служебные).

      Если ключ "PATH" не задан, то компонент не будет присутствовать в визуальном редакторе;
    • "AREA_BUTTONS" - пользовательские кнопки, которые показываются для компонента в режиме редактирования сайта;
    • "CACHE_PATH" - если значение равно "Y", то отображается кнопка очистки кэша компонента в режиме редактирования сайта (предполагается, что кэш лежит по стандартному пути: /<код сайта>/<относительный путь к компоненту>). Если равно не пустой отличной от "Y" строке, отображается кнопка очистки кэша компонента в режиме редактирования сайта (кэш лежит по пути, равному значению с ключом "CACHE_PATH" - для нестандартных путей);
    • "COMPLEX" - элемент должен иметь значение "Y" для комплексного компонента, для простых ключ не имеет значения.

    Параметры компонента

    В файле .parameters.php содержится описание входных параметров компонента. Данные файла нужны исключительно для создания формы ввода свойств компонента в среде Bitrix Framework (например, в визуальном редакторе). Это описание применяется для работы с компонентом, а также при работе в режиме редактирования сайта. При работе самого компонента (при обращении к странице, на которой расположен компонент) описание не используется и указанный файл не подключается. Для комплексного компонента в этом файле задаются параметры простых компонентов, входящих в состав комплексного. Также здесь будут задаваться и настройки ЧПУ.

    Файл .parameters.php должен находиться в папке компонента. Языковой файл подключается автоматически (должен лежать в папке /lang/<язык>/.parameters.php, относительно папки компонента).

    Подсказка по параметрам

    В файле определяется массив $arComponentParameters, который описывает входные параметры компонента. Если необходимо, производится выборка каких-либо дополнительных данных. Например, для формирования выпадающего списка типов информационных блоков (входной параметр IBLOCK_TYPE_ID) выбираются все активные типы.

    Структура типичного файла .parameters.php (на примере компонентов, работающих с модулем Информационные блоки):

    <?
    CModule::IncludeModule("iblock");
    
    $dbIBlockType = CIBlockType::GetList(
    	array("sort" => "asc"),
    	array("ACTIVE" => "Y")
    );
    while ($arIBlockType = $dbIBlockType->Fetch())
    {
    	if ($arIBlockTypeLang = CIBlockType::GetByIDLang($arIBlockType["ID"], LANGUAGE_ID))
    		$arIblockType[$arIBlockType["ID"]] = "[".$arIBlockType["ID"]."] ".$arIBlockTypeLang["NAME"];
    }
    
    $arComponentParameters = array(
    	"GROUPS" => array(
    		"SETTINGS" => array(
    			"NAME" => GetMessage("SETTINGS_PHR")
    		),
    	"PARAMS" => array(
    		"NAME" => GetMessage("PARAMS_PHR")
    		),
    	),
    	"PARAMETERS" => array(
    		"IBLOCK_TYPE_ID" => array(
    			"PARENT" => "SETTINGS",
    			"NAME" => GetMessage("INFOBLOCK_TYPE_PHR"),
    			"TYPE" => "LIST",
    			"ADDITIONAL_VALUES" => "Y",
    			"VALUES" => $arIblockType,
    			"REFRESH" => "Y"
    	),
    	"BASKET_PAGE_TEMPLATE" => array(
    		"PARENT" => "PARAMS",
    		"NAME" => GetMessage("BASKET_LINK_PHR"),
    		"TYPE" => "STRING",
    		"MULTIPLE" => "N",
    		"DEFAULT" => "/personal/basket.php",
    		"COLS" => 25
    	),
    	"SET_TITLE" => array(),
    	"CACHE_TIME" => array(),
    	"VARIABLE_ALIASES" => array(
    		"IBLOCK_ID" => array(
    			"NAME" => GetMessage("CATALOG_ID_VARIABLE_PHR"),
    		),
    		"SECTION_ID" => array(
    			"NAME" => GetMessage("SECTION_ID_VARIABLE_PHR"),
    		),
    	),
    	"SEF_MODE" => array(
    		"list" => array(
    			"NAME" => GetMessage("CATALOG_LIST_PATH_TEMPLATE_PHR"),
    			"DEFAULT" => "index.php",
    			"VARIABLES" => array()
    		),
    		"section1" => array(
    			"NAME" => GetMessage("SECTION_LIST_PATH_TEMPLATE_PHR"),
    			"DEFAULT" => "#IBLOCK_ID#",
    			"VARIABLES" => array("IBLOCK_ID")
    			),
    		"section2" => array(
    			"NAME" => GetMessage("SUB_SECTION_LIST_PATH_TEMPLATE_PHR"),
    			"DEFAULT" => "#IBLOCK_ID#/#SECTION_ID#",
    			"VARIABLES" => array("IBLOCK_ID", "SECTION_ID")
    			),
    		),
    	)
    );
    ?>

    Опишем ключи массива $arComponentParameters подробнее.

    GROUPS

    Значением этого ключа является массив групп параметров компонента. Параметры в визуальных средствах среды Bitrix Framework (например, в визуальном редакторе) группируются. Группы в среде Bitrix Framework располагаются в том порядке, в котором заданы в файле. Массив групп параметров компонента состоит из элементов следующего вида:

    "код группы" => array(
        "NAME" => "название группы на текущем языке",
        "SORT" => "сортировка",
        )

    Перечень стандартных групп:

    кодСортировкаНазваниеОписание
    BASE 100 Основные параметры Базовые параметры для работы компонента
    DATA_SOURCE 200 Источник данных Параметры, указывающие, откуда выбирать данные для компонента (к примеру, для компонент модуля Инфоблоки это тип и ID инфоблока).
    VISUAL 300 Настройки внешнего вида Сюда предполагается размещать параметры, отвечающие за внешний вид.
    USER_CONSENT 350 Согласие пользователя Настройка параметров на получение согласия пользователя, согласно законодательству РФ,.
    URL_TEMPLATES 400 Шаблоны ссылок Служебная
    SEF_MODE 500 Управление адресами страниц Группа для всех параметров, связанных с использованием ЧПУ.
    AJAX_SETTINGS 550 Управление режимом AJAX Все, что касается ajax.
    CACHE_SETTINGS 600 Настройки кеширования Появляется при указании параметра CACHE_TIME.
    ADDITIONAL_SETTINGS 700 Дополнительные настройки Эта группа появляется, например, при указании параметра SET_TITLE.

    PARAMETERS

    Значением этого ключа является массив параметров компонента. В каждой группе параметров параметры располагаются в том порядке, в котором заданы в файле. Массив обычных параметров компонента состоит из элементов следующего вида:

    "код параметра" => array(
    	"PARENT" => "код группы",  // если нет - ставится ADDITIONAL_SETTINGS
    	"NAME" => "название параметра на текущем языке",
    	"TYPE" => "тип элемента управления, в котором будет устанавливаться параметр",
    	"REFRESH" => "перегружать настройки или нет после выбора (N/Y)",
    	"MULTIPLE" => "одиночное/множественное значение (N/Y)",
    	"VALUES" => "массив значений для списка (TYPE = LIST)",
    	"ADDITIONAL_VALUES" => "показывать поле для значений, вводимых вручную (Y/N)",
    	"SIZE" => "число строк для списка (если нужен не выпадающий список)",
    	"DEFAULT" => "значение по умолчанию",
    	"COLS" => "ширина поля в символах",
    ),

    Для типа элемента управления TYPE есть значения:

    • LIST - выбор из списка значений. Для типа LIST ключ VALUES содержит массив значений следующего вида:
      VALUES => array(
      	"ID или код, сохраняемый в настройках компонента" => "языкозависимое описание",
      ),

      Если в списке значений используются строковые ключи (RUB, 'TRIPLEX' и так далее), то массив выводится в том порядке, как [dw]сформирован[/dw][di]То есть строковые ключи массива автоматически JS-ом преобразуются при формировании данных из PHP в js. Если важен и критичен порядок, то лучше использовать ключи строки. [/di]. Если же ключи могут быть приведены к числовым - массив значений выводится отсортированный по ключам.

    • STRING - текстовое поле ввода.
    • CHECKBOX - да/нет.
    • CUSTOM - позволяет создавать кастомные элементы управления.
    • FILE - выбор файла.

      Пример реализации
    • COLORPICKER - указание цвета:

      $arComponentParameters["PARAMETERS"]["COLOR"]  = Array(
      	"PARENT" => "BASE",
      	"NAME" => 'Выбор цвета',
      	"TYPE" => "COLORPICKER",
      	"DEFAULT" => 'FFFF00'
      );
      

    Внешний вид списка меняется в зависимости от наличия/отсутствия ключей MULTIPLE и ADDITIONAL_VALUES:

    • Если MULTIPLE и ADDITIONAL_VALUES отсутствуют или равны "N", то выводится просто список, никаких значений в список не добавляется.
    • Если ADDITIONAL_VALUES = "Y", MULTIPLE = "N", то в список добавляется значение "другое" и рядом доп.поле для ввода значения вручную:

    • Если ADDITIONAL_VALUES = "N", MULTIPLE = "Y", то в список ничего не добавляется, просто появляется возможность выбрать несколько элементов:

    • Если ADDITIONAL_VALUES = "Y", MULTIPLE = "Y", то в список добавляется значение не выбрано и рядом множественное дополнительное поле для ввода значения вручную.
    • Примечание: Скриншоты делались для значения SIZE = 9. Если этот ключ не указывать, список будет выпадающим.


    Параметр REFRESH позволяет после выбора значения перегрузить всю форму с параметрами. Делается это, например, для выбора инфоблока конкретного типа. То есть имеем два параметра - тип инфоблока и код инфоблока. Исходное положение - в первом список всех типов инфоблоков, во втором - список всех инфоблоков данного сайта, а после выбора нужного типа инфоблока параметры компонента перегружаются и мы видим только инфоблоки нужного типа.

    Внешне для параметров типа LIST этот ключ проявляется как кнопка с надписью ОК возле параметра (см. скриншоты выше).

    Если нужно, чтобы некий параметр появлялся или нет в зависимости от другого, делается это так. Пусть нам необходимо показать список свойств инфоблока. Предположим, что ID инфоблока содержится в параметре компонента IBLOCK_ID, а параметр, где будет список свойств назовем PROP_LIST. У параметра IBLOCK_ID должен быть выставлен ключ REFRESH = 'Y'. Код:

    if (0 < intval($arCurrentValues['IBLOCK_ID']))
    {
    	$arPropList = array();
    	$rsProps = CIBlockProperty::GetList(array(),array('IBLOCK_ID' => $arCurrentValues['IBLOCK_ID']));
    	while ($arProp = $rsProps->Fetch())
    	{
    		$arPropList[$arProp['ID']] = $arProp['NAME'];
    	}
    	$arComponentParameters['PARAMETERS']['PROP_LIST'] = array(
    		'NAME' => 'название параметра',
    		'TYPE' => 'LIST',
    		'VALUES' => $arPropList,
    	);
    }

    Существуют особые параметры, которые стандартизованы и которые нет необходимости описывать полностью. Достаточно указать, что они есть. Например,

    "SET_TITLE" => array(),
    "CACHE_TIME" => array(),

    Первый из указанных параметров указывает, следует ли компоненту установить заголовок страницы, а второй - все настройки, связанные с кешированием.


    Только комплексные компоненты могут работать в режиме ЧПУ или переопределять переменные, которые приходят из HTTP запроса. В этом случае необходимо среди параметров указать ещё два особых параметра:

    • "VARIABLE_ALIASES" - массив, описывающий переменные, которые компонент может получать из HTTP запроса. Каждый элемент массива имеет вид:
      "внутреннее название переменной" => array(
      	"NAME" => "название переменной на текущем языке",
      )
    • "SEF_MODE" - массив, описывающий шаблоны путей в режиме ЧПУ. Каждый элемент массива имеет вид:
      "код шаблона пути" => array(
      	"NAME" => "название шаблона пути на текущем языке",
      	"DEFAULT" => "шаблон пути по-умолчанию",
      	"VARIABLES" => "массив внутренних названий переменных, которые могут использоваться в шаблоне"
      )

    Важно! Наиболее правильный вариант кастомизации компонента - скопировать его в отдельное пространство имен и работать уже с копией компонента. При этом нужно учитывать последствия:
    • Увеличивается общее количество компонентов, соответственно растет и количество выделяемых на их поддержку ресурсов.
    • Сложность в освоении новым разработчиком: сначала ему нужно будет найти в чем отличие от уже существующего стандартного компонента.


    Шаблоны компонента

    Цитатник веб-разработчиков.

    Роман Петров: Не все знают, что у стандартных компонентов "1С-Битрикс" есть много других красивых штатных шаблонов, которые практически никогда не используются. Для любопытствующих вопрос: знаете ли вы, что компонент постраничной навигации имеет аж пять шаблонов: .default, arrows, js, modern, orange?

    Шаблон компонента – программный код, преобразующий данные, подготовленные компонентом, непосредственно в HTML-код.

  • Переменные
  • Шаблон простого компонента
  • Шаблон комплексного компонента
  • Как система ищет шаблон
  • Подключение шаблона
  • Редактирование шаблона
  • Особенности работы с ajax
  • Пример шаблона
  • Шаблоны компонента делятся на системные и пользовательские:

    • Системные шаблоны поставляются вместе в дитрибутиве и лежат в подпапке templates папки компонента.
    • Пользовательские шаблоны компонента - шаблоны, которые изменены под нужды конкретного сайта. Они должны лежать в папках шаблонов сайтов (т.е. в /local/templates/шаблон_сайта/). При таком расположении шаблона при копировании шаблона средствами системы, они будут расположены по следующему пути: /local/templates/шаблон_сайта/components/namespace/название_компонента/название_шаблона.

    Шаблоны компонента определяются по именам. Шаблон по умолчанию имеет имя .default. Если в настройках параметра компонента не указывается имя шаблона, вызывается тот, что по умолчанию. Остальные шаблоны могут называться произвольно.

    Шаблоны компонента могут быть папками или файлами. Если шаблону не требуется перевод на другие языки, собственные стили и прочие ресурсы, такой его можно расположить в файле. В противном случае шаблон следует располагать в папке.

    Каждый шаблон компонента является неделимым целым. Если требуется изменить системный шаблон под конкретный сайт, то его нужно целиком скопировать в папку шаблона сайта и только потом править под потребности проекта.

    Например, у компонента bitrix:main.site.selector есть системный шаблон dropdown, который лежит в подпапке templates папки компонента. Если его требуется изменить под конкретный сайт, то папку шаблона dropdown следует скопировать в папку /local/templates/шаблон_сайта/components/bitrix/main.site.selector/ и изменить по своему усмотрению.

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


    Переменные, доступные в шаблоне компонента

    Используются следующие переменные:

    • $templateFile – путь к шаблону относительно корня сайта, например /bitrix/components/bitrix/iblock.list/templates/.default/template.php).
    • $arResult – массив результатов работы компонента.
    • $arParams – массив входящих параметров компонента, может использоваться для учета заданных параметров при выводе шаблона (например, отображении детальных изображений или ссылок).
    • $arLangMessages – массив языковых сообщений (для php шаблонов не устанавливается).
    • $templateFolder – путь к папке с шаблоном от DOCUMENT_ROOT (например /bitrix/components/bitrix/iblock.list/templates/.default).
    • $parentTemplateFolder - путь относительно корня сайта к папке шаблона комплексного компонента, в составе которого подключается данный компонент (если компонент подключается самостоятельно, то эта переменная пуста).
    • $component – ссылка на текущий вызванный компонент (тип CBitrixComponent).
    • $this - ссылка на текущий шаблон (объект, описывающий шаблон, тип CBitrixComponentTemplate)
    • $templateName – имя шаблона (например: .dеfault)
    • $componentPath – путь к папке с компонентом от DOCUMENT_ROOT (напр. /bitrix/components/bitrix/iblock.list)
    • $templateData – массив для записи, обратите внимание, таким образом можно передать данные из template.php в файл component_epilog.php, причем эти данные попадают в кеш, т.к. файл component_epilog.php исполняется на каждом хите.

    Кроме того внутри PHP шаблона объявлены глобальными переменные $APPLICATION, $USER, $DB.


    Шаблон простого компонента

    Папка шаблона простого компонента может содержать следующие подпапки и файлы:

    • подпапку /lang, в которой расположены файлы языковых сообщений (переводов);
    • файл result_modifier.php, который подключается непосредственно перед подключением шаблона компонента. Этот файл получает на вход массив результатов работы компонента $arResult и массив параметров его вызова $arParams. Таким образом, можно, например, изменить массив результатов работы компонента под конкретный шаблон.
    • файл component_epilog.php, который подключается после исполнения шаблона.
    • файл style.css, который определяет необходимы стили.
    • файл script.js, который определяет и подключает необходимые яваскрипты. Этот файл может отсутствовать.
    • файл .description.php, который содержит название и описание шаблона для визуального редактора.
      Пример файла .description.php
    • файл .parameters.php, который содержит описание дополнительных входных параметров шаблона для визуального редактора.
      Пример файла .parameters.php
    • файл template.ext, который и является собственно шаблоном. Расширение ext зависит от того, какой движок шаблонизации нужно подключать. По умолчанию расширение равно php. Этот файл должен обязательно присутствовать.
    • любые другие папки и файлы с ресурсами, необходимыми шаблону компонента. Например, папка image, содержащая изображения.

    Шаблон комплексного компонента


    Шаблон комплексного компонента содержит все те же папки, что и шаблон простого, и дополнительно к ним:

    • шаблоны простых компонентов, которые входят в состав комплексного. Эти шаблоны располагаются в папках вида /пространство_имен/название_простого_компонента/ относительно папки шаблона комплексного компонента.
    • простые компоненты, входящие в состав комплексного, подключаются на шаблонах страниц комплексного компонента.

    Как система ищет шаблон

    Применяется следующий алгоритм поиска подходящего шаблона:

    • Если используется шаблон сайта из папки [dw]\local[/dw][di]Папка \local - папка для разработки в рамках ядра D7 Подробнее...[/di], то система сначала ищет его в папке /local/templates/текущий_шаблон_сайта/components/.
      Если там шаблон не найден, то берётся папка /local/templates/.default/components/. Если шаблон найден, то поиск прекращается.
    • Если используется шаблон сайта из папки /bitrix/templates/, то берется папка /bitrix/templates/текущий_шаблон_сайта/components/. В этой папке в пути /пространство_имен_компонента/название_компонента/ проверяется последовательно наличие файла или папки с именем шаблона. Если таковых нет, то проверяется наличие файла имя_шаблона.ext, где в качестве ext берутся последовательно все доступные расширения всех установленых на сайте движков шаблонизации. Если шаблон найден, то алгоритм завершается.
    • Если на шаге 1 шаблон не найден, то берется папка /bitrix/templates/.default/components/. И применяется алгоритм, описанный в шаге 1. Если шаблон найден, то алгоритм завершается.
    • Если на шаге 2 шаблон не найден, то производится поиск среди системных шаблонов.

    Особенности поиска:

    • Если имя шаблона не задано, то ищется шаблон с именем .default.
    • Если шаблон задан именем папки, то в случае простого компонента в этой папке ищется файл template.ext, а в случае комплексного - название_страницы.ext. Расширение ext сначала принимается равным php, а затем расширениям других доступных на сайте движков шаблонизации.

    Например, требуется показать компонент bitrix:catalog.list с помощью шаблона table. Пусть на сайте, кроме стандартного движка шаблонизации (файлы с расширением php), доступен так же движок Smarty (файлы с расширением tpl). Система проверит сначала папку /local/templates/текущий_шаблон_сайта/components/bitrix/catalog.list/ на наличие файла или папки с именем table. Если таковых нет, система проверит эту же папку на наличие файлов table.php и table.tpl. Если ничего не найдено, система изучит папки /bitrix/templates/.default/components/bitrix/catalog.list/ и /bitrix/components/bitrix/catalog.list/templates/.

    Если папка компонента найдена, в этой папке сначала ищется файл template.php, и если этот файл не найден, то ищется template.tpl. Если шаблон задан в виде table/template.php, то сразу берется указанный файл.

    Если простой компонент вызывается в составе комплексного, то его шаблон сначала ищется в составе шаблона комплексного компонента, а потом (если не найден) в собственных шаблонах. Чтобы это правило работало, при вызове простых компонентов в составе комплексного не забывайте указывать четвертым параметром переменную $component, указывающую на родительский компонент. Т.е. код вызова простого компонента должен иметь вид:

    $APPLICATION->IncludeComponent("custom:catalog.element", "", array(...), $component);

    Примечание:

    В одной папке (например, /bitrix/templates/текущий_шаблон_сайта/components/) есть шаблоны двух компонентов, комплексного и простого:

    • catalog (комплексный, в котором есть еще простой catalog.section)
    • catalog.section (простой)
    По условиям работы сайта необходимо чтобы для двух вхождений catalog.section использовался один единственный шаблон. В этом случае нужно, чтобы этот шаблон имел имя, отличное от .default, иначе он не будет подхвачен.


    Подключение шаблона

    В коде подключения указывается только <namespace>, имя компонента, имя шаблона (и параметры самого компонента). При обработке кода ядро сначала проверяет наличие шаблона компонента в шаблоне текущего сайта: /local/templates/<имя шаблона сайта>/components/<namespace>/<имя компонента>/<имя шаблона>/template.php.

    Если - bitrix (/bitrix/components/bitrix), то это папка для шаблонов стандартных компонентов. Если - выбранное вами <имя> (/local/components/<имя>), то это папка для шаблонов ваших компонентов.

    Если файла шаблона нет, проверяется шаблон сайта по умолчанию: /bitrix/templates/.default/components/<namespace>/<имя компонента>/<имя шаблона>/template.php.

    И только после этого происходит подключение шаблона компонента из папки компонента.

    Шаблон подключается командой:

    $this->IncludeComponentTemplate($templatePage = "");

    Где $templatePage – для комплексного компонента имя текущей страницы, для обычного – пустая строка.

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

    1. Подключим шаблон текущего комплексного компонента для страницы section:

      $this->IncludeComponentTemplate("section");

    2. Подключим шаблон текущего простого компонента:

      $this->IncludeComponentTemplate();


    Редактирование

    Редактирование шаблона - один из способов получения нужного результата работы компонента под требования сайта. Редактировать системный шаблон не рекомендуется в силу того, что при первом же обновлении он восстановит свое прежнее состояние. Редактировать и применять можно только пользовательский шаблон.

    Копирование выполняется по команде Копировать шаблон компонента при включенном режиме Правка:

    При копировании шаблона можно сразу задать его применение к компоненту и открыть форму для редактирования шаблона:

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

    Редактирование допускает добавление в него логики действий, но такую модификацию лучше выносить в файлы result_modifier.php и component_epilog.php (которые должны быть расположены в папке шаблона) для более сложного изменения результата работы.


    Особенности работы с ajax

    Использование режима ajax имеет свои особенности. Чтобы строка навигации в открываемой по ajax странице имела в цепочке навигации своё название, необходимо, чтобы в шаблоне компонента обязательно присутствовал элемент с id="navigation". Это необязательно должен быть div, это может быть span, h1, p и так далее.

    Аналогично, для заголовка обязательно наличие элемента с id="pagetitle".


    Пример шаблона

    Шаблон компонента Меню
    Проверка включения <?if (!defined("B_PROLOG_INCLUDED") || B_PROLOG_INCLUDED!==true)die();?>
    Старт скрипта <?if (!empty($arResult)):?>
    Открытие тега <ul> — ненумерованный список <ul class="…">
    Старт цикла поиска <?foreach ($arResult as $arItem):?>
    Ссылка активная <?if($arItem["SELECTED"]):?>
    Вывод ссылки <li><a href="<?=$arItem["LINK"]?>" class= "selected"><?=$arItem["TEXT"]?></a></li>
    Проверка на продолжение цикла <?else:?>
    Ссылка неактивная <li><a href="<?=$arItem["LINK"]?>"> <?=$arItem["TEXT"]?></a></li>
    Завершение вывода ссылки <?endif;?>
    Завершение цикла поиска <?endforeach;?>
    Закрытие тега <ul> — ненумерованный список </ul>
    Завершение скрипта <?endif;?>

    Список ссылок по теме:


    Типичные ошибки

    Цитатник веб-разработчиков.

    TeppopucT: И правда, все проблемы в руках! Ищите ошибки в коде!!! Все теги должны отвечать стандартам. А Битрикс, подхватит!!! Который раз помогает очистка кода. Пусть и ручная работа, и кропотливая, но с достойным финалом!

    Не удалось обнаружить код вызова компонента

    Довольно распространенная ошибка, когда вы в режиме редактирования пытаетесь отредактировать параметры какого-то компонента на странице. Хоть в коде и присутствует строка $APPLICATION->IncludeComponent() (вызов компонента), всё равно иногда появляется ошибка Не удалось обнаружить код вызова компонента. К сожалению, универсального решения данной проблемы нет.

    Ошибка может возникать из-за разных причин:

    • Код вызова компонента не взят в отдельные <? ?>.

      Решение: проверить отделенность кода компонента от другого php-кода на странице.

      То есть, если у вас на странице php-код в таком виде:

      <?
      php-код
      
      компонент
      
      php-код
      ?>

      то будет ошибка.

      Необходимо, чтобы было так:

      <?
      php-код
      ?>
      
      <?
      компонент
      ?>
      
      <?
      php-код
      ?>
      
      

      Можно также попробовать вставить такую конструкцию перед вызовом компонента: <?/**/?>.

    • Ошибки в html коде на странице.

      Решение: проверить валидность html кода, убрать все html-комментарии со страницы.

    • Несоответствие кодировки файла. До версии 20.100.0 модуля main

    • Несоответствие между владельцем файла и пользователем под которым система файлы редактирует.

      Решение: проверить права пользователя.

    • Взаимное влияние аналогичных компонентов.

      Решение: удалить несколько аналогичных компонентов рядом с неработающим.


    Кастомизация шаблонов компонентов

    Цитатник веб-разработчиков.

    Рамиль Юналиев: По себе знаю, что не хватало примеров работы именно с шаблонами, потому что с них начинал. Если программисты "въедут" в эти шаблоны, то там уже пойдет...

    Внимание! Шаблоны корпоративного портала Битрикс24 являются системными шаблонами и их кастомизирование запрещено! В продукте сохранена техническая возможность копирования шаблона, его кастомизации и применения, но при использовании этой возможности будет потеряна обратная совместимость.

    Кастомизация [dw]штатного[/dw][di]В штатных шаблонах "1С-Битрикс: Управление сайтом" используется Bootstrap 4.
    Это определяет требования к клиентскому обеспечению.
    Рекомендации таковы:

    Chrome - не ниже версии 45,
    Firefox - не ниже версии 38
    Edge - не ниже версии 12
    Explorer - не ниже версии 10
    Safari - не ниже версии 9
    Opera - не ниже версии 30
    iOS - не ниже версии 9
    Android - не ниже версии 4.4
    [/di] шаблона компонента, как правило, преследует две цели:

    • Приведение формы вывода данных компонента в соответствие с дизайном сайта;
    • Организация вывода данных компонента в виде, недоступном в стандартном варианте.

    На простых сайтах, как правило, шаблоны требуют кастомизации только при решении первой задачи. Для изменения системного шаблона компонента под конкретный сайт, его необходимо сначала целиком скопировать в папку шаблона сайта. После этого можно перейти к редактированию скопированного шаблона.


    Кастомизация шаблона

    Как изменить стандартный вид компонента

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

    Иван, веб-разработчик:

    1. Действительно хотеть это.
    2. Очень много работать в выбранном направлении.
    3. Начинать с CSS руководство, HTML manual, и т.д. для javascript и php думаю найдете сами. Возможно стоит вначале посмотреть вот этот ресурс: htmlbook.ru
    4. Никогда не верстать от "сделаю так, посмотрю на результат". Действуйте от знаний, полученных в п. 3.
    5. Не париться если не получается и делать заново пока не получится и не разберетесь в основах основ почему именно так работает, а так не работает.
    6. Вам еще предстоит узнать, что такое IE, вы стрессоустойчивы? Это вам поможет.
    7. Никогда не брать чужие куски кода/верстки и использовать "как есть", всегда добираться до сути реализации.

    В веб-программировании и программировании вообще есть популярный миф, что логику, данные и представления легко и очевидно можно отделить друг от друга, что решение этой задачи однозначно. Если немного подумать, становится очевидно что логика, данные и представления легко переходят между собой в зависимости от того, с какого уровня абстракции вы смотрите на задачу. Например, html-код шаблона для разработчика сайта это безусловно представление, для браузера это код, а для ядра CMS это данные.

    Разработчики, впервые видящие код шаблона любого компонента, например списка новостей, ужасаются циклу, каше из php и HTML (и js изредка можно встретить). Это шаблон, он для вывода. Все данные уже собраны, запросы отработали, понятно что и куда мы выводим, даже отлов угроз уже прошел. Шаблон - последнее звено в цепочке кода и данных. Тут логика вывода действительно смешана с оформлением, и это правильно.

    Начинающему разработчику часто очень непросто разобраться в том, где в шаблоне данные, где представление. В шаблоне можно писать любой php-код, можно написать прямое, не API, обращение к базе данных, можно написать десяток строк на API и решить задачу. Битрикс этим очень «развращает» разработчика: не запрещено писать бизнес-логику в шаблонах и HTML в компонентах, слишком много свободы. В собственном коде и по идеологии Bitrix Framework задача разделения данных решена нормально, а вот написание в шаблоне ерунды, не рекомендованной по идеологии, система не контролирует.

    Приступая к кастомизации нужно помнить:

    Внимание! Вся логика должна быть в компоненте, в шаблоне - только представление вывода полученных данных!

    Кастомизация шаблона компонента, как правило, преследует две цели:

    1. Приведение формы вывода данных компонента в соответствие с дизайном сайта;
    2. Организация вывода данных компонента в виде, недоступном в стандартном варианте.

    Пользовательские шаблоны компонента – шаблоны, которые изменены под нужды конкретного проекта. Они должны лежать в папках шаблонов портала (т.е. в [ds]/local[/ds][di]При обработке папок приоритет всегда у папки /local перед /bitrix. Это означает, что если в /local/templates/ и /bitrix/templates/ будут находиться шаблоны сайта с одинаковым названием, то подключится шаблон из /local.

    Подробнее...[/di]/templates/шаблон_сайта/
    ). При копировании шаблона компонента средствами системы, они будут расположены по следующему пути: /local/templates/шаблон_сайта/components/namespace/название_компонента/название_шаблона.

    Примечание: Теоретически шаблон можно расположить в каталоге /bitrix/templates/текущий_шаблон_сайта/components/. Но в этом случае возникают нюансы, которые нужно учитывать. Предположим, что в этой папке оказались шаблоны двух компонентов с общей начальной частью в названии. Например: catalog (комплексный) и собственно catalog.section (простой). При этом в шаблоне комплексного есть еще простой catalog.section. В этом случае для catalog.section нужно использовать единый шаблон с именем, отличным от .default. Иначе он не будет подхвачен.

    Копировать шаблон можно следующими способами:

    • В рамках файловой системы копированием папки /bitrix/components/bitrix/_нужный_компонент_/templates/ в папку /local/templates/шаблон_сайта/components/namespace/название_компонента/_название_шаблона.
    • Средствами интерфейса системы с помощью команды Копировать шаблон компонента (при включённом режиме Правка):

    После копирования шаблон можно изменять и, после изменения, применить его к компоненту в настройках компонента.

    Внимание! Если при редактировании шаблона компонента вы добавляете новые идентификаторы языковых сообщений, то помните, что идентификаторы должны быть уникальны в рамках всего продукта.

    При кастомизации можно использовать буферизацию.

    В главе приведены некоторые примеры кастомизации шаблонов.

    Примечание: Еще один пример редактирования шаблона (шаблона меню) представлен на странице Кастомизация шаблонов компонентов главы Как создать простой сайт.

    В главе приводятся примеры кастомизации шаблонов. Кроме этих материалов рекомендуем познакомится ещё с сообщениями в блогах разработчиков:


    Модификация шаблона простого компонента в составе комплексного

    Комплексный компонент обеспечивает взаимодействие простых компонентов с общей тематикой. Простые компоненты содержат код непосредственной работы с данными. Например, компоненты соцсети настраивать по отдельности неудобно.

    Но если надо лишь изменить внешний вид некоторых элементов, это можно легко сделать, не отказываясь от остальных стандартных шаблонов.

    Желательно обходиться без кастомизации компонента там, где в этом нет особой необходимости. В этом случае:

    • не теряются обновления;
    • легче решать проблемы через техподдержку (ТП не занимается решением проблем, возникающих в работе кастомного кода если явно не обозначена ошибка в работе API);
    • в комплексных компонентах стандартно реализован AJAX.

    В качестве примера возьмем компонент Социальная сеть. Задача: заменить форму редактирования описания группы (textarea) на визуальный редактор с возможностью вставки html.

    Внимание! После решения этой задачи нужно модифицировать шаблон компонента вывода описания, чтобы не экранировались теги html. Но решение аналогичное и выходит за рамки этого описания.

    Как сделать

    • Скопируйте шаблон компонента bitrix:socialnetwork.group_create.ex.
    • Сделайте замену в шаблоне:
      <textarea name="GROUP_DESCRIPTION"     style="width:98%" rows="5"><?= $arResult["POST"]["DESCRIPTION"]; ?></textarea>
      на
      <input type="hidden" name="GROUP_DESCRIPTION"><?
      $GLOBALS['APPLICATION']->IncludeComponent( 
      	"bitrix:fileman.light_editor", 
      	".default", 
      	Array( 
      		"CONTENT" => htmlspecialchars_decode($arResult["POST"]["DESCRIPTION"]), 
      		"INPUT_NAME" => "GROUP_DESCRIPTION", 
      		"WIDTH" => "98%", 
      		"HEIGHT" => "200px", 
      		"USE_FILE_DIALOGS" => "N", 
      		"FLOATING_TOOLBAR" => "N", 
      		"ARISING_TOOLBAR" => "N", 
      	"VIDEO_ALLOW_VIDEO" => "N",
      	) 
      );
      ?>
    • В файлах group_edit.php, group_create.php комплексного компонента укажите, что в вызове простого компонента нужно использовать кастомный шаблон:
      'POPUP_COMPONENT_NAME' => 'bitrix:socialnetwork.group_create.ex',
      'POPUP_COMPONENT_TEMPLATE_NAME' => 'test_template'

    Получаем результат. Было:

    Стало:

    При этом весь остальной код остаётся стандартный, т.е. будет обновляться и поддерживаться компанией "1С-Битрикс".


    Пример. Вывод голосования

    Задача:

    1. Выдать голосование не в детальном товаре, а на странице со списком товаров.
    2. Выдать ее на AJAX стандартными средствами

    В каталоге нет голосования. Зато оно есть в комплексном компоненте Новости, в детальном просмотре элемента.

    Решение (для слабоподготовленного разработчика, методом копи-паст).

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

    В шаблоне комплексного компонента Новости находим такой код:

    <?$APPLICATION->IncludeComponent(
    	"bitrix:iblock.vote",
    	"",
    	Array(
    		"IBLOCK_TYPE" => $arParams["IBLOCK_TYPE"],
    		"IBLOCK_ID" => $arParams["IBLOCK_ID"],
    		"ELEMENT_ID" => $ElementID,
    		"MAX_VOTE" => $arParams["MAX_VOTE"],
    		"VOTE_NAMES" => $arParams["VOTE_NAMES"],
    		"CACHE_TYPE" => $arParams["CACHE_TYPE"],
    		"CACHE_TIME" => $arParams["CACHE_TIME"],
    	),
    	$component
    );?>

    Вставляем этот код в шаблон компонента bitrix:catalog.top куда-нибудь, где он должен выводиться. Например, в таблицу после вывода <?=$arElement["PREVIEW_TEXT"]?>.

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

    "ELEMENT_ID" => $ElementID,

    на

    "ELEMENT_ID" =>$arElement["ID"],

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

    В папке с шаблонами компонента bitrix:iblock.vote есть два шаблона: .default и ajax. Применяем второй. Вторая проблема тоже решена. В итоге вызов компонента получился вот таким:

    <?$APPLICATION->IncludeComponent(
    	"bitrix:iblock.vote",
    	"ajax",
    	Array(
    		"IBLOCK_TYPE" => $arParams["IBLOCK_TYPE"],
    		"IBLOCK_ID" => $arParams["IBLOCK_ID"],
    		"ELEMENT_ID" =>$arElement["ID"],
    		"MAX_VOTE" => $arParams["MAX_VOTE"],
    		"VOTE_NAMES" => $arParams["VOTE_NAMES"],
    		"CACHE_TYPE" => $arParams["CACHE_TYPE"],
    		"CACHE_TIME" => $arParams["CACHE_TIME"],
    	),
    	$component
    );?>

    Пример. Добавление типа отсутствия

    Примечание: С версии модуля intranet 11.0.4 в действиях, описанных ниже нет необходимости. Данный материал оставлен в курсе с целью ознакомления с возможностями системы.

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

    В качестве примера рассмотрим такую задачу в рамках коробочной версии "Битрикс24": необходимо добавить в Графике отсутствий (Сотрудники > График отсутствий) новый Тип отсутствий, например: отсутствие Отгул в счет отработанного времени.

    Задания свойств инфоблока

    • В свойствах инфоблока (Контент > Информ. блоки > Типы информ. блоков > Оргструктура > график отсутствий) в закладке Свойства в строке Тип присутствия нажмите на кнопку с многоточием. Откроется форма Настройка свойства информационного блока:
    • В Значениях списка добавьте новое значение Отгул в счёт отработанного времени. И смените параметры сортировки, чтобы расположить новый тип отсутствия в нужном месте.

    Изменение шаблона

    • Скопируйте шаблон и откройте его для редактирования.
    • Найдите строки:
      GetMessage('INTR_ABSC_TPL_LEGEND_*************')?>'},
      			{NAME:'************',TITLE:'
      
    • Вместо звездочек вставьте XML_ID, который вы использовали при добавлении значения свойства в форме настройки информационного блока.
    • Сохраните внесенные изменения.
    • Назначьте модифицированный шаблон для использования компонентом.

    Изменение файла CSS шаблона

    • Откройте для редактирования файл стилей этого шаблона.
    • Найдите строки:
      div.bx-calendar-color-************* 
         {background-image: url(/bitrix/components/bitrix/intranet.absence.calendar/templates/.default/images/calendar_grad_red.gif);}
    • Добавьте свою аналогичную строку с файлом формата gif (или другого, это не важно) нужного вам цвета (предварительно загрузив файл в систему).
    • Введите вместо звездочек XML_ID, который вы задали при добавлении значения свойства в форме настройки информационного блока.
    • Сохраните внесенные изменения.

    Добавление языкового сообщения

    • Откройте для редактирования файл с языковыми сообщениями \bitrix\templates\.default\components\bitrix\intranet.absence.calendar\_имя вашего шаблона_\lang\ru\template.php.
    • Добавьте строку:
      $MESS["INTR_ABSC_TPL_LEGEND_************"] = "отгул за отработанное время";
      Вместо звездочек используйте XML_ID, который вы задали при добавлении значения свойства в форме настройки информационного блока.
    • Сохраните внесенные изменения.

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


    Пример. Внешние файлы css

    С версии 15.5.1 появилась штатная поддержка вызова стороннего файла css. Под сторонним файлом понимается как локальный файл вне компонента так и файл на внешнем домене.

    Для подключения такого файла в template.php необходимо прописать:

    $this->addExternalCss("/local/styles.css");
    $this->addExternalJS("/local/liba.js");

    До версии 15.5.1


    Пример. Редактирование шаблона меню

    Пример редактирования шаблона на основе компонента меню

    Выделите в HTML-верстке код, отвечающий за показ верхнего меню. Например:

    <div class="topmenu">
    	<ul class="topmenu">
    		<li><a href="#" class="first">На главную</a></li>
    		<li><a href="#">Новости</a></li>
    		<li><a href="#" class="selected">Магазины</a></li>
    		<li><a href="#">Книги</a></li>
    		<li><a href="#">Форум</a></li>
    		<li><a href="#">О компании</a></li>
    		<li><a href="#">О Контакты</a></li>
    	</ul>
    </div>

    В этом коде пункты меню представлены в виде списка, который обладает следующими нюансами:

    • У первого пункта меню должен быть указан стиль first;
    • У выделенного пункта меню должен быть указан стиль selected;
    • Меню является одноуровневым.

    Модифицировать будем код шаблона gray-tabs-menu. Скопируйте шаблон в собственное пространство имен и откройте его для редактирования.

    Редактирование шаблона можно проводить как в форме Bitrix Framework, так и копированием кода и правкой его в другом редакторе. Использовать другой редактор удобнее в случае объемных текстов, так как форма редактирования в Bitrix Framework не поддерживает цветовое выделение тегов. Для примера используйте Notepad++.

    Код меню для этого шаблона выглядит так:

    1 <?if (!defined("B_PROLOG_INCLUDED") || B_PROLOG_INCLUDED!==true)die();?>
    2
    3 <?if (!empty($arResult)):?>
    4 <div class="gray-tabs-menu">
    5	<ul>
    6 <?foreach($arResult as $arItem):?>
    7
    8   <?if ($arItem["PERMISSION"] > "D"):?>
    9	<li><a href="<?=$arItem["LINK"]?>"><nobr><?=$arItem["TEXT"]?></nobr></a>
    10	<?endif?>
    11
    12 <?endforeach?>
    13
    14	</ul>
    15</div>
    16<div class="menu-clear-left"></div>
    17<?endif?>

    Рассмотрим каждую строчку этого шаблона:

    Строки шаблона
    1. Проверка, вызван ли этот файл напрямую или нет. Если напрямую – прекратить работу.
    3. Если массив с пунктами меню $arResult не пуст, то выполнять дальнейшие действия
    4,5. Внешний блок и начало списка пунктов меню
    6. Цикл по массиву с пунктами меню. В $arItem – текущий элемент цикла.
    8-10. Если текущий пользователь обладает правами на просмотр данной страницы, вывести элемент списка с ссылкой на эту страницу. В полях LINK и TEXT содержится адрес страницы и название пункта меню, соответственно.
    12. Конец цикла по массиву с пунктами меню.
    14,15. Конец списка пунктов меню и конец блока
    16. Специальный HTML-тэг, специфичный для использованной верстки
    17. Конец условия на наличие пунктов меню (см. строку 3)

    Таким образом, шаблон меню содержит:

    • область пролога шаблона меню;
    • область тела шаблона меню (вывод повторяющихся элементов);
    • область эпилога шаблона меню.

    После адаптации шаблон примет вид (зеленым цветом выделены изменения):

    1 <?if (!defined("B_PROLOG_INCLUDED") || B_PROLOG_INCLUDED!==true)die();?>
    2
    3 <?if (!empty($arResult)):?>
    4 <div class="topmenu">
    5	<ul class="topmenu">
    5a <? $cnt=0; ?>
    6 <?foreach($arResult as $arItem):?>
    7
    8   <?if ($arItem["PERMISSION"] > "D"):?>
    8a   <?if ($arItem["SELECTED"]): ?>
    8b	<li><a href="<?=$arItem["LINK"]?>" class="selected"><?=$arItem["TEXT"]?></a></li>
    8c   <?else:?>
    8d        <?if ($cnt==0):?>
    8e	<li><a href="<?=$arItem["LINK"]?>" class="first"><?=$arItem["TEXT"]?></a></li>
    8f        <?else:?>
    9	<li><a href="<?=$arItem["LINK"]?>"><?=$arItem["TEXT"]?></a></li>
    10a	<?endif?>
    10b   <?endif?>
    10c <?$cnt++; ?>
    10 <?endif?>
    11
    12 <?endforeach?>
    13
    14	</ul>
    15</div>
    16
    17<?endif?>

    Итак, что мы сделали?

    • В строках 4,5 заменили стили у блока и у списка.
    • В строке 5а ввели переменную $cnt с единственной целью – отследить первый элемент списка – в верстке он задается другим стилем. Эта переменная используется в строке 8d в строке 10c.
    • В строках 8-10 добавили условие проверки активного пункта меню и первого пункта меню.
    • И, наконец, удалили специфику верстки из 16 строки.
    • Кроме того, у нас нет необходимости в специализированных стилях и картинках для этого шаблона. Поэтому нужно удалить из каталога /bitrix/templates/.default/components/menu/top_menu/ файл style.css и папку /images/.
    Внимание! Вы можете использовать стили компонента и каталог шаблона компонента для хранения стилей и дополнительных файлов. Это позволит вам переносить шаблон компонента между проектами.

    Пример. Редактирование шаблонов "Корзина" и "Оформление заказа"

      Постановка задачи

    Рассмотрим кастомизацию шаблонов компонентов [comp include_sale_basket_basket]Корзина[/comp] и [comp include_sale_order_ajax]Оформление заказа[/comp] на примере ограничения минимальной стоимости заказа.

    Задача:

    1. В корзине:
      • Деактивировать кнопку "Оформить заказ", если сумма заказа меньше установленной.
      • Выдать сообщение о возможности оформления заказа при достижении суммы.
      • При изменении суммы заказа выше минимальной кнопку активировать, предупреждение убрать.

    2. При оформлении заказа:
      • Добавить проверку итоговой стоимости товаров для избежания возможности оформления заказа по прямой ссылке.
      • Вывести предупреждение, если сумма заказа меньше установленной.

      Компонент "Корзина"

    Посмотрите в видеоролике, как можно кастомиризовать шаблон компонента Корзина:

    Описание решения:

    Копируем шаблон компонента sale.basket.basket средствами интерфейса системы с помощью команды Копировать шаблон компонента, как описано в уроке Теория.Кастомизация шаблона.

    В шаблоне компонента Корзина в файле mutator.php находим такой код:

    $totalData = array(
    	'DISABLE_CHECKOUT' => (int)$result['ORDERABLE_BASKET_ITEMS_COUNT'] === 0,
    	'PRICE' => $result['allSum'],
    	'PRICE_FORMATED' => $result['allSum_FORMATED'],
    	'PRICE_WITHOUT_DISCOUNT_FORMATED' => $result['PRICE_WITHOUT_DISCOUNT'],
    	'CURRENCY' => $result['CURRENCY']
    );

    Вставляем сразу за этим фрагментом код:

    if ($result['allSum'] < 1500) {
    $totalData['DISABLE_CHECKOUT'] = true;
    echo "<h3>Оформление заказа возможно после наполнения корзины на сумму более 1500 рублей.</h3>";
    }

      Компонент "Оформление заказа"

    Посмотрите в видеоролике, как можно кастомиризовать шаблон компонента Оформление заказа:

    Описание решения:

    Копируем шаблон компонента sale.order.ajax средствами интерфейса системы с помощью команды Копировать шаблон компонента, как описано в уроке Теория.Кастомизация шаблона.

    В шаблоне компонента Оформление заказа в файле template.php находим следующий код:

    if ($request->get('ORDER_ID') <> '')
    {
    	include(Main\Application::getDocumentRoot().$templateFolder.'/confirm.php');
    }
    elseif ($arParams['DISABLE_BASKET_REDIRECT'] === 'Y' && $arResult['SHOW_EMPTY_BASKET'])
    {
    	include(Main\Application::getDocumentRoot().$templateFolder.'/empty.php');
    }
    else
    {
    

    Добавляем код:

        if ($arResult['ORDER_PRICE'] < 1500)
        {
            echo "<h3>Оформление заказа возможно после наполнения корзины на сумму более 1500 рублей.</h3>";
            return;
        }
    

    Задача решена.



    Создание структуры сайта

    Цитатник веб-разработчиков.

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

    Создание структуры сайта производится в соответствии с ТЗ на сайт.

    На сайте должна быть представлена статическая (о компании, контакты) и динамическая (новости, каталоги, форум) информация.

    Создайте требуемую для вас структуру файлов и папок. При создании структуры нельзя забывать про служебные страницы - поиск и карту сайта.

    Примечание: разработчик должен взять за правило: на проекте должны быть вменяемые тексты на страницах 404, 500 ошибок и сообщение о технических работах.

    Пример структуры сайта:

    Раздел сайта Каталог/файл Тип информации Используемые компоненты
    Главная /index.php Динамическая информация bitrix:news.list
    bitrix:catalog.top
    Новости /news Динамическая информация bitrix:news
    Магазины /shops Статическая страница нет
    Книги /books/ Динамическая информация bitrix:catalog
    Форум /forum Динамическая информация bitrix:forum
    О компании /about/ Статическая страница нет
    Контакты /contacts/ Статическая страница нет
    Карта сайта (служебная страница) /search/map.php Динамическая информация bitrix:main.map
    Результаты поиска (служебная страница) /search/index.php Динамическая информация bitrix:search.page
    Обработчик 404 ошибки /404.php Динамическая информация bitrix:main.map
    обработчик 500-й ошибки /500.html Статическая HTML страница (не Bitrix Framework) нет

    Список ссылок по теме:

    Структура файлов

    Структура файлов

    Файловая структура Bitrix Framework организована таким образом, что программные компоненты ядра продукта были отделены от пользовательских файлов, а также файлов, определяющих внешнее представление сайта. Данная особенность позволяет:

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

    Вся система целиком лежит в каталоге /bitrix/, в него входят следующие подкаталоги и файлы:

    • /activities/ - папки действий для бизнес-процессов;
    • /admin/ - административные скрипты;
    • /cache/ - файлы кэша;
    • /components/ - папка для системных и пользовательских компонентов;
    • /css/ - общие стили модулей;
    • /gadgets/ - папки гаджетов;
    • /js/ - файлы javascript модулей;
    • /stack_cache/ - файлы кеша "с вытеснением";
    • /services/ - публичные сервисы модулей;
    • /themes/ - темы административного раздела;
    • /wizards/ - папки мастеров;
    • /images/ - изображения используемые как системой в целом, так и отдельными модулями;
    • /managed_cache/ - управляемый кеш;
    • /modules/ - каталог с модулями системы, каждый подкаталог которого имеет свою строго определённую структуру;
    • /php_interface/ - вспомогательный служебный каталог, в него входят следующие каталоги и файлы:
      • dbconn.php - параметры соединения с базой. С версии 20.900.0 параметры соединения берутся из файла /bitrix/.settings.php,
      • init.php - дополнительные параметры портала,
      • after_connect.php - подключается сразу же после создания соединения с базой,
      • dbconn_error.php - подключается при ошибке в момент создания соединения с базой,
      • dbquery_error.php - подключается при ошибке в момент выполнения SQL запроса,
      • /ID сайта/init.php - дополнительные параметры сайта; файл подключается сразу же после определения специальной константы c идентификатором сайта - SITE_ID,
    • /templates/ - каталог с шаблонами сайтов и компонентов , в него входят следующие подкаталоги:
      • /.default/ - подкаталог с общими файлами, используемыми тем или иным шаблоном по умолчанию, структура данного каталога аналогична нижеописанной структуре каталога содержащего конкретный шаблон,
      • /ID шаблона сайта/ - подкаталог с шаблоном сайта, в него входят следующие подкаталоги и файлы:
        • /components/ - каталог с кастомизированными шаблонами компонентов,
        • /lang/ - языковые файлы принадлежащие как данному шаблону в целом, так и отдельным компонентам,
        • /images/ - каталог с изображениями данного шаблона,
        • /page_templates/ - каталог с шаблонами страниц и их описанием хранящимся в файле .content.php. Когда пользователь создает новую страницу, он может выбрать, по какому шаблону из представленных в этом каталоге это будет сделано,
        • header.php - пролог данного шаблона,
        • footer.php - эпилог данного шаблона,
        • template_styles.css - основной файл стилей для шаблона,
        • styles.css - CSS стили шаблона для визуального редактора (вкладка Стили сайта),
    • /tools/ - при инсталляции в этот каталог копируются дополнительные страницы, которые могут быть непосредственно использованы на любых страницах сайта: помощь, календарь, показ изображения и т.п.;
    • /updates/ - каталог, автоматически создаваемый системой обновлений;
    • .settings.php - [ds]файл настроек[/ds][di]Bitrix Framework имеет ряд специфичных настроек ядра, которые не имеют визуального интерфейса редактирования. Этот подход вызван тем, что изменение настроек или ошибка в них легко могут привести к неработоспособности системы (настройки подключения к базе данных, настройки кеширования и т.д.).

      Подробнее ...[/di] ядра D7;
    • header.php - стандартный файл, подключающий в свою очередь конкретный пролог текущего шаблона сайта; данный файл должен использоваться на всех страницах публичной части;
    • footer.php - стандартный файл, подключающий в свою очередь конкретный эпилог текущего шаблона сайта; данный файл должен использоваться на всех страницах публичной части;
    • license_key.php - файл с лицензионным ключом;
    • spread.php - файл используемый главным модулем для переноса [ds]cookie (куков)[/ds][di]; Cookie - это текстовая строка информации, которую веб-сервер передает в браузер посетителя сайта и которая сохраняется в файле на устройстве посетителя сайта. Как правило, используется для определения уникальности посетителя, времени его последнего визита, личных настроек, уникального идентификатора корзины покупок и т.д.

      Подробнее...[/di] посетителя на дополнительные домены различных сайтов;
    • .access.php - файл формируется динамически правами доступа;
    • routing_index.php - входная точка нового [ds]роутинга[/ds][di]Для запуска новой системы роутинга нужно перенаправить обработку 404 ошибок на файл routing_index.php в файле .htaccess:

      Подробнее ...[/di];
    • virtual_file_system.php - поддержка кириллицы в системе;
    • и другие служебные файлы и папки.

    В зависимости от используемой редакции некоторые каталоги и файлы могут отсутствовать либо добавлены теми или иными [dw]модулями[/dw][di]Например:
    redirect.php - файл используемый модулем Статистика для фиксации событий перехода по ссылке;
    rk.php - файл по умолчанию используемый модулем Реклама для фиксации событий клика по баннеру;
    stop_redirect.php - файл используемый модулем Статистика для выдачи какого либо сообщения посетителю, попавшему в стоп-лист;
    activity_limit.php - файл используемый модулем Статистика для выдачи сообщения роботу при превышении им лимита активности;
    и другие. [/di].


    Настройка инфоблоков

    Вывод динамичной информации из базы данных в Bitrix Framework осуществляется в основном с помощью информационных блоков. Создавая сайт необходимо продумать структуру информационных блоков. Рассмотрим пример простого использования информационного блока на примере каталога.

    Схема каталога товаров которую необходимо построить на сайте:

    • Группа 1
      • Группа 1.1
        • Свой фильтр по свойствам
      • Группа 1.2
        • Свой фильтр по свойствам

    Возможные способы реализации

    Первый способ. Все товары в одном инфоблоке. Информационный блок расположен на первом уровне (Группа 1).

    Плюсы:

    • иерархия, которой можно управлять из 1С;
    • легко управляемая структура каталогов в рамках сайта;

    Минусы:

    • сложности со свойствами товаров, если товары разнородные;
    • свойства будут храниться в одной таблице, что плохо повлияет на производительность.

    Второй способ. Товары размещены в нескольких инфоблоках. Информационные блоки расположены на втором уровне (Группа 1.1; Группа 1.2 и так далее).

    Плюсы:

    • индивидуальный фильтр с возможностью хранения свойств в различных таблицах;
    • списки сравнения будут сразу разбиты по типам товаров и соответствующим свойствам;

    Минусы:

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

    После выбора схемы реализации нужно создать тип информационного блока, собственно информационный блок, задать его свойства и наполнить контентом через импорт (csv, xml, 1C) или вручную.

    Последний шаг: настройка параметров компонента на созданный информационный блок.

    Список ссылок по теме:

    • Инфоблоки в курсе Контент-менеджер
    • Пример создания каталога товаров в курсе Администратор. Бизнес.

    Инфоблоки

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

    Информационные блоки - ключевой момент Bitrix Framework. Практически всё, что делается в системе в той или иной мере завязано на этот модуль, даже если это и не отображается явно.

    Информационные блоки представляют собой очередной уровень абстракции над обычными таблицами СУБД, своеобразная "база данных в базе данных". Поэтому к ним частично применимы все те правила, которых придерживаются при проектировании БД.

    Инфоблоки - сущность, которая в физической структуре БД создает 4 таблицы, не меняющиеся при изменении структуры данных: типы объектов, экземпляры объектов, свойства объектов и значения свойств объектов.

    Плюсы такого подхода:

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

    Минусы такого подхода:

    • повышенные требования к производительности,
    • непрозрачность при прямом доступе к данным.

    Особенности упорядочивания элементов по разделам

    Упорядочивание элементов инфоблоков по разделам может быть очень удобным для навигации по инфоблоку в административном интерфейсе. Фасетное упорядочивание делает навигацию ещё более удобной.

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

    Особенности работы заключаются в некотором неудобстве работать отдельно именно с разделами. Так, например, если помимо инфоблока Статьи существует инфоблок Книги, то велика вероятность, что его элементы также будут нуждаться в классификации по дате публикации и по тематикам. В этом случае придётся ещё раз создавать такую же структуру разделов. Также весьма непросто будет вывести, например, список всех материалов (статей и книг) по одной тематике, упорядочив их по дате публикации. Также сложно будет вывести общий рубрикатор тематик в меню сайта.

    В таком случае следует завести отдельный инфоблок Тематики и добавить в инфоблоки Книги и Статьи свойство ссылочного типа Тематика, а также свойство Дата Публикации типа Дата. Навигацию в административном интерфейсе тогда будет удобнее осуществлять при помощи установки фильтров по этим свойствам.

    Интеграция ORM в информационных блоках

    С версии 19.0.0 модуля iblock добавлена поддержка ORM при работе с элементами инфоблоков.

    Список ссылок по теме:

    Работа с инфоблоками штатными средствами

    Порядок работы

    При создании раздела сайта с использованием информационных блоков желательно придерживаться определённого порядка действий. Этот порядок может отличаться в зависимости от степени готовности проекта и сложности конкретного ТЗ. Практически в любом случае вам потребуются следующие действия:

    • Внимательное продумывание структуры инфоблоков.
    • Создание нужного типа инфоблоков с настройкой параметров.
    • Создание самих инфоблоков с настройкой параметров.
    • Создание структуры внутри инфоблока.
    • Создание элементов инфоблока.
    • Создание физической страницы (в случае использования комплексного компонента) или страниц (при использовании простых компонентов) и размещение на ней компонента (компонентов) с последующей настройкой его свойств.
    • Кастомизация работы компонента под потребности ТЗ и дизайна сайта (кастомизация шаблона компонента, использование файлов result_modifier.php или component_epilog.php, кастомизация собственно компонента).
    • Настройка отображения данных инфоблока в административной части под нужды обычного пользователя. Как показывает практика, почти никто из Контент-менеджеров не желает ознакомиться с этой возможностью и не умеет перенастраивать отображение структуры инфоблоков под себя.

    Штатные возможности

    Штатные средства модуля Информационные блоки достаточно обширны. Не ограничено ни количество типов инфоблоков, ни число самих инфоблоков, ни количество свойств каждого инфоблока, ни количество разделов или элементов.

    Советы от веб-разработчиков.

    Максим Месилов: Для важных ИБ, которыми управляют несколько людей, желательно включать журналирование. Так вы сможете быстро найти концы в случае непонятного удаления элементов или их редактирования.

    Настройки журналирования - вкладка в настройках ИБ.

    Свойства инфоблоков

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

    • Строка - значение свойства задается в виде текстовой строки;
    • Число - значение свойства задается в виде числа;
    • Список - значение свойства выбирается из списка;
    • Файл - в качестве значения свойства используется файл;
    • Привязка к разделам - с помощью данного свойства можно задать связь между элементом данного инфоблока и разделами другого информационного блока;
    • Привязка к элементам - задание связи между элементами информационных блоков «поштучно»;
    • HTML/текст - значение свойства задается в виде текста с HTML-тегами;
    • Привязка к элементам по XML_ID - привязка хранится как строка и значением является XML_ID привязанного элемента;
    • Привязка к карте Google Maps - задается связь между элементом инфоблока и компонентом [dw]Google Map[/dw][di]для использования компонентов Google необходимо иметь ключ доступа. Инструкция по получению ключа находится в этом уроке.[/di]
    • Справочник - задается связь между элементом инфоблока и highload блоками;
    • Привязка к Яндекс.Карте - задается связь между элементом инфоблока и компонентом Яндекс.Карта;
    • Счетчик - аналог autoincrement для БД. При добавлении элемента инфоблока значение будет больше на единицу, чем последнее. Стартовое значение задается произвольно. Можно использовать для журналов учета входящих документов и т.п., где должна быть непрерывная нумерация документов.
    • Привязка к пользователю - с помощью данного свойства можно задать связь между элементом данного инфоблока и пользователями системы;
    • Дата/Время - значение свойства задается в виде даты/времени;
    • Видео - задается связь между элементом списка и медиафайлом;
    • Привязка к элементам в виде списка - задание связи между элементами списком;
    • Привязка к теме форума - с помощью данного свойства можно задать связь между элементом данного инфоблока и темами форума;
    • Привязка к файлу (на сервере) - с помощью данного свойства можно задать связь между элементом инфоблока и файлом на удаленном сервере;
    • Привязка к элементам с автозаполнением - задается связь с элементами с автозаполнением;
    • Привязка к разделам с автозаполнением - задается связь с разделами с автозаполнением;
    • Привязка к товарам (SKU) - задается связь с товарными предложениями (SKU).

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

    Свойства разделов инфоблока

    Имеется возможность задавать пользовательские свойства для разделов инфоблоков. Пользовательские поля в своем коде должны обязательно иметь приставку UF_. Список типов полей несколько меньше, чем для самого инфоблока:

    • Видео;
    • Привязка к элементам highload-блоков;
    • Строка;
    • Целое число;
    • Число;
    • Дата со временем;
    • Дата;
    • Да/Нет;
    • Файл;
    • Список;
    • Привязка к разделам инф. блоков;
    • Привязка к элементам инф. блоков;
    • Опрос;
    • Шаблон.

    Как и свойства самого инфоблока, свойства разделов могут быть множественными и обязательными. Можно задать, будет ли участвовать в поиске и в фильтрации, может ли пользователь редактировать значение свойства, и будет ли оно отображаться в общем списке свойств. Кроме этого, у полей имеются дополнительные настройки, которые зависят от конкретного типа.

    Экспорт-импорт

    Добавление большого числа элементов инфоблоков вручную - очень трудоемкое занятие. С целью облегчения добавления информации можно применять импорт/экспорт данных с использованием разных форматов файлов. Поддерживаются форматы:

    • RSS
    • CSV
    • XML

    Экспорт и импорт в формате RSS организуются с помощью специальных компонентов RSS новости (экспорт) (bitrix:rss.out) и RSS новости (импорт) (bitrix:rss.show) соответственно.

    Экспорт данных из инфоблока в CSV файл выполняется с помощью формы Выгрузка информационного блока (Контент > Информационные блоки > Экспорт > CSV). Импорт данных, хранящихся в отдельном CSV файле, в информационный блок выполняется в форме Загрузка информационного блока (Контент > Информационные блоки > Импорт > CSV).

    Примечание: начиная с версии модуля 14.0.5, уровень глубины вложенности разделов для CSV-экспорта/импорта определяется настройками модуля Информационные блоки.

    В более ранних версиях экспорт в CSV был ограничен тремя уровнями вложенности и, если была необходимость увеличить уровень вложенности, то приходилось менять значение у переменной $NUM_CATALOG_LEVELS в файле \bitrix\modules\iblock\admin\data_import.php. Такая операция являлась изменением ядра продукта, пользоваться ею можно было только в крайнем случае, т.к. при последующем обновлении это изменение удалялось из кода.

    Примечание: если нужно осуществить экспорт инфоблока как торгового каталога, то необходимо воспользоваться путем Магазин > Настройки > Экспорт данных. Возможен и импорт из файла формата CSV: в качестве торгового каталога. В этом случае необходимо воспользоваться путем Магазин > Настройки > Импорт данных.

    Функционал экспорта\импорта инфоблоков в формат XML позволяет переносить не только содержимое инфоблоков, но и их свойства и изображения. Экспорт производится на странице Экспорт XML (Контент > Информ. блоки > Экспорт > XML). Импорт осуществляется на странице Импорт XML (Контент > Информ. блоки > Импорт > XML).

    Настройка форм

    Добавление/редактирование информационных блоков возможно как с административной, так и с публичной части. С публичной части это осуществляют контент-менеджеры. Формы добавления\редактирования инфоблоков желательно кастомизировать. В этом случае работа контент-менеджеров станет более легкой и удобной. Функция настройки форм - штатная и не требует программирования. Система позволяет:

    • Задать значения полей формы по умолчанию.
    • Задать автоматическую обработку фотографий по предварительно обозначенным параметрам.
    • Задать порядок следования закладок формы и полей на них.

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

    Типы хранения инфоблоков

    При создании информационных блоков рекомендуется хранить свойства инфоблока в отдельной таблице, причем все значения свойств одного элемента хранятся в одной строке. Эта технология называется Инфоблоки 2.0 и позволяет существенно ускорить работу системы, а также снять ряд ограничений в предыдущей версии инфоблоков. Например, теперь нет необходимости в дополнительном запросе CIBlockElement::GetProperty при выборе значений свойств функцией CIBlockElement::GetList.

    Возможности инфоблоков 2.0:

    • При выборке элементов можно сразу получать значения свойств, т.к. количество присоединяемых таблиц в запросе не увеличивается с каждым свойством, а всегда равно единице.
    • Фильтрация по значениям свойств происходит аналогично инфоблокам 1.0 (за исключением множественных).
    • Выборка значений множественных свойств не приводит к декартовому произведению результата запроса - значения свойств передаются в виде массива.
    • Для комбинированных фильтров по немножественным (единичным) свойствам появилась возможность ручного создания составных индексов БД для ускорения операций выборки.
    • Для инфоблоков 2.0 нет возможности "сквозной" выборки элементов, когда в фильтре указывается тип инфоблока и символьный код свойства. В фильтре необходимо указывать IBLOCK_ID.

    Важным является полная совместимость API. Т.е. техника использования инфоблоков, свойств, элементов и их значений одинакова для обеих версий инфоблоков.

    Связь между инфоблоками

    Bitrix Framework допускает создание взаимосвязей между информационными блоками с помощью свойств типа Привязка к элементам, Привязка к разделам, Привязка к элементам в виде списка, Привязка к элементам с автозаполнением, Привязка к разделам с автозаполнением и Привязка к товарам (SKU).

    Кеширование

    Цитатник веб-разработчиков.

    Антон Долганин: На данный момент кеширование Битрикса фактически совершенно, и не стоит изобретать своих велосипедов.

    Производительность

    При большом объеме базы данных может возникнуть проблема производительности. Связано это со следующими причинами:

    • обращения к этому массиву информации на чтение или на запись порождают конкурентные запросы;
    • запросы сами по себе быстрые, но их такое число, что БД начинает выстраивать из них очередь;
    • запросы медленные и тяжёлые, и к тому же очень частые.

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

    Примечание: пока разработчик не определится со стратегией кеширования и тем, что он хочет получить от неё, слепое включение кеширования может не дать ощутимых результатов.

    Если в качестве примера брать интернет-магазин, то для каждого товара будет создан файл в кеше, чтобы при следующем обращении покупателя сервер не напрягался с запросами к БД. Это и позволяет запускать магазины уровня Эльдорадо.

    Кеширование

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

    Для реализации этого созданы классы:

    • Cache - финальный класс для кеширования PHP переменных и HTML результата выполнения скрипта.
    • В старом ядре есть классы:
    • CPageCache - класс для кеширования HTML результата выполнения скрипта;
    • CPHPCache - класс для кеширования PHP переменных и HTML результата выполнения скрипта.

    Система Bitrix Framework включают в себя разные технологии кеширования:

    • Кеширование компонентов (или Автокеширование) - все динамические компоненты, которые используются для создания веб-страниц, имеют встроенную поддержку управления кешированием.

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

    • Неуправляемое кеширование - возможность задать правила кеширования ресурсоемких частей страниц. Результаты кеширования сохраняются в виде файлов в каталоге /bitrix/cache/. Если время кеширования не истекло, то вместо ресурсоемкого кода будет подключен предварительно созданный файл кеша.

      Кеширование называется неуправляемым, поскольку кеш не перестраивается автоматически после модификации исходных данных, а действует указанное время после создания, которое задается в диалоге Параметры компонента.

      Правильное использование кеширования позволяет увеличить общую производительность сайта на порядок. Однако необходимо учитывать, что неразумное использование кеширования может привести к серьезному увеличению размера каталога /bitrix/cache/.

    • Управляемый кеш - автоматически обновляет кеш компонентов при изменении данных. Для часто обновляемого большого массива данных использование управляемого кеша неоправданно.
    • HTML кеш лучше всего включить на какой-нибудь редко изменяющийся раздел с регулярным посещением анонимных посетителей. Технология проста в эксплуатации, не требует от пользователя отслеживать изменения, защищает дисковой квотой от накрутки данных и самовосстанавливает работоспособность при превышении квоты или изменении данных. (Считается устаревшей, рекомендуется использовать технологию Композитный сайт.) В плане многосайтовости поддерживает только многосайтовость на одном домене.
    • Кеширование меню. Для кеширования меню применяется специальный алгоритм, который учитывает тот факт, что большая часть посетителей - это незарегистрированные пользователи.

      Кеш меню управляемый и обновляется при редактировании меню или изменении прав доступа к файлам и папкам через административный интерфейс и API.

    Основные настройки кеширования расположены на странице Настройки кеширования (Настройки > Настройки продукта > Автокеширование).

    Примечание: В ядре D7 настройки кеширования производятся в специальном файле.

    Список ссылок по теме:


    Кеширование компонентов (Автокеширование)

    Цитатник разработчиков.

    Денис Шаромов: Периодически приходится наблюдать картину: хостер отключает аккаунт за большую нагрузку на сервер, клиент обращается к нам в техподдержку, мы видим, что кеширование компонентов не используется. Клиент объясняет это так: "мне сказали ваши партнёры-разработчики компонентов, что для этого компонента нельзя включать кеширование т.к. оно в битриксе криво работает".

    На самом деле проблема в неумении работать с кешированием.

    Одним из видов кеширования в Bitrix Framework является кеширование компонентов.

    Для ускорения обработки запроса клиента и уменьшения нагрузки на сервер компоненты должны использовать кеширование. Кешировать, как правило, необходимо ту информацию, которая не зависит от конкретного обратившегося человека. Например, список новостей сайта идентичен для всех посетителей. Поэтому нет смысла выбирать данные каждый раз из базы.

    Все динамические компоненты, которые используются для создания веб-страниц, имеют встроенную поддержку управления кешированием. Для использования технологии достаточно включить автокеширование одной кнопкой на административной панели. Это удобно в [dw]большинстве случаев[/dw][di]Возможны ситуации, когда как раз при разработке можно по ошибке "заставить" страницу с одним или несколькими компонентами работать по-разному с кэшированием или без него.[/di] использовать на этапе разработки, когда автокеширование можно выключить, что облегчит работу, а перед сдачей проекта снова включить. При этом все компоненты, у которых в настройках был включен режим автокеширования, создадут кеши и полностью перейдут в режим работы без запросов к базе данных.

    Внимание! При использовании режима Автокеширования, обновление информации, выводимой компонентами, происходит в соответствии с параметрами отдельных компонентов.

    Управление автокешированием располагается на закладке Кеширование компонентов (Настройки > Настройки продукта > Автокеширование):

    Примечание: При включении режима автокеширования компонентов, компоненты с настройкой кеширования Авто + Управляемое будут переведены в режим работы с кешированием.

    Чтобы обновить содержимое закешированных объектов на странице, вы можете:

    1. Перейти на нужную страницу и обновить ее содержимое, используя кнопку Сбросить кеш на панели инструментов.
    2. В режиме Правки сайта использовать кнопки для очистки кеша в панели отдельных компонентов.
    3. Использовать автоматический сброс кеша по истечении времени кеширования, для чего в настройках компонента выбрать режим кеширования Кешировать или Авто + Управляемое.
    4. Использовать автоматический сброс кеша при изменении данных, для чего в настройках компонента выбрать режим кеширования Авто + Управляемое.
    5. Перейти к настройкам выбранных компонентов и перевести их в режим работы без кеширования.

    Примечание: дополнительно о кешировании компонентов смотрите в уроке Кеширование в собственных компонентах.


    Добавление произвольного PHP кода

    Внимание! PHP-код непосредственно в теле страницы не рекомендуется!
    Использование php-кода в теле страницы считается дурным тоном при разработке сайта и говорит о низкой квалификации разработчика.

    В рабочую область страницы также может быть добавлен произвольный PHP-код, который в режиме редактирования в визуальном редакторе будет отображаться в виде значка . Если установить курсор на этот значок, то в панели Свойства отобразится непосредственно сам код:

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

    Список ссылок по теме:


    Middle, Средний уровень подготовки

    Вы уже можете работать со штатным функционалом. Теперь надо овладеть API и другими возможностями системы. К концу изучения главы вы должны уметь:

    • Обновлять систему.
    • Делать бекап и уметь восстанавливать сайт.
    • Настраивать систему через файл /bitrix/.settings.php.
    • Работать с компонентами: кастомизация и кеширование.
    • Работать с инфоблоками штатными методами и с помощью API.
    • Работать с агентами и событиями.
    • Уметь пользоваться отложенными функциями.
    • Уметь управлять SEF.
    • Уметь работать с ORM.
    • Знать и использовать Пользовательские поля
    • Понимать как можно тестировать готовые проекты.

    Обновление, бекап и восстановление

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

    • Обновление системы. Обновление системы необходимо для получения нового функционала, созданного вендором и для закрытия уязвимостей и обнаруженных багов.
    • Резервное копирование. Наличие бекапа - не просто "хороший тон" в работе. Это и гарантия того, что вам не придётся всё делать заново, с нуля.
    • Перенос сайта на хостинг. После завершения разработки необходимо перенести сайт с локальной установки на выбранный клиентом хостинг.

    Ядро D7

    Ядро D7

    Цитатник веб-разработчиков.

    Антон Долганин: D7 упрощает работу, значительно. Можно забыть про богомерзкое копирование осточертевших getlist из модуля в модуль. В общем, все низкоуровневое убрали под капот - круто, спасибо.

    Принцип совместимости, от которого компания "1С-Битрикс" не имеет права отказаться, обязывал выполнять большой объём работ, не направленных непосредственно на развитие Bitrix Framework. Это прямо влияло на скорость и качество разработки самой платформы, и косвенно влияло на распространение продуктов компании на рынке.

    D7 - основное программное ядро Bitrix Framework. Создано взамен начального на новом технологическом уровне с избавлением от "наслоений" устаревших технологий.

    Ядро D7 является основным, однако не весь функционал старого ядра на данный момент перенесён в него. В продукте продолжает работать весь старый API. И добавляется новый API D7. Постепенно, старый API должен стать чем-то типа адаптера, для совместимости. А вся логика с соответствующим рефакторингом должна переехать в D7.

    Внимание! Перед началом разработки убедитесь что в выбранном вами модуле есть классы и методы нового ядра.

    Основные отличия D7 от старого ядра

    • Базы данных
      • Поддерживаются базы данных: MySQL, [dw]MS SQL, Oracle, NoSQL[/dw][di]С 1 января 2017 года эти базы данных поддерживаются ограниченно: клиенты не могут скачивать обновления продукта платформы и воспользоваться возможностями новых версий продукта. [/di].
      • Отказ от неэффективного драйвера MSSQL ODBC, поддерживается только native драйвер.
      • Используется ORM (построитель запросов) c noSQL.
    • ООП
      • Сильная связанность. Весь код, относящийся к какой-то определённой области должен быть сосредоточен в одном месте, в одном классе, в одном наборе классов.
      • Компоненты с ООП (class.php) – возможность писать более структурированный код компонента и возможность наследования.
    • Разработка
      • Единообразный код. Все одинаковые вызовы называются одинаково, имеют одинаковые наборы параметров, возвращают унифицированные данные. То есть GetList пользователей не отличается от GetList'а групп пользователей.
      • Поддержка пространств имен.
      • Новые единые правила форматирования кода с жёстким контролем на уровне разработки.
      • Отказ от глобальных переменных
      • Поддержка exceptions.
      • Поддержка новых типов: дата, время, файлы. Неформатированные данные заменяются классами с методами. Значения таких типов являются объектами с методами форматирования и т.п.
      • Библиотека классов.
      • Унифицированные события. Возможность модификации и интеграции при помощи обработчиков.
      • Автозагрузка (autoload). Все сущности системы находятся в заранее определённых местах, соответственно, поддерживается автозагрузка без каких-то дополнительных действий со стороны разработчика.
      • Специализированные обработчики (классы, сущности) для разных ситуаций – типы приложений (http, cli).
      • Отложенная загрузка языковых файлов. Файлы из папкок /lang не подключаются одновременно с подключением компонента, они загружаются при первом запросе языковой фразы.
      • Провайдеры объектов для основных операций (кеш, лог).

    Настройка параметров ядра


    Особый файл

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

    Настройки в D7 выполняются в файле /bitrix/.settings.php. Напомним, что в старом ядре аналогичные настройки выполнялись в файле /bitrix/php_interface/dbconn.php. Файл .settings.php структурно сильно отличается от прежнего dbconn.php.

    Примечание: т.к. в системе параллельно используются 2 ядра – старое ядро и D7, то и оба файла настроек используются одновременно. Поэтому необходимо производить настройки обоих файлов.

    Даже если вы используете код только старого ядра, то файл .settings.php должен быть создан. Возможна ситуация, когда при установке обновлений какой-то из встроенных механизмов системы будет переписан на ядро D7. Если этот файл корректно не настроен, то это может привести к неработоспособности системы.

    Иногда бывают ситуации, что файл .settings.php отсутствует. Его можно создать в автоматическом режиме, если выполнить в [dw]командной строке[/dw][di]Командная PHP-строка – инструмент системы, позволяющий запускать произвольный код на PHP с вызовами функций. Подробнее...[/di]: Bitrix\Main\Config\Configuration::wnc();.

    Править параметры можно с помощью класса Configuration (Bitrix\Main\Config\Configuration).

    Примечание: Некоторые секции файла настроек содержат параметр readonly. Этот параметр означает, что данные настройки не будут изменены через API.

    Кроме этого настройки могут задаваться в файле .settings_extra.php. Базовый файл настроек содержит неизменные настройки, к которым есть API. Файл .settings_extra.php может содержать произвольный код, который меняет настройки динамически. Соответственно к нему нет API.


    Описание параметров

    Ниже описаны параметры, которые возможны для изменения:

  • Секция cache
  • Секция exception_handling
  • Секция connections
  • Корневая секция
  • Секция pull
  • Секция http_client_options
  • Секция services
  • Секция routing
  • Секция session
  • Ключ crypto_key
  • Секция smtp


  • Секция cache

    Отвечает за настройки кеширования, позволяет задать способ кеширования и его параметры.

    До версии 18.5.200 действовал такой формат записи:

    В версии 18.5.200 формат записи был изменён одновременно с возможностью использовать в кешировании Redis. Оба формата на данный момент работоспособны, но вендор настойчиво рекомендует использовать новый вариант записи.

    Примеры нового формата записи для разных способов кеширования.

    Параметр Значение
    type В качестве значения можно задать:
    • memcache
    • apc
    • xcache
    • files
    • [dw]redis[/dw][di]С версии 18.5.200.[/di]
    • none
    или указать массив со значениями:
    • class_name – класс, реализующий интерфейс ICacheEngine,
    • required_file – подключаемый файл с путем относительно папки /bitrix или /local (если требуется),
    • required_remote_file – подключаемый файл с абсолютным путем (если требуется),
    • extension – будет произведена попытка подключения расширения через extension_loaded. И только тогда подключится уже указанный класс.
    cache_flags Запрет на кеширование выборки или изменение ttl. Для этого установите ключи, куда входит название таблицы и суффиксы:
    'cache_flags'=>   array(
          'value'=> array(
             "b_group_max_ttl" => 200,
             "b_group_min_ttl" => 100,
          )
       ),

    Устанавливая b_group_max_ttl = 0, администратор запрещает кеширование этой сущности. Устанавливая b_group_min_ttl = 86400, админ расширяет наш TTL до суток (если в коде написано 3600).

    Примечание: Кроме type могут быть дополнительные параметры, если они нужны конкретному классу кеширования.

    Примечание: Настройки memcache могут задаваться так же в файле /bitrix/.settings_extra.php.

    Пример файла /bitrix/.settings_extra.php

    В базовом файле .settings.php содержатся неизменные настройки, к которым есть API. Файл .settings_extra.php может содержать произвольный код, который меняет настройки динамически в зависимости от каких-либо факторов. Соответственно для изменения настроек в этом файле нет API. Естественно в ходе выполнения этого произвольного кода должен быть возвращен массив подобной структуры базового файла.


    Секция exception_handling

    Отвечает за обработку ошибок.

      'exception_handling' => array (
        'value' => array (
          'debug' => false,
          'handled_errors_types' => E_ALL & ~E_NOTICE & ~E_STRICT & ~E_USER_NOTICE,
          'exception_errors_types' => E_ALL & ~E_NOTICE & ~E_WARNING & ~E_STRICT & ~E_USER_WARNING & ~E_USER_NOTICE & ~E_COMPILE_WARNING & ~E_DEPRECATED,
          'ignore_silence' => false,
          'assertion_throws_exception' => true,
          'assertion_error_type' => 256,
          'log' => array (
            'settings' => array (
              'file' => 'bitrix/modules/error.log',
              'log_size' => 1000000,
            ),
          ),
        ),
        'readonly' => false,
      ),
    Параметр Значение
    debug Ключ отвечает за то, будет ли выведена ошибка на страницу в браузере. Выводить ошибки рекомендуется только на время разработки или отладки. Иначе потенциально может быть разглашение информации.
    handled_errors_types В ключе задаются типы ошибок, которые система отлавливает (не игнорирует).
    exception_errors_types В ключе задаются типы ошибок, при которых система выбрасывает исключение.
    ignore_silence Ключ отменяет действие оператора управления ошибками (@).
    log В ключе задаются параметры логирования ошибок. Если ключа нет – логирования не будет. Если задать как показано в примере:
    'log' => array (
       'settings' => array (
          'file' => 'bitrix/modules/error.log',
          'log_size' => 1000000,
       ),
    ),

    то логироваться будет в файл с ограничением его размера. Если в корне сайта лежит файл error.php и выключен вывод ошибок на экран, то этот файл будет подключен в случае возникновения необработанного исключения.

    Если задать в общем случае, то можно логировать куда угодно:

    'log' => array(
       'class_name' => 'MyLog', // custom log class, must extends ExceptionHandlerLog; 
                                // can be omited, in this case default Diag\FileExceptionHandlerLog will be used
       'extension' => 'MyLogExt', // php extension, is used only with 'class_name'
       'required_file' => 'modules/mylog.module/mylog.php' // included file, is used only with 'class_name'
       'settings' => array( // any settings for 'class_name'
          ),
    ),

    В приведенном примере:

    • class_name – пользовательский класс, наследуемый от \ExceptionHandlerLog. Может быть не указан. В этом случае будет использоваться \Bitrix\Main\Diag\FileExceptionHandlerLog.
    • extension – расширение PHP, использовать можно только вместе с class_name.
    • required_file – включаемый файл. Используется только вместе с class_name.
    • settings – настройки для класса, указанного в class_name
    assertion_throws_exception Включение поддержки команды assert.
    assertion_error_type В ключе задаются типы ошибок, для которых не верный assert выбрасывает исключение.

    В handled_errors_types, exception_errors_types, assertion_error_type необходимо передать тип ошибки. Тип ошибки представляет из себя числовой код. Но коды знать и помнить не нужно. Например, параметр exception_errors_types. Что означает данная запись: E_ALL & ~E_NOTICE & ~E_WARNING & ~E_STRICT & ~E_USER_WARNING & ~E_USER_NOTICE & ~E_COMPILE_WARNING?

    Для начала обратимся к уровням ошибок интерпретатора PHP. Есть определенные значения и определенные константы, которые им соответствуют. В нашем же случае данная запись означает, что E_ALL (значение константы 2047), побитовое и не E_NOTICE, и не E_WARNING и не E_STRICT и не E_USER_WARNING и не E_USER_NOTICE и не E_COMPILE_WARNING. То есть E_ALL за исключением далее указанных констант, которые определяют тот или иной уровень ошибок интерпретатора PHP.


    Секция connections

    Внимание! С версии главного модуля 20.900.0 ядро продукта не использует параметры соединения БД из файла dbconn.php, настройки читаются только из .settings.php.

    Параметры соединения с базой данных и другими источниками данных. Задается имя класса и параметры соединения.

    'connections' => array (
            'value' => array (
                'default' => array (
                    'className' => '\\Bitrix\\Main\\DB\\MysqlConnection',
                    'host' => 'localhost:31006',
                    'database' => 'admin_bus',
                    'login' => 'admin_bus',
                    'password' => 'admin_bus',
                    'options' => 2,
                    'handlersocket' => array (
                        'read' => 'handlersocket',
                    ),
                ),
                'handlersocket' => array (
                    'className' => '\\Bitrix\\Main\\Data\\HsphpReadConnection',
                    'host' => 'localhost',
                    'port' => '9998',
                ),
            ),
            'readonly' => true,
        ),

    Внимание: Начиная с версии ядра 14.5.2 и выше возможно использование расширения mysqli.
    'className' => '\\Bitrix\\Main\\DB\\MysqliConnection',
    
    Также для этого в PHP должно быть установлено расширение mysqli, дополнительные проверки на наличие расширения не производятся! Включать mysqli нужно отдельно для старого (dbconn.php) и (.settings.php) ядра D7.

    Параметр Значение
    options Задаются флаги постоянного соединения и отложенности соединения с базой. Например:
    Connection::PERSISTENT == 1
    Connection::DEFERRED == 2
    Можно записывать их комбинации с помощью битовых операций. Например 3 – это и PERSISTENT, и DEFERRED.
    handlersocket В ключе указывается, какое соединение использовать для чтения (ключ read). Необходимо создать подключение, где будут указаны класс, хост и порт. В примере кода выше установлен параметр read, у которого указано значение handlersocket. А ниже собственно описание для [ds]соединения handlersocket[/ds][di]Традиционные ACID Базы данных в целом ряде задач затрудняют реализацию проектов. Для решения этих задач были предложены технологии NoSQL и HandlerSocket (в виде плагина к обычной MySQL).

    Подробнее...[/di].
    className имя класса, в которой собственно реализуется работа с конкретным типом БД.
    host имя хоста, где находится база данных. можно указать с портом
    database имя базы
    login логин пользователя базы данных
    password пароль пользователя базы данных

    Примечания:
    В рамках ORM возможна работа с несколькими базами данных.
    Для использования соединения handlersocket должна быть установлена библиотека HSPHP – PHP HandlerSocket client


    Корневая секция

    В корневой секции размещаются настройки общего характера.

    Параметр Значение
    disable_iconv Запрещает использование библиотеки iconv. Аналог константы BX_ICONV_DISABLE в старом ядре.
    logger Логгеры, реализующие интерфейс PSR-3. Подробнее...


    Секция pull

    Эта секция реально нужна только для хостинг-партнеров (для автоматизации развертывания), для всех остальных рекомендуется использовать настройку через административный интерфейс.

    Пример кода секции и таблица параметров


    Секция http_client_options

    Секция задаёт опции по умолчанию для класса Bitrix\Main\Web\HttpClient.

    [ICO_NEW data-adding-timestamp="1706700990"]

    С версии main 23.0.0 в HttpClient добавлена поддержка PSR-18. Помимо PSR, клиент работает в legacy-режиме, поддерживает очереди асинхронных запросов и библиотеку CURL.

    [/ICO_NEW]

    Параметр Значение
    redirect * по умолчанию true, производить ли редирект
    redirectMax * максимальное число таких редиректов (по умолчанию 5)
    waitResponse если true, то – ожидание ответа (по умолчанию), в ином случае – сразу возврат ответа
    socketTimeout время ожидания ответа в секундах (по умолчанию 30)
    streamTimeout таймаут потока в секундах (по умолчанию 60)
    version * версия http – 1.0 или 1.1 (по умолчанию 1.0)
    proxyHost / proxyPort / proxyUser / proxyPassword группа параметров для установки прокси
    compress если true, будет послан Accept-Encoding: gzip
    charset кодировка для тела объекта (используется в поле заголовка запроса Content-Type для POST и PUT)
    disableSslVerification если true, верификация ssl-сертификатов производиться не будет
    bodyLengthMax максимальная длина запроса
    privateIp если true, то будут включены запросы к частным IP-адресам
    debugLevel уровень отладки с использованием констант HttpDebug::*
    cookies * массив файлов cookie для HTTP-запроса
    headers * массив заголовков для HTTP-запроса
    useCurl включить использование библиотеки CURL (по умолчанию false)
    curlLogFile полный путь к файлу с логами CURL

    * – опции, работающие только в legacy-режиме.

    Пример настройки:

      'http_client_options' =>
       array (
         'value' =>
            array (
             'redirect' => true,//делаем редиректы, если требуется
             'redirectMax' => 10,//но не более 10
             'version' => '1.1'//работаем по протоколу http 1.1
            ),
         'readonly' => false,
       ),

    Правильно ли вы указали настройки, можно проверить так:

    use Bitrix\Main\Config\Configuration;
    print_r(Configuration::getValue("http_client_options"));

    Должен быть выведен ваш массив.


    Секция services

    Секция предназначена для регистрации сервисов. Подробнее о настройках смотрите в уроке [ds]Сервис Локатор[/ds][di]Сервис локатор (локатор служб) – это шаблон проектирования для удобной работы с сервисами приложения. Идея сервиса в том, что вместо создания конкретных сервисов напрямую (с помощью new), используется специальный объект (сервис локатор), который будет отвечать за создание, нахождение сервисов.

    Подробнее ...[/di]


    Секция routing

    Секция отвечает за подключение файлов с конфигурацией маршрутов [ds]роутинга[/ds][di]Доступно в модуле main начиная с версии 21.100.0.

    Подробнее...[/di], которые располагаются в папках /bitrix/routes/ и /local/routes/. Для подключения файлов опишите их:

    'routing' => ['value' => [
      'config' => ['web.php', 'api.php']
    ]], 
    
    // подключатся файлы:
    // /bitrix/routes/web.php, /local/routes/web.php,  
    // /bitrix/routes/api.php, /local/routes/api.php
    

    Секция session

    Ядро поддерживает четыре варианта для хранения (файлы, redis, database, memcache) данных сессии. Способ хранения описывается в секции session. Подробнее.

    Ключ crypto_key

    Чтобы ядро могло шифровать данные необходимо указать в настройках /bitrix/.settings.php ключ crypto_key. Подробнее...

    Секция smtp

    С версии 21.900.0 модуля main в файле /bitrix/.settings.php можно использовать опцию smtp, которая позволяет [ds]включить[/ds][di] Чтобы в продуктах 1С-Битрикс: Управление сайтом и коробочных версиях Битрикс24 включить возможность использования SMTP-сервера отправителя, отредактируйте файл /bitrix/.settings.php, добавив следующий код:

    Подробнее...[/di] возможность использования SMTP-сервера отправителя и организовать разделение потоков отправки писем.

    Список ссылок по теме:

    • Пример скрипта для редактирования настроек. (блог)


    Подключения к Redis, Memcache

    Для создания подключения в файле настроек bitrix/.settings.php, необходимо добавить в секцию connections именованное подключение.

    Redis

    Убедитесь, что у вас установлено расширение Redis для работы через PHP.

    Обычное подключение:

    'connections' => [
    	'value' => [
    		'default' => [
    		'className' => \Bitrix\Main\DB\MysqliConnection::class,
    		//... настройки существующего подключения в БД
    		],
    	'custom.redis' => [
    		'className' => \Bitrix\Main\Data\RedisConnection::class,
    		'port' => 6379,
    		'host' => '127.0.0.1',
    		'serializer' => \Redis::SERIALIZER_IGBINARY,
    	],
    	'custom2.redis' => [
    		'className' => \Bitrix\Main\Data\RedisConnection::class,
    		'port' => 6379,
    		'host' => '127.0.0.4',
    		'serializer' => \Redis::SERIALIZER_IGBINARY,
    		],
    		],
    	'readonly' => true,
    	]

    Про настройки вариантов serializer можно прочитать в официальной документации.

    Кластер

    Отличие от обычной конфигурации заключается лишь в servers дополнительных опциях: serializer, persistent, failover, timeout, read_timeout. Про них можно прочитать в официальной документации.

    Redis в режиме cluster может быть настроен двумя способами:

    1. Мультимастер кластер: N мастеров (и могут быть слейвы у каждого).
    2. Обычный кластер: 1 мастер и N слейвов

    Redis cluster в режиме мультимастер, указываются параметры всех мастеров:

    // bitrix/.settings.php
    return [
    //...        
    	'session' => [
    		'value' => [
    			'mode' => 'default',
    			'handlers' => [
    				'general' => [
    					'type' => 'redis',   
    					'servers' => [
    						[
    						'port' => 6379,
    						'host' => '127.0.0.1',
    						],
    						[
    						'port' => 6379,
    						'host' => '127.0.0.2',
    						],
    						[
    						'port' => 6379,
    						'host' => '127.0.0.3',
    						],
    					'serializer' => \Redis::SERIALIZER_IGBINARY,
    					'persistent' => false,
    					'failover' => \RedisCluster::FAILOVER_DISTRIBUTE,
    					'timeout' => null,
    					'read_timeout' => null,
    					],
    				],           
    			],
    		]                   
    	] 
    ];

    Redis cluster в режиме 1 мастер + N слейвов. Указываются только параметры мастера блок с опциями опускается:

    return [
    	'session' => [
    		'value' => [
    			'mode' => 'default',
    			'handlers' => [
    				'general' => [
    					'type' => 'redis',
    					'servers' => [
    					[
    						'port' => '30015',
    						'host' => '127.0.0.1'
    						],
    					],
    				],
    			],
    		],
    	],
    ];

    Использование

    Чтобы получить экземпляр соединения, достаточно обратиться по имени соединения, используя метод \Bitrix\Main\Application::getConnection.

    /** @var \Redis $redisConnection **/
    $redisConnection = \Bitrix\Main\Application::getConnection('custom.redis')->getResource();
    $redisConnection->setnx('foo', 'bar');

    Memcache

    Убедитесь, что у вас установлено расширение Memcache для работы через PHP.

    Обычное подключение

    'connections' => [
    	'value' => [
    		'default' => [
    			'className' => \Bitrix\Main\DB\MysqliConnection::class,
    			//... настройки существующего подключения в БД
    		],
    		'custom.memcache' => [
    			'className' => \Bitrix\Main\Data\MemcacheConnection::class,
    			'port' => 11211,
    			'host' => '127.0.0.1',
    		],
          'custom42.memcache' => [
            'className' => \Bitrix\Main\Data\MemcacheConnection::class,
            'port' => 6379,
            'host' => '127.0.0.4',
    		],
    	],
    	'readonly' => true,
    ]
    

    Кластер

    Если необходимо создать кластер из memcache серверов, то достаточно добавить настройку servers.

    'connections' => [
    	'value' => [
    		'default' => [
    			'className' => \Bitrix\Main\DB\MysqliConnection::class,
    			//... настройки существующего подключения в БД
    				],
    		'custom.memcache' => [
    			'className' => \Bitrix\Main\Data\MemcacheConnection::class,
    			'servers' => [
    				[
    				'port' => 11211,
    				'host' => '127.0.0.1',
    				'weight' => 1, //про настройку weight читайте внимательно в документации по memcahe 
    				],
    				[
    				'port' => 11211,
    				'host' => '127.0.0.2',
    				'weight' => 1, //про настройку weight читайте внимательно в документации по memcahe
    				],
    			],
    		],
    	],
    	'readonly' => true,
    ]

    Использование

    Чтобы получить экземпляр соединения, достаточно обратиться по имени соединения, используя метод \Bitrix\Main\Application::getConnection.

    /** @var \Memcache $memcacheConnection **/
    $memcacheConnection = \Bitrix\Main\Application::getConnection('custom.memcache')->getResource();
    $memcacheConnection->set('foo', 'bar');

    Локальные настройки SMTP-сервера

    С версии 21.900.0 модуля main в продукты 1С-Битрикс: Управление сайтом и коробочные версии Битрикс24 добавлена новая опция smtp, с помощью которой можно организовать разделение потоков отправки писем.

       Теория

    Как письма уходят с сайта/портала

    В продуктах 1С-Битрикс: Управление сайтом и коробочных версиях Битрикс24 вызывается глобальная функция bxmail, которая вызывает стандартную функцию mail языка PHP, оборачивая письмо заголовками. Далее эта функция mail обращается к вашей внутренней инфраструктуре (в зависимости от того, как вы [ds]её настроили[/ds][di] Работа через Bitrix Framework требует настройки отправки и приема электронной почты. Возможны три варианта отправки:

    - через локальный sendmail или postfix (если сайт на Linux);

    - через внешний SMTP-сервер без авторизации (если на Windows);

    - через внешний сервер с авторизацией путем замены функции отправки почты.

    Подробнее...[/di]: postfix, sendmail или какое-то собственное решение, работающее на очередях – все письма отправляются через функцию mail).

    Все письма (рассылки, письма CRM, одиночные письма, письма для восстановления пароля, роботы и т.д.) уходят в один поток.

    С помощью новых локальных настроек SMTP-сервера можно настроить разделение потоков нужным вам образом.

    Преимущества использования SMTP-сервера отправителя

    • разделение потоков;
    • использование [dw]алиасов[/dw][di] Алиасы — это дополнительные имена почтовых ящиков. С их помощью можно присвоить одному почтовому ящику на вашем домене дополнительные имена. Например, у вас есть почтовый ящик mysite@example.com. Можно добавить алиас marketing<mysite@example.com> для отдела маркетинга. После этого пользователи, получившие письмо от отдела маркетинга, в качестве отправителя увидят просто marketing.

      Если пользователи ответят на это письмо, то их ответ попадёт на реальный ящик mysite@example.com. [/di] (alias);
    • простота настройки;
    • возможность отладки;
    • возможность отправки на хитах;
    • возможность держать подключение открытым (полезно для массовых рассылок).

      Включение SMTP-сервера

    Чтобы в продуктах 1С-Битрикс: Управление сайтом и коробочных версиях Битрикс24 включить возможность использования SMTP-сервера отправителя, отредактируйте файл [ds]/bitrix/.settings.php[/ds][di] Bitrix Framework имеет ряд специфичных настроек ядра, которые не имеют визуального интерфейса редактирования. Этот подход вызван тем, что изменение настроек или ошибка в них легко могут привести к неработоспособности системы (настройки подключения к базе данных, настройки кеширования и т.д.).

    Подробнее...[/di], добавив секцию smtp со следующим кодом:

     'smtp' =>
    	array (
    		'value' =>
    		array(
    			'enabled' => true,
    			'debug' => true, //optional
    			'log_file' => '/var/mailer.log', //optional
    		),
    	),
    

    Напоминаем, что коды всех секций файла .settings.php должны располагаться внутри одного (основного) массива настроек.

    Важно! При редактировании файла .settings.php будьте внимательны: ошибка может привести к неработоспособности системы.

    Параметры настройки:

    • enabled – включение возможности использования SMTP-сервера отправителя;
    • debug – необязательный параметр (по умолчанию выключен). Включите его, если хотите увидеть полностью весь процесс взаимодействия с SMTP-серверами;
    • log_file – необязательный параметр. Можно указать файл для сбора логов, прописав полный путь до файла (главное, чтобы к этому файлу был доступ). По умолчанию в директории проекта/сайта уже создан файл mailer.log, куда будут записываться все логи.

      Настройка SMTP-подключения

    SMTP-подключение можно настроить двумя способами:

    • При создании нового почтового сообщения [dw]добавить отправителя[/dw][di] [/di], в открывшейся форме кликнуть по ссылке [dw]SMTP-сервер[/dw][di] [/di] и заполнить появившиеся поля настройки подключения SMTP-сервера:

      • введите имя и email;
      • определите, кто сможет видеть данного отправителя письма, использующего этот SMTP-сервер: только вы или же все пользователи;

        Примечание: Каждому сотруднику можно подключить свой SMTP-сервер.

      • укажите почтовый сервер;
      • пропишите порт (в основном используются три значения: 25, 465, 587);
      • укажите [dw]ограничение[/dw][di] У разных почтовых серверов (Яндекс, Google, Mail и т.д.) есть разные ограничения на количество отправленных писем (чаще всего это ограничение на число отправленных писем за одни сутки). Если использовать эту опцию, то система 1С-Битрикс сама будет вести статистику числа отправленных писем и выдавать ошибку, если лимит писем исчерпан. [/di] отправки писем;
      • введите логин и пароль подключаемого SMTP-сервера.
    • Аналогичную форму подключения SMTP-сервера можно заполнить и в административном разделе сайта на странице Настройки > Настройки продукта > Почтовые и СМС события > Настройки SMTP, кликнув на кнопку Добавить SMTP-подключение:

      Нажмите на рисунок, чтобы увеличить

      Примечание: На текущий момент при добавлении SMTP-подключения через административный раздел сайта нельзя указать лимиты отправки писем.

    Обратите внимание! Отправка писем от имени и адреса, указанного при настройке локального SMTP-сервера, возможна только в тех случаях, когда в письме можно выбрать отправителя. Например, при создании Рассылок в Маркетинг > E-mail-маркетинг > [dw]Рассылки[/dw][di]local_smtp.png[/di], отправке писем из CRM или из раздела Почта корпоративного портала.

    Cистемные письма из "Битрикс24" или "1С-Битрикс: Управление сайтом" отправляются:
    • через SMTP, если e-mail по умолчанию совпадает с e-mail одного из настроенных SMTP отправителей;
    • либо
    • через .msmtprc, если таковых совпадений нет.

    Дополнительно

    Связаны ли описанные в уроке настройки с настройками SMTP в виртуальной машине?

    Описанные в данном уроке настройки SMTP-серверов не связаны с настройками [ds]SMTP виртуальной машины BitrixVM[/ds][di] Для настройки SMTP-клиента выполните следующее:

    1. Перейти в главном меню в 6. Configure pool sites > 4. Change e-mail settings on site и ввести имя хоста, для которого нужно настроить отправку почты

    Подробнее...[/di].

    Пример: допустим, у сайта на BitrixVM настроена почта Gmail [ds]через меню виртуальной машины[/ds][di] В уроке представлены настройки некоторых почтовых сервисов в виртуальной машине BitrixVM.

    Подробнее...[/di]. Добавим новое SMTP-подключение к Mail.ru через административный раздел сайта. Теперь при создании нового письма или рассылки в поле От кого можно выбрать отправителей двух почтовых сервисов: Gmail и Mail.ru. В зависимости от этого выбора почта будет отправлена либо средствами виртуальной машины, либо средствами ядра.

    Если SMTP настроен и локально, и в виртуальной машине, то не произойдёт ли конфликт настроек или дублирование писем?

    Нет, конфликта настроек или дублирования писем не возникнет. При отправке письма система проверяет, есть ли активный параметр smtp в файле /bitrix/.settings.php:

    • если есть, то отправка письма будет происходить через локальный SMTP;
    • если параметра нет, то письмо будет отправляться средствами сервера через [ds]msmtp[/ds][di]Для настройки SMTP-клиента выполните следующее:

      1. Перейти в главном меню в 6. Configure pool sites > 4. Change e-mail settings on site и ввести имя хоста, для которого нужно настроить отправку почты

      Подробнее...[/di].

    Можно ли проверить работу SMTP-сервера с помощью инструмента «Проверка системы»?

    Тест отправки почты в инструменте [ds]Проверка системы[/ds][di] Форма Проверка системы (Настройки > Инструменты > Проверка системы) предназначена для всесторонней проверки соответствия параметров системы, на которой осуществляется функционирование проекта, минимальным и рекомендуемым техническим требованиям продукта.

    Подробнее...[/di] проверяет только php функцию e-mail, что не имеет отношения к SMTP отправке. Таким образом, если у вас выполнены только локальные настройки SMTP-сервера, то письма отправляться будут, но тест выдаст ошибку.

    Для успешного прохождения теста настройте отправку писем через [ds]msmtp[/ds][di]Для настройки SMTP-клиента выполните следующее:

    1. Перейти в главном меню в 6. Configure pool sites > 4. Change e-mail settings on site и ввести имя хоста, для которого нужно настроить отправку почты

    Подробнее...[/di].



    Пространства имён

    Пространства имён

    Понятие [ds]пространств имен[/ds][di]Пространство имён (англ. namespace) - некоторое множество, под которым подразумевается модель, абстрактное хранилище или окружение, созданное для логической группировки уникальных идентификаторов (то есть имён).

    Подробнее...[/di] позволяет давать элементам системы более четкие имена, избавиться от множества префиксов имен, а также избежать потенциальных конфликтов. Все классы, поставляемые в стандартном дистрибутиве, должны находиться в пространстве имен Bitrix, которое не пересекается ни с PHP, ни с разработками партнёров. Каждый стандартный модуль определяет в пространстве имен Bitrix свое подпространство, совпадающее с именем модуля. Например, для модуля forum пространством имен будет Bitrix\Forum, а для модуля main - Bitrix\Main.

    Примечание: Для классов партнеров namespace может быть таким:
    namespace Asd\Metrika;  
     
    class CountersTable extends Entity\DataManager  
    {  
       ....

    Это значит, что данный класс (в /lib/) принадлежит модулю asd.metrika и к нему (после подключения указанного модуля) можно обращаться так:

    \Asd\Metrika\CountersTable::update();

    Сам класс лежит в файле asd.metrika/lib/counters.php.

    При необходимости модуль может организовывать подпространства внутри своего пространства имен. Например, Bitrix\Main\IO, Bitrix\Forum\SomeName\SomeNameTwo. Но такой возможностью следует пользоваться только если это оправдано для организации правильной архитектуры данного модуля.

    Правила наименования

    • Пространства имен должны именоваться "ВерхнимКэмелКейсом".
    • Не могут содержать в названии никаких символов, кроме букв латинского алфавита.
    • Название класса должно быть существительным. Надо стараться избегать ненужных сокращений и аббревиатур.

    Примеры:

    namespace Bitrix\Main\Localization;
    namespace Bitrix\Main\Entity\Validator;

    Сокращения, не являющиеся общепринятыми (в Bitrix Framework), использовать нельзя.

    Внимание! В тексте примеров на страницах курсов, как правило, будут отсутствовать упоминания о пространствах имён. Это делает текст читабельнее и проще для восприятия. Перед использованием примеров из документации в ваших проектах необходимо добавить неймспейс.

    Это можно сделать:

    • Используя PHPdoc
    • Используя IDE
    • кроме того, из контекста документации, как правило, понятно о каком классе идет речь.

    Допустимо сокращение полной записи. Вместо \Bitrix\Main\Class::Function() можно писать Main\Class::Function().


    Также допустимо и использование синонимов, вместо длинных пространств имен. Для этого необходимо использовать use. Например, мы имеем длинную конструкцию:

    \Bitrix\Main\Localization\Loc::getMessage("NAME");

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

    use \Bitrix\Main\Localization\Loc;
    ...
    Loc::getMessage("NAME");

    Список ссылок по теме:


    Исключения

    Исключения

    В D7 используется механизм исключений (exceptions).

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


    Примеры

    Если пользователь отправил форму с пустым полем Имя, то это - не исключительная ситуация. Это обычная ожидаемая ситуация, которая должна быть обработана соответствующим образом.

    Если же при вызове метода API для изменения элемента инфоблока был указан пустой id элемента, то это исключительная ситуация. Она не ожидаема и продолжать изменение элемента не имеет смысла.

    Если метод ожидает id пользователя, а вы передаёте строку, то это - исключение, так как метод не знает что делать со строкой в данном случае.

    Если метод GetList принимает фильтр timestamp, а разработчик написал tymestamp, то это будет исключением.

    Иерархия исключений

    Все исключения D7 наследуются от встроенного в PHP класса \Exception, который присутствует в PHP начиная с версии 5.1. У данного класса есть не переопределяемые методы getMessage(), getCode(), getFile(), getLine(), getTrace(), getTraceAsString(), а так же переопределяемый метод __toString().

    Есть понятие иерархии исключений. Оно нужно для того, чтобы можно было их (исключения) обработать, посмотреть какое из них сработало и в зависимости от этого предпринять какие-то действия. Общая схема иерархии имеет вид:

    Bitrix\Main\SystemException базовый класс всех системных исключений, от которого наследуются все остальные исключения. Данный класс переопределяет конструктор системного класса \Exception. Если системный класс на вход принимает сообщение и код ошибки:

    <?php
    public function __construct($message = null, $code = 0, Exception $previous = null);

    , то конструктор Main\SystemException на вход принимает кроме этого файл в котором было выброшено исключение и номер строки:

    <?php
    public function __construct($message = "", $code = 0, $file = "", $line = 0, \Exception $previous = null);

    Выбрасываемое исключение должно иметь максимально подходящий тип.

    Если ваш метод создает исключение, то необходимо описать это в phpDoc метода.

    В Bitrix Framework это делается таким образом:

     /**
         * Searches connection parameters (type, host, db, login and password) by connection name
         *
         * @param string $name Connection name
         * @return array|null
         * @throws \Bitrix\Main\ArgumentTypeException
         * @throws \Bitrix\Main\ArgumentNullException
         */
        protected function getConnectionParameters($name) {}

    Игнорирование исключений

    Иногда необходимо чтобы произошедшая ошибка не приводила к прерыванию выполнения скрипта. Пример такой реализации можно рассмотреть в самом продукте в виде работы административной страницы модуля CDN.

    Если включён CDN, то вверху страницы отображается информация о расходе трафика. В коде это реализовано так:

    $cdn_config = CBitrixCloudCDNConfig::getInstance()->loadFromOptions();
    $APPLICATION->SetTitle(GetMessage("BCL_TITLE"));
    require($_SERVER["DOCUMENT_ROOT"]."/bitrix/modules/main/include/prolog_admin_after.php");
    if (is_object($message))
    	echo $message->Show();
    
    if (CBitrixCloudCDN::IsActive())
    {
    	try
    	{
    		if ($cdn_config->getQuota()->isExpired())
    			$cdn_config->updateQuota();
    
    		$cdn_quota = $cdn_config->getQuota();
    		if ($cdn_quota->getAllowedSize() > 0.0 || $cdn_quota->getTrafficSize() > 0.0)
    		{
    			CAdminMessage::ShowMessage(array(
    				"TYPE" => "PROGRESS",
    				"DETAILS" => '

    '.GetMessage("BCL_CDN_USAGE", array( "#TRAFFIC#" => CFile::FormatSize($cdn_quota->getTrafficSize()), "#ALLOWED#" => CFile::FormatSize($cdn_quota->getAllowedSize()), )).'

    #PROGRESS_BAR#', "HTML" => true, "PROGRESS_TOTAL" => $cdn_quota->getAllowedSize(), "PROGRESS_VALUE" => $cdn_quota->getTrafficSize(), )); } } catch (Exception $e) { CAdminMessage::ShowMessage($e->getMessage()); } }

    По коду видно, что если CDN активен, то формируется прогресс-бар с выводом информации о расходе трафика. Но если в ходе выполнения этого кода произойдёт ошибка, то будет выброшено исключение. Это исключение будет перехвачено так как весь код находится в try и сработает ветка, после catch, где выводится сообщение об ошибке штатной функцией. Выполнение скрипта при этом не будет прервано:



    Приложения и контекст

    Приложение - это объект, отвечающий за инициализацию ядра.

    Приложение является базовой точкой входа (маршрутизатором) для обращения к глобальным сущностям ядра: соединение с источниками данных, управляемый кеш и т.п. Также приложение содержит глобальные данные, которые относятся к самому сайту и не зависят от конкретного хита. То есть, приложение является неизменяемой частью, не зависящей от конкретного хита.

    Любой конкретный класс приложения является наследником абстрактного класса Bitrix\Main\Application.

    Конкретный класс Bitrix\Main\HttpApplication отвечает за обычный http-хит на сайте.

    Приложение поддерживает шаблон Singleton (Одиночка). Т.е. в рамках хита существует только один экземпляр конкретного типа приложения. Его можно получить инструкцией

    $application = Application::getInstance();

    Контекст - это объект, отвечающий за конкретный хит. Он содержит запрос текущего хита, ответ ему, а также серверные параметры текущего хита. То есть это изменяемая часть, зависящая от текущего хита.

    Любой конкретный класс контекста является наследником абстрактного класса Bitrix\Main\Context. Осуществляется поддержка двух конкретных классов контекста: Bitrix\Main\HttpContext и Bitrix\Main\CliContext. Конкретный класс Bitrix\Main\HttpContext отвечает за обычный http-хит на сайте.

    Чтобы получить контекст выполнения текущего хита, можно воспользоваться кодом

    $context = Application::getInstance()->getContext();

    Если было инициализировано приложение типа Bitrix\Main\HttpApplication, то этот вызов вернет экземпляр контекста типа Bitrix\Main\HttpContext.

    Контекст содержит в себе запрос текущего хита. Для того, чтобы получить запрос, можно воспользоваться кодом:

    $context = Application::getInstance()->getContext();
    $request = $context->getRequest();

    Примечание: везде в примерах используется полная форма записи (иногда без указания пространств имен), позволяющая получить результат из любой точки кода. Но для данной конкретной точки кода могут существовать сокращенные формы для доступа к результату.

    Запрос представляет собой экземпляр класса, являющегося наследником класса Bitrix\Main\Request. В случае обычного http-запроса запрос будет являться экземпляром класса Bitrix\Main\HttpRequest, расширяющего Bitrix\Main\Request. Этот класс является по сути словарем, предоставляющим доступ к парам "ключ-значение" входящих параметров.

    Для того чтобы обратиться к входящему параметру, переданному методами GET или POST, можно использовать код:

    $value = $request->get("some_name");
    $value = $request["some_name"];

    Примечание: Код $value = $request["some_name"]; возвращает строку, которая прошла уже фильтры модуля безопасности. Однако это не говорит о ее безопасности, всё зависит от того, что с ней необходимо делать дальше.

    Другие полезные методы запроса:

    $value = $request->getQuery("some_name");   // получение GET-параметра
    $value = $request->getPost("some_name");   // получение POST-параметра
    $value = $request->getFile("some_name");   // получение загруженного файла
    $value = $request->getCookie("some_name");   // получение значения кука
    $uri = $request->getRequestUri();   // получение запрошенного адреса
    $method = $request->getRequestMethod();   // получение метода запроса
    $flag = $request->isPost();      // true - POST-запрос, иначе false

    Ошибки в D7

    В старом ядре API пытался додумывать за пользователя/разработчика, прощал ему ошибки и неточности. Результатом такого поведения обычно бывают сложно-отлавливаемые ошибки. К тому же такой подход порождает массу неявных соглашений, которые нужно знать.

    Например, пользователь/разработчик выбирает записи для удаления по фильтру. При этом он случайно описывается в названии фильтра. Типичное API старого ядра проигнорирует этот фильтр и вернет все записи. Следующая инструкция эти все записи успешно удалит.

    В D7 идеология другая. API ничего не должен додумывать за пользователя. API должен адекватно реагировать, если он встречается с неожиданной для него ситуацией, такой как незнакомый фильтр, не передан id, не хватает значения, лишнее значение, не должно вызываться в этом режиме и т.д.

    При выводе ошибки на экран (если режим отладки отсутствует), подключается файл [dwi include_error_php]/error.php[/dwi].

    Пример файла error.php

    В этом файле вы можете вывести ошибку в дизайне сайта, а также установить код статуса HTTP (например, "500 Internal Server Error").

    API

    API

    API (классы) модуля не делятся по базам данных. ORM скрывает в себе все тонкости работы с конкретной базой данных.

    В названиях классов не должны использоваться какие-либо префиксы или суффиксы.

    Каждый класс API модуля может лежать в отдельном файле с названием, совпадающим с именем класса, написанном в нижнем регистре. Классы, лежащие в корне пространства имен модуля, должны быть расположены в файлах, лежащих в корне папки /lib модуля. Классы, лежащие в подпространствах внутри пространства имен модуля, должны быть расположены в файлах, лежащих в соответствующих подпапках папки /lib модуля.

    Например, класс Bitrix\Main\Application должен быть расположен в файле /lib/application.php относительно корневой папки модуля main, класс Bitrix\Main\IO\File должен быть расположен в файле /lib/io/file.php относительно корневой папки модуля main, класс Bitrix\Forum\Message должен быть расположен в файле /lib/message.php относительно корневой папки модуля forum.

    При соблюдении этих правил именования после подключения модуля его классы подгружаются автоматически при первом обращении к ним. Никаких дополнительных действий для регистрации и подключения файлов с классами не требуется.

    Примечание: Тем не менее, дополнительная регистрация и подключение рекомендуется для часто используемых классов по соображениям производительности.

    Исключением из правил именования классов и файлов являются классы сущностей ORM (наследников Bitrix\Main\Entity\DataManager). Имена таких классов формируются с суффиксом Table. (Например: CultureTable, LanguageTable.) А имя файла не содержит суффикса table. Такие классы также подключаются автоматически.

    Примечание: Существует возможность вручную зарегистрировать класс в системе автозагрузки с помощью метода
    void Bitrix\Main\Loader::registerAutoLoadClasses(
    	$moduleName,
    	array $arClasses
    )
    Это можно использовать для объединения маленьких классов в один файл.

    Нестандартные классы (кастомные, партнёрские), должны находиться в собственных пространствах имен, совпадающих с названиями соответствующих партнеров. Каждый партнерский модуль определяет в пространстве имен партнера свое подпространство, совпадающее с именем модуля без имени партнера. Например, для модуля mycompany.catalog партнера "Mycompany" пространством имен будет MyCompany\Catalog. Остальные правила совпадают с правилами для стандартных модулей.

    Для подключения модуля в новом ядре используется инструкция:

    mixed Bitrix\Main\Loader::includeModule($moduleName);
    

    Правила наименования

    Классы:

    • Должны именоваться "ВерхнимКэмелКейсом".
    • Не могут содержать в названии никаких символов, кроме букв латинского алфавита.
    • Название класса должно быть существительным. Надо стараться избегать ненужных сокращений и аббревиатур.

    Примеры:

    class User;
    class UserInformation;

    Методы

    • Методы, в том числе методы класса, должны именоваться "нижнимКэмелКейсом".
    • Не могут содержать в названии никаких символов, кроме букв латинского алфавита.
    • Использование цифр допускается, если избежать иного не получается. Например: encodeBase64, getSha1Key.
    • Название метода должно начинаться с глагола.
    • Длина названия должна быть не менее 3-х символов.

    Примеры:

    run();
    setImage();
    getName();

    Константы

    • Константы, в том числе константы класса, должны быть написаны в ВЕРХНЕМ_РЕГИСТРЕ_С_РАЗДЕЛИТЕЛЕМ_ПОДЧЕРКИВАНИЕМ.
    • Могут содержать буквы латинского алфавита, знак подчеркивания и числа (не в первой позиции).

    Примеры:

    DATE_TIME_FORMAT
    LEVEL_7

    Члены класса, параметры методов и переменные

    • Должны именоваться "нижнимКэмелКейсом".
    • Не должны содержать префиксов означающих членство в классе, принадлежность параметрам, тип и прочие бессодержательные вещи. Пример лишних префиксов: $this->mAge, function setName($pName), $arrArray.
    • Могут содержать буквы латинского алфавита и числа (не в первой позиции).

    Примеры:

    $firstName = '';
    $counter = 0;

    Общепринятые сокращения в названиях переменных и методов

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

    Пример:

    $xmlDocument
    $mainXmlDocument
    HttpParser

    Сокращения, не являющиеся общепринятыми (в Битриксе), использовать нельзя.

    Документация


    Если нет описания API

    Живое описание АПИ

    Цитатник веб-разработчиков.

    Дмитрий Яковенко: Метод как всегда прекрасен и незадокументирован.

    При работе с Bitrix Framework очень большое значение имеет описание API. К сожалению составление описаний API нового функционала никогда не выходит одновременно с функционалом. Причин этого несколько:

    • Создавать описание API и сам функционал одновременно невозможно;
    • Создавать описание API сразу после выхода функционала – не практично, так как в первое время после выхода обнаруживается много багов, которые требуют исправления и, соответственно учета этих исправлений в документации.

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

    Для работы в таких условиях единственный выход у разработчика – смотреть сам код. В помощь для таких случаев есть специальный бесплатный модуль Живое описание АПИ, который сканирует текущие файлы ядра и выводит список доступных API функций и событий всех модулей.

    Возможности модуля:

    • Модуль доступен только пользователю с правами администратора системы.
    • Все модули сканируются последовательно один раз, после этого рядом с live_api.php появляется файл live_api.data.php, который содержит данные о функциях;
    • Можно выбрать не только модуль, но и интересующий класс;
    • В исходном коде имеющиеся функции и методы Bitrix Framework отображены в виде ссылок, которые ведут на их исходный код этих функций и методов.

    Как работать?

    • С помощью кнопки Сканировать модули отсканируйте текущее состояние.
    • В полях формы выберите модуль, класс, или воспользуйтесь поиском.

      Примечание: Если будет выбран только модуль, то в форме отобразится все, относящееся к этому модулю.

    Система автоматически отобразит все найденные сущности. По списку аргументов можно легко догадаться, что они означают.

    Например, метод CCurrency::GetList (см. иллюстрацию выше) имеет два обязательных параметра: поле сортировки и порядок сортировки. Оба передаются по ссылке. Третий параметр язык, по умолчанию принимает значение текущего языка.

    По клику на функцию открывается её описание в новом окне. Код функции читается прямо из файла, при этом уже скрипты не парсятся, вся необходимая информация передаётся в URL.

    По клику на событие или константу открывается метод, где оно инициируется. Вызов события (константы) подсвечивается.

    Примечание: Есть и онлайн версия этого модуля, созданная Антоном Долганиным.

    Примечание: В файлах ядра D7 есть комментарии в PHPdoc, оставляемые разработчиками. Эти комментарии не всегда попадают в документацию по ряду причин, но они могут пригодиться разработчикам в работе.

    Практика. Некоторые классы

    Конфигурация

    Расположено в пространстве имен \Bitrix\Main\Config. Состоит из двух классов: \Bitrix\Main\Config\Configuration и \Bitrix\Main\Config\Option.

    Configuration

    $realm = \Bitrix\Main\Config\Configuration::getValue("http_auth_realm");
    if (is_null($realm))
    	$realm = "Bitrix Site Manager"

    Класс отвечает за глобальные настройки всего приложения. (Это то, что в старом ядре определяется константами.) Класс оперирует единой базой настроек, которые хранятся в файле /bitrix/.settings.php. Данные хранятся произвольные. Например, для соединений может храниться целый пул данных для именованных соединений.

    Option

    $cookiePrefix = \Bitrix\Main\Config\Option::get('main', 'cookie_name', 'BITRIX_SM');
    $cookieLogin = $request->getCookie($cookiePrefix.'_LOGIN');
    $cookieMd5Pass = $request->getCookie($cookiePrefix.'_UIDN');

    Является в некоторой степени аналогом класс COption старого ядра и работает с параметрами модулей, сайтов, хранимых в базе данных. Это то, что управляется из административной части: настройки каких-то форм, установка и так далее.

    Файлы

    Работа с файлами объектно-ориентированная, вынесена в пространство имён Bitrix\Main\IO и обладает тремя базовыми классами:

    • Path – работа с путями, статический.
    • Directory – работа с папками.
    • File – работа с файлами.

    Кроме них есть и другие классы, в том числе и абстрактные, для организации иерархии.

    Другие классы

    В папке bitrix/modules/main/lib расположена библиотека классов для осуществления разных частых действий, которые вынесены в Main, а не разнесены по разным модулям. В том числе в соответствующих пространствах лежат файлы и API для работы:

    • Bitrix\Main\Data - с кешем, в том числе управляемый кеш.
    • Bitrix\Main\Text - с текстом: классы для конвертации текста и другие
    • Bitrix\Main\Type - с типами данных: дата, файл и другие.
    • Bitrix\Main\Web - с web: работа с URL, обращения по web'у и другие.

    Аналог CUtil::jSPostUnescape() в D7

    Если необходимо использовать HttpRequest при аякс запросах:

    Application::getInstance()->getContext()->getRequest()->getPost('name')

    то надо учитывать, что CUtil::JSPostUnescape не поможет в случае установки win-1251.

    Можно использовать:

    use Bitrix\Main\Web\PostDecodeFilter;
    ...
    Application::getInstance()->getContext()->getRequest()->addFilter(new PostDecodeFilter)
    

    После этого можно получать декодированные данные через getPost.


    Практика. Работа с D7 на примере местоположений

    Не забудьте подключить модуль sale.

    Типы местоположений

    Добавление типа местоположения:

    $res = \Bitrix\Sale\Location\TypeTable::add(array(
    	'CODE' => 'CITY',
    	'SORT' => '100', // уровень вложенности
    	'DISPLAY_SORT' => '200', // приоритет показа при поиске
    	'NAME' => array( // языковые названия
    		'ru' => array(
    			'NAME' => 'Город'
    		),
    		'en' => array(
    			'NAME' => 'City'
    		),
    	)
    ));
    if($res->isSuccess())
    {
    	print('Type added with ID = '.$res->getId());
    }

    Обновление типа местоположения

    $res = \Bitrix\Sale\Location\TypeTable::update(21, array(
    	'SORT' => '300',
    	'NAME' => array(
    		'ru' => array(
    			'NAME' => 'Новый Город'
    		),
    	)
    ));
    if($res->isSuccess())
    {
    	print('Updated!');
    }

    Удаление типа местоположения

    $res = \Bitrix\Sale\Location\TypeTable::delete(21);
    if($res->isSuccess())
    {
    	print('Deleted!');
    }

    Получение типа местоположения по ID

    $item = \Bitrix\Sale\Location\TypeTable::getById(14)->fetch();
    print_r($item);

    Получение списка типов с названиями на текущем языке

    $res = \Bitrix\Sale\Location\TypeTable::getList(array(
    	'select' => array('*', 'NAME_RU' => 'NAME.NAME'),
    	'filter' => array('=NAME.LANGUAGE_ID' => LANGUAGE_ID)
    ));
    while($item = $res->fetch())
    {
    	print_r($item);
    }

    Получаем группы с учётом иерархии, в которых есть данное местоположение

    <?
    \Bitrix\Main\Loader::includeModule('sale');
    
    function getGroupsByLocation($locationId)
    {
        $res = \Bitrix\Sale\Location\LocationTable::getList([
            'filter' => ['=ID' => $locationId],
            'select' => [
                'ID', 'LEFT_MARGIN', 'RIGHT_MARGIN'
            ]
        ]);
    
        if(!$loc = $res->fetch())
        {
            return [];
        }
    
        $locations = [$locationId];
    
        $res = \Bitrix\Sale\Location\LocationTable::getList([
            'filter' => [
                '<LEFT_MARGIN' => $loc['LEFT_MARGIN'],
                '>RIGHT_MARGIN' => $loc['RIGHT_MARGIN'],
                'NAME.LANGUAGE_ID' => LANGUAGE_ID,
            ],
            'select' => [
                'ID',
                'LOCATION_NAME' => 'NAME.NAME'
            ]
        ]);
    
        while($locParent = $res->fetch())
        {
            $locations[] = $locParent['ID'];
        }
    
        $res = \Bitrix\Sale\Location\GroupLocationTable::getList([
            'filter' => ['=LOCATION_ID' => $locations]
        ]);
    
        $groups = [];
    
        while($groupLocation = $res->fetch())
        {
            $groups[] = $groupLocation['LOCATION_GROUP_ID'];
        }
    
        return $groups;
    }

    Местоположения

    Добавление

    $res = \Bitrix\Sale\Location\LocationTable::add(array(
    	'CODE' => 'newly-created-location-code',
    	'SORT' => '100', // приоритет показа при поиске
    	'PARENT_ID' => 1, // ID родительского местоположения
    	'TYPE_ID' => 14, // ID типа
    	'NAME' => array( // языковые названия
    		'ru' => array(
    			'NAME' => 'Архангельск'
    		),
    		'en' => array(
    			'NAME' => 'Arkhangelsk'
    		),
    	),
    	'EXTERNAL' => array( // значения внешних сервисов
    		array(
    			'SERVICE_ID' => 1, // ID сервиса
    			'XML_ID' => '163000' // значение
    		),
    		array(
    			'SERVICE_ID' => 1,
    			'XML_ID' => '163061'
    		),
    	)
    ));
    if($res->isSuccess())
    {
    	print('Location added with ID = '.$res->getId());
    }
    else
    {
    	print_r($res->getErrorMessages());
    }

    Обновление

    $res = \Bitrix\Sale\Location\LocationTable::update(3156, array(
    	'PARENT_ID' => 33,
    	'NAME' => array(
    		'de' => array(
    			'NAME' => 'Arkhangelsk'
    		),
    	)
    ));
    if($res->isSuccess())
    {
    	print('Updated!');
    }

    Удаление

    $res = \Bitrix\Sale\Location\LocationTable::delete(3156);
    if($res->isSuccess())
    {
    	print('Deleted!');
    }

    Получение местоположения по ID

    $item = \Bitrix\Sale\Location\LocationTable::getById(3159)->fetch();
    print_r($item);

    Получение местоположения по CODE, с опциональной фильтрацией\выборкой полей. Фактически это обертка над \Bitrix\Sale\Location\LocationTable::getList().

    $item = \Bitrix\Sale\Location\LocationTable::getByCode('newly-created-location-code', array(
    	'filter' => array('=NAME.LANGUAGE_ID' => LANGUAGE_ID),
    	'select' => array('*', 'NAME_RU' => 'NAME.NAME')
    ))->fetch();
    print_r($item);

    Получение списка местоположений с названиями на текущем языке и кодами типов

    $res = \Bitrix\Sale\Location\LocationTable::getList(array(
    	'filter' => array('=NAME.LANGUAGE_ID' => LANGUAGE_ID),
    	'select' => array('*', 'NAME_RU' => 'NAME.NAME', 'TYPE_CODE' => 'TYPE.CODE')
    ));
    while($item = $res->fetch())
    {
    	print_r($item);
    }

    Получение прямых потомков узла с ID=1 с названиями на текущем языке, кодами и названиями типов местоположений

    $res = \Bitrix\Sale\Location\LocationTable::getList(array(
    	'filter' => array(
    		'=ID' => 1, 
    		'=CHILDREN.NAME.LANGUAGE_ID' => LANGUAGE_ID,
    		'=CHILDREN.TYPE.NAME.LANGUAGE_ID' => LANGUAGE_ID,
    	),
    	'select' => array(
    		'CHILDREN.*',
    		'NAME_RU' => 'CHILDREN.NAME.NAME',
    		'TYPE_CODE' => 'CHILDREN.TYPE.CODE',
    		'TYPE_NAME_RU' => 'CHILDREN.TYPE.NAME.NAME'
    	)
    ));
    while($item = $res->fetch())
    {
    	print_r($item);
    }

    Получение родительских узлов для трех узлов

    $res = \Bitrix\Sale\Location\LocationTable::getList(array(
    	'filter' => array(
    		'=ID' => array(3159, 85, 17), 
    		'=PARENT.NAME.LANGUAGE_ID' => LANGUAGE_ID,
    		'=PARENT.TYPE.NAME.LANGUAGE_ID' => LANGUAGE_ID,
    	),
    	'select' => array(
    		'PARENT.*',
    		'NAME_RU' => 'PARENT.NAME.NAME',
    		'TYPE_CODE' => 'PARENT.TYPE.CODE',
    		'TYPE_NAME_RU' => 'PARENT.TYPE.NAME.NAME'
    	)
    ));
    while($item = $res->fetch())
    {
    	print_r($item);
    }

    Получение пути от корня дерева до текущего элемента

    $res = \Bitrix\Sale\Location\LocationTable::getList(array(
    	'filter' => array(
    		'=ID' => 224, 
    		'=PARENTS.NAME.LANGUAGE_ID' => LANGUAGE_ID,
    		'=PARENTS.TYPE.NAME.LANGUAGE_ID' => LANGUAGE_ID,
    	),
    	'select' => array(
    		'I_ID' => 'PARENTS.ID',
    		'I_NAME_RU' => 'PARENTS.NAME.NAME',
    		'I_TYPE_CODE' => 'PARENTS.TYPE.CODE',
    		'I_TYPE_NAME_RU' => 'PARENTS.TYPE.NAME.NAME'
    	),
    	'order' => array(
    		'PARENTS.DEPTH_LEVEL' => 'asc'
    	)
    ));
    while($item = $res->fetch())
    {
    	print_r($item);
    }

    Получение списка корневых узлов с указанием количества потомков

    $res = \Bitrix\Sale\Location\LocationTable::getList(array(
    	'filter' => array(
    		'=PARENT_ID' => 0,
    		'=NAME.LANGUAGE_ID' => LANGUAGE_ID,
    		'=TYPE.NAME.LANGUAGE_ID' => LANGUAGE_ID,
    	),
    	'select' => array(
    		'ID',
    		'NAME_RU' => 'NAME.NAME',
    		'TYPE_CODE' => 'TYPE.CODE',
    		'TYPE_NAME_RU' => 'TYPE.NAME.NAME',
    		'CHILD_CNT'
    	)
    ));
    while($item = $res->fetch())
    {
    	print_r($item);
    }

    Получение внешних данных для местоположений с указанием кода сервиса

    $res = \Bitrix\Sale\Location\LocationTable::getList(array(
    	'filter' => array(
    		'CODE' => array('newly-created-location-code', '0000028090'),
    	),
    	'select' => array(
    		'EXTERNAL.*',
    		'EXTERNAL.SERVICE.CODE'
    	)
    ));
    while($item = $res->fetch())
    {
    	print_r($item);
    }

    Получение поддерева узла с названиями на текущем языке

    $res = \Bitrix\Sale\Location\LocationTable::getList(array(
    	'runtime' => array(
    		'SUB' => array(
    			'data_type' => '\Bitrix\Sale\Location\Location',
    			'reference' => array(
    				'>=ref.LEFT_MARGIN' => 'this.LEFT_MARGIN',
    				'<=ref.RIGHT_MARGIN' => 'this.RIGHT_MARGIN'
    			),
    			'join_type' => "inner"
    		)
    	),
    	'filter' => array(
    		'=CODE' => '0000028042',
    		'=SUB.NAME.LANGUAGE_ID' => LANGUAGE_ID
    	),
    	'select' => array(
    		'S_CODE' => 'SUB.CODE',
    		'S_NAME_RU' => 'SUB.NAME.NAME',
    		'S_TYPE_CODE' => 'SUB.TYPE.CODE'
    	)
    ));
    while($item = $res->fetch())
    {
    	print_r($item);
    }

    Получаем местоположения входящие в группу без учёта иерархии.

    \Bitrix\Main\Loader::includeModule('sale');
    
    
    /* Идентификатор группы */
    $groupId = 1
    
    /* Получаем местоположения входящие в группу */
    $res = \Bitrix\Sale\Location\GroupLocationTable::getConnectedLocations(1);
    
    while($item = $res->fetch())
    {
        var_dump($item);
    }

    Сервис Локатор

    Описание

    Сервис локатор (локатор служб) - это шаблон проектирования для удобной работы с сервисами приложения. Подробнее можно прочитать в статье.

    Идея сервиса в том, что вместо создания конкретных сервисов напрямую (с помощью new), используется специальный объект (сервис локатор), который будет отвечать за создание, нахождение сервисов. Своего рода реестр.

    Класс \Bitrix\Main\DI\ServiceLocator реализует интерфейс PSR-11. Доступен с версии main 20.5.400.

    Простой пример использования:

    $serviceLocator = \Bitrix\Main\DI\ServiceLocator::getInstance();
    
    if ($serviceLocator->has('someService'))
    {
    	$someService = $serviceLocator->get('someService');
    	//...$someService использование сервиса
    }

    Регистрация сервиса

    Регистрация через файлы настроек bitrix/.settings.php

    Прежде чем обращаться к сервису его необходимо зарегистрировать и один из способов это использование файлов настроек .settings.php. Все необходимые сервисы перечисляются в секции services.

    <?php
    // /bitrix/.settings.php
    	return [
    		//...
    		'services' => [
    			'value' => [
    				'someServiceName' => [
    					'className' => \VendorName\Services\SomeService::class,
    				],                      
    				'someGoodServiceName' => [
    					'className' => \VendorName\Services\SecondService::class,
    					'constructorParams' => ['foo', 'bar'],
    				],                      
    		],
    	'readonly' => true,
    	],	
    //...
    	];            

    В итоге сервисы будут доступны сразу после инициализации ядра. О том, какие есть способы их описания можно прочитать ниже.

    $serviceLocator = \Bitrix\Main\DI\ServiceLocator::getInstance();
    $someGoodServiceName = $serviceLocator->get('someGoodServiceName');
    $someServiceName = $serviceLocator->get('someServiceName');

    Регистрация через файлы настроек модуля {moduleName}/.settings.php

    В корне модуля так же может располагаться свой файл .settings.php. И в нём можно описать сервисы, которые принадлежат данному модулю и используются в нём. Семантика аналогична описанию в глобальном bitrix/.settings.php и правилам описания конфигураций.

    <?php
    // someModule/.settings.php
    	return [
    	//...
    	'services' => [
    		'value' => [
    			'someModule.someServiceName' => [
    			'className' => \VendorName\SomeModule\Services\SomeService::class,
    			],                      
    			'someModule.someAnotherServiceName' => [
    				'constructor' => static function () {
    					return new \VendorName\SomeModule\Services\SecondService('foo', 'bar');
    				},
    			],                      
    			'someModule.someGoodServiceName' => [
    				'className' => \VendorName\SomeModule\Services\SecondService::class,
    					'constructorParams' => static function (){
    						return ['foo', 'bar'];
    				},
    			],                      
    		],
    		'readonly' => true,
    	],
    	//...
    ];
    Внимание! Сервисы будут зарегистрированы только после подключения модуля. Также советуем именовать сервисы модулей, используя префикс имени модуля, чтобы не возникало проблемы уникальности кодов, например:
     iblock.imageUploader
    	disk.urlManager
    	crm.entityManager
    	crm.urlManager
    	someModule.urlManager.

    Регистрация через API

    Сервисы можно зарегистрировать и через API. Для этого воспользуйтесь методами класса \Bitrix\Main\DI\ServiceLocator

    Конфигурация сервиса

    Конфигурация описывается в виде массива и подсказывает сервис локатору способ создания объекта. На данный момент есть три способа описания:

    1. Указание класса сервиса. Сервис локатор создаст сервис вызвав new $className.
       'someModule.someServiceName' => [
      	'className' => \VendorName\SomeModule\Services\SomeService::class,
      ]
    2. Указание класса сервиса и параметров, которые будут переданы в конструктор. Сервис локатор создаст сервис вызвав new $className('foo', 'bar').
              'someModule.someServiceName' => [
                      'className' => \VendorName\SomeModule\Services\SomeService::class,
                     'constructorParams' => ['foo', 'bar'],
              ]                      
      
              'someModule.someServiceName' => [
                      'className' => \VendorName\SomeModule\Services\SomeService::class,
                      'constructorParams' => static function (){
                      return ['foo', 'bar'];
                  },
              ]
    3. Указание замыкания-конструктора, который должен создать и вернуть объект сервиса.
       'someModule.someAnotherServiceName' => [
                  'constructor' => static function () {
                              return new \VendorName\SomeModule\Services\SecondService('foo', 'bar');
                      },
              ]

    Контроллеры

    Термины

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

    Соглашения

    Примечание: С версии 20.600.87 Главного модуля (main) добавлена поддержка PSR-4 в ajax-контроллерах.
    • При вызове все имена параметров регистроЗАвисимые
    • При вызове все имена контроллеров регистроЗАвисимые
    • При вызове все имена действий регистроНЕзависимые
    • Полное имя действия из модуля генерируется по шаблону vendor:module.partOfNamespace0.partOfNamespace1.Controller.action.
      \Bitrix\Disk\Controller\Folder::getAction() 
      bitrix:disk.Controller.Folder.get
      
      \Bitrix\Disk\Controller\Intergation\Dropbox::connectAction() 
      bitrix:disk.Controller.Intergation.Dropbox.connect
      
      \Qsoft\Somedisk\Controller\SuperFolder::getAction() 
      qsoft:somedisk.Controller.SuperFolder.get
    • Если не указывать vendor:, то это означает, что это bitrix:
      \Bitrix\Disk\Controller\Folder::getAction() 
      disk.Controller.Folder.get
    • Если указан defaultNamespace в настройках модуля, то его можно опускать и не указывать в действии.
      defaultNamespace = \Bitrix\Disk\Controller
      	
      \Bitrix\Disk\Controller\Folder::getAction() 
      disk.Folder.get
    • Если указан альяс в настройках модуля, то можно использовать его вместо сокращаемого namespace.
      \Bitrix\Disk\CloudIntegration\Controller => cloud
      	
      \Bitrix\Disk\CloudIntegration\Controller\File::getAction() 
      disk.cloud.File.get
    • При вызове действия из компонента необходимо указывать полное имя компонента и имя действия (без суффикса Action).
      bitrix:list.example
      showFormAction
      
      BX.ajax.runComponentAction('bitrix:list.example', 'showForm', {
              ...
      }).then(function (response) {});
    • Время, дата, ссылки должны возвращаться не в строковом формате, а объектами.
      \Bitrix\Main\Type\DateTime
      \Bitrix\Main\Type\Date
      \Bitrix\Main\Web\Uri

    Контроллер

    Контроллеры - это часть MVC архитектуры, которая отвечает за обработку запроса и генерирование ответа.

    Действия

    Контроллеры состоят из действий, которые являются основной сутью и их в конечном итоге запрашивает пользователь для получения результата. В одном контроллере может быть одно или несколько действий. Для примера сделаем контроллер с двумя действиями item.add и item.view в модуле example.

    Первый шаг - создать в корне модуля файл .settings.php.

    <?php
    //modules/vendor.example/.settings.php
    return [
    	'controllers' => [
    		'value' => [
    			'defaultNamespace' => '\\Vendor\\Example\\Controller',
    		],
    		'readonly' => true,
    	]
    ];

    Далее создаётся сам файл контроллера (смотри подробное описание доступных методов):

    namespace Vendor\Example\Controller;
    use \Bitrix\Main\Error;
    class Item extends \Bitrix\Main\Engine\Controller
    {
    	public function addAction(array $fields):? array
    	{
    		$item = Item::add($fields);
    		if (!$item)
    		{
    			$this->addError(new Error('Could not create item.', {код_ошибки}));
    			return null;
    		}
    		return $item->toArray();
    	}
    	public function viewAction($id):? array
    	{
    		$item = Item::getById($id);
    		if (!$item)
    		{
    			$this->addError(new Error('Could not find item.', {код_ошибки}));
    					
    			return null;
    		} 
    		return $item->toArray();
    	}
    }

    В действии add (определенным методом Item::addAction) сначала идёт попытка создания некого Item по переданным $fields.

    Примечание. Массив $fields получается путем автоматического связывания параметров метода и $_REQUEST. Подробнее о принципах в уроке Внедрение зависимостей.

    Если не удалось выполнить создание по каким-то причинам, то возвращаем null и наполняем ошибками сам контроллер. В этом случае ядро сгенерирует ответ:

    {
    	"status": "error", //обратите внимание, что статус автоматически сменился
    	"data": null,
    	"errors": [
    		{
    			"message": "Could not create item.",
    			"code": {код}
    		}
    	]
    }

    Иначе добавляем Item и возвращаем из действия его представление в виде массива $item->toArray(). Таким образом ядро сгенерирует ответ:

    {
    	"status": "success",
    	"data": {
    		"ID": 1,
    		"NAME": "Nobody",
    		//...поля элемента
    	},
    	"errors": null
    }

    В целом, действие может вернуть не просто скаляры, но и объекты.

    В действии view (определенным методом Item::viewAction) сначала идёт попытка загрузки некого объекта Item по переданному параметру $id. Важно заметить, что $id будет автоматически получен из $_POST['id'] или $_GET['id'].

    Если данный параметр не найден, то ядро сгенерирует ответ с ошибкой:

    {
    	"status": "error",
    	"data": null,
    	"errors": [
    		{
    			"message": "Could not find value for parameter {id}",
    			"code": 0
    		}
    	]
    }
    

    Как обратиться к действию контроллера?

    Для вызова конкретного аякс-действия нужно знать и пользоваться соглашением по именованию. В нашем случае: Item::addAction -> vendor:example.Item.add Item::viewAction -> vendor:example.Item.view.

    vendor:example.Item.add, vendor:example.Item.view можно использовать для вызова действий через BX.ajax.runAction:

    BX.ajax.runAction('vendor:example.Item.add', {
    	data: {
    		fields: {
    			ID: 1,
    			NAME: "test"
    		} 
    	}
    }).then(function (response) {
    	console.log(response);
    	/**
    	{
    		"status": "success", 
    		"data": {
    			"ID": 1,
    			"NAME": "test"
    		}, 
    		"errors": []
    	}
    	**/			
    }, function (response) {
    	//сюда будут приходить все ответы, у которых status !== 'success'
    	console.log(response);
    	/**
    	{
    		"status": "error", 
    		"errors": [...]
    	}
    	**/				
    });

    Либо можно получить ссылку на действие и послать http-запрос самостоятельно.

    /** @var \Bitrix\Main\Web\Uri $uri **/
    $uri = \Bitrix\Main\Engine\UrlManager::getInstance()->create('vendor:example.Item.view', ['id' => 1]);
    echo $uri;
    // /bitrix/services/main/ajax.php?action=vendor:example.Item.view&id=1
    // выполняем GET-запрос

    Создание контроллеров и действий

    Создание контроллеров

    Контроллеры должны быть унаследованы от \Bitrix\Main\Engine\Controller или его потомков. Контроллеры могут располагаться внутри модуля, либо внутри компонента в файле ajax.php и быть контроллером для компонента.

    Создание действий

    Создание действий, это создание просто методов в конкретном контроллере. Метод обязан быть public и иметь суффикс Action.

    namespace Vendor\Example\Controller;
    class Item extends \Bitrix\Main\Engine\Controller
    {
    	public function addAction(array $fields)
    	{
    		//...
    	}
    }

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

    Если действие возвращает \Bitrix\Main\HttpResponse или его наследников, то данный объект и будет отправлен клиенту. Если действие возвращает некие данные, то они должны приводиться к скаляру или объекту, который после будет превращен в JSON и на основе него будет сформирован \Bitrix\Main\Engine\Response\AjaxJson.

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

    • \JsonSerializable
    • \Bitrix\Main\Type\Contract\Arrayable
    • \Bitrix\Main\Type\Contract\Jsonable

    Либо конкретные наследники \Bitrix\Main\HttpResponse:

    Создание классов-действий

    Есть возможность создавать классы-действия, которые унаследованы от \Bitrix\Main\Engine\Action. Подобная возможность может потребоваться, когда необходимо повторно использовать логику в нескольких контроллерах. Например, если реализуется одинаковый протокол обмена в разных модулях (стандартный поиск, выполнение пошаговых действий с прогрессом и тому подобное.). Для использования нужно описать в карте конфигурации контроллера метод configureActions:

    class Test extends \Bitrix\Main\Engine\Controller
    {
    	public function configureActions()
    	{
    		return [
    			'testoPresto' => [
    				'class' => \TestAction::class,
    				'configure' => [
    					'who' => 'Me',
    				],
    			],
    		];
    	}
    }

    И вот сам TestAction:

    <?php
    
    use \Bitrix\Main\Engine\Action;
    
    class TestAction extends Action
    {
    	protected $who;
    
    	//метод для дополнительного конфигурирования из контроллера. Если требуется установить
    	//какие-то значения во внутреннее состояние
    	public function configure($params)
    	{
    		parent::configure($params);
    
    		$this->who = $params['who']?: 'nobody';
    	}
    
    	//основной метод работы. Параметры так же автоматически связываются, как и в методе
    	//аякс-действии
    	public function run($objectId = null)
    	{
    		return "Test action is here! Do you know object with id {$objectId}? Mr. {$this->who}";
    	}
    }

    Для вызова этого действия нужно обращаться к нему testoPresto, как описано в карте конфигурации. Класс-действие поддерживает пре- и пост-фильтры и по сути ничем не отличается от обычного метода-действия. Смысл метода run() аналогичен другим методам аякс-действиям.

    Использование контроллеров внутри компонентов

    Предпочтительно создавать и использовать классы контроллеров, которые располагаются в модулях, как указано в данной статье, так как позволяет лучше организовать повторное использование вспомогательного кода и бизнес-логики.

    В простых случаях, если компонент самодостаточен и не используется активно с API-модуля, то можно использовать контроллеры внутри компонента.

    Жизненный цикл контроллера

    При обработке запроса Application создаёт контроллер на основе соглашения по именованию. Далее работу выполняет контроллер:

    • Controller::init() будет вызван после того, как контроллер создан и сконфигурирован.
    • Контроллер создает объект действия
      • Если действие не удалось создать, выкидывается исключение.
    • Контроллер вызывает метод подготовки параметров Controller::prepareParams.
    • Контроллер вызывает метод Controller::processBeforeAction(Action $action), в случае возврата true выполнение продолжается.
    • Контроллер выкидывает событие модуля main {полное_имя_класс_контроллера}::onBeforeAction, в случае возврата EventResult не равном EventResult::SUCCESS выполнение блокируется. На данном событии выполняются префильтры.
    • Контроллер запускает действие
    • Контроллер выкидывает событие модуля main {полное_имя_класс_контроллера}::onAfterAction. На данном событии выполняются постфильтры.
    • Контроллер вызывает методController::processAfterAction(Action $action, $result).
    • Приложение получает результат выполнения действия и если это данные, то создает \Bitrix\Main\Engine\Response\AjaxJson с этими данными, либо отправляет объект ответа от действия.
    • Приложение вызывает метод Controller::finalizeResponse($response), передавая финальный вариант ответа, который будет отправлен пользователю после всех событий и подготовок.
    • Вывод $response пользователю.

    Несколько namespaces

    Указание нескольких namespaces в модуле.

    В .settings.php можно указать несколько namespaces, помимо defaultNamespace. Это может быть необходимо, когда контроллеры расположены рядом со своими бизнес-сущностями. Например, в некотором модуле "Диск" у нас есть интеграция с облаками.

    <?php
    return [
    	'controllers' => [
    		'value' => [
    			'namespaces' => [
    				'\\Bitrix\\Disk\\CloudIntegration\\Controller' => 'cloud', //cloud - это альяс
    			],
    			'defaultNamespace' => '\\Bitrix\\Disk\\Controller',
    		],
    		'readonly' => true,
    	]
    ];

    Теперь у нас доступны для вызова контроллеры, которые расположены в обоих namespaces. Оба из них поддерживают вызов через полное имя действия и через сокращенную запись, так как у нас есть альяс cloud.

    Равносильны:

    disk.CloudIntegration.Controller.GoogleFile.get
    disk.cloud.GoogleFile.get
    
    disk.Controller.File.get
    disk.File.get

    Вызов модульного контроллера с подписанными параметрами компонента

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

    BX.ajax.runAction('socialnetwork.api.user.stresslevel.get', {
    	signedParameters: this.signedParameters, // результат $this->getComponent()->getSignedParameters() 
    	data: {
    		c: myComponentName, // например, 'bitrix:intranet.user.profile', параметры которого нам будут нужны
    		fields: {
    			//..
    		}
    	}
    });

    После этого внутри кода действия используйте:

    <?php
    	//...
    	public function getAction(array $fields)
    	{
    		//внутри распакованный, проверенный массив параметров
    		$parameters = $this->getUnsignedParameters();
            
    		return $parameters['level'] * 100;
    	}

    Контроллеры и компонент

    Для обработки аяксовых запросов в компоненте можно использовать два подхода:

    class.php

    Обработчик запросов в классе компонента (файл class.php) позволяет:

    • Инкапсулировать весь код в одном классе
    • Повторно использовать методы, данные и параметры компонента
    • Использовать языковые фразы, шаблоны компонента
    • Переопределять в компонентах-потомках стандартное поведение

    Чтобы класс компонента мог обрабатывать запросы необходимо:

    • Реализовать интерфейс \Bitrix\Main\Engine\Contract\Controllerable
    • Определить метод-действия с суффиксом Action
    • Реализовать метод configureActions (обычно возвращает пустой массив === конфигурацию по умолчанию)
    • Если нужно добавлять, обрабатывать ошибки, то стоит реализовать \Bitrix\Main\Errorable

    Примечание: При выполнении компонента в аяксовом режиме выполняются последовательно CBitrixComponent::onIncludeComponentLang, CBitrixComponent::onPrepareComponentParams и запуск действия с фильтрами..

    Внимание! При выполнении компонента в аяксовом режиме метод CBitrixComponent::executeComponent() не запускается.

    Пример

    <?php
    #components/bitrix/example/class.php
    use Bitrix\Main\Error;
    use Bitrix\Main\ErrorCollection;
    if(!defined("B_PROLOG_INCLUDED") || B_PROLOG_INCLUDED!==true)die();
    class ExampleComponent extends \CBitrixComponent implements \Bitrix\Main\Engine\Contract\Controllerable, \Bitrix\Main\Errorable
    {
    	/** @var ErrorCollection */
    	protected $errorCollection;
    	public function configureActions()
    	{
    		//если действия не нужно конфигурировать, то пишем просто так. И будет конфиг по умолчанию 
    		return [];
    	}
    	public function onPrepareComponentParams($arParams)
    	{
    		$this->errorCollection = new ErrorCollection();
    		
    		//подготовка параметров
    		//Этот код **будет** выполняться при запуске аяксовых-действий
    	}
    	
    	public function executeComponent()
    	{
    		//Внимание! Этот код **не будет** выполняться при запуске аяксовых-действий
    	}
    		
    	//в параметр $person будут автоматически подставлены данные из REQUEST
    	public function greetAction($person = 'guest')
    	{
    		return "Hi {$person}!";
    	}
    	//пример обработки ошибок
    	public function showMeYourErrorAction():? string
    	{
    		if (rand(3, 43) === 42)
    		{
    			$this->errorCollection[] = new Error('You are so beautiful or so handsome');
    			//теперь в ответе будут ошибки и будет автоматически выставлен статус ответа 'error'. 
    			
    			return  null;					
    		}
    		return "Ok";
    	}
    	
    	/**
    	* Getting array of errors.
    	* @return Error[]
    	*/
    	public function getErrors()
    	{
    		return $this->errorCollection->toArray();
    	}
    	/**
    	* Getting once error with the necessary code.
    	* @param string $code Code of error.
    	* @return Error
    	*/
    	public function getErrorByCode($code)
    	{
    		return $this->errorCollection->getErrorByCode($code);
    	}
    }

    ajax.php

    Обработчик контроллера запросов в файле ajax.php позволяет создать легковесный обработчик аякс-запросов, явно выделяя логику из компонента.

    Чтобы его реализовать:

    • Создать в корне компонента файл ajax.php
    • Определить метод-действия с суффиксом Action

    Сама логика работы контроллера полностью аналогична описанию контроллера модуля.

    <?php
    #components/bitrix/example/ajax.php
    if(!defined("B_PROLOG_INCLUDED") || B_PROLOG_INCLUDED!==true)die();
    class ExampleAjaxController extends \Bitrix\Main\Engine\Controller
    {
    	#в параметр $person будут автоматически подставлены данные из REQUEST
    	public function sayByeAction($person = 'guest')
    	{
    		return "Goodbye {$person}";
    	}
    	public function listUsersAction(array $filter)
    	{
    		$users = [];
    		//выборка пользователей по фильтру
    		//наполнения массива данными для ответа
    		
    		return $users;
    	}
    }

    Практика. Советы

    Для удобной отладки ошибок в жизненном цикле AJAX включайте debug => true в .settings.php, тогда вы сможете увидеть трейс ошибок, исключений.

    Если нужно:

    • отдать файл, то воспользуйтесь классами \Bitrix\Main\Engine\Response\File и \Bitrix\Main\Engine\Response\BFile:
      class Controller extends Engine\Controller
      	{
      	public function downloadAction($orderId)
      		{
      		//... find attached fileId by $orderId
      			return \Bitrix\Main\Engine\Response\BFile::createByFileId($fileId);
      		}
      	public function downloadGeneratedTemplateAction()
      		{
      		//... generate file ... $generatedPath
      		return new \Bitrix\Main\Engine\Response\File(
      			$generatedPath, 
      			'Test.pdf',
      			\Bitrix\Main\Web\MimeType::getByFileExtension('pdf')
      		);
      		}
      	public function showImageAction($orderId)
      		{
      		//... find attached imageId by $orderId
      		return \Bitrix\Main\Engine\Response\BFile::createByFileId($imageId)
      			->showInline(true)
      		;
      		}
      	}
    • отдать отресайзенное изображение, то используйте \Bitrix\Main\Engine\Response\ResizedImage.

      Помните, что нельзя давать пользователю запрашивать произвольные размеры для ресайза. Всегда подписывайте параметры или явно указывайте в коде размеры.
      class Controller extends Engine\Controller
      	{
      	public function showAvatarAction($userId)
      		{
      		//... find attached imageId by $userId
      		return \Bitrix\Main\Engine\Response\ResizedImage::createByImageId($imageId, 100, 100);
      		}
      	}
    • сгенерировать ссылку в контроллере на действие из этого же контроллера, то используйте \Bitrix\Main\Engine\Controller::getActionUri
      public function getAction(File $file)
      	{
      		return [
      			'file' => [
      				'id' => $file->getId(),
      				'name' => $file->getName(),
      				'links' => [
      					'rename' => $this->getActionUri('rename', array('fileId' => $file->getId())),
      				]				
      			]
      		];
      	}    
      
      public function renameAction(File $file)
      	{
      		...
      	} 
    • сгенерировать ссылку в контроллере на действие, которое будет отдавать контент, например, скачивание файла, то используйте \Bitrix\Main\Engine\Response\DataType\ContentUri. Это нужно для интеграции с модулем REST.
      public function getAction(File $file)
      	{
      		return [
      			'file' => [
      			'id' => $file->getId(),
      			'name' => $file->getName(),
      			'links' => [
      				'download' => new ContentUri($this->getActionUri('download', array('fileId' => $file->getId()))),
      				]				
      			]
      		];
      	}    
      
      public function downloadAction(File $file)
      	{
      		...
      	}
    • преобразовать данные SNAKE_CASE по стандарту в camelCase, то можно воспользоваться вспомогательными методами контроллера \Bitrix\Main\Engine\Controller::convertKeysToCamelCase, либо явной настройкой \Bitrix\Main\Engine\Response\Converter:
      public function getAction(File $file)
      	{
      		return [
      			'file' => $this->convertKeysToCamelCase($fileData)
      		];
      	}    
      
      public function showInformationAction(File $file)
      	{
      		$converter = new \Bitrix\Main\Engine\Response\Converter(Converter::OUTPUT_JSON_FORMAT & ~Converter::RECURSIVE);
          		
      		return $converter->process($data);
      	} 


    Практика. Взаимодействие с контроллерами из Javascript

    Как в AJAX-действии использовать параметры компонента?

    Часто бывает, что необходимо в аякс-запросе получить у компонента те же параметры, которые были при его отображении на странице.

    • Нужно описать те параметры, которые нужно использовать в методе listKeysSignedParameters
            class ExampleComponent extends \CBitrixComponent implements \Bitrix\Main\Engine\Contract\Controllerable
            {
            	protected function listKeysSignedParameters()
            	{
            		//перечисляем те имена параметров, которые нужно использовать в аякс-действиях					
            		return [
            			'STORAGE_ID',
            			'PATH_TO_SOME_ENTITY',
            		];
            	}
    • Получить подписанные параметры в шаблоне и, например, передать в ваш js класс компонента
            <!--template.php-->
            <script type="text/javascript">
            	new BX.ExampleComponent({
            		signedParameters: '<?= $this->getComponent()->getSignedParameters() ?>',
            		componentName: '<?= $this->getComponent()->getName() ?>'
            	});
            </script>
    • Вызывать BX.ajax.runComponentAction (как в примерах) c параметром signedParameters.
            BX.ajax.runComponentAction(this.componentName, action, {
            	mode: 'class',
            	signedParameters: this.signedParameters, //вот способ для передачи параметров компоненту.
            	data: data
            }).then(function (response) {
            	//some work
            });

    В итоге в вашем аякс-действии можно использовать параметры STORAGE_ID, PATH_TO_SOME_ENTITY. При этом параметры подписаны и целостность контролируется ядром.

    Если необходимо работать с подписанными параметрам внутри ajax.php, то используйте внутри действия контроллера метод Controller::getUnsignedParameters(), в нём будет массив распакованных данных.

    Дополнительно

    Практика. Постраничная навигация

    Чтобы организовать в аякс-действии постраничную навигацию, нужно в параметрах метода внедрить \Bitrix\Main\UI\PageNavigation и вернуть \Bitrix\Main\Engine\Response\DataType\Page.

    Пример:

    use \Bitrix\Main\Engine\Response;
    use \Bitrix\Main\UI\PageNavigation;
    public function listChildrenAction(Folder $folder, PageNavigation $pageNavigation)
    {
    	$children = $folder->getChildren([
    		'limit' => $pageNavigation->getLimit(),
    		'offset' => $pageNavigation->getOffset(),
    	]);
    	return new Response\DataType\Page('files', $children, function() use ($folder) {
    		//отложенный подсчет количества всего записей по фильтру		
    		return $folder->countChildren(); 	
    	});
    }

    Чтобы передать номер страницы в JS API, обратите внимание на navigation.

    BX.ajax.runAction('vendor:someController.listChildren', {
    	data: {
    		folderId: 12 
    	},
    	navigation: {
    		page: 3
    	}
    });
    Внимание! В Response\DataType\Page($id, $items, $totalCount) $totalCount может быть как числом, так и \Closure, которое может быть вычислено отложено. Это сделано из соображений производительности..

    Например, для rest вычисление общего количества требуется всегда, а для обычного аякса - это необязательно. Куда производительней и удобнее сделать отдельное аякс-действие для получения количества записей по определенном фильтру.


    Практика. Интеграция с модулем REST

    Если запрограммированы контроллеры внутри модуля, то их легко сделать доступными для модуля REST. Это удобно, так как мы повторно используем уже написанный код.

    Для этого нужно лишь поправить конфиг модуля .settings.php.

    Важно! Это новый способ, должна быть проставлена зависимость от REST 18.5.1.

    Важно! Если вы включили интеграцию с модулем REST, то все контроллеры и их действия будут доступны по REST API.

    Поэтому, если вы не хотите, чтобы какой-то контроллер был доступен по REST API, то:

    • либо не включайте интеграцию с модулем rest для этого модуля;
    • либо сделайте префильтрпо умолчанию в контроллере вашего модуля, который будет проверять, что текущий запрос не по REST, а в нужных действиях просто его убирайте.
    protected function getDefaultPreFilters()
    	{
    		return [
    			parent::getDefaultPreFilters(),
    			new Engine\ActionFilter\Scope(Engine\ActionFilter\Scope::NOT_REST),
    		];
    	}

    Итак, как включить в модуле интеграцию:

    <?php
    #.settings.php
    return [
    	'controllers' => [
    		'value' => [
    			'defaultNamespace' => '\\Bitrix\\Disk\\Controller',
    			'restIntegration' => [
    				'enabled' => true,
    			],
    		],
    		'readonly' => true,
    	]
    ];

    Как использовать в ajax-действии \CRestServer?

    Если вдруг ajax-действие должно использовать \CRestServer для какой-то специфической задачи, то это можно легко решить, объявив одним из параметров \CRestServer.

    Пример:

    public function getStorageForAppAction($clientName, \CRestServer $restServer)
    {
    	$clientId = $restServer->getClientId();
    	...
    }
    

    Будьте внимательны, в примере выше действие не будет работать через обычный ajax, так как \CRestServer $restServer там отсутствует и не может быть внедрен. Оно будет доступно только для модуля REST. Если же объявить его необязательным, то всё будет работать.

    public function getStorageForAppAction($clientName, \CRestServer $restServer = null)
    {
    	if ($restServer)
    	{
    		$clientId = $restServer->getClientId();
    	}
    	...
    }
    

    Как понять, вызываются ли действия в REST или AJAX окружении?

    Может возникнуть задача, что нужно отличить в каком контексте сейчас выполняется действие: это REST или это AJAX? Для этого можно спросить у контролера:

    \Bitrix\Main\Engine\Controller::getScope()
    
    //возможные варианты
    \Bitrix\Main\Engine\Controller::SCOPE_REST
    \Bitrix\Main\Engine\Controller::SCOPE_AJAX
    \Bitrix\Main\Engine\Controller::SCOPE_CLI
    


    Практика. Внедрение зависимостей

    Скалярные и нескалярные параметры

    Рассмотрим на примере ajax-действие, в котором есть параметры:

    public function renameUserAction($userId, $newName = 'guest', array $groups = array(2))
    {
    	$user = User::getById($userId);
    	...
    	$user->rename($newName);
    	
    	return $user;
    }
    

    Как будут получены параметры метода?

    Скалярные параметры $userId, $newName, $groups будут получены автоматически из REQUEST.

    • Сопоставление регистрозависимое.
    • Если поиск не удался, но есть значение по умолчанию - оно будет использовано.
    • Сначала поиск в $_POST, после в $_GET.

    Если параметр не удалось найти, то действие не будет запущено, сервер пошлёт ответ с сообщением об ошибке, что не указан обязательный параметр.

    Как внедрить объекты (нескалярные параметры)?

    По умолчанию внедрить можно:

    При этом имя параметра может быть произвольное. Связывание идёт по классу:

    public function listChildrenAction(Folder $folder, PageNavigation $pageNavigation);
    public function listChildrenAction(Folder $folder, PageNavigation $navigation);
    public function listChildrenAction(Folder $folder, PageNavigation $nav, \CRestServer $restServer);
    

    Внедрение своих типов

    Начнем с примера:

    class Folder extends Controller
    {
    	public function renameAction($folderId)
    	{
    		$folder = Folder::getById($folderId);
    		if (!$folder)
    		{
    			return null;
    		}
    		...
    	}
    	
    	public function downloadAction($folderId)
    	{
    		$folder = Folder::getById($folderId);
    		...
    	}
    	
    	public function deleteAction($folderId)
    	{
    		$folder = Folder::getById($folderId);
    		...
    	}
    }
    

    У нас есть обычный ajax-контроллер для некой папки Folder. Но все действия у нас в итоге производятся над объектом и везде у нас идёт попытка загрузки папки и т.д. Было бы здорово получать на вход метода сразу Folder $folder.

    class Folder extends Controller
    {
    	public function renameAction(Folder $folder);
    	public function downloadAction(Folder $folder);
    	public function deleteAction(Folder $folder);
    }
    

    И теперь это возможно:

    class Folder extends Controller
    {
    	public function getPrimaryAutoWiredParameter()
    	{
    		return new ExactParameter(
    			Folder::class, //полное имя класса подклассы, которого нужно создавать 
    			'folder', //конкретное имя параметра, который будет внедряться
    			function($className, $id){ //функция, которая создаст объект для внедрения. На вход приходит конкретный класс и $id
    				return Folder::loadById($id);
    			}
    		);
    	}
    }
    

    В js вызов:

    BX.ajax.runAction('folder.rename', {
    	data: {
    		id: 1 
    	}
    });
    

    Важно, в создающем замыкании после $className можно указывать сколько угодно параметров, которые требуются для создания объекта. При этом они будут связываться с данными из $_REQUEST так же, как и скаляры в обычных методах-действиях.

    class Folder extends Controller
    {
    	public function getPrimaryAutoWiredParameter()
    	{
    		return new ExactParameter(
    			Folder::class, 
    			'folder',
    			function($className, $entityId, $entityType){
    				return $className::buildByEntity($entityId, $entityType);
    			}
    		);
    	}
    	
    	public function workAction(Folder $folder);
    }
    

    В js вызов:

    BX.ajax.runAction('folder.work', {
    	data: {
    		entityId: 1,
    		entityType: 'folder-type'
    	}
    });
    

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

    class Folder extends Controller
    {
    	/**
    	 * @return Parameter[]
    	 */
    	public function getAutoWiredParameters()
    	{
    		return [
    			new ExactParameter(
    				Folder::class, 
    				'folder',
    				function($className, $id){
    					return $className::loadById($id);
    				}
    			),
    			new ExactParameter(
    				File::class, 
    				'file',
    				function($className, $fileId){
    					return $className::loadById($fileId);
    				}
    			),
    		];
    	}
    	
    	public function workAction(Folder $folder, File $file);
    }
    

    Есть ещё обобщенный способ описания внедрений:

    new \Bitrix\Main\Engine\AutoWire\Parameter(
    	Folder::class, 
    	function($className, $mappedId){
    		return $className::buildById($mappedId);
    	}
    );
    

    Чуть подробнее: сначала объявили имя класса, подклассы которого мы будем пытаться создавать, когда встретим их в ajax-действиях. Анонимная функция будет заниматься созданием экземпляра.

    • $className - это конкретное имя класса, которое указано в type-hinting'e.
    • $mappedId - это значение, которое получено из $_REQUEST. При этом в $_REQUEST будет искаться folderId. Имя параметра, который мы будем искать в $_REQUEST, по умолчанию создается как {имя переменной} + Id.
    •       Folder $folder   => folderId
            Folder $nene     => neneId
            File $targetFile => targetFileId 
      

      Таким образом, если в модуле есть класс, например, Model от которого наследуются все сущности, то можно описать тип:

      new \Bitrix\Main\Engine\AutoWire\Parameter(
      	Model::class, 
      	function($className, $mappedId){
      		/** @var Model $className */
      		return $className::buildById($mappedId);
      	}
      );
      

      И в дальнейшем легко и просто использовать type-hinting в своих ajax-действиях, сразу оперируя сущностями.



    Роутинг

    Доступно в модуле main начиная с версии 21.400.0. Для пользовательских модулей использование собственных роутов в папке модуля на данный момент не предусмотрено.

      Запуск

    Для запуска новой системы роутинга нужно перенаправить обработку 404 ошибок на файл routing_index.php в файле .htaccess:

    #RewriteCond %{REQUEST_FILENAME} !/bitrix/urlrewrite.php$
    #RewriteRule ^(.*)$ /bitrix/urlrewrite.php [L]
    
    RewriteCond %{REQUEST_FILENAME} !/bitrix/routing_index.php$
    RewriteRule ^(.*)$ /bitrix/routing_index.php [L]

    Примечание: С версии 23.500.0 роутер получается через метод \Bitrix\Main\Application::getRouter().

      Конфигурация

    Файлы с конфигурацией маршрутов располагаются в папках /bitrix/routes/ и /local/routes/. Для подключения файла следует описать его в файле [ds].settings.php[/ds][di]Bitrix Framework имеет ряд специфичных настроек ядра, которые не имеют визуального интерфейса редактирования. Этот подход вызван тем, что изменение настроек или ошибка в них легко могут привести к неработоспособности системы (настройки подключения к базе данных, настройки кеширования и т.д.).

    Подробнее ...[/di] в секции routing:

    'routing' => ['value' => [
            'config' => ['web.php', 'api.php']
    ]], 
    
    // подключатся файлы:
    // /bitrix/routes/web.php, /local/routes/web.php,  
    // /bitrix/routes/api.php, /local/routes/api.php
    

    Формат файла предполагает возврат замыкания, в которое передается объект конфигурации маршрутов:

    <?php
    
    use Bitrix\Main\Routing\RoutingConfigurator;
    
    return function (RoutingConfigurator $routes) {
            // маршруты
    };

    Поиск совпадений производится в том же порядке, в каком маршруты описаны в конфигурации.


    Маршруты

      Запросы

    Описание маршрутов начинается с определения метода запроса. Поддерживаются 3 комбинации методов:

    $routes->get('/countries', function () {
        // сработает только на GET запрос
    });
    
    $routes->post('/countries', function () {
        // сработает только на POST запрос
    });
    
    $routes->any('/countries', function () {
        // сработает на любой тип запроса
    });

    Для указания произвольного набора методов следует использовать метод methods:

    $routes->any('/countries', function () {
        // сработает на любой тип запроса
    })->methods(['GET', 'POST', 'OPTIONS']);

      Параметры

    Для определения параметра в адресе используются фигурные скобки:

    $routes->get('/countries/{country}', function ($country) {
            return "country {$country} response";
    });

    По умолчанию для параметров используется паттерн [^/]+. Для указания своего критерия используется метод маршрута where:

    $routes->get('/countries/{country}', function ($country) {
            return "country {$country} response";
    })->where('country', '[a-zA-Z]+');

    Если значение параметра может содержать /, то следует использовать паттерн .*:

    $routes->get('/search/{search}', function ($search) {
            return "search {$search} response";
    })->where('search', '.*'); 

    У параметров могут быть значения по умолчанию, в таком случае им необязательно присутствовать в адресе:

    $routes->get('/countries/{country}', function ($country) {
            return "country {$country} response";
    })->default('country', 'Australia');
    
    // маршрут будет выбран при запросе /countries/
    // при этом параметр country будет иметь указанное значение

    Для удобства можно задать и вовсе не участвующие в формировании адреса параметры:

    $this->routes->get('/countries/hidden', function ($viewMode) {
            return 'countries response {$viewMode}';
    })->default('viewMode', 'custom');

    Доступ к значениям параметров маршрута получается через параметры контроллера или объект текущего маршрута:

    $routes->get('/countries/{country}', function ($country) {
            return "country {$country} response";
    });
    
    ...
    
    $app = \Bitrix\Main\Application::getInstance(); 
    $country = $app->getCurrentRoute()->getParameterValue('country');    

      Имена

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

    $routes->get('/path/with/name', function () {
            return 'path with name';
    })->name('some_name');

    В дальнейшем это позволит обращаться к маршруту при генерации ссылок.

      Контроллеры

    В роутинге поддерживается несколько видов контроллеров:

    1. Контроллеры Bitrix\Main\Engine\Controller:
      $routes->get('/countries', [SomeController::class, 'view']);
      
           // будет запущено действие SomeController::viewAction()
    2. Отдельные действия контроллеров Bitrix\Main\Engine\Contract\RoutableAction:
      $routes->get('/countries', SomeAction::class);
    3. Замыкания:
      $routes->get('/countries/', function () {
               return "countries response";
           });

      В качестве аргументов возможно указать объект запроса Bitrix\Main\HttpRequest, объект текущего маршрута Bitrix\Main\Routing\Route, а также именованные параметры маршрута в любой комбинации:

      use Bitrix\Main\HttpRequest;
      use Bitrix\Main\Routing\Route;
      
      $routes->get('/countries/{country}', function ($country, HttpRequest $request) {
              return "country {$country} response";
      });
      
      $routes->get('/countries/{country}', function (Route $route) {
              return "country {$route->getParameterValue('country')} response";
      });
    4. Для обратной совместимости с публичными страницами предусмотрен класс Bitrix\Main\Routing\Controllers\PublicPageController:
       $routes->get('/countries/', new PublicPageController('/countries.php'));

    Группы

      Объединение в группы

    При схожих признаках у нескольких маршрутов рекомендуется объединять их в группы:

    $routes->group(function (RoutingConfigurator $routes) {
            $routes->get('/path1, function () {});
            $routes->get('/path2, function () {});
            $routes->get('/path3, function () {});
    });

    Само по себе объединение не влияет на поведение системы и имеет смысл именно при наличии общих признаков: параметры, prefix или name, которые будут рассмотрены ниже.

      Параметры группы

    Если у нескольких маршрутов есть общий параметр, то имеет смысл вынести его на уровень группы. Это позволит не описывать параметр отдельно для каждого маршрута:

    $routes
            ->where('serviceCode', '[a-z0-9]+')
            ->group(function (RoutingConfigurator $routes) {
            $routes->get('/{serviceCode}/info', [ServicesController::class, 'info']);
            $routes->get('/{serviceCode}/stats', [ServicesController::class, 'stats']);
    });

      prefix группы

    Если у маршрутов совпадает начало адреса, то вынесите его общим для группы:

    $routes->prefix('about')->group(function (RoutingConfigurator $routes) {
            $routes->get('company', function () {});
            $routes->get('personal', function () {});
            $routes->get('contact', function () {});
    }); 

    В примере выше адреса маршрутов будут восприняты как /about/company, /about/personal и /about/contact, таким образом не придется дублировать общую часть.

      name группы

    Похожим на prefix образом работает формирование общей части в имени роутов:

    $routes
         ->prefix('about')
         ->name('about_')
         ->group(function (RoutingConfigurator $routes) {
            $routes->name('company')->get('company', function () {});
            $routes->name('personal')->get('personal', function () {});
            $routes->name('contact')->get('contact', function () {});
        })
    ;

    В примере выше будут установлены имена маршрутов about_company, about_personal и about_contact.


    Генерация ссылок

      Маршруты с именем

    При описании маршрута задайте для него уникальное имя:

    $routes->get('/countries/{country}', function () {
            return 'some output';
    })->name('country_detail');

    И используйте это имя для генерации ссылки:

    $router = \Bitrix\Main\Application::getInstance()->getRouter();
    $url = $router->route('country_detail', ['country' => 'Australia']);
    
    // $url: /countries/Australia

    Имена выступают в роли уникальных идентификаторов. Если понадобится поменять формат ссылки, например поменять статическую часть:

    - $routes->get('/countries/{country}', function () {
    + $routes->get('/страны/{country}', function () {
            return 'some output';
    })->name('country_detail');

    То в этом случае не придется менять все ссылки на данный маршрут, поскольку они используют именно name для адресации.

      Маршруты без имени

    Если для маршрута не задано уникальное имя, то допустимо в ссылке указывать его адрес вручную. При наличии GET параметров может быть полезен хелпер \Bitrix\Main\Routing\Router::url():

    $country = 'Australia';
    $router = \Bitrix\Main\Application::getInstance()->getRouter();
    $url = $router->url("/contries/{$country}", [
            'showMoreDetails' => 1
    ]);
    
    // $url: /contries/Australia?showMoreDetails=1

    Логгеры

      Введение

    В ядро добавлены логгеры, реализующие интерфейс PSR-3:

    • базовый абстрактный класс \Bitrix\Main\Diag\Logger, реализующий интерфейс PSR-3;
    • файловый логгер \Bitrix\Main\Diag\FileLogger;
    • syslog логгер \Bitrix\Main\Diag\SysLogger.

    Логгеры пользуются форматтером логов \Bitrix\Main\Diag\LogFormatter, который делает замены плейсхолдеров в соответствии с PSR-3.

    Примечание: Библиотека доступна с версии main 21.900.0.

      Logger Interface

    Интерфейс \Psr\Log\LoggerInterface довольно прост, это - набор функций логирования, поддерживающих уровни логирования. Уровни задаются константами \Psr\Log\LogLevel::*.

    interface LoggerInterface
    {
    	public function emergency($message, array $context = array());
    	public function alert($message, array $context = array());
    	public function critical($message, array $context = array());
    	public function error($message, array $context = array());
    	public function warning($message, array $context = array());
    	public function notice($message, array $context = array());
    	public function info($message, array $context = array());
    	public function debug($message, array $context = array());
    	public function log($level, $message, array $context = array());
    }

    В сообщении могут быть {плейсхолдеры}, которые заменяются данными из ассоциативного массива $context.

    Также полезным может быть интерфейс \Psr\Log\LoggerAwareInterface, если вы хотите сообщить, что ваш объект готов принять логгер PSR-3:

    interface LoggerAwareInterface
    {
    	public function setLogger(LoggerInterface $logger);
    }

      Логгеры в продукте

    Логгеры в продукте расширены по сравнению с PSR-3. Можно:

    • установить минимальный уровень логирования, ниже которого логгер ничего не выведет,
    • установить форматтер.

    Файловый логгер \Bitrix\Main\Diag\FileLogger умеет записывать сообщения в файл, указанный в конструкторе. Если размер лога превышает указанный максимальный, производится однократная ротация файла. Ноль означает не делать ротацию. По умолчанию размер 1 Мб.

    $logger = new Diag\FileLogger($logFile, $maxLogSize);
    $logger->setLevel(\Psr\Log\LogLevel::ERROR);
    // выведет в лог
    $logger->error($message, $context);
    // НЕ выведет в лог
    $logger->debug($message, $context);

    Syslog логгер \Bitrix\Main\Diag\SysLogger является надстройкой над функцией php syslog. Конструктор принимает параметры, использующиеся функцией openlog.

    $logger = new Diag\SysLogger('Bitrix WAF', LOG_ODELAY, $facility);
    $logger->warning($message);

    На файловый логгер переведена функция AddMessage2Log и класс \Bitrix\Main\Diag\FileExceptionHandlerLog, а также логирование в модуле Проактивная защита (security).

    В настройках .settings.php можно:

    • указать логгер для AddMessage2Log с версии 23.500.0;
    • [ICO_NEW data-adding-timestamp="1703579666"]
    • переопределить логгер по умолчанию для модуля Конвертер файлов (transformer). Идентификатор id логгера – transformer.Default. С версии модуля transformer 23.0.0.
    • [/ICO_NEW]

      Форматирование сообщения

    В логгер можно установить форматтер сообщения. По умолчанию используется форматтер \Bitrix\Main\Diag\LogFormatter, реализующий интерфейс \Bitrix\Main\Diag\LogFormatterInterface:

    interface LogFormatterInterface
    {
    	public function format($message, array $context = []): string;
    }

    Конструктор форматтера принимает параметры $showArguments = false, $argMaxChars = 30 (показывать значение аргументов в трейсе, максимальная длина аргумента).

    $logger = new Main\Diag\FileLogger(LOG_FILENAME, 0);
    $formatter = new Main\Diag\LogFormatter($showArgs);
    $logger->setFormatter($formatter);

    Основная задача форматтера - подставлять значения в плейсхолдеры сообщения из массива контекста. Форматтер умеет обрабатывать определенные плейсхолдеры:

    • {date} - текущее время * ;
    • {host} - HTTP host * ;
    • {exception} - объект исключения (\Throwable);
    • {trace} - массив бектрейса;
    • {delimiter} - разделитель сообщений * .

    * - не обязательно передавать в массиве контекста, подставляется автоматически.

    $logger->debug(
    	"{date} - {host}\n{trace}{delimiter}\n", 
    	[
    		'trace' => Diag\Helper::getBackTrace(6, DEBUG_BACKTRACE_IGNORE_ARGS, 3)
    	]
    );

    Значения из массива контекста форматтер старается привести к удобному виду в зависимости от типа значения. Принимаются строки, массивы, объекты.

    Использование

    В простом виде объект может принять логгер снаружи, поддерживая интерфейс \Psr\Log\LoggerAwareInterface. Можно использовать соответствующий трейт:

    use Bitrix\Main\Diag;
    use Psr\Log;
    
    class MyClass implements Log\LoggerAwareInterface
    {
    	use Log\LoggerAwareTrait;
    	
    	public function doSomething()
    	{
    		if ($this->logger)
    		{
    			$this->logger->error('Error!');
    		}
    	}
    }
    
    $object = new MyClass();
    $logger = new Diag\FileLogger("/var/log/php/error.log");
    $object->setLogger($logger);
    $object->doSomething();

    Однако, не удобно менять код на работающем проекте, чтобы передать логгер в нужный объект. Для этого в классе логгера предусмотрена своя фабрика. На вход фабрике передается строка-идентификатор логгера:

    use Bitrix\Main\Diag;
    use Psr\Log;
    
    class MyClass implements Log\LoggerAwareInterface
    {
    	use Log\LoggerAwareTrait;
    	
    	public function doSomething()
    	{
    		if ($logger = $this->getLogger())
    		{
    		$logger->error('Error!');
    		}
    	}
    
    	protected function getLogger()
    	{
    		if ($this->logger === null)
    		{
    			$logger = Diag\Logger::create('myClassLogger', [$this]);
    			$this->setLogger($logger);
    		}
    
    		return $this->logger;
    	}
    }

    Настройка

    В корневой секции файла .settings.php логгеры указываются в ключе loggers. Синтаксис описания совпадает с настройками ServiceLocator. Отличие состоит в том, что сервис-локатор является реестром, а здесь настраивается фабрика.

    В замыкание-конструктор constructor можно передать дополнительные параметры через второй параметр фабрики Diag\Logger::create('logger.id', [$this]). Параметры позволяют гибко включать логирование в зависимости от переданных параметров, в том числе вызывать методы самого объекта.

    Дополнительно можно указать минимальный уровень журналирования (level) и собственный форматтер (formatter). Форматтер ищется в сервис локаторе по его идентификатору.

    // /bitrix/.settings.php
    return [
    	//...
    	'services' => [
    		'value' => [
    			//...
    			'formatter.Arguments' => [
    				'className' => '\Bitrix\Main\Diag\LogFormatter',
    				'constructorParams' => [true],
    			],
    		],
    		'readonly' => true,
    	],
    	'loggers' => [
    		'value' => [
    			//...
    			'main.HttpClient' => [
    //				'className' => '\\Bitrix\\Main\\Diag\\FileLogger',
    //				'constructorParams' => ['/home/bitrix/www/log.txt'],
    //				'constructorParams' => function () { return ['/home/bitrix/www/log.txt']; },
    				'constructor' => function (\Bitrix\Main\Web\HttpClient $http, $method, $url) { 
    					$http->setDebugLevel(\Bitrix\Main\Web\HttpDebug::ALL);
    					return new \Bitrix\Main\Diag\FileLogger('/home/bitrix/www/log.txt');
    				},
    				'level' => \Psr\Log\LogLevel::DEBUG,
    				'formatter' => 'formatter.Arguments',
    			],
    		],
    		'readonly' => true,
    	],
    	//...
    ];

    При указании замыкания-конструктора constructor желательно использовать файл .settings_extra.php, чтобы не потерять код при сохранении настроек из API.

    Существует \Psr\Log\NullLogger, его можно установить, если не хочется писать if($logger) перед каждым вызовом логгера. Однако, следует учитывать, стоит ли делать дополнительную работу по формированию сообщения и контекста.

      Классы

    Список классов, поддерживающих фабрику логгеров:

    КлассId логгераПередаваемые параметры
    \Bitrix\Main\Web\HttpClient main.HttpClient [$this, $this->queryMethod, $this->effectiveUrl]


    Вложенные транзакции

    "Вложенные" транзакции доступны с версии main 22.200.0.

    Что такое транзакция

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

    Mysql в движке InnoDB отвечает требованиям ACID к обработке транзакций (в MyISAM не поддерживается):

    • Atomicity - атомарность
    • Consistency - согласованность
    • Isolation - изолированность
    • Durability - надежность

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

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

    При отправке писем методом CEvent::Send файл может быть не отправлен, если в момент выполнения скрипта будет запущен крон, который отправляет письма. Это происходит потому, что в функции /bitrix/modules/main/lib/mail/event.php запись в таблицу b_event происходит до сохранения файла (58 строка и далее по условию).

    Старое правило

    Ранее, до обновления main 22.200.0, действовало правило:

    В API не должны использоваться функции управления транзакциями базы данных. Это прерогатива вызывающего (конечного) сценария.

    Такое правило было потому, что Mysql не поддерживает вложенные транзакции. Могла сложиться ситуация, когда конечный сценарий открывает транзакцию, вызываемый API тоже открывает свою транзакцию, в итоге все коммиты и роллбеки перемешиваются непредсказуемым образом.

    Сейчас в ядре появилась поддержка вложенных транзакций на уровне приложения (то есть при вызове соответствующего API, а не прямых запросов).

    Вложенные транзакции

    Для транзакций нужно использовать API драйвера БД \Bitrix\Main\DB\Connection, методы startTransaction(), commitTransaction() и rollbackTransaction().

    Если транзакции вложенные, то повторные старты транзакций создают именованные точки сохранения MySQL SAVEPOINT. Промежуточные коммиты ничего не коммитят. Последний закрывающий коммит коммитит все произведенные с начала первой транзакции изменения.

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

    Поэтому мы действуем в парадигме "вложенные роллбеки не поддерживаются" и даем возможность конечному сценарию решить, как поступить правильно. При наступлении события вложенного роллбека происходит частичный откат к соответствующей точке сохранения ROLLBACK TO SAVEPOINT ... и выбрасывается исключение \Bitrix\Main\DB\TransactionException.

    Далее возможны три сценария:

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

    Пример кода:

    $conn = \Bitrix\Main\Application::getConnection();
    
    $conn->query("truncate table test");
    
    try
    {
    	$conn->startTransaction();
    
    	$conn->query("insert into test values (1, 'one')");
    
    	// nested transaction
    	$conn->startTransaction();
    
    	$conn->query("insert into test values (2, 'two')");
    
    	if (true)
    	{
    		$conn->commitTransaction();
    	}
    	else
    	{
    		$conn->rollbackTransaction();
    	}
    	// end of nested transaction
    
    	$conn->commitTransaction();
    }
    catch(\Bitrix\Main\DB\TransactionException $e)
    {
    	$conn->rollbackTransaction();
    }

    Новое правило

    В итоге новое правило работы с транзакциями выглядит так:

    Рекомендуется использовать транзакции в API, чтобы делать атомарные изменения, обеспечивать целостность данных и изолировать изменения. Транзакции должны охватывать непосредственно модификацию данных; эти изменения должны рассматриваться как одна логическая операция. Вложенные транзакции поддерживаются. Тем не менее, вложенные роллбеки должны быть обработаны конечным сценарием и в целом не рекомендуются.

    Не стоит также забывать, что транзакции не должны быть сильно протяженными по времени, иначе можно легко попасть на deadlock транзакций вплоть до полной остановки приложения. Могут наблюдаться бесконечные откаты транзакций, если операция не может быть завершена до прекращения скрипта по тайм-ауту.

    ORM

    ORM (англ. Object-relational mapping, рус. Объектно-реляционное отображение) - технология программирования, которая связывает базы данных с концепциями объектно-ориентированных языков программирования, создавая «виртуальную объектную базу данных». (Wikipedia)

    Введение

    В старом ядре на каждую сущность программируется свой GetList, Update, Add, Delete.

    Недостатки такой идеологии:

    • разный набор параметров;
    • разный синтаксис полей фильтров;
    • события могут быть или не быть;
    • иногда разный код под разные БД (Add).

    В D7:

    Операции выборки и сохранения в БД - однотипные, с одинаковыми параметрами и фильтрами. По возможности, таблицы сущностей обслуживаются минимумом нового кода. Стандартные события добавления/изменения/удаления доступны автоматически.

    Для реализации этих целей введены понятия:

    1. Cущности (Bitrix\Main\Entity\Base);
    2. Поля сущностей (Bitrix\Main\Entity\Field и его наследники);
    3. Датаменеджер (Bitrix\Main\Entity\DataManager).

    Сущность описывает таблицу в БД, в том числе содержит поля сущностей. Датаменеджер производит операции выборки и изменения сущности. На практике же работа в основном ведется на уровне датаменеджера.

    Внимание! Перед началом разработки убедитесь что в выбранном вами модуле есть классы и методы ядра D7. Проверить это можно по наличию описания в документации по D7.

    Примечание: В главе приведены абстрактные примеры для понимания. Использование их в режиме copy\paste может не дать результатов в вашем проекте. Примеры нужно адаптировать под ваш конкретный код и структуру базы данных.


    Концепция, описание сущности

    При создании интернет-проектов на платформе Bitrix Framework доступен обширный функционал "из коробки", использовать который можно посредством вызовов API соответствующих модулей. При этом каждый модуль в концепции фреймворка является самостоятельной рабочей единицей, которая обеспечивает решение определенного круга задач.

    Как правило, API каждого модуля разрабатывается исходя из специфики задач, и нередко формат вызовов отличается от модуля к модулю. Чтобы свести эти различия к минимуму базовый функционал, который присутствует практически в каждом модуле, стандартизирован. Это CRUD-операции: Create, Read, Update, Delete (создание, чтение, обновление и удаление данных).

  • Концепция сущностей
  • Типизация полей
  • Primary & autoincrement & required
  • Маппинг имени колонки
  • Выражения ExpressionField
  • Пользовательские поля
  • Пример
  • Концепция сущностей

    Сущность - совокупность коллекции объектов с присущей им базовой (низкоуровневой) бизнес-логикой. Сущность обладает набором характеристик, значения которых подчиняются определенным правилам обработки.

    Например, сущность Пользователь - это множество пользователей с набором полей:

    • ID
    • Имя
    • Фамилия
    • Пароль
    • Логин
    • и т.д.

    При этом ID автоматически выдается базой данных, Имя и Фамилия ограничены длиной 50 символов, Логин должен состоять только из латинских букв, цифр и знака подчеркивания и так далее.

    Вместо программирования каждой такой сущности, мы бы хотели описывать ее в определенном формате. Такое описание обрабатывалось бы ядром системы, являлось для него своего рода конфигурацией:

    Book
    	ID int [autoincrement, primary]
    	ISBN str [match: /[0-9X-]+/]
    	TITLE str [max_length: 50]
    	PUBLISH_DATE date

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

    Типизация полей

    Для конфигурации сущностей не используются средства разметки (xml, yml и т.п.), вместо этого используется php. Такой вариант дает максимум возможностей развития и гибкости.

    Так выглядит определение типов данных из приведенного выше примера:

    namespace SomePartner\MyBooksCatalog;
    
    use Bitrix\Main\Entity;
    
    class BookTable extends Entity\DataManager
    {
    	public static function getTableName()
    	{
    		return 'my_book';
    	}
    	
    	public static function getMap()
    	{
    		return array(
    			new Entity\IntegerField('ID'),
    			new Entity\StringField('ISBN'),
    			new Entity\StringField('TITLE'),
    			new Entity\DateField('PUBLISH_DATE')
    		);
    	}
    }
    Внимание! Несмотря на то, что в примере под сущностью подразумевается Книга (Book), к имени класса дописан постфикс: BookTable. Это сделано специально - имя описательного класса сущности всегда должно завершаться словом Table. Основное имя Book в этом же пространстве имен считается зарезервированным, в будущем предполагается использовать основное имя (в данном случае - класс Book) для представления элементов сущности в виде объектов (в настоящий момент данные сущности представлены массивами, как и в старых методах getList).

    За описание структуры сущности отвечает метод getMap(), который возвращает массив экземплярами полей.

    Каждый тип поля представлен в виде класса-наследника Entity\ScalarField - эти поля работают с простыми скалярными значениями, которые сохраняются в базу данных "как есть". По умолчанию доступно 8 таких типов:

    • Целое число
    • Число
    • Строка
    • Текст
    • Дата
    • Дата/Время
    • Да/Нет
    • Значение из списка

    В рамках соблюдения стандартов кодирования рекомендуется называть поля в верхнем регистре. Имена должны быть уникальными в рамках одной сущности.

    Как правило, в конструкторе поля первым параметром передается имя поля, а вторым параметром - дополнительные настройки. Общие настройки будут рассмотрены далее в этой главе, но есть и специфическая настройка для BooleanField и EnumField:

    new Entity\BooleanField('NAME', array(
    	'values' => array('N', 'Y')
    ))
    
    new Entity\EnumField('NAME', array(
    	'values' => array('VALUE1', 'VALUE2', 'VALUE3')
    ))

    Для BooleanField, поскольку true и false не могут храниться в таком виде в БД, задается маппинг значений в виде массива, где первый элемент заменяет при хранении false, а второй true.

    Примечание: при описании сущности можно задать имя таблицы в методе getTableName, в данном примере это `my_book`. Если не определить этот метод, то имя таблицы будет сформировано автоматически из неймспейса и названия класса, для данной сущности это будет `b_somepartner_mybookscatalog_book`.

    Primary & autoincrement & required

    В большинстве случаев у сущности есть первичный ключ по одному полю. Он же, как правило, является автоинкрементным. Чтобы рассказать об этом сущности, необходимо воспользоваться параметрами в конструкторе поля:

    new Entity\IntegerField('ID', array(
    	'primary' => true
    ))

    Примечание: составной первичный ключ тоже возможен. Например, в отношениях двух сущностей составным ключом будут ID обеих сущностей. Подробнее узнать об этом и посмотреть пример можно в разделе N:M relations.

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

    Часто не указывается явно значение ID, а оно получается из базы данных уже после успешного добавления записи. В таком случае нужно сообщить об этом сущности:

    new Entity\IntegerField('ID', array(
    	'primary' => true,
    	'autocomplete' => true
    ))

    Флаг 'autocomplete', для сущности означает, что при добавлении новой записи не нужно требовать от разработчика установки значения для данного поля. По умолчанию, такое требование применяется только к полям из первичного ключа, но можно попросить систему требовать установку и любого другого поля:

    new Entity\StringField('ISBN', array(
    	'required' => true
    ))

    Теперь нельзя будет добавить новую книгу, не указав ее ISBN код.

    Маппинг имени колонки

    При описании сущности для уже имеющейся таблицы может возникнуть желание по-другому назвать колонку. Например, изначально в таблице `my_book` поле ISBN называлось как ISBNCODE, и старый код использует это название колонки в SQL запросах. Если в новом API необходимо оптимизировать название до более читаемого ISBN, то в этом поможет параметр 'column_name':

    new Entity\StringField('ISBN', array(
    	'required' => true,
    	'column_name' => 'ISBNCODE'
    ))

    Бывают и другие случаи, когда в одной физической колонке в таблице хранятся разные по смыслу значения. В таком случае можно создать несколько полей сущности, у которых будет одинаковый 'column_name'.

    Выражения ExpressionField

    Предусмотрено не только хранение данных как есть, но и их преобразование при выборке. Допустим, возникла потребность наравне с датой издания сразу же получать возраст книги в днях. Хранить это число в БД накладно: придется каждый день пересчитывать обновлять данные. Можно просто считать возраст на стороне базы данных:

    SELECT DATEDIFF(NOW(), PUBLISH_DATE) AS AGE_DAYS FROM my_book

    Для этого нужно описать в сущности виртуальное поле, значение которого базируется на SQL-выражении с другим полем или полями:

    new Entity\ExpressionField('AGE_DAYS',
    	'DATEDIFF(NOW(), %s)', array('PUBLISH_DATE')
    )

    Первым параметром, как и у остальных полей, задается имя. Вторым параметром нужно передать текст SQL выражения, но при этом другие поля сущности нужно заменить на плейсхолдеры согласно формату sprintf. Третьим параметром нужно передать массив с именами полей сущности в определенном порядке, который был задан в выражении.

    Примечание: в качестве плейсхолдеров рекомендуется использовать `%s` или `%1$s`, `%2$s` и так далее. Например, когда в выражении EXPR участвует несколько полей (FIELD_X + FIELD_Y) * FIELD_X, то выражение можно описать так: '(%s + %s) * %s', [FIELD_X, FIELD_Y, FIELD_X]; или так: '(%1$s + %2$s) * %1$s', [FIELD_X, FIELD_Y].

    Очень часто выражения могут применяться для агрегации данных (например, COUNT(*) или SUM(FIELD)), такие примеры будут рассмотрены в главе Выборка данных.

    Примечание: expression поля можно использовать только при выборке данных: выбирать, фильтровать, группировать и сортировать по ним. Поскольку физически таких колонок в таблице БД нет, то записать значение поля некуда: система сгенерирует исключение.

    Пользовательские поля

    Помимо полей ScalarField и ExpressionField, сущность может содержать Пользовательские поля. Они конфигурируются через Административный интерфейс и не требуют дополнительного описания на стороне сущности. Все, что требуется указать в сущности, это выбранный Объект пользовательского поля:

    class BookTable extends Entity\DataManager
    {
    	...
    	
    	public static function getUfId()
    	{
    		return 'MY_BOOK';
    	}
    	
    	...
    }

    Примечание: С версии 20.5.200 Главного модуля (main) добавлена поддержка SqlExpression значений для пользовательских полей в ORM.

    В дальнейшем именно этот идентификатор нужно указывать при прикреплении пользовательских полей к сущности:

    Таким образом, можно выбирать и обновлять значения пользовательских полей наравне со значениями штатных полей сущности.


    Пример

    По результатам данной главы получена следующая сущность:

    namespace SomePartner\MyBooksCatalog;
    
    use Bitrix\Main\Entity;
    
    class BookTable extends Entity\DataManager
    {
    	public static function getTableName()
    	{
    		return 'my_book';
    	}
    	
    	public static function getUfId()
    	{
    		return 'MY_BOOK';
    	}
    
    	public static function getMap()
    	{
    		return array(
    			new Entity\IntegerField('ID', array(
    				'primary' => true,
    				'autocomplete' => true
    			)),
    			new Entity\StringField('ISBN', array(
    				'required' => true,
    				'column_name' => 'ISBNCODE'
    			)),
    			new Entity\StringField('TITLE'),
    			new Entity\DateField('PUBLISH_DATE')
    		);
    	}
    }
    
    // код для создания таблицы в MySQL 
    // (получен путем вызова BookTable::getEntity()->compileDbTableStructureDump())
    CREATE TABLE `my_book` (
    	`ID` int NOT NULL AUTO_INCREMENT,
    	`ISBNCODE` varchar(255) NOT NULL,
    	`TITLE` varchar(255) NOT NULL,
    	`PUBLISH_DATE` date NOT NULL,
    	PRIMARY KEY(`ID`)
    );

    Таким образом можно описать в сущности обычные скалярные поля, выделить из них первичный ключ, указать автоинкрементные поля и какие поля должны быть обязательно заполнены. При расхождении имени колонки в таблице и желаемого имени в сущности будет возможность уладить этот момент.

    Внимание! Метод getMap используется только как получение первичной конфигурации сущности. Если вы хотите получить действительный список полей сущности, воспользуйтесь методом BookTable::getEntity()->getFields().

    Осталось только зафиксировать код сущности в проекте. Согласно общим правилам именования файлов в D7, код сущности нужно сохранить в файле: local/modules/somepartner.mybookscatalog/lib/book.php

    После чего система автоматически будет подключать файл при нахождении вызовов класса BookTable.

    Примечание: В примере выше использована рекомендуемая форма записи данных. Старая форма записи в виде массива:
    'ID' => array(
    'data_type' => 'integer',
    'primary' => true,
    'autocomplete' => true,
    ),
    оставлена для совместимости. При инициализации все равно создаются объекты классов \Bitrix\Main\Entity\*. Использовать можно оба варианта, правильней - через объекты.


    Операции с сущностями

    Для операций записи используются три метода уже описанного нами класса: BookTable::add, BookTable::update, BookTable::delete.

  • BookTable::add
  • BookTable::update
  • BookTable::delete
  • Валидаторы
  • События
  • Форматирование значений
  • Вычисляемые значения
  • Предупреждения об ошибках
  • BookTable::add

    Метод для добавления записи принимает на вход массив со значениями, где ключи - имена полей сущности:

    namespace SomePartner\MyBooksCatalog;
    
    use Bitrix\Main\Type;
    
    $result = BookTable::add(array(
    	'ISBN' => '978-0321127426',
    	'TITLE' => 'Patterns of Enterprise Application Architecture',
    	'PUBLISH_DATE' => new Type\Date('2002-11-16', 'Y-m-d')
    ));
    
    if ($result->isSuccess())
    {
    	$id = $result->getId();
    }

    Метод возвращает объект результата Entity\AddResult, и в примере выше показано, как проверить успешность добавления и получить ID добавленной записи.

    Примечание. Для значений полей типов DateField и DateTimeField, а также для пользовательских полей Дата и Дата со временем, необходимо использовать объекты классов Bitrix\Main\Type\Date и Bitrix\Main\Type\DateTime. По умолчанию в конструктор передается строковая дата в формате сайта, но можно и явно указать формат передаваемой даты.

    BookTable::update

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

    $result = BookTable::update($id, array(
    	'PUBLISH_DATE' => new Type\Date('2002-11-15', 'Y-m-d')
    ));

    В примере исправлена неправильно указанная при добавлении дата. В качестве результата возвращается объект Entity\UpdateResult, у которого так же есть проверочный метод isSuccess() (не было ли ошибок в запросе), и, дополнительно, можно узнать была ли запись фактически обновлена: getAffectedRowsCount().

    BookTable::delete

    Для удаления записи нужен только первичный ключ:

    $result = BookTable::delete($id);

    Результаты операции

    Если во время операции произошла одна или несколько ошибок, их текст можно получить из результата:

    $result = BookTable::update(...);
    
    if (!$result->isSuccess())
    {
    	$errors = $result->getErrorMessages();
    }

    Значения по умолчанию

    Бывает, что у большинства новых записей значение какого-то поля всегда одно и то же, или вычисляется автоматически. Пусть у каталога книг дата издания/публикации по умолчанию будет сегодняшним днем (логично добавлять книгу в каталог сразу в день ее выхода). Вернемся к описанию поля в сущности и используем параметр `default_value`:

    new Entity\DateField('PUBLISH_DATE', array(
    	'default_value' => new Type\Date
    ))

    Теперь при добавлении записи без явного указания даты издания ее значением будет текущий день:

    $result = BookTable::add(array(
    	'ISBN' => '978-0321127426',
    	'TITLE' => 'Some new book'
    ));

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

    new Entity\DateField('PUBLISH_DATE', array(
    	'default_value' => function () {
    		// figure out last friday date
    		$lastFriday = date('Y-m-d', strtotime('last friday'));
    		return new Type\Date($lastFriday, 'Y-m-d');
    	}
    ))

    Значением параметра `default_value` может быть любой `callable`: имя функции, массив из класса/объекта и названия метода, или анонимная функция.

    Валидаторы

    Перед записью новых данных в БД нужно обязательно проверять их на корректность. Для этого предусмотрены валидаторы:

    new Entity\StringField('ISBN', array(
    	'required' => true,
    	'column_name' => 'ISBNCODE',
    	'validation' => function() {
    		return array(
    			new Entity\Validator\RegExp('/[\d-]{13,}/')
    		);
    	}
    ))

    Теперь при добавлении и изменении записей ISBN будет проверен по шаблону [\d-]{13,} - код должен содержать только цифры и дефис, минимум 13 цифр.

    Валидация задается параметром 'validation' в конструкторе поля и представляет собой callback, который возвращает массив валидаторов.

    Примечание: Почему validation - callback, а не сразу массив валидаторов? Это своего рода отложенная загрузка: валидаторы будут инициализированы только тогда, когда действительно нужна будет валидация данных. В большинстве же случаев - при выборке данных из БД - валидация не нужна.

    В качестве валидатора принимается наследник Entity\Validator\Base или любой callable, который должен вернуть true, или текст ошибки, или объект Entity\FieldError (в случае, если вы хотите использовать собственный код ошибки).

    Точно известно, что в ISBN коде должно быть 13 цифр, эти цифры могут разделять несколько дефисов:

    978-0321127426
    978-1-449-31428-6
    9780201485677

    Чтобы удостовериться, что цифр там именно 13, напишем свой собственный валидатор:

    new Entity\StringField('ISBN', array(
    	'required' => true,
    	'column_name' => 'ISBNCODE',
    	'validation' => function() {
    		return array(
    			function ($value) {
    				$clean = str_replace('-', '', $value);
    				
    				if (preg_match('/^\d{13}$/', $clean))
    				{
    					return true;
    				}
    				else
    				{
    					return 'Код ISBN должен содержать 13 цифр.';
    				}
    			}
    		);
    	}
    ))

    Первым параметром в валидатор передается значение данного поля, но опционально доступно больше информации:

    new Entity\StringField('ISBN', array(
    	'required' => true,
    	'column_name' => 'ISBNCODE',
    	'validation' => function() {
    		return array(
    			function ($value, $primary, $row, $field) {
    				// value - значение поля
    				// primary - массив с первичным ключом, в данном случае [ID => 1]
    				// row - весь массив данных, переданный в ::add или ::update
    				// field - объект валидируемого поля - Entity\StringField('ISBN', ...)
    			}
    		);
    	}
    ))

    С таким набором данных можно произвести гораздо больший спектр сложных проверок.

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

    // описываем валидатор в поле сущности
    new Entity\StringField('ISBN', array(
    	'required' => true,
    	'column_name' => 'ISBNCODE',
    	'validation' => function() {
    		return array(
    			function ($value) {
    				$clean = str_replace('-', '', $value);
    
    				if (preg_match('/^\d{13}$/', $clean))
    				{
    					return true;
    				}
    				else
    				{
    					return 'Код ISBN должен содержать 13 цифр.';
    				}
    			},
    			function ($value, $primary, $row, $field) {
    				// проверяем последнюю цифру
    				// ...
    				// если цифра неправильная - возвращаем особую ошибку
    				return new Entity\FieldError(
    					$field, 'Контрольная цифра ISBN не сошлась', 'MY_ISBN_CHECKSUM'
    				);
    			}
    		);
    	}
    ))
    // выполняем операцию
    $result = BookTable::update(...);
    
    if (!$result->isSuccess())
    {
    	// смотрим, какие ошибки были выявлены
    	$errors = $result->getErrors();
    	
    	foreach ($errors as $error)
    	{
    		if ($error->getCode() == 'MY_ISBN_CHECKSUM')
    		{
    			// сработал наш валидатор
    		}
    	}
    }

    По умолчанию есть 2 стандартных кода ошибки: BX_INVALID_VALUE, если сработал валидатор, и BX_EMPTY_REQUIRED, если при добавлении записи не указано обязательное required поле.

    Валидаторы срабатывают как при добавлении новых записей, так и при обновлении существующих. Такое поведение исходит из общего назначения валидаторов - гарантировать корректные и целостные данные в БД. Для проверки данных только при добавлении или только при обновлении, а также для других манипуляций существует механизм событий.

    В типовых случаях рекомендуем вам использовать штатные валидаторы:

    • Entity\Validator\RegExp - проверка по регулярному выражению,
    • Entity\Validator\Length - проверка на минимальную/максимальную длину строки,
    • Entity\Validator\Range - проверка на минимальное/максимальное значение числа,
    • Entity\Validator\Unique - проверка на уникальность значения

    Описанные валидаторы не применимы к Пользовательским полям — проверка их значений конфигурируется в настройках поля через административный интерфейс.

    События

    В примере с валидаторами одной из проверок поля ISBN была проверка на наличие 13 цифр. Помимо цифр, в ISBN коде могут встречаться дефисы, но с технической точки зрения они не несут никакой ценности. Чтобы хранить в БД "чистые" данные - только 13 цифр, без дефисов - можно воспользоваться внутренним обработчиком события:

    class BookTable extends Entity\DataManager
    {
    	...
    	
    	public static function onBeforeAdd(Entity\Event $event)
    	{
    		$result = new Entity\EventResult;
    		$data = $event->getParameter("fields");
    
    		if (isset($data['ISBN']))
    		{
    			$cleanIsbn = str_replace('-', '', $data['ISBN']);
    			$result->modifyFields(array('ISBN' => $cleanIsbn));
    		}
    
    		return $result;
    	}
    }

    Метод onBeforeAdd, определенный в сущности, автоматически распознается системой как обработчик события "перед добавлением", и в нем можно изменить данные или провести дополнительные проверки. В приведенном примере мы изменили поле ISBN посредством метода `modifyFields`.

    // до преобразования
    978-0321127426
    978-1-449-31428-6
    9780201485677
    
    // после преобразования
    9780321127426
    9781449314286
    9780201485677

    После такого преобразования можно вновь вернуться к лаконичному валидатору RegExp вместо анонимной функции (ведь мы уже знаем, что допустимых дефисов в значении не будет, должны остаться только цифры):

    'validation' => function() {
    	return array(
    		//function ($value) {
    		//	$clean = str_replace('-', '', $value);
    		//
    		//	if (preg_match('/^\d{13}$/', $clean))
    		//	{
    		//		return true;
    		//	}
    		//	else
    		//	{
    		//		return 'Код ISBN должен содержать 13 цифр.';
    		//	}
    		//},
    		new Entity\Validator\RegExp('/\d{13}/'),
    		...
    	);
    }

    Помимо изменения данных, в обработчике события можно удалить данные или вовсе прервать выполнение операции. Например, необходимо запретить обновление ISBN кода для уже существующих в каталоге книг. Сделать это можно в событии onBeforeUpdate двумя способами:

    public static function onBeforeUpdate(Entity\Event $event)
    {
    	$result = new Entity\EventResult;
    	$data = $event->getParameter("fields");
    
    	if (isset($data['ISBN']))
    	{
    		$result->unsetFields(array('ISBN'));
    	}
    
    	return $result;
    }

    В таком варианте ISBN будет "тихо" удален из набора данных, будто его и не передавали. Второй способ запретить его обновлять - сгенерировать ошибку:

    public static function onBeforeUpdate(Entity\Event $event)
    {
    	$result = new Entity\EventResult;
    	$data = $event->getParameter("fields");
    
    	if (isset($data['ISBN']))
    	{
    		$result->addError(new Entity\FieldError(
    			$event->getEntity()->getField('ISBN'),
    			'Запрещено менять ISBN код у существующих книг'
    		));
    	}
    
    	return $result;
    }

    В случае возврата ошибки мы сформировали объект Entity\FieldError для того, чтобы впоследствии при обработке ошибок знать, на каком именно поле сработала проверка. Если ошибка относится к нескольким полям или целиком ко всей записи, то более уместно будет воспользоваться объектом Entity\EntityError:

    public static function onBeforeUpdate(Entity\Event $event)
    {
    	$result = new Entity\EventResult;
    	$data = $event->getParameter("fields");
    
    	if (...) // комплексная проверка данных
    	{
    		$result->addError(new Entity\EntityError(
    			'Невозможно обновить запись'
    		));
    	}
    
    	return $result;
    }

    В примерах использовались два события: onBeforeAdd и onBeforeUpdate, всего же таких событий девять:

    • OnBeforeAdd (параметры: fields)
    • OnAdd (параметры: fields)
    • OnAfterAdd (параметры: fields, primary)

    • OnBeforeUpdate (параметры: primary, fields)
    • OnUpdate (параметры: primary, fields)
    • OnAfterUpdate (параметры: primary, fields)

    • OnBeforeDelete (параметры: primary)
    • OnDelete (параметры: primary)
    • OnAfterDelete (параметры: primary)

    Порядок вызова событий и допустимые действия в обработчиках каждого из них:

    Конечно же, обрабатывать эти события можно не только в самой сущности в одноименных методах. Чтобы подписаться на событие в произвольном месте выполнения скрипта, нужно вызвать менеджер событий:

    $em = \Bitrix\Main\ORM\EventManager::getInstance();
            
    $em->addEventHandler(
    	BookTable::class, // класс сущности
        	DataManager::EVENT_ON_BEFORE_ADD, // код события
    		function () { // ваш callback
    			var_dump('handle entity event');
    		}
    );

    Форматирование значений

    Иногда может возникнуть необходимость хранить данные в одном формате, а работать с ними в программе уже в другом. Самый распространенный пример: работа с массивом и его сериализация перед сохранением в БД. На этот случай предусмотрены параметры поля 'save_data_modification' и 'fetch_data_modification'. Определяются они аналогично валидаторам, через callback.

    На примере каталога книг опишем текстовое поле EDITIONS_ISBN: оно будет хранить коды ISBN других изданий книги, если таковые имеются:

    new Entity\TextField('EDITIONS_ISBN', array(
    	'save_data_modification' => function () {
    		return array(
    			function ($value) {
    				return serialize($value);
    			}
    		);
    	},
    	'fetch_data_modification' => function () {
    		return array(
    			function ($value) {
    				return unserialize($value);
    			}
    		);
    	}
    ))

    В параметре save_data_modification мы указали сериализацию значения перед сохранением в БД, а в параметре fetch_data_modification рассериализацию при выборке из БД. Теперь при написании бизнес-логики вы можете просто работать с массивом, не отвлекаясь на вопросы конвертации.

    Внимание! Прежде чем создать у себя сериализованное поле, подумайте не помешает ли сериализация при фильтрации или связывании таблиц. Искать по одиночному значению в WHERE среди сериализованных строк крайне неэффективно. Возможно, вам больше подойдет нормализованная схема хранения данных.

    Поскольку сериализация - это наиболее типичный пример для конвертации значений, она вынесена в отдельный параметр serialized:

    new Entity\TextField('EDITIONS_ISBN', array(
    	'serialized' => true
    ))

    Но вы по-прежнему можете описать свои callable для других вариантов модификации данных.

    Вычисляемые значения

    Очень часто разработчики сталкиваются с реализацией счетчиков, где для целостности данных предпочтительно рассчитывать новое значение на стороне БД, вместо выборки старого значения и пересчете его на стороне приложения. Другими словами, нужно выполнять запросы вида:

    UPDATE my_book SET READERS_COUNT = READERS_COUNT + 1 WHERE ID = 1

    Если описать числовое поле READERS_COUNT в сущности, то инкремент счетчика можно будет запустить следующим образом:

    BookTable::update($id, array(
    	'READERS_COUNT' => new DB\SqlExpression('?# + 1', 'READERS_COUNT')
    ));

    Плейсхолдер ?# означает, что следующим аргументом в конструкторе идет идентификатор БД - имя базы данных, таблицы или колонки, и это значение будет экранировано соответствующим образом. Для всех изменяемых параметров рекомендуется обязательно использовать плейсхолдеры - такой подход поможет избежать проблем с SQL инъекциями.

    Например, если инкрементируемое число читателей переменно, то лучше описать выражение так:

    // правильно
    BookTable::update($id, array(
    	'READERS_COUNT' => new DB\SqlExpression('?# + ?i', 'READERS_COUNT', $readersCount)
    ));
    
    // неправильно
    BookTable::update($id, array(
    	'READERS_COUNT' => new DB\SqlExpression('?# + '.$readersCount, 'READERS_COUNT')
    ));

    Список доступных на данный момент плейсхолдеров:

    • ? или ?s - значение экранируется и заключается в кавычки '
    • ?# - значение экранируется как идентификатор
    • ?i - значение приводится к integer
    • ?f - значение приводится к float

    Предупреждения об ошибках

    В предыдущих примерах есть нюанс: запрос на обновление данных вызывается без проверки результата:

    // вызов без проверки успешности выполнения запроса
    BookTable::update(...);
    
    // с проверкой
    $result = BookTable::update(...);
    if (!$result->isSuccess())
    {
    	// обработка ошибки
    }

    Несомненно, второй вариант более предпочтителен с точки зрения контроля происходящего. Но если код выполняется только в режиме агента, нам некому и незачем показывать список возникших в процессе валидации ошибок. В таком случае, если запрос не прошел из-за "проваленной" валидации, и не была вызвана проверка isSuccess(), система сгенерирует E_USER_WARNING со списком ошибок, который можно будет увидеть в логе сайта (если соответствующим образом настроить .settings.php).

    По результатам данной главы произошли некоторые изменения в описании сущности, теперь оно выглядит так:

    namespace SomePartner\MyBooksCatalog;
    
    use Bitrix\Main\Entity;
    use Bitrix\Main\Type;
    
    class BookTable extends Entity\DataManager
    {
    	public static function getTableName()
    	{
    		return 'my_book';
    	}
    	
    	public static function getUfId()
    	{
    		return 'MY_BOOK';
    	}
    
    	public static function getMap()
    	{
    		return array(
    			new Entity\IntegerField('ID', array(
    				'primary' => true,
    				'autocomplete' => true
    			)),
    			new Entity\StringField('ISBN', array(
    				'required' => true,
    				'column_name' => 'ISBNCODE',
    				'validation' => function() {
    					return array(
    						new Entity\Validator\RegExp('/\d{13}/'),
    						function ($value, $primary, $row, $field) {
    							// проверяем последнюю цифру
    							// ...
    							// если цифра неправильная - возвращаем особую ошибку
    							return new Entity\FieldError(
    								$field, 'Контрольная цифра ISBN не сошлась', 'MY_ISBN_CHECKSUM'
    							);
    						}
    					);
    				}
    			)),
    			new Entity\StringField('TITLE'),
    			new Entity\DateField('PUBLISH_DATE', array(
    				'default_value' => function () {
    					// figure out last friday date
    					$lastFriday = date('Y-m-d', strtotime('last friday'));
    					return new Type\Date($lastFriday, 'Y-m-d');
    				}
    			)),
    			new Entity\TextField('EDITIONS_ISBN', array(
    				'serialized' => true
    			)),
    			new Entity\IntegerField('READERS_COUNT')
    		);
    	}
    
    	public static function onBeforeAdd(Entity\Event $event)
    	{
    		$result = new Entity\EventResult;
    		$data = $event->getParameter("fields");
    
    		if (isset($data['ISBN']))
    		{
    			$cleanIsbn = str_replace('-', '', $data['ISBN']);
    			$result->modifyFields(array('ISBN' => $cleanIsbn));
    		}
    
    		return $result;
    	}
    }

    Скопировав этот код, вы можете поэкспериментировать со всеми описанными выше возможностями.


    Объекты

    Для начала использования объектов достаточно иметь лишь описанную сущность. Просто замените fetch на fetchObject в своем коде:

    $book = \Bitrix\Main\Test\Typography\BookTable::getByPrimary(1)
    	->fetchObject();
    

    Теперь $book - это полноценный объект сущности Book, наделенный множеством методов по манипуляции с собственными данными и отношениями с другими сущностями.



    Класс объекта

    Все объекты сущностей являются наследниками класса Bitrix\Main\ORM\Objectify\EntityObject, при этом у каждой сущности – свой собственный класс для объектов. По умолчанию, такой класс создается автоматически, на лету. Если вы уже сгенерировали аннотации классов ORM, то IDE раскроет этот момент:

    Как видно, класс объекта EO_Book находится в том же пространстве имен, что и класс Table, так же назван, но вместо суффикса Table имеет префикс EO_ (аббревиатура EntityObject). Такой префикс добавлен из соображений обратной совместимости: в существующих проектах уже может быть класс Book или конструкция вида

    use Some\Another\Book;
    что приведет к конфликту повторного использования слова Book. Мы посчитали префикс EO_ достаточно уникальным, тем более в обычном случае использование знака _ противоречит стандартам именования кода, и конфликтов с вручную описанными классами быть не должно.

    При этом мы заложили возможность указать свое собственное, красивое имя класса, и даже разместить его в другом пространстве имен при необходимости:

    //Файл bitrix/modules/main/lib/test/typography/book.php
    
    namespace Bitrix\Main\Test\Typography;
    
    class Book extends EO_Book
    {
    }
    

    Ключевой момент - наследование от базового виртуального класса объекта EO_Book.

    Сущность нужно уведомить о новом классе следующим образом:

    //Файл bitrix/modules/main/lib/test/typography/booktable.php
    
    namespace Bitrix\Main\Test\Typography;
    
    class BookTable extends Bitrix\Main\ORM\Data\DataManager
    {
    	public static function getObjectClass()
    	{
    		return Book::class;
    	}
    	//...
    }

    Теперь метод fetchObject будет возвращать объекты класса Bitrix\Main\Test\Typography\Book. А после перегенерации аннотаций новый класс начнет показывать IDE:

    Определять собственные классы объектов стоит лишь тогда, когда вам требуется явно использовать имя класса или расширить класс своим функционалом. Не рекомендуется использовать имена стандартных классов в своем коде, например instanceof EO_Book, new EO_Book или EO_Book::class. В таких случаях рекомендуется описать свой класс с «красивым» именем, соответствующий стандартам именования, либо использовать обезличенные методы BookTable::getObjectClass(), BookTable::createObject(), BookTable::wakeUpObject() и т.п.

    В своем классе можно не только добавлять свой функционал, но и переопределять стандартные именованные методы. Не следует лишь описывать в классе свойства с именами primary, entity и dataClass, поскольку эти имена уже используются базовым классом.



    Именованные методы

    Большая часть методов сделаны именованными. Это значит, что для каждого поля доступен набор персональных методов:

    $book->getTitle();
    $book->setTitle($value);
    $book->remindActualTitle();
    $book->resetTitle();
    // и т.д.
    

    С полными списком методов вы ознакомитесь ниже, в других уроках.

    Такой подход был выбран по нескольким причинам:

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

    При этом для всех методов существует альтернатива в виде универсальных методов, принимающих имя поля в качестве одного из аргументов:

    $fieldName = 'TITLE';
    
    $book->get($fieldName);
    $book->set($fieldName, $value);
    $book->remindActual($fieldName);
    $book->reset($fieldName);
    // и т.д.
    

    Такой подход удобен, когда имена полей хранятся в памяти, и вы работаете с ними обезличенно.

    Если вы описали свой класс для объекта и переопределили какой-либо именованный метод, то при использовании универсального метода будет вызван ваш явно заданный именованный метод:

    namespace Bitrix\Main\Test\Typography;
    
    class Book extends EO_Book
    {
    	public function getTitle()
    	{
    		return 'custom title';
    	}
    }
    
    $book = \Bitrix\Main\Test\Typography\BookTable::getByPrimary(1)
    	->fetchObject();
    
    echo $book->getTitle(); // выведет 'custom title'
    echo $book->get('TITLE'); // тоже выведет 'custom title'
    

    Исключение составляет метод fill, в нем именованный метод вызван не будет. Архитектурно метод рассчитан на оптимизацию работы с базой данных, и в случае заполнения нескольких полей одновременно вызовы именованных методов по отдельности создадут дополнительную нагрузку.

    Внутренняя реализация именованных методов основана на magic-методе __call. Альтернативой могла быть кодогенерация - компиляция классов со всеми методами и их последующее кеширование. Мы сделали выбор в пользу magic по следующим причинам:

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

    Минусом magic-методов можно назвать увеличенный расход ресурсов процессора, но в особых случаях это можно решить явным определением часто используемых методов, как это сделано с методом getId в базовом классе. В то же время, такой ресурс проще всего поддается горизонтальному масштабированию, позволяя добавлять новые машины вместо бесконечного "апгрейда" одной существующей.

    В отсутствие magic-методов и кодогенерации было бы невозможно автоматически охватить все поля из класса Table, пришлось бы описывать нужные поля и методы вручную.



    Приведение типов

    В объектах действует строгое приведение значений к типу поля. Это значит, что числа всегда будут числами, а строки - строками:

    $book = \Bitrix\Main\Test\Typography\BookTable::getByPrimary(1)
    	->fetchObject();
    
    var_dump($book->getId());
    // выведет int 1
    
    var_dump($book->getTitle());
    // выведет string 'Title 1' (length=7)
    

    Особое внимание стоит уделить типу BooleanField: в качестве значения ожидается true или false, несмотря на то, что фактически в базе могут храниться другие значения:

    //(new BooleanField('IS_ARCHIVED'))
    //	->configureValues('N', 'Y'),
    
    $book = \Bitrix\Main\Test\Typography\BookTable::getByPrimary(1)
    	->fetchObject();
    
    var_dump($book->getIsArchived());
    // выведет boolean true
    
    // при установке значений тоже ожидается boolean
    $book->setIsArchived(false);
    


    Чтение (get, require, remindActual, primary, collectValues, runtime)

    • get

      Чтение данных реализовано несколькими методами. Самый простой из них возвращает значение поля либо null в случае его отсутствия (например, если поле не было указано в select при выборке):

      $book = \Bitrix\Main\Test\Typography\BookTable::getByPrimary(1)
      	->fetchObject();
      
      $title = $book->getTitle();
      
    • require

      Если вы уверены, что поле должно быть заполнено значением, и без этого значения продолжать выполнение сценария не имеет смысла, можете требовать это значение методом require:

      $book = \Bitrix\Main\Test\Typography\BookTable::getByPrimary(1)
      	->fetchObject();
      
      $title = $book->requireTitle();
      

      В данном случае результат requireTitle() не будет отличаться от вышеприведенного getTitle(). А следующий пример уже закончится исключением, поскольку поле не будет заполнено значением:

      $book = \Bitrix\Main\Test\Typography\BookTable::getByPrimary(1, ['select' => ['ID', 'PUBLISHER_ID', 'ISBN']])
      	->fetchObject();
      
      $title = $book->requireTitle();
      // SystemException: "TITLE value is required for further operations"
      
    • remindActual

      Еще один "геттер" remindActual пригодится вам при переустановке значения, чтобы отличить оригинальное значение от установленного в процессе сеанса и еще несохраненного в базу данных:

      $book = \Bitrix\Main\Test\Typography\BookTable::getByPrimary(1)
      	->fetchObject();
      
      echo $book->getTitle();
      // выведет "Title 1"
      
      $book->setTitle("New title");
      
      echo $book->getTitle();
      // выведет "New title"
      
      echo $book->remindActualTitle();
      // выведет "Title 1"
      

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

      $fieldName = 'TITLE';
      
      $title = $book->get($fieldName);
      $title = $book->require($fieldName);
      $title = $book->remindActual($fieldName);
      
    • primary

      Системный "геттер" primary реализован в виде виртуального read-only свойства, чтобы не использовать метод getPrimary(), резервируя тем самым имя поля PRIMARY с соответствующим именованным геттером. Свойство возвращает значения первичного ключа в формате массива независимо от того, составной ли первичный ключ или одиночный:

      $book = \Bitrix\Main\Test\Typography\BookTable::getByPrimary(1)
      	->fetchObject();
      
      $primary = $book->primary;
      // вернет ['ID' => 1]
      
      $id = $book->getId();
      // вернет 1
      
    • collectValues

      Метод collectValues используется для получения всех значений объекта в виде массива.

      $book = \Bitrix\Main\Test\Typography\BookTable::getByPrimary(1)
      	->fetchObject();
      
      $values = $book->collectValues();
      

      В данном примере вернутся все имеющиеся значения. Если для некоторых полей значения были переустановлены через "сеттер", но еще не сохранены, то вернутся именно эти значения. Для неизмененных полей будут взяты актуальные значения.

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

      $book = \Bitrix\Main\Test\Typography\BookTable::getByPrimary(1)
      	->fetchObject();
      
      $values = $book->collectValues(\Bitrix\Main\ORM\Objectify\Values::ACTUAL);
      // вернет только актуальные значения, без учета еще не сохраненных
      
      $values = $book->collectValues(\Bitrix\Main\ORM\Objectify\Values::CURRENT);
      // вернет только текущие значения, еще не сохраненные в базу данных
      
      $values = $book->collectValues(\Bitrix\Main\ORM\Objectify\Values::ALL);
      // равнозначно вызову collectValues() без параметров - сначала CURRENT, затем ACTUAL
      

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

      $book = \Bitrix\Main\Test\Typography\BookTable::getByPrimary(1)
      	->fetchObject();
      
      $values = $book->collectValues(
      	\Bitrix\Main\ORM\Objectify\Values::CURRENT,
      	\Bitrix\Main\ORM\Fields\FieldTypeMask::SCALAR
      );
      // вернутся только измененные значения скалярных полей
      
      $values = $book->collectValues(
      	\Bitrix\Main\ORM\Objectify\Values::ALL,
      	\Bitrix\Main\ORM\Fields\FieldTypeMask::ALL & ~\Bitrix\Main\ORM\Fields\FieldTypeMask::USERTYPE
      );
      // вернутся значения всех полей, кроме пользовательских
      
    • runtime

      Для runtime полей, создаваемых в рамках отдельных запросов, предусмотрен только универсальный "геттер" get:

      $author = \Bitrix\Main\Test\Typography\AuthorTable::query()
      	->registerRuntimeField(
      		new \Bitrix\Main\Entity\ExpressionField(
      			'FULL_NAME', 'CONCAT(%s, " ", %s)', ['NAME', 'LAST_NAME']
      		)
      	)
      	->addSelect('ID')
      	->addSelect('FULL_NAME')
      	->where('ID', 17)
      	->fetchObject();
      
      echo $author->get('FULL_NAME');
      // выведет 'Name 17 Last name 17'
      

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



    Запись (set, reset, unset)

    • set

      Установка значения происходит схожим образом:

      $book = \Bitrix\Main\Test\Typography\BookTable::getByPrimary(1)
      	->fetchObject();
      
      $book->setTitle("New title");
      

      При этом объект запоминает свои исходные значения. С этого момента доступ к текущему значению осуществляется через основной "геттер" get, а к изначальному, актуальному для базы данных значению, через вспомогательный "геттер" remindActual:

      $book->getTitle(); // текущее значение
      $book->remindActualTitle(); // актуальное для базы данных значение
      

      Значения первичного ключа primary можно устанавливать только в новых объектах, в существующих изменить его будет нельзя. При такой необходимости придется создать новый объект и удалить старый. Также не сработает установка полей Bitrix\Main\ORM\Fields\ExpressionField, поскольку их значения рассчитываются автоматически и не подлежат изменению извне.

      При установке значения, не отличающегося от актуального, значение фактически не будет изменено и не попадет в SQL-запрос при сохранении объекта.
    • reset

      Чтобы отменить установку нового значения и вернуть исходное, можно воспользоваться вспомогательным "сеттером" reset:

      $book = \Bitrix\Main\Test\Typography\BookTable::getByPrimary(1)
      	->fetchObject();
      
      echo $book->getTitle();
      // выведет "Title 1"
      
      $book->setTitle("New title");
      
      echo $book->getTitle();
      // выведет "New title"
      
      $book->resetTitle();
      
      echo $book->getTitle();
      // выведет "Title 1"
      
    • unset

      Еще один вспомогательный "сеттер" unset удалит значение объекта так, будто бы оно никогда не выбиралось из базы данных и не было установлено:

      $book = \Bitrix\Main\Test\Typography\BookTable::getByPrimary(1)
      	->fetchObject();
      
      echo $book->getTitle();
      // выведет "Title 1"
      
      $book->unsetTitle();
      
      echo $book->getTitle();
      // null
      

      Для "сеттеров" тоже есть универсальные варианты вызова с именем поля в качестве аргумента:

      $fieldName = 'TITLE';
      
      $book->set($fieldName, "New title");
      $book->reset($fieldName);
      $book->unset($fieldName);
      
      Все операции по изменению значения приводят к изменениям только во время сеанса. Чтобы зафиксировать изменения в базе данных, объект нужно сохранить - об этом читайте в уроке Создание и редактирование (save, new).


    Проверки (isFilled, isChanged, has)

    • isFilled

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

      use \Bitrix\Main\Test\Typography\Book;
      
      // актуальными считаются значения из методов fetch* и wakeUp
      // в примере при инициализации объекта передается только первичный ключ
      $book = Book::wakeUp(1);
      
      var_dump($book->isTitleFilled());
      // false
      
      $book->fillTitle();
      
      var_dump($book->isTitleFilled());
      // true
    • isChanged

      Метод isChanged отвечает на вопрос, было ли установлено новое значение в течение сеанса:

      use \Bitrix\Main\Test\Typography\Book;
      
      // объект может иметь исходное значение, а может и не иметь
      // это не повлияет на дальнешее поведение
      $book = Book::wakeUp(['ID' => 1, 'TITLE' => 'Title 1']);
      
      var_dump($book->isTitleChanged());
      // false
      
      $book->setTitle('New title 1');
      
      var_dump($book->isTitleChanged());
      // true

      Такое поведение справедливо и для новых объектов, пока их значения не сохранены в базу данных.

    • has

      Метод has проверяет, есть ли в объекте хоть какое-то значение поля - актуальное из БД или установленное в течение сеанса. По сути, это сокращение от isFilled() || isChanged():

      use \Bitrix\Main\Test\Typography\Book;
      
      $book = Book::wakeUp(['ID' => 1, 'TITLE' => 'Title 1']);
      $book->setIsArchived(true);
      
      var_dump($book->hasTitle());
      // true
      
      var_dump($book->hasIsArchived());
      // true
      
      var_dump($book->hasIsbn());
      // false


    Состояние объекта

    Объект может принимать 3 состояния:

    • новый, данные которого еще ни разу не сохранялись в БД;
    • актуальный, данные которого совпадают с хранящимися в БД;
    • измененный, данные которого отличаются от хранящихся в БД.

    Проверить состояние объекта можно с помощью публичного read-only свойства state и констант класса \Bitrix\Main\ORM\Objectify\State:

    use \Bitrix\Main\Test\Typography\Book;
    use \Bitrix\Main\ORM\Objectify\State;
    
    $book = new Book;
    $book->setTitle('New title');
    
    var_dump($book->state === State::RAW);
    
    $book->save();
    
    var_dump($book->state === State::ACTUAL);
    
    $book->setTitle('Another one title');
    
    var_dump($book->state === State::CHANGED);
    
    $book->delete();
    
    var_dump($book->state === State::RAW);
    
    // true


    Создание и редактирование (save, new)

    Для фиксации изменений объекта в базе данных используется метод save:

    $book = \Bitrix\Main\Test\Typography\BookTable::getByPrimary(1)
    	->fetchObject();
    
    $book->setTitle("New title");
    
    $book->save();
    

    Примечание: если вы скопируете этот пример и попробуете выполнить его с тестовой сущностью из пространства имен Bitrix\Main\Test\Typography, то в силу специфики тестовых данных получите SQL- ошибку. Но при этом вы увидите, что часть запроса с данными построена корректно.

    С момента сохранения все текущие значения объекта преобразуются в актуальные:

    $book = \Bitrix\Main\Test\Typography\BookTable::getByPrimary(1)
    	->fetchObject();
    
    echo $book->remindActualTitle();
    // выведет "Title 1"
    
    $book->setTitle("New title");
    
    echo $book->remindActualTitle();
    // выведет "Title 1"
    
    $book->save();
    
    echo $book->remindActualTitle();
    // выведет "New title"
    

    Что касается новых объектов, есть два пути их создания. Наиболее читаемый способ — через прямое инстанциирование:

    $newBook = new \Bitrix\Main\Test\Typography\Book;
    $newBook->setTitle('New title');
    $newBook->save();
    
    $newAuthor = new \Bitrix\Main\Test\Typography\EO_Author;
    $newAuthor->setName('Some name');
    $newAuthor->save();
    

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

    Более универсальный и обезличенный метод создавать новые объекты — через фабрику сущности:

    $newBook = \Bitrix\Main\Test\Typography\BookTable::createObject();
    $newBook->setTitle('New title');
    $newBook->save();
    

    По умолчанию, в новом объекте устанавливаются все значения по умолчанию, описанные в "маппинге" getMap. Абсолютно чистый объект можно получить, передав соответствующий аргумент в конструктор:

    $newBook = new \Bitrix\Main\Test\Typography\Book(false);
    $newBook = \Bitrix\Main\Test\Typography\BookTable::createObject(false);
    

    Состояние значений меняется аналогично, как при редактировании. До сохранения объекта значения считаются текущими, после сохранения в базе данных переходят в статус actual.



    Удаление (delete)

    Для удаления используется метод delete():

    // удаление записи
    $book = \Bitrix\Main\Test\Typography\BookTable::getByPrimary(1)
    	->fetchObject();
    
    $book->delete();
    
    // удаление по primary ключу
    $book = \Bitrix\Main\Test\Typography\Book::wakeUp(1);
    
    $book->delete();

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

    Как и в случае удаления через одноименный метод Table-класса, в этом случае сработают все нужные События. Поэтому дополнительные действия можно описать в обработчике события onDelete.



    Восстановление (wakeUp)

    Если у вас уже есть данные записи, для инициализации объекта необязательно выбирать их повторно из базы данных. Объект можно восстановить, имея как минимум значения первичного ключа:

    $book = \Bitrix\Main\Test\Typography\Book::wakeUp(1);

    Указывать можно не только первичный ключ, но и частичный или полный набор данных:

    $book = \Bitrix\Main\Test\Typography\Book::wakeUp(['ID' => 1, 'TITLE' => 'Title 1', 'PUBLISHER_ID' => 253]);

    Аналогично созданию объектов, метод актуален и для EO_ классов, и для вызова непосредственно из entity:

    // свой класс
    $book = \Bitrix\Main\Test\Typography\Book::wakeUp(
    	['ID' => 1, 'TITLE' => 'Title 1']
    );
    
    // системный класс
    $book = \Bitrix\Main\Test\Typography\EO_Book::wakeUp(
    	['ID' => 1, 'TITLE' => 'Title 1']
    );
    
    // через фабрику entity
    $book = \Bitrix\Main\Test\Typography\BookTable::wakeUpObject(
    	['ID' => 1, 'TITLE' => 'Title 1']
    );
    

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

    $book = \Bitrix\Main\Test\Typography\Book::wakeUp([
    	'ID' => 2,
    	'TITLE' => 'Title 2',
    	'PUBLISHER' => ['ID' => 253, 'TITLE' => 'Publisher Title 253'],
    	'AUTHORS' => [
    		['ID' => 17, 'NAME' => 'Name 17'],
    		['ID' => 18, 'NAME' => 'Name 18']
    	]
    ]);
    


    Заполнение (fill)

    Когда в объекте заполнены не все поля, и вам нужно дозаполнить их, не следует применять следующий подход:

    // изначально у нас есть только ID и NAME
    $author = \Bitrix\Main\Test\Typography\EO_Author::wakeUp(
    	['ID' => 17, 'NAME' => 'Name 17']
    );
    
    // мы хотим дозаписать LAST_NAME, довыбрав его из базы данных
    $row = \Bitrix\Main\Test\Typography\AuthorTable::getByPrimary($author->getId(),
    	['select' => ['LAST_NAME']]
    )->fetch();
    
    // добавление значения в объект
    $author->setLastName($row['LAST_NAME']);
    

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

    Правильно будет воспользоваться именованным методом объекта fill:

    // изначально у нас есть только ID и NAME
    $author = \Bitrix\Main\Test\Typography\EO_Author::wakeUp(
    	['ID' => 17, 'NAME' => 'Name 17']
    );
    
    // добавляем LAST_NAME из базы данных
    $author->fillLastName();
    

    Кроме именованных методов, есть и универсальный. И он предоставляет значительно больше возможностей, чем другие универсальные методы:

    $author = \Bitrix\Main\Test\Typography\EO_Author::wakeUp(17);
    
    // заполнение нескольких полей
    $author->fill(['NAME', 'LAST_NAME']);
    
    // заполнение всех незаполненных на данный момент полей
    $author->fill();
    
    // заполнение полей по маске, например все незаполненные скалярные поля
    $author->fill(\Bitrix\Main\ORM\Fields\FieldTypeMask::SCALAR);
    
    // незаполненные скалярные и пользовательские поля
    $author->fill(
    	\Bitrix\Main\ORM\Fields\FieldTypeMask::SCALAR
    	| \Bitrix\Main\ORM\Fields\FieldTypeMask::USERTYPE
    );
    
    /*
     * Маски бывают следующие:
     *
     * SCALAR - скалярные поля (ORM\ScalarField)
     * EXPRESSION - выражения (ORM\ExpressionField)
     * USERTYPE - пользовательские поля
     * REFERENCE - отношения 1:1 и N:1 (ORM\Fields\Relations\Reference)
     * ONE_TO_MANY - отношения 1:N (ORM\Fields\Relations\OneToMany)
     * MANY_TO_MANY - отношения N:M (ORM\Fields\Relations\ManyToMany)
     *
     * FLAT - скалярные поля и выражения
     * RELATION - все отношения
     *
     * ALL - абсолютно все доступные поля
     */
    

    Если вам нужно дозаполнить несколько объектов, то категорически не рекомендуется выполнять эту команду в цикле - это приведет к большому количеству запросов к базе данных. Для работы с несколькими объектами одного типа одновременно следует использовать аналогичный метод Коллекции.



    Отношения (addTo, removeFrom, removeAll)

    Подробное описание отношений можно найти в соседней главе Отношения. Здесь же приведена спецификация управляющих отношениями методов.

    Для полей отношений работают уже описанные выше методы get, require, fill, reset, unset.

    Важно! Несмотря на то, что в качестве значения отношений используется объект Коллекции, изменять связи можно только через методы addTo, removeFrom, removeAll объектов-партнеров. Изменение коллекции напрямую (add, remove) не приведет к желаемому результату.

    • addTo

      Метод addTo добавляет новую связь между объектами:

      // инициализация издателя
      $publisher = \Bitrix\Main\Test\Typography\PublisherTable::getByPrimary(253)
      	->fetchObject();
      
      // инициализация книги
      $book = \Bitrix\Main\Test\Typography\BookTable::getByPrimary(2)
      	->fetchObject();
      
      // добавление книги в коллекцию отношения
      $publisher->addToBooks($book);
      
      // сохранение
      $publisher->save();
      

      Вызов метода связывает объекты лишь в памяти, после него необходимо зафиксировать изменения методом save.

    • removeFrom

      Удаление связей отношений removeFrom работает похожим образом:

      // инициализация издателя
      $publisher = \Bitrix\Main\Test\Typography\PublisherTable::getByPrimary(253)
      	->fetchObject();
      
      // инициализация книги
      $book = \Bitrix\Main\Test\Typography\BookTable::getByPrimary(2)
      	->fetchObject();
      
      // удаление одной конкретной книги издателя
      $publisher->removeFromBooks($book); 
      
      // сохранение
      $publisher->save();
      
    • removeAll Удаление сразу всех записей можно сделать одним вызовом:
      // инициализация издателя
      $publisher = \Bitrix\Main\Test\Typography\PublisherTable::getByPrimary(253)
      	->fetchObject();
      	
      // удаление всех книг издателя
      $publisher->removeAllBooks();
      
      // сохранение
      $publisher->save();
      

      Для такой операции необходимо знать исходные значения - какие в данный момент есть Книги у Издателя. Поэтому, если значение поля BOOKS не было выбрано изначально, оно будет выбрано автоматически перед удалением.

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

    $fieldName = 'BOOKS';
    
    $publisher->addTo($fieldName, $book);
    $publisher->removeFrom($fieldName, $book);
    $publisher->removeAll($fieldName);
    

    ArrayAccess

    Интерфейс доступа к объекту как к массиву может помочь обеспечить обратную совместимость при переходе с массивов на объекты:

    $author = \Bitrix\Main\Test\Typography\AuthorTable::getByPrimary(17)->fetchObject();
    		
    echo $author['NAME'];
    // вызов аналогичен методу $author->getName()
    
    $author['NAME'] = 'New name';
    // вызов аналогичен методу $author->setName('New name')
    

    Что касается runtime полей, то в данном случае можно только считывать их значения, но не устанавливать:

    $author = \Bitrix\Main\Test\Typography\AuthorTable::query()
    	->registerRuntimeField(
    		new \Bitrix\Main\Entity\ExpressionField('FULL_NAME', 'CONCAT(%s, " ", %s)', ['NAME', 'LAST_NAME'])
    	)
    	->addSelect('ID')
    	->addSelect('FULL_NAME')
    	->where('ID', 17)
    	->fetchObject();
    
    echo $author['FULL_NAME'];
    // вызов аналогичен методу $author->get('FULL_NAME');
    
    $author['FULL_NAME'] = 'New name';
    // вызовет исключение
    


    Коллекции

    Коллекция - это умный массив, позволяющий оптимизировать групповые операции с объектами одного типа.

    Как и в случае с Объектами, для начала использования Коллекции объектов достаточно иметь лишь описанную сущность. Используйте fetchCollection вместо привычного fetchAll или зацикленного fetch:

    $books = \Bitrix\Main\Test\Typography\BookTable::getList()
    	->fetchCollection();
    

    $books - коллекция объектов класса Book, наделенная методами для групповых манипуляций с объектами. Также коллекции используются в Отношениях.



    Класс коллекции

    Действует абсолютно та же логика с префиксом EO_, что и у Объектов. У каждой сущности свой класс Коллекции, унаследованный от Bitrix\Main\ORM\Objectify\Collection. Для сущности Book по умолчанию он будет иметь вид EO_Book_Collection. Чтобы задать свой класс, нужно создать наследника этого класса и обозначить его в классе Table сущности:

    //Файл bitrix/modules/main/lib/test/typography/books.php
    
    namespace Bitrix\Main\Test\Typography;
    
    class Books extends EO_Book_Collection
    {
    }
    
    //Файл bitrix/modules/main/lib/test/typography/booktable.php
    
    namespace Bitrix\Main\Test\Typography;
    
    class BookTable extends Bitrix\Main\ORM\Data\DataManager
    {
    	public static function getCollectionClass()
    	{
    		return Books::class;
    	}
    	//...
    }

    Теперь метод fetchCollection будет возвращать коллекцию Bitrix\Main\Test\Typography\Books объектов класса Bitrix\Main\Test\Typography\Book. Аннотации позволят IDE давать подсказки, облегчая работу разработчика.



    Доступ к элементам коллекции

    • foreach

      Базовый класс коллекции реализует интерфейс \Iterator, что позволяет перебирать элементы в цикле:

      $books = \Bitrix\Main\Test\Typography\BookTable::getList()
      	->fetchCollection();
      
      foreach ($books as $book)
      {
      	// ...
      }
    • getAll, getByPrimary

      Объекты коллекции можно получить не только в цикле, но и напрямую. Метод getAll вернет все содержащиеся объекты в виде массива:

      $books = \Bitrix\Main\Test\Typography\BookTable::getList()
      	->fetchCollection();
      
      $bookObjects = $books->getAll();
      
      echo $bookObjects[0]->getId();
      // выведет значение ID первого объекта
      

      Для получения конкретных объектов, содержащихся в коллекции, предусмотрен метод getByPrimary:

      // 1. пример с простым первичным ключном
      $books = \Bitrix\Main\Test\Typography\BookTable::getList()
      		->fetchCollection();
      	
      $book = $books->getByPrimary(1);
      // книга с ID=1
      
      // 2. пример с составным первичным ключом
      $booksToAuthor = \Bitrix\Main\Test\Typography\BookAuthorTable::getList()
      	->fetchCollection();
      
      $bookToAuthor = $booksToAuthor->getByPrimary(
      	['BOOK_ID' => 2, 'AUTHOR_ID' => 18]
      );
      // будет присвоен объект отношения книги ID=2 с автором ID=18
      
    • has, hasByPrimary

      Проверить наличие конкретного объекта в коллекции можно методом has:

      $book1 = \Bitrix\Main\Test\Typography\Book::wakeUp(1);
      $book2 = \Bitrix\Main\Test\Typography\Book::wakeUp(2);
      
      $books = \Bitrix\Main\Test\Typography\BookTable::query()
      	->addSelect('*')
      	->whereIn('ID', [2, 3, 4])
      	->fetchCollection();
      
      var_dump($books->has($book1));
      // выведет false
      
      var_dump($books->has($book2));
      // выведет true
      

      Аналогично работает метод hasByPrimary, когда удобнее сделать проверку по первичному ключу:

      $books = \Bitrix\Main\Test\Typography\BookTable::query()
      	->addSelect('*')
      	->whereIn('ID', [2, 3, 4])
      	->fetchCollection();
      
      var_dump($books->hasByPrimary(1));
      // выведет false
      
      var_dump($books->hasByPrimary(2));
      // выведет true
      
    • add, []

      Добавление объектов реализовано методом add и интерфейсом ArrayAccess, позволяющим использовать конструкцию []:

      $book1 = \Bitrix\Main\Test\Typography\Book::wakeUp(1);
      
      $books = \Bitrix\Main\Test\Typography\BookTable::query()
      	->addSelect('*')
      	->whereIn('ID', [2, 3, 4])
      	->fetchCollection();
      
      $books->add($book1);
      // или
      $books[] = $book1;
    • remove, removeByPrimary

      Удалить объект из коллекции можно, задав его явно или указав первичный ключ:

      $book1 = \Bitrix\Main\Test\Typography\Book::wakeUp(1);
      
      $books = \Bitrix\Main\Test\Typography\BookTable::getList()
      	->fetchCollection();
      
      $books->remove($book1);
      // из коллекции удалится книга с ID=1
      
      $books->removeByPrimary(2);
      // из коллекции удалится книга с ID=2
      


    Групповые действия

    Коллекции позволяют совершать групповые операции над содержащимися в ней элементами.

    • save (добавление)

      Метод save() в случае с новыми объектами выполняет их первичное сохранение, формируя один групповой запрос:

      use \Bitrix\Main\Test\Typography\Books;
      use \Bitrix\Main\Test\Typography\Book;
      
      $books = new Books;
      
      $books[] = (new Book)->setTitle('Title 112');
      $books[] = (new Book)->setTitle('Title 113');
      $books[] = (new Book)->setTitle('Title 114');
      
      $books->save(true);
      
      // INSERT INTO ...  (`TITLE`, `ISBN`) VALUES
      	('Title 112', DEFAULT),
      	('Title 113', DEFAULT),
      	('Title 114', '114-000')

      При этом в метод передан параметр $ignoreEvents = true, отменяющий выполнение событий ORM во время добавления записей. Дело в том, что именно при мульти-вставке с автоинкрементным полем (ID) невозможно получить множественные значения этого поля, как это возможно при вставке одной записи с помощью функций вроде mysqli_insert_id().

      В остальных случаях, когда в сущности нет автоинкрементных полей, выполнение событий остается на усмотрение разработчика. По умолчанию события будут выполняться.

    • save (редактирование)

      Метод save() в случае с уже существующими, но измененными объектами, выполняет их сохранение одним запросом UPDATE:

      use \Bitrix\Main\Test\Typography\PublisherTable;
      use \Bitrix\Main\Test\Typography\BookTable;
      
      $books = BookTable::getList()->fetchCollection();
      $publisher = PublisherTable::wakeUpObject(254);
      
      foreach ($books as $book)
      {
      	$book->setPublisher($publisher);
      }
      
      $books->save();
      
      // UPDATE ... SET `PUBLISHER_ID` = '254'
      	WHERE `ID` IN ('1', '2')

      Групповое обновление сработает только в случае, если набор изменных данных единый для всех объектов. Если же хотя бы в одном объекте измененные данные отличаются, то все записи будут сохранены по отдельности.

      Как и в случае с добавлением, при обновлении можно отключать выполнение событий параметром $ignoreEvents в методе save(). По умолчанию они выполняются для каждого элемента коллекции.

    • fill

      Коллекционная операция fill является прекрасной альтернативой аналогичной операции в Объекте, выполненной в цикле. В случае с циклом количество запросов к базе данных будет равно количеству объектов:

      /** @var \Bitrix\Main\Test\Typography\Book[] $books */
      $books = [
      	\Bitrix\Main\Test\Typography\Book::wakeUp(1),
      	\Bitrix\Main\Test\Typography\Book::wakeUp(2)
      ];
      
      foreach ($books as $book)
      {
      	$book->fill();
      	// SELECT ... WHERE ID = ...
      	// так делать не надо!
      }

      В случае же с коллекцией запрос будет только один:

      $books = new \Bitrix\Main\Test\Typography\Books;
      // или $books = \Bitrix\Main\Test\Typography\BookTable::createCollection();
      
      $books[] = \Bitrix\Main\Test\Typography\Book::wakeUp(1);
      $books[] = \Bitrix\Main\Test\Typography\Book::wakeUp(2);
      
      $books->fill();
      // SELECT ... WHERE ID IN(1,2)
      

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

      $books->fill(['TITLE', 'PUBLISHER_ID']);
      $books->fill(\Bitrix\Main\ORM\Fields\FieldTypeMask::FLAT);

      Более подробно возможные значения параметра описаны в fill Объектов.

    • get*List

      Не самый редкий сценарий - получить из результата запроса список значений отдельного поля. В обычном случае это может выглядеть так:

      $books = \Bitrix\Main\Test\Typography\BookTable::getList()
      	->fetchCollection();
      
      $titles = [];
      
      foreach ($books as $book)
      {
      	$titles[] = $book->getTitle();
      }

      Именованный групповой "геттер" позволяет сократить такой цикл до одной строчки кода:

      $books = \Bitrix\Main\Test\Typography\BookTable::getList()
      	->fetchCollection();
      
      $titles = $books->getTitleList();

      Такие "геттеры" доступны для всех полей сущности и описываются в аннотациях для IDE.



    Восстановление коллекции

    Восстановление коллекции из готовых данных работает так же, как это сделано в Объектах:

    // восстановление по первичному ключу
    $books = \Bitrix\Main\Test\Typography\Books::wakeUp([1, 2]);
    
    // восстановление по набору полей
    $books = \Bitrix\Main\Test\Typography\Books::wakeUp([
    	['ID' => 1, 'TITLE' => 'Title 1'],
    	['ID' => 2, 'TITLE' => 'Title 2']
    ]);

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

    Примечание: Если необходимо сохранение коллекции для объектов, которых не было в базе данных и восстановленных через wakeUp, то сохранятся объекты изменённые с момента wakeUp значения запросом UPDATE.



    Отношения

    На примере схемы отношений тестовых сущностей Book, Author и Publisher рассмотрим все комбинации и направления отношений: 1:N, 1:1, N:M.

    Будем исходить из того, что книга принадлежит одному издательству, но у нее может быть несколько авторов и она продается в нескольких магазинах.



    1:N

    В нашей тестовой вселенной книга может принадлежать и издаваться строго в одном издательстве. Получаем отношение "1 издательство - N книг".

    Книга и издательство

    В таких случаях в таблицу Книги следует добавить поле PUBLISHER_ID, значение которого будет указывать на Издательство.

    namespace Bitrix\Main\Test\Typography;
    
    use Bitrix\Main\ORM\Fields\IntegerField;
    
    class BookTable extends \Bitrix\Main\ORM\Data\DataManager
    {
    	public static function getMap()
    	{
    		return [
    			// ...
    			(new IntegerField('PUBLISHER_ID'))
    		];
    	}
    }
    

    Но для ORM одного этого поля недостаточно для понимания связи между сущностями Book и Publisher. Чтобы такое понимание возникло, используются поля множества Bitrix\Main\ORM\Fields\Relations, в данном случае для указания направленной связи "много к одному" нужно поле типа Reference:

    namespace Bitrix\Main\Test\Typography;
    
    use Bitrix\Main\ORM\Fields\IntegerField;
    use Bitrix\Main\ORM\Fields\Relations\Reference;
    use Bitrix\Main\ORM\Query\Join;
    
    class BookTable extends \Bitrix\Main\ORM\Data\DataManager
    {
    	public static function getMap()
    	{
    		return [
    			// ...
    			(new IntegerField('PUBLISHER_ID')),
    
    			(new Reference(
    					'PUBLISHER',
    					PublisherTable::class,
    					Join::on('this.PUBLISHER_ID', 'ref.ID')
    				))
    				->configureJoinType('inner')
    		];
    	}
    }
    

    Параметры конструктора Reference:

    ПараметрОписание
    $name Имя поля.
    $referenceEntity Класс связываемой сущности.
    $referenceFilter Условия "джойна". Ожидается объект фильтра. В отличие от регулярного использования фильтра, здесь к именам колонок нужно добавлять префиксы "this." и "ref.", чтобы обозначить принадлежность к текущей и связываемой сущности соответственно.

    Для читаемости создан класс Bitrix\Main\ORM\Query\Join, единственный метод которого on возвращает объект фильтра Bitrix\Main\ORM\Query\Filter\ConditionTree, задавая перед этим наиболее популярное условие whereColumn.

    Дополнительно можно сконфигурировать тип "джойна". По умолчанию это left join, в примере выше задается inner join.

    Теперь можно воспользоваться описанным отношением при выборке данных:

    $book = \Bitrix\Main\Test\Typography\BookTable::getByPrimary(1, [
    	'select' => ['*', 'PUBLISHER']
    ])->fetchObject();
    
    echo $book->getPublisher()->getTitle();
    // выведет Publisher Title 253
    

    Доступ к объекту сущности Publisher реализуется через "геттер" getPublisher(). Таким образом, можно подключать более глубокие цепочки отношений, и так же по цепочкам "геттеров" добираться до конечных объектов.

    Чтобы установить связь, достаточно передать объект сущности Publisher в соответствующий "сеттер":

    // инициализация издателя
    $publisher = \Bitrix\Main\Test\Typography\PublisherTable::wakeUpObject(253);
    
    // инициализация книги
    $book = \Bitrix\Main\Test\Typography\BookTable::getByPrimary(1)
    	->fetchObject();
    
    // установка значения объекта
    $book->setPublisher($publisher);
    
    // сохранение
    $book->save();
    

    Значение поля PUBLISHER_ID будет заполнено автоматически из переданного объекта.

    С массивами результат выглядит не так лаконично:

    $result = \Bitrix\Main\Test\Typography\BookTable::getByPrimary(1, [
    	'select' => ['*', 'PUBLISHER']
    ]);
    
    print_r($result->fetch());
    /* выведет
    Array (
    	[ID] => 1
    	[TITLE] => Title 1
    	[PUBLISHER_ID] => 253
    	[ISBN] => 978-3-16-148410-0
    	[IS_ARCHIVED] => Y
    	[MAIN_TEST_TYPOGRAPHY_BOOK_PUBLISHER_ID] => 253
    	[MAIN_TEST_TYPOGRAPHY_BOOK_PUBLISHER_TITLE] => Publisher Title 253
    )
    */
    

    Полям связанной сущности присваиваются уникальные имена, основанные на пространстве имен и имени класса. Можно воспользоваться механизмом "алиасов" для получения более коротких и практичных имен:

    $result = \Bitrix\Main\Test\Typography\BookTable::getByPrimary(1, [
    	'select' => ['*', 'PUB_' => 'PUBLISHER']
    ]);
    
    print_r($result->fetch());
    /* выведет
    Array (
    	[ID] => 1
    	[TITLE] => Title 1
    	[PUBLISHER_ID] => 253
    	[ISBN] => 978-3-16-148410-0
    	[IS_ARCHIVED] => Y
    	[PUB_ID] => 253
    	[PUB_TITLE] => Publisher Title 253
    )
    */
    

    Издательство и книги

    Пока что доступ к отношению работает только по направлению "Книга" -> "Издатель". Чтобы сделать его двунаправленным, понадобится описать отношение на стороне сущности Publisher:

    namespace Bitrix\Main\Test\Typography;
    
    use Bitrix\Main\ORM\Data\DataManager;
    use Bitrix\Main\ORM\Fields\Relations\OneToMany;
    
    class PublisherTable extends DataManager
    {
    	public static function getMap()
    	{
    		return [
    			// ...
    
    			(new OneToMany('BOOKS', BookTable::class, 'PUBLISHER'))->configureJoinType('inner')
    		];
    	}
    }
    

    Параметры конструктора OneToMany:

    ПараметрОписание
    $name Имя поля.
    $referenceEntity Класс связываемой сущности.
    $referenceFilter Имя поля `Reference` в сущности-партнере, через которое осуществляется связь.

    Дополнительно можно переопределить тип джойна. По умолчанию используется тип, заданный в Reference поле связываемой сущности.

    Теперь можно воспользоваться описанным отношением при выборке данных:

    $publisher = \Bitrix\Main\Test\Typography\PublisherTable::getByPrimary(253, [
    	'select' => ['*', 'BOOKS']
    ])->fetchObject();
    
    foreach ($publisher->getBooks() as $book)
    {
    	echo $book->getTitle();
    }
    
    // цикл выведет "Title 1" и "Title 2"
    

    В примере выше видно принципиальное преимущество объектной модели перед массивами. Несмотря на то, что фактически выбрано две записи (для одного Издателя нашлось две книги), по факту из результата получается только один объект. Система самостоятельно распознала этот случай и склеила все книги издателя в одну Коллекцию.

    Если запросить из результата массив, будет классическая структура данных с двоением данных Издателя:

    $data = \Bitrix\Main\Test\Typography\PublisherTable::getByPrimary(253, [
    	'select' => ['*', 'BOOK_' => 'BOOKS']
    ])->fetchAll();
    
    // вернет
    Array (
    	[0] => Array (
    		[ID] => 253
    		[TITLE] => Publisher Title 253
    		[BOOK_ID] => 2
    		[BOOK_TITLE] => Title 2
    		[BOOK_PUBLISHER_ID] => 253
    		[BOOK_ISBN] => 456-1-05-586920-1
    		[BOOK_IS_ARCHIVED] => N 
    	) 
    	[1] => Array (
    		[ID] => 253
    		[TITLE] => Publisher Title 253
    		[BOOK_ID] => 1
    		[BOOK_TITLE] => Title 1
    		[BOOK_PUBLISHER_ID] => 253
    		[BOOK_ISBN] => 978-3-16-148410-0
    		[BOOK_IS_ARCHIVED] => Y
    	)
    )

    Чтобы добавить новую Книгу Издателю, используется именованный "сеттер" addTo:

    // инициализация издателя
    $publisher = \Bitrix\Main\Test\Typography\PublisherTable::getByPrimary(253)
    	->fetchObject();
    
    // инициализация книги
    $book = \Bitrix\Main\Test\Typography\BookTable::getByPrimary(2)
    	->fetchObject();
    
    // добавление книги в коллекцию отношения
    $publisher->addToBooks($book);
    
    // сохранение
    $publisher->save();
    

    Для удаления связи со стороны книги достаточно установить setPublisher() другого издателя или null. А чтобы сделать это со стороны Издателя, существуют специализированные "сеттеры" removeFrom() и removeAll():

    // инициализация книги
    $book = \Bitrix\Main\Test\Typography\Book::wakeUp(2);
    
    // инициализация издателя
    $publisher = \Bitrix\Main\Test\Typography\PublisherTable::getByPrimary(253, [
    	'select' => ['*', 'BOOKS']
    ])->fetchObject();
    
    // удаление одной конкретной книги издателя
    $publisher->removeFromBooks($book);
    
    // или удаление всех книг издателя
    $publisher->removeAllBooks();
    
    // во время сохранения поле PUBLISHER_ID в Книгах будет обновлено на пустое значение
    // сами книги удалены не будут, удаляется именно связь
    $publisher->save();
    

    Важно обратить внимание, что для корректной работы поле отношения должно быть заполнено - в примере выше оно указано при выборке данных. Если вы не выбирали значения из базы данных или не уверены в их заполненности у конкретного объекта, необходимо предварительно вызвать метод fill:
    // инициализация книги
    $book = \Bitrix\Main\Test\Typography\BookTable::wakeUpObject(2);
    
    // у издателя будет заполнен только первичный ключ
    $publisher = \Bitrix\Main\Test\Typography\PublisherTable::wakeUpObject(253);
    
    // заполняем поле отношения
    $publisher->fillBooks();
    
    // удаление одной конкретной книги
    $publisher->removeFromBooks($book);
    
    // или удаление всех книг
    $publisher->removeAllBooks();
    
    // во время сохранения поле PUBLISHER_ID в Книгах будет обновлено на пустое значение
    // сами книги удалены не будут
    $publisher->save();
    

    В случае с массивами операции addTo, removeFrom и removeAll невозможны, можно создать связь только со стороны сущности Книги.



    1:1

    Отношения один-к-одному работают аналогично один-ко-многим с той лишь разницей, что в обеих сущностях вместо пары Reference + OneToMany оба поля будут Reference.



    N:M

    Примитивные отношения без вспомогательных данных

    У книги может быть несколько авторов, у автора может быть несколько книг. В таких случаях создается отдельная таблица с двумя полями AUTHOR_ID и BOOK_ID. В ORM не придется оформлять ее отдельной сущностью, достаточно описать отношение специальным полем ManyToMany:

    //Файл bitrix/modules/main/lib/test/typography/booktable.php
    
    namespace Bitrix\Main\Test\Typography;
    
    use Bitrix\Main\ORM\Fields\Relations\ManyToMany;
    
    class BookTable extends \Bitrix\Main\ORM\Data\DataManager
    {
    	public static function getMap()
    	{
    		return [
    			// ...
    
    			(new ManyToMany('AUTHORS', AuthorTable::class))
    				->configureTableName('b_book_author')
    		];
    	}
    }
    
    //Файл bitrix/modules/main/lib/test/typography/authortable.php
    
    namespace Bitrix\Main\Test\Typography;
    
    use Bitrix\Main\ORM\Fields\Relations\ManyToMany;
    
    class AuthorTable extends \Bitrix\Main\ORM\Data\DataManager
    {
    	public static function getMap()
    	{
    		return [
    			// ...
    
    			(new ManyToMany('BOOKS', BookTable::class))
    				->configureTableName('b_book_author')
    		];
    	}
    }

    Необязательно описывать поле в обеих сущностях - можно только в одной, но тогда и доступ к данным будет осуществляться только через нее.

    В конструктор передаются название поля и класс партнерской сущности. В случае примитивных отношений достаточно вызвать метод configureTableName с указанием имени таблицы, где хранятся связующие данные. Более сложный случай будет рассмотрен ниже, в примере отношений Книг с Магазинами.

    В данном же случае в памяти автоматически создается временная сущность для работы с промежуточной таблицей. Фактически вы нигде не увидите ее следов, но для понимания происходящего и возможных дополнительных настроек приоткроем внутренности. Системная сущность промежуточной таблицы имеет примерно такой вид:

    class ... extends \Bitrix\Main\ORM\Data\DataManager
    {
    	public static function getTableName()
    	{
    		return 'b_book_author';
    	}
    
    	public static function getMap()
    	{
    		return [
    			(new IntegerField('BOOK_ID'))
    				->configurePrimary(true),
    
    			(new Reference('BOOK', BookTable::class,
    				Join::on('this.BOOK_ID', 'ref.ID')))
    				->configureJoinType('inner'),
    
    			(new IntegerField('AUTHOR_ID'))
    				->configurePrimary(true),
    
    			(new Reference('AUTHOR', AuthorTable::class,
    				Join::on('this.AUTHOR_ID', 'ref.ID')))
    				->configureJoinType('inner'),
    		];
    	}
    }
    

    Это не более чем типовая сущность с референсами (направленными отношениями 1:N) к исходным сущностям-партнерам. Имена полей формируются на основе имен сущностей и имен их первичных ключей:

    new IntegerField('BOOK_ID') - snake_case от Book + primary поле ID
    new Reference('BOOK') - snake_case от Book
    new IntegerField('AUTHOR_ID') - snake_case от Author + primary поле ID
    new Reference('AUTHOR') - snake_case от Author
    

    Чтобы задать имена полей явно, особенно это актуально в сущностях с составными первичными ключами для избегания путаницы, используются конфигурирующие методы:

    //Файл bitrix/modules/main/lib/test/typography/booktable.php
    
    namespace Bitrix\Main\Test\Typography;
    
    use Bitrix\Main\ORM\Fields\Relations\ManyToMany;
    
    class BookTable extends \Bitrix\Main\ORM\Data\DataManager
    {
    	public static function getMap()
    	{
    		return [
    			// ...
    
    			(new ManyToMany('AUTHORS', AuthorTable::class))
    				->configureTableName('b_book_author')
    				->configureLocalPrimary('ID', 'MY_BOOK_ID')
    				->configureLocalReference('MY_BOOK')
    				->configureRemotePrimary('ID', 'MY_AUTHOR_ID')
    				->configureRemoteReference('MY_AUTHOR')
    		];
    	}
    }
    
    //Файл bitrix/modules/main/lib/test/typography/authortable.php
    
    namespace Bitrix\Main\Test\Typography;
    
    use Bitrix\Main\ORM\Fields\Relations\ManyToMany;
    
    class AuthorTable extends \Bitrix\Main\ORM\Data\DataManager
    {
    	public static function getMap()
    	{
    		return [
    			// ...
    
    			(new ManyToMany('BOOKS', BookTable::class))
    				->configureTableName('b_book_author')
    				->configureLocalPrimary('ID', 'MY_AUTHOR_ID')
    				->configureLocalReference('MY_AUTHOR'),
    				->configureRemotePrimary('ID', 'MY_BOOK_ID')
    				->configureRemoteReference('MY_BOOK')
    		];
    	}
    }

    Метод configureLocalPrimary указывает, как будет называться привязка к полю из первичного ключа текущей сущности, аналогично configureRemotePrimary задает соответствие полей первичного ключа сущности-партнера. Методы configureLocalReference и configureRemoteReference задают имена референсов к исходным сущностям. При описанной выше конфигурации системная сущность отношений будет иметь примерно такой вид:

    class ... extends \Bitrix\Main\ORM\Data\DataManager
    {
    	public static function getTableName()
    	{
    		return 'b_book_author';
    	}
    
    	public static function getMap()
    	{
    		return [
    			(new IntegerField('MY_BOOK_ID'))
    				->configurePrimary(true),
    
    			(new Reference('MY_BOOK', BookTable::class,
    				Join::on('this.MY_BOOK_ID', 'ref.ID')))
    				->configureJoinType('inner'),
    
    			(new IntegerField('MY_AUTHOR_ID'))
    				->configurePrimary(true),
    
    			(new Reference('MY_AUTHOR', AuthorTable::class,
    				Join::on('this.MY_AUTHOR_ID', 'ref.ID')))
    				->configureJoinType('inner'),
    		];
    	}
    }

    Как и в случае Reference с OneToMany, здесь тоже можно переопределить тип джойна методом configureJoinType (значение по умолчанию - "left"):

    (new ManyToMany('AUTHORS', AuthorTable::class))
    	->configureTableName('b_book_author')
    	->configureJoinType('inner')
    

    Чтение данных работает аналогично отношениям 1:N:

    // выборка со стороны авторов
    $author = \Bitrix\Main\Test\Typography\AuthorTable::getByPrimary(18, [
    	'select' => ['*', 'BOOKS']
    ])->fetchObject();
    
    foreach ($author->getBooks() as $book)
    {
    	echo $book->getTitle();
    }
    // цикл выведет "Title 1" и "Title 2"
    
    // выборка со сороны книг
    $book = \Bitrix\Main\Test\Typography\BookTable::getByPrimary(2, [
    	'select' => ['*', 'AUTHORS']
    ])->fetchObject();
    
    foreach ($book->getAuthors() as $author)
    {
    	echo $author->getLastName();
    }
    // цикл выведет "Last name 17" и "Last name 18"
    

    Выборка объектов вместо массивов вновь выгодно отличается тем, что не происходит "двоения" данных, как это происходит с массивами:

    $author = \Bitrix\Main\Test\Typography\AuthorTable::getByPrimary(18, [
    	'select' => ['*', 'BOOK_' => 'BOOKS']
    ])->fetchAll();
    
    // вернет
    Array (
    	[0] => Array 
    		[ID] => 18
    		[NAME] => Name 18
    		[LAST_NAME] => Last name 18
    		[BOOK_ID] => 1
    		[BOOK_TITLE] => Title 1
    		[BOOK_PUBLISHER_ID] => 253
    		[BOOK_ISBN] => 978-3-16-148410-0
    		[BOOK_IS_ARCHIVED] => Y 
    	)
    	[1] => Array (
    		[ID] => 18
    		[NAME] => Name 18
    		[LAST_NAME] => Last name 18
    		[BOOK_ID] => 2
    		[BOOK_TITLE] => Title 2
    		[BOOK_PUBLISHER_ID] => 253
    		[BOOK_ISBN] => 456-1-05-586920-1
    		[BOOK_IS_ARCHIVED] => N
    	)
    )

    Создание связи между объектами двух сущностей происходит так же, как в случае с отношениями 1:N:

    // со стороны авторов
    $author = \Bitrix\Main\Test\Typography\AuthorTable::getByPrimary(17)
    	->fetchObject();
    
    $book = \Bitrix\Main\Test\Typography\BookTable::getByPrimary(1)
    	->fetchObject();
    
    $author->addToBooks($book);
    
    $author->save();
    
    
    // со стороны книг
    $author = \Bitrix\Main\Test\Typography\AuthorTable::getByPrimary(17)
    	->fetchObject();
    
    $book = \Bitrix\Main\Test\Typography\BookTable::getByPrimary(1)
    	->fetchObject();
    
    $book->addToAuthors($author);
    
    $book->save();
    

    Методы removeFrom и removeAll работают тоже аналогично.

    Для массивов подобных конструкций не предусмотрено. Как связывать сущности, используя массивы - смотрите ниже в примере отношений Книг с Магазинами.

    Отношения со вспомогательными данными


    STORE_ID BOOK_ID QUANTITY
    33 14
    33 20
    43 29

    Когда есть дополнительные данные (количество книг в наличие), а не только первичные ключи исходных сущностей, такое отношение следует описать отдельной сущностью:

    namespace Bitrix\Main\Test\Typography;
    
    use Bitrix\Main\ORM\Data\DataManager;
    use Bitrix\Main\ORM\Fields\IntegerField;
    use Bitrix\Main\ORM\Fields\Relations\Reference;
    use Bitrix\Main\ORM\Query\Join;
    
    class StoreBookTable extends DataManager
    {
    	public static function getTableName()
    	{
    		return 'b_store_book';
    	}
    
    	public static function getMap()
    	{
    		return [
    			(new IntegerField('STORE_ID'))
    				->configurePrimary(true),
    
    			(new Reference('STORE', StoreTable::class,
    				Join::on('this.STORE_ID', 'ref.ID')))
    				->configureJoinType('inner'),
    
    			(new IntegerField('BOOK_ID'))
    				->configurePrimary(true),
    
    			(new Reference('BOOK', BookTable::class,
    				Join::on('this.BOOK_ID', 'ref.ID')))
    				->configureJoinType('inner'),
    
    			(new IntegerField('QUANTITY'))
    				->configureDefaultValue(0)
    		];
    	}
    }

    Если для простых отношений без вспомогательных данных использовались поля ManyToMany, здесь их использование будет сильно ограничено. Можно будет создавать и удалять связи, но не будет доступа к вспомогательному полю QUANTITY. С помощью removeFrom*() можно будет удалить связь, с помощью addTo*() можно будет добавить связь со значением QUANTITY только по умолчанию, и не будет возможности обновить значение QUANTITY. Поэтому в таких случаях более гибким будет использование непосредственно сущности-посредника:

    // объект книги
    $book = \Bitrix\Main\Test\Typography\BookTable::getByPrimary(1)
    	->fetchObject();
    
    // объект магазина
    $store = \Bitrix\Main\Test\Typography\StoreTable::getByPrimary(34)
    	->fetchObject();
    
    // новый объект связи книги с магазином
    $item = \Bitrix\Main\Test\Typography\StoreBookTable::createObject()
    	->setBook($book)
    	->setStore($store)
    	->setQuantity(5);
    
    // сохранение
    $item->save();

    Обновление количества книг:

    // объект существующей связи
    $item = \Bitrix\Main\Test\Typography\StoreBookTable::getByPrimary([
    	'STORE_ID' => 33, 'BOOK_ID' => 2
    ])->fetchObject();
    
    // обновление количества
    $item->setQuantity(12);
    
    // сохранение
    $item->save();
    

    Удаление связи:

    // объект существующей связи
    $item = \Bitrix\Main\Test\Typography\StoreBookTable::getByPrimary([
    	'STORE_ID' => 33, 'BOOK_ID' => 2
    ])->fetchObject();
    
    // удаление
    $item->delete();
    

    То есть, работа с объектом связи ведется как с объектом любой другой сущности. Для массивов тоже следует использовать стандартные подходы по работе с данными:

    // добавление
    \Bitrix\Main\Test\Typography\StoreBookTable::add([
    	'STORE_ID' => 34, 'BOOK_ID' => 1, 'QUANTITY' => 5
    ]);
    
    // обновление
    \Bitrix\Main\Test\Typography\StoreBookTable::update(
    	['STORE_ID' => 34, 'BOOK_ID' => 1],
    	['QUANTITY' => 12]
    );
    
    // удаление
    \Bitrix\Main\Test\Typography\StoreBookTable::delete(
    	['STORE_ID' => 34, 'BOOK_ID' => 1]
    );

    Выше было упомянуто, что использовать поле ManyToMany в случае со вспомогательными данными - непродуктивно. Правильнее будет использовать тип OneToMany:

    //Файл bitrix/modules/main/lib/test/typography/booktable.php
    
    namespace Bitrix\Main\Test\Typography;
    
    use Bitrix\Main\ORM\Fields\Relations\OneToMany;
    
    class BookTable extends \Bitrix\Main\ORM\Data\DataManager
    {
    	public static function getMap()
    	{
    		return [
    			// ...
    
    			(new OneToMany('STORE_ITEMS', StoreBookTable::class, 'BOOK'))
    		];
    	}
    }
    //Файл bitrix/modules/main/lib/test/typography/storetable.php
    
    namespace Bitrix\Main\Test\Typography;
    
    use Bitrix\Main\ORM\Fields\Relations\OneToMany;
    
    class StoreTable extends \Bitrix\Main\ORM\Data\DataManager
    {
    	public static function getMap()
    	{
    		return [
    			// ...
    
    			(new OneToMany('BOOK_ITEMS', StoreBookTable::class, 'STORE'))
    		];
    	}
    }

    В таком случае выборка ничем не будет отличаться от отношений 1:N, только в этот раз будут возвращаться объекты отношения StoreBook, а не сущности-партнера:

    $book = \Bitrix\Main\Test\Typography\BookTable::getByPrimary(1, [
    	'select' => ['*', 'STORE_ITEMS']
    ])->fetchObject();
    
    
    foreach ($book->getStoreItems() as $storeItem)
    {
    	printf(
    		'store "%s" has %s of book "%s"',
    		$storeItem->getStoreId(), $storeItem->getQuantity(), $storeItem->getBookId()
    	);
    	// выведет store "33" has 4 of book "1"
    }
    


    Аннотации классов

    Большая часть методов Объекта и Коллекции - виртуальные, обрабатываются через magic вызов __call. В то же время они сделаны для интуитивно понятных и говорящих именованных методов, и без автокомплита в IDE их ценность резко снижается.

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

    С версии 20.100.0 Главного модуля (main) файл с аннотациями ORM-классов ядра включен в поставку и расположен в /bitrix/modules/main/meta/orm.php.

    Для генерации таких аннотаций используется cli-команда orm:annotate:

    $ cd bitrix
    $ php bitrix.php orm:annotate

    Примечание: перед использованием CLI-окружения убедитесь, что установили зависимости проекта через composer.

    В процессе выполнения команды производится сканирование модулей, а именно всех файлов из папок bitrix/modules/[module]/lib. Если в файле обнаруживается "маппинг" сущности (класс Table, подкласс Bitrix\Main\ORM\Data\DataManager), то анализируется ее карта (список полей).

    Результатом команды является файл (по умолчанию bitrix/modules/orm_annotations.php), который содержит описания классов Объекта и Коллекции сущностей. Также в нем декларируются дубликат класса Table и несколько фактически несуществующих вспомогательных классов, помогающих сопровождать автокомплит IDE от момента запроса до использования результирующих объектов.

    По умолчанию сканируется только главный модуль main. Сканирование произвольных модулей можно задать явно:

    // аннотирование сущностей произвольного модуля:
    
    $ php bitrix.php orm:annotate -m tasks
    // аннотирование нескольких модулей:
    
    $ php bitrix.php orm:annotate -m main,intranet,faceid
    // аннотирование всех модулей:
    
    $ php bitrix.php orm:annotate -m all

    В ближайшее время мы планируем ввести мониторинг всех известных сущностей в режиме разработки, чтобы при изменении полей перестраивать их аннотации автоматически. Тогда не придется часто использовать консоль.

    Частично удобство есть уже в том, что при перегенерации классы заменяются точечно. Если в аннотациях уже есть описанные модули, то при повторном аннотировании одного из них обновится описание только его классов, и другие не будут удалены. Для сброса используйте параметр -c:

    $ php bitrix.php orm:annotate -c -m all

    Чтобы посмотреть все доступные параметры команды, выполните команду:

    $ php bitrix.php help orm:annotate


    Обратная совместимость

    С выходом объектов в модуле main версии 18.0.4 изменились некоторые внутренние механизмы ORM. Ситуации достаточно редкие и не совсем корректные изначально, по сути появилось больше строгости в их обработке.

    • Имена полей теперь регистронезависимы. До этого можно было описать два поля LAST_NAME и last_name, и это были бы разные поля. Теперь же такая сущность не сможет инициализироваться. Изменение связано с именованными методами в Объектах.
    • В выборке нельзя присвоить одноименный алиас в другом регистре, например, getList(['select' => ['id' => 'ID']]).
    • Поле BooleanField раньше допускало пустую строку в качестве значения, что могло привести к дальнейшему ошибочному трактованию значения. Теперь пустая строка недопустима, можно указывать true/false или значения values, указанные в конфигурации поля.

    В объектах пока что не поддерживаются:

    • Поля с сериализацей, поскольку в них существует противоречие: тип поля указывается как StringField или TextField, а фактически в нем хранится массив, что не соответствует заявленному типу.


    Выборка данных

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

    getList

    Чтобы новое API выглядело для разработчика менее пугающим и более знакомым, сохранено имя самого популярного метода: getList. Но если раньше каждый getList имел свой набор параметров и зашитое непрозрачное поведение, то теперь этот метод един для всех сущностей и подчиняется одним законам. Даже при желании у разработчика сущности сделать "костыль" в getList ничего не выйдет.

    Сущность BookTable, взятая в качестве примера, не исключение. Какие параметры принимает метод BookTable::getList?

    BookTable::getList(array(
    	'select'  => ... // имена полей, которые необходимо получить в результате
    	'filter'  => ... // описание фильтра для WHERE и HAVING
    	'group'   => ... // явное указание полей, по которым нужно группировать результат
    	'order'   => ... // параметры сортировки
    	'limit'   => ... // количество записей
    	'offset'  => ... // смещение для limit
    	'runtime' => ... // динамически определенные поля
    ));

    getList всегда возвращает объект DB\Result, из которого можно получить данные с помощью метода fetch():

    $rows = array();
    $result = BookTable::getList(array(
    	...
    ));
    while ($row = $result->fetch())
    {
    	$rows[] = $row;
    }

    Для получения сразу всех записей можно воспользоваться методом fetchAll():

    $result = BookTable::getList($parameters);
    $rows = $result->fetchAll();
    
    // или совсем лаконично:
    $rows = BookTable::getList($parameters)->fetchAll();

    Теперь рассмотрим подробнее все параметры.

    select

    Параметр `select` определяется в виде массива с именами полей сущности:

    BookTable::getList(array(
    	'select' => array('ISBN', 'TITLE', 'PUBLISH_DATE')
    ));
    
    // SELECT ISBN, TITLE, PUBLISH_DATE FROM my_book

    Если по каким-то причинам не устраивают оригинальные названия полей в результате, можно использовать алиасы:

    BookTable::getList(array(
    	'select' => array('ISBN', 'TITLE', 'PUBLICATION' => 'PUBLISH_DATE')
    ));
    
    // SELECT ISBN, TITLE, PUBLISH_DATE AS PUBLICATION FROM my_book

    В данном примере название поля `PUBLISH_DATE` заменяется на `PUBLICATION`, именно такое название будет фигурировать в результирующем массиве.

    Если необходимо выбрать все поля, то можно воспользоваться символом '*':

    BookTable::getList(array(
    	'select' => array('*')
    ));

    При этом будут выбраны только поля ScalarField, а поля-выражения ExpressionField и отношения с другими сущностями затронуты не будут - их всегда нужно указывать явно.

    Вычисляемое поле в select минуя runtime

    В примере ниже показана упомянутая выше автоматическая группировка — система сама распознала, что необходимо группировать по полю PUBLISH_DATE. Подробнее такое поведение описывается в блоге разработчиков.

    Если вычисляемое поле необходимо вам только в секции `select`, как это чаще всего бывает, то секцию `runtime` использовать необязательно: можно сэкономить время, поместив выражение сразу в select.

    Система позволяет использовать вложенные выражения, которые будут последовательно развернуты в финальном SQL коде:

      
          BookTable::getList(array(
           'select' => array(
           'runtime' => array(
                new ORM\Fields\ExpressionField('MAX_AGE', 'MAX(%s)', array('AGE_DAYS'))
                )
          ));
          // SELECT MAX(DATEDIFF(NOW(), PUBLISH_DATE)) AS MAX_AGE FROM my_book

    Обратите внимание, что внутри нового Expression поля MAX_AGE было использовано уже существующее другое Expression поле AGE_DAYS. Таким образом, система позволяет использовать вложенные выражения, которые будут последовательно развернуты в финальном SQL коде.

    Внутри нового Expression поля MAX_AGE было использовано уже существующее другое Expression поле AGE_DAYS.

    В секции runtime можно регистрировать не только Expression поля, но и поля любых других типов. Механизм runtime работает таким образом, что к сущности добавляется новое поле, будто оно было описано в ней изначально в методе getMap. Но такое поле находится в зоне видимости только в рамках одного запроса - в следующем вызове getList такое поле уже будет недоступно, потребуется заново его зарегистрировать.

    filter

    Параметр `filter` унаследовал формат фильтра инфоблоков:

    // WHERE ID = 1
    BookTable::getList(array(
    	'filter' => array('=ID' => 1)
    ));
    
    // WHERE TITLE LIKE 'Patterns%'
    BookTable::getList(array(
    	'filter' => array('%=TITLE' => 'Patterns%')
    ));

    Фильтр может быть многоуровневым массивом со склейкой выражений AND/OR:

    // WHERE ID = 1 AND ISBN = '9780321127426'
    BookTable::getList(array(
    	'filter' => array(
    		'=ID' => 1,
    		'=ISBN' => '9780321127426'
    	)
    ));
    
    // WHERE (ID=1 AND ISBN='9780321127426') OR (ID=2 AND ISBN='9781449314286')
    BookTable::getList(array(
    	'filter' => array(
    		'LOGIC' => 'OR',
    		array(
    			// 'LOGIC' => 'AND', // по умолчанию элементы склеиваются через AND
    			'=ID' => 1,
    			'=ISBN' => '9780321127426'
    		),
    		array(
    			'=ID' => 2,
    			'=ISBN' => '9781449314286'
    		)
    	)
    ));

    Полный список операторов сравнения, которые можно использовать в фильтре:

    • = равно (работает и с массивами)
    • % подстрока
    • > больше
    • < меньше
    • @  IN (EXPR), в качестве значения передается массив или объект DB\SqlExpression
    • !@  NOT IN (EXPR), в качестве значения передается массив или объект DB\SqlExpression

    • != не равно
    • !% не подстрока
    • >< между, в качестве значения передается массив array(MIN, MAX)
    • >= больше или равно
    • <= меньше или равно
    • =% LIKE
    • %= LIKE
    • Пояснение по префиксам
    • == булевое выражение для ExpressionField (например, для EXISTS() или NOT EXISTS())
    • !>< не между, в качестве значения передается массив array(MIN, MAX)
    • !=% NOT LIKE
    • !%= NOT LIKE
    • '==ID' => null - условие, что поле ID равно NULL (в sql-запросе будет преобразовано в ID IS NULL)
    • '!==NAME' => null - условие, что поле NAME не равно NULL (в sql-запросе будет преобразовано в NAME IS NOT NULL)
    Внимание! Если не указывать явно оператор сравнения =, то по умолчанию будет выполнен LIKE. В данном случае использован код построения фильтров из модуля Инфоблоков, который по историческим причинам подразумевает такое поведение.

    Для полей типа int ставится:
    - [dw]до выхода объектного ORM[/dw][di]С версии 18.0.3 модуля main.[/di]: = (сравнение по равенству, массив разворачивается в набор условий OR =)
    - после выхода: IN().

    group

    В параметре `group` перечисляются поля для группировки:

    BookTable::getList(array(
    	'group' => array('PUBLISH_DATE')
    ));

    В подавляющем большинстве случаев явно указывать группировку не требуется - система автоматически сделает это. Подробнее смотрите ниже в секции про динамически определенные поля.

    order

    Параметр `order` позволяет указать порядок сортировки:

    BookTable::getList(array(
    	'order' => array('PUBLISH_DATE' => 'DESC', 'TITLE' => 'ASC')
    ));
    
    BookTable::getList(array(
    	'order' => array('ID') // направление по умолчанию - ASC
    ));

    offset/limit

    Параметры `offset` и `limit` помогут ограничить количество выбираемых записей или реализовать постраничную выборку:

    // 10 последних записей
    BookTable::getList(array(
    	'order' => array('ID' => 'DESC')
    	'limit' => 10
    ));
    
    // 5-я страница с записями, по 20 на страницу
    BookTable::getList(array(
    	'order' => array('ID')
    	'limit' => 20,
    	'offset' => 80
    ));

    runtime

    Упоминаемые в 1-й части вычисляемые поля (ExpressionField) часто нужны не столько в описании сущности, сколько при выборке для различных вычислений с группировкой.

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

    BookTable::getList(array(
    	'select' => array('CNT'),
    	'runtime' => array(
    		new ORM\Fields\ExpressionField('CNT', 'COUNT(*)')
    	)
    ));
    // SELECT COUNT(*) AS CNT FROM my_book

    В данном примере вычисляемое поле не просто преобразовывает значение какого-то поля, а реализует произвольное SQL выражение с функцией COUNT.

    После регистрации поля в секции `runtime`, на него можно ссылаться не только в секции `select`, но и в других секциях:

    BookTable::getList(array(
    	'select' => array('PUBLISH_DATE'),
    	'filter' => array('>CNT' => 5),
    	'runtime' => array(
    		new ORM\Fields\ExpressionField('CNT', 'COUNT(*)')
    	)
    ));
    // выбрать дни, в которые выпущено более 5 книг
    // SELECT PUBLISH_DATE, COUNT(*) AS CNT FROM my_book GROUP BY PUBLISH_DATE HAVING COUNT(*) > 5

    Примечание. В данном примере показана упомянутая выше автоматическая группировка — система сама распознала, что необходимо группировать по полю PUBLISH_DATE. Подробнее такое поведение описывается здесь.

    Если вычисляемое поле необходимо только в секции `select` (как это чаще всего бывает), то секцию `runtime` использовать необязательно: можно сэкономить время, поместив выражение сразу в `select`.

    BookTable::getList(array(
    	'select' => array(
    		new ORM\Fields\ExpressionField('MAX_AGE', 'MAX(%s)', array('AGE_DAYS'))
    	)
    ));
    // SELECT MAX(DATEDIFF(NOW(), PUBLISH_DATE)) AS MAX_AGE FROM my_book

    Обратите внимание, что внутри нового Expression поля MAX_AGE было использовано уже существующее другое Expression поле AGE_DAYS. Таким образом, система позволяет использовать вложенные выражения, которые будут последовательно развернуты в финальном SQL коде.

    В секции `runtime` можно регистрировать не только Expression поля, но и поля любых других типов. Механизм `runtime` работает таким образом, что к сущности добавляется новое поле, будто оно было описано в ней изначально в методе `getMap`. Но такое поле находится в зоне видимости только в рамках одного запроса - в следующем вызове getList такое поле уже будет недоступно, потребуется заново его зарегистрировать.

    Кеширование выборки

    Доступно кеширование конкретной выборки с версии 16.5.9 . В самой сущности ничего не надо описывать. По умолчанию не кешируется.

    В getList, в параметры добавился ключ cache:

    $res = \Bitrix\Main\GroupTable::getList(array("filter"=>array("=ID"=>1), "cache"=>array("ttl"=>3600)));

    То же самое реализуется и с помощью Query:

    $query = \Bitrix\Main\GroupTable::query();
    $query->setSelect(array('*'));
    $query->setFilter(array("=ID"=>1));
    $query->setCacheTtl(150);
    $res = $query->exec();

    Возможно, что в результате кешированной выборки придет объект ArrayResult.

    По умолчанию выборки с JOIN не кешируются. Но, если вы уверены в том, что делаете, можно явно закешировать:

    "cache"=>array("ttl"=>3600, "cache_joins"=>true);
    //or
    $query->cacheJoins(true);

    Сброс кеша происходит в любом методе add/update/delete. Принудительный сброс кеша для таблицы:

    /* Пример для таблицы пользователей */
    \Bitrix\Main\UserTable::getEntity()->cleanCache();

    Администратору проекта доступен запрет кеширования или изменение TTL.


    Примечание: Если необходимо произвести форматирование данных при выборке, используйте fetch_data_modification.

    Получение всех элементов

    Для получения списка всех элементов без постраничного вывода используйте параметр count_total со значением true.

    $res = MyTable::getList(...'count_total' => true, );

    Это позволяет получить весь список в один запрос.

    $res->getCount(); // все элементы без пагинации

    Короткие вызовы

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

    • getById($id) - производит выборку по первичному ключу;
    • getByPrimary($primary, array $parameters) - выборка по первичному ключу с возможностью указания дополнительных параметров;

      Примечание: в обоих методах мы можем как передать id в виде числа, так и явно указать какой элемент является ключом, передав массив. Массив необходимо использовать, если у вас есть несколько primary полей. Если вы передаете в массиве элемент, который не является первичным ключом, то это будет ошибкой.

      BookTable::getById(1);
      BookTable::getByPrimary(array('ID' => 1));
      
      // такие вызовы будут аналогичны следующему вызову getList:
      BookTable::getList(array(
      	'filter' => array('=ID' => 1)
      ));
    • getRowById($id) - производит выборку по первичному ключу, но возвращается массив данных;
      $row = BookTable::getRowById($id);
      
      // аналогичный результат можно получить так:
      $result = BookTable::getById($id);
      $row = $result->fetch();
      
      // или так
      $result = BookTable::getList(array(
      	'filter' => array('=ID' => $id)
      ));
      
      $row = $result->fetch();
    • getRow(array $parameters) - производит выборку не по первичному ключу, а по каким-то другим параметрам. При этом возвращается только одна запись.
      $row = BookTable::getRow(array(
      	'filter' => array('%=TITLE' => 'Patterns%'),
      	'order' => array('ID')
      ));
      
      // аналогичный результат можно получить так:
      $result = BookTable::getList(array(
      	'filter' => array('%=TITLE' => 'Patterns%'),
      	'order' => array('ID')
      	'limit' => 1
      ));
      
      $row = $result->fetch();

    Объект Query

    Все параметры getList, getRow и другие передаются вместе, при этом сразу же выполняется запрос и возвращается результат: все происходит за один вызов. Но существует и альтернативный способ конфигурации запроса и контроля за его выполнением - это объект Entity\Query:

    // получение данных через getList
    $result = BookTable::getList(array(
    	'select' => array('ISBN', 'TITLE', 'PUBLISH_DATE')
    	'filter' => array('=ID' => 1)
    ));
    
    // аналогично через Entity\Query
    $q = new Entity\Query(BookTable::getEntity());
    $q->setSelect(array('ISBN', 'TITLE', 'PUBLISH_DATE'));
    $q->setFilter(array('=ID' => 1));
    $result = $q->exec();

    Такой подход может быть удобным, когда необходима гибкость в построении запроса. Например, если параметры запроса заранее неизвестны и формируются программно, можно вместо множества разных аргументов использовать один объект Query, накапливая в нем параметры запроса:

    $query = new Entity\Query(BookTable::getEntity());
    attachSelect($query);
    attachOthers($query);
    $result = $query->exec();
    
    function attachSelect(Entity\Query $query)
    {
    	$query->addSelect('ID');
    	
    	if (...)
    	{
    		$query->addSelect('ISBN');
    	}
    }
    
    function attachOthers(Entity\Query $query)
    {
    	if (...)
    	{
    		$query->setFilter(...);
    	}
    	
    	if (...)
    	{
    		$query->setOrder(...);
    	}
    }

    Также объект Entity\Query позволяет построить запрос, но не выполнять его. Это бывает полезным для выполнения подзапросов или же просто для получения текста запроса и последующего его использования:

    $q = new Entity\Query(BookTable::getEntity());
    $q->setSelect(array('ID'));
    $q->setFilter(array('=PUBLISH_DATE' => new Type\Date('2014-12-13', 'Y-m-d')));
    $sql = $q->getQuery();
    
    file_put_contents('/tmp/today_books.sql', $sql);
    
    // таким образом, запрос "SELECT ID FROM my_book WHERE PUBLISH_DATE='2014-12-31'" будет сохранен в файл, но не будет выполнен.

    Полный список методов Entity\Query для реализации описанных выше возможностей:

    select, group:

    • setSelect, setGroup - устанавливает массив с именами полей
    • addSelect, addGroup - добавляет имя поля
    • getSelect, getGroup - возвращает массив с именами полей

    filter:

    • setFilter - устанавливает одно- или многомерный массив с описанием фильтра
    • addFilter - добавляет один параметр фильтра со значением
    • getFilter - возвращает текущее описание фильтра

    order:

    • setOrder - устанавливает массив с именами полей и порядком сортировки
    • addOrder - добавляет одно поле с порядком сортировки
    • getOrder - возвращает текущее описание сортировки

    limit/offset:

    • setLimit, setOffset - устанавливает значение
    • getLimit, getOffset - возвращает текущее значение

    runtime fields:

    • registerRuntimeField - регистрирует новое временное поле для исходной сущности

    Объект Query является основополагающим элементом при выборке данных, он же используется внутри стандартного getList. Именно поэтому эффективность переопределения методов getList сводится на нет: если при вызове соответствующего метода хак сработает, то при аналогичном запросе напрямую через Query уже нет.

    Предустановленные выборки

      Глобальная область данных

    При необходимости одну таблицу можно описать несколькими сущностями, разделив записи на сегменты:

    class Element4Table extends \Bitrix\Iblock\ElementTable
    {
    	public static function getTableName()
    		{
    		return 'b_iblock_element';
    		}
    
    	public static function setDefaultScope(Query $query)
    		{
    		$query->where("IBLOCK_ID", 4);
    		}
    
    }
        
    class Element5Table extends \Bitrix\Iblock\ElementTable
    {
    		public static function getTableName()
    		{
    		return 'b_iblock_element';
    		}
            
    	public static function setDefaultScope(Query $query)
    		{
    		$query->where("IBLOCK_ID", 5);
    		}
    }
    

    Метод setDefaultScope будет выполняться при каждом запросе, пропуская через себя объект запроса. В нем можно задавать не только фильтр, но и любые другие параметры запроса.

      Локальная область данных

    Начиная с версии 20.5.500 появилась возможность задать предустановленные выборки – методы with*. Это аналог setDefaultScope, но не на глобальном уровне, а на пользовательском – вызов при необходимости. После описания метода в сущности можно вызывать его в конструкторе запросов:

    class UserTable
    {
    	public static function withActive(Query $query)
    		{
    		$query->where('ACTIVE', true);
    		}
    }
        
    $activeUsers = UserTable::query()
    	->withActive()	
    	->fetchCollection();
    
    // WHERE `ACTIVE`='Y'
    

    В качестве аргумента используется объект Bitrix\Main\ORM\Query\Query, поэтому можно задавать не только фильтр, но и любые другие параметры запроса. Кроме того, можно дополнить метод своими аргументами, которые также будут переданы при вызове из конструктора запросов:

    class UserTable
    {
    	public static function withActive(Query $query, $value)
    	{
    	$query
    		->addSelect('LOGIN')
    		->where('ACTIVE', $value);	
    	}
    }
        
    $activeUsers = UserTable::query()
    	->withActive(false)
    	->fetchCollection();
            
    // SELECT `LOGIN` ... WHERE `ACTIVE`='N'
    



    Выбор данных из хранимых процедур вместо таблиц

    ORM подходит даже для таких экзотических запросов, как выборка данных не из таблицы, а из хранимых процедур. Такие процедуры могут быть созданы в MSSQL-базе данных.

    Укажем название функции в методе getTableName:

    public static function getTableName()
    {
    	// return "foo_table_name"
    	return "foo_table_procedure()";
    }

    В этом виде такой код работать не будет. Дело в том, что при использовании подключения Bitrix\Main\DB\MssqlConnection все вхождения имен таблиц проходят через экранирование. Попытка сразу выполнить такой запрос приведет к выбрасыванию исключения:

    MS Sql query error: Invalid object name 'foo_table_procedure()'. (400)
    
    SELECT
    
    [base].[bar] AS [BAR],
    
    [base].[baz] AS [BAZ],
    
    FROM [foo_table_procedure()] [base]

    Получению нужного результата мешают только знаки [ и ], которыми MssqlSqlHelper защитил имя "таблицы". Проблема решается созданием собственного подключения Connection и SqlHelper.

    Вариант решения: на сервер установите расширение mssql и реализуйте следующую архитектуру:

    class MssqlSqlHelper extends Bitrix\Main\DB\SqlHelper
    {
    	public function quote($identifier)
    	{
    		if (self::isKnowFunctionalCall($identifier))
    		{
    			return $identifier
    		}
    			else
    		{
    			return parent::quote($identifier);
    		}
    	}
    }
    

    Где self::isKnownFunctionCall - метод проверки, который возвращает true, если в $identifier находится “foo_table_procedure()”.

    Примечание: Пример разработан компанией Интерволга.


    Выборки в отношениях 1:N и N:M

    При работе с отношениями 1:N и N:M в общем случае и с множественными свойствами инфоблока в частности можно столкнуться с двумя проблемами.

    Логика LIMIT

    Интуитивное ожидание логики работы LIMIT:

       $iblockEntity = IblockTable::compileEntity(…);
       
    	$query = $iblockEntity->getDataClass()::query()
    		->addSelect('NAME')
    		->addSelect('MULTI_PROP_1')
    	->setLimit(5);
    
    	$elements = $query->fetchCollection();

    Ожидать в данном примере выборку 5 элементов будет ошибкой. Лимит указывается не на уровне объектов, а на уровне SQL запроса:

    SELECT ... FROM `b_iblock_element`
    	LEFT JOIN `b_iblock_element_property` ...
    LIMIT 5

    Фактически будет выбрано 5 значений свойств с соответствующими элементами. Поэтому в выборке может оказаться менее 5 элементов, или вовсе 1 элемент с не полностью выбранными значениями свойства.

    Выбор полей соотношений в одном запросе

    Если выбирать несколько полей отношений в одном запросе, то результатом будет декартово произведение всех записей. Например:

    $iblockEntity = IblockTable::compileEntity(…);
        
    $query = $iblockEntity->getDataClass()::query()
    	->addSelect('NAME')
    	->addSelect('MULTI_PROP_1')
    	->addSelect('MULTI_PROP_2')
    	->addSelect('MULTI_PROP_3');
        
    $elements = $query->fetchCollection();

    Выполнится запрос вида:

    SELECT ... FROM `b_iblock_element`
    	LEFT JOIN `b_iblock_element_property` ... // 15 значений свойства
    	LEFT JOIN `b_iblock_element_property` ... // 7 значений свойства
    	LEFT JOIN `b_iblock_element_property` ... // 11 значений свойства

    И если интуитивно кажется, что будет выбрано 15 + 7 + 11 = 33 строки, то фактически будет выбрано 15 * 7 * 11 = 1155 строк. Если свойств или значений в запросе еще больше, то счет может идти на миллионы результирующих записей, и как следствие - о нехватке памяти в приложении для получения всего результата.

    Решение проблем

    Для обхода этих проблем был добавлен класс Bitrix\Main\ORM\Query\QueryHelper с универсальным методом decompose:

    /**
    ** Декомпозиция запросов с 1:N и N:M отношениями
    ** 
    ** @param Query $query
    ** @param bool $fairLimit При установке этой опции сначала выбираются ID объектов, а следующим запросом остальные данные с фильтром по ID
    ** @param bool $separateRelations При установке этой опции каждое 1:N или N:M отношение выбирается отдельным запросом
    ** @return Collection
    **/
    public static function decompose(Query $query, $fairLimit = true, $separateRelations = true)

    Параметр fairLimit приводит к двум запросам: сначала выбирается primary записей с заданным Limit / Offset в запросе, и после этого для всех primary одним запросом выбираются все отношения.

    Дополнительный параметр separateRelations позволяет выполнить отдельный запрос на каждое отношение, чтобы не возникало декартова произведения всех записей.

    В качестве результата будет возвращена готовая коллекция объектов с уже объединенными данными.

    Сортировка применяется при первичном выборе primary и в дальнейшем приводится в соответствии с этими primary. Что касается объектов отношений внутри объектов верхнего уровня, то там сортировка как правило не настолько важна, как при работе с массивами.


    Взаимосвязи между сущностями (устаревший вариант)

    Примечание: в уроке описан вариант отношений до версии 18.0.4 модуля main. Новый вариант приведен в главе Отношения.

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

  • отношения один к одному
  • один ко многим 1:N
  • много ко многим M:N
  • Отношения 1:1

    Самый простой тип связи - это когда элемент сущности ссылается на один элемент другой или этой же сущности. В описываемых примерах рассматривается каталог книг, но до сих пор в записях книг не было какого-либо указания на авторство. Реализуем это: прежде всего, опишем сущность Автора книги:

    <?php
    
    namespace SomePartner\MyBooksCatalog;
    
    use Bitrix\Main\Entity;
    
    class AuthorTable extends Entity\DataManager
    {
    	public static function getTableName()
    	{
    		return 'my_book_author';
    	}
    
    	public static function getMap()
    	{
    		return array(
    			new Entity\IntegerField('ID', array(
    				'primary' => true,
    				'autocomplete' => true
    			)),
    			new Entity\StringField('NAME'),
    			new Entity\StringField('LAST_NAME')
    		);
    	}
    }

    В данном примере считается, что у книги есть только один автор - это человек с именем и фамилией, запись о котором есть в сущности Author. При этом у одного автора может быть много книг. Таким образом, получается отношение "1 автор : N книг".

    Описать такое отношение можно следующим образом:

    class BookTable extends Entity\DataManager
    {
    	...
    	public static function getMap()
    	{
    		return array(
    			...
    			new Entity\IntegerField('AUTHOR_ID'),
    			new Entity\ReferenceField(
    				'AUTHOR',
    				'SomePartner\MyBooksCatalog\Author',
    				array('=this.AUTHOR_ID' => 'ref.ID'),
    			)
    		);
    	}
    	...
    }

    В первую очередь нужно добавить числовое поле AUTHOR_ID, в котором будет храниться ID автора. На основе этого поля конфигурируется связь между сущностями через новый тип поля - ReferenceField. Это виртуальное поле, не имеющее фактического отражения в БД:

    new Entity\ReferenceField(
    	'AUTHOR',
    	'SomePartner\MyBooksCatalog\Author',
    	array('=this.AUTHOR_ID' => 'ref.ID'),
    	array('join_type' => 'LEFT')
    )
    // LEFT JOIN my_book_author ON my_book.AUTHOR_ID = my_book_author.ID
    ПараметрыОписание
    Первый задается имя поля
    Второй название сущности-партнера, с которым формируется отношение
    Третий описывает, по каким полям связаны сущности, и задается в формате, похожем на фильтр для секции `select` в getList. Ключами и значениями являются имена полей с префиксами:
    • this. - поле текущей сущности,
    • ref. - поле сущности-партнера.
    Четвёртый, дополнительный можно указать тип подключения таблицы `join_type` - LEFT (по умолчанию), RIGHT, INNER.

    Теперь можно воспользоваться описанным отношением при выборке данных:

    BookTable::getList(array(
    	'select' => array('TITLE', 'AUTHOR.NAME', 'AUTHOR.LAST_NAME')
    ));
    
    SELECT 
    	`somepartner_mybookscatalog_book`.`TITLE` AS `TITLE`,
    	`somepartner_mybookscatalog_author`.`NAME` AS `SOMEPARTNER_MYBOOKSCATALOG_AUTHOR_NAME`,
    	`somepartner_mybookscatalog_author`.`LAST_NAME` AS `SOMEPARTNER_MYBOOKSCATALOG_AUTHOR_LAST_NAME`
    FROM `my_book`
    LEFT JOIN `my_book_author` `somepartner_mybookscatalog_author` ON `somepartner_mybookscatalog_book`.`AUTHOR_ID` = `somepartner_mybookscatalog_author`.`ID`

    Переход в сущность Автор осуществляется записью "AUTHOR." - указывается имя Reference поля, и после точки контекст переключатся на данную сущность. Далее можно указать имя поля из этой сущности, в том числе Reference, перейдя таким образом еще дальше и образовав новое подключение таблицы:

    'select' => array('AUTHOR.CITY.COUNTRY.NAME')

    Примерно так выглядел бы запрос по выбору страны проживания автора книги, если бы была структура Страны > Города > Живущие в городах авторы книг.

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

    BookTable::getList(array(
    	'select' => array(
    		'TITLE',
    		'AUTHOR_NAME' => 'AUTHOR.NAME',
    		'AUTHOR_LAST_NAME' => 'AUTHOR.LAST_NAME'
    	)
    ));
    SELECT 
    	`somepartner_mybookscatalog_book`.`TITLE` AS `TITLE`,
    	`somepartner_mybookscatalog_author`.`NAME` AS `AUTHOR_NAME`,
    	`somepartner_mybookscatalog_author`.`LAST_NAME` AS `AUTHOR_LAST_NAME`
    FROM ...

    И, аналогично исходной сущности, можно использовать символ '*' для выборки всех скалярных полей сущности, по желанию так же используя сокращение алиасов:

    BookTable::getList(array(
    	'select' => array(
    		'TITLE',
    		'AR_' => 'AUTHOR.*'
    	)
    ));
    SELECT 
    	`somepartner_mybookscatalog_author`.`ID` AS `AR_ID`,
    	`somepartner_mybookscatalog_author`.`NAME` AS `AR_NAME`,
    	`somepartner_mybookscatalog_author`.`LAST_NAME` AS `AR_LAST_NAME`
    FROM `my_book` `somepartner_mybookscatalog_book` 
    LEFT JOIN `my_book_author` `somepartner_mybookscatalog_author` ON `somepartner_mybookscatalog_book`.`AUTHOR_ID` = `somepartner_mybookscatalog_author`.`ID`

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

    $author_type = 5;
    
    new Entity\ReferenceField(
    	'AUTHOR',
    	'SomePartner\MyBooksCatalog\Author',
    	array(
    		'=this.AUTHOR_ID' => 'ref.ID',
    		'=ref.TYPE' => new DB\SqlExpression('?i', $author_type)
    	)
    )
    // LEFT JOIN my_book_author ON my_book.AUTHOR_ID = my_book_author.ID AND my_book_author.TYPE = 5

    Поле ReferenceField, как и другие поля, можно описывать при выборке данных в секции `runtime` и использовать для подключения других сущностей, с которыми изначально отношения не описаны.

    При частом использовании поля соседней сущности можно воспользоваться ExpressionField и определить удаленное поле как локальное:

    new Entity\ExpressionField('AUTHOR_NAME', '%s', 'AUTHOR.NAME')

    В данном примере разница неочевидна, но, если вместо AUTHOR.NAME у вас будет более длинная цепочка переходов, то использовать одно короткое имя окажется удобным.


    Отношение 1:N или обратный Reference

    Концепция ReferenceField подразумевает, что это поле нужно размещать в сущности N отношения 1:N. То есть, Reference должен указывать строго на одну запись: в примере выше принято, что у книги может быть только 1 автор, и таким образом ReferenceField в сущности Книг указывает на одну запись сущности Автор.

    Как выбрать автора книги понятно, поскольку в сущности Книги есть указание на связь с сущностью автора. Но как выбрать все книги автора, если в сущности Автора нет явного указания на Книги?

    Дело в том, что описанного Reference в сущности книги уже достаточно для двусторонней выборки, нужно лишь использовать специальный синтаксис:

    \SomePartner\MyBooksCatalog\AuthorTable::getList(array(
    	'select' => array(
    		'NAME',
    		'LAST_NAME',
    		'BOOK_TITLE' => '\SomePartner\MyBooksCatalog\BookTable:AUTHOR.TITLE'
    	)
    ));
    SELECT 
    	`somepartner_mybookscatalog_author`.`NAME` AS `NAME`,
    	`somepartner_mybookscatalog_author`.`LAST_NAME` AS `LAST_NAME`,
    	`somepartner_mybookscatalog_author_book_author`.`TITLE` AS `BOOK_TITLE`
    FROM `my_book_author` `somepartner_mybookscatalog_author` 
    LEFT JOIN `my_book` `somepartner_mybookscatalog_author_book_author` ON `somepartner_mybookscatalog_author_book_author`.`AUTHOR_ID` = `somepartner_mybookscatalog_author`.`ID`

    Вместо имени Reference, нужно указать "Название сущности, у которой есть Reference на текущую сущность":"Имя референса на текущую сущность". После такой конструкции контекст переключается на сущность Книга, и в ней уже можно выбрать поле TITLE и другие поля.


    Отношения M:N

    Любую книгу можно охарактеризовать с точки зрения жанра/категории, будь то деловая литература или художественная, исторические книги, обучающие и т.п., триллеры, комедии, драмы и т.п., книги по маркетингу, разработке, продажам и т.п. Для упрощения, назовем все это тегами и присвоим каждой книге несколько тегов.

    Описываем сущность тегов:

    <?php
    
    namespace SomePartner\MyBooksCatalog;
    
    use Bitrix\Main\Entity;
    
    class TagTable extends Entity\DataManager
    {
    	public static function getTableName()
    	{
    		return 'my_book_tag';
    	}
    
    	public static function getMap()
    	{
    		return array(
    			new Entity\IntegerField('ID', array(
    				'primary' => true,
    				'autocomplete' => true
    			)),
    			new Entity\StringField('NAME')
    		);
    	}
    }

    Чтобы связать эту сущность с Книгами по принципу N:M (у одной книги может быть несколько тегов, один тег может быть связан с несколькими книгами), необходимо создать промежуточную сущность с таблицей в БД, где будут храниться данные о связях книг с тегами.

    <?php
    
    namespace SomePartner\MyBooksCatalog;
    
    use Bitrix\Main\Entity;
    
    class BookTagTable extends Entity\DataManager
    {
    	public static function getTableName()
    	{
    		return 'my_book_to_tag';
    	}
    
    	public static function getMap()
    	{
    		return array(
    			new Entity\IntegerField('BOOK_ID', array(
    				'primary' => true
    			)),
    			new Entity\ReferenceField(
    				'BOOK',
    				'SomePartner\MyBooksCatalog\Book',
    				array('=this.BOOK_ID' => 'ref.ID')
    			),
    			new Entity\IntegerField('TAG_ID', array(
    				'primary' => true
    			)),
    			new Entity\ReferenceField(
    				'TAG',
    				'SomePartner\MyBooksCatalog\Tag',
    				array('=this.TAG_ID' => 'ref.ID')
    			)
    		);
    	}
    }

    Все, что в данном случае необходимо от этой сущности, это хранить связь "ID книги - ID тега", а соответствующие ReferenceField помогут программно описать данную связь.

    Сама по себе промежуточная сущность может быть неинтересна, типовые задачи в данном случае - это получить список тегов для книги или получить список книг по тегу. Решаются с помощью описанных выше методов работы с Reference:

    // теги для книги с ID = 5
    \SomePartner\MyBooksCatalog\BookTable::getList(array(
    	'filter' => array('=ID' => 5),
    	'select' => array(
    		'ID',
    		'TITLE',
    		'TAG_NAME' => 'SomePartner\MyBooksCatalog\BookTag:BOOK.TAG.NAME'
    	)
    ));
    SELECT 
    	`somepartner_mybookscatalog_book`.`ID` AS `ID`,
    	`somepartner_mybookscatalog_book`.`TITLE` AS `TITLE`,
    	`somepartner_mybookscatalog_book_book_tag_book_tag`.`NAME` AS `TAG_NAME`
    FROM `my_book` `somepartner_mybookscatalog_book` 
    LEFT JOIN `my_book_to_tag` `somepartner_mybookscatalog_book_book_tag_book` ON `somepartner_mybookscatalog_book_book_tag_book`.`BOOK_ID` = `somepartner_mybookscatalog_book`.`ID`
    LEFT JOIN `my_book_tag` `somepartner_mybookscatalog_book_book_tag_book_tag` ON `somepartner_mybookscatalog_book_book_tag_book`.`TAG_ID` = `somepartner_mybookscatalog_book_book_tag_book_tag`.`ID`
    WHERE `somepartner_mybookscatalog_book`.`ID` = 5

    Запись `SomePartner\MyBooksCatalog\BookTag:BOOK.TAG.NAME` может показаться сложной, но при рассмотрении ее поэтапно все должно проясниться:

    ПараметрыОписание
    BookTable::getList исходная сущность - BookTable
    SomePartner\MyBooksCatalog\BookTag:BOOK переход в сущность BookTag через ее референс BOOK, текущая сущность - BookTag
    TAG переход по референсу TAG из BookTag, текущая сущность - Tag
    NAME поле из текущей сущности Tag

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

    // книги для тега с ID = 11
    \SomePartner\MyBooksCatalog\TagTable::getList(array(
    	'filter' => array('=ID' => 11),
    	'select' => array(
    		'ID',
    		'NAME',
    		'BOOK_TITLE' => 'SomePartner\MyBooksCatalog\BookTag:TAG.BOOK.TITLE'
    	)
    ));

    Таким образом, используя два вида перехода между сущностями - REFERENCE и Entity:REFERENCE - можно достаточно легко добраться до нужной связанной информации.


    Фильтр ORM

    В обновлении main 17.5.2 в ORM появился новый фильтр.

    Одиночные условия

    Пример простейшего запроса:

    \Bitrix\Main\UserTable::query()
    	->where("ID", 1)
    	->exec();
    
    // WHERE `main_user`.`ID` = 1

    Если нужен другой оператор сравнения, то он указывается явно:

    \Bitrix\Main\UserTable::query()
    	->where("ID", "<", 10)
    	->exec();
    
    // WHERE `main_user`.`ID` < 10

    Пример с использованием IS NULL:

    \Bitrix\Main\UserTable::query()
    	->whereNull("ID")
    	->exec();
    
    // WHERE `main_user`.`ID` IS NULL

    Для всех where* методов есть whereNot* аналоги. Пример:

    \Bitrix\Main\UserTable::query()
    	->whereNotNull("ID")
    	->exec();
    
    // WHERE `main_user`.`ID` IS NOT NULL

    Помимо общего where, можно использовать следующие операторные методы:

    whereNull($column) / whereNotNull($column)
    
    whereIn($column, $values|Query|SqlExpression) / whereNotIn($column, $values|Query|SqlExpression)
    
    whereBetween($column, $valueMin, $valueMax) / whereNotBetween($column, $valueMin, $valueMax)
    
    whereLike($column, $value) / whereNotLike($column, $value)
    
    whereExists($query|SqlExpression) / whereNotExists($query|SqlExpression)

    Для произвольного выражения с привязкой к полям сущности следует использовать метод [dw]whereExpr[/dw][di]Доступно с версии 20.400.0 модуля main.[/di]:

    \Bitrix\Main\UserTable::query()
    	->whereExpr('JSON_CONTAINS(%s, 4)', ['SOME_JSON_FIELD'])
    	->exec();
    
    // WHERE JSON_CONTAINS(`main_user`.`SOME_JSON_FIELD`, 4)
    

    Аргументы выражения и составных полей аналогичны конструктору поля ExpressionField и подставляются через функцию [ds]sprintf[/ds][di]sprintf — Возвращает отформатированную строку.

    Подробнее в документации по PHP.[/di].

    Список операторов находится в \Bitrix\Main\ORM\Query\Filter\Operator::$operators (см. ключи массива):

    = , <> , != , < , <= , > , >= , in , between , like , exists

    Сравнение с другим полем

    Отдельный метод whereColumn упрощает сравнение полей друг с другом:

    \Bitrix\Main\UserTable::query()
    	->whereColumn('NAME', 'LOGIN')
    	->exec();
    
    // WHERE `main_user`.`NAME` = `main_user`.`LOGIN`

    Этот метод мало чем отличается от where, и формально это тот же самый вызов с небольшой оберткой:

    \Bitrix\Main\UserTable::query()
    	->where('NAME', new Query\Filter\Expression\Column('LOGIN'))
    	->exec();
    
    // WHERE `main_user`.`NAME` = `main_user`.`LOGIN`

    whereColumn обеспечивает особую гибкость использования колонок в фильтре, например:

    \Bitrix\Main\UserTable::query()
    	->whereIn('LOGIN', [
    		new Column('NAME'),
    		new Column('LAST_NAME')
    	])
    	->exec();
    
    // WHERE `main_user`.`LOGIN` IN (`main_user`.`NAME`, `main_user`.`LAST_NAME`)

    Колонки можно использовать в любом операторе. И они будут восприняты именно как поля конкретных сущностей, а не просто произвольное SQL выражение.

    Множественные условия

    Для нескольких условий предполагается такая запись:

    \Bitrix\Main\UserTable::query()
    	->where('ID', '>', 1)
    	->where('ACTIVE', true)
    	->whereNotNull('PERSONAL_BIRTHDAY')
    	->whereLike('NAME', 'A%')
    	->exec();
    
    // WHERE `main_user`.`ID` > 1 AND `main_user`.`ACTIVE` = 'Y' AND `main_user`.`PERSONAL_BIRTHDAY` IS NOT NULL AND `main_user`.`NAME` LIKE 'A%'

    Примечание: для boolean полей со значениями Y/N, 1/0 и т.п. можно использовать true и false.

    Если необходимо указать несколько условий в одном вызове, то использовать такой формат: (операторные методы можно заменять кодами операторов)

    \Bitrix\Main\UserTable::query()
    	->where([
    		['ID', '>', 1],
    		['ACTIVE', true],
    		['PERSONAL_BIRTHDAY', '<>', null],
    		['NAME', 'like', 'A%']
    	])
    	->exec();
    
    // WHERE `main_user`.`ID` > 1 AND `main_user`.`ACTIVE` = 'Y' AND `main_user`.`PERSONAL_BIRTHDAY` IS NOT NULL AND `main_user`.`NAME` LIKE 'A%'

    OR и вложенные фильтры

    Для хранения всех условий фильтра в Query используется контейнер условий \Bitrix\Main\Entity\Query\Filter\ConditionTree. Помимо стандартных условий, в него допускается добавление других экземпляров ConditionTree, таким образом создавая любой уровень ветвления и вложенности.

    Все приведенные выше вызовы where - проксирование к базовому контейнеру. Следующие два вызова приведут к совершенно одинаковому результату:

    \Bitrix\Main\UserTable::query()
    	->where([
    		['ID', '>', 1],
    		['ACTIVE', true]
    	])
    	->exec();
    
    \Bitrix\Main\UserTable::query()
    	->where(Query::filter()->where([
    		["ID", '>', 1],
    		['ACTIVE', true]
    	]))->exec();
    
    // WHERE `main_user`.`ID` > 1 AND `main_user`.`ACTIVE` = 'Y'

    Вместо массива использовался объект фильтра. Это позволяет создавать субфильтры и менять логику с AND на OR:

    \Bitrix\Main\UserTable::query()
    	->where('ACTIVE', true)
    	->where(Query::filter()
    		->logic('or')
    		->where([
    			['ID', 1],
    			['LOGIN', 'admin']
    		])
    	)->exec();
    
    // WHERE `main_user`.`ACTIVE` = 'Y' AND (`main_user`.`ID` = 1 OR `main_user`.`LOGIN` = 'admin')

    Допускается использование цепочки вызовов:

    \Bitrix\Main\UserTable::query()
    	->where('ACTIVE', true)
    	->where(Query::filter()
    		->logic('or')
    		->where('ID', 1)
    		->where('LOGIN', 'admin')
    	)
    	->exec();
    
    // WHERE `main_user`.`ACTIVE` = 'Y' AND (`main_user`.`ID` = 1 OR `main_user`.`LOGIN` = 'admin')

    Выражения

    В фильтре в качестве имен полей допустимо задание ExpressionField, которые автоматически регистрируются как runtime поля сущности.

    \Bitrix\Main\UserTable::query()
    	->where(new ExpressionField('LNG', 'LENGTH(%s)', 'LAST_NAME'), '>', 10)
    	->exec();
    
    // WHERE LENGTH(`main_user`.`LAST_NAME`) > '10'

    Для упрощения подобных конструкций добавлен хелпер, строящий вычисляемые поля:

    \Bitrix\Main\UserTable::query()
    	->where(Query::expr()->length("LAST_NAME"), '>', 10)
    	->exec();
    
    // WHERE LENGTH(`main_user`.`LAST_NAME`) > '10'
    
    \Bitrix\Main\UserTable::query()
    	->addSelect(Query::expr()->count("ID"), 'CNT')
    	->exec();
    
    // SELECT COUNT(`main_user`.`ID`) AS `CNT` FROM `b_user` `main_user`

    В хелпере заложены наиболее популярные SQL выражения:

    • count
    • countDistinct
    • sum
    • min
    • avg
    • max
    • length
    • lower
    • upper
    • concat

    Совместимость с getList

    Если вместо цепочки вызовов Query использовать getList, то фильтр вставляется в него вместо массива:

    \Bitrix\Main\UserTable::getList([
    	'filter' => ['=ID' => 1]
    ]);
    
    \Bitrix\Main\UserTable::getList([
    	'filter' => Query::filter()
    		->where('ID', 1)
    ]);
    
    // WHERE `main_user`.`ID` = 1

    Условия JOIN

    Описания референсов представлено в таком формате:

    new Entity\ReferenceField('GROUP', GroupTable::class,
    	Join::on('this.GROUP_ID', 'ref.ID')
    )

    Метод `on` - короткая и более семантически уместная запись Query::filter() с предустановленным условием по колонкам. Возвращает инстанс фильтра, и можно строить какие угодно условия JOIN:

    new Entity\ReferenceField('GROUP', GroupTable::class,
    	Join::on('this.GROUP_ID', 'ref.ID')
    		->where('ref.TYPE', 'admin')
    		->whereIn('ref.OPTION', [
    			new Column('this.OPTION1'),
    			new Column('this.OPTION2'),
    			new Column('this.OPTION3')
    		]
    )
    

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

    ->whereColumn('this.AUTHOR.UserGroup:USER.GROUP.OWNER.ID', 'ref.ID');

    Формат массива

    Для использования фильтра в виде массива существует метод конвертации из массива в объект \Bitrix\Main\ORM\Query\Filter\ConditionTree::createFromArray. Формат массива имеет общий вид:

    $filter = [
    	['FIELD', '>', 2],
    	[
    		'logic' => 'or',
    		['FIELD', '<', 8],
    		['SOME', 9]
    	],
    	['FIELD', 'in', [5, 7, 11]],
    	['FIELD', '=', ['column' => 'FIELD2']],
    	['FIELD', 'in', [
    		['column' => 'FIELD1'],
    		['value' => 'FIELD2'],
    		['FIELD3']
    	],
    	[
    		'negative' => true,
    		['FIELD', '>', 19]
    	],
     ];
    

    При обычном сравнении значение передается либо непосредственно:

    ['FIELD', '>', 2]
    

    либо в виде массива с ключом value:

    ['FIELD', '>', ['value' => 2]]
    

    При сравнении с колонкой следует использовать массив с ключом column:

    ['FIELD1', '>', ['column' => 'FIELD2']]
    

    Вложенные фильтры передаются в качестве аналогичных вложенных массивов. В качестве замены методов объекта для отрицания negative() и изменения логики logic() используются одноименные ключи:

    $filter = [
    	['FIELD', '>', 2],
    	[
    		'logic' => 'or',
    		['FIELD', '<', 8],
    		['SOME', 9]
    	],
    	[
    		'negative' => true,
    		['FIELD', '>', 19]
    	]
    ]
    

    Остальные методы where* заменяются соответствующими операторами сравнения in, between, like и т.д.

    ['FIELD', 'in', [5, 7, 11]]
    

    Внимание: Будьте внимательны при использовании массивов. Не подставляйте сырые, переданные пользователем, данные в качестве фильтра, так как в них могут содержаться опасные условия для раскрытия данных БД. Проверяйте все входящие условия через белый список полей.



    Автоматическая генерация ORM-классов

    Автоматическая генерация

    Для использования генератора ORM классов перейдите на страницу Настройки > Настройки продукта > Настройки модулей > Монитор производительности (модуль Монитор производительности (perfmon) должен быть установлен) и на вкладке [dw]Генератор таблетов[/dw][di]Вкладка доступна с версии 20.0.100 модуля Монитор производительности (perfmon).

    [/di] отметьте поле Разрешить генерацию таблетов для ORM.

    У генератора имеются следующие параметры:

    ПараметрОписание
    Использовать короткие алиасы классовПри включенной опции вызовы классов будут вида:
    new IntegerField('ИМЯ_ПОЛЯ')
    new LengthValidator(null, 255)
    new Reference()
    

    При выключенной:

    new Fields\IntegerField('ИМЯ_ПОЛЯ')
    new Fields\Validators\LengthValidator(null, 255)
    new Fields\Relations\Reference()
    
    Описание параметров полей таблета через методыПри включенной опции практически все параметры поля задаются через вызовы соответствующих методов:
    (new StringField('API_CODE'))->configureSize(50)
    

    При выключенной:

    new StringField('API_CODE', ['size' => 50])
    
    Имена полей в картеПри включенной опции метод getMap таблета будет возвращать массив, где индексами являются имена полей. При выключенной - нумерованный массив.

    Использование генератора ORM классов до версии perfmon 20.0.100

    После включения генератора таблетов на странице Настройки > Производительность > Таблицы в меню действий станет доступен пункт ORM.

    Примечание: Имена файлов таблетов предлагаются с суффиксом table (Например: было product.php, стало - producttable.php).

    Примечание: Описания полей создаются в виде создания объектов-наследников \Bitrix\Main\ORM\Data\Field (а не в виде ассоциативных массивов как было до версии 20.0.100 модуля perfmon )

    Для полей типа timestamp / date / datetime анализируется дефолтное значение (sql). Если это функция, возвращающая текущее время, то в таблете создается функция для получения текущего времени/даты:

    'default' => function()
    {
    	return new DateTime();
    }
    

    Другие особенности для полей:

    • Поле помечается как mandatory только в случае, когда поле является обязательным и не имеет значения по-умолчанию;
    • Поля типа text / mediumtext не помечаются как строка.

      Пример

    Пример автоматического создания ORM-класса

    Рассмотрим пример автоматического создания ORM-класса для штатной таблицы шаблонов сайта b_site_template. Воспользуемся пунктом ORM и получим следующий код:

    <?php
    namespace Bitrix\Site;
    
    use Bitrix\Main\Localization\Loc,
    	Bitrix\Main\ORM\Data\DataManager,
    	Bitrix\Main\ORM\Fields\IntegerField,
    	Bitrix\Main\ORM\Fields\StringField,
    	Bitrix\Main\ORM\Fields\Validators\LengthValidator;
    
    Loc::loadMessages(__FILE__);
    
    /**
     * Class TemplateTable
     * 
     * Fields:
     * <ul>
     * <li> ID int mandatory
     * <li> SITE_ID string(2) mandatory
     * <li> CONDITION string(255) optional
     * <li> SORT int optional default 500
     * <li> TEMPLATE string(255) mandatory
     * </ul>
     *
     * @package Bitrix\Site
     **/
    
    class TemplateTable extends DataManager
    {
    	/**
    	 * Returns DB table name for entity.
    	 *
    	 * @return string
    	 */
    	public static function getTableName()
    	{
    		return 'b_site_template';
    	}
    
    	/**
    	 * Returns entity map definition.
    	 *
    	 * @return array
    	 */
    	public static function getMap()
    	{
    		return [
    			new IntegerField(
    				'ID',
    				[
    					'primary' => true,
    					'autocomplete' => true,
    					'title' => Loc::getMessage('TEMPLATE_ENTITY_ID_FIELD')
    				]
    			),
    			new StringField(
    				'SITE_ID',
    				[
    					'required' => true,
    					'validation' => [__CLASS__, 'validateSiteId'],
    					'title' => Loc::getMessage('TEMPLATE_ENTITY_SITE_ID_FIELD')
    				]
    			),
    			new StringField(
    				'CONDITION',
    				[
    					'validation' => [__CLASS__, 'validateCondition'],
    					'title' => Loc::getMessage('TEMPLATE_ENTITY_CONDITION_FIELD')
    				]
    			),
    			new IntegerField(
    				'SORT',
    				[
    					'default' => 500,
    					'title' => Loc::getMessage('TEMPLATE_ENTITY_SORT_FIELD')
    				]
    			),
    			new StringField(
    				'TEMPLATE',
    				[
    					'required' => true,
    					'validation' => [__CLASS__, 'validateTemplate'],
    					'title' => Loc::getMessage('TEMPLATE_ENTITY_TEMPLATE_FIELD')
    				]
    			),
    		];
    	}
    
    	/**
    	 * Returns validators for SITE_ID field.
    	 *
    	 * @return array
    	 */
    	public static function validateSiteId()
    	{
    		return [
    			new LengthValidator(null, 2),
    		];
    	}
    
    	/**
    	 * Returns validators for CONDITION field.
    	 *
    	 * @return array
    	 */
    	public static function validateCondition()
    	{
    		return [
    			new LengthValidator(null, 255),
    		];
    	}
    
    	/**
    	 * Returns validators for TEMPLATE field.
    	 *
    	 * @return array
    	 */
    	public static function validateTemplate()
    	{
    		return [
    			new LengthValidator(null, 255),
    		];
    	}
    }
    File: /bitrix/modules/site/lang/ru/lib/templatetable.php
    <?
    $MESS["TEMPLATE_ENTITY_ID_FIELD"] = "";
    $MESS["TEMPLATE_ENTITY_SITE_ID_FIELD"] = "";
    $MESS["TEMPLATE_ENTITY_CONDITION_FIELD"] = "";
    $MESS["TEMPLATE_ENTITY_SORT_FIELD"] = "";
    $MESS["TEMPLATE_ENTITY_TEMPLATE_FIELD"] = "";
    ?>
    Таблицы базы данных

    Преобразовав этот результат нужным образом получим настоящий класс для работы с шаблонами, расположенный по пути /bitrix/modules/site/lib/templatetable.php.


    Использование ORM или почему поля из SELECT и ORDER BY автоматически попадают в GROUP BY

    При использовании ORM, а именно в процессе выборки данных, у разработчиков часто возникает вопрос: "Почему в GROUP BY автоматически попадают некоторые поля? Я не указывал этого явно в вызове getList/Query". В уроке будет рассмотрено, что это за явление, и почему так и должно быть.

    Коробочные продукты 1С-Битрикс ранее поддерживали работу с тремя СУБД: MySQL, Oracle, SQL Server. И если MySQL всем хорошо знаком и в интернет-проектах его выбирают чаще других, то Oracle и SQL Server - выбор более серьезных и масштабных проектов. Enterprise сегмент обязывает СУБД отвечать более высоким требованиям, в том числе соблюдать стандарты.


    Рассмотрим пошагово, как работает выборка данных в вышеперечисленных системах:

    SELECT t.* FROM a_city t

    Это обычная выборка всех данных из таблицы: у нас есть 7 населенных городов, привязанных к федеральным округам.


    Зададим условия выборки - только Центральный и Северо-Западный округа:

    SELECT NAME, REGION FROM a_city t WHERE t.REGION IN ('ЦФО','СЗФО') 

    Как видно, формат данных не изменился, произведена лишь фильтрация.


    Теперь сгруппируем получившуюся выборку - посчитаем, сколько городов в каждом округе:

    SELECT COUNT(*), t.REGION FROM a_city t WHERE t.REGION IN ('ЦФО','СЗФО') GROUP BY t.REGION 

    Предыдущая выборка была "схлопнута" по уникальным значениям REGION, и для каждого такого значения было подсчитано количество "схлопнувшихся записей":

    Группируем выборку SELECT NAME, REGION FROM a_city t WHERE t.REGION IN ('ЦФО','СЗФО') по региону.


    До этого момента ни у кого, как правило, вопросов не возникает.


    А теперь довольно распространенный случай - разработчик решает, что в сгруппированной выборке ему не хватает количества жителей из городов:

    SELECT COUNT(*), t.REGION, POPULATION FROM a_city t WHERE t.REGION IN ('ЦФО','СЗФО') GROUP BY t.REGION 

    Отлично, MySQL успешно отработал запрос и вернул жителей (на самом деле - нет), разработчик доволен (а зря). Задача, казалось бы, решена, но почему такой же запрос на Oracle выдаст ошибку

    ORA-00979: not a GROUP BY expression

    а SQL Server ответит

    Column 'a_city.POPULATION' is invalid in the select list because it is not contained in either an aggregate function or the GROUP BY clause.

    Давайте разберемся, что за числа вернул нам MySQL вместо ошибки:

    SELECT NAME, REGION, POPULATION FROM a_city t WHERE t.REGION IN ('ЦФО','СЗФО') 

    Именно так выглядела выборка перед тем, как началась группировка. Похоже, MySQL просто взял первые попавшиеся значения для каждого округа. Что это дает разработчику? Совершенно ничего - эти числа не имеют никакого смысла, их значения непредсказуемы.

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

    SELECT COUNT(*), t.REGION, SUM(POPULATION) FROM a_city t WHERE t.REGION IN ('ЦФО','СЗФО') GROUP BY t.REGION  

    или среднее количество жителей в городах для каждого региона:

    SELECT COUNT(*), t.REGION, ROUND(AVG(POPULATION),0) FROM a_city t WHERE t.REGION IN ('ЦФО','СЗФО') GROUP BY t.REGION  

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

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

    Возвращаясь к построителю запросов в Bitrix Framework - он следит за соблюдением данного правила и при обнаружении неагрегированных полей добавляет их в секцию GROUP BY.

    В приведенном примере с городами, в отсутствие явно указанной агрегации, запрос примет следующий вид:

    SELECT COUNT(*), t.REGION, t.POPULATION FROM a_city t WHERE t.REGION IN ('ЦФО','СЗФО') GROUP BY t.REGION, t.POPULATION  

    Результат показывает, сколько в регионе городов с определенным количеством жителей. Только так СУБД понимает, что от нее хочет разработчик.


    Вроде бы разобрались: нужно либо указывать агрегацию, либо группировать по полю, иначе его значение не имеет смысла. Вдруг разработчик решает добавить сортировку по ID, и вновь видит, как поле ID автоматически попадает в GROUP BY и "ломает" результат:

    SELECT COUNT(*), t.REGION, SUM(t.POPULATION) FROM a_city t WHERE t.REGION IN ('ЦФО','СЗФО') GROUP BY t.REGION, t.ID ORDER BY ID DESC  

    Как уже можно было догадаться, если не добавить ID в группировку, то Oracle и SQL Server вновь откажутся выполнять запрос, ссылаясь на неопределенность при агрегации данных. Что на этот раз?

    Все дело в том, что сортировка происходит уже после группировки/агрегации данных. Неважно, что поле ID есть в исходной таблице - после группировки мы получаем новую виртуальную таблицу с агрегированными данными.

    Получается, нужно добавить поле ID в промежуточный сгруппированный результат - только тогда сортировка по этому полю будет возможна. И тут мы возвращаемся к прежнему правилу, уточняя его:

    Важное правило: Если в выборке есть агрегация или группировка (агрегация по уникальному значению) хотя бы для одной колонки, то все остальные колонки из SELECT и ORDER BY должны быть так же агрегированы или сгруппированы.

    Следование этому правилу поможет вам более точно представлять, какие вычисления производит для вас база данных. Если же ему не следовать, в случае MySQL вы можете быть введены в заблуждение полученными результатами - вроде бы и похоже на правду, но данные не несут смысловой нагрузки.

    Примечание: На WHERE это правило не распространяется - данная фильтрация производится как раз ДО группировки, там нужны именно оригинальные значения колонок. Фильтрация по агрегированным значениям происходит в секции HAVING, и если там окажется колонка без агрегации - вновь для осмысленного результата будет необходима предварительная группировка значений этой колонки. При этом построитель запросов ORM сам позаботится о распределении фильтра в WHERE и HAVING - вам не нужно забивать этим голову, как и в случае с автоматической группировкой.


    Заключение

    Если в конкретном запросе автоматическое добавление полей в GROUP BY стало для вас неприятным сюрпризом, то:

    • Вы добавили поле в выборку по привычке или случайно, на самом деле его значение вам не нужно
    • или
    • Вы забыли указать агрегацию (MAX, MIN, SUM, AVG и т.д.)

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

    ORM в Bitrix Framework сама исправляет подобные неточности. В случае же прямых запросов отключить такое поведение и заставить MySQL следовать стандартам и здравому смыслу поможет настройка ONLY_FULL_GROUP_BY.

    Постраничная навигация

    Как реализовать постраничную навигацию в ядре D7 для выборок из ORM. (Доступно с версии 16.0.0)

    Принципы

    Постраничная навигация - вспомогательный объект пользовательского интерфейса. В старом ядре постраничка была в объекте результата запроса, а чтобы выбрать с лимитами, приходилось вызывать специальную функцию CDBResult::NavQuery(), в которую передавался текст запроса. В D7 постраничная навигация - это просто вспомогательный объект пользовательского интерфейса. Первичным считается запрос, а постраничка просто помогает подставить правильные limit и offset.

    Обязательность использования limit в запросах. Некорректно оставлять на долю php всю потенциально большую выборку без ограничений. Если выборка все-таки без лимитов, то возможно использование новой навигации, но разработчик это делает на свой страх и риск, без рекомендаций вендора. При этом необходимо решать вопросы использования разных БД и другие задачи.


    Для постраничной навигации нужно знать количество записей. Как и в старом ядре для этого необходимо сделать отдельный запрос с COUNT. На большом количестве записей это эффективнее полной выборки, а на маленьком - не имеет значения. Но если имеется не просто большая, а гигантская выборка, то постраничка на текущем ядре вполне справится и без COUNT.

    Инструменты для постраничной навигации

    • класс \Bitrix\Main\UI\PageNavigation для простой навигации;
    • класс \Bitrix\Main\UI\ReversePageNavigation для обратной навигации;
    • класс \Bitrix\Main\UI\AdminPageNavigation для навигации в админке;
    • компонент main.pagenavigation с шаблонами .default (основан на round старого компонента), admin (для админки), modern (для гридов).

    Постраничка поддерживает как параметры в GET, так и ЧПУ. Вместо глобальной переменной $NavNum теперь нужно явно указывать идентификатор навигации. Поддерживаются URL следующего вида:

    /page.php?nav-cars=page-5&nav-books=page-2&other=params
    /page.php?nav-cars=page-5-size-20&nav-books=page-2
    /page.php?nav-cars=page-all&nav-books=page-2
    /dir/nav-cars/page-2/size-20/
    /dir/nav-cars/page-all/?other=params
    /dir/nav-cars/page-5/nav-books/page-2/size-10

    где nav-cars и nav-books - идентификаторы двух разных постраничек на одной странице.

    Постраничная навигация в административном разделе

    $nav = new \Bitrix\Main\UI\AdminPageNavigation("nav-culture");
    
    $cultureList = CultureTable::getList(array(
    	'order' => array(strtoupper($by) => $order),
    	'count_total' => true,
    	'offset' => $nav->getOffset(),
    	'limit' => $nav->getLimit(),
    ));
    
    $nav->setRecordCount($cultureList->getCount());
    
    $adminList->setNavigation($nav, Loc::getMessage("PAGES"));
    
    while($culture = $cultureList->fetch())
    {
    }

    'count_total' => true - новый параметр, который заставляет ORM выполнить отдельный запрос COUNT, результат которого оказывается в объекте результата выборки и может быть получен через $cultureList->getCount(). Без этого параметра выполнять getCount() бесполезно и даже вредно (получите исключение).

    $nav->getOffset() возвращает позицию первой записи для текущей страницы навигации.
    $nav->getLimit() возвращает количество записей на странице или 0, если выбрано "все записи".
    $nav->setRecordCount() устанавливает количество записей для навигации.

    Для удобства в административном отделе добавлена функция $adminList->setNavigation(), которая просто подключает компонент main.pagenavigation с шаблоном admin.

    Прямая постраничная навигация в Публичном разделе

    <?
    $filter = array("=IBLOCK_ID"=>6);
    
    $nav = new \Bitrix\Main\UI\PageNavigation("nav-more-news");
    $nav->allowAllRecords(true)
    	->setPageSize(5)
    	->initFromUri();
    
    $newsList = \Bitrix\Iblock\ElementTable::getList(
    	array(
    		"filter" => $filter,
    		"count_total" => true,
    		"offset" => $nav->getOffset(),
    		"limit" => $nav->getLimit(),
    	)
    );
    
    $nav->setRecordCount($newsList->getCount());
    
    while($news = $newsList->fetch())
    {
    }
    ?>
    
    <?
    $APPLICATION->IncludeComponent(
    	"bitrix:main.pagenavigation",
    	"",
    	array(
    		"NAV_OBJECT" => $nav,
    		"SEF_MODE" => "Y",
    	),
    	false
    );
    ?>

    Добавилась инициализация навигации:

    $nav->allowAllRecords(true) ->setPageSize(5) ->initFromUri();

    В публичном разделе больше настроек типа "разрешить все записи", а это все нужно знать до того, как будет проинициализирован объект. (В административном разделе это уже переопределено классом-наследником.) Кроме того, необязательно получать текущую страницу из URL так как её можно взять где угодно и установить.

    У компонента есть важный параметр "SEF_MODE" => "Y", по которому он генерирует ЧПУ постраничной навигации.

    Обратная постраничная навигация в Публичном разделе

    <?
    $filter = array("=IBLOCK_ID"=>6);
    
    $cnt = \Bitrix\Iblock\ElementTable::getCount($filter);
    
    $nav = new \Bitrix\Main\UI\ReversePageNavigation("nav-news", $cnt);
    $nav->allowAllRecords(true)
    	->setPageSize(5)
    	->initFromUri();
    
    $newsList = \Bitrix\Iblock\ElementTable::getList(
    	array(
    		"filter" => $filter,
    		"offset" => $nav->getOffset(),
    		"limit" => $nav->getLimit(),
    	)
    );
    
    while($news = $newsList->fetch())
    {
    }
    ?>
    
    <?
    $APPLICATION->IncludeComponent(
    	"bitrix:main.pagenavigation",
    	"",
    	array(
    		"NAV_OBJECT" => $nav,
    		"SEF_MODE" => "Y",
    	),
    	false
    );
    ?>

    Здесь конструируется класс-наследник ReversePageNavigation и обязательно передаётся ему количество записей уже в конструкторе, т.к. обратная навигация математически не может работать без количества записей. Для этого в ORM обновился метод getCount(), теперь он принимает на вход параметр фильтра.

    Постраничная навигация в гриде

    <?
    $filter = array("=IBLOCK_ID"=>6);
    
    $nav = new \Bitrix\Main\UI\PageNavigation("nav-grid-news");
    $nav->allowAllRecords(true)
    	->setPageSize(5)
    	->initFromUri();
    
    $newsList = \Bitrix\Iblock\ElementTable::getList(
    	array(
    		"filter" => $filter,
    		"count_total" => true,
    		"offset" => $nav->getOffset(),
    		"limit" => $nav->getLimit(),
    	)
    );
    
    $rows = array();
    while($news = $newsList->fetch())
    {
    	$cols = array(
    		"ID" => $news["ID"],
    		"NAME" => $news["NAME"],
    	);
    
    	$rows[] = array(
    		"columns"=>$cols,
    	);
    }
    
    $nav->setRecordCount($newsList->getCount());
    ?>
    
    <?
    $APPLICATION->IncludeComponent(
    	"bitrix:main.interface.grid",
    	"",
    	array(
    		"GRID_ID"=>"news_grid",
    		"HEADERS"=>array(
    			array("id"=>"ID", "name"=>"ID", "default"=>true),
    			array("id"=>"NAME", "name"=>"Название", "default"=>true),
    		),
    		"ROWS"=>$rows,
    		"FOOTER"=>array(array("title"=>"Всего", "value"=>$newsList->getCount())),
    		"NAV_OBJECT"=>$nav,
    		"NAV_PARAMS"=>array(
    		"SEF_MODE" => "Y"
    		),
    	)
    );
    ?>

    "NAV_PARAMS"=>array( "SEF_MODE" => "Y" ) - новый параметр, который по сути передаёт параметры в компонент навигации.

    Постраничная навигация без COUNT

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

    <?
    $filter = array("=IBLOCK_ID"=>6);
    
    $nav = new \Bitrix\Main\UI\PageNavigation("nav-less-news");
    $nav->allowAllRecords(true)
    	->setPageSize(5)
    	->initFromUri();
    
    $newsList = \Bitrix\Iblock\ElementTable::getList(
    	array(
    		"filter" => $filter,
    		"offset" => ($offset = $nav->getOffset()),
    		"limit" => (($limit = $nav->getLimit()) > 0? $limit + 1 : 0),
    	)
    );
    
    $n = 0;
    while($news = $newsList->fetch())
    {
    	$n++;
    	if($limit > 0 && $n > $limit)
    	{
    		break;
    	}
    }
    
    $nav->setRecordCount($offset + $n);
    ?>
    
    <?
    $APPLICATION->IncludeComponent(
    	"bitrix:main.pagenavigation",
    	"",
    	array(
    		"NAV_OBJECT" => $nav,
    		"SEF_MODE" => "Y",
    		"SHOW_COUNT" => "N",
    	),
    	false
    );
    ?>

    Обратите внимание: выставляется "уже известное" количество записей: $nav->setRecordCount($offset + $n);

    Это делается для того, чтобы компонент навигации понял, что есть еще страницы. Но для шаблонов, которые выводят количество записей, мы передаем "SHOW_COUNT" => "N", чтобы они не пытались выводить неверную цифру (она будет верна только на последней странице).

    Внимание! Если осуществляется навигация без COUNT, то имеется в виду, что записей очень много. Поэтому не стоит разрешать все записи методом $nav->allowAllRecords(true).

    Файл с примерами.

    Интеграция ORM в информационных блоках

    С версии 19.0.0 модуля iblock добавлена поддержка ORM при работе с элементами инфоблоков.

    Обратная совместимость

    События старого ядра модуля инфоблоков не поддерживаются. Штатные возможности других модулей, опирающиеся на вызовы этих событий, недоступны.

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

      Для API работы с элементами (добавление, изменение, удаление)
    • ресайз изображений PREVIEW_PICTURE, DETAIL_PICTURE;
    • обновление фасетного индекса инфоблока (при условии использования);
    • обновление seo-параметров элемента;
    • сброс тегированного кеша;
    • установка прав доступа;
    • поддержка документооборота;
    • проверка дисковой квоты для файловых свойств;
    • пересчет доступности товаров с SKU;
    • пересчет цен для сортировки товаров с SKU;
    • индексация модулем поиска;
    • логирование операций с элементами.

      Для API работы с разделами (добавление, изменение, удаление).
    • автопересчет значений полей LEFT_MARGIN, RIGHT_MARGIN, GLOBAL_ACTIVE, DEPTH_LEVEL;
    • ресайз изображений PREVIEW_PICTURE, DETAIL_PICTURE;
    • обновление фасетного индекса инфоблока (при условии использования);
    • обновление seo-параметров раздела и дочерних сущностей (подразделов и элементов);
    • сброс тегированного кеша;
    • установка прав доступа на раздел и дочерние сущности;
    • проверка дисковой квоты для файловых полей;
    • индексация модулем поиска;
    • привязка свойств к разделам;
    • логирование операций с разделами.

    Внимание! Перечисленный функционал необходимо реализовывать самостоятельно.

    Список ссылок по теме:

    • [ds]REST ORM API для инфоблоков[/ds][di] REST API для инфоблоков доступен с версии 20.5.0 модуля Информационные блоки.

      Для доступа к данным инфоблоков через REST за основу взята интеграция с ORM. А именно концепция, когда один инфоблок - это одна самостоятельная сущность ORM, а элемент инфоблока - это запись (объект) сущности.

      Подробнее...[/di]

    Концепция и архитектура

    Каждый инфоблок является самостоятельным типом данных со своим собственным набором свойств. В ORM он представляется отдельной сущностью:

    Имя классов сущности включает в себя значение нового поля из настроек инфоблока Символьный код API. За счет этого кода обеспечивается уникальность классов вне зависимости от ID и среды окружения.

    Важно! Чтобы начать использовать ORM для конкретного инфоблока, ему необходимо задать через административный интерфейс Символьный код API (поле API_CODE). Это строка от 1 до 50 символов, начинающаяся с буквы и состоящая из латинских букв и цифр.

    Свойства - это не просто скалярные значения, а отношения с отдельными мини-сущностями с двумя ключевыми полями: VALUE и DESCRIPTION. Единичные свойства представляются в элементе инфоблока полем Reference, множественные - OneToMany:

    В сущности некоторых типов свойств могут быть добавлены дополнительные поля, например ссылка на привязанный элемент инфоблока:

    Подробнее о базовых типах свойств

    Ориентироваться среди большого количества свойств поможет [ds]механизм аннотаций[/ds][di]Большая часть методов Объекта и Коллекции - виртуальные, обрабатываются через magic вызов __call. В то же время они сделаны для интуитивно понятных и говорящих именованных методов, и без автокомплита в IDE их ценность резко снижается.
    Чтобы IDE все же знала об их существовании и помогала ориентироваться в большом количестве классов и методов, мы сделали для нее специальный служебный файл с аннотациями всех сущностей.

    Подробнее ...[/di]. При индексации модуля iblock все инфоблоки будут описаны в виде сущностей ORM. Для получения подсказок в коде необходимо явно обозначить класс инфоблока:

    // подключение модуля инфоблоков
    \Bitrix\Main\Loader::includeModule('iblock');
    
    // вводные данные
    $iblockId = 32;
    $iblockElementId = 678;
    
    // объект инфоблока
    $iblock = \Bitrix\Iblock\Iblock::wakeUp($iblockId);
    
    // объект элемента
    /** @var \Bitrix\Iblock\Elements\EO_ElementLink $element */
    $element = $iblock->getEntityDataClass()::getByPrimary($iblockElementId)
    	->fetchObject();
    
    // получение свойства SOME_STRING
    $element->getSomeString();
    

    Автолоадинг классов автоматически обработает вызов getEntityDataClass(), т.е. вам не придется предварительно компилировать сущность инфоблока.

    Если вы хотите использовать подсказки IDE по типизации элемента инфоблока, необходимо явно задать его класс аннотацией вида:

    • для элемента;
      /** @var \Bitrix\Iblock\Elements\EO_ElementLink $element */
      
    • для коллекции,
      /** @var \Bitrix\Iblock\Elements\EO_ElementLink_Collection $element */
      
    где Link в имени класса - символьный код API инфоблока.
    Внимание! Символьный код свойства не должен совпадать с названием поля элемента инфоблока. В противном случае будет невозможно работать со свойством в объектном ORM по имени (символьному коду).

    Чтение и запись

    Для получения значений свойств достаточно указать их имена в запросе:

    $elements = $iblock->getEntityDataClass()::getList([
    	'select' => ['ID', 'SOME_FIELD', 'ANOTHER_FIELD.ELEMENT']
    ])->fetchCollection();
    
    foreach ($elements as $element)
    {
    	echo $element->getSomeField()->getValue();
    	echo $element->getAnotherField()->getElement()->getTitle();
    }

    Поддерживаются стандартные механики отношений ORM.

    При фильтрации следует помнить о структуре свойств. Поле свойства является ссылкой (Reference или OneToMany) на целую сущность, поэтому указание в фильтре имени свойства ни к чему не приведет:

    // неправильный пример
    $element = $iblock->getEntityDataClass()::query()
    	->where('SOME_FIELD', 'some value')
    	->fetchObject();

    Значение свойства хранится в поле сущности, и это поле всегда называется VALUE. Поэтому корректно указывать именно его в фильтре:

    // правильный пример
    $element = $iblock->getEntityDataClass()::query()
    	->where('SOME_FIELD.VALUE', 'some value')
    	->fetchObject();
    Перебор по классике в while

    Для создания нового объекта можно использовать как конструктор соответствующего класса,

    $newElement = new \Bitrix\Iblock\Elements\EO_ElementLink;

    так и фабрику сущности.

    $newElement = $iblock->getEntityDataClass()::createObject();

    Изменение значений и описаний свойств происходит непосредственно через объект свойства:

    // установка строкового значения
    $element->getSomeString()->setValue('new value');
    
    // установка описания
    $element->getSomeString()->setDescription('new descr');
    
    // установка привязки к элементу
    $element->getSomeElement()->setElement($anotherElement);

    Кроме этого, можно поставить значение напрямую в поле свойства:

    $element->setSomeString('new value');

    А также можно воспользоваться псевдо объектом значения свойства Bitrix\Iblock\ORM\PropertyValue:

    use Bitrix\Iblock\ORM\PropertyValue;
    
    // только значение
    $value = new PropertyValue('new value');
    
    // значение и описание
    $value = new PropertyValue('new value', 'new descr');
    
    // установка значения/описания
    $element->setSomeString($value);

    Установка значений для множественных свойств работает аналогично с той лишь разницей, что речь идет не о Reference, а об отношении OneToMany:

    use Bitrix\Iblock\ORM\PropertyValue;
    
    foreach ($element->getOtherField() as $value)
    {
    	$value->setValue('new value');
    	$value->setDescription('new descr');
    }
    
    $element->addToOtherField(new PropertyValue('new value'));
    $element->addToOtherField(new PropertyValue('new value', 'new descr'));

    Объект элемента сохраняется так же, как и любой другой объект ORM:

    $element->save();

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

    Удалить элемент можно через метод объекта delete:

    $element->delete();

    При удалении так же, как и при сохранении, значения свойств обрабатываются автоматически. Удалятся и привязки к секциям.

    События и кастомные типы свойств

    События

    Для подписи на события сущности инфоблоков можно использовать штатные механизмы ORM:

    use Bitrix\Main\ORM\Data\DataManager;
    		
    // ID инфоблока
    $iblockId = 32;
    
    // объект инфоблока
    $iblock = \Bitrix\Iblock\Iblock::wakeUp($iblockId);
    
    // диспетчер событий
    $em = \Bitrix\Main\ORM\EventManager::getInstance();
    
    $em->registerEventHandler(
    	$iblock->getEntityDataClass(),
    	DataManager::EVENT_ON_BEFORE_ADD,
    	'mymodule',
    	'MyClass',
    	'method'
    );

    Внимание! Поддержка действующих событий инфоблоков в данный момент не реализована.

    Кастомные типы свойств

    Чтобы добавить свои поля в сущность свойства, при описании свойства нужно задать отдельный коллбэк GetORMFields:

    public static function GetUserTypeDescription()
    {
    	return [
    	 ...
    	"GetORMFields" => array(__CLASS__, "GetORMFields"),
    	];
    }
    
    /**
     * @param \Bitrix\Main\ORM\Entity $valueEntity
     * @param \Bitrix\Iblock\Property $property
     */
    public static function GetORMFields($valueEntity, $property)
    {
    	$valueEntity->addField(
    	...
    	);
    }

    Наследование

    ORM API инфоблоков позволяет [dw]наследовать[/dw][di] Возможность наследования от ORM сущностей элементов появилась с версии 21.500.0 модуля iblock. [/di] сущность конкретного инфоблока, а также дополнить или переопределить его поведение. Предусмотрено наследование классов самой сущности и её объекта. Для наследования сущности достаточно указать Table класс инфоблока:

    class MyExtTable extends Bitrix\Iblock\Elements\Element{API_CODE}Table
    {
    }

    В имени родительского класса задействован API CODE из настроек инфоблока. Описание класса объекта задается по [ds]общим правилам ORM[/ds][di] Все объекты сущностей являются наследниками класса Bitrix\Main\ORM\Objectify\EntityObject, при этом у каждой сущности - свой собственный класс для объектов. По умолчанию, такой класс создается автоматически, на лету.

    Подробнее...[/di], включая конфигурирование Table класса:

    class MyExtTable extends Bitrix\Iblock\Elements\Element{API_CODE}Table
    {
    	public static function getObjectClass()
    	{
    		return MyExt::class;
    	}
    }
    
    class MyExt extends EO_MyExt
    {
    }
    



    Работа с компонентами

    Цитатник веб-разработчиков.

    Антон Долганин: Строжайше запрещено писать любую PHP-логику на обычной странице сайта. Любой php-код должен инкапсулироваться в компоненты. Максимум, что позволяется - использовать подключение скриптов с помощью IncludeFile, но это если разработчик полностью отдает себе отчет в том, что этого делать нельзя и это будет исправлено в ближайшее время, или того действительно требует логика проекта (единичные случаи).

    Компонент – основной способ вывода информации в Bitrix Framework. Соответственно именно работа с ним дает максимальные возможности по изменению условий вывода данных и изменению (добавлению) функционала системы.

    Рекомендуемое соотношение задач и способов их решений:

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

    Достаточно часто задачу приходится решать комбинацией методов. То есть, например, редактировать шаблон и добавлять код в result_modifier.php.

    В указанном порядке задач и способов решений и мы рассмотрим работу с компонентами в главах ниже.

    Внимание! Осуществляя любые действия по работе с компонентами надо также учитывать кеширование. Внедрение тяжелых кодов в component_epilog.php под кеширование не попадает. И бывают случаи, когда все же правильнее кастомизировать компонент, что дает выигрыш в производительности (особенно, если какой-то тяжелый код используется на главной или наиболее часто посещаемой странице).

    Переменные в компоненте 2.0

    Переменная, описаниеИспользование в файлах *.php
    componenttemplateresult_modifiercomponent_epilogclass
    arParams
    Параметры, чтение/изменение, затрагивает одноименный член класса компонента.
    да да да,
    изменения отразятся на arParams в файле template.php
    да [dw]да[/dw][di]Доступно как $this->arParams[/di]
    arResult
    Результат, чтение/изменение, затрагивает одноименный член класса компонента
    да [dw]да[/dw][di]затрагивает ссылку на поле компонента[/di] дада,
    изменение не затрагивает одноименный член класса компонента
    [dw]да[/dw][di]Доступен как $this->arResult[/di]
    APPLICATION
    доступна, можно не объявлять как global
    да да дада
    USER
    доступна, можно не объявлять как global
    да да дада
    DB
    доступна, можно не объявлять как global
    да да дада
    this
    ссылка на текущий:
    [dw]компонент[/dw][di]Объект класса CBitrixComponent, можно использовать все методы этого класса.[/di] [dw]шаблон[/dw][di]Объект, описывающий шаблон, тип CBitrixComponentTemplate[/di] [dw]шаблон[/dw][di]Объект, описывающий шаблон, тип CBitrixComponentTemplate[/di][dw]компонент[/dw][di]Объект класса CBitrixComponent, можно использовать все методы этого класса.[/di] [dw]Объект
    компонента[/dw][di] Наследуется от \CBitrixComponent[/di]
    componentPath
    путь к вызванному компоненту от DOCUMENT_ROOT
    да да да[dw]да[/dw][di]Доступен через $this->getPath()[/di]
    componentName
    имя вызванного компонента
    да [dw]да[/dw][di]Доступен через $this->getName()[/di]
    componentTemplate
    шаблон вызванного компонента
    да [dw]да[/dw][di]Доступен через $this->getTemplate()[/di]
    parentComponentPath
    если компонент вызван в составе другого компонента, идут отсылки на родительский компонент
    да [dw]да[/dw][di]Получется через
    $this->getParent()->getPath()[/di]
    parentComponentName
    если компонент вызван в составе другого компонента, идут отсылки на родительский компонент
    да [dw]да[/dw][di]Получается через
    $this->getParent()->getName()[/di]
    parentComponentTemplate
    если компонент вызван в составе другого компонента, идут отсылки на родительский компонент
    да [dw]да[/dw][di]Получается через
    $this->getParent()->getTemplate()[/di]
    templateName
    имя шаблона компонента
    да да
    templateFile
    путь к файлу шаблона от DOCUMENT_ROOT
    да да
    templateFolder
    путь к папке с шаблоном от DOCUMENT_ROOT
    да да
    templateData
    массив для записи, передающий данные из template.php в файл component_epilog.php, данные кешируются, т.к. файл component_epilog.php исполняется на каждом хите.
    да да
    component
    ссылка на:
    да,
    текущий [dw]компонент[/dw][di]Объект класса CBitrixComponent, можно использовать все методы этого класса.[/di]
    да,
    текущий [dw]шаблон[/dw][di]Объект, описывающий шаблон, тип CBitrixComponentTemplate[/di]
    да,
    $this
    да,
    $this

    Примечание: Компонент получает все параметры вызова следующим образом:
    1. В ключах, начинающихся с ~, данные содержатся в исходном виде (т.е. без всякой обработки).
      Если это комплексный компонент или в шаблоне компонента вызывается другой и часть параметров передается ему, то необходимо передавать значение ключей с ~.
    2. В ключах без ~ данные приведены к безопасному виду с помощью метода htmlspecialcharsEx. Если ключ содержит массив, то будут обработаны строковые ключи массива (тоже с помощью htmlspecialcharsEx).

    Классы компонентов

    Поддержка классов

    Зайцев Артемий:
    Сейчас думаю, что большие компоненты - это зло, даже с классами. Выношу классы в отдельные файлы и автолоадом подключаю.
    Но бывает так, что нужна в компоненте пара функций. И тогда такой класс будет очень полезным. А еще статические переменные в классе.

    Максим Смирнов:

    Проектировалось именно с прицелом на это.
    Тяжелую бизнес логику лучше держать в такой сущности как "модуль".

    Поддержка классов компонентов реализована в виде файла /component_name/class.php. Имя class.php - зарезервированно. Этот файл автоматически подключается при вызове:

    $APPLICATION->IncludeComponent()

    При этом происходит вызов final метода initComponent в котором и подключается class.php (если он есть) и из него берется самый последний класс наследник от CBitrixComponent.

    Действия вида:

    class CDemoTest extends CBitrixComponent{}
    class CDemoTestDecorator1 extends CDemoTest {}
    class CDemoTestDecorator2 extends CDemoTest {}

    не будут иметь успеха. В итоге будет использоваться CDemoTestDecorator2.

    Учтите что при изменении базового класса компонента нужно будет учитывать поведение всех его потомков (других компонентов).

      Пример

    Рассмотрим простейший компонент возводящий параметр в квадрат.

    Файл /bitrix/components/demo/sqr/component.php:

    <?if(!defined("B_PROLOG_INCLUDED") || B_PROLOG_INCLUDED!==true) die();
    
    $arParams["X"] = intval($arParams["X"]);
    if($this->startResultCache()) //startResultCache используется не для кеширования html, а для кеширования arResult
    {
    	$arResult["Y"] = $arParams["X"] * $arParams["X"];
    }
    $this->includeComponentTemplate();
    ?>

    Файл /bitrix/components/demo/sqr/templates/.default/template.php:

    <?if(!defined("B_PROLOG_INCLUDED") || B_PROLOG_INCLUDED!==true)die();?>
    <div class="equation">
    <?echo $arParams["X"];?> в квадрате равно <?echo $arResult["Y"];?>
    </div>

    В реальных компонентах вместо операции умножения может быть три десятка строк и таких операций может быть 5-6. В результате файл component.php превращается в тяжело понимаемую "вещь в себе".

    Выделяем логику компонента в класс.

    Файл /bitrix/components/demo/sqr/class.php:

    <?if(!defined("B_PROLOG_INCLUDED") || B_PROLOG_INCLUDED!==true) die();
    
    class CDemoSqr extends CBitrixComponent
    {
    	//Родительский метод проходит по всем параметрам переданным в $APPLICATION->IncludeComponent
    	//и применяет к ним функцию htmlspecialcharsex. В данном случае такая обработка избыточна.
    	//Переопределяем.
    	public function onPrepareComponentParams($arParams)
    	{
    		$result = array(
    			"CACHE_TYPE" => $arParams["CACHE_TYPE"],
    			"CACHE_TIME" => isset($arParams["CACHE_TIME"]) ?$arParams["CACHE_TIME"]: 36000000,
    			"X" => intval($arParams["X"]),
    		);
    		return $result;
    	}
    
    	public function sqr($x)
    	{
    		return $x * $x;
    	}
    }?>

    Файл /bitrix/components/demo/sqr/component.php:

    <?if(!defined("B_PROLOG_INCLUDED") || B_PROLOG_INCLUDED!==true) die();
    
    if($this->startResultCache())//startResultCache используется не для кеширования html, а для кеширования arResult
    {
    	//$this - экземпляр CDemoSqr
    	$arResult["Y"] = $this->sqr($arParams["X"]);
    }
    $this->includeComponentTemplate();
    ?>

    Теперь код в файле component.php стал управляемым.

      Наследование

    Например:

    Файл /bitrix/components/demo/double_sqr/class.php:

    <?if(!defined("B_PROLOG_INCLUDED") || B_PROLOG_INCLUDED!==true) die();
    
    //Необходимо для корректного поиска класса CDemoSqr
    CBitrixComponent::includeComponentClass("demo:sqr");
    //Наследник расширяющий функциональность:
    class CDemoDoubleSqr extends CDemoSqr
    {
    	public function sqr($x)
    	{
    		return parent::sqr($x)*2;
    	}
    }?>

    Файл /bitrix/components/demo/double_sqr/component.php идентичен файлу /bitrix/components/demo/sqr/component.php, можно просто скопировать содержание.

    Файл /bitrix/components/demo/double_sqr/templates/.default/template.php:

    <?if(!defined("B_PROLOG_INCLUDED") || B_PROLOG_INCLUDED!==true)die();?>
    <div class="equation">
    <?echo $arParams["X"];?> в квадрате умноженное на два равно <?echo $arResult["Y"];?>
    </div>

      Компонент без component.php

    Можно создать и компонент без файла component.php

    Для этого достаточно перекрыть метод executeComponent. Например:

    class CDemoSqr extends CBitrixComponent
    {
    ...
    	public function executeComponent()
    	{
    		if($this->startResultCache())  
    	{
    		$this->arResult["Y"] = $this->sqr($this->arParams["X"]);
    		$this->includeComponentTemplate();
    	}
    	return $this->arResult["Y"];
    	}
    };

    Теперь из обоих компонентов можно удалить файлы component.php.


    Файл result_modifier.php

    Файл result_modifier.php - инструмент для модификации данных работы компонента произвольным образом. Создается разработчиком самостоятельно. Работает при выключенном кешировании.

    Если в папке шаблона есть файл result_modifier.php, то он вызывается перед подключением шаблона.

    Схема работы компонента с файлом result_modifier.php:

    В этом файле можно запросить дополнительные данные и занести их в массив результатов работы компонента $arResult. Это может оказаться полезным, если требуется вывести на экран какие-либо дополнительные данные, но не хочется кастомизировать компонент и отказываться от его поддержки и обновлений.

    Примечание: файл result_modifier.php запускается только перед подключением шаблона. При использовании кеширования шаблон не подключается. И через него не получится устанавливать динамические свойства типа: title, keywords, description.

    В файле доступны языковые фразы шаблона компонента и следующие переменные:

    • $arParams - параметры, чтение, изменение. Не затрагивает одноименный член компонента, но изменения тут влияют на $arParams в файле template.php.
    • $arResult — результат, чтение/изменение. Затрагивает одноименный член класса компонента.
    • $APPLICATION, $USER, $DB - объявлять их как global избыточно, они уже доступны по-умолчанию.
    • $this — ссылка на текущий шаблон (объект, описывающий шаблон, тип CBitrixComponentTemplate)

    Примечание: дополнительно об $arParams и $arResult.

    Важно! Наиболее правильный вариант кастомизации компонента - скопировать его в отдельное пространство имен и работать уже с копией компонента. При этом нужно учитывать последствия:
    • Увеличивается общее количество компонентов, соответственно растет и количество выделяемых на их поддержку ресурсов.
    • Сложность в освоении новым разработчиком: сначала ему нужно будет найти в чем отличие от уже существующего стандартного компонента.


    Примеры решения задач

    Несколько примеров решения задач с помощью файла result_modifier.

    Ограничение срока показа новых сотрудников

    Задача: показывать новых сотрудников на корпоративном портале только небольшой период времени, допустим, месяц с момента их приема на работу, а не до тех пор, пока не будет принят кто-то еще.

    Чтобы решить задачу можно слегка изменить результаты работы компонента intranet.structure.informer.new, на основе которого работает гаджет Новые сотрудники.

    • Копируем шаблон .default компонента в шаблон портала. В файл result_modifier.php добавляем код:
      //show only users hired in the last N days
      $period_days = 30;
      foreach ($arResult['ENTRIES'] as $key => $arEntry) {
      	$user_reg_timestamp = MakeTimeStamp($arEntry["DATE_ACTIVE_FROM"] , "MM/DD/YYYY HH:MI:SS");
      	$from = strtotime("-".$period_days." days");
      	if ($user_reg_timestamp < $from) {
      		unset($arResult['ENTRIES'][$key]);
      	} 
      }

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

    • Нужно предусмотреть ситуацию, если новых сотрудников за данный период нет. В таком случае можно выводить сообщение (добавляется в код шаблона):
      <?
      if (count($arResult['ENTRIES']) == 0) {
      	echo GetMessage('INTR_NO_ENTRIES');
      }
      ?>

    Само текстовое сообщение будет находиться в файле шаблона /lang/ru/template.php

    $MESS['INTR_NO_ENTRIES'] = "Новых сотрудников в последнее время не было";

    После изменений гаджет Новые сотрудники сможет показывать новых сотрудников в соответствии с выбранными параметрами.

    Показ описания файлов

    Примечание: добавляемый в примере функционал уже есть в продукте с версии 10.5. Описание оставлено для примера работы с файлом result_modifier.php.

    На тех страницах корпоративного портала, где используется комплексный компонент Библиотека (bitrix:webdav), можно настроить список полей для показа. Но на вкладке Файлы на персональной странице или на вкладке Файлы в рабочих группах штатно список полей для показа настроить нельзя.

    Если надо показывать описание файла на персональной странице или в рабочих группах, то сделайте следующее. Скопируйте компонент в собственное пространство имён. Далее в каталог /local/components/_ваше_пространство_имен_/webdav.section.list/templates/.default/ добавьте файл result_modifier.php со следующим кодом:

    <?
    if (!defined('B_PROLOG_INCLUDED') || B_PROLOG_INCLUDED!==true)die();
    $arParams['COLUMNS'][] = 'PREVIEW_TEXT';
    ?>

    В результате имеем дополнительную колонку Описание для анонса:

    Рекламный баннер внутри текста

    Для размещения рекламного баннера внутри текста новости используйте разделитель #BANNER_BOTTOM#, где BOTTOM - тип баннера, который будет показан.

    В режиме визуального редактора код может выглядеть следующим образом:

    Используйте result_modifier.php, который следует поместить рядом с соответствующим шаблоном показа новости или статьи. Шаблон компонента предварительно скопирован в текущий шаблон сайта:

    <?if(!defined("B_PROLOG_INCLUDED") || B_PROLOG_INCLUDED!==true) die();
    $arResult["DETAIL_TEXT"] = preg_replace(
    	"/#BANNER_([A-Za-z-0-9]+)#/e",
    	'CAdvBanner::GetHTML(CAdvBanner::GetRandom("\1"))',
    	$arResult["DETAIL_TEXT"]
    );
    ?>
    

    В итоге на сайте это будет выглядеть следующим образом:

    Ресайз изображения в списочном компоненте

    foreach ($arResult['ITEMS'] as $key => $item) {
    
    	if (!empty($item['PREVIEW_PICTURE']['SRC'])) {
    
    		$resizeImg = CFile::ResizeImageGet(
    			$item['PREVIEW_PICTURE'],
    			[
    				'width'  => 300,
    				'height' => 300,
    			]
    		);
    
    		if (!empty($resizeImg['src'])) {
    			$resizeImg = array_change_key_case($resizeImg, CASE_UPPER);
    			$arResult['ITEMS'][$key]['PREVIEW_PICTURE'] = $resizeImg;
    		}
    	}
    }

    Еще примеры:


    Пример. Выборка из Информационного блока

    Типичная ошибка

    Вот как выглядит типичная попытка начинающего разработчика осуществить выборку из Информационного блока в явном виде в теле страницы:

    <?
    require($_SERVER["DOCUMENT_ROOT"]."/bitrix/header.php");
    $APPLICATION->SetTitle("Прямой вызов API Битрикса на странице");
    ?>
    <h1><?=$APPLICATION->ShowTitle()?></h1>
    <div class="box">
    <?
    $IBLOCK_ID = intval($arParams['IBLOCK_ID']);
    if ($IBLOCK_ID <=0) $IBLOCK_ID = 1;
     
    if(!CModule::IncludeModule("iblock"))
    	die('iblock module is not included!');
    //делаем выборку из Инфоблока
    $arSort = Array("SORT"=>"ASC", "NAME"=>"ASC");
    $arFilter = Array("IBLOCK_ID"=>$IBLOCK_ID,"ACTIVE"=>"Y");
    $obIBlockResult = CIBlockElement::GetList($arSort, $arFilter);
     
    //выводим результат выборки в виде списка	
    echo "<ol>";	
    while($arFields = $obIBlockResult->GetNext())
    {
    	echo "<li>{$arFields["NAME"]}</li>";
    }
    echo "</ol>";
    ?>
    </div>
    <?require($_SERVER["DOCUMENT_ROOT"]."/bitrix/footer.php");?>
    

    Чем же плохо такое решение?

    Основных проблем несколько:

    • отсутствие кеширования на такой странице (в отличие от компонента);
    • отсутствие внятной и явной передачи параметров (в отличие от компонента);
    • риск открытия страницы визуальным редактором с заменой спецсимволов, приводящей к неработающему коду на странице.

    Эти проблемы можно решить поместив код в шаблон пустого компонента, не потеряв при этом в скорости разработки за счет использования правильных инструментов.

    Переход на использование компонента

    Предлагается использовать result_modifier.php, который находится в папке шаблона компонента и изначально предназначен для модификации массива $arResult перед его выводом в файле шаблона template.php.

    Рассмотрим классическую схему работы компонента с result_modifier.php в шаблоне, представленную на рисунке:

    В данном случае, в result_modifier.php будет помещен непосредственно код, который до этого планировалось разместить прямо на странице.

    Общая схема решения выглядит следующим образом:

    1. На странице вместо кода с вызовами API помещается вызов пустого компонента system.empty со специальным шаблоном (например, get_list)
    2. В этом шаблоне компонента создается файл result_modifier.php, в который помещается необходимый код с вызовами API. Файл template.php оставляется пустым. (Но в дальнейшем возможно его использование стандартным образом – для этого в result_modifier.php нужно будет сформировать массив $arResult, который и будет выводиться в шаблоне.)
    3. В результате, на странице остается только вызов компонента, параметры которому можно передавать через массив параметров функции IncludeComponent()

    Примечание: компонент system.empty отсутствует в штатной поставке. Его нужно создавать самостоятельно.

    Пример кода страницы:

    <?
    require($_SERVER["DOCUMENT_ROOT"]."/bitrix/header.php");
    $APPLICATION->SetTitle("Компонентный подход: вызов API Битрикса в шаблоне компонента");
    ?>
    <h1><?=$APPLICATION->ShowTitle()?></h1>
    <div class="box">
    <?
    //задаем Инфоблок
    $IBLOCK_ID = 1;
     
    //подключаем пустой компонент, где логика лежит в шаблоне get_list в файле result_modifier.php
    $APPLICATION->IncludeComponent(
    	"bitrixonrails:system.empty",
    	"get_list",
    	Array(
    		"IBLOCK_ID" => $IBLOCK_ID,
    		"CACHE_TYPE" => "A",
    		"CACHE_TIME" => "3600"
    	),
    false
    );?>
     
    </div>
    <?require($_SERVER["DOCUMENT_ROOT"]."/bitrix/footer.php");?>
    

    Перенесенный в result_modifier.php исходный код:

    <?
    //получаем идентификатор Инфоблока из параметров компонента
    $IBLOCK_ID = intval($arParams['IBLOCK_ID']);
    if ($IBLOCK_ID <=0) $IBLOCK_ID = 1;
     
    if(!CModule::IncludeModule("iblock"))
    	die('iblock module is not included!');
    //делаем выборку из Инфоблока
    $arSort = Array("SORT"=>"ASC", "NAME"=>"ASC");
    $arFilter = Array("IBLOCK_ID"=>$IBLOCK_ID,"ACTIVE"=>"Y");
    $obIBlockResult = CIBlockElement::GetList($arSort, $arFilter);
     
    //выводим результат выборки в виде списка	
    echo "<ol>";	
    while($arFields = $obIBlockResult->GetNext())
    {
    	echo "<li>{$arFields["NAME"]}</li>";
    }
    echo "</ol>";
    ?>
    

    Если необходимы и другие вставки кода, то просто создается новый шаблон (можно просто копированием шаблона get_list) и в его result_modifier.php вставляется нужный код. В результате получается множество шаблонов ("квази-компонентов"), каждый из которых соответствует исходной странице.


    Модификация шаблона или создание result_modifier?

    Достаточно часто можно решить одну и ту же задачу разными способами. Выбор конечного варианта в таких задачах остается за разработчиком, который он принимает в зависимости от конкретной задачи. Рассмотрим пример решений задачи, которую можно решить двумя способами.

    Как сделать возможность вставки видео при публикации новостей на сайте? На первый взгляд это может показаться сложно. Но фактически все легко реализуемо. Идея состоит в том, чтобы для прикреплённого к новости файла подключать компонент Медиа проигрыватель (bitrix:player). Для отображения новости будет использоваться компонент Новость детально (bitrix:news.detail).

    Какой бы способ вы не использовали, необходимо создать свойство типа Файл в инфоблоке новостей.

    Решение с редактированием шаблона

    • Скопируйте шаблон компонента news.detail в шаблон сайта. Сам компонент менять не придётся.
    • Создайте новую страницу в визуальном редакторе и разместите на ней компонент Медиа проигрыватель (bitrix:player). Укажите базовые настройки (путь к видео файлу пока не заполняйте). Скопируйте полученный код вызова компонента. Например такой:
      <?$APPLICATION->IncludeComponent(
      	"bitrix:player",
      	"",
      	Array(
      		"PLAYER_TYPE" => "auto", 
      		"USE_PLAYLIST" => "N", 
      		"PATH" => "",
      		"WIDTH" => "400", 
      		"HEIGHT" => "300", 
      		"FULLSCREEN" => "Y", 
      		"SKIN_PATH" => "/bitrix/components/bitrix/player/mediaplayer/skins", 
      		"SKIN" => "bitrix.swf", 
      		"CONTROLBAR" => "bottom", 
      		"WMODE" => "transparent", 
      		"HIDE_MENU" => "N", 
      		"SHOW_CONTROLS" => "Y", 
      		"SHOW_STOP" => "N", 
      		"SHOW_DIGITS" => "Y", 
      		"CONTROLS_BGCOLOR" => "FFFFFF", 
      		"CONTROLS_COLOR" => "000000", 
      		"CONTROLS_OVER_COLOR" => "000000", 
      		"SCREEN_COLOR" => "000000", 
      		"AUTOSTART" => "N", 
      		"REPEAT" => "N", 
      		"VOLUME" => "90", 
      		"DISPLAY_CLICK" => "play", 
      		"MUTE" => "N", 
      		"HIGH_QUALITY" => "Y", 
      		"ADVANCED_MODE_SETTINGS" => "N", 
      		"BUFFER_LENGTH" => "10", 
      		"DOWNLOAD_LINK_TARGET" => "_self" 
      	)
      );?>
    • В шаблоне компонента вместо свойства movie настройте подключение медиаплеера. Найдите строки вывода свойств:
      30	<?foreach($arResult["DISPLAY_PROPERTIES"] as $pid=>$arProperty):?>
       31
       32	<?=$arProperty["NAME"]?>: 
       33		<?if(is_array($arProperty["DISPLAY_VALUE"])):?>
       34			<?=implode(" / ", $arProperty["DISPLAY_VALUE"]);?>
       35		<?else:?>
       36			<?=$arProperty["DISPLAY_VALUE"];?>
       37		 <?endif?>
       38		<br />
       39	<?endforeach;?>
    • Вставьте проверку и замену, получается:
      <?foreach($arResult["DISPLAY_PROPERTIES"] as $pid=>$arProperty):?>
      <?if ($arProperty["CODE"]=='movie' && $arProperty["DISPLAY_VALUE"]) {?>
      
      <?$APPLICATION->IncludeComponent(
      	"bitrix:player",
      	"",
      	Array(
      		"PLAYER_TYPE" => "auto", 
      		"USE_PLAYLIST" => "N", 
      		"PATH" => CFile::GetPath($arProperty["VALUE"]),
      		"WIDTH" => "400", 
      		"HEIGHT" => "300", 
      		"FULLSCREEN" => "Y", 
      		"SKIN_PATH" => "/bitrix/components/bitrix/player/mediaplayer/skins", 
      		"SKIN" => "bitrix.swf", 
      		"CONTROLBAR" => "bottom", 
      		"WMODE" => "transparent", 
      		"HIDE_MENU" => "N", 
      		"SHOW_CONTROLS" => "Y", 
      		"SHOW_STOP" => "N", 
      		"SHOW_DIGITS" => "Y", 
      		"CONTROLS_BGCOLOR" => "FFFFFF", 
      		"CONTROLS_COLOR" => "000000", 
      		"CONTROLS_OVER_COLOR" => "000000", 
      		"SCREEN_COLOR" => "000000", 
      		"AUTOSTART" => "N", 
      		"REPEAT" => "N", 
      		"VOLUME" => "90", 
      		"DISPLAY_CLICK" => "play", 
      		"MUTE" => "N", 
      		"HIGH_QUALITY" => "Y", 
      		"ADVANCED_MODE_SETTINGS" => "N", 
      		"BUFFER_LENGTH" => "10", 
      		"DOWNLOAD_LINK_TARGET" => "_self" 
      	),
      	$component   
      );?> 
      <? } else {?>
      	<?=$arProperty["NAME"]?>: 
      	<?if(is_array($arProperty["DISPLAY_VALUE"])):?>
      		<?=implode(" / ", $arProperty["DISPLAY_VALUE"]);?>
      	<?else:?>
      		<?=$arProperty["DISPLAY_VALUE"];?>
      	<?endif?>
      <?}?>
      	<br />
      	<?endforeach;?>
    Примечание: Здесь следует обратить внимание на следующие моменты:
    • Для получения пути к файлу из ID используется системный вызов CFile::GetPath.
    • При подключении компонентов указан четвёртый параметр $component для того чтобы из публичной части случайно не изменить его параметры (см. класс CMain::IncludeComponent).

    Решение с помощью result_modifier.php

    Если вы хотите, чтобы решение не было похоже на "костыли", необходимо вынести замену свойства в result_modifier.php. Тогда шаблон компонента будет стандартный.

    • Создайте файл result_modifier.php с кодом:
      <?
      // передадим значение свойства по ссылке:
      $arProperty = &$arResult['DISPLAY_PROPERTIES'][$arParams['PROPERTY_VIDEO']];
      
      if ($arProperty['DISPLAY_VALUE']) // проверим, установлено ли свойство
      {
      	global $APPLICATION;
      	ob_start(); // включим буферизацию чтобы отловить вывод компонента
      	$APPLICATION->IncludeComponent(
      	"bitrix:player",
      	"",
      	Array(
      		"PLAYER_TYPE" => "auto", 
      		"USE_PLAYLIST" => "N", 
      		"PATH" => CFile::GetPath($arProperty["VALUE"]),
      		"WIDTH" => "400", 
      		"HEIGHT" => "300", 
      		"FULLSCREEN" => "Y", 
      		"SKIN_PATH" => "/bitrix/components/bitrix/player/mediaplayer/skins", 
      		"SKIN" => "bitrix.swf", 
      		"CONTROLBAR" => "bottom", 
      		"WMODE" => "transparent", 
      		"HIDE_MENU" => "N", 
      		"SHOW_CONTROLS" => "Y", 
      		"SHOW_STOP" => "N", 
      		"SHOW_DIGITS" => "Y", 
      		"CONTROLS_BGCOLOR" => "FFFFFF", 
      		"CONTROLS_COLOR" => "000000", 
      		"CONTROLS_OVER_COLOR" => "000000", 
      		"SCREEN_COLOR" => "000000", 
      		"AUTOSTART" => "N", 
      		"REPEAT" => "N", 
      		"VOLUME" => "90", 
      		"DISPLAY_CLICK" => "play", 
      		"MUTE" => "N", 
      		"HIGH_QUALITY" => "Y", 
      		"ADVANCED_MODE_SETTINGS" => "N", 
      		"BUFFER_LENGTH" => "10", 
      		"DOWNLOAD_LINK_TARGET" => "_self" 
      		)
      	); 
      	$arProperty['DISPLAY_VALUE'] = ob_get_contents(); // подменим $arResult
      	ob_clean(); // очистим наш буфер чтобы плеер не появился дважды
      	ob_end_clean(); // закроем буфер
      }
      ?>

      Символьный код свойства можно сделать параметром компонента, чтобы не привязываться жёстко к конкретному инфоблоку. Для этого нужно доработать файл .parameters.php компонента Новость детально, расположенный в скопированном шаблоне компонента.

    • Измените код файла .parameters.php:
      <?
      if (!defined("B_PROLOG_INCLUDED") || B_PROLOG_INCLUDED!==true) die();
      $arProps = array(); 
      $rs=CIBlockProperty::GetList(array(),array("IBLOCK_ID"=>$arCurrentValues['IBLOCK_ID'],"ACTIVE"=>"Y"));
      while($f = $rs->Fetch())
      	$arProps[$f['CODE']] = $f['NAME'];
      
      $arTemplateParameters = array(
      	"DISPLAY_DATE" => Array(
      		"NAME" => GetMessage("T_IBLOCK_DESC_NEWS_DATE"),
      		"TYPE" => "CHECKBOX",
      		"DEFAULT" => "Y",
      	),
      	"DISPLAY_NAME" => Array(
      		"NAME" => GetMessage("T_IBLOCK_DESC_NEWS_NAME"),
      		"TYPE" => "CHECKBOX",
      		"DEFAULT" => "Y",
      	),
      	"DISPLAY_PICTURE" => Array(
      		"NAME" => GetMessage("T_IBLOCK_DESC_NEWS_PICTURE"),
      		"TYPE" => "CHECKBOX",
      		"DEFAULT" => "Y",
      	),
      	"DISPLAY_PREVIEW_TEXT" => Array(
      		"NAME" => GetMessage("T_IBLOCK_DESC_NEWS_TEXT"),
      		"TYPE" => "CHECKBOX",
      		"DEFAULT" => "Y",
      	),
      	"PROPERTY_VIDEO" => Array(
      		"NAME" => "Свойство, в котором хранится видео",
      		"TYPE" => "LIST",
      		"VALUES" => $arProps
      	),
      );
      ?>

    В результате в настройках параметра появляется новое поле: Свойство, в котором хранится видео.

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

    Файл component_epilog.php

    Файл component_epilog.php - инструмент для модификации данных работы компонента с включенным кешированием. Создается разработчиком самостоятельно. (Доступен с версии 9.0.)

    Схема работы компонента с файлами result_modifier.php и component_epilog.php:

    Этот файл подключается после исполнения шаблона. Аналогично файлам стилей, родительский компонент сохраняет в своем кеше список файлов эпилогов всех шаблонов дочерних компонентов (возможно вложенных), а при хите в кеш подключает эти файлы в том же порядке, как они исполнялись без кеширования. Точно так же при вызове дочерних компонентов в шаблоне нужно передавать значение $component.

    В файле component_epilog.php доступны $arParams, $arResult, но эти значения берутся из кеша. Набор ключей массива $arResult, попадающих в кеш, определяется в component.php вызовом вида:

    $this->SetResultCacheKeys(array(
    	"ID",
    	"IBLOCK_TYPE_ID",
    	"LIST_PAGE_URL",
    	"NAV_CACHED_DATA",
    	"NAME",
    	"SECTION",
     	"ELEMENTS",
    ));

    При разработке своих компонентов обязательно используйте такую конструкцию чтобы ограничить размер кеша только необходимыми данными.

    Примечание В файле component_epilog.php может быть любой код - только следует учитывать, что исполняться он будет независимо от наличия кеша на каждом хите. До версии главного модуля 10.0 в шаблон компонента передавалась копия массива arResult. В силу этого модификация этого массива в файле result_modifier.php не давала никаких результатов. Чтобы стало возможным изменение результатов вывода закешированных данных нужно разместить в файле result_modifier.php следующий код:
    <?if(!defined("B_PROLOG_INCLUDED") || B_PROLOG_INCLUDED!==true)die();
    global $APPLICATION;
    
    $cp = $this->__component; // объект компонента
    
    if (is_object($cp))
    {
    	// добавим в arResult компонента два поля - MY_TITLE и IS_OBJECT
    	$cp->arResult['MY_TITLE'] = 'Мое название';
    	$cp->arResult['IS_OBJECT'] = 'Y';
    	$cp->SetResultCacheKeys(array('MY_TITLE','IS_OBJECT'));
    
    	$APPLICATION->SetTitle($cp->arResult['MY_TITLE']); // не будет работать на каждом хите: 
    //отработает только первый раз, затем будет все браться из кеша и вызова $APPLICATION->SetTitle()
    // не будет. Поэтому изменение title делается в component_epilog, который выполняется на каждом хите.
    
    }
    ?>

    После чего изменения arResult, совершенные в шаблоне, станут доступны в component_epilog.php.

    Пример файла component_epilog.php

    <?if(!defined("B_PROLOG_INCLUDED") || B_PROLOG_INCLUDED!==true)die();
    
    global $APPLICATION;
    
    if (isset($arResult['MY_TITLE']))
    	$APPLICATION->SetTitle($arResult['MY_TITLE']);
    ?>

    Особенность использования

    Файл component_epilog.php подключается непосредственно после подключения и исполнения шаблона. Таким образом, если в компоненте идёт подключение шаблона, а затем в коде компонента следуют ещё операции, то они будут выполнены уже после выполнения файла component_epilog.php.

    Соответственно, в случае совпадения изменяемых данных в component_epilog.php и в коде компонента после подключения шаблона выведены будут только последние данные, то есть из кода компонента.

    Пример такой ситуации

    Используем файл component_epilog.php из примера выше. А в коде компонента (файл component.php) есть такой код:

    <?
    $this->IncludeComponentTemplate();
    if($arParams["SET_TITLE"])
    {
    	$APPLICATION->SetTitle($arResult["NAME"]);
    }
    ?>

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

    В файле component_epilog.php доступны:

    • $arParams - параметры, чтение/изменение не затрагивает одноименный член компонента.
    • $arResult — результат, чтение/изменение не затрагивает одноименный член класса компонента.
    • $componentPath — путь к папке с компонентом от DOCUMENT_ROOT (например /bitrix/components/bitrix/iblock.list).
    • $component — ссылка на $this.
    • $this — ссылка на текущий вызванный компонент (объект класса CBitrixComponent), можно использовать все методы класса.
    • $epilogFile — путь к файлу component_epilog.php относительно DOCUMENT_ROOT
    • $templateName - имя шаблона компонента (например: .dеfault)
    • $templateFile — путь к файлу шаблона от DOCUMENT_ROOT (напр. /bitrix/components/bitrix/iblock.list/templates/.default/template.php)
    • $templateFolder — путь к папке с шаблоном от DOCUMENT_ROOT (напр. /bitrix/components/bitrix/iblock.list/templates/.default)
    • $templateData — обратите внимание, таким образом можно передать данные из template.php в файл component_epilog.php, причем эти данные закешируются и будут доступны в component_epilog.php на каждом хите/
    • $APPLICATION, $USER, $DB — глобальные переменные.

    Важно! Наиболее правильный вариант кастомизации компонента - скопировать его в отдельное пространство имен и работать уже с копией компонента. При этом нужно учитывать последствия:
    • Увеличивается общее количество компонентов, соответственно растет и количество выделяемых на их поддержку ресурсов.
    • Сложность в освоении новым разработчиком: сначала ему нужно будет найти в чем отличие от уже существующего стандартного компонента.

    Особенности подключения языковых файлов

    Задача: языковые фразы компонента по результатам работы файла component_epilog.php должны быть иными, чем в самом компоненте. В этом случае надо учитывать что размещать текстовые фразы в /lang/ru/template.php для файла component_epilog.php нет смысла: при попадании в кеш языковый файл не подключается.

    Решение: создавать файл /lang/ru/component_epilog.php и самостоятельно подключить его кодом:

    use \Bitrix\Main\Localization\Loc; 
    
    Loc::loadLanguageFile(__FILE__); 
    Loc::getMessage("MY_MESS");

    Пример. Компонент в элементе ИБ

    Задача не частая, но встречающаяся: разместить в теле элемента информационного блока какой-либо компонент. Например, опрос. Попробуем реализовать эту задачу.

    • Обозначьте маркер для замены на текущий опрос. Пусть этот маркер будет #VOTE_ID_XX#, где XX это ID нужного нам опроса. Этот маркер вставьте в тело новости в нужное место:

    • Настройте компонент (в нашем случае это bitrix:voting.current) на отдельной странице так, как вам нужно. Деталь: отключите работу в AJAX-режиме.
    • Скопируйте шаблон новости в свой шаблон для редактирования. Откройте шаблон для детального просмотра и создайте два файла: result_modifier.php и component_epilog.php:

      result_modifier.php такого содержания:

      <?if(!defined("B_PROLOG_INCLUDED") || B_PROLOG_INCLUDED!==true)die();?>
      <?$this->__component->SetResultCacheKeys(array("CACHED_TPL"));?>
      component_epilog.php такого:
      <?if(!defined("B_PROLOG_INCLUDED") || B_PROLOG_INCLUDED!==true)die();?>
      <?
      echo preg_replace_callback(
      	"/#VOTE_ID_([\d]+)#/is".BX_UTF_PCRE_MODIFIER,
      	function ($matches) {
      		ob_start();
      		/*component here*/
      		$retrunStr = @ob_get_contents();
      		ob_get_clean();
      		return $retrunStr;
      	},
      	$arResult["CACHED_TPL"]);
      ?>
    • Вместо /*component here*/ в component_epilog.php вставьте вызов нашего компонента (для большей наглядности этот код пишем отдельно от общего кода):
      $GLOBALS["APPLICATION"]->IncludeComponent(
      	"bitrix:voting.current",
      	"main_page",
      	Array(
      		"CHANNEL_SID" => "ANKETA",
      		"VOTE_ID" => $matches[1],
      		"CACHE_TYPE"   =>   "A",
      		"CACHE_TIME"   =>   "3600",
      		"AJAX_MODE" => "N",
      		"AJAX_OPTION_SHADOW" => "Y",
      		"AJAX_OPTION_JUMP" => "Y",
      		"AJAX_OPTION_STYLE" => "Y",
      		"AJAX_OPTION_HISTORY" => "N",
      	)
      );
      Примечание: Обратите внимание, что вместо обычного $APPLICATION написано $GLOBALS["APPLICATION"]. Так надо для видимости объекта внутри временной функции. В остальном это полностью код компонента bitrix:voting.current.

      И обратите внимание на $matches[1]. Это единственный динамический параметр в вызываемом компоненте. Динамический в том плане, что он будет зависеть от того какой маркер мы меняем. Для #VOTE_ID_1# он будет равен 1, для #VOTE_ID_2# 2 и так далее.

    • Теперь надо изменить template.php. На второй строчке впишите конструкцию:
      <?ob_start();?>
      а в самом конце:
      <?
      $this->__component->arResult["CACHED_TPL"] = @ob_get_contents();
      ob_get_clean();
      ?>

    Манипуляции с component_epilog.php сделаны чтобы обойти кеширование.

    Что получили в итоге:

    В этом примере от файла result_modifier.php можно избавиться совсем (лишняя файловая операция все же). То есть $component->SetResultCacheKeys(array("CACHED_TPL")); можно добавить прямо в файл template.php. Здесь result_modifier.php был создан только для следования академическим правилам написания кода в Bitrix Framework.

    Примечание: Указанный способ не подходит для случая, когда внутренним компонентом выступает, к примеру, catalog.smart.filter, у которого ajax-режим подразумевает очистку буфера вывода. В результате, из-за вложенности буфера вывода, вы можете получить неадекватный вывод компонента.


    Пример. Исключение шаблона компонента из кэша

    Универсальный способ исключения шаблона компонента из кэша

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

    Идея решения проста: переместить шаблон компонента в эпилог компонента:

    1. Скопировать шаблон компонента в адресное пространство шаблона сайта.
    2. В папке шаблона компонента создать файл component_epilog.php и полностью скопировать в него код из файла шаблона template.php. Затем в самом верху component_epilog.php сразу после проверки подключения эпилога ядра <?if(!defined("B_PROLOG_INCLUDED") || B_PROLOG_INCLUDED!==true)die();?> перед началом непосредственно самого кода шаблона нужно добавить такой код:
      <?
      // заменяем $arResult эпилога значением, сохраненным в шаблоне
      if(isset($arResult['arResult'])) {
      	$arResult =& $arResult['arResult'];
      		// подключаем языковой файл
      	global $MESS;
      	include_once(GetLangFileName(dirname(__FILE__).'/lang/', '/template.php'));
      } else {
      	return;
      }
      ?>
      
    3. Полностью очистить файл template.php и добавить такой код:
      <?if(!defined('B_PROLOG_INCLUDED')||B_PROLOG_INCLUDED!==true)die();
      ?>
      // добавляем к кэшируемому результату $arResult
      if(property_exists($component, 'arResultCacheKeys')) {
      	if(!is_array($component->arResultCacheKeys)) {
      		$component->arResultCacheKeys = array();
      	}
      	$sVarName = 'arResult';
      	$component->arResultCacheKeys[] = $sVarName;
      	$component->arResult[$sVarName] = $$sVarName;
      }

    Теперь результат компонента кэшируется, а шаблон – нет.

    Недостатки этого способа:

    1. Увеличение размера кэша. С размером кэша можно бороться путем включения в кэш только необходимых данных
    2. Шаблон все же постоянно генерирует html-код, что медленнее, чем вывод уже готового, ранее сформированного html-кода.

    Примечание: если в компоненте используются методы CBitrixComponent::InitComponentTemplate и CBitrixComponent::ShowComponentTemplate, то такой компонент не подключает component_epilog.php. Например: bitrix:search.page.


    Кеширование компонентов

    Автокеширование

    Автокеширование может выключаться глобально на весь сайт одной кнопкой в административной части на странице Автокеширование Настройки > Настройки продукта > Автокеширование.

    Как правило на этапе разработки оно выключено, и включается перед сдачей проекта. Не стоит питать иллюзий относительно того, что Bitrix Framework сам будет выбирать время кеширования и подходящий момент для очистки кеша. Это может делать только разработчик решения, основываясь на реальных потребностях конкретного проекта: необходимо указывать в настройках компонентов время кеширования, адекватное периодичности обновления информации.

    Устройство и место хранения

    Кеш компонентов хранится в файлах в папке /bitrix/cache.

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

    Зная все эти параметры, можно очистить кеш любого компонента. Иначе кеш будет сброшен по истечении времени кеширования, кнопкой обновить кеш на панели инструментов публичной части или полной очисткой кеша из административной части.

    Примечание: Когда сбрасываете кеш страницы кнопкой , имейте в виду, что компонент может использовать привязку к группам для хранения кеша, тогда незарегистрированные пользователи будут по-прежнему видеть не актуальную страницу.

    Непосредственно после добавления/изменения элемента инфоблока в административной части кеш публичных компонентов не сбрасывается. Образно это объясняется тем, что инфоблок Новости "не знает", где выводятся новости в публичной части и сколько компонентов их отображает. Это не проблема если время кеширования выставлено правильно.

    Структурная схема

    Структурная схема работы компонента

    В $arParams содержится набор параметров компонента, component.php работает с входными параметрами запроса и базой данных, формирует результат в массив $arResult. Шаблон компонента преобразует результат в текст HTML.

    При первом хите сформированный HTML попадает в кеш. При последующих хитах запросов в базу не делается (или делается мало), данные читаются из кеша.

    Внимание! Учтите порядок выполнения, в этом случае код шаблона компонента и result_modifier.php не исполняются.

    Типичная ошибка: в шаблоне компонента вызываются отложенные функции: $APPLICATION->SetTitle(), $APPLICATION->AddChainItem() и т.д. В этом случае они работают только при выключенном кешировании.

    При разработке шаблонов собственных компонентов надо следовать простому правилу: задача шаблона - на основе входного массива $arResult сформировать текст HTML на выходе.

    В собственных компонентах надо формировать такой ID кеша, который однозначно определяет результирующий html. Но не допускайте попадание избыточных данных в ID кеша: это приводит к расходу места на диске и снижает попадания в кеш (а значит эффективность кеширования).

    Пример

    Пример кеширования классом CPHPCache

    $cntIBLOCK_List = 10;
    $cache = new CPHPCache();
    $cache_time = 3600;
    $cache_id = 'arIBlockListID'.$cntIBLOCK_List;
    $cache_path = 'arIBlockListID';
    if ($cache_time > 0 && $cache->InitCache($cache_time, $cache_id, $cache_path))
    {
    	$res = $cache->GetVars();
    	if (is_array($res["arIBlockListID"]) && (count($res["arIBlockListID"]) > 0))
    		$arIBlockListID = $res["arIBlockListID"];
    }
    if (!is_array($arIBlockListID))
    {
    	$res = CIBlock::GetList(
    		Array(), 
    		Array(
    			TYPE' => 'catalog', 
    			'SITE_ID' => SITE_ID, 
    			'ACTIVE' => 'Y', 
    			"CNT_ACTIVE" => "Y", 
    			"!CODE" => 'test%'
    		), true
    	);
    	while($ar_res = $res->Fetch())
    	{
    		if($ar_res['ELEMENT_CNT'] > 0)
    		$arIBlockListID[] = $ar_res['ID'];
    	}
       //////////// end cache /////////
    	if ($cache_time > 0)
    	{
    		$cache->StartDataCache($cache_time, $cache_id, $cache_path);
    		$cache->EndDataCache(array("arIBlockListID"=>$arIBlockListID));
    	}
    }

    Список ссылок по теме:



    Сache Dependencies (тегированный кеш)

    Теги кеша

    Главный модуль поддерживает [dw]теги кеша[/dw][di]Тегируется не выборка, а файл кеша.[/di]. Кеш можно помечать тегами и сбрасывать по тегам же. Сброс кеша компонентов инфоблоков происходит при изменении информации в них.

    Внимание! Для часто обновляемого большого массива данных использование тегированного кеша не оправданно.

    Базовый код тегирования кеша:

    01: $cache_id = md5(serialize($arParams));
    02: $cache_dir = "/tagged_getlist";
    03:
    04: $obCache = new CPHPCache;
    05: if($obCache->InitCache(36000, $cache_id, $cache_dir))
    06: {
    07: 	$arElements = $obCache->GetVars();
    08: }
    09: elseif(CModule::IncludeModule("iblock") && $obCache->StartDataCache())
    10: {
    11: 	$arElements = array();
    12: 	$rsElements = CIBlockElement::GetList($arParams["order"], $arParams["filter"]);
    13:
    14: 	global $CACHE_MANAGER;
    15: 	$CACHE_MANAGER->StartTagCache($cache_dir);
    16: 	while($arElement = $rsElements->Fetch())
    17: 	{
    18: 		$CACHE_MANAGER->RegisterTag("iblock_id_".$arElement["IBLOCK_ID"]);
    19: 		$arElements[] = $arElement;
    20: 	}
    21: 		$CACHE_MANAGER->RegisterTag("iblock_id_new");
    22: 		$CACHE_MANAGER->EndTagCache();
    23:
    24: 		$obCache->EndDataCache($arElements);
    25: }
    26: else
    27: {
    28: 	$arElements = array();
    29: }

    В строке 01 инициализируется уникальный идентификатор файла кеша. Далее определяется каталог относительно /bitrix/cache, в котором будут сохранятся файлы кеша с разными значениями $arParams. Важно, что этот путь начинается со слеша и им не заканчивается. При использовании в качестве кеша memcached или APC это будет критичным при сбросе кеша.

    В строках 04-05 инициализируется объект кеша. Если время кеширования не истекло, то будет выполнена строка 07 и мы получим данные из файла кеша.

    Условие в строке 09 фактически всегда будет true. Это подключение модуля и начало кеширования.

    В строке 12 происходит обращение к базе данных. Важно, чтобы все параметры от которых зависит результат выборки "поучаствовали" в идентификаторе кеша ($cache_id).

    В 14-й строчке объявляется доступ к переменной $CACHE_MANAGER. Этот объект будет управлять тегами.

    Строка 15 - все последующие назначаемые теги будут привязаны к каталогу $cache_dir. При сбросе кеша по одному из них содержимое этого каталога будет удалено. StartTagCache - может использоваться вложено. Например:

    $CACHE_MANAGER->StartTagCache($cache_dir1);
    	$CACHE_MANAGER->StartTagCache($cache_dir2);
    		$CACHE_MANAGER->StartTagCache($cache_dir3);
    		$CACHE_MANAGER->EndTagCache();
    	$CACHE_MANAGER->EndTagCache();
    $CACHE_MANAGER->EndTagCache();

    Важно чтобы вызовы StartTagCache и EndTagCache были сбалансированы. Объект $CACHE_MANAGER создает и отслеживает стек каталогов кеша. При этом теги назначенные на каталог $cache_dir3 будут также связаны и с $cache_dir2 и $cache_dir1. Теги назначенные на cache_dir2 будут связаны и с $cache_dir1.

    В строке 18 происходит отметка кеша тегом с помощью метода RegisterTag. Длина тела может быть до 100 символов. В методе RegisterTag автоматически удаляются дубликаты тегов.

    Строка 21 - необходима для сброса кеша при добавлении нового инфоблока, ведь у него на момент создания кеша тег не записывается (инфоблока ещё нет).

    Строка 22 - запись тегов каталога в таблицу базы данных. Можно считать по одному insert'у на тег.

    Сброс кеша:

    $CACHE_MANAGER->ClearByTag("iblock_id_7");
    или (с 15.0.7):
    CIBlock::clearIblockTagCache( 7 );

    Для отключения тегирования конкретных инфоблоков используйте:

    CIBlock::DisableTagCache($iblock_id)

    Компоненты инфоблоков

    Для запуска механизма необходимо определить константу в файле dbconn.php. (Это можно сделать простым включением Управляемого кеша на закладке Управляемый кеш на странице Настройки > Настройки продукта > Автокеширование.)

    define("BX_COMP_MANAGED_CACHE", true);

    При этом в методе StartResultCache компонента будет вызываться StartTagCache с путем к кешу компонента (с учетом страницы). А в методе EndResultCache (который в свою очередь вызывается из IncludeComponentTemplate) - EndTagCache.

    В модуле инфоблоков CIBlockElement::GetList и CIBlockSection::GetList возвращают объект класса CIBlockResult.

    В методе Fetch/GetNext этого объекта будут вызываться $CACHE_MANAGER->RegisterTag("iblock_id_".$res["IBLOCK_ID"]);.
    Если выборка не содержит элементов, то значение идентификатора инфоблока будет взято из фильтра.
    Если выборка НЕ пустая, в arSelect не запрашивается IBLOCK_ID, но есть фильтрация по IBLOCK_ID, то сброс кеша произойдёт.

    Сброс кеша вызывается из методов Add/Update/Delete для элементов, разделов и инфоблоков.

    Не стоит использовать этот механизм при частом обновлении элементов или разделов. С другой стороны это должно быть удобным для редакторов сайтов: изменения моментально отображаются на сайте. Еще один плюс - можно задавать практически бесконечное время кеширования.


    Пример. Добавление своего тега

    При выполнении инструкций по добавлению своего тега к кешам компонентов подразумевается, что у вас включено тегированное кеширование.

    Способ 1

    В тело компонента добавьте следующий код:

    if ($this->StartResultCache(......))
    {
    	if (defined('BX_COMP_MANAGED_CACHE') && is_object($GLOBALS['CACHE_MANAGER']))
    	{
    		$GLOBALS['CACHE_MANAGER']->RegisterTag('my_custom_tag');   
    	}
    
    	// do something
    
    	$this->IncludeComponentTemplate();
    }
    else
    {
    	$this->AbortResultCache();
    }
    

    Способ 2

    В шаблон компонента (в result_modifier.php) добавьте следующий код:

    <?
    if(!defined("B_PROLOG_INCLUDED") || B_PROLOG_INCLUDED!==true)die();
    
    if (defined('BX_COMP_MANAGED_CACHE') && is_object($GLOBALS['CACHE_MANAGER']))
    {
    	$cp =& $this->__component;
    	if (strlen($cp->getCachePath()))
    	{      
    		$GLOBALS['CACHE_MANAGER']->RegisterTag('my_custom_tag');
    	}
    }
    ?>
    

    Чтобы сбросить все кеши, помеченные вашим тегом, выполните следующий код:

    if (defined('BX_COMP_MANAGED_CACHE') && is_object($GLOBALS['CACHE_MANAGER']))
    	$GLOBALS['CACHE_MANAGER']->ClearByTag('my_custom_tag');
    

    Примечание: один и тот же кеш может быть помечен несколькими тегами. Например, если вы пометите своим тегом кеш компонента bitrix:news.list, то у кеша будет два тега: штатный "iblock_id_XX" и ваш "my_custom_tag". Соответственно, кеш будет сбрасываться и при добавлении/изменении элемента в инфоблоке XX (штатный функционал), и при сбросе кеша вручную через ClearByTag('my_custom_tag').


    Пример. Сортировка в компонентах

    Для выполнения сортировки в компоненте news.list или catalog.section.list компоненту необходимо передать параметры ELEMENT_SORT_FIELD и ELEMENT_SORT_ORDER.

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

    • id - ID элемента;
    • sort - индекс сортировки;
    • timestamp_x - дата изменения;
    • name - название;
    • active_from или date_active_from - начало периода действия элемента;
    • active_to или date_active_to - окончание периода действия элемента;
    • status - код статуса элемента в документообороте;
    • code - мнемонический код элемента;
    • iblock_id - числовой код информационного блока;
    • modified_by - код последнего изменившего пользователя;
    • active - признак активности элемента;
    • show_counter - количество показов элемента (учитывается функцией CIBlockElement::CounterInc);
    • show_counter_start - время первого показа элемента (учитывается функцией CIBlockElement::CounterInc);
    • shows - усредненное количество показов (количество показов / продолжительность показа);
    • rand - случайный порядок;
    • xml_id или external_id - внешний код;
    • tags - теги;
    • created - время создания;
    • created_date - дата создания без учета времени;
    • cnt - количество элементов (только при заданной группировке).

    Примечание: Поля active_from и active_to - устаревшие.

    Также сортировать можно по созданным вами свойствам элемента информационного блока:

    • property_<PROPERTY_CODE> - по значению свойства с числовым или мнемоническим кодом PROPERTY_CODE (например, PROPERTY_123 или PROPERTY_NEWS_SOURCE).
    • propertysort_<PROPERTY_CODE> - по индексу сортировки варианта значения свойства. Только для свойств типа Список.
    • catalog_<CATALOG_FIELD>_<PRICE_TYPE> - по полю CATALOG_FIELD (может быть PRICE - цена или CURRENCY - валюта) из цены с типом PRICE_TYPE (например, catalog_PRICE_1 или CATALOG_CURRENCY_3). Сортировка должна иметь формат: CATALOG_(PRICE или CURRENCY)_ID-типа-цены.
    • catalog_QUANTITY - сортировка по количеству.
    • PROPERTY_<PROPERTY_CODE>.<FIELD> - по значению поля элемента указанного в качестве привязки. PROPERTY_CODE - мнемонический или символьный код свойства типа привязка к элементам. FIELD может принимать значения:
      • ID
      • TIMESTAMP_X
      • MODIFIED_BY
      • CREATED
      • CREATED_DATE
      • CREATED_BY
      • IBLOCK_ID
      • ACTIVE
      • ACTIVE_FROM
      • ACTIVE_TO
      • SORT
      • NAME
      • SHOW_COUNTER
      • SHOW_COUNTER_START
      • CODE
      • TAGS
      • XML_ID
      • STATUS
    • PROPERTY_<PROPERTY_CODE>.PROPERTY_<PROPERTY_CODE2> - по значению свойства элемента указанного в качестве привязки. PROPERTY_CODE - мнемонический или символьный код свойства типа привязки к элементам. PROPERTY_CODE2 - код свойства связанных элементов.
    • HAS_PREVIEW_PICTURE и HAS_DETAIL_PICTURE - сортировка по наличию и отсутствию картинок.

    Примечание: Свойства catalog_*** доступны только при наличии модуля Торговый каталог.

    Тип сортировки указывается в соответствии со списком:

    • asc - по возрастанию;
    • nulls,asc - по возрастанию с пустыми значениями в начале выборки;
    • asc,nulls - по возрастанию с пустыми значениями в конце выборки;
    • desc - по убыванию;
    • nulls,desc - по убыванию с пустыми значениями в начале выборки;
    • desc,nulls - по убыванию с пустыми значениями в конце выборки.

    Самый простой способ передать новые параметры для сортировки в компонент - это использовать $_GET запрос и передать соответствующие переменные.

    Также можно воспользоваться $_SESSION и записать переменные в массив переменных сессии. Предположим нам необходимо сделать ссылки или кнопки(название, цена, лидер продаж, дата поступления) для сортировки товаров в разделе каталога (используем комплексный компонент catalog). После того как мы скопировали шаблон, необходимо открыть файл section.php и внести в него следующие модификации перед подключением компонента bitrix:catalog.section.list:

    <?if (
    	isset($_GET["sort"]) && isset($_GET["method"]) && (
    $_GET["sort"] == "name" || 
    		$_GET["sort"] == "catalog_PRICE_3" ||
    		$_GET["sort"] == "property_PRODUCT_TYPE" ||
    		$_GET["sort"] == "timestamp_x")){
    	$arParams["ELEMENT_SORT_FIELD"] = $_GET["sort"];
    	$arParams["ELEMENT_SORT_ORDER"] = $_GET["method"];
    }?>
    

    Этот код необходим для изменения параметров сортировки в компоненте. Далее откроем файл template.php компонента catalog.section и добавим ссылки управления сортировками:

    <p class="sort">Сортировка:
    	<a <?if ($_GET["sort"] == "name"):?> class="active" <?endif;?>href="<?=$arResult["SECTION_PAGE_URL"]?>?sort=name&method=asc">название</a> 
    	<a <?if ($_GET["sort"] == "catalog_PRICE_3"):?> class="active" <?endif;?>href="<?=$arResult["SECTION_PAGE_URL"]?>?sort=catalog_PRICE_3&method=asc">цена</a> 
    	<a <?if ($_GET["sort"] == "property_PRODUCT_TYPE"):?> class="active" <?endif;?>href="<?=$arResult["SECTION_PAGE_URL"]?>?sort=property_PRODUCT_TYPE&method=desc">лидер продаж</a> 
    	<a <?if ($_GET["sort"] == "timestamp_x"):?> class="active" <?endif;?>href="<?=$arResult["SECTION_PAGE_URL"]?>?sort=timestamp_x&method=desc">дата поступления</a>
    </p>
    

    Данную сортировку можно выполнить без перезагрузки страницы с использованием jQuery либо JS-библиотеки Bitrix Framework.

    В случае использования отдельного компонента catalog.section.list или news.list необходимо четко понимать, что редактировать [dw]сортировку данных в компоненте[/dw][di][/di] через визуальный редактор будет нельзя.

    Перед вызовом компонента разместить код:

    <?
    $sortField = 'ID'; // поле сортировки по умолчанию
    $sortOrder = 'ASC'; // направление сортировки по умолчанию
    
    if (
    	isset($_GET["sort"]) && isset($_GET["method"]) && (
    $_GET["sort"] == "name" || 
    		$_GET["sort"] == "catalog_PRICE_3" ||
    		$_GET["sort"] == "property_PRODUCT_TYPE" ||
    		$_GET["sort"] == "timestamp_x")){
    	$sortField = $_GET["sort"];
    	$sortOrder = $_GET["method"];
    }
    
    ?>
    <?$APPLICATION->IncludeComponent(
    	"bitrix:catalog.section",
    	"",
    	array(
    		... // остальные настройки компонента
    		"ELEMENT_SORT_FIELD" => $sortField,
    		"ELEMENT_SORT_ORDER" => $sortOrder,
    		... // еще настройки компонента
    	);
    );?>


    Пример. Использование событий

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

    Прямое назначение ответственного в Техподдержке

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

    Есть два варианта решения данной задачи:

    • Модифицируется код стандартного компонента support.wizard в файлах component.php и .description.php. Также, для большей гибкости, можно установить переменную-флаг в файле .parameters.php.

      Такой подход для непосвященного в такие понятия, как События в Bitrix Framework наиболее очевиден. В результате, задача резко усложняется - нужно разбираться в логике кода компонента, чтобы понять, где нужно добавить свой функционал, а также, не стоит забывать о том, что Bitrix Framework постоянно обновляется. Решение довольно трудоёмкое.

    • Достаточно будет использовать События и модифицировать шаблон.

    Теория Событий описана ниже. Вкратце: в код ядра, обычно в начало и конец вызова системной функции, разработчики уже вставили вызов системной функции, в нашем случае это: CTicket::ExecuteEvents. Если мы хотим дополнить эту функцию своей, то в файле /bitrix/php_interface/init.php пишем:

    AddEventHandler("support", "OnAfterTicketAdd", "MyHandler");

    Если вызываемая функция принадлежит классу, то нужно вместо MyHandler написать array("<имя_класса>","<имя_функции>");. Код файла init.php:

    <?
    // В массиве $arFields передаются все параметры, которые были переданы созданному тикету + ID и MID тикета
    // $_REQUEST["PERSONAL"] - Значение, которое мы получаем из нашего шаблона (тега select)    
    function AfterTicketAdd($arFields) 
    {
    	if ($_REQUEST["PERSONAL"]>0) 
    		{
    		//Добавляем к уже созданому тикету свойство "RESPONSIBLE_USER_ID"=>    
    		CTicket::Set(array(
    			"RESPONSIBLE_USER_ID"=>$_REQUEST["PERSONAL"]), $intMessage, $arFields['ID'], "N"); 
    		}
    }
    ?>

    $intMessage - ID нового сообщения, нам не нужно, "N" - здесь указывается, что права на добавление нам проверять не надо.

    Модификация шаблона

    • Скопируйте шаблон системного компонента
    • Добавьте в шаблон следующий код:
      <?
      use Bitrix\Main\Localization\Loc;
      
      // список всех сотрудников техподдержки
      // полученный через CTicket::GetSupportTeamList()
      $teamList = $arResult['TEAM_LIST'];
      
      ?>
      <select name="PERSONAL" id="PERSONAL">
      	<option><?= Loc::getMessage('select_any') ?></option>
      	<? foreach ($teamList as $item): ?>
      		<option value="<?= $item['REFERENCE_ID'] ?>"><?= $item['REFERENCE'] ?></option>
      	<? endforeach ?>
      </select>
    • При необходимости допишите локализацию на вашем родном языке.
    • Добавьте шаблон в файл ticket_edit.php.

    Кастомизация компонентов

    Кастомизация стандартного компонента - копирование стандартного компонента в собственное пространство имён и изменение логики его работы с целью изменения/добавления функционала.

    Большинство задач в Bitrix Framework реализуется через компоненты, и в шаблоне компонента вы оперируете массивами $arResult - это результат работы компонента (данные) и $arParams - это входные параметры.

    Чтобы кастомизировать стандартный компонент необходимо:

    • Создать новое пространство имён компонентов в папке /local/components/, например создать директорию /local/components/my_components/.
    • В созданную папку необходимо скопировать папку с компонентом, который хотите изменить (копировать из папки /bitrix/components/bitrix/).
    • Изменить компонент под текущие задачи.
      • изменить описание компонента на свое в файлах .description.php и /lang/ru/.description.php;
      • исправить файлы .parameters.php и component.php, модифицировав (добавив необходимый) функционал с помощью API продукта;
    • Отредактировать шаблон компонента под текущие задачи.
    • [dw]Очистите кеш[/dw][di][/di] визуального редактора. В результате в визуальном редакторе отобразится кастомизированный компонент.

      Важно! У работы с копией компонента есть определенные недостатки, которые нужно учитывать:
      • Увеличивается общее количество компонентов, соответственно растет и количество выделяемых на их поддержку ресурсов.
      • Сложность в освоении новым разработчиком: сначала ему нужно будет найти, в чем отличие кастомизированного компонента от уже существующего стандартного.

    Важно! Выполняя кастомизацию компонента помните, что все ключи в $MESS, содержащие название, описание и параметры компонента, а также идентификаторы веток компонента в дереве компонентов визуального редактора должны быть уникальными в рамках всего продукта.


    Простой пример кастомизации компонента

    Компонент news.list при большом числе элементов может существенно тормозить генерацию страницы. Задача – оптимизировать работу компонента. Одним из вариантов оптимизации может стать удаление ссылки на детальный текст новости в виде части текста (останется ссылка в виде названия новости).

    • Скопируйте компонент в свое пространство имен.
    • В коде скопированного компонента удалите строку:
      "DETAIL_TEXT",
      "DETAIL_TEXT_TYPE",
    • Сохраните внесенные изменения.
    • Примените вместо стандартного свой собственный компонент.

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


    Модификация простого компонента в составе сложного

    При работе с комплексным компонентом можно модифицировать один или несколько простых компонентов, а остальные остаются стандартные.

    Например, необходимо в компоненте socialnetwork.user_groups, который в составе комплексного компонента socialnetwork_group выводит список групп, увеличить длину выводимого описания группы с 50 до 100 символов (специально выберем простую модификацию чтобы акцентировать внимание на самом процессе).

    • Скопируйте шаблон комплексного компонента.

      Теперь в шаблоне сайта имеем шаблон комплексного компонента, перейдя в который увидим большой набор файлов в папке /local/templates/<шаблон сайта>/components/bitrix/socialnetwork_group/.default.

      Каждый из файлов вызывается на определённой странице социальной сети и подключает требуемые простые компоненты.

      Теперь надо найти файл, который подключает тот компонент, который нужно изменить. В нашем случае это index.php. Остальные файлы в шаблоне комплексного компонента, расположенного в шаблоне сайта, можно удалить. Комплексный компонент будет подключать эти файлы из ядра. А значит, они будут обновляться.

    • Теперь в оставшемся файле заменяем
      $APPLICATION->IncludeComponent(
                  "bitrix:socialnetwork.user_groups",
      на
      $APPLICATION->IncludeComponent(
                  "custom:socialnetwork.user_groups",
    • Копируем папку /bitrix/components/bitrix/socialnetwork.user_groups в /local/components/custom/socialnetwork.user_groups.
    • в файле /local/components/custom/socialnetwork.user_groups/component.php заменяем
      "GROUP_DESCRIPTION" => (strlen($arGroup["DESCRIPTION"]) > 50 ? substr($arGroup["DESCRIPTION"], 0, 50)."..." : $arGroup["DESCRIPTION"]), 
      на
      "GROUP_DESCRIPTION" => (strlen($arGroup["DESCRIPTION"]) > 100 ? substr($arGroup["DESCRIPTION"], 0, 100)."..." : $arGroup["DESCRIPTION"]), 

    Теперь весь функционал социальной сети остаётся стандартный, кроме компонента socialnetwork.user_groups.


    Тип параметров CUSTOM

    Тип параметров CUSTOM предоставляет разработчику полную свободу кастомизации. Например, есть какой-то системный или сторонний компонент. В зависимости от шаблона возникает необходимость добавить к компоненту какие-то свои настройки.

    Реализуется это с помощью:

    Параметр Описание
    JS_FILE файл с JS кодом ответственным за отображение кастомной опции.
    JS_EVENT callback функция которая будет вызвана после загрузки JS_FILE
    JS_DATA дополнительные данные, передаваемые в JS_EVENT

    Пример JS_DATA:

    {
    data:JS_DATA, //JS_DATA из .parameters.php
    oCont: td,    /* контейнер, в котором предлагается размещать кастомный контрол  управления параметром */
    oInput: input,//input в котором и будет предаваться значение параметра на сервер при сохранении
    propertyID:"MAP_DATA",//название параметра
    propertyParams: { /*...*/ },//Объект содержащий всё то же, что и массив параметра в .parameters.php
    fChange:function(){ /*...*/ },//callback для вызова, при изменении параметра
    getElements:function(){ /*...*/ }//возвращает объект со всеми параметрами компонента
    }

    Реализация в штатном компоненте

    Рассмотрим пример использования типа параметров CUSTOM в штатном компоненте map.google.view.

    Примечание: для использования компонентов Google необходимо иметь ключ доступа. Инструкция по получению ключа находится в документации.

    В файле .parameters.php видим:

    $arComponentParameters = array(
    //...
    'MAP_DATA' => array(
    	'NAME' => GetMessage('MYMS_PARAM_DATA'),
    	'TYPE' => 'CUSTOM',
    	'JS_FILE' => '/bitrix/components/bitrix/map.google.view/settings/settings.js',
    	'JS_EVENT' => 'OnGoogleMapSettingsEdit',
    	'JS_DATA' => LANGUAGE_ID.'||'.GetMessage('MYMS_PARAM_DATA_SET'),
    	'DEFAULT' => serialize(array(
    		'google_lat' => GetMessage('MYMS_PARAM_DATA_DEFAULT_LAT'),
    		'google_lon' => GetMessage('MYMS_PARAM_DATA_DEFAULT_LON'),
    		'google_scale' => 13
    	)),
    	'PARENT' => 'BASE',
    	)
    //...
    );

    В файле /bitrix/components/bitrix/map.google.view/settings/settings.js:

    function JCEditorOpener(arParams)
    {
    	this.jsOptions = arParams.data.split('||');
    	this.arParams = arParams;
    
    	var obButton = document.createElement('BUTTON');//создаём кнопку
    	this.arParams.oCont.appendChild(obButton);// добавляем в контейнер
       
    	obButton.innerHTML = this.jsOptions[1];//текст из JS_DATA
       
    	obButton.onclick = BX.delegate(this.btnClick, this);//навешиваем callback'и
    	this.saveData = BX.delegate(this.__saveData, this);
    }

    По нажатию кнопки открывается диалог, который генерируется в /bitrix/components/bitrix/map.google.view/settings/settings.php. В запросе к settings.php передаётся текущее значение MAP_DATA.

    Заголовок

    $obJSPopup->ShowTitlebar();
    $obJSPopup->StartDescription('bx-edit-menu');
    <p><b><? echo GetMessage('MYMV_SET_POPUP_WINDOW_TITLE')?></b></p><!-- Заголовок диалогового окна-->
    <p class="note"><? echo GetMessage('MYMV_SET_POPUP_WINDOW_DESCRIPTION')?></p><!-- Описание -->

    Блок контента

    $obJSPopup->StartContent();

    Блок кнопок

    $obJSPopup->StartButtons();

    Кнопка сохранения

    <input type="submit" value="<?echo GetMessage('MYMV_SET_SUBMIT')?/>" onclick="return jsGoogleCE.__saveChanges();"/>
    $obJSPopup->ShowStandardButtons(array('cancel'));//кнопка отмены
    $obJSPopup->EndButtons();

    В __saveChanges() данные сериализуются в строку и записываются в oInput, функцию сериализации на js в формат php можно посмотреть в bitrix/components/bitrix/map.google.view/settings/settings_load.js. В компоненте десериализация проводится из $arParam[~MAP_DATA].

    Локализация

    Языковой файл находится в lang/ru/.parameters.php. При использовании типа параметров CUSTOM не забывайте добавлять сообщения в этот файл.

    Список ссылок по теме:

    • Пример подключения календаря в настройках компонента.
    • Пример подключения форумов для механизма комментариев в каталоге.

    Ещё пара примеров работы

    Каким образом можно вывести капчу в своём компоненте?

    Пример использования CAPTCHA на странице. Ну а дальше - смотрите сами что куда вставлять: что в компонент (например, проверку), что в шаблон (например, вывод картинки)

    <?
    require($_SERVER["DOCUMENT_ROOT"]."/bitrix/header.php");
    $APPLICATION->SetTitle("Title");
    ?>
    <?
    if (isset($submit)) {
    	echo 'сабмит прошел...<br>';
    	echo $myname.'<br>';
    	echo $cap.'<br>';
    	if (!$GLOBALS["APPLICATION"]->CaptchaCheckCode($cap, $captcha_sid))
    	{
    	$error=true;
    	echo 'error captcha
    '; } } ?> <form id="linkForm" name="mailForm" action="test.php" method="post"> <table cellspacing="3" cellpadding="0" width="100%" bgcolor="#eeeeee" border="0"> <tbody> <tr><td valign="top" align="right">Моё имя *</td><td><input size="40" value="" name="myname" /></td></tr> <tr><td valign="top" align="right">CAPTCHA *</td><td><? $capCode = $GLOBALS["APPLICATION"]->CaptchaGetCode(); ?> <input type="hidden" name="captcha_sid" value="<?= htmlspecialchars($capCode) ?>"> <img src="/bitrix/tools/captcha.php?captcha_sid=<?= htmlspecialchars($capCode) ?>" width="180" height="40"><br> <input size="40" value="" name="cap" /></td></tr> <tr><td valign="top" align="right"> </td><td><input type="submit" value="Отправить" name="submit" />  <input type="reset" value="Сбросить" name="Reset" /></td></tr> </tbody> </table> </form><?require($_SERVER["DOCUMENT_ROOT"]."/bitrix/footer.php");?>

    Как сделать чтобы компонент на главной не виден, а на других был виден?

    Решение (добавить в шаблон компонента):

    if ($curPage = $APPLICATION->GetCurPage(true))
    {
    	if (($curPage != "/index.php"))
    		{
    		....
    		}
    } 

    Инфоблоки, работа с ними

    Цитатник веб-разработчиков.

    Ban Dmitry: Ну да, при работе с битриксом часто надо думать, в какие запросы трансформируется твой вызов API. Но, думается мне, в любых других высокоуровневых средах разработки то же самое.

    Начальные сведения об инфоблоках.

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

    Внимание! Прямые обращения к базе данных настоятельно не рекомендуются. В этом случае не гарантируется работа базовых функций системы. Кроме того, это может привести к нарушению целостности данных.

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

    Перед использованием модуля необходимо проверить, установлен ли он, и подключить его при помощи конструкции:

    <?
    if(CModule::IncludeModule("iblock"))
    {
    	//здесь можно использовать функции и классы модуля
    }
    ?>

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

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

    Ряд функций API доступен всегда, т.е. описан в главном модуле, а ряд функций зависит от используемого модуля, и может присутствовать или отсутствовать в различных редакциях продукта. Например, функции для работы с социальной сетью присутствуют в редакциях «1С-Битрикс: Управление сайтом - Бизнес» и выше, а также в «1С-Битрикс: Корпоративный портал».

    Для большинства классов Bitrix Framework доступны функции:

    • Выборка данных (GetList).
    • Занесение нового элемента (Add).
    • Обновление и удаление элемента (Update).
    • Удаление элемента (Delete).
    • И другие функции.

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

    • Всех используемых таблиц в базе данных, в том числе полей таблиц.
    • Классов для работы с типами инфоблоков, инфоблоками, элементами, разделами, полями.
    • Событий, происходящих при добавлении, изменении и удалении объектов модуля.
    • Функций, расширяющих возможности ядра.
    • Способов создать пользовательские формы редактирования и свои типы данных.
    • Другая информация.

    В уроках главы будут рассмотрены некоторые примеры использования API информационных блоков.

    Совет от веб-разработчиков

    Антон Долганин: Всегда минимизируйте запросы, если в цикле идет запрос к элементу ИБ, к примеру, то уже думайте над минимизацией. Да, это займет больше времени, но и вам скажут спасибо.

    Нельзя:

    foreach($arResult["ORDERS"] as $key => $val)
    {
    	foreach($val["BASKET_ITEMS"] as $vvval)
    	{
    		$rsEls = CIBlockElement::GetByID();
    	}
    }
    Следует:
    foreach($arResult["ORDERS"] as $key => $val)
    {
    	foreach($val["BASKET_ITEMS"] as $vvval)
    	{
    		$arIDs[] = $vvval["PRODUCT_ID"];
    	}
    }
    
    $rsEls = CIBlockElement::GetList(array(), array("ID" => $arIDs));
    ....
    
    foreach($arResult["ORDERS"] as $key => $val)
    {
    	foreach($val["BASKET_ITEMS"] as $vvval)
    	{
    		//наполняем данные, налаживая соответствие ID-ков
    	}
    } 
    Фактически, вы сводите порой десятки, если не сотни, запросов к одному. Разве это не круто?

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

    В главе приведены примеры работы с инфоблоками через API. Кроме этого советуем познакомиться со следующими статьями:

    Инфоблоки 2.0

    При создании информационных блоков рекомендуется хранить свойства инфоблока в отдельной таблице, причем все значения свойств одного элемента хранятся в одной строке. Эта технология называется [dw]Инфоблоки 2.0[/dw][di]В документации к Bitrix Framework, в сообщениях форума на сайте компании и в других местах могут встречаться прежние названия технологии: инфоблоки +.[/di] и позволяет существенно ускорить работу системы, а также снять ряд ограничений в предыдущей версии инфоблоков. Например, теперь нет необходимости в дополнительном запросе CIBlockElement::GetProperty при выборе значений свойств функцией CIBlockElement::GetList.

    Возможности инфоблоков 2.0:

    • При выборке элементов можно сразу получать значения свойств, т.к. количество присоединяемых таблиц в запросе не увеличивается с каждым свойством, а всегда равно единице.
    • Фильтрация по значениям свойств происходит аналогично инфоблокам 1.0 (за исключением множественных).
    • Выборка значений множественных свойств не приводит к декартовому произведению результата запроса - значения свойств передаются в виде массива.
    • Для комбинированных фильтров по немножественным (единичным) свойствам появилась возможность ручного создания составных индексов БД для ускорения операций выборки.
    • Для инфоблоков 2.0 нет возможности "сквозной" выборки элементов, когда в фильтре указывается тип инфоблока и символьный код свойства. В фильтре необходимо указывать IBLOCK_ID.
    • В инфоблоках 2.0 таблица со связанными элементами будет присоединяться (join) только один раз.

    Важным является полная совместимость API. То есть техника использования инфоблоков, свойств, элементов и их значений одинакова для обоих версий инфоблоков.

    Для разработчика очень удобно и гибко то, что свойства хранятся в одной общей таблице и управляются информацией метаданных из IBLOCK_PROPERTY_ID. Так как всегда можно поправить какую-то метаинформацию и никакая другая информация при этом у вас не пропадет. Это свойственно простым инфоблокам.

    Если информация хранится в инфоблоках 2.0 и у свойства меняется его тип (например, с Числа на Строку), то изменяется и тип хранения в самой базе данных. То есть меняется не логика интерпретации продуктом значения этого свойства, а меняется само значение. То есть нельзя "играться" с данными.

    С точки зрения производительности инфоблоки 2.0 выигрывают на небольших справочниках с небольшим количеством (20-30) редко изменяемых свойств. Ленту новостей нет никакого смысла переводить на этот вид инфоблоков. Вы выиграете в числе запросов, но проиграете во времени их исполнения.

    В инфоблоках 2.0 существует физическое ограничение БД на количество свойств инфоблока. На данный момент это не отслеживается в системе, так как зависит от множества непрогнозируемых факторов: типа свойств, конфигурации MySQL и других. При превышении этого физического ограничения вы получите редкую для Bitrix Framework ошибку MySQL. Данные при этом потеряны не будут.

    Большое преимущество инфоблоков 2.0 – возможность использования составных индексов. Однако достаточно редка ситуация, когда выполняется фильтр по = и по нескольким полям одновременно.

    Уровень информационного блока.

    У информационного блока есть признак VERSION, который при создании нового инфоблока определяет выбор хранения значений свойств информационного блока в общем хранилище или в выделенном. При выборе выделенного хранения для данного инфоблока в БД создаются две дополнительные таблицы, включающие в своё имя идентификатор инфоблока. В одной из них будут храниться множественные свойства, а в другой единичные и кешированные значения множественных.

    При редактировании инфоблока доступна ссылка на "конвертер" между типами хранения. Пользоваться им надо с большой осторожностью, т.к. продолжительность процесса конвертации зависит от общего объема значений свойств инфоблока. В течение всего процесса инфоблок находится в несогласованном состоянии (часть значений перенесена, а часть нет). На тестовой конфигурации для MySQL версии скорость была порядка 1500 элементов за 20-ти секундный шаг.

    Внимание! Перевод из обычных инфоблоков в инфоблоки 2.0 невозможен при числе свойств более 50.

    В классе CIBlockResult переопределён метод Fetch. В нем происходит кеширование значений множественных свойств элемента, участвующих в выборке. Для этих же свойств типа список выбираются пары ID=>VALUE из справочника списков.

    В API предусмотрен параметр VERSION в массиве полей $arFields метода CIBlock::Add. Его значения: 1 - для общего хранения и 2 - для выделенного (нового).

    Уровень свойств информационного блока

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

    Уровень элементов информационного блока

    При вставке элемента вставляется и соответствующая запись в таблицу хранения значений свойств элемента.

    Уровень значений свойств элементов информационного блока

    Значения единичных свойств инфоблока с выделенным хранилищем ID - составные и состоят из ID элемента и ID свойства, разделенных двоеточием. При обновлении множественных свойств происходит сброс кеша этих значений.

    Значения свойств хранятся в 2-х таблицах (описание таблиц и их структуры имеет справочный характер и могут меняться в следующих версиях):

    • b_iblock_element_prop_mNN - для множественных. Имеет ту же самую структуру, что и b_iblock_element_property;
    • b_iblock_element_prop_sNN - для единичных. Имеет поле IBLOCK_ELEMENT_ID - ID элемента инфоблока которому принадлежат свойства:
      • PROPERTY_XX - хранит значения единичного свойства XX или кеш значений для множественного свойства;
      • DESCRIPTION_XX - хранит описание для единичного свойства.

    Как достичь наибольшей эффективности при использовании Инфоблоков 2.0?

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

    Например: если раньше шаблон кода был примерно таким:

    <?
    //Определяем массив нужных полей элемента
    $arSelect = array(
    	"ID",
    	"IBLOCK_ID",
    	"IBLOCK_SECTION_ID",
    	"NAME",
    	"PREVIEW_PICTURE",
    	"DETAIL_PICTURE",
    	"DETAIL_PAGE_URL",
    );
    //Получаем список элементов. (+1 запрос)
    if($rsElements = GetIBlockElementListEx($IBLOCK_TYPE, false, false, array($ELEMENT_SORT_FIELD => $ELEMENT_SORT_ORDER), 
    $ELEMENT_COUNT, $arFilter, $arSelect))
    {
    	//Инициализация постраничного вывода.
    	$rsElements->NavStart($ELEMENT_COUNT);
    	$count = intval($rsElements->SelectedRowsCount());
    	if ($count>0)
    	{
    		//Для каждого элемента:
    		while ($obElement = $rsElements->GetNextElement())
    		{
    			$arElement = $obElement->GetFields();
    			//Получаем его свойства. (+1 запрос)
    			$arProperty = $obElement->GetProperties();
    			//Ниже можно пользоваться значениями свойств.
    			//Например:
    			echo $arProperty["SOURCE"],"
    "; //и т.д. и т.п. } } } ?>

    Теперь, после преобразования в новый тип хранения, стало возможным избавится от запросов в цикле:

    <?
    //Определяем массив нужных полей элемента
    $arSelect = array(
    	"ID",
    	"IBLOCK_ID",
    	"IBLOCK_SECTION_ID",
    	"NAME",
    	"PREVIEW_PICTURE",
    	"DETAIL_PICTURE",
    	"DETAIL_PAGE_URL",
    	"PROPERTY_SOURCE", //Выбираем нужное нам свойство
    	// И все другие какие могут понадобится
    	// непосредственно в списке
    );
    //Получаем список элементов. (+1 запрос)
    if($rsElements = GetIBlockElementListEx($IBLOCK_TYPE, false, false, array($ELEMENT_SORT_FIELD => $ELEMENT_SORT_ORDER), 
    Array("nPageSize"=>$ELEMENT_COUNT), $arFilter, $arSelect))
    {
    	//Инициализация постраничного вывода.
    	$rsElements->NavStart($ELEMENT_COUNT);
    	if($obElement = $rsElements->GetNextElement())
    	{
    		//Для каждого элемента:
    		do
    		{
    			$arElement = $obElement->GetFields();
    			//Ниже можно пользоваться значениями свойств.
    			//Например:
    			echo $arElement["PROPERTY_SOURCE"],"
    "; //и т.д. и т.п. } while ($obElement = $rsElements->GetNextElement()) } } ?>

    Инфоблоки в Документообороте

    При работе в режиме документооборота создаётся два элемента инфоблока: один «конечный» с пустым WF_PARENT_ELEMENT_ID (который мы видим в административной части), второй - «промежуточный» с WF_PARENT_ELEMENT_ID равным ID только что созданного конечного элемента. «Промежуточный» превращается в «конечный» когда в рамках документооборота он достигнет финального статуса Опубликован. Либо любого другого статуса с поднятым флагом IS_FINAL. Этот флаг нельзя выставить методами API Bitrix Framework, только правкой БД. Соответственно до этого момента записи в инфоблок заноситься будут, однако для стандартных методов API с параметрами по умолчанию они будут не доступны.

    Основным отличием «промежуточных» элементов от «конечных» является поле WF_PARENT_ELEMENT_ID, которое пусто для «конечных» и содержит идентификатор «конечного» элемента для промежуточных элементов. В случае создания нового элемента в документообороте (при условии, что начальный статус документооборота не является финальным) будет создан элемент, в поле WF_PARENT_ELEMENT_ID будет записан его собственный идентификатор. При последующем переводе элемента в любой другой статус документооборота, в том числе и в финальный, в инфоблоке будут создаваться новые элементы с полем WF_PARENT_ELEMENT_ID.

    При переводе элемента в финальный статус элемент, с которого всё началось, будет обновлён так, что поле WF_PARENT_ELEMENT_ID станет пустым, а поле WF_STATUS_ID станет равным значению финального статуса (зачастую это 1). При последующем переводе элемента в какой-либо промежуточный статус круг повторится с той небольшой разницей, что в качестве отправной точки будет использоваться текущий опубликованный элемент.

    По умолчанию API позволяет работать только с опубликованными элементами. Причём если перевести опубликованный элемент в какой-либо другой статус, то API будет возвращать именно ту версию элемента, которая соответствует опубликованной.

    Чтобы получить список всех элементов (в том числе и не опубликованных) у метода CIBlockElement::GetList в свойствах фильтра необходимо задать параметр "SHOW_HISTORY" => "Y".

    Кроме того, существует возможность: зная ID элемента получить его последнюю версию в документообороте с помощью функции CIBlockElement::WF_GetLast, и наоборот - зная последнюю версию элемента получить его оригинальный ID с помощью функции CIBlockElement::GetRealElement.

    Отдельно стоит упомянуть обработчики событий, в том числе OnAfterIBlockElementUpdate. Так как в случае работы с документооборотом непосредственный Update происходит только в случае перевода элемента в финальный статус, не следует ожидать вызова обработчиков этого события при переводе элемента в «промежуточные» статусы. Вместо этого стоит добавить обработчик на событие OnAfterIBlockElementAdd и ориентироваться на поля "WF" = "Y" (признак того, что элемент участвует в документообороте) и WF_PARENT_ELEMENT_ID (идентификатор «конечного» элемента).

    Выбрать всю историю изменений элемента можно с помощью функции CIBlockElement::WF_GetHistoryList. Для получения детальной информации о промежуточных элементах полученных с помощью этой функции рекомендуется использовать функции CIBlockElement::GetByID и CIBlockElement::GetProperty.

    Фильтрация

    Фильтрация

    Цитатник веб-разработчиков.

    Nikolay Ryzhonin: Все-таки более правильный и эффективный путь - это анализ с explain медленных запросов и анализ кода, их вызвавшего. А уже по результатам - добавление необходимых индексов или изменение кода приложения. Так как не всегда медленные запросы - следствие проблем в API, часто встречаются случаи его неправильного использования.

    Кеширование иногда - это зло. Если кешировать каталог с многовариативным фильтром при большой посещаемости, то генерация кеша может превышать 200 мегабайт в минуту. Любая квота на диске заполнится достаточно быстро. Возникают и проблемы с очисткой этого кеша. Отключение такого кеша снизит количество операций на запись.

    Не надо бояться создавать индексы. Хотя точно сказать какие индексы создавать в каждой конкретной ситуации заочно нельзя, надо всегда рассматривать конкретную ситуацию. В этом помогает инструмент Индексы.

    Один из часто используемых индексов - индекс по списочному свойству. Это индекс для простых инфоблоков. b_iblock_element_property - в этой таблице хранятся значения свойств. Её и индексируем: значение свойства, его ID, ID элемента. Создание такого индекса при фильтре по списковому свойству фактически исключает обращение MySQL к указанной таблице, так как все имеющиеся в запросе поля присутствуют в индексе.

    Индексы нужны для конкретных выборок на конкретных проектах. В зависимости от архитектуры и логики проекта медленные запросы получаются свои, и для них нужны свои индексы, часто составные.

    Совет от веб-разработчиков.

    Антон Долганин: Если требуется обновить большое количество элементов, то имеет смысл обновление производить в обход индексации поиском:
    $el->Update($arEls['ID'], array('PREVIEW_TEXT' => $preview, 'DETAIL_TEXT' => $detail), false, false);
    4-й параметр в false. А уже после обработки запустить переиндексацию инфоблоков сайта. Запись в модуль поиска увеличивает конечное время исполнения в десятки-сотни раз.

    Типы фильтрации

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

    Типы:

    • (пустой) - для строковых полей означает поиск по маске (в маске: "%" - произвольное число любых символов, "_" - один произвольный символ), для полей с нестроковыми типами поиск "равно".
    <?
    // найти элементы у которых название начинается на "#"
    $res = CIBlockElement::GetList(Array(), Array("NAME"=>"%#"));
    
    // найти элементы с идентификатором 100
    $res = CIBlockElement::GetList(Array(), Array("ID"=>"100"));
    ?>
    • "!" - для строк выражение не попадающее под маску, или не равно (для остальных типов полей).
    <?
    // найти элементы у которых название не начинается на "#"
    $res = CIBlockElement::GetList(Array(), Array("!NAME"=>"#%"));
    ?>
    • "?" - с применением логики, работает только для строковых свойств.
    <?
    // найти элементы у которых название содержит "One" или "Two"
    $res = CIBlockElement::GetList(Array(), Array("?NAME"=>"One | Two"));
    ?>
    • "<" - меньше;
    • "<=" - меньше либо равно;
    • ">" - больше;
    • ">=" - больше либо равно.
    <?
    // найти элементы у которых название начинается на "A"
    $res = CIBlockElement::GetList(Array(), Array(">=NAME"=>"A", "<NAME"=>"B"));
    
    // найти элементы с идентификатором большим 100
    $res = CIBlockElement::GetList(Array(), Array(">ID"=>"100"));
    ?>
    • "=" - равно;
    • "!=" - не равно.
    <?
    // найти элементы у которых название равно "ELEMENT%1"
    $res = CIBlockElement::GetList(Array(), Array("=NAME"=>"ELEMENT%1"));
    ?>
    • "%" - подстрока;
    • "!%" - не подстрока.
    // найти элементы у которых в названии есть последовательность "123"
    $res = CIBlockElement::GetList(Array(), Array("%NAME"=>"123"));
    • "><" - между;
    • "!><" - не между.

    В качестве аргумента данные типы фильтров принимают массив вида Array("значение ОТ", "значение ДО").

    Указанные операторы отображаются в sql BETWEEN, то есть граничные значения попадают в результат, используется выражение (min <= expr AND expr <= max).

    <?
    // найти элементы у которых название начинается между "A" и "B" или между "D" и "E"
    $res = CIBlockElement::GetList(Array(), Array("><NAME"=>Array(Array("A", "B"), Array("D", "E"))));
    
    // найти элементы, у которых дата начала активности не в периоде 2003 года
    $res = CIBlockElement::GetList(Array(),
    	Array("!><DATE_ACTIVE_FROM"=>
    		Array(date($DB->DateFormatToPHP(CLang::GetDateFormat("FULL")), mktime(0,0,0,1,1,2003)), date($DB->DateFormatToPHP(CLang::GetDateFormat("FULL")), mktime(0,0,0,1,1,2004)-1)
    		)
    	)
    );
    ?>

    Примеры

    Несколько частных случаев фильтрации

    $arFilter = array("PROPERTY_CML2_SCAN_CODE"=>false ) - используется, чтобы выбрать все элементы с незаполненным свойством; 
    $arFilter = array("PROPERTY_CML2_SCAN_CODE"=>"" ) - используется, чтобы выбрать все элементы;  
    $arFilter = array("PROPERTY_CML2_SCAN_CODE"=>"qwe" ) - при фильтрации элементов проверяется точное совпадение свойства с заданной строкой; 
    $arFilter = array("?PROPERTY_CML2_SCAN_CODE"=>"qwe" ) - при фильтрации элементов проверяется наличие заданной подстроки в свойстве. 
    $arFilter = array("!PROPERTY_CML2_SCAN_CODE"=>false ) - используется, чтобы выбрать только элементы с заполненным свойством; 
    $arFilter = array("!PROPERTY_CML2_SCAN_CODE"=>"qwe" ) - при фильтрации элементов проверяется отсутствие точного совпадения с заданной строкой ; 
    $arFilter = array("!?PROPERTY_CML2_SCAN_CODE"=>"qwe" ) - при фильтрации элементов проверяется отсутствие заданной подстроки в свойстве. 
    

    Настройка фильтрации для отображения связанных элементов

    Задача: допустим, что есть 2 инфоблока, связанные между собой по одному из свойств. Как настроить фильтрацию, чтобы среди элементов инфоблока показывались только связанные?

    Решение:

    <? 
    	$arrFilter = array(); 
    	$arrFilter['!PROPERTY_<код свойства>'] = false; 
    ?>

    Настройка фильтрации по свойству типа "Дата/время"

    Свойство типа "Дата/время" хранится как строковое в формате YYYY-MM-DD HH:MI:SS, поэтому значение для фильтрации формируется следующим образом:

    $cat_filter[">"."PROPERTY_available"] = date("Y-m-d");


    Список ссылок по теме.


    Фильтрация элементов инфоблока без компонента фильтра

    Если для публикации информационного блока используются простые компоненты, то можно сделать фильтрацию элементов без использования компонента Фильтр и без кастомизации компонента, с помощью которого выводится список элементов. Такая фильтрация основана на использовании параметра Имя массива со значениями фильтра для фильтрации (FILTER_NAME) и доступна в следующих компонентах: bitrix:catalog.section, bitrix:catalog.sections.top и bitrix:news.list.

    Массив фильтра можно определить прямо на странице, где подключается компонент, перед его подключением. Однако, в таком случае, придется создавать несколько страниц, на каждой помещать компонент и т.д. Можно сделать гораздо проще - передавать переменные массива фильтра в ссылке.

    В нашем примере будет происходить переход с другой страницы, переменные для фильтрации можно передать в ссылке методом GET, а на странице определять фильтр $arrFilter из массива $_GET. Публикацию элементов инфоблока будем выполнять с помощью компонента Элементы раздела (bitrix:catalog.section).

    Допустим, что у нас имеется инфоблок Книги, фильтровать элементы которого мы будем по свойству Год выпуска (YEAR):

    Создадим стартовую страницу с набором ссылок (в нашем случае со списком годов выпуска книг):

    Код страницы будет следующим:

    <?
    require($_SERVER["DOCUMENT_ROOT"]."/bitrix/header.php");
    $APPLICATION->SetTitle("Поиск книги по году выпуска");
    ?>
    <p>Выберите год:</p>
    
    <ul>
    <li><a href="/content/filter/section.php?SECTION_ID=10&YEAR=2000">2000</a></li>
    <li><a href="/content/filter/section.php?SECTION_ID=10&YEAR=2001">2001</a></li>
    <li><a href="/content/filter/section.php?SECTION_ID=10&YEAR=2002">2002</a></li>
    <li><a href="/content/filter/section.php?SECTION_ID=10&YEAR=2003">2003</a></li>
    <li><a href="/content/filter/section.php?SECTION_ID=10&YEAR=2004">2004</a></li>
    <li><a href="/content/filter/section.php?SECTION_ID=10&YEAR=2005">2005</a></li>
    </ul>
    
    <?require($_SERVER["DOCUMENT_ROOT"]."/bitrix/footer.php");?>
    

    Теперь создаем страницу section.php и размещаем на ней компонент (bitrix:catalog.section), в настройках которого задаем необходимый инфоблок, а также заполняем поле Имя массива со значениями фильтра для фильтрации элементов значением arrFilter.

    Перед подключением компонента добавляем следующий код:

    $year = intval($_GET["YEAR"]); 
    if ($year >= 1970 && $year <= 2015) 
    { 
    	$arrFilter=array("PROPERTY"=>array("YEAR"=>"$year")); 
    }
    

    В результате при переходе со стартовой страницы (например, по ссылке 2002) откроется список книг раздела с идентификатором 10, у которых выбранный год выпуска:

    Вычисляемые свойства SEO

    Начиная с версии 14.0.0, в форме редактирования инфоблока, его разделов и элементов доступна вкладка SEO. Данный функционал основан на паре следующих технологий:

    • хранение - под хранением понимается механизм наследуемых свойств (значения свойств распространяются сверху вниз по иерархии инфоблоков: от инфоблока через разделы до элемента);
    • шаблонизатор - построитель шаблона с использованием подстановок и функций.

    Рассмотрим детально каждую из этих технологий.

    Хранение

    Все шаблоны, которые наследуются для вычисляемых наследуемых свойств, хранятся в таблице b_iblock_iproperty. Таблица одна, но в ней хранятся шаблоны для трех сущностей: для элементов, для разделов и для инфоблоков.

    Шаблоны привязываются к инфоблоку, разделу или элементу через пару полей: ENTITY_TYPE и ENTITY_ID. Чтобы определить какие шаблоны для какой сущности находятся, происходит внутренний поиск, используя существующие таблицы инфоблоков. Вычисленные значения хранятся в трех разных табличках: отдельно для элементов, отдельно для разделов и отдельно для инфоблоков.

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

    Классы наследуемых свойств используют всю мощь ООП и принадлежат новому ядру D7. Они лежат в пространстве имён Bitrix\Iblock\InheritedProperty. Пользоваться ими достаточно просто:

    use Bitrix\Iblock\InheritedProperty; 
    
    //ООП  ElementTemplates или SectionTemplates или IblockTemplates )) 
    $ipropTemplates = new InheritedProperty\ElementTemplates($IBLOCK_ID, $ELEMENT_ID);
    //Установить шаблон для элемента 
    $ipropTemplates->set(array(
    	"MY_PROP_CODE" => "{=this.Name}",
    	"SOME_CODE" => "", //Удалить шаблон
    ));
    //Получить шаблоны для "редактирования" 
    $templates = $ipropTemplates->findTemplates();
    //Удалить все собственные шаблоны элемента 
    $ipropTemplates->delete();
    
    //ООП  ElementValues или SectionValues или IblockValues )) 
    $ipropValues = new InheritedProperty\ElementValues($IBLOCK_ID, $ELEMENT_ID);
    //Получить значения 
    $values = $ipropValues->getValues();
    echo $values [" MY_PROP_CODE "]; 
    //Сбросить кеш 
    $ipropValues->clearValues(); 
    

    • Создаем экземпляр класса в зависимости от типа сущности (для элементов это будет ElementTemplates, для разделов - SectionTemplates и для инфоблока - IblockTemplates).
    • Используем метод set для манипуляций шаблонами.

      Примечание: на текущий момент метод set используется в методах Add и Update классов CIBlock, CIBlockSection и CIBlockElement (обрабатывается поле IPROPERTY_TEMPLATES).

    • Чтобы выбирать данные, которые вычисляются в процессе выборки по заданным шаблонам, используем метод getValues (его можно встретить в компонентах инфоблоков, чтобы SEO свойства выбрать и поставить их на странице).
    • Метод clearValues позволяет сбросить закешированные значения и пересчитать.

    Шаблоны

    Шаблоны строятся независимо от механизма хранения, что позволяет использовать динамичные формы. Для построения шаблона используются следующие составляющие:

    • Во-первых, это просто текст, который вычисляется в такой же простой текст.
    • Во-вторых, это подстановки, которые начинаются внутри фигурных скобок знаком равно (например, {=this.Name}). Такой псевдообъектный синтаксис позволил реализовать экономную модель с отложенными запросами данных. В шаблоне могут использоваться следующие области: this, parent, sections, iblock, property или catalog. Поля могут быть самыми разными: name, code, previewtext, detailtext, property_CODE и т.д. (см. файлы с классами в папке /bitrix/modules/iblock/lib). Количество запросов к БД напрямую зависит от количества областей, использованных в шаблоне.
    • В-третьих, это функции (например, {=concat " \ " "!" iblock.name sections.name this.name}). Есть набор встроенных функций (upper, lower, translit, concat, limit, contrast, min, max и distinct) и событие OnTemplateGetFunctionClass, которое позволяет написать собственную функцию (см. пример функции).

    Шаблоны могут иметь модификаторы: приведение к нижнему регистру (/l) и транслитерация (/t-). В интерфейсе вкладки SEO они представлены отдельными чекбоксами.

    Кроме того, все шаблоны поддерживают вложенность. Например:

    //Для элемента берутся анонсовый и детальный тексты его раздела, соединяются вместе, затем выбираются 
    //первые 50 слов. После чего они соединяются с первыми 50 словами текста анонса элемента. 
    //Из них выбирается 20 самых контрастных и все они приводятся к нижнему регистру.
    
    {=lower {=contrast 20 " .,?!" {=limit 50 " .,?!" this.previewtext} {=limit 50 " .,?!" parent.previewtext parent.detailtext}}}
    

    Рассмотрим пример кода шаблона:

    use Bitrix\Iblock\Template;
    //Подключение модуля инфоблоков.
    if (\Bitrix\Main\Loader::includeModule('iblock'))
    {
    	//Задаём шаблон.
    	$template = "Name: {=this.Name}. Code:{=this.code}";
    	//Исходные данные будем брать из элемента.
    	$entity = new Template\Entity\Element($ELEMENT_ID);
    	//Не забываем про безопасность.
    	echo \Bitrix\Main\Text\HtmlFilter::encode(
    		//Вычисляем значение по шаблону.
    		Template\Engine::process($entity, $template) 
    	);
    }
    

    Должен быть создан объект entity. Парсинг и вычисление шаблона обернуто статическим методом process, в который передаются entity и template. Кроме того, метод process можно использовать в цикле с одним entity по разным template, причем данные будут «переиспользованы», т.е. лишних запросов не будет. Также обратите внимание на метод htmlEncode, который используется для формирования безопасного html.


    Фасетный поиск

    Умный фильтр

    Компонент Умный фильтр доступен в продукте, начиная с версии 12.0 модуля Информационные блоки. При всех своих достоинствах он обладал существенным недостатком - в каталогах больших размеров фильтрация элементов занимала продолжительное время. Для решения этой проблемы в версии 15.0 была добавлена технология фасетного поиска.

    Фасетная классификация – это совокупность нескольких независимых классификаций, осуществляемых одновременно по различным основаниям. Применительно к информационным блокам термин фасета означает фильтрацию по свойствам и ценам.

    При разработке фасетного индекса упор был сделан на максимальное использование возможностей нового ядра. Взаимодействие с другим кодом происходит с помощью 3-х методов специального класса:

    Use \Bitrix\Iblock\PropertyIndex;
    PropertyIndex\Manager::updateElementIndex($iblockId, $elementId);
    PropertyIndex\Manager::deleteElementIndex($iblockId, $elementId);
    PropertyIndex\Manager::markAsInvalid($iblockId);
    

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

    CRE ATE   TABLE b_iblock_X_index (
    	SECTION_ID INT NOT NULL,
    	ELEMENT_ID INT NOT NULL,
    	FACET_ID   INT NOT NULL,
    	VALUE      INT NOT NULL,
    	VALUE_NUM  FLOAT NOT NULL,
    	INCLUDE_SUBSECTIONS VARCHAR(1) NOT NULL,
    	KEY IX_b_iblock_X_index_0 (SECTION_ID,ELEMENT_ID,FACET_ID, VALUE_NUM, VALUE, ELEMENT_ID),
    	KEY IX_b_iblock_X_index_1 (ELEMENT_ID, SECTION_ID, FACET_ID)
    	);
    CRE ATE   TABLE b_iblock_X_index_val (
    	ID    INT NOT NULL AUTO_INCREMENT,
    	VALUE VARCHAR(2000),
    	PRIMARY KEY (ID)
    );
    

    Рассмотрим детальнее первую таблицу. Для этого в административном разделе сайта перейдите на страницу Настройки > Производительность > Таблицы и выберите таблицу b_iblock_X_index (где Х - идентификатор вашего каталога). Данные рассмотрим на примере конкретного элемента каталога (в нашем случае для элемента с идентификатором 36):

    По колонке SECTION_ID можно видеть, что данные сохранены не только для раздела элемента (в колонке INCLUDE_SUBSECTIONS стоит 1), но и всех его родителей (в колонке INCLUDE_SUBSECTIONS стоит 0).

    FACET_ID - это синтетический идентификатор цен и свойств. Для цен используются нечётные идентификаторы (идентификатор цены умножается на 2 и прибавляется 1), а для свойств - четные (идентификатор свойства умножается на 2). Именно этот прием позволяет строить комбинированные фильтры по цене и значениям свойств.

    Примечание: подробную информацию о работе с таблицами базы данных смотрите в главе Монитор производительности.

    Производительность

    Производительность умного фильтра на каталоге среднего размера.

    Рассмотрим работу умного фильтра на каталоге среднего размера: 20 тысяч товаров, 60 тысяч предложений и 780 тысяч значений свойств. Таким образом, каталог с товарами имеет следующие характеристики:

    • таблица b_iblock_element содержит 80 тысяч записей, 20 Мбайт занимают данные и 8 Мб - индексы, средняя длина строки - 265 байт;
    • таблица b_iblock_element_property содержит 780 тысяч записей, 35 Мбайт занимают данные, индексы - 70 Мб, средняя длина строки - 44 байта.

    После того, как для каталога будут созданы фасетные индексы, таблица b_iblock_X_index будет содержать 520 тыс. записей, данные будут занимать 12 Мбайт, индексы - 30 Мб, средняя длина строки - 24 байта.

    На следующей диаграмме представлено время работы компонента умного фильтра без фильтрации на одном из корневых разделов каталога с 10 тысячами элементов, подразделе с 3 тысячами товаров и подразделе подраздела со 100 товарами:

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

    Если посмотреть показатели с установленным фильтром по цене, то видим, что использование фасетного индекса также эффективно:

    Хорошие показатели наблюдаются и при различных вариантах фильтрации каталога по цене и свойствам (например, по свойству Цвет):

    Кроме того, использование фасетных индексов позволяет снизить и стабилизировать расход памяти PHP:

    Это связано с тем, что вся работа по группировке товаров ложится на базу данных. Часть работы выполняется на этапе индексации, а часть - при выборке, опираясь на мощную поддержку в виде фасетного индекса. Поэтому на долю PHP остается малая часть, которая выполняется быстро и эффективно.


    План действий при проблемах

    При возникновении проблем в работе проекта для их устранения рекомендуем воспользоваться следующим алгоритмом:

    • Ставьте перед собой конкретную цель. Так как процесс оптимизации, улучшения – бесконечный. Например: каждая страница с каталогом ваших товаров должна открываться за определенное время, скажем 0,2 секунды. Только установка таких конкретных целей и их решение может быть эффективна, в отличие от неясных и формализованных пожеланий, "чтобы лучше работало".
    • С помощью страницы отладки (Настройки > Производительность > SQL Запросы) найти и убрать лишние запросы.

      Лишний запрос – это:

      • Запрос в цикле (вынести из цикла в переменную).
      • Запросы, которые добирают данные в цикле. (Лучше собрать данные в фильтре, а потом одним запросом вывести данные, потом дополнительный цикл – разложить данные) в этом случае запросы не зависят линейно от количества элементов.
    • Убрать из запросов неиспользуемые данные ($arSelect). Точно указывать поля, которые впоследствии будут использоваться. Это в разы повышает производительность, потому что такое четкое указание используемых данных означает для базы данных сокращение объемов сортировки. База данных производит такую сортировку не на жестком диске, а в оперативной памяти.
    • Оценить возможность использования PROPERTY_* в сочетании с инфоблоками 2.0. Инфоблоки 2.0 хранят свои свойства в отдельной таблице. Эта таблица при упоминании PROPERTY_* присоединяется на этапе выборки. И до выборки значения свойств присоединение этих свойств не происходит.

      Когда это не применимо? Например, при малой выборке из большого числа элементов (10 новостей из нескольких тысяч записей). Момент этот не очень однозначный, и малозависим от разработчика. Причина в том, что результат выборки из инфоблоков и инфоблоков 2.0 – разный. В простом инфоблоке при выборке записи начинают размножаться. А инфоблоки 2.0 возвращают массив значения свойства. Если код в шаблоне не предусматривает эту ситуацию, то смена обычных инфоблоков на инфоблоки 2.0 приведет только к тому, что шаблон «развалится».

    • Посмотреть план исполнения самых тяжелых запросов и добавить/удалить индексы.

    Список ссылок по теме:



    Highload-блоки

    Поскольку модуль Информационные блоки считается очень "тяжелым" для создания легких справочников или хранения большого количества данных (когда модуль может вести себя не очень оптимально), то появилась потребность в создании аналога инфоблоков (модуль Highload-блоки), но гораздо проще и "полегче". Новый модуль доступен с версии 14.0 продукта и написан на новом ядре D7. Структура данных этого модуля предполагает возможность использования в нагруженных проектах.

    Примечание: Highload-блоки и традиционные инфоблоки - это разные вещи. Поэтому нет возможности конвертации традиционных инфоблоков в Highload-блоки. Это все равно как конвертировать форум в wiki. Технически можно создать какую-то аналогичную структуру и перенести данные. Но это имеет смысл только для конкретного проекта при определенной необходимости.

    Highload-блоки

    Highload-блоки - это быстрые справочники, без поддержки иерархии, с ограниченной поддержкой свойств. Они могут обращаться к БД в том числе и через HandlerSocket и работать с большими объёмами данных. Highload-блоки хранят элементы в своих таблицах и используют свои индексы.

    Примечание: Кроме модуля Highload-блоки, в ядре D7 реализована возможность обращения к БД через HandlerSocket для всех сущностей ORM.

    Раньше данные хранились с использованием модуля, который закрывал таблицы - паттерн Table Module. В Highload-блоках дополнительно создан слой, который называется Table Data Gateway, который поддерживает всю технику работы с таблицами, а бизнес-логика находится в расширяющем классе.

    Highload-блоки - это логика для работы с данными, не более. Для конкретного применения данных нужно предусмотреть реализацию бизнес-логики самим приложением. Чтобы реализовать эту бизнес-логику надо расширить класс Highloadblock:

    use Bitrix\Highloadblock as HL;
    $hlblock   = HL\HighloadBlockTable::getById( # )->fetch();
    $entity   = HL\HighloadBlockTable::compileEntity( $hlblock ); //генерация класса 
    $entityClass = $entity->getDataClass();
    
    class MyDomainObjectTable extends #entityClass# {
    …//наша бизнес логика проекта, посмотрите содержимое $entityClass и впишите его в #entityClass#
    } 
    

    Преимущества Highload-блоков

    • Низкие накладные расходы (PHP, меньше запросов SQL).
    • Низкий риск блокировок в БД: так как данные хранятся в своих таблицах, нет единой, глобальной таблицы, которая может заблокироваться при больших нагрузках (выборка и одновременно импорт).
    • Тысячи, миллионы сущностей, справочники.
    • Снижение нагрузки на БД, хостинг.

    О производительности и ресурсах

    Highloadblock - это прослойка между пользователем и ORM. ООП само по себе ведет к бо́льшему потреблению памяти и CPU, предлагая взамен более удобную и эффективную разработку. Highloadblock предназначены для высоких нагрузок. Их преимущества лежат в области архитектуры, то есть на них проще достичь масштабируемости, проще управлять разработкой, проще учитывать риски.

    Выигрыш будет именно при работе с БД. Разработчик получает:

    • отдельные таблицы для сущностей (что может стать заметным на больших объемах),
    • возможность легко расставлять индексы для нужных вам выборок,
    • возможность прозрачно использовать handlersocket, который может заметно снизить нагрузку на СУБД.

    Архитектура модуля

    Описание

    В отличие от модуля Информационные блоки в модуле Highload-блоки предусматривается хранение каждой сущности в своей таблице, то есть формально не принадлежат какому-либо модулю. Поля сущностей - это [ds]пользовательские свойства ядра[/ds][di]Необходимо отличать Пользовательские поля в модулях системы и свойства используемые в рамках инфоблоков , хотя в формах системы (форма создания/редактирования пользователя, форме создания/редактирования раздела инфоблока и другие) используется термин пользовательские свойства.

    Подробнее ...[/di]. Сущности конструируются в административном интерфейсе, то есть без дополнительного программирования.

    Доступ к данным предоставляется на основе ORM.

    Разнесение данных может быть не только по таблицам одной базы данных, но в принципе оно может быть и по разным базам данных. Таким образом, становится возможным горизонтальное масштабирование проекта, когда одни объекты живут на одном сервере, а другие - на другом.

    В модуле Highload-блоки добавлена поддержка концепции NoSQL на основе базы данных MySQL. Соединение с базой данных предусмотрено как с помощью расширения handlersocket для MySQL, так и без него. Если используется handlersocket, то необходимо произвести дополнительные настройки.

    Допустим, что в проекте сконфигурирована поддержка NoSQL, стоят все необходимые драйверы к MySQL и в конфигурации ядра указано соединение через эти драйверы. В результате модуль Highload-блоки позволяет создавать сущности и строить проект так, чтобы очень быстро осуществлялась выборка из базы через драйвер по первичному ключу.

    Чтобы воспользоваться ORM-сущностью для Highload-блоков, необходимо выполнить следующие действия:

    • Инициализировать сущность в системе:
      //сначала выбрать информацию о ней из базы данных
      $hldata = Bitrix\Highloadblock\HighloadBlockTable::getById($ID)->fetch();
      
      //затем инициализировать класс сущности
      $hlentity = Bitrix\Highloadblock\HighloadBlockTable::compileEntity($hldata);
      
    • Дальше можно создавать запросы с данной сущностью:
      $query = new Entity\Query($hlentity);
      
      или использовать ее через интерфейс DataManager:
      $hlDataClass = $hldata['NAME'].'Table';
      $hlDataClass::getList();
      

    Вся дальнейшая работа с Highload-блоками подчиняется законам ORM, поскольку созданная в модуле Highload-блоки сущность является ORM-сущностью.

    Модуль включает в себя два компонента: список записей сущностей (highloadblock.list) и детальный просмотр сущности (highloadblock.view).

    Привязка справочника

    Привязка справочника к Highload-блоку осуществляется через свойства USER_TYPE_SETTINGS. В них указывается имя таблицы.

    Привязка значений свойств идет по UF_XML_ID (поле должно быть обязательно заполнено), иначе значения свойства "справочник" не сохраняются у элемента.

    Highloadblock и handlersocket

    Традиционные ACID Базы данных в целом ряде задач затрудняют реализацию проектов. Для решения этих задач были предложены технологии NoSQL и HandlerSocket (в виде плагина к обычной MySQL).

    HandlerSocket позволяет клиентскому приложению подключаться напрямую к движку данных MySQL для устранения избыточной нагрузки, характерной для традиционных запросов через интерфейс SQL и неприемлемой для высоконагруженных БД.

    Примерное сравнение числа запросов, допускаемых разными способами работы с БД:

    Способ доступаЧисло запросов в секундуНагрузка на CPU
    MySQL через SQL-клиент 105 000 Пользовательские процессы: 60%
    Системные процессы: 28%
    memcached 420 000 Пользовательские процессы: 8%
    Системные процессы: 88%
    MySQL через HandlerSocket-клиента 750 000 Пользовательские процессы: 45%
    Системные процессы: 53%

    Как работает memcache мы уже описывали. Разница между обращением через MySQL-клиент и через HandlerSocket заключается в том, что во втором случае минуется парсинг, открытие таблиц, оптимизация планов исполнения. То есть обращение происходит напрямую. Нагрузка на MySQL резко снижается. Запросы не выполняются быстрее, они меньше нагружают сервер.

    Подключение HandlerSocket задаётся в файле настроек параметров ядра.

    Установить сам плагин можно либо скачав исходники MySQL и собрав плагин, либо установить [dw]PerconaServer[/dw][di]С версии Percona 5.7 этот функционал более не доступен.[/di] или MariaDB, в которых он включён по умолчанию.

    При обращении к Highloadblock ($obj = $entityClass::getById( $arData["ID"] )->fetch();) через HandlerSocket происходит вызов HS API (обращение open_index и find в MySQL) и обработка результата вызова приложением.

    HandlerSocket запускает внутри БД пул потоков работающих в асинхронном режиме. (Аналогично работает NGINX.) Если обычно MySQL для каждого клиентского соединения поднимает один поток (thread) и работает внутри его, то в случае с HandlerSocket вводится пул потоков c использованием мультиплексирующих системных вызовов pool/select, поэтому, например, 5 потоков могут обработать сотни тысяч запросов.

    Примеры работы с Highload-блоками

    <?
    //Подготовка:
    if (CModule::IncludeModule('highloadblock')) {
    	$arHLBlock = Bitrix\Highloadblock\HighloadBlockTable::getById(1)->fetch();
    	$obEntity = Bitrix\Highloadblock\HighloadBlockTable::compileEntity($arHLBlock);
    	$strEntityDataClass = $obEntity->getDataClass();
    }
    
    //Добавление:
    if (CModule::IncludeModule('highloadblock')) {
    	$arElementFields = array(
    		'UF_NAME' => $arPost['name'],
    		'UF_MESSAGE' => $arPost['message'],
    		'UF_DATETIME' => new \Bitrix\Main\Type\DateTime
    	);
    	$obResult = $strEntityDataClass::add($arElementFields);
    	$ID = $obResult->getID();
    	$bSuccess = $obResult->isSuccess();
    }
    
    //Получение списка:
    if (CModule::IncludeModule('highloadblock')) {
    	$rsData = $strEntityDataClass::getList(array(
    		'select' => array('ID','UF_NAME','UF_MESSAGE','UF_DATETIME'),
    		'order' => array('ID' => 'ASC'),
    		'limit' => '50',
    	));
    	while ($arItem = $rsData->Fetch()) {
    		$arItems[] = $arItem;
    	}
    }
    ?>

    Выбор случайного значения:

    $q = new Entity\Query($entity);
    	$q->setSelect(array('*'));
    	$q->setFilter($arFilter);
    	$q->setLimit(1);
    	$q->registerRuntimeField(
    		'RAND', array('data_type' => 'float', 'expression' => array('RAND()'))
    	);
    	$q->addOrder("RAND", "ASC");
    	$result = $q->exec();
    

    Практика. Работа с элементами, разделами и свойствами

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

    Работа с пользовательскими свойствами инфоблоков

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

  • Получить значения всех свойств элемента, зная его ID
  • Получить свойства элементов, используя метод CIBlockElement::GetList
  • Добавить свойство типа TEXT/html для элемента
  • Заполнить множественное свойство типа Файл
  • Заполнить множественное свойство типа Список
  • Получить пользовательское свойство раздела
  • Пример создания своего типа данных для пользовательского свойства
  • Как удалить файл в свойстве элемента инфоблока
  • Задача 1:
    Получить значения всех свойств элемента, зная его ID.

    1 <? $db_props = CIBlockElement::GetProperty(IBLOCK_ID, ELEMENT_ID, "sort", "asc", array());
    2 $PROPS = array();
    3 while($ar_props = $db_props->GetNext())
    4 $PROPS[$ar_props['CODE']] = $ar_props['VALUE'];?>

    Теперь символьный код свойства является ключом ассоциативного массива $PROPS, то есть, если вам нужно значение свойства с кодом price, то оно будет храниться в $PROPS['price'].

    Задача 2:
    Получить свойства элементов, используя метод CIBlockElement::GetList

    1	<? $arSelect = array("ID", "NAME", "PROPERTY_prop_code_1", "PROPERTY_prop_code_2");
    2 $res = CIBlockElement::GetList(array(), array(), false, array(), $arSelect);?>

    Дальше использовать цикл и получить свойства с символьными кодами prop_code_1 и prop_code_2.

    Советы веб-разработчиков.

    Антон Долганин: Если для какого-либо изменения в БД предусмотрен специальный метод, следует использовать именно его, а не более общий метод изменения БД.

    Хороший пример: модуль интернет-магазина и работа с заказом. Можно изменить флаг оплаты заказа путем CSaleOrder::Update, а можно путем CSaleOrder::PayOrder. Так вот, следует применять именно PayOrder, потому что в нем произойдет вызов соответствующих обработчиков.

    Задача 3:
    Добавить свойство типа TEXT/html для элемента.

    Если свойство не множественное:

    01 <? $element = new CIBlockElement;
    02 $PROP = array();
    03 $PROP['символьный код свойства']['VALUE']['TYPE'] = 'text'; // или html
    04 $PROP['символьный код свойства']['VALUE']['TEXT'] = 'значение, которое нужно забить';
    05 $arLoadArray = array(
    06 	"IBLOCK_ID"      => IBLOCK_ID,
    07 	"PROPERTY_VALUES"=> $PROP,
    08 	"NAME"           => "Название элемента"
    09 	);
    10 	$element->Add($arLoadArray);?>

    Если свойство множественное:

    01	<? // В $ITEMS хранятся значения множественного свойства, которое нужно забить
    02 foreach($ITEMS as $item)
    03 {
    04 	$VALUES[]['VALUE'] = array(
    05 	'TYPE' => 'text', // или html
    06 	'TEXT' => $item,
    07 	);
    08	}
    09	$element = new CIBlockElement;
    10	$PROPS = array();
    11	$PROPS['символьный код свойства'] = $VALUES;
    12	$arLoadArray = array(
    13	  "IBLOCK_ID"      => IBLOCK_ID,
    14	  "PROPERTY_VALUES"=> $PROPS,
    15	  "NAME"           => "Название элемента"
    16	  );
    17 $element->Add($arLoadArray);?>
    Задача 4:

    Заполнить множественное свойство типа Файл. Довольно часто при добавлении элемента в инфоблок может понадобиться привязать к нему несколько файлов. Для этого удобно создать у инфоблока множественное свойство типа Файл и хранить файлы в нём. Пример заполнения свойства:

    01 <?
    02 $arFiles = array();
    03 for($i = 1; $i < 10; $i++)
    04 {
    05 	if(file_exists($_SERVER['DOCUMENT_ROOT'].'/images/image_'.$i.'.jpg'))
    06 	{
    07 		$arFiles[] = array('VALUE' => CFile::MakeFileArray($_SERVER["DOCUMENT_ROOT"].'/images/image_'.$i.'.jpg'), 'DESCRIPTION' => '');
    08 	}
    09 }
    10 ?>

    После этого массив $arFiles передается как значение свойства при добавлении элемента.

    Задача 5:

    Заполнить множественное свойство типа Список с отображением в виде флажков. В данном случае у каждого элемента списка значений есть свой ID. Посмотреть их можно, зайдя в детальное редактирование свойства. Заполняется свойство следующим образом:

    1 <?
    2 if($first_condition == true) $values[] = array('VALUE' => 1);
    3 if($second_condition == true) $values[] = array('VALUE' => 2);
    4 CIBlockElement::SetPropertyValuesEx($ELEMENT_ID, $IBLOCK_ID, array('property_code' => $values));
    5 ?>

    В данном случае при выполнении первого и второго условий мы отмечаем флажками элементы списка с ID =1 и ID=2 соответственно. Заменить следует $ELEMENT_ID, $IBLOCK_ID и property_code на нужные значения.

    Задача 6:
    Получить пользовательское свойство раздела

    1 <? $section_props = CIBlockSection::GetList(array(), array('IBLOCK_ID' => IBLOCK_ID, 'ID' => SECTION_ID), true, array("UF_ADDITIONAL_PRICE"));
    2 $props_array = $section_props->GetNext(); ?>

    Теперь в $props_array['UF_ADDITIONAL_PRICE'] лежит значение свойства UF_ADDITIONAL_PRICE раздела инфоблока.

    Совет от веб-разработчиков.

    Алексей Коваленко: При работе с инфоблоками удобнее все коды свойств именовать заглавными буквами. В таком случае вы сможете избежать небольших несостыковок в своей работе.

    Например, значение свойства с кодом foto при работе с компонентами часто доступно через [PROPERTIES][foto][VALUE]?, а при использовании метода GetList вы можете получить PROPERTY_FOTO_VALUE.

    Пример создания своего типа данных для пользовательского свойства

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

    Один из вариантов реализации: хранить изображения в отдельном инфоблоке и показывать как привязку к элементу. Пример кода:

    AddEventHandler("iblock", "OnIBlockPropertyBuildList", array("CIBlockPropertyPicture", "GetUserTypeDescription"));
    AddEventHandler("iblock", "OnBeforeIBlockElementDelete", array("CIBlockPropertyPicture", "OnBeforeIBlockElementDelete"));
    class CIBlockPropertyPicture
    {
    	function GetUserTypeDescription()
    	{
    		return array(
    			"PROPERTY_TYPE"      =>"E",
    			"USER_TYPE"      =>"Picture",
    			"DESCRIPTION"      =>"Картинка",
    			"GetPropertyFieldHtml" =>array("CIBlockPropertyPicture", "GetPropertyFieldHtml"),
    			"GetPublicViewHTML" =>array("CIBlockPropertyPicture", "GetPublicViewHTML"),
    			"ConvertToDB" =>array("CIBlockPropertyPicture", "ConvertToDB"),
    			//"GetPublicEditHTML" =>array("CIBlockPropertyPicture","GetPublicEditHTML"),
    			//"GetAdminListViewHTML" =>array("CIBlockPropertyPicture","GetAdminListViewHTML"),
    			//"CheckFields" =>array("CIBlockPropertyPicture","CheckFields"),
    			//"ConvertFromDB" =>array("CIBlockPropertyPicture","ConvertFromDB"),
    			//"GetLength" =>array("CIBlockPropertyPicture","GetLength"),
    			);
    	}
    	function GetPropertyFieldHtml($arProperty, $value, $strHTMLControlName)
    	{
    		$LINK_IBLOCK_ID = intval($arProperty["LINK_IBLOCK_ID"]);
    		if($LINK_IBLOCK_ID)
    		{
    			$ELEMENT_ID = intval($value["VALUE"]);
    			if($ELEMENT_ID)
    			{
    				$rsElement = CIBlockElement::GetList(array(), array("IBLOCK_ID" => $arProperty["LINK_IBLOCK_ID"], "ID" => $value["VALUE"]), false, false, array("ID", "PREVIEW_PICTURE", "DETAIL_PICTURE"));
    			$arElement = $rsElement->Fetch();
    			if(is_array($arElement))
    				$file_id = $arElement["DETAIL_PICTURE"];
    			else
    				$file_id = 0;
    			}
    			else
    			{
    				$file_id = 0;
    			}
    			if($file_id)
    			{
    				$db_img = CFile::GetByID($file_id);
    				$db_img_arr = $db_img->Fetch();
    			if($db_img_arr)
    			{
    				$strImageStorePath = COption::GetOptionString("main", "upload_dir", "upload");
    				$sImagePath = "/".$strImageStorePath."/".$db_img_arr["SUBDIR"]."/".$db_img_arr["FILE_NAME"];
    				return '<label><input name="'.$strHTMLControlName["VALUE"].'[del]" value="Y" type="checkbox">Удалить файл '.$sImagePath.'</label>.'<input name="'.$strHTMLControlName["VALUE"].'[old]" value="'.$ELEMENT_ID.'" type="hidden">';
    			}
    			}
    			return '<input type="file" size="'.$arProperty["COL_COUNT"].'" name="'.$strHTMLControlName["VALUE"].'"/>';
    		}
    		else
    		{
             return "Ошибка настройки свойства. Укажите инфоблок в котором будут храниться картинки.";
    		}
    	}
    	function GetPublicViewHTML($arProperty, $value, $strHTMLControlName)
    	{
    		$LINK_IBLOCK_ID = intval($arProperty["LINK_IBLOCK_ID"]);
    		if($LINK_IBLOCK_ID)
    		{
    			$ELEMENT_ID = intval($value["VALUE"]);
    			if($ELEMENT_ID)
    			{
    				$rsElement = CIBlockElement::GetList(array(), array("IBLOCK_ID" => $arProperty["LINK_IBLOCK_ID"], "ID" => $value["VALUE"]), false, false, array("ID", "PREVIEW_PICTURE", "DETAIL_PICTURE"));
    				$arElement = $rsElement->Fetch();
    				if(is_array($arElement))
    					return CFile::Show2Images($arElement["PREVIEW_PICTURE"], $arElement["DETAIL_PICTURE"]);
    			}
    		}
    		return "";
    	}
    	function ConvertToDB($arProperty, $value)
    	{
    		$arResult = array("VALUE" => "", "DESCRIPTION" => "");
    		$LINK_IBLOCK_ID = intval($arProperty["LINK_IBLOCK_ID"]);
    		if($LINK_IBLOCK_ID)
    		{
    			if(
    				is_array($value["VALUE"])
    				&& is_array($value["VALUE"]["error"])
    				&& $value["VALUE"]["error"]["VALUE"] == 0
    				&& $value["VALUE"]["size"]["VALUE"] > 0
    			)
    			{
    				$arDetailPicture =  array(
    					"name" => $value["VALUE"]["name"]["VALUE"],
    					"type" => $value["VALUE"]["type"]["VALUE"],
    					"tmp_name" => $value["VALUE"]["tmp_name"]["VALUE"],
    					"error" => $value["VALUE"]["error"]["VALUE"],
    					"size" => $value["VALUE"]["size"]["VALUE"],
    				);
    				$obElement = new CIBlockElement;
    				$arResult["VALUE"] = $obElement->Add(array(
    					"IBLOCK_ID" => $LINK_IBLOCK_ID,
    					"NAME" => $arDetailPicture["name"],
    					"DETAIL_PICTURE" => $arDetailPicture,
    				), false, false, true);
    			}
    			elseif(
    				is_array($value["VALUE"])
    				&& isset($value["VALUE"]["size"])
    				&& !is_array($value["VALUE"]["size"])
    				&& $value["VALUE"]["size"] > 0
    			)
    			{
    				$arDetailPicture =  array(
    					"name" => $value["VALUE"]["name"],
    					"type" => $value["VALUE"]["type"],
    					"tmp_name" => $value["VALUE"]["tmp_name"],
    					"error" => intval($value["VALUE"]["error"]),
    					"size" => $value["VALUE"]["size"],
    					);
    					$obElement = new CIBlockElement;
    					$arResult["VALUE"] = $obElement->Add(array(
    					"IBLOCK_ID" => $LINK_IBLOCK_ID,
    					"NAME" => $arDetailPicture["name"],
    					"DETAIL_PICTURE" => $arDetailPicture,
    				), false, false, true);
    			}
    			elseif($value["VALUE"]["del"])
    			{
    				$obElement = new CIBlockElement;
    				$obElement->Delete($value["VALUE"]["old"]);
    			}
    			elseif($value["VALUE"]["old"])
    			{
    				$arResult["VALUE"] = $value["VALUE"]["old"];
    			}
    				elseif(!is_array($value["VALUE"]) && intval($value["VALUE"]))
    			{
    				$arResult["VALUE"] = $value["VALUE"];
    			}
    		}
    		return $arResult;
    	}
    	function OnBeforeIBlockElementDelete($ELEMENT_ID)
    	{
    		$arProperties = array();
    		$rsElement = CIBlockElement::GetList(array(), array("ID" => $ELEMENT_ID), false, false, array("ID", "IBLOCK_ID"));
    		$arElement = $rsElement->Fetch();
    		if($arElement)
    		{
    			$rsProperties = CIBlockProperty::GetList(array(), array("IBLOCK_ID" => $arElement["IBLOCK_ID"], "USER_TYPE" => "Picture"));
    			while($arProperty = $rsProperties->Fetch())
                $arProperties[] = $arProperty;
    		}
    		$arElements = array();
    		foreach($arProperties as $arProperty)
    		{
             $rsPropValues = CIBlockElement::GetProperty($arElement["IBLOCK_ID"], $arElement["ID"], array(), array(
                "EMPTY" => "N",
                "ID" => $arProperty["ID"],
    				));
    				while($arPropValue = $rsPropValues->Fetch())
    			{
    					$ID = intval($arPropValue["VALUE"]);
    					if($ID > 0)
    						$arElements[$ID] = $ID;
    			}
    		}
    		foreach($arElements as $to_delete)
    		{
             CIBlockElement::Delete($to_delete);
    		}
    	}
    }

    Что мы в итоге имеем:

    • Интерфейс редактирования элемента с возможностью добавления и удаления изображений.
    • При удалении элемента связанная с ним информация удаляется.
    • Поддержка компонента в публичной части.

    Инструкция по применению:

    • Этот код разместите в файле /bitrix/php_interface/init.php.
    • Создайте инфоблок для хранения изображений и в его настройках укажите параметры генерации картинки предварительного просмотра из детальной (на вкладке Поля).
    • В инфоблоке Гостиницы добавьте свойство типа Картинка и в дополнительных настройках этого свойства укажите созданный на первом шаге инфоблок. Не забудьте указать символьный код свойства.
    • Создайте элемент и "поиграйтесь" со значениями этого свойства.
    • В публичной части, например в компоненте news, в параметрах настройки списка элементов выбрать это свойство.

    Как удалить файл в свойстве элемента инфоблока

    Обновить любое свойство можно с помощью методов:

    При использовании любого метода на ключ массива обновления идет код свойства, а значение - новое значение. Для удаления файла нам надо передать вот такой простой массив:

    array('MY_FILE' => array('XXX' => array('del' => 'Y')));

    Способ универсален и подходит что для инфоблоков, что для инфоблоков 2.0, что для документооборота. MY_FILE - это код вашего свойства типа Файл. А что такое ХХХ? В нём содержится ID значения_ свойства. То есть не ID свойства, а именно ID значения.

    CModule::IncludeModule('iblock');
    $IB = 24;
    $ID = 220304;
    $CODE = 'ONE_FL';
    if ($arProp = CIBlockElement::GetProperty($IB, $ID, 'ID', 'DESC', array('CODE' => $CODE))->fetch()) {
    	$XXX = $arProp['PROPERTY_VALUE_ID'];
    	CIBlockElement::SetPropertyValueCode($ID, $CODE, array($XXX => array('del' => 'Y')));
    }

    Вот таким образом получается этот универсальный XXX, именно так его и надо передавать для каждого файла, который мы хотим удалить.

    Что делать в случае множественного файла? Как удалить конкретный файл в списке? Все просто - используем в примере выше не if, а while, ну и дополнительно фильтруем, какой файл надо удалить.


    Примеры работы с множественными свойствами

    Задача 1: удаление одного из значений множественного свойства элемента инфоблока.

    Решение:

    $el = new CIBlockElement;
    $PROP = array();
    $PROP[property_id][id] = "4";  
    $PROP[property_id][id] = "5";
    $PROP[property_id][id] = "6";
    
    $arLoadProductArray = Array(
    	"IBLOCK_ID" => $B_ID,
    	"PROPERTY_VALUES" => $PROP,
    	"NAME" => "Element",
    	);
    
    $PRODUCT_ID = $E_ID;
    $res = $el->Update($PRODUCT_ID, $arLoadProductArray);
    

    При этом для удаления достаточно исключить из массива $PROP пару: ключ и значение удаляемого свойства. Данное решение является оптимальным в ситуации, когда необходимо сохранить id значения свойства прежним:

    $PROP[property_id ][id ]

    Также вариантом решения задачи может стать использование метода SetPropertyValues:

    CIBlockElement::SetPropertyValues($ELEMENT_ID, $IBLOCK_ID, $PROPERTY_VALUE, $PROPERTY_CODE);

    в четвёртый параметр функции следует передавать false, а в третий - массив "код свойства"=>"значение".

    При этом все значения будут удалены кроме тех, которые указаны в массиве, переданном в третий параметр.


    Задача 2: добавление определенного значения для множественного свойства типа файл:

    Решение:

    //FILES - символьный код множественного свойства типа файл;
    
    $ELEMENT_ID = 392;
    $arFile = CFile::MakeFileArray($_SERVER["DOCUMENT_ROOT"]."/images/help.gif");
    $arFile["MODULE_ID"] = "iblock";
    
    CIBlockElement::SetPropertyValueCode($ELEMENT_ID, "FILES", Array("VALUE"=>$arFile)  );
    

    Задача 3: добавление нескольких значений для множественного свойства типа файл:

    Решение:

    $arFile = array(
    0 => array("VALUE" => CFile::MakeFileArray($_SERVER["DOCUMENT_ROOT"]."/images/01.gif"),"DESCRIPTION"=>""),
    1 => array("VALUE" => CFile::MakeFileArray($_SERVER["DOCUMENT_ROOT"]."/images/help.gif"),"DESCRIPTION"=>"")
    );
    CIBlockElement::SetPropertyValueCode($ELEMENT_ID, $PROPERTY_CODE, $arFile);
    

    Задача 4: удаление определенного значения множественного свойства типа файл:

    Решение:

    //FILES - символьный код множественного свойства типа файл;
    //2033 - id значения свойства;
    
    $ELEMENT_ID = 392;
    $arFile["MODULE_ID"] = "iblock";
    $arFile["del"] = "Y";
    
    CIBlockElement::SetPropertyValueCode($ELEMENT_ID, "FILES", Array ("2033" => Array("VALUE"=>$arFile) ) );
    

    Задача 5: обновление определенного значения множественного свойства типа файл:

    Решение:

    
    //FILES - символьный код множественного свойства типа файл;
    //2033 - id значения свойства;
    
    $ELEMENT_ID = 392;
    $arFile = CFile::MakeFileArray($_SERVER["DOCUMENT_ROOT"]."/images/help.gif");
    $arFile["MODULE_ID"] = "iblock";
    
    CIBlockElement::SetPropertyValueCode($ELEMENT_ID, "FILES", Array ("2033" => Array("VALUE"=>$arFile) ) );
    

    Задача 6: установка множественного свойства типа строка с полем для описания значения:

    Решение с помощью SetPropertyValueCode:

    $arValues = array(
    	0 => array("VALUE"=>"значение","DESCRIPTION"=>"описание значения"),
    	1 => array("VALUE"=>"значение2","DESCRIPTION"=>"описание значения2") 
    );  
    CIBlockElement::SetPropertyValueCode($ELEMENT_ID, $PROP_CODE, $arValues);  
    

    Решение с помощью SetPropertyValuesEx:

    $PROPERTY_VALUE = array(
    	0 => array("VALUE"=>"значение","DESCRIPTION"=>"описание значения"),
    	1 => array("VALUE"=>"значение2","DESCRIPTION"=>"описание значения2") 
    );
    CIBlockElement::SetPropertyValuesEx($ELEMENT_ID, $IBLOCK_ID, array($PROPERTY_CODE => $PROPERTY_VALUE));
    

    Задача 7: обновление множественного свойства типа Текст и сохранение при этом DESCRIPTION:

    Решение:

    CIBlockElement::SetPropertyValues($nProductID, $nIblockID, array(
    	array(
    		"VALUE" => array(
    			"TEXT"=>time(),
    			"TYPE"=>"HTML"
    		),
      		"DESCRIPTION"=>"111"),
    	array(
    		"VALUE" => array(
    		"TEXT"=>time(),
    		"TYPE"=>"HTML"
    		),
    		"DESCRIPTION"=>"222"),
    	), $prop['ID']);
    

    Копирование значений полей элементов в свойства

    Рассмотрим пример функции, которая копирует значения полей элементов инфоблока ($_FROM_FIELD_NAMES) в свойства элементов инфоблока ($TO_PROPERTY_NAMES).

    Копироваться будут поля Начало активности (DATE_ACTIVE_FROM) и Окончание активности (DATE_ACTIVE_TO) в свойства DATE_BEGIN и DATE_END элементов инфоблока с ID = 22:

    function copy_from_fields_to_propertys_values( $IBLOCK_ID, $_FROM_FIELD_NAMES, $TO_PROPERTY_NAMES ){
    /* *
    * $_FROM_FIELD_NAMES = array(DATE_ACTIVE_FROM, DATE_ACTIVE_TO);
    * $TO_PROPERTY_NAMES = array(DATE_BEGIN, DATE_END);
    * copy_from_fields_to_propertys_values(22, array("DATE_ACTIVE_FROM","DATE_ACTIVE_TO"), array("DATE_BEGIN","DATE_END"));
    * */
    	if ( CModule::IncludeModule ( "iblock" ) ){
    		$arOrder = array(
    			"sort" => "ASC",
    		);
    
    		$arFilter = array(
    			"IBLOCK_ID" => $IBLOCK_ID,
    		);
    
    		foreach ( $TO_PROPERTY_NAMES as $property_name ) {
    			$TO_PROPERTY_NAMES_with_prop[] = 'PROPERTY_' . $property_name;
    		}
    
    		$arSelect = array(
    			"NAME",
    			"ID"
    		);
    
    		$arSelect = array_merge ( $arSelect, $_FROM_FIELD_NAMES, $TO_PROPERTY_NAMES_with_prop );
    		
    		$db_elemens = CIBlockElement::GetList ( $arOrder, $arFilter, false, false, $arSelect );
    
    		while ( $arElement = $db_elemens->Fetch () ) {
    			$PRODUCT_ID = $arElement["ID"];
    
    		foreach ( $TO_PROPERTY_NAMES as $key => $property_name ) {
    			CIBlockElement::SetPropertyValues ( $PRODUCT_ID, $IBLOCK_ID, $arElement[$_FROM_FIELD_NAMES[$key]], $property_name );
     			}
    		}
    
    		} 
    		else
    		{
    			die( "Модуль iblock не установлен" );
    		}
    	}
    

    Дополнительная информация



    Получение суммы значений полей связанных инфоблоков

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

    Решение возможно как с использованием модуля Интернет-магазин, так и без него. При использовании интернет-магазина все инфоблоки должны иметь свойства торгового каталога с указанием цены в соответствующих полях. Если используется редакция без интернет-магазина, то все поля исходных инфоблоков должны иметь тип число и (в рамках данного решения) иметь код PRICE.

    Пусть поле результирующего инфоблока называется COST. Его тип должен быть - число. В поле "Значение по умолчанию" параметра COST должно быть внесено выражение типа:

    • {PROP_1_PRICE}+{PROP_2_PRICE}+... - для редакций с Интернет-магазином.
    • {PROP_1} + {PROP_2}+... - для редакций без Интернет- магазина.

    Код ниже приведен для вывода результата в компоненте catalog.section. Для вывода результатов в другом компоненте требуется модификация кода. Приведенный код вносится в файл result_modifer.php указанного компонента:

    <?
    //Эту строчку можно раскомментировать и посмотреть содержимое arResult
    //того компонента под который будет адаптирован этот модификатор
    //echo "<pre>",htmlspecialchars(print_r($arResult, 1)),"</pre>";
    
    //Символьный код свойства значение по умолчанию которого содержит выражения
    //результат вычисления будет показан шаблоном компонента
    //Само выражение представляет собой исполняемый eval'ом PHP код
    //в котором по шаблонам вида {<СИМВОЛЬНЫЙ КОД СВОЙСТВА>} будут подставлены конкретные значения
    //Эти свойства должны быть выбраны в настройках компонента и доступны через arResult
    //в противном случае надо воспользоваться функцией CIBlockElement::GetProperty для доступа к БД
    //Эти свойства должны быть НЕ множественными
    //Пример выражения: "({PROP_1_PRICE} + {PROP_2_PRICE}) * {PROP_COUNTER}"
    //Обратите внимание на _PRICE - это указание на необходимость выборки цены привязанного элемента! 
       //Само свойство должно иметь символьный код PROP_1 и PROP_2 соответственно
    
    $CALCULATOR_CODE="COST";
    //ИД цены которая будет браться для вычислений
    $PRICE_ID = 1;
    //Идентификатор инфоблока (для разных компонент надо брать разные поля arResult)
    $IBLOCK_ID = $arResult["IBLOCK_ID"];
    //Получаем метаданные свойства "Калькулятора"
    $arProperty = CIBlockProperty::GetPropertyArray($CALCULATOR_CODE, $IBLOCK_ID);
    //Если такое свойство есть и у него задано выражение для вычислений:
    if($arProperty && strlen($arProperty["DEFAULT_VALUE"]) > 0)
    {
    	//Цикл по всем элементам каталога
    	foreach($arResult["ITEMS"] as $i => $arElement)
    	{
    		//Берем выражение "Калькулятора"
    		$EQUATION = $arProperty["DEFAULT_VALUE"];
    		//Проверим надо ли выполнять подстановку шаблонов
    		if(preg_match_all("/(\\{.*?\\})/", $EQUATION, $arMatch))
    		{
    			//Цикл по всем использованным в выражении свойствам
    			$arPropCodes = array();
    			foreach($arMatch[0] as $equation_code)
    			{
    				//Это "цена" и она потребует больших усилий
    				$bPrice = substr($equation_code, -7)=="_PRICE}";
    				//Символьный код свойства значение которого будет подставлено в выражение
    				$property_code = ($bPrice? substr($equation_code, 1, -7): substr($equation_code, 1, -1));
    				if($bPrice)
    				{
    					//Находим связанный элемент
    					$rsLinkedElement = CIBlockElement::GetList(
    						array(),
    						array(
    							"=ID" => $arElement["PROPERTIES"][$property_code]["~VALUE"],
    							"IBLOCK_ID" => $arElement["PROPERTIES"][$property_code]["~LINK_IBLOCK_ID"]
    						),
    						false, false,
    						array(
    							"ID", "IBLOCK_ID", "CATALOG_GROUP_".$PRICE_ID, "PROPERTY_PRICE"
    						)
    					);
    					$arLinkedElement = $rsLinkedElement->Fetch();
    					//Забираем его цену
    					if($arLinkedElement["CATALOG_PRICE_".$PRICE_ID])
    						$value = doubleval($arLinkedElement["CATALOG_PRICE_".$PRICE_ID]);
    					else
    						$value = doubleval($arLinkedElement["PROPERTY_PRICE_VALUE"]);
    				}
    				else
    				{
    					//Если вам потребуются не только числа, но и строки
    					//избавьтесь от doubleval и добавьте экранирование строковых символов
    					$value = doubleval($arElement["PROPERTIES"][$property_code]["~VALUE"]);
    				}
    				//Подстановка значения
    				$EQUATION = str_replace($equation_code, $value, $EQUATION);
    			}
    
    		}
    		//Собственно вычисление
    		$VALUE = @eval("return ".$EQUATION.";");
    		//и сохрание его результата для показа в шаблоне
    		$arResult["ITEMS"][$i]["DISPLAY_PROPERTIES"][$CALCULATOR_CODE] = $arResult["ITEMS"][$i]["PROPERTIES"][$CALCULATOR_CODE];
    		$arResult["ITEMS"][$i]["DISPLAY_PROPERTIES"][$CALCULATOR_CODE]["DISPLAY_VALUE"] = htmlspecialchars($VALUE);
    	}
    }
    ?>
    


    Вывод свойств элемента инфоблока

    Задача

    Выбрать свойство(-а) элемента инфоблока и вывести его на экран.

    Решение

    Решение первой части банально: метод GetProperty класса CIBlockElement подробно описан в документации.

    Решение второй части. Возьмём свойство типа HTML\текст. Для этого свойства нельзя просто вывести его значение (ключ VALUE), т.к. это — массив, содержащий «сырое» значение и его тип (HTML или текст). Всего один вызов метода GetDisplayValue класса CIBlockFormatProperties:

    $arResult['DISPLAY_PROPERTIES'][$pid] = CIBlockFormatProperties::GetDisplayValue($arResult, $prop);

    Теперь в шаблоне мы можем писать так:

    echo $arResult['DISPLAY_PROPERTIES'][$pid]['DISPLAY_VALUE'];

    И любое свойство, тип которого предполагает форматирование значения перед выводом, будет соответствующим образом преобразовано.

    Практика. Копирование инфоблока

    В Bitrix Framework штатно не предусмотрена возможность копирования инфоблоков. Иногда возникает такая потребность и ее можно решить. Автоматизация этого процесса и будет примером использования API инфоблоков.

    Использование импорта XML

    Копирование инфоблоков можно осуществить через функцию импорта/экспорта XML:

    • Создайте инфоблок в который будете копировать данные.
    • Выгрузите необходимый инфоблок экспортом в XML.
    • Откройте для редактирования файл XML и аккуратно в нужных местах скорректируйте ID инфоблока-донора на ID инфоблока-реципиента. В начале XML можно поменять узел ИД и узел Наименование:
      <?xml version="1.0" encoding="UTF-8"?>
      <КоммерческаяИнформация ВерсияСхемы="2.021" ДатаФормирования="2010-03-20T09:55:13">
      	<Классификатор>
      		<Ид>2
      		<Наименование>ноутбуки
      		<Свойства>
      			<Свойство>
      				<Ид>CML2_CODE
      				<Наименование>Символьный код
      после описания инфоблока и его свойств найдите код:
      <Каталог>
      	<Ид>2
      	<ИдКлассификатора>2
      	<Наименование>ноутбуки
      установите данные в соответствии с внесенными изменениями выше в узлах ИД, ИД классификатора и Наименование.

    Автоматизация копирования

    Есть небольшой инструмент для импорта метаданных с ранее созданного информационного блока при генерации нового:

    Настройка копирования метаданных задается тремя полями:

    • Копируем ИБ. Поле обязательно для заполнения и всегда предустановлено. В данной секции указывается с какого ИБ будут импортироваться метаданные (за исключением описания свойств).
    • Копируем в новый ИБ свойства другого ИБ. Поле не обязательное, может быть использовано для импорта в новый информационный блок только метаданных свойств любого инфоблока. В случае если поле не заполнено, метаданные свойств берутся из инфоблока указанного в поле Копируем ИБ.
    • Копируем ИБ в тип. Поле не обязательное и может быть указано, в случае если новый информационный блок необходимо сгенерировать в каком либо типе ИБ. Если настройка не указана, используется тип инфоблока, указанного в поле Копируем ИБ.

      Новый инфоблок после копирования будет иметь имя старого с суффиксом _new.

    Код скрипта:

    CModule::IncludeModule("iblock");
    	if(intval($_REQUEST["IBLOCK_ID_FIELDS"])>0){
    		$bError = false;
    		$IBLOCK_ID = intval($_REQUEST["IBLOCK_ID_FIELDS"]);
    		$ib = new CIBlock;
    		$arFields = CIBlock::GetArrayByID($IBLOCK_ID);
    		$arFields["GROUP_ID"] = CIBlock::GetGroupPermissions($IBLOCK_ID);
    		$arFields["NAME"] = $arFields["NAME"]."_new";
    		unset($arFields["ID"]);
    		if($_REQUEST["IBLOCK_TYPE_ID"]!="empty")
    			$arFields["IBLOCK_TYPE_ID"]=$_REQUEST["IBLOCK_TYPE_ID"];
    		$ID = $ib->Add($arFields);
    			if(intval($ID)<=0)
    				$bError = true;        
    		if($_REQUEST["IBLOCK_ID_PROPS"]!="empty")
    			$iblock_prop=intval($_REQUEST["IBLOCK_ID_PROPS"]);
    		else
    			$iblock_prop=$IBLOCK_ID;
    	$iblock_prop_new = $ID;
    	$ibp = new CIBlockProperty;
    	$properties = CIBlockProperty::GetList(Array("sort"=>"asc", "name"=>"asc"), Array("ACTIVE"=>"Y", "IBLOCK_ID"=>$iblock_prop));
    	while ($prop_fields = $properties->GetNext()){
    		if($prop_fields["PROPERTY_TYPE"] == "L"){
    			$property_enums = CIBlockPropertyEnum::GetList(Array("DEF"=>"DESC", "SORT"=>"ASC"), Array("IBLOCK_ID"=>$iblock_prop, "CODE"=>$prop_fields["CODE"]));
    			while($enum_fields = $property_enums->GetNext()){
    				$prop_fields["VALUES"][] = Array(
    					"VALUE" => $enum_fields["VALUE"],
    					"DEF" => $enum_fields["DEF"],
    					"SORT" => $enum_fields["SORT"]
    				);
    			}
    		}
    		$prop_fields["IBLOCK_ID"]=$iblock_prop_new;
    		unset($prop_fields["ID"]);
    		foreach($prop_fields as $k=>$v){
    			if(!is_array($v))$prop_fields[$k]=trim($v);
    			if($k[0]=='~') unset($prop_fields[$k]);
    		}
    		$PropID = $ibp->Add($prop_fields);
    		if(intval($PropID)<=0)
    			$bError = true;
    		}
            if(!$bError && $IBLOCK_ID>0)
    LocalRedirect($APPLICATION->GetCurPageParam("success=Y",array("success","IBLOCK_ID_FIELDS")));
    	else 
    LocalRedirect($APPLICATION->GetCurPageParam("error=Y",array("success","IBLOCK_ID_FIELDS")));
    	}
    	$str .='<form action='.$APPLICATION->GetCurPageParam().' method="post"><table>';    
    	if($_REQUEST["success"]=="Y") $str .='<tr><td><font color="green">ИБ успешно скопирован</font><b></td></tr>';
    	elseif($_REQUEST["error"]=="Y") $str .='<tr><td><font color="red">Произошла ошибка</font><br/></td></tr>';
    	$str .='<tr><td>Копируем мета данные ИБ в новый ИБ</b><br/></td></tr>';
    	$res = CIBlock::GetList(Array(),Array(),true);
    		while($ar_res = $res->Fetch())
    			$arRes[]=$ar_res;
    	$str .='<tr><td>Копируем ИБ:<br><select name="IBLOCK_ID_FIELDS">';
    	foreach($arRes as $vRes)    
    		$str .= '<option value='.$vRes['ID'].'>'.$vRes['NAME'].' ['.$vRes["ID"].']</option>';
    	$str .='</select></td>';
    	$str .='<td>Копируем в новый ИБ свойства другого ИБ: *<br><select name="IBLOCK_ID_PROPS">';
    	$str .='<option value="empty">';
    	foreach($arRes as $vRes)    
    		$str .= '<option value='.$vRes['ID'].'>'.$vRes['NAME'].' ['.$vRes["ID"].']</option>';
    	$str .='</select></td></tr>';
    	$str .='<tr><td>Копируем ИБ в тип:<br><select name="IBLOCK_TYPE_ID">';
    	$str .='<option value="empty">';
    	$db_iblock_type = CIBlockType::GetList();
    	while($ar_iblock_type = $db_iblock_type->Fetch()){
    		if($arIBType = CIBlockType::GetByIDLang($ar_iblock_type["ID"], LANG))
    			$str .= '<option value='.$ar_iblock_type["ID"].'>'.htmlspecialcharsex($arIBType["NAME"])."</option>";
    	}
    	$str .='</select></td></tr>';
    	$str .='<tr><td><br/>* если значение не указано мета данные ИБ секции "Свойства" берутся из ИБ первого поля</td></tr>';
    	$str .='<tr><td><input type="submit" value="копируем"></td></tr>';
    	$str .='</table></form>';    
    	echo $str;

    Скрипт может оказать неоценимую помощь например при копировании ИБ не прибегая к использованию механизмов XML экспорта и XML импорта информационных блоков.

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

    Скрипт должен быть размещен в корне сайта.


    Список ссылок по теме:


    Некоторые ошибки при работе с инфоблоками

    Ошибка типа:

    Fatal error: Class 'CIBlockElement' not found in /hosting/site.ru/www/index.php on line XX

    Если используете модуль Инфоблоки, его нужно сначала подключить:

    CModule::IncludeModule("iblock");


    Ошибка типа:

    Fatal error: Call to a member function GetNextElement() on a non-object in /hosting/site.ru/www/index.php on line XX

    Скорее всего вы передали неверные параметры какому-то методу. Например, так:

    $res = CIBlockElement::GetList(array(), $arFilter, array(), array(), $arSelect);

    Третий-то параметр должен быть true/false, а не array. Читайте внимательно описание используемого метода.

    Практика. Ограничение области поиска разделом

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

    Поиск по разделу инфоблока

    Чтобы поиск выполнялся по элементам некоторого раздела инфоблока, необходимо выполнить следующие действия:

    • Связать поисковый индекс и набор разделов, к которым привязан элемент.

      Для этого в файле /bitrix/php_interface/init.php следует создать обработчик события BeforeIndex и получить разделы привязки элемента (CIBlockElement::GetElementGroups):

      <?
      // файл /bitrix/php_interface/init.php
      // регистрируем обработчик
      AddEventHandler("search", "BeforeIndex", Array("MyClass", "BeforeIndexHandler"));
      
      class MyClass
      {
      	// создаем обработчик события "BeforeIndex"
      	function BeforeIndexHandler($arFields)
      	{
      		// элемент инфоблока 6 (не раздел)
      		if($arFields["MODULE_ID"] == "iblock" && $arFields["PARAM2"] == 6 && substr($arFields["ITEM_ID"], 0, 1) != "S")
      	{
      			$arFields["PARAMS"]["iblock_section"] = array();
      			//Получаем разделы привязки элемента (их может быть несколько)
      			$rsSections = CIBlockElement::GetElementGroups($arFields["ITEM_ID"], true);
      			while($arSection = $rsSections->Fetch())
      			{
       				//Сохраняем в поисковый индекс
      				$arFields["PARAMS"]["iblock_section"][] = $arSection["ID"];
      			}
      		}
      		//Всегда возвращаем arFields
      		return $arFields;
      	}
      }
      ?>
    • Пересохранить элемент этого инфоблока (в данном случае инфоблока с ID=6) или выполнить полную переиндексацию. Теперь в модуле производительности на странице просмотра содержания таблицы b_search_content_param можно увидеть результат работы этого обработчика:

    • Создать страницу поиска. В коде странице необходимо обязательно указать идентификатор раздела (в данном случае раздел с ID=12), поиск по которому будет выполняться, а в настройках компонента поиска должен быть задан соответствующий инфоблок (инфоблок с ID=6):

      Полный код страницы

    В результате поиск будет выполняться только по элементам указанного раздела.

    Поиск по разделу и его подразделам

    Для того чтобы поиск мог выполняться не только по элементам раздела, но и по всем элементам вложенных подразделов, необходимо выполнить следующие действия:

    • В файле init.php перед сохранением в поисковый индекс воспользоваться функцией CIBlockSection::GetNavChain:
      <?
      // файл /bitrix/php_interface/init.php
      // регистрируем обработчик
      AddEventHandler("search", "BeforeIndex", Array("MyClass", "BeforeIndexHandler"));
      
      class MyClass
      {
      	// создаем обработчик события "BeforeIndex"
      	function BeforeIndexHandler($arFields)
      	{
      		// элемент инфоблока 6 (не раздел)
      		if($arFields["MODULE_ID"] == "iblock" && $arFields["PARAM2"] == 6 && substr($arFields["ITEM_ID"], 0, 1) != "S")
      		{
      			$arFields["PARAMS"]["iblock_section"] = array();
      			//Получаем разделы привязки элемента (их может быть несколько)
      			$rsSections = CIBlockElement::GetElementGroups($arFields["ITEM_ID"], true);
      			while($arSection = $rsSections->Fetch())
      			{
      				$nav = CIBlockSection::GetNavChain(6, $arSection["ID"]);
      				while($ar = $nav->Fetch()) {
      					//Сохраняем в поисковый индекс
      					$arFields["PARAMS"]["iblock_section"][] = $ar[ID];
      					}
      			}
      		}
      		//Всегда возвращаем arFields
      		return $arFields;
      	}
      }
      ?>
      
    • Выполнить переиндексацию инфоблоков.

    Кроме элементов, в результатах поиска могут отображаться и сами подразделы раздела, по которому осуществляется поиск. Для этого необходимо выполнить следующие действия:

    • В файле init.php добавить сохранение подразделов в поисковый индекс:
      <?
      // файл /bitrix/php_interface/init.php
      // регистрируем обработчик
      AddEventHandler("search", "BeforeIndex", Array("MyClass", "BeforeIndexHandler"));
      
      class MyClass
      {
      	// метод модифицирует поисковый индекс для элементов и разделов инфоблока
          
      	function BeforeIndexHandler($arFields)
      	{
        	      $IBLOCK_ID = 6;
              
      		// Обрабатываем только нужный инфоблок
      		if($arFields["MODULE_ID"] == "iblock" && $arFields["PARAM2"] == $IBLOCK_ID)
      		{
      			$arFields["PARAMS"]["iblock_section"] = array();
                  
      			// Добавляем разделы элемента с учетом родительских разделов
                 
      			if(substr($arFields["ITEM_ID"], 0, 1) != "S")
      			{
      				// Получаем разделы привязки элемента (их может быть несколько)
      				$rsSections = CIBlockElement::GetElementGroups($arFields["ITEM_ID"], true);
      				while($arSection = $rsSections->Fetch())
      				{
      					$nav = CIBlockSection::GetNavChain($IBLOCK_ID, $arSection["ID"]);
      					while($ar = $nav->Fetch())
      					{
      						//Сохраняем в поисковый индекс
      						$arFields["PARAMS"]["iblock_section"][] = $ar['ID'];
      					}
      				}
      			}
      			// Добавляем разделы раздела с учетом родительских разделов
      			else
      			{
      				// Получаем разделы
      				$nav = CIBlockSection::GetNavChain($IBLOCK_ID, substr($arFields["ITEM_ID"], 1, strlen($arFields["ITEM_ID"])));
      				while($ar = $nav->Fetch())
      				{
      					//Сохраняем в поисковый индекс
      					$arFields["PARAMS"]["iblock_section"][] = $ar['ID'];
      				}
      			}
      		}
      		//Всегда возвращаем arFields
      		return $arFields;
      	}
      }
      ?>
      
    • Выполнить переиндексацию инфоблоков.

    Программирование в Bitrix Framework

    Цитатник веб-разработчиков.

    Хан Эрли: В конечном счёте качество сайта определятся не платформой, а мастерством разработчика.

    Программирование в Bitrix Framework не представляет особой сложности. Это обычное программирование на PHP. Особенности, конечно, есть, но это не те особенности, которые делают из "программистов на Битрикс" какую-то особую касту. Всё в пределах досягаемости для неленивого ума.

    В этой главе освещаются вопросы особенности программирования на Bitrix Framework.

    Видео Время
    Continuous Integration – от простого к сложному 10 минут 36 секунд
    Подводные камни разработки - чего делать нельзя 10 минут 50 секунд
    Программная архитектура веб-систем на Битриксе: от простого сайта до веб-кластера 28 минут 36 секунд
    Особенности проектирования под Битрикс 37 минут 35 секунд


    Командная PHP-строка

    Иногда бывают ситуации, когда нужно быстро исполнить некоторый код, вызывающий функции API Bitrix Framework без создания новых страниц на сайте для этого. В этом случае поможет удобный и простой инструмент — командная PHP-строка, позволяющий запускать произвольный код на PHP с вызовами функций.

    Инструмент расположен в административной части сайта по следующему пути: Настройки > Инструменты > Командная PHP-строка и имеет адрес /bitrix/admin/php_command_line.php.

    Вот как выглядит результат выполнения кода, использующего функции класса CUser Главного модуля:

    С помощью кнопки "+" можно создавать новые закладки и сохранять в них частоиспользуемый php-код. Для переименования закладки используйте //title:*** в начале вашего кода.

    Примечание: Аналогичный инструмент есть и для работы с БД.


    Организация разработки

    В главе описываются способы и инструменты для организации разработки как одним разработчиком, так и группой.



    Система контроля версий

    Организовать сопровождение проекта с помощью системы контроля версий не сложно, если ограничиваться файлами. Для этого можно использовать, например, Mercurial - кроссплатформенную распределённую систему управления версиями, разработанную для эффективной работы с очень большими репозиториями кода. Рекомендуется использовать надстройку с графическим интерфейсом.

    Простая схема репозиториев

    В схеме три элемента:

    • Центральный репозиторий - место хранения изменений.
    • Копия для разработки - рабочие места для разработчиков, которых может быть несколько.
    • Боевой сайт - конечная цель всех изменений

    В ходе работы изменения из Копии для разработки переносятся в Центральный репозиторий, а с него уже на Боевой сайт.

    Как ведётся работа

    Разработчики изменили какие-то файлы. Ответственное за коммиты лицо использует команду Hg Commit, появляется диалог, в котором отражено где и что изменилось:

    Нажмите на рисунок, чтобы увеличить

    Красный текст - удаление, зелёный - добавление. После проверки изменений нажимаем кнопку Commit и изменения отображаются в среде Mercurial:

    Нажмите на рисунок, чтобы увеличить

    Далее пишем изменения на Центральный репозиторий и переходим на "боевой" сайт. Изменения на "боевом" сайте вносятся уже из командной строки. Так же необходимо проверить вносились ли изменения на самом "боевом" проекте и, при необходимости, перенести их на Центральный репозиторий и на Копии для разработки.

    Сложности

    Версия ядра на сервере сайта и на сервере Копии для разработчиков могут отличаться. Поэтому ядро исключается из системы контроля версий. Технически это делается набором правил для файла настройки .hgignore.

    Ещё одна сложность: файлы ядра изменяются не разработчиками проекта, а приходят "снаружи" в виде обновлений. При этом нельзя просто исключить папку /bitrix/, так как в ней могут находиться в том числе файлы проекта: модули, компоненты, шаблоны сайта и так далее. В итоге файл .hgignore приобретает избыточный вид:

    /bitrix/activities/bitrix/
    /bitrix/admin
    /bitrix/cache
    /bitrix/components/bitrix/
    /bitrix/gadgets/bitrix
    /bitrix/image_uploader
    /bitrix/images
    /bitrix/js
    /bitrix/managed_cache
    /bitrix/stack_cache
    /bitrix/modules/advertising
    /bitrix/modules/bitrix.sitecommunity
    ...
    /bitrix/modules/xdimport
    /bitrix/modules/xmpp
    /bitrix/modules/.htaccess
    /bitrix/otp
    /bitrix/sounds
    /bitrix/template/
    /bitrix/themes
    /bitrix/tmp
    /bitrix/tools
    /bitrix/wizards/bitrix
    /bitrix/[^/]*\.php$
    /upload
    /bitrix/php_interface
    /bitrix/panel/
    /bitrix/updates/
    /bitrix/fonts/

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

    Внимание! При копировании файла init.php в папку /local, файл init.php находящийся в папке /bitrix перестает работать.

    Папка /local

      Папка для своих доработок

    Чтобы сделать жизнь разработчиков проектов удобнее, в ядре D7 с версии главного модуля 14.0.1 основные файлы пользовательских проектов вынесены из папки /bitrix в папку /local. Это позволяет изолировать изменяющиеся файлы вашего проекта от папки продукта. По сути, в исключения достаточно будет добавить одну папку /bitrix.

    Какие папки обрабатываются в /local?
    • activities - действия БП;
    • components - компоненты;
    • gadgets - гаджеты рабочего стола;
    • modules - модули;
    • php_interface - init.php, папка user_lang;
    • templates - шаблоны сайтов, шаблоны компонентов, шаблоны страниц;
    • blocks - блоки Сайтов24.
    • routes - файлs с конфигурацией маршрутов роутинга

    При обработке папок приоритет всегда у папки /local перед /bitrix. Это означает, что если в /local/templates/ и /bitrix/templates/ будут находиться шаблоны сайта с одинаковым названием, то подключится шаблон из /local.

    Примечание: не рекомендуется копировать сущности с одинаковыми именами в обе папки. Более правильное решение - это перенос сущности из одной папки в другую. При копировании система все равно будет корректно работать, но велика вероятность путаницы в работе разработчиков.

    Внимание! В целях безопасности проекта необходимо задать права доступа к папке /local/php_interface/ аналогичные правам доступа к папке /bitrix/php_interface/.

      Перенос проекта

    Как старый проект перенести в папку /local.

    Вам достался старый и запущенный проект. Кто когда и как вносил правки - неизвестно, куда вносились правки - неизвестно. Вам нужно навести в нём порядок.

    1. Для начала следует воспользоваться [dwi include_monitor]Монитором качества[/dwi] в нём есть проверка на модификацию файлов ядра. Этот тест поможет определить какие файлы были изменены.
    2. Далее создаете папку /local, в ней не создаете файл /local/php_interface/[dwi include_int]init[/dwi].php, а лишь /local/php_interface/constants.php, /local/php_interface/events.php, автолоадер для своих классов и так далее, и эти файлы подключаете в /bitrix/php_interface/init.php.
    3. В /local/templates/.default постепенно начинаете перенос ваши измененных шаблонов, при этом действовать нужно так: копируете в локал с другим названием, работаете с тестовой страницей (если тестового сервера нет). Потом заменяете название и из папки вашегo шаблона удаляете (или переименовываете) старый шаблон. Таким образом новый шаблон (чистый и готовый к использованию) постепенно будет собираться в local. Как только все шаблоны и компоненты перенесены в /local, init.php тоже можно будет перенести.
    4. С компонентами иначе - если это ваши родные компоненты, то их можно перенести и так (ничего от этого не изменится). Измененные штатные компоненты нужно копировать сначала из ядра, а потом уже сравнивать изменения и вносить правки.
    5. В шаблонах и компонентах нужно заменять прямые ссылки на /bitrix/templates/** , /bitrix/components/** и заменять на /local/**. Также внимательно проверяем и модули, если есть необходимость и их переносить.

    Сколько времени уйдёт на этот процесс сказать невозможно. Всё зависит от степени "запущенности". Но опыт разработчиков, выполнявших эти действия, говорит о нескольких неделях работы.

    Цитатник веб-разработчиков.

    Иван Поддубный: Есть простой лайфхак, которым мы пользуемся: Переносим в локал все что нужно, и ставим симлинки с папок в битриксе в локал. Да, это конечно не полноценное решение проблемы, но это даст возможность разработчикам сразу начать работать с проектом с git с клиентом.

    Ведь когда такая необходимость возникает? Когда клиент приходит в компанию с старым сайтом. А на входе клиенту начать заниматься рефакторингом путей в шаблонах - такое, особенно если шаблонов много, выкатывать клиенту N часов на исправление вместо того чтобы решить более важные проблемы.

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

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


    Composer и Bitrix Framework

    Composer - это пакетный менеджер уровня приложений для языка программирования PHP, который предоставляет средства по управлению зависимостями в PHP-приложении.

    В версии 18.0.5 мы начали использовать composer внутри продукта в режиме разработки. Он понадобится вам, если вы захотите воспользоваться такими преимуществами, как аннотация ORM классов и в целом интерфейсом командной строки CLI. Если вы уже используете composer в своем проекте на 1С-Битрикс, мы подготовили готовый рецепт интеграции с нашей конфигурацией зависимостей.

    Прежде всего, вам нужен установленный composer. Простая инструкция по установке есть на официальном сайте.

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

    $ composer -V
    Composer version 1.6.5 2018-05-04 11:44:59
    

    Установка зависимостей

    1. Вы еще не используете composer в проекте

    Устанавливаем зависимости из нашего bitrix/composer-bx.json:

    $ cd bitrix
    $ COMPOSER=composer-bx.json composer install
    

    После этого появится папка bitrix/vendor, в которую будут установлены необходимые библиотеки. Если вы хотите задать другое расположение этой директории, вам понадобится создать свой composer.json - смотрите второй вариант установки зависимостей.

    2. Вам нужна своя конфигурация composer.json

    По умолчанию система ожидает увидеть ваш файл composer.json в папке bitrix, но мы рекомендуем разместить его где-либо за пределами DOCUMENT_ROOT (чтобы он не был доступен публично). В этом случае нужно указать путь до файла в .settings.php, чтобы его конфигурация могла быть использована в продукте.

    Файл .settings.php:

    <?php
    return [
    	'composer' => [
    		'value' => ['config_path' => '/path/to/your/composer.json']
    	],
    	// ...
    ];  
    

    В нем необходимо подключить наш файл с зависимостями bitrix/composer-bx.json с помощью плагина Composer Merge Plugin. В минимальном виде ваш composer.json должен содержать вызов плагина и подключение нашей конфигурации.

    Файл composer.json (можно скопировать из bitrix/composer.json.example):

    {
    	"require": {
    		"wikimedia/composer-merge-plugin": "dev-master"
    	},
    	"extra": {
    		"merge-plugin": {
    			"require": [
    				"/path/to/bitrix/composer-bx.json"
    			]
    		}
    	}
    }
    

    Вместо /path/to/bitrix/ вам нужно указать реальный путь до папки bitrix.

    К этому вы можете добавить свои зависимости и настройки. Например, чтобы явно задать путь до папки vendor (по умолчанию она будет там же, где файл composer.json), используйте директиву "vendor-dir".

    Файл composer.json:

    {
    	"require": {
    		"wikimedia/composer-merge-plugin": "dev-master"
    	},
    	"config": {
    		"vendor-dir": "../../vendor"
    	},
    	"extra": {
    		"merge-plugin": {
    			"require": [
    				"/path/to/bitrix/composer-bx.json"
    			]
    		}
    	}
    }
    

    После описания своей конфигурации останется установить библиотеки:

    $ composer install
    

    Теперь вы можете использовать преимущества composer в своем проекте, подключая файл vendor/autoload.php. При использовании CLI-команд он будет подключен автоматически.



    Bitrix CLI

    Командный интерфейс реализован на основе библиотеки symfony/console. Перед началом использования убедитесь, что установили зависимости через composer.

    Исполняемый файл находится в папке bitrix:

    $ cd bitrix
    $ php bitrix.php
    

    Для удобства вы можете создать символическую ссылку без постфикса php:

    $ chmod +x bitrix.php
    $ ln -s bitrix.php bitrix
    $ ./bitrix
    

    Список доступных команд «из коробки»:

    [ICO_NEW data-adding-timestamp="1705568540"]

    С версии main 24.0.0 появится возможность добавлять свои команды через файлы настроек модуля {moduleName}/.settings.php. Команды модуля перечисляются в секции console:

    <?php
    	
    return [
    	//...
    	'console' => [
    		'value' => [
    			'commands' => [
    				\Module\Name\Cli\CustomCommand::class,
    				\Module\Name\Feature\Path\Cli\AnotherCommand::class,
    			],
    		],
    		'readonly' => true,
    	],
    ];

    Примечание. Команду рекомендуется называть с префиксом модуля в виде module:command.

    [/ICO_NEW]

    Дополнительно:



    Немного теории PHP

    Не считаем наших читателей ламерами в PHP, тем не менее слегка напомним теорию. А заодно и зададим стандарты написания кода в Bitrix Framework.

    Замечания по $arParams и $arResult

    $arParams

    $arParams - это предопределенная для компонента переменная, представляющая собой массив входных параметров компонента. Ключами в этом массиве являются названия параметров, а значениями - их значения.

    Перед подключением компонента ко всем значениям параметров применяется функция htmlspecialcharsEx. Исходные значения параметров сохраняются в этом же массиве с теми же ключами, но с префиксом ~. Например, $arParams["NAME"] - входной параметр, к которому применена функция htmlspecialcharsEx, а $arParams["~NAME"] - исходный входной параметр.

    Переменная $arParams является псевдонимом для члена класса компонента, поэтому все изменения этой переменной отражаются и на этом члене класса. В начале кода компонента должна быть произведена проверка входных параметров, инициализация не установленных параметров, приведение к нужному типу (например, IntVal()). Все эти изменения входных параметров будут доступны и в шаблоне. То есть параметры будут там уже проверенными и максимально безопасными. Дублирование подготовки параметров в шаблоне компонента не требуется.

    $arResult

    $arResult - это предопределенная для компонента переменная, в которую собирается результат работы компонента для передачи в шаблон. Перед подключением файла компонента эта переменная инициализируется пустым массивом array().

    Переменная $arResult является псевдонимом для члена класса компонента, поэтому все изменения этой переменной отражаются и на этом члене класса. Значит явно передавать в шаблон эту переменную не нужно, это сделают внутренние механизмы класса компонента.

    Ссылки в PHP

    Ссылки (references) в PHP служат для того, чтобы к одним и тем же данным можно было обратиться по разным именам. Если переменные $arParams и $arResult изменены некоторым образом в коде компонента, то они будут доступны измененными и в шаблоне.

    При этом нужно учитывать следующие нюансы:

    • Если в компоненте написать код:
      $arParams = & $arSomeArray;
      то переменная $arParams будет отвязана от члена класса компонента и привязана к массиву $arSomeArray. В этом случае дальнейшие изменения $arParams не попадут в шаблон компонента.
    • Если в компоненте написать код:
      unset($arParams);
      то это так же разорвет связь между $arParams и соответствующим членом класса компонента.

    HTTP POST запросы

    Если на сайте не существующие страницы обрабатываются с помощью опции ErrorDocument (ErrorDocument 404 /404.php), то при отправке HTTP POST запроса на несуществующую страницу данные POST запроса будут потеряны. Поэтому, такие запросы нужно направлять на скрипты, которые физически существуют.

    Один из способов организации ЧПУ в компонентах 2.0 предполагает использование опции ErrorDocument. Для компонентов 2.0 существует унифицированное решение, которое при написании компонентов и шаблонов компонентов позволяет не заботиться о том, осуществляется ли поддержка ЧПУ с помощью опции ErrorDocument или mod_rewrite.

    При написании в шаблонах компонентов форм, отправляющих данные методом POST, в качестве action необходимо указывать константу POST_FORM_ACTION_URI:

    <form method="post" action="<?=POST_FORM_ACTION_URI?>">
    	* * *
    </form>

    В этом случае при работе с использованием опции ErrorDocument POST-запрос будет направлен на физически существующий скрипт, а в остальных случаях - по текущему адресу. При этом для компонента не будет разницы, вызван ли он POST- или GET-запросом. Все необходимые переменные будут соответствующим образом установлены.

    Архитектура продукта

    Цитатник веб-разработчиков.

    Степан Овчинников: совместимость новых версий Битрикса со всем, что было написано ранее, поддерживается разработчиками постоянно. Для партнеров, поддерживающих разные проекты, уверенность в такой совместимости является на мой взгляд одной из самых важных положительных сторон.

    Любое программное обеспечение, развиваясь, должно соответствовать заявленной изначально цели. Эту задачу решает архитектурное проектирование. Архитектура продукта - подход к проектированию, гарантирующий, что программное обеспечение будет отвечать своему предназначению.

    Архитектура программного обеспечения - (англ. software architecture) — это структура программы или вычислительной системы, которая включает программные компоненты, видимые снаружи свойства этих компонентов, а также отношения между ними.

    Архитектура Bitrix Framework решает следующие задачи:

    • Преемственность. Каждый новый релиз продуктов поддерживает все предыдущие решения и технологии. Это позволяет осуществлять переход на новые версии продуктов сайтов, созданных на практически любой предыдущей версии.
    • Единство принципов работы с любой версией и любым решением на базе системы.
    • Безопасность. Архитектура позволяет создать достаточный уровень безопасности для сайтов любой направленности.
    • Масштабируемость. Не наложено никаких ограничений на развитие проектов по мере роста контента, сервисов, числа пользователей.
    • Производительность. Скорость работы системы зависит от качества настройки ее элементов, то есть в большей степени на производительность влияет уровень подготовки разработчика проекта, возможности хостинга.
    • Возможность развития системы усилиями сторонних разработчиков. Архитектура не накладывает никаких ограничений на создание собственных модулей, компонентов, решений.
    Цитатник веб-разработчиков.

    Степан Овчинников: Я убежден, что логика, представления и данные в Битриксе разделены самым разумным для задач CMS образом.

    Архитектура MVC для Bitrix Framework

    Шаблон MVC для Bitrix Framework:

    • Модель - это API;
    • Представление - это шаблоны;
    • Контроллер - это компонент.

    Сплошные линии - прямые связи, пунктир - косвенные связи.

    Немного теории

    Структура

    Bitrix Framework по уровням архитектуры структуру можно описать так:

    Bitrix Framework:
  • модули
  • компоненты
  • файлы страниц
  • сайт:
  • шаблон
  • компоненты
  • страница
  • компонент:
  • вызов
  • параметры
  • шаблон
  • страница:
  • header
  • workarea
  • footer
  • Элементы структуры Bitrix Framework

    Модули

    Модуль - это модель данных и API для доступа к этим данным. Статические методы классов модуля могут вызываться в компонентах, шаблонах, других модулях. Также внутри контекста Bitrix Framework могут создаваться экземпляры классов.

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

    Внимание! На уровне ядра и модулей вмешательство в работу системы крайне не рекомендуется.

    Ядро продукта - файлы, находящиеся в директории /bitrix/modules/, а также файлы системных компонентов: /bitrix/components/bitrix/.

    Компоненты

    Компонент - это контроллер и представление для использования в публичном разделе. Компонент с помощью API одного или нескольких модулей манипулирует данными. Шаблон компонента (представление) выводит данные на страницу.

    Компоненты входят в состав модулей, но решают более узкую, частную задачу — например, выводят список новостей или товаров. Вносить свои изменения в код продукта рекомендуется на уровне компонентов. Программист может модифицировать их как угодно, использовать свои наработки и использовать неограниченное число шаблонов на каждый из компонентов. На одной странице сайта может располагаться несколько компонентов, кроме того, их можно включать в шаблон сайта. Таким образом, программист имеет возможность собрать сайт как конструктор, после чего доработать необходимые компоненты для получения желаемого результата как в функциональном, так и в визуальном плане.

    Чтобы работать с API нужно просто понять структуру компонентов Bitrix Framework.

    Примечание: Модуль - это набор каких-либо сущностей. Компонент - это то, что этими сущностями управляет.

    Посмотрим на примере модуля Инфоблоки. Этот модуль представляет собой совокупность таблиц в базе данных и php-классов, которые могут проводить какие-либо операции с данными из таблиц (например, CIBlockElement::GetList() или CIBlockElement::GetByID ()). Компонентом является уже, например, Новость детально, который имеет собственные настройки (показывать дату, картинку и т.д. и т.п.) и работает с методами php-классов модуля.

    Страница

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

    Видео по теме:

    Права доступа

    В системе Bitrix Framework поддерживается два уровня разграничения прав доступа:

    Доступ на файлы и каталоги

    Этот уровень прав проверяется в прологе, задается с помощью специального файла .access.php, содержащего PHP массив следующего формата:

    $PERM[файл/каталог][ID группы пользователей] = "ID права доступа";
    Где:
    • файл/каталог - имя файла или каталога для которых назначаются права доступа;
    • ID группы пользователей - ID группы пользователей на которую распространяется данное право (допустимо также использование символа - *, что означает - для всех групп);
    • ID права доступа - на сегодняшний день поддерживаются следующие значения (в порядке возрастания):
      • D - запрещён (при обращении к файлу доступ будет всегда запрещён);
      • R - чтение (при обращении к файлу доступ будет разрешен);
      • U - документооборот (файл может быть отредактирован в режиме документооборота);
      • W - запись (файл может быть отредактирован непосредственно);
      • X - полный доступ (подразумевает право на "запись" и модификацию прав доступа).

    В административной части сайта права доступа на файлы и каталоги можно назначать с помощью Менеджера файлов.

    Если пользователь принадлежит нескольким группам, то берется максимальное право из всех прав заданных для этих групп.

    Если для текущего файла или каталога явно не задан уровень прав, то берется уровень прав заданный для вышележащих каталогов.

    Пример 1

    Файл /dir/.access.php

    <?
    	$PERM["index.php"]["2"] = "R";
    	$PERM["index.php"]["3"] = "D";
    ?>

    При попытке открытия страницы /dir/index.php пользователь, принадлежащий группе ID=3, будет иметь право доступа D (запрещено), пользователь из группы ID=2 будет иметь право R (чтение). Пользователь, принадлежащий обеим группам, будет иметь максимальный уровень доступа - R (чтение).

    Пример 2

    Файл /.access.php

    <?
    	$PERM["admin"]["*"] = "D";
    	$PERM["admin"]["1"] = "R";
    	$PERM["/"]["*"] = "R";
    	$PERM["/"]["1"] = "W";
    ?>

    Файл /admin/.access.php

    <?
       $PERM["index.php"]["3"] = "R";
    ?>

    При доступе к странице /admin/index.php пользователь, принадлежащий группе ID=3, будет иметь доступ, а пользователю, принадлежащему группе ID=2, будет в доступе отказано. При доступе к странице /index.php все посетители будут иметь доступ.



    Права в рамках логики модуля

    Если речь идет об обычных статичных публичных страницах, то к ним применяется только первый уровень доступа на файлы и каталоги.

    Если пользователь имеет на файл как минимум право R (чтение) и если данный файл является функциональной частью того или иного модуля, то проверяется 2-ой уровень прав, задаваемый в настройках соответствующего модуля. Например: При заходе на страницу Список обращений в техподдержке администратор видит все обращения, сотрудник техподдержки - только те за которые он ответственнен, а обычный пользователь - только свои обращения. Так работает право доступа в рамках логики модуля Техподдержка.

    Используются две методологии разграничения прав доступа 2-го уровня (уровень прав в рамках логики модуля):

    • права;
    • роли.

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

    Модули, в которых поддерживаются роли, можно увидеть в фильтре Модуль на странице Настройки > Пользователи > Уровни доступа в Административном разделе. Во всех остальных модулях и во всех остальных настройках системы используются права.

    Пример:

    • Права. Если вы принадлежите группам для которых в модуле Статистика заданы права Полный административный доступ и например, Просмотр статистики без финансовых показателей, то вы будете обладать максимальным правом - Полный административный доступ.
    • Роли. Если вы принадлежите к группам для которых в модуле Техподдержка заданы роли Клиент техподдержки и Демо-доступ, то вы одновременно будете обладать возможностями этих двух ролей. Т.е. вы сможете видеть все обращения в режиме демо-доступа и одновременно с этим можете создавать свои обращения как клиент техподдержки.

    Список ссылок по теме:

    Файлы и База данных

    Файлы и База данных

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

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

    Bitrix Framework реализован на файлах, что дает больше свободы разработчику сайта. Поскольку файл в системе - это просто исполняемый файл, то и исполнять он может что угодно: хоть собственный PHP-код программиста, хоть стандартные компоненты - в любом порядке. Как ни странно, эта полная свобода может напугать начинающего разработчика, но с опытом это проходит.

    Примечание: исполнение PHP это большое преимущество статической страницы Bitrix Framework.

    Файлы можно править как по FTP, так и в SSH, не прибегая к дополнительным инструментам СУБД. Их легко копировать, перемещать, делать резервные копии и т.п. Строго говоря, вы можете весь контент хранить в БД. Но для простых статичных сайтов это будет явное усложнение и замедление.

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

    Например, для каталога товаров действительно нужно создать папку на диске, но только одну, например, /catalog, поместить туда комплексный компонент и далее страницы товаров могут иметь вид, например: http://***.ru/catalog/1029.html Естественно, что эти адреса будут "мнимыми" и обрабатываться системой. Файлов под них в папке /catalog не создается.

    Однако, для каждого товара будет создан файл в кэше, чтобы, при следующем обращении покупателя сервер не напрягался с запросами к БД. Это и позволяет запускать магазины уровня Эльдорадо.

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

    Размер системы довольно большой, так как в её состав включено множество компонентов необходимых для быстрого старта и работы административной части. Компоненты не консолидированы, потому что система модульная. Модули, компоненты и шаблоны имеют определенную структуру. Это важно и для обновлений системы и для разработки своих компонентов.

    Большое количество файлов - свойство аналогичных систем. (У ZendFramework есть такая же особенность). При правильной конфигурации хостинга эту проблему возьмут на себя прекомпиляторы php. Критичным может оказаться размер выделяемого хостером места и большое число файлов системы. (Проблемой становится не штатная работа Bitrix Framework, а, например, работа систем бэкапов у хостеров. На большом количестве файлов они начинают себя чувствовать не очень хорошо.) Поэтому для выбора хостера рекомендуем пользоваться списком рекомендуемых хостингов.

    Резюме. В качестве инструмента хранения структуры сайта выбрана именно файловая система, а не база данных в силу того что:

    • Файл дает больше свободы разработчику сайта. Поскольку файл в системе - это просто исполняемый файл.
    • Так понятнее для управления. В корне такого представления - структура статических страниц HTML, разложенных по папкам. Путем некоторого совершенствования (внедряя небольшое количество PHP-кода), мы из такого сайта сразу получаем работающий на Bitrix Framework проект.
    • В какой-то мере это - традиция, которая имела большое значение на заре становления CMS.
    • Такое представление соответствует опыту контент-менеджеров, которые работают с локальными файловыми системами (папки и файлы).

    Структура сайта также может быть и в БД (инфоблоки), но управлять иерархией в реляционной БД не очень-то удобно.

    Рассмотрим использование файлов в Bitrix Framework на примерах:

    1. Файловая система и меню. Меню в файлах позволяет не подключать БД там, где это реально не нужно. То же самое относится к свойствам страниц и разделов, а также правам доступа к файлам. Теоретически можно собрать информационный сайт, где вообще не будет ни одного обращения к БД. Будет работать быстрее, особенно на разделяемом хостинге. Есть и бонусы: при копировании раздела сразу естественным образом копируются меню, права доступа, свойства раздела.
    2. Файловая система и пользователи. Пользователям из административного раздела открыт доступ к файлам ядра и другим программным файлам. Но пользователи бывают разные. Например, техподдержка 1С-Битрикса. Если веб-разработчик не уверен в своих пользователях, то он всегда может запретить им как редактирование кода PHP, так и целых разделов (ядра). По современной концепции Bitrix Framework в публичной части не должно быть кода PHP - все должно быть инкапсулировано в компоненты. Тогда пользователь редактирует или "голую" статику, или настраивает компонент.
    3. Файловая система и языковые версии. Было бы трудно сопровождать языковую информацию в БД. Информация в языковых файлах меняется крайне редко - проще раз в год отредактировать строчку в языковом файле, чем хранить эти статические фразы в базе. И повторимся: база данных - это медленно и избыточно.

    Структура файлов

    Файловая структура Bitrix Framework организована таким образом, что программные компоненты ядра продукта были отделены от пользовательских файлов, а также файлов, определяющих внешнее представление сайта. Данная особенность позволяет:

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

    Вся система целиком лежит в каталоге /bitrix/, в него входят следующие подкаталоги и файлы:

    • /admin/ - административные скрипты;
    • /cache/ - файлы кэша;
    • /activities/ - папки действий для бизнес-процессов;
    • /components/ - папка для системных и пользовательских компонентов;
    • /gadgets/ - папки гаджетов;
    • /js/ - файлы javascript модулей;
    • /stack_cache/ - файлы кеша "с вытеснением";
    • /themes/ - темы административного раздела;
    • /wizards/ - папки мастеров;
    • /images/ - изображения используемые как системой в целом, так и отдельными модулями;
    • /managed_cache/ - управляемый кеш;
    • /modules/ - каталог с модулями системы, каждый подкаталог которого имеет свою строго определённую структуру;
    • /php_interface/ - вспомогательный служебный каталог, в него входят следующие каталоги и файлы:
      • dbconn.php - параметры соединения с базой. С версии 20.900.0 параметры соединения берутся из файла /bitrix/.settings.php;
      • init.php - дополнительные параметры портала;
      • after_connect.php - подключается сразу же после создания соединения с базой;
      • dbconn_error.php - подключается при ошибке в момент создания соединения с базой;
      • dbquery_error.php - подключается при ошибке в момент выполнения SQL запроса;
      • /ID сайта/init.php - дополнительные параметры сайта; файл подключается сразу же после определения специальной константы c идентификатором сайта - SITE_ID;
    • /templates/ - каталог с шаблонами сайтов и компонентов , в него входят следующие подкаталоги:
      • /.default/ - подкаталог с общими файлами, используемыми тем или иным шаблоном по умолчанию, структура данного каталога аналогична нижеописанной структуре каталога содержащего конкретный шаблон;
      • /ID шаблона сайта/ - подкаталог с шаблоном сайта, в него входят следующие подкаталоги и файлы:
        • /components/ - каталог с кастомизированными шаблонами компонентов;
        • /lang/ - языковые файлы принадлежащие как данному шаблону в целом, так и отдельным компонентам;
        • /images/ - каталог с изображениями данного шаблона;
        • /page_templates/ - каталог с шаблонами страниц и их описанием хранящимся в файле .content.php. Когда пользователь создает новую страницу, он может выбрать, по какому шаблону из представленных в этом каталоге это будет сделано;
        • header.php - пролог данного шаблона;
        • footer.php - эпилог данного шаблона;
        • template_styles.css - основной файл стилей для шаблона;
        • styles.css - CSS стили шаблона для визуального редактора (вкладка Стили сайта);
    • /tools/ - при инсталляции в этот каталог копируются дополнительные страницы, которые могут быть непосредственно использованы на любых страницах сайта: помощь, календарь, показ изображения и т.п.;
    • /updates/ - каталог, автоматически создаваемый системой обновлений;
    • header.php - стандартный файл, подключающий в свою очередь конкретный пролог текущего шаблона сайта; данный файл должен использоваться на всех страницах публичной части;
    • footer.php - стандартный файл, подключающий в свою очередь конкретный эпилог текущего шаблона сайта; данный файл должен использоваться на всех страницах публичной части;
    • license_key.php - файл с лицензионным ключом;
    • spread.php - файл используемый главным модулем для переноса куков посетителя на дополнительные домены различных сайтов;
    • redirect.php - файл используемый модулем Статистика для фиксации событий перехода по ссылке;
    • rk.php - файл по умолчанию используемый модулем Реклама для фиксации событий клика по баннеру;
    • stop_redirect.php - файл используемый модулем Статистика для выдачи какого либо сообщения посетителю, попавшему в стоп-лист;
    • activity_limit.php - файл используемый модулем Статистика для выдачи сообщения роботу при превышении им лимита активности;
    • .settings.php - [ds]файл настроек[/ds][di]Bitrix Framework имеет ряд специфичных настроек ядра, которые не имеют визуального интерфейса редактирования. Этот подход вызван тем, что изменение настроек или ошибка в них легко могут привести к неработоспособности системы (настройки подключения к базе данных, настройки кеширования и т.д.).

      Подробнее ...[/di] ядра D7.
    • и другие служебные файлы и папки.

    В зависимости от используемой редакции некоторые каталоги и файлы могут отсутствовать.


    Работа с базами данных

    Цитатник веб-разработчиков.

    Антон Долганин:

    Новичкам надо поклясться на мануале, что они забудут про прямые запросы, вообще про БД забудут. Когда появится немного опыта и будет казаться что вы зверь в Битриксе - тогда можно почитать о том как происходит общение с БД, но все равно не трогать, подождать еще год-два активной работы с системой. И вот только потом уже можно подружиться с БД полностью, но помнить, что в первую очередь надо всеми правдами и неправдами сделать стандартными методами Битрикс. К БД прибегать только в случае:

    • Невозможно сделать стандартно без большой нагрузки на PHP;
    • Невозможно сделать стандартно без большой нагрузки на MySQL.
    Разработчик должен точно понимать БД. Понимать что изменится, а что нет при ближайшем обновлении.

    Одним из первых впечатлений, возникающих у начинающего осваивать Bitrix Framework программиста, бывает непонимание запросов к базе данных. Код Bitrix Framework порождает очень большие и не сразу понятные запросы. Несколько экранов не очень понятного SQL традиционно пугают людей, которые редко пишут запросы сложнее select * from ... where id=... и увереных, что объединение таблиц по условиям делается через where так же производительно, как и через on.

    Но всё не так просто. Тема работы с БД включает в себя вопросы:

    • узкотехнологические, вроде производительности конкретных выборок;
    • вопросы удобного применения технологий работы с БД в больших развиваемых проектах на растущей CMS;
    • вопросы стоимости владения и поддержки.

    Почему в Bitrix Framework запрос получается неудобочитаемым?

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

    В мощных СУБД есть умные и очень эффективные оптимизаторы запросов, а совсем плохие запросы такие СУБД даже не дают исполнять. Подавляющее большинство установок продуктов на Bitrix Framework работают на MySQL, чей оптимизатор достаточно слаб. В итоге мы имеем большие запросы, ограниченные средства их изменения с сохранением логики, и механизмы кеширования поверх всего этого.


    Минус такого положения дел

    Сложные запросы очень велики и вы почти не можете влиять на их структуру. Даже если есть желание заняться их профилировкой и отладкой, вы фактически ограничены теми не слишком богатыми средствами изменения запроса, которые предлагает Bitrix Framework. Вы не можете:

    • изменять порядок объединений;
    • задавать специфические условия на выборки;
    • создавать локальные кеширующие временные таблицы.

    Плюсы такого положения дел

    Плюсы оказываются важнее, чем минусы:

    • Запросы не надо писать руками. Создание проекта на Bitrix Framework обычно предусматривает визуальную настройку готовых компонентов, создании структуры сайта и интеграцию дизайна. Не в каждом проекте приходится кастомизировать компоненты, но даже эта работа не заставляет писать запросы.
    • Безопасность. Обертки над запросами решают задачу защиты от атак или глупостей разработчика.

    Квалификация разработчика

    Как и в любом другом деле, всё решает профессионализм работы с Bitrix Framework. И мастерство работы с БД не играет существенной роли. Объективно в большинстве проектов нагрузка на sql (время исполнения) несущественна по сравнению с нагрузкой на процессор и затратами на интерпретацию скриптов. Тормозит не MySQL, а PHP.

    В связи с этим огромную роль играет правильность проектирования структуры данных, выбор связей и их реализация средствами системы инфоблоков или таблиц. Устранить "тормоза" на проекте гораздо правильнее и эффективнее грамотным проектированием, чем оптимизацией запроса, который Bitrix Framework генерирует по вашим указаниям.

    Использование кеширования позволяет сэкономить на времени исполнения запросов. Грамотное распределение логики по компонентам позволяет вообще их не вызывать. Есть правило: главная страница сайта (обычно не самая простая) не должна отправлять запросы к БД при включенном кеше.

    Разработчик должен учитывать специфику проекта и правильно выбирать тип таблиц: InnoDB или MyISAM .

    Разработчик должен уметь оценивать уровень задач. Для действительно серьезных проектов есть кластеризация My SQL.


    Что есть в Bitrix Framework для облегчения жизни простого программиста?

    Перед началом работ по оптимизации проектов воспользуйтесь Монитором производительности для поиска узких мест на сайте.

    В составе административной части системы есть инструменты проверки БД и её оптимизации. Эти действия автоматизированы и вмешательство разработчика в них исключено.

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

    Используйте настраиваемую из административной части кластеризацию БД на стандартных механизмах MySQL.

    Используйте специфические для вашего проекта индексы. Большой запрос не всегда синоним долгого исполнения. Возьмите тяжелый запрос из системы и сделайте explain в консоли. Вы удивитесь как хорошо он покрыт индексами и как мало там будет циклических переборов записей.

    Цитатник веб-разработчиков.

    Денис Шаромов: Есть миф, что длинный запрос долго исполняется. В действительности, всё зависит от того, какой объем данных приходится обработать базе для исполнения запроса. Разбор собственно запроса занимает ничтожно малую часть времени. Основное время занимает фильтрация и сортировка. Время фильтрации зависит от параметров фильтра и индексов, а сортировка - от числа записей в выборке.

    Работа с БД

    Перед тем как начать работать с БД усвойте, что:

    Внимание! Прямое обращение к базе данных в рамках Bitrix Framework не приветствуется. Более того, если речь идет о системных таблицах самого Bitrix Framework, это не просто не приветствуется, это не поддерживается. Необходимо с ними работать через API системы, так как физическая структура БД может измениться, а работа даже самого древнего API гарантирована.

    В силу этого предупреждения названия таблиц не афишируются. Если, все же, вы возьмёте на себя ответственность прямой работы с базой данных, то учтите, что все таблицы от Bitrix Framework начинаются с b_. Соответственно ваши префиксы должны быть другими: необходимо чтобы имена ваших таблиц не пересеклись с битриксовыми. Вполне возможна ситуация, когда в новых обновлениях появится таблица с таким же именем, как и созданная вами (если вы используете префикс b_). В лучшем случае обновление не установится.

    Для работы с собственными таблицами используйте методы глобальной переменной $DB (класс CDatabase).

    Детально об оптимизации БД читайте в главе Оптимизация базы данных Курса для хостеров.

    Список ссылок по теме:

    Отложенные функции

    Отложенные функции

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

    Технология была создана в первую очередь для использования в компонентах, которые, как правило, выводятся в теле страницы, но при этом внутри них могут быть заданы заголовок страницы, добавлен пункт в навигационную цепочку, добавлена кнопка в панель управления и так далее. Отложенные функции нельзя использовать в файлах шаблона компонента: template.php и result_modifier.php (так как результаты их выполнения кешируются).

    Внутри отложенной функции можно подключать компоненты, но при этом необходимо вручную подключать файлы CSS и js.

    Примечание: Есть ряд новых функций, которые могут работать в условиях кеширования (SetViewTarget, EndViewTarget). Но такие функции новые, ещё не описаны в документации и их надо рассматривать скорее как исключение, чем как правило.

    Алгоритм работы

    1. Любой исходящий поток из PHP скрипта буферизируется.
    2. Как только в коде встречается одна из следующих функций:

      или другая функция, обеспечивающая откладывание выполнения какой-либо функции, то:

      1. весь буферизированный до этого контент запоминается в очередном элементе стека A;
      2. в стек A добавляется пустой элемент, который в дальнейшем будет заполнен результатом выполнения отложенной функции;
      3. имя отложенной функции запоминается в стеке B;
      4. буфер очищается и буферизация снова включается.

      Таким образом, существует стек A, в котором находится весь контент страницы, разбитый на части. В этом же стеке есть пустые элементы, предназначенные для их дальнейшего заполнения результатами отложенных функций.

      Также существует стек B, в котором запоминаются имена и параметры отложенных функции в порядке их следования в коде.
    3. В конце страницы в служебной части эпилога выполняются следующие действия:
      1. все отложенные функции из стека B начинают выполняться одна за другой;
      2. результаты их выполнения вставляются в специально предназначенные для этого места в стек A;
      3. весь контент из стека A "склеивается" (конкатенируется) и выводится на экран.

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

    Внимание! При использовании этой технологии необходимо учитывать, что над результатами работы функций, обеспечивающих откладывание других функций, нельзя выполнять какие-либо действия.
    Значение, возвращаемое отложенной функцией, не возвращается, а сразу выводится в месте вызова AddBufferContent, а всё, что выводится в отложенной функции, будет выведено до начала шаблона.

    Пример кода, в котором отложенная функция не будет отрабатывать код в шаблоне как ожидается:

    if (!$APPLICATION->GetTitle())
    	echo "Стандартная страница";
    else
    	echo $APPLICATION->GetTitle();

    А такой код будет работать:

    $APPLICATION->AddBufferContent('ShowCondTitle');
    
    function ShowCondTitle()
    {
    	global $APPLICATION;
    if (!$APPLICATION->GetTitle())
    	return "Стандартная страница";
    else
    	return $APPLICATION->GetTitle();
    }

    Ещё один пример

    $page_title = $APPLICATION->GetPageProperty($title);
    if (strlen($page_title)<=0) $page_title = "Заголовок страницы по умолчанию";
    echo $page_title;

    этот код не будет работать по той причине, что все отложенные функции выполняются в самом конце страницы, в служебной части эпилога.

    Пример:

    <?
    require($_SERVER["DOCUMENT_ROOT"]."/bitrix/header.php");
    $APPLICATION->SetTitle("Старый заголовок");
    ?>
    <?
    global $APPLICATION;
    $strTitle = $APPLICATION->GetTitle();
    echo $strTitle." - Заголовок страницы

    "; $APPLICATION->SetTitle('Новый заголовок'); ?> <?require($_SERVER["DOCUMENT_ROOT"]."/bitrix/footer.php");?>

    На странице будет напечатано - Старый заголовок, а в браузере - Новый заголовок.

    Задействованные группы функций

    Имя функции обеспечивающей откладывание Выполнение какой функции откладываетсяДополнительные связанные функции
    CMain::ShowTitle CMain::GetTitle CMain::SetTitle
    CMain::ShowCSS CMain::GetCSS CMain::SetTemplateCSS
    CMain::SetAdditionalCSS
    CMain::ShowNavChain CMain::GetNavChain CMain::AddChainItem
    CMain::ShowProperty CMain::GetProperty CMain::SetPageProperty
    CMain::SetDirProperty
    CMain::ShowMeta CMain::GetMeta CMain::SetPageProperty
    CMain::SetDirProperty
    CMain::ShowPanel CMain::GetPanel CMain::AddPanelButton

    Технология позволяет создавать отложенные функции с помощью метода CMain::AddBufferContent.

    Список ссылок по теме:

    Файл init.php

    Файл init.php

    Цитатник веб-разработчиков.

    Максим Месилов: С течением времени на проекте накапливается ворох «общих» функций, обработчиков событий, специфических библиотек и т.д. Очень часто всё это валят в init.php и там чёрт ногу сломит.

    init.php - необязательный файл в рамках структуры файлов Bitrix Framework. Он автоматически подключается в прологе.

    Файл может содержать в себе инициализацию обработчиков событий, подключение дополнительных функций - общие для всех сайтов. В этом случае он располагается по пути /bitrix/php_interface/init.php. Для каждого отдельного сайта может быть свой аналогичный файл. В этом случае он располагается по пути /bitrix/php_interface/ID сайта/init.php. Если есть оба файла, то система подключит оба, но первым при этом будет файл /bitrix/php_interface/init.php.
    Начиная с версии 14.0.1 рекомендуется размещать этот файл в папке [ds]/local[/ds][di]Чтобы сделать жизнь разработчиков проектов удобнее основные файлы проекта вынесены из папки /bitrix в папку /local. Это позволит изолировать изменяющиеся файлы проекта от папки продукта. По сути, в исключения достаточно будет добавить одну папку /bitrix.

    Подробнее ...[/di] по пути /local/php_interface/ID сайта/init.php. Файл /bitrix/php_interface/init.php при этом перестаёт работать.

    Примечание: Файл /bitrix/php_interface/ID сайта/init.php не подключается в административном разделе, так как там отсутствует понятие сайта. Учтите и то, что SITE_ID равен текущему языку и, следовательно, может подключиться не тот файл, который ожидался.

    Код условного отключения по значению $_SESSION в файле init.php не работает, так как эта переменная определяется позже.

    Чтобы init.php не превращался в свалку непонятного кода следует код размещать логически группируя по файлам и классам.

    Рекомендуется придерживаться следующих самых общих правил:

    1. init.php содержит только подключения файлов. Причем подключать эти файлы желательно через __autoload. Штатными средствами это делается так:
      session_start();
      CModule::AddAutoloadClasses(
      	'', // не указываем имя модуля
      	array(
      		// ключ - имя класса, значение - путь относительно корня сайта к файлу с классом
      			'CMyClassName1' => '/path/cmyclassname1file.php',
      			'CMyClassName2' => '/path/cmyclassname2file.php',
      		)
      );
    2. Если функционал используется только на одном из сайтов в системе, то он выносится в свой init.php;
    3. Обработчики событий лучше группировать в одном файле и тщательно аннотировать где они используются и какая задача перед ними стоит.

    Примечание: Переменная $_SESSION не доступна в init.php, так как ядро стартует сессию позже, совершая определенные настройки и проверки. Настоятельно не рекомендуется самостоятельно подключать сессию "руками".

    Проблемы при редактировании

    Как избежать проблем при редактировании init.php без ftp/ssh доступа

    Ошибка в файле init.php приводит к полной потере работоспособности сайта и невозможности что-то исправить без доступа к файлу через ftp/ssh напрямую с диска. Такое возможно, например, в случаях:

    • У клиента хостинг под Windows, а у вас "серый" IP и ftp не работает.
    • Хостинг под Linux, но php и ftp работают из-под разных пользователей и файл недоступен для редактирования по ftp.
    • У клиента свой сервер и доступ по ftp он не дает.

    Если доступ есть только через веб, то один из наиболее простых способов - вынос всего вашего кода во внешний файл и подключение его примерно так:

    if (isset($_GET['noinit']) && !empty($_GET['noinit']))
    {
    	$strNoInit = strval($_GET['noinit']);
    	if ($strNoInit == 'N')
    	{
    		if (isset($_SESSION['NO_INIT']))
    			unset($_SESSION['NO_INIT']);
    	}
    	elseif ($strNoInit == 'Y')
    	{
    		$_SESSION['NO_INIT'] = 'Y';
    	}
    }
    
    if (!(isset($_SESSION['NO_INIT']) && $_SESSION['NO_INIT'] == 'Y'))
    {
    	if (file_exists($_SERVER["DOCUMENT_ROOT"]."/bitrix/php_interface/functions.php"))
    		require_once($_SERVER["DOCUMENT_ROOT"]."/bitrix/php_interface/functions.php");
    }

    Примечание: Параметр в адресной строке noinit=Y - отключает подключение, noinit=N - включает подключение. Свой функционал должен быть размещен (в примере) в /bitrix/php_interface/functions.php.

    Рекомендуется использовать собственное именование ключа. К примеру, nomysuperinit, так как курс в открытом доступе и любой может ознакомиться с этим методом, в том числе и с неблаговидными целями.

    Хорошо если проверки безопасности и прочего не содержатся в этом файле, к тому случаю если злоумышленник все же угадает как отключить init.php.

    При таком подходе вы сможете безболезненно редактировать и допускать ошибки в файле functions.php, не боясь оказаться у разбитого корыта неработающего сайта без возможности как-то повлиять на ситуацию.

    init.php vs собственный модуль

    У разработчика проектов есть два способа постоянного использования уже созданных наработок: собственный модуль или файл init.php. Оба варианта имеют свои плюсы и минусы.

    init.php. Когда у вас есть классы, единые для нескольких сайтов (при многосайтовости, или на одном сервере), то для удобства сами файлы с классами располагаются в одной папке, далее создаются символические ссылки.

    Либо, при многосайтовости, есть и ещё один способ: воспользоваться уже готовыми папками. Например, папками шаблонов. (Теоретически, с точки зрения системы, это "шаблон", для нас - просто общее хранилище файлов.)

    И при втором и первом способах код include $_SERVER["DOCUMENT_ROOT"]."/bitrix/templates/..." ведет в одно и то же место для всех сайтов, где можно разместить общие для всех проектов файлы. В том числе никто не мешает создать свою папку для своих классов и уже оттуда их подключать, обращаясь к /bitrix/templates/my_classes/....

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

    Модули. Использование init.php будет предпочтительным если создаются проекты, которые точно всю жизнь проживут на одном сервере, и вы хотите максимально минимизировать затраты на поддержку кастомных библиотек. Желательно также чтобы эти библиотеки поддерживал один и тот же человек. Но если планируется эти наработки также использовать для заказных проектов, то дальновиднее делать модуль.

    Модуль больше подходит для распространяемых API. При этом придется поддерживать и их интерфейс и формат ответа и другие параметры. Иначе: случайное обновление на одном из сайтов, использующих модуль, может оказаться роковым в случае изменения интерфейсов или форматов ответов API. Выигрыш спорный. С одной стороны - выигрываете, с другой - должны обеспечить пожизненные гарантии для всех использующих API модуля сайтов.

    Языковые файлы

    Цитатник веб-разработчиков.

    Антон Долганин: В компонентах языковые фразы выношу в ланг-файлы, просто потому что это системная часть и там хотелось бы видеть порядок.

    Языковой файл - представляет из себя PHP скрипт, хранящий переводы языковых фраз на тот или иной язык. Данный скрипт состоит из массива $MESS, ключи которого - идентификаторы языковых фраз, а значения - переводы на соответствующий язык.

    Языковые файлы не являются обязательными.

    Пример языкового файла для русского языка:

    <?
    $MESS ['SUP_SAVE'] = "Сохранить";
    $MESS ['SUP_APPLY'] = "Применить";
    $MESS ['SUP_RESET'] = "Сбросить";
    $MESS ['SUP_EDIT'] = "Изменить";
    $MESS ['SUP_DELETE'] = "Удалить";
    ?>

    Пример языкового файла для английского языка:

    <?
    $MESS ['SUP_SAVE'] = "Save";
    $MESS ['SUP_APPLY'] = "Apply";
    $MESS ['SUP_RESET'] = "Reset";
    $MESS ['SUP_EDIT'] = "Change";
    $MESS ['SUP_DELETE'] = "Delete";
    ?>

    Примеры работы

    • просмотр всего массива словаря:

      <? echo'<pre>';print_r($MESS);echo'</pre>'; ?>
      

    • получение названия месяца в двух падежах:

      <?
      	echo $MESS['MONTH_'.date('n')]; // Июнь
      	echo $MESS['MONTH_'.date('n').'_S']; // Июня
      ?>
      


    Работа с языковыми файлами

    Языковые файлы

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


    Языковые файлы, как правило, используются в административных скриптах модулей или в компонентах и в зависимости от этого подключаются одной из следующих функций:

    Для удобства поиска и дальнейшей модификации языковых фраз можно пользоваться параметром страницы show_lang_files=Y, позволяющим быстро найти и исправить ту или иную языковую фразу в модуле Перевод.

    Файлы в собственных компонентах

    При создании собственного компонента языковой путь должен выглядеть следующим образом:

    /bitrix/templates/[шаблон_сайта|.default]/components/[пространство_имен]/[имя_компонента]/[имя_шаблона_компонента]/lang/[код_языка]/template.php

    Где код языка, к примеру = ru.

    В таком случае языковые файлы подключатся автоматически.


    Для подключения из компонента языковых сообщений другого компонента можно использовать следующую функцию:

    function IncludeComponentLangFile ($abs_path, $lang = false)
    {
    if ($lang === false) $lang = LANGUAGE_ID;
    
    global $BX_DOC_ROOT;
    
    $filepath = rtrim (preg_replace ("'[\\\\/]+'", "/", $abs_path), "/ ");
    
    if (strpos ($filepath, $BX_DOC_ROOT) !== 0)
    {
    return;
    }
    
    $relative_path = substr ($filepath, strlen ($BX_DOC_ROOT));
    
    if (preg_match ("~^/bitrix/components/([-a-zA-Z0-9_\.%]+)/([-a-zA-Z0-9\._%]+)/templates/([-a-zA-Z0-9\._%]+)/(.*)$~", $relative_path, $matches))
    {
    $lang_path = $BX_DOC_ROOT."/bitrix/components/$matches[1]/$matches[2]/templates/$matches[3]/lang/$lang/$matches[4]";
    __IncludeLang ($lang_path);
    return;
    }
    
    if (preg_match ("~^/bitrix/components/([-a-zA-Z0-9_\.%]+)/([-a-zA-Z0-9\._%]+)/(.*)$~", $relative_path, $matches))
    {
    $lang_path = $BX_DOC_ROOT."/bitrix/components/$matches[1]/$matches[2]/lang/$lang/$matches[3]";
    __IncludeLang ($lang_path);
    return;
    }
    }
    

    Замена фраз в продукте

    Иногда при разработке требуется заменить какие-то слова или фразы в компонентах или модулях.

    Коснемся этой технологии, суть которой в том, что после подключения языкового файла фразы продукта заменяются на определенные разработчиком.

    Путь к файлу замен:

    /bitrix/php_interface/user_lang/<код языка>/lang.php

    Примечание: если в php_interface нет нужных папок, их следует создать.
    Возможно использование папки /local/php_interface/user_lang/<код языка>/lang.php.

    В файле должны определяться элементы массива $MESS в виде $MESS['языковой файл']['код фразы'] = 'новая фраза', например:

    <?
    $MESS["/bitrix/components/bitrix/system.auth.form/templates/.default/lang/ru/template.php"]["AUTH_PROFILE"] = "Мой любимый профиль";
    $MESS["/bitrix/modules/main/lang/ru/public/top_panel.php"]['top_panel_tab_view'] = "Смотрим";
    $MESS["/bitrix/modules/main/lang/ru/interface/index.php"]['admin_index_sec'] = "Проактивка";
    ?>
    

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

    Важно! Возможны проблемы сопровождения этого файла при изменении кода фразы или расположения языкового файла.

    Языковые фразы в D7

    По умолчанию подстановка языковых фраз не работает в файле component_epilog.php шаблона компонента. Поэтому в нем языковые фразы подключаются следующим образом:

    use \Bitrix\Main\Localization\Loc;
    Loc::loadLanguageFile(__FILE__);
    echo Loc::getMessage("SOMETHING_LANGUAGE_CONSTANT"); 
    

    Сами языковые фразы, например, для русского языка должны быть заданы в файле lang/ru/component_epilog.php. Кроме того, при таком подключении языковые фразы будут работать как в кеше, так и "мимо" кеша.


    Гаджеты и их создание

    Гаджет – особый программный элемент, выполняющий функцию вывода определенных данных.

    Об имеющихся гаджетах, их настройке и управлении ими, вы можете прочитать в курсах:

    Для отображения гаджетов используется компонент Рабочий стол (Desktop). Этот одностраничный компонент позволяет создать настраиваемый рабочий стол с использованием этих программных элементов. В компоненте гаджеты инсталлируются вместе с главным модулем системы. Разработчики могут создавать собственные гаджеты. Компонент их увидит, если они будут размещаться в папке /bitrix/gadgets/. (Название папки обязательно должно быть в нижнем регистре.) Системные гаджеты располагаются во вложенной папке /bitrix/.

    Внимание! Категорически не рекомендуется без крайней необходимости трогать структуру системных гаджетов.

    Структура гаджета

    • .description.php - файл описания, содержащий массив параметров;
    • .parameters.php - файл с настройками. В данном файле формируется массив $arParameters. Особое внимание стоит обратить на ключи PARAMETERS (общие настройки) и USER_PARAMETERS (уникальные настройки конкретного пользователя);
    • файл index.php, который содержит исполняемый код, реализующий задачу гаджета;
    • языковые файлы в папке /lang/.
    • другие служебные файлы и папки.

    Гаджеты и компоненты имеют похожую структуру и назначение, однако гаджеты:

    • Не используют шаблоны. HTML код зашит в файле index.php, в отличие от компонентов, где представление и логика разнесены.
    • Могут запоминать настройки для каждого пользователя, в отличие от компонентов, которые могут только выводить или не выводить информацию в зависимости от прав доступа.
    • Их настройки разделены на 2 группы: общие настройки, для гаджетов одного типа (например, для всех гаджетов Новости в рамках одного Рабочего стола), а также настройки каждого конкретного гаджета. Общие настройки задаются в компоненте Рабочий стол. Индивидуальные настройки задаются в контекстном меню каждого конкретного гаджета.

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


    Служебные параметры

    Служебные параметры недоступны настройке через интерфейс.


    ПараметрОписание
    NAME Название
    DESCRIPTION Описание
    ICONИконка (указать путь)
    GROUP Группа, к которой относится гаджет. Указание группы определяет где в выпадающем списке гаджетов он будет показан
    SU_ONLY Только для подключения у пользователя.
    SG_ONLY Только для подключения в группах.
    BLOG_ONLY Только для подключения в блогах.
    AI_ONLY Только для подключения в административном разделе.
    NOPARAMS Нет параметров

    Список ссылок по теме:



    JS-библиотека

    Цитатник веб-разработчиков.

    Рамиль Юналиев: Почему-то многие неохотно используют битриксовую библиотеку аякса, почти всегда делая предпочтения в сторону Jquery или других js библиотек. Несомненно то, что Jquery очень неплохая библиотека и отлично работает с ajax, но чтобы свободно разрабатывать сайты на 1с-Битрикс надо знать родное API.

    Документация по JS-библиотеке Bitrix предполагает уверенные знания разработчика в JavaScript.

    Javascript-библиотека Битрикс очень разнообразна. Основным (главным) классом является класс BX. Также к услугам разработчика представлено достаточно большое количество расширений. Например, расширение по работе с датами.

    При работе с JS в Bitrix Framework можно использовать как штатную библиотеку, так и сторонние библиотеки. При использовании сторонних библиотек, их предварительно необходимо подключить. Подробнее про подключение внешнего JS-кода вы можете прочитать тут.

    Внимание! С версии главного модуля 22.0 прекращена поддержка IE в core.js.

    Список ссылок по теме:


    Подключение JS-кода

    Варианты расположения кода

    Перед написанием JS-кода встает резонный вопрос - где его хранить?

    Существуют следующие варианты:

    • Если вы разрабатываете JS-код для компонента и данный код больше нигде не применяется, разумнее расположить файл script.js в шаблоне самого компонента.
    • Если JS-код общий для всей публичной части, разумнее его разместить в шаблоне сайта. Как правило, такие JS-файлы хранятся в подпапке шаблона js/ и подключаются в самом шаблоне методом: Bitrix\Main\Page\Asset::getInstance()->addJs(); Но этот путь не подходит, если шаблонов сайта несколько, или вы разрабатываете код для административной части, или публичный шаблон запрещено менять по причине его обновлений компанией Битрикс (например, коробочный шаблон Битрикс24).
    • В случае, описанном выше, применяется метод хранения файлов по пути /local/file.js. Этот же метод размещения стоит избрать в случае создания вами своего модуля.

    Регистрация и подключение библиотек

    Остановимся поподробнее на последнем варианте. Естественно, вы можете также разместить код в шаблоне сайта через Bitrix\Main\Page\Asset::getInstance()->addJs();. Но более правильным решением будет третий подход.

    Каждый файл в вашей папке по сути является отдельной мини-библиотекой, которую надо зарегистрировать. [dw]Регистрация[/dw][di]Регистрация библиотек в include.php модуля или в init.php.[/di] осуществляется с помощью следующего кода:

    $arJsConfig = array( 
    	'custom_main' => array( 
    		'js' => '/bitrix/js/custom/main.js', 
    		'css' => '/bitrix/js/custom/main.css', 
    		'rel' => array(), 
    	) 
    ); 
    
    foreach ($arJsConfig as $ext => $arExt) { 
    	\CJSCore::RegisterExt($ext, $arExt); 
    }
    

    Как видите, код универсален, и можно зарегистрировать несколько файлов. В ключе CSS вы дополнительно можете указать CSS-файл (в случае если CSS-код идет в пару к JS), а ключом rel перечислить коды других BX-библиотек, которые будут автоматически подключены при подключении этой библиотеки.

    Когда библиотеки зарегистрированы, их можно подключить с помощью следующей конструкции:

    CUtil::InitJSCore(array('custom_main'));
    

    Два указанных выше блока кода иногда идут вместе, иногда - раздельно. Например, в случае разработки собственного модуля регистрацию надо совершать в include.php модуля (к примеру), а инициировать (вызывать InitJSCore) в нужном вам месте (например, в шаблоне компонента).

    Ошибка слияния файлов

    JS-класс к шаблону компонента

    Иногда при разработке компонента его шаблон необходимо наделить js-функциональностью, событиями и прочим. Выглядеть это может примерно так:

    if (typeof(BX.CrmQuickPanelEdit) === 'undefined')
    {
    	BX.CrmQuickPanelEdit = function(id)
    	{
    		this._id = id;
    		this._settings = {};
    		this._submitHandler = BX.delegate(this._clickHandler, this);
    		BX.bind(BX(this._id + '_submit'), 'click', this._submitHandler);
    	};
    	BX.CrmQuickPanelEdit.prototype =
    	{
    		initialize: function(id, settings)
    	{
    		this._id = id;
    		this._settings = settings;
    	},
    		getId: function()
    	{
    		return this._id;
    	},
    	_clickHandler: function(e)
    	{
    		console.log(e);
    	}
    	};
    	BX.CrmQuickPanelEdit.create = function(id, settings)
    	{
    	var _self = new BX.CrmQuickPanelEdit(id);
    		_self.initialize(id, settings);
    		return _self;
    	};
    }

    Аналогичные подходы вы можете видеть в JS-ядре Bitrix Framework (расположен в /bitrix/js/). Разобравшись в данном примере вы сможете легче понимать JS-код разработчиков Битрикс.

    Пример вызова:

    <script type="text/javascript">
    	BX.ready(function(){
    		BX.CrmQuickPanelEdit.create('some_id', null);
    	});
    </script>

    Если нужен паттерн "одиночка", то реализовать это можно как часть create:

    BX.CrmQuickPanelEdit._self  = null;
    BX.CrmQuickPanelEdit.create = function(id, settings)
    {
    	if (!this._self) {
    	this._self = new BX.CrmQuickPanelEdit();
    }
    	this._self.initialize(id, settings);
    	return this._self;
    };

    Что делает JS-класс: запоминает внутри себя некий ID (например, это может быть ID контейнера) и массив параметров, а также вешает обработчик на событие клика по кнопке подтверждения в форме указанного контейнера.


    JS-расширение медиаплеера

    Примеры работы на js с плеером

    Не забываем подключить расширение:

    CJSCore::Init(['player']);

    Создание и инициализация плеера

    Здесь самое важное - передать mime-type для каждого файла. В данном примере плеер будет проходить по всем файлам из списка и проиграет первый, который сможет. Поэтому в списке должен быть один и тот же ролик с разными расширениями.

    BX.ready(function()
    {
    	var player = new BX.Fileman.Player('player_id', {
    		sources: [
    		{
    			src: 'https://dev.1c-bitrix.ru/download/files/video/learning/hermitage.mp4',
    			type: 'video/mp4'
    		}
    		]
    	});
    	var playerNode = player.createElement();
    	BX('player_node').appendChild(playerNode);
    	player.init();
    });

    Описание параметров плеера

    Параметр Тип Описание
    sources array Массив файлов для проигрывания
    autostart bool Запускать автоматически или нет
    hasFlash bool Установить true, если придётся проигрывать *.flv файлы. В этом случае плеер подгрузит swf-файл плеера.
    playbackRate float От 0 до 3. Скорость проигрывания (работает не всегда).
    volume float От 0 до 3. Громкость
    startTime int Время в секундах, с которого надо начать проигрывание
    onInit function Вызовется сразу после инициализации плеера
    lazyload bool Если true - плеер будет инициализироваться только при нахождении на экране пользователя.
    skin string Название класса-скина. CSS-файл надо предварительно загрузить самостоятельно.
    width int Ширина плеера
    height int Высота плеера
    isAudio bool Установить true, если плеер нужен для проигрывания аудио.

    После создания получить объект плеера можно через менеджер

    var player = BX.Fileman.PlayerManager.getPlayerById('player_id'); 

    Некоторые полезные методы

    МетодОписание
    player.createElement(); Создает html-ноду и возвращает её.
    player.isPlaying(); Возвращает true, если плеер сейчас проигрывает что-то.
    player.pause(); Ставит проигрывание на паузу.
    player.isEnded(); Возвращает true, если файл был проигран до конца.
    player.isReady(); Возвращает true, если плеер полностью инициализирован.
    player.play(); Запускает проигрывание.
    player.setSource({
    src: 'path',
    type: 'mime-type'
    });
    Устанавливает источник проигрывания
    player.getSource(); Возвращает текущий источник
    player.init(); Выполняет инициализацию плеера.
    player.mute(status); Включает / выключает звук

    Пример создания и инициализации аудио плеера

    BX.ready(function()
    {   
    	var audioPlayer = new BX.Fileman.Player('audio_player_id', {
    	isAudio: true,
    	sources: [
    		{
    			src: '/upload/SampleAudio_0.7mb.mp3',
    			type: 'audio/mp3'
    		}
    	],
    	onInit: function(player)
    	{
    		// следующие три строки нужны, чтобы скрыть кнопку разворачивания на весь экран
    		player.vjsPlayer.controlBar.removeChild('timeDivider');
    		player.vjsPlayer.controlBar.removeChild('durationDisplay');
    		player.vjsPlayer.controlBar.removeChild('fullscreenToggle');
    		// это прячет большую кнопку плей
    		player.vjsPlayer.hasStarted(true);
    	}
    	});
    	var audioPlayerNode = audioPlayer.createElement();
    	BX('audio_player_node').appendChild(audioPlayerNode);
    	audioPlayer.init();
    });


    Примеры кастомизации публичной части

    Описание

    Задачи добавления функционала в визуальную часть действующего проекта - частая просьба клиента. Например, просьба добавить кнопку в карточку задачи, группы или сотрудника. Для "1С-Битрикс: Управление сайтом" более приемлем классический способ копирования и кастомизации шаблона компонента. Кастомизация визуальной части проекта, реализованная с помощью JS, более практична для решения задач в коробочной версии Битрикс24, хотя может использоваться и в БУС тоже.

    Использование для этих целей JS вместо кастомизации шаблона, имеет свои особенности:

    1. Возможна задержка вывода контента при построении страницы.
    2. Зависимость от обновлений визуальной части вендором.

    В каждом конкретном случае разработчик самостоятельно решает какой способ добавления визуального функционала (JS или классический) предпочтителен.

    Добавляемый JS-код хранится в рамках системы в нескольких местах, но при кастомизации визуальной части лучше использовать место для файлов по пути: /bitrix/js/<ваша уникальная папка>/file.js.

    Кнопка в карточке задачи

    Добавить кнопку в карточку задачи

    Заказчик поставил задачу: Поставить кнопку Скачать в PDF в карточке задачи.

    После выбора места для кнопки находим конкретного "соседа" к которому нужно привязаться при внедрении кнопки. В нашем примере место для кнопки - рядом с кнопкой Редактировать, значит ищем класс элемента-соседа:

    Нажмите на рисунок, чтобы увеличить

    Далее используем код:

    //код исполняем, только когда DOM загружен 
    BX.ready(function(){ 
    	var editButton = BX.findChild(//найти пасынков... 
    		BX('task-view-buttons'),//...для родителя 
    			{//с такими вот свойствами 
    			tag: 'a', 
    		className: 'task-view-button edit' 
    			}, 
    			true//поиск рекурсивно от родителя 
    			); 
    	if (editButton)  
    	{ 
    		var href = window.location.href, matches, taskId; 
    		//узнаем id задачи из URL 
    		if (matches = href.match(/\/task\/view\/([\d]+)\//i)) { 
    			taskId = matches[1]; 
    	} 
    	//создаем кнопку 
    	var newButton = BX.create('a', { 
    		attrs: { 
    			href: href + (href.indexOf('?') === -1 ? '?' : '&') + 'task=' + taskId + '&' + 'pdf=1&sessid=' + BX.bitrix_sessid(), 
    			className: 'task-view-button edit webform-small-button-link task-button-edit-link' 
    		}, 
    		text: 'Скачать как PDF' 
    	}); 
    	//вставляем кнопку 
    	BX.insertAfter(newButton, editButton); 
    	} 
    });

    Допускается такой способ создания кнопки:

    Добавление в меню

    Аналогичную команду Скачать как PDF внедряем в меню Ещё карточки задачи.

    Задача решается с помощью обработчика:

    //код исполняем, только когда DOM загружен 
    BX.ready(function(){ 
    	BX.addCustomEvent('onPopupFirstShow', function(p) { 
    		var menuId = 'task-view-b'; 
    		if (p.uniquePopupId === 'menu-popup-' + menuId) 
    		{ 
    			var menu = BX.PopupMenu.getMenuById(menuId), 
    				href = window.location.href,  
    				matches, taskId;             
    			//узнаем id задачи из URL 
    			if (matches = href.match(/\/task\/view\/([\d]+)\//i)) { 
    			taskId = matches[1]; 
    		} 
    			//добавляем пункт меню, полученному по id 
    			menu.addMenuItem({ 
    				text: 'Скачать как PDF', 
    				href: href + (href.indexOf('?') === -1 ? '?' : '&') + 'task=' + taskId + '&' + 'pdf=1&sessid=' + BX.bitrix_sessid(), 
    				className: 'menu-popup-item-create' 
    			}); 
    		} 
    	}); 
    });

    Добавление пункта в меню предпочтительнее, чем использование кнопки. В этом случае при построении DOM не будет "миганий" и "дёрганий".

    Узнать ID popup'а можно так:

    Нажмите на рисунок, чтобы увеличить

    Внедрение в меню - частный случай внедрения в popup. Общий пример выглядит так:

    //код исполняем, только когда DOM загружен 
    BX.ready(function(){ 
    	BX.addCustomEvent('onPopupFirstShow', function(p) { 
    		if (p.uniquePopupId === 'task-templates-popup-templateselector') 
    		{ 
    			p.contentContainer.innerHTML = 'blabla'; 
    		} 
    	}); 
    });

    Результат такого внедрения:

    Замена действий

    Например, при нажатии на кнопку Завершить должен появиться popup для введения обязательного комментария.

    //метод, вызываемый при клике
    	var completeAction = function(){ 
    		var popup = new BX.PopupWindow('customComplete', BX.proxy_context, { 
    			darkMode: true, 
    			closeByEsc : true, 
    			contentColor: 'white' 
    		}); 
    		popup.setContent('Введите сначала комментарий: <input type="text">'); 
    		popup.show(); 
    	}; 
    
    //селектор места вставки
    var completeButton = BX.findChild(//найти пасынков... 
    	BX('task-view-buttons'),//...для родителя 
    	{//с такими вот свойствами 
    		tag: 'span', 
    		className: 'task-view-button complete' 
    	}, 
    		true//поиск рекурсивно от родителя 
    	); 
    
    if (completeButton)  
    	{ 
    		//сначала вставляем свою кнопку 
    		BX.insertAfter(BX.create('span', { 
    			attrs: { 
    				href: '#', 
    				className: 'task-view-button complete webform-small-button webform-small-button-accept' 
    			}, 
    			events: { 
    				click: BX.proxy(completeAction, this) 
    			}, 
    			text: 'Завершить' 
    		}), completeButton); 
    		//затем удаляем старую 
    		BX.remove(completeButton); 
    	} 

    Серверный контроль

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

    <?php 
    AddEventHandler('main', 'onProlog', function(){ 
    	$request = \Bitrix\Main\Context::getRequest(); 
    	if ($request->get('pdf_download')) 
    	{ 
    		// 
    	} 
    });

    Форматирование дат в Javascript

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

    Подключение в PHP

    CJSCore::Init("date");

    Вызов в Javascript

    BX.date.format("формат", дата);

    формат - полный аналог формата функции date, за исключением формата T и e (символьное название таймзоны). Также поддерживаются расширенные форматы функции FormatDate.

    Если есть символы, которые не должны быть отформатированы, то необходимо эти символы оформить слешами.

    BX.date.format("H:m:s \\m \\i\\s \\m\\o\\n\\t\\h")

    Если формат начинается с символа ^, то функция обрежет нули. Примеры:

    15.04.12 13:00:00 => 15.04.12 13:00
    00:01:00 => 00:01
    4 may 00:00:00 => 4 may
    01-01-12 00:00 => 01-01-12

    дата - это либо timestamp в секундах (тип Number), либо объект класса Date. Иначе по умолчанию текущее время (new Date()). Примеры:

    BX.date.format("d-m-Y H:i:s");
    BX.date.format("j F Y H:i:s");
    BX.date.format("^d-m-Y H:i:s");
    BX.date.format("H:m:s \\m \\i\\s \\m\\o\\n\\t\\h");
    BX.date.format("Hago | dago | sago | iago | mago | Yago", new Date(2007, 2, 1, 0, 0, 0));
    BX.date.format("sago | iago", 1320271200);

    Также как и FormatDate, BX.date.format может принимать массив форматов для вычисление вида "1 секунда назад", "2 минуты назад" и так далее.

    var format = [
    	["tommorow", "tommorow, H:i:s"],
    	["s" , "sago"],
    	["H", "Hago"],
    	["d", "dago"],
    	["m100", "mago"],
    	["m", "mago"],
    	["-", ""]
    ];
    BX.date.format(format, new Date(2007, 2, 1, 0, 0, 0));

    Массив format состоит из элементов вида ["интервал формата", "формат"], где интервал формата определяет на каком интервале времени применится формат.
    Массив format обрабатывается последовательно до первого совпадения.
    Интервал времени определяется между указанной датой (первый параметр в BX.format.date) и текущей датой.

    Стандарные значения "интервал формата"
    s до 60 секунд
    i до 60 минут
    H до 24 часов
    d до 31 дня
    m до года
    sN до N секунд *
    iN до N минут *
    HN до N часов *
    dN до N дней *
    mN до N месяцев *
    today сегодня
    yesterday вчера
    tommorow завтра
    - дефис означает дату в будущем

    * - где N - это любое положительное число.

    Рассмотрим пример:

    var format = [
    	["-", "d.m.Y H:i:s"]
    	["s300" , "sago"],
    	["H", "Hago"],
    	["d", "dago"],
    	["m", "mago"]
    ];
    
    BX.date.format(format, new Date(2007, 2, 2, 9, 58, 0), new Date(2007, 2, 2, 10, 0, 0)); //1
    BX.date.format(format, new Date(2007, 2, 2, 0, 0, 0), new Date(2007, 2, 2, 10, 0, 0)); //2
    BX.date.format(format, new Date(2007, 2, 1, 0, 0, 0), new Date(2007, 2, 2, 10, 0, 0)); //3
    BX.date.format(format, new Date(2007, 2, 3, 0, 0, 0), new Date(2007, 2, 2, 10, 0, 0)); //4

    Третий параметр в BX.date.format - это текущая дата (описание смотри ниже).

    1. "120 секунд назад", сработало ["s300" , "sago"], т.к интервал времени меньше 300 секунд
    2. "10 часов назад", сработало ["H", "Hago"], т.к интервал времени меньше 24 часов
    3. "1 сутки назад", сработало ["d", "dago"], т.к интервал времени меньше 31 дня
    4. "03.03.2007 00:00:00" сработало ["-", "d.m.Y H:i:s"], так указана дата в будущем

    Для значения по умолчанию последним элементом можно указать пустой "интервал формата":

    var format = [
    	["s" , "sago"],
    	["H", "Hago"],
    	["d", "dago"],
    	["m", "mago"],
    	["", "d.m.Y H:i:s"]
    ];
    BX.date.format(format, new Date(2007, 2, 1, 0, 0, 0));

    Полная сигнатура BX.date.format

    BX.date.format("формат", дата, текущее время, utc);

    текущая дата - дата (timestamp в секундах, либо объект класса Date), которая используется для вычислений типа "1 секунда назад", "2 года назад". Если не задано, по умолчанию new Date().

    utc - дата в UTC. По умолчанию false. Если необходимо работать с датами в UTC.


    Небольшая шпаргалка для соответствия серверного и клиентского кода:

    time() = new Date()
    mktime(...) = new Date(...)
    gmmktime(...) = new Date(Date.UTC(...))
    mktime(0,0,0, 1, 1, 1970) != 0          new Date(1970,0,1).getTime() != 0
    gmmktime(0,0,0, 1, 1, 1970) == 0        new Date(Date.UTC(1970,0,1)).getTime() == 0
    date("d.m.Y H:i:s") = BX.date.format("d.m.Y H:i:s")
    gmdate("d.m.Y H:i:s") = BX.date.format("d.m.Y H:i:s", null, null, true);

    BX.date.convertBitrixFormat

    Функция конвертирует битрикс-формат даты в формат функции date. Форматы даты текущего сайта можно получить так:

    BX.message("FORMAT_DATE");
    BX.message("FORMAT_DATETIME");

    Типовые ошибки и советы

    Небольшой список типовых ошибок и советов по работе с JS-библиотекой.

    • Не используйте старые библиотеки utils.js, ajax.js и chttprequest.js.
    • Избегайте использования BX.findChild и BX.findChildren. Функции эти не страшные, но из-за неправильного использования и большого DOM'а, количество итераций возрастает до нескольких тысяч. Лучше используйте выборки по ID или по firstChild, parentNode, nextSibling, previousSibling.
    • Избегайте BX.loadScript и BX.loadCSS. Они плохи тем, что:
      • Не работает сжатие JS и CSS
      • loadScript повторно выполняет скрипт. В этом примере
        BX.loadScript("script.js"); BX.loadScript("script.js");
        первый вызов скачает и выполнит скрипт, второй вызов выполнит скрипт повторно.
      • loadScript грузит скрипты последовательно (скрипты, подключенные через тег script, грузятся параллельно), делая задержки в 50 мс между загрузками. Если вы грузите 5 файлов, то в лучшем случае это займет 200мс.
      • Загрузка CSS вызывает пересчет стилей
      • Практически везде, где используется loadScript, разработчики не добавляют timestamp после названия файла (my_script.js?12345678). Из-за этого происходит двойное скачивание и выполнение скрипта. Плюс при изменении файла, у клиентов не сбросится кеш.
      Выход: использовать методы AddHeadScript и SetAdditionalCSS.

      Еще один момент, который может показаться очевидным. Браузер гарантирует подключение и выполнение скриптов в той последовательности, в которой они идут на странице. Скрипты с атрибутами async и defer не подчиняются этому правилу, но в Bitrix Framework таких нет. Не нужно проверять объект на существование перед использованием и уж тем более не стоит его загружать через loadScript.

    • Не рекомендуется Inline CSS (имеется в виду теги link и style, выводимые в body).
    • Не делайте больших inlinе скриптов. Основной код желательно выносить во внешние файлы. Данные из PHP - вносить JSON'ом. Хороший вариант, когда на странице только вызов конструктора или Init'а c передачей данных в JSON'е.
    • Внимательно используйте setTimeout. Достаточно часто таймауты используются необоснованно. Например, если не работает код в браузере, то часто прибегают к установке таймаута "на глаз". Это происходит из-за недопонимая событийной модели.

      Бесконечных таймеров необходимо избегать вообще. Если все таки это нужно, то рассмотрите вариант использования core_timer.js. Это синглтон для таймеров.

    • Используйте "ленивую" инициализацию (загрузку). Создавайте объекты, верстку, окна и прочее только тогда, когда это действительно нужно. Например, нет смысла заранее создавать BX.PopupWindow (а он вставляет в DOM новые узлы) до того момента, когда это действительно нужно (нажатие на ссылку или кнопку). Если интерфейс используется редко и некритично показать/не показать процесс загрузки, то лучше сделать ajax'ую подзагрузку.
    • Глобальные переменные используйте минимально, локальные переменные используйте с var.
    • С осторожностью относитесь к глобальным обработчикам (window, document, document.body). Этот код вызывается на каждый клик, на каждое движение мыши, на каждое изменение скроллинга. И не забывайте делать unbind.
    • Делайте один appendChild вместо нескольких в цикле. Еще посмотрите что такое DocumentFragment.
    • Объединяйте и разделяйте CSS/JS. Здесь нужно руководствоваться здравым смыслом. Есть компоненты, которые содержат множество мелких скриптов и CSS-файлов. Имеет смысл их объединить в один файл.
      Есть и обратные примеры. Например, в создаваемом вами модуле есть большой CSS-файл, который содержит почти все стили для своего отображения. И это лучше, чем множество мелких (отдельно на таблицу, тулбар, попапы и фильтры). Но вот появилась новая задача - добавить в живую ленту небольшой блок из вашего модуля. В этом случае не нужно тащить в живую ленту весь файл CSS из модуля, лучше сделать небольшой отдельный файл.
    • Создайте защиту от двойного подключения на странице, так как компонент может быть подключен на странице несколько раз.
    • Для замера производительности запускайте профилировщики Chrome и Internet Explorer'а.
    • Проверяйте свой код в PhpStorm'е или аналогичных инструментах.

    Расширения (extensions)

    Расширение (экстеншн, extension) — способ организации JS и CSS кода в продуктах 1С-Битрикс: Управление Сайтом и Битрикс24.

    Расположение расширений

    В продукте:

    bitrix/js/<module>/<extension>
    bitrix/modules/js/<module>/<extension>
    local/js/<module>/<extension>
    

    Примечание: в директории bitrix расположены только те экстеншны, которые поставляются с продуктами 1С-Битрикс: Управление Сайтом и Битрикс24. Клиентские экстеншны должны располагаться в папке local.

    Структура

    • myextension
      • src — исходные файлы
      • dist — [dw]бандлы[/dw][di] Бандл (bundle, комплект/набор) - это совокупность каких-либо программных данных (файлов), объеденных по какому-либо признаку. В данном случае бандлы — это обработанные и объединенные исходные файлы. [/di] для браузера
      • bundle.config.js — конфигурационный файл для сборщика
      • config.php — конфигурационный файл экстеншна
      • lang — локализации
      • test — тесты
      • @types — файлы *.d.ts

    Обязательные элементы: src, dist, bundle.config.js, config.php.

    Необязательные элементы (директории): lang, test, @types.

    Если у вас установлен @bitrix/cli, то структуру расширения можно [ds]создать[/ds][di] Для создания расширения ("экстеншна"):

    1) Перейдите в директорию local/js/{module}

    2) Выполните команду bitrix create

    3) Ответьте на вопросы мастера

    Подробнее...[/di], выполнив команду bitrix create.

      src

      В директории src следует располагать исходные файлы в формате ES6. Из файлов в данной директории на основании данных файла конфигурации и внутренних ссылок будут созданы финальные версии для подключения в браузере (бандлы) в формате ES5.

      Внутри файла вы можете использовать import других файлов из текущей директории или импортировать другие CoreJS расширения.

      Для импорта переменных и классов из другого файла в текущей директории используйте следующий синтаксис:

      • если в папке src есть файл file.js и в нем есть экспортируемый класс SomeClass:

        import {SomeClass} from "./file";
        

      • для импорта file.css:

        import './file.css';
        

      • для импорта CoreJS 2.0:

        import {Loader} from 'main.loader';
        

      • для импорта библиотеки из CoreJS 1.0:

        import "main.date";
        

      dist

      В директории dist располагаются файлы, автоматически созданные с помощью сборщика для последующего подключения в браузере. Обычно это файлы <extension>.bundle.js и <extension>.bundle.css.

      bundle.config.js

      Файл конфигурации сборщика.

      • Базовая конфигурация:

        module.exports = {
        	input: './src/app.js', 
        	output: './dist/app.bundle.js',
        };
        

        Обратите внимание: в данном файле конфигурации не указываются CSS файлы, их необходимо импортировать в файле input.

      • Все параметры:

        module.exports = {
        	// Файл, для которого необходимо выполнить сборку. 
        	// Обычно это './src/<extension>.js
        	// Необходимо указать относительный путь 
        	input: string, 
        	
        	// Путь к бандлу, который будет создан в результате сборки 
        	// Обычно это ./dist/<extension>.bundle.js
        	// Необходимо указать относительный путь 
        	output: string,
        	
        	// Неймспейс, в который будут добавлены все экспорты из файла указанного в input
        	// Например 'BX.Main.Filter'
        	namespace: string,
        	
        	// Списки файлов для принудительного объединения. 
        	// Файлы будут объединены без проверок на дублирование кода. 
        	// sourcemap's объединяются автоматически 
        	// Необходимо указать относительные пути
        	concat: {
        		js: Array<string>,
        		css: Array<string>,
        	},
        	
        	// Разрешает или запрещает сборщику модифицировать config.php
        	// По умолчанию true (разрешено)
        	adjustConfigPhp: boolean,
        	
        	// Разрешает или запрещает сборщику удалять неиспользуемый код. 
        	// По умолчанию true (включено).
        	treeshake: boolean,
        	
        	// Разрешает или запрещает пересобирать бандлы 
        	// если сборка запущена не в корне текущего экстеншна 
        	// По умолчанию `false` (разрешено)
        	'protected': boolean,
        	
        	plugins: {
        		// Переопределяет параметры Babel.
        		// Можно указать собственные параметры Babel
        		// https://babeljs.io/docs/en/options
        		// Если указать false, то код будет собран без транспиляции
        		babel: boolean | Object,
        		
        		// Дополнительные плагины Rollup, 
        		// которые будут выполняться при сборке бандлов 
        		custom: Array<string | Function>,
        	},
        };
        

      config.php

      Конфигурационный файл расширения определяет, какие файлы необходимо подключить на страницу.

      При использовании @bitrix/cli файл config.php будет создан автоматически при сборке и будет обновляться автоматически по мере необходимости. Например, если в JS коде появится зависимость которая не указана в config.php, то она будет автоматически добавлена в rel.

      • Базовая конфигурация:

        if (!defined("B_PROLOG_INCLUDED") || B_PROLOG_INCLUDED !== true)
        {
        	die();
        }
        
        return [
        	'css' => './dist/loader.bundle.css',
        	'js' => './dist/loader.bundle.js',
        	'rel' => [
        		'main.core'
        	]
        ];
        

      • Все параметры:

        ...
        
        return [
        	// Путь к `css` файлу или массив путей 
        	// Рекомендуется указывать относительный путь 
        		'css' => String | Array<String>,
        	
        	// Путь к `js` файлу или массив путей к `js` файлам 
        	// Рекомендуется указывать относительный путь 
        	'js' => String | Array<String>,
        	
        	// Список зависимостей
        	// Необходимо указать имена расширений, которые должны быть 
        	// подключены перед подключением текущего расширения
        	// Зависимости подключаются рекурсивно и с учетом указанного порядка 
        	'rel' => String | Array<String>,
        	
        	// Путь к файлу с языковыми фразами или массив путей 
        	// файл `lang//config.php` подключается автоматически, 
        	// его тут можно не указывать
        	'lang' => String | Array<String>,
        	
        	// Запрещает подключать `main.core` автоматически, как зависимость
        	// По умолчанию `false` — `main.core` подключается. 
        	// При сборке бандла значение параметра устанавливается автоматически
        	// если в коде нет прямой зависимости на `main.core`
        	'skip_core' => Boolean,
        	
        	// Обработчик, который вызывается перед подключением расширения на страницу
        	// в качестве первого параметра будет передан массив конфигурации расширения
        	// обработчик может модифицировать этот массив и вернуть из функции
        	// Это может быть полезно когда необходимо добавить в языковые фразы 
        	// какие-то данные с сервера
        	'oninit' => Function,
        	
        	// Дополнительные языковые фразы
        	// Это может быть полезно для передачи вычисляемых значений языковых фраз
        	// Принимает массив. В качестве ключей необходимо указывать идентификаторы языковых фраз
        	'lang_additional' => Array<string, string>,
        
        	// Параметр доступен с версии 20.5.100 модуля main.
        	// Параметр позволяет указать настройки,
        	// которые могут быть получены в JS,
        	// с помощью метода Extension.getSettings().
        	'settings' => Array
        ];
        

      @types

      Директория может содержать файлы <name>.d.ts с описанием публичного JS API расширения, на TypeScript. Рекомендуется использовать *.d.ts файлы для описания API библиотек, написанных на ES5. Описывать ES6 код не нужно.

      Пример описания расширения main.loader:

      declare module 'main.loader' 
      {
      	type loaderOptions = {
      		target?: HTMLElement,
      		size?: number,
      		mode?: 'absolute' | 'inline' | 'custom',
      		offset?: {
      			top?: string,
      			left?: string
      		},
      		color?: string
      	};
          
      	class Loader 
      	{
      		constructor(options?: loaderOptions);
      
      		readonly layout: HTMLElement;
      		readonly circle: HTMLElement;
      
      		createLayout(): HTMLElement;
      		show(target?: HTMLElement): Promise<any>;
      		hide(): Promise<any>;
      		isShown(): boolean;
      		setOptions(options: loaderOptions): void;
      		destroy(): void;
      	}
      }
      

      test

      Директория должна содержать вложенные директории и файлы [ds]Mocha[/ds][di] Mocha (Мока) — JavaScript тест-фреймворк, который можно запускать как на node.js, так и в браузере, удобен для асинхронного тестирования. Тесты Mocha запускаются серийно, позволяя гибко и точно создавать отчеты.

      Подробнее...[/di]-тестов. Для каждого файла должна создаваться директория с именем тестируемого файла и вложенным в нее файлом с тестами в формате <sourceName>.test.js.

      Например, при такой структуре в `src`

      • src
        • entity
          • column.js
          • row.js
        • app.js

      директория test должна иметь следующую структуру:

      • test
        • entity
          • column
            • column.test.js
          • row
            • row.test.js
        • app
          • app.test.js

    Использование расширений

    • В PHP

      \Bitrix\Main\UI\Extension::load('main.loader');
      

      Метод \Bitrix\Main\UI\Extension::load принимает в качестве параметра имя расширения, либо массив имен.

    • В JS
    1. Импорт экспортов расширения:

      import {Loader} from 'main.loader';
      

      Если вы хотите импортировать старое расширение (не поддерживающие import ES6, например, main.date), укажите импорт без экспорта расширения:

      import "main.date";
      

      Когда вы импортируете расширение в JS, сборщик автоматически добавляет это расширение как зависимость в config.php.

    2. Отложенное подключение:

      import {loadExtension} from 'main.core';
      
      loadExtension('main.loader').then(() => {
      	// Код который использует `main.loader`
      });
      

      Отложенная загрузка может быть полезна когда ваш функционал используется на странице не сразу, а например только тогда, когда пользователь откроет попап или выполнит какое-нибудь действие.



    Инструмент @bitrix/cli

    Описание

    @bitrix/cli — консольный инструмент Битрикс-разработчика. Основная цель — упростить и автоматизировать разработку фронтенда для проектов на 1С-Битрикс: Управление Сайтом и Битрикс24.

    @bitrix/cli - это набор консольных команд:

    1. bitrix build для сборки и [dw]транспиляции[/dw][di] Транспиляция — это перевод исходного кода с одного языка на другой. [/di] ES6+ кода в кросс-браузерный ES5;
    2. bitrix test для запуска [ds]Mocha[/ds][di] Mocha (Мока) — JavaScript тест-фреймворк, который можно запускать как на node.js, так и в браузере, удобен для асинхронного тестирования. Тесты Mocha запускаются серийно, позволяя гибко и точно создавать отчеты.

      Подробнее...[/di]-тестов;
    3. bitrix create для быстрого создания [ds]расширения[/ds][di] Расширение (экстеншн, extension) — способ организации JS и CSS кода в продукте.

      Подробнее...[/di] («экстеншна»).

    Примечание: В первую очередь @bitrix/cli предназначен для работы расширениями («экстеншнами»), шаблонами сайта и шаблонами компонентов.

    Установка

    • [ds]NPM:[/ds][di] npm — менеджер пакетов, входящий в состав Node.js.

      Подробнее...[/di]

      $ npm install -g @bitrix/cli
      

    • [ds]YARN:[/ds][di] Yarn — это новый менеджер пакетов, созданный совместно Facebook*, Google, Exponent и Tilde.

      * Социальная сеть признана экстремистской и запрещена на территории Российской Федерации.

      Подробнее...[/di]

      $ yarn global add @bitrix/cli
      

    Конфигурация

    • Базовая конфигурация:

      module.exports = {
      	input: './app.js', 
      	output: './dist/app.bundle.js',
      };
      

    • Все параметры:

      module.exports = {
      	// Файл, для которого необходимо выполнить сборку. 
      	// Необходимо указать относительный путь 
      	input: string, 
      	
      	// Путь к бандлу, который будет создан в результате сборки. 
      	// Обычно это ./dist/<extension_name>.bundle.js
      	// Необходимо указать относительный путь 
      	output: string || {js: string, css: string},
      	
      	// Неймспейс, в который будут добавлены все экспорты из файла, 
      	// указанного в input. Например, 'BX.Main.Filter'
      	namespace: string,
      	
      	// Списки файлов для принудительного объединения. 
      	// Файлы будут объединены без проверок на дублирование кода. 
      	// sourcemap's объединяются автоматически 
      	// Необходимо указать относительные пути
      	concat: {
      		js: Array<string>,
      		css: Array<string>,
      	},
      	
      	// Разрешает или запрещает сборщику модифицировать config.php
      	// По умолчанию true (разрешено)
      	adjustConfigPhp: boolean,
      	
      	// Разрешает или запрещает сборщику удалять неиспользуемый код. 
      	// По умолчанию true (включено)
      	treeshake: boolean,
      	
      	// Разрешает или запрещает пересобирать бандлы, 
      	// если сборка запущена не в корне текущего расширения 
      	// По умолчанию `false` (разрешено)
      	'protected': boolean,
      	
      	plugins: {
      		// Переопределяет параметры Babel.
      		// Можно указать собственные параметры Babel
      		// https://babeljs.io/docs/en/options
      		// Если указать false, то код будет собран без транспиляции
      		babel: boolean | Object,
      		
      		// Дополнительные плагины Rollup, 
      		// которые будут выполняться при сборке бандлов 
      		custom: Array<string | Function>,
      	},
          // Определяет правила обработки путей к изображениям в CSS
          // Доступно с версии 3.0.0
          cssImages: {
              // Определяет правило, по которому изображения должны 
              // быть обработаны:
              // 'inline' — преобразует изображения в инлайн 
              // 'copy' — копирует изображения в директорию 'output'
              // По умолчанию 'inline'
              type: 'inline' | 'copy', 
      
              // Путь к директории, в которую должны быть скопированы 
              // используемые изображения 
              output: string,
      
              // Максимальный размер изображений в кб, которые могут быть 
              // преобразованы в инлайн.
              // По умолчанию 14кб
              maxSize: number,
      
              // Использовать ли svgo для оптимизации svg 
              // По умолчанию true 
              svgo: boolean, 
          },
          resolveFilesImport: {
              // Путь к директории, в которую должны быть скопированы 
              // импортированные изображения 
              output: string,
              
              // Определяет разрешенные для импорта типы файлов.
              // По умолчанию ['**/*.svg', '**/*.png', '**/*.jpg', '**/*.gif']
              // https://github.com/isaacs/minimatch
              include: Array<string>,
      
              // По умолчанию []
              exclude: Array<string>,
          },
          
          // Определяет правила Browserslist: 
          // false — не использовать (по умолчанию)
          // true — использовать файл .browserslist / .browserslistrc
          browserslist: boolean | string | Array<string>,
        
          // Включает или отключает минификацию. 
          // По умолчанию отключено. 
          // Может принимать объект настроек Terser:
          // false — не минифицировать (по умолчанию)
          // true — минифицировать с настройками по умолчанию 
          // object — минифицировать с указанными настройками 
          minification: boolean | object,
        
          // Включает или отключает преобразование нативных JS классов. 
          // По умолчанию значение параметра выставляется автоматически 
          // на основании browserslist
          transformClasses: boolean,
        
          // Включает или отключает создание Source Maps файлов 
          sourceMaps: boolean,
          
          // Настройки тестов 
          tests: {
              // Настройки локализации 
              localization: {
                  // Код языка локализации. По умолчанию 'en'
                  languageId: string,
                  // Включает или выключает автозагрузку фраз в тестах. 
                  // По умолчанию включено 
                  autoLoad: boolean,
              },
          },
      };
      

    Подробнее о структуре расширений читайте в [ds]соответствующем уроке.[/ds][di] Расширение (экстеншн, extension) — способ организации JS и CSS кода в продуктах 1С-Битрикс: Управление Сайтом и Битрикс24.

    Подробнее...[/di]

    Сборка

    Для запуска сборки выполните команду:

    $ bitrix build
    

    Сборщик рекурсивно найдет все файлы bundle.config.js и выполнит для каждого конфига сборку и транспиляцию.

    Дополнительные параметры:

    • --watch [<fileExtension>[, ...]], -w=[<fileExtension>[, ...]]

      Режим отслеживания изменений. Пересобирает [dw]бандлы[/dw][di] Бандл (bundle, комплект/набор) - это совокупность каких-либо программных данных (файлов), объеденных по какому-либо признаку. В данном случае бандлы — это обработанные и объединенные исходные файлы. [/di] после изменения исходных файлов. В качестве значения можно указать список расширений файлов, в которых нужно отслеживать изменения.

      $ bitrix build --watch
      

      Сокращённый вариант:

      $ bitrix build -w
      

      Вариант с отслеживанием изменений в указанных типах файлов:

      $ bitrix build -w=defaults,json,mjs,svg
      

      Примечание: defaults – набор расширений файлов, которые отслеживаются по умолчанию. Он равен js,jsx,vue,css,scss.

    • --test, -t

      Режим непрерывного тестирования. Тесты запускаются после каждой сборки. Обратите внимание, сборка с параметром --test выводит в отчёте только статус прохождения тестов — прошли или не прошли, полный отчет выводит только команда bitrix test.

      $ bitrix build --test
      

    • --modules <moduleName>[, ...], -m=<moduleName>[, ...]

      Сборка только указанных модулей. Параметр поддерживается только в корневой директории c модулями local/js и bitrix/modules. В значении укажите имена модулей через запятую, например:

      $ bitrix build --modules main,ui,landing
      

    • --path <path>, -p=<path>

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

      $ bitrix build --path ./main/install/js/main/loader
      

      Сокращённый вариант:

      $ bitrix build -p=./main/install/js/main/loader
      

    • --extensions <extensionName>[, ...], -e=<extensionName>[, ...]

      Запускает сборку указанных экстеншнов. В качестве значения нужно указать имя экстеншна либо список имен через запятую. Команду можно запускать из любой директории проекта.

      $ bitrix build -e=main.core,ui.buttons,landing.main
      

    Запуск тестов

    Команда запускает Mocha-тесты и выводит подробный отчет о прохождении тестов.

    $ bitrix test
    

    Примечание: Тестами считаются JS файлы, расположенные в директории ./test относительно файла bundle.config.js. В момент запуска тестов исходный код и код тестов на лету обрабатывается сборщиком и после чего выполняется. Поэтому тесты можно писать на ES6+.

    Дополнительные параметры:

    • --watch [<fileExtension>[, ...]], -w=[<fileExtension>[, ...]]

      Режим отслеживания изменений. Запускает тесты после изменения исходных файлов и кода тестов. В качестве значения можно указать список расширений файлов, в которых нужно отслеживать изменения.

      $ bitrix test --watch
      

      Сокращённый вариант:

      $ bitrix test -w
      

      Вариант с отслеживанием изменений в указанных типах файлов:

      $ bitrix test -w=defaults,json,mjs,svg
      

      Примечание: defaults – набор расширений файлов, которые отслеживаются по умолчанию. Он равен js,jsx,vue,css,scss.

    • --modules <moduleName>[, ...], -m=<moduleName>[, ...]

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

      $ bitrix test --modules main,ui,landing
      

    • --path <path>, -p=<path>

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

      $ bitrix test --path ./main/install/js/main/loader.
      

      Сокращённый вариант:

      $ bitrix test -p=./main/install/js/main/loader
      

    • --extensions <extensionName>[, ...], -e=<extensionName>[, ...]

      Запускает тесты в указанных экстеншнах. В качестве значения нужно указать имя экстеншна либо список имен через запятую. Команду можно запускать из любой директории проекта.

      $ bitrix test -e=main.core,ui.buttons,landing.main
      

    Создание расширения

    Для создания расширения ("экстеншна"):

    • Перейдите в директорию local/js/[dw]{module}[/dw][di]{module} – должно быть без точек в названии, например, партнерский префикс. Подробнее...[/di]
    • Выполните команду bitrix create. После выполнения команды создаётся папка расширения, например, /ext, и подключается так: \Bitrix\Main\UI\Extension::load("partner.ext"); для пути local/js/partner/ext/.
    • Ответьте на вопросы мастера

    Примечание: Можно не переходить в /{module}. Останьтесь в local/js/, выполните в bitrix create расширение myext и загрузите его \Bitrix\Main\UI\Extension::load("myext");.

      Видео от разработчика



    @bitrix/cli: сборка проекта с NPM

    В предыдущем уроке мы познакомились с консольным инструментом [ds]@bitrix/cli[/ds][di] @bitrix/cli — консольный инструмент Битрикс-разработчика. Основная цель — упростить и автоматизировать разработку фронтенда для проектов на 1С-Битрикс: Управление Сайтом и Битрикс24.
    Требования для его нормальной работы, версии:
    Node: 9.11.2
    NPM: 5.6.0.

    Подробнее...[/di]. В этом уроке рассмотрим, как осуществить сборку проектов с [dw]NPM[/dw][di] NPM (Node Package Manager, для его аббревиатуры на английском языке) является менеджером пакетов для JavaScript, по умолчанию для node.js. Он используется для скачивания пакетов из облачного сервера npm, либо для загрузки пакетов на эти сервера. [/di].

    Сборка проекта с NPM

    1. Создайте package.json.

      В нем объявляются внешние зависимости, необходимые для работы приложения. Например, если приложение использует React, Lodash или какую-то другую библиотеку, то эти библиотеки должны быть описаны в package.json с указанием версий и описанием вашего проекта.

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

      Быстро создать package.json можно с помощью команды npm init. Она запустит мастер сборки, и вам останется только ответить на вопросы. Команду необходимо выполнить в директории [ds]экстеншна.[/ds][di] Расширение (экстеншн, extension) — способ организации JS и CSS кода в продукте.

      Подробнее...[/di]

    2. В файле bundle.config.js укажите параметр plugins.resolve = true;

      Этот параметр сообщит сборщику, что все импорты NPM пакетов необходимо разрешить ("разрезолвить") и добавить в бандл вашего приложения.

      Пример bundle.config.js, уже настроенного на работу с NPM пакетами:

      module.exports = {
      	input: 'src/app.js',
      	output: 'dist/app.bundle.js',
      	plugins: {
      		resolve: true,
      	},
      };
      



    Вложенные библиотеки


    Библиотеки могут содержать вспомогательные библиотеки, которые могут быть подключены отдельно.

    Полный путь в файловой системе:

    `/bitrix/modules/<module_name>/install/js/<module_name>/<library_name>/<sub_library_name>/`
    

    В таком формате вызов вашей библиотеки будет таким:

    \Bitrix\Main\UI\Extension::load('<module_name>.<library_name>.<sub_library_name>');
    

    Количество вложенных папок не ограничено. Каждая новая папка должно быть указана в названии расширения через точку:

    <module_name>.<library_name>.<sub_library_name>.<sub_library_name_2>.<sub_library_name_3>
    

    В данном случае путь будет таким:

    /bitrix/modules/<module_name>/install/js/<module_name>/<library_name>/<sub_library_name>/<sub_library_name_2>/<sub_library_name_3>/
    


    Использование ES6

    Использование ES6

    С 2019 г. все новые расширения Bitrix CoreJS пишутся на [ds]ES6.[/ds][di] ECMAScript — это встраиваемый расширяемый не имеющий средств ввода-вывода язык программирования, используемый в качестве основы для построения других скриптовых языков.

    Подробнее...[/di]

    Использование в старых браузерах

    Количество браузеров, поддерживающих современные JavaScript, зависит от регионов и составляет от 90 до 96%. В остальных браузерах необходимо использовать [dw]полифилы[/dw][di] Полифилы - это файлы, содержащие недостающие функции. [/di] и [dw]транспиляцию кода.[/dw][di] Транспиляция кода - это перевод кода из ES6 в ES5. [/di]

    • Полифилы

      В расширениях, использующих код ES6, необходимо подключить зависимость на библиотеку main.polyfill.complex.

    • Транспиляция

      Для корректной работы кода в старых браузерах необходимо перевести его из нового формата в старый, воспользовавшись специальной программой - [ds]транспилятором[/ds][di]@bitrix/cli — консольный инструмент Битрикс-разработчика. Основная цель — упростить и автоматизировать разработку фронтенда для проектов на 1С-Битрикс: Управление Сайтом и Битрикс24.

      Подробнее ...[/di].

      Принцип работы транспилятора заключается в следующем: вы пишете код на ES6, а транспилятор в автоматическом режиме создает отдельный файл в старом формате:



    Работа с магазином

    Цитатник веб-разработчиков.

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

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

    Список ссылок по теме:

    Товары и CIBlockElement::GetList

    CIBlockElement::GetList

    Как вы знаете, метод CIBlockElement::GetList модуля Информационные блоки может работать с данными товара (при наличии модуля Торговый каталог). Это подробно описано в документации и активно используется как в публичных компонентах, так и на административных страницах и скриптах. Однако архитектурные особенности реализации, равно как и неправильное использование этих возможностей, приводят к резкому падению производительности.

    Давайте сделаем разные вызовы CIBlockElement::GetList и посмотрим, какие запросы будут выполнены в итоге.

    1. Сначала сделаем простую выборку товаров из инфоблока с ID = 2:
      $iterator = \CIBlockElement::GetList(
      	array(),
      	array('IBLOCK_ID' => 2),
      	false,
      	false,
      	array('ID', 'NAME', 'IBLOCK_ID')
      );
      

      Запрос, который пойдет в базу:

      SELECT  BE.ID as ID,BE.NAME as NAME,BE.IBLOCK_ID as IBLOCK_ID
      FROM b_iblock B
      INNER JOIN b_lang L ON B.LID=L.LID
      INNER JOIN b_iblock_element BE ON BE.IBLOCK_ID = B.ID
      WHERE 1=1 
      	AND (
      		((((BE.IBLOCK_ID = '2'))))
      	)
      	AND (((BE.WF_STATUS_ID=1 AND BE.WF_PARENT_ELEMENT_ID IS NULL)))
      
    2. Теперь в выборку добавим фильтрацию по доступности товара:
      $iterator = \CIBlockElement::GetList(
      	array(),
      	array('IBLOCK_ID' => 2, 'CATALOG_AVAILABLE' => 'Y'),
      	false,
      	false,
      	array('ID', 'NAME', 'IBLOCK_ID')
      );
      

      Это приводит к запросу:

      SELECT  BE.ID as ID,BE.NAME as NAME,BE.IBLOCK_ID as IBLOCK_ID ,CAT_PR.QUANTITY as CATALOG_QUANTITY,
      IF (CAT_PR.QUANTITY_TRACE = 'D', 'Y', CAT_PR.QUANTITY_TRACE) as CATALOG_QUANTITY_TRACE,
      CAT_PR.QUANTITY_TRACE as CATALOG_QUANTITY_TRACE_ORIG, CAT_PR.WEIGHT as CATALOG_WEIGHT,
      CAT_PR.VAT_ID as CATALOG_VAT_ID, CAT_PR.VAT_INCLUDED as CATALOG_VAT_INCLUDED,
      IF (CAT_PR.CAN_BUY_ZERO = 'D', 'N', CAT_PR.CAN_BUY_ZERO) as CATALOG_CAN_BUY_ZERO,
      CAT_PR.CAN_BUY_ZERO as CATALOG_CAN_BUY_ZERO_ORIG,
      CAT_PR.PURCHASING_PRICE as CATALOG_PURCHASING_PRICE, CAT_PR.PURCHASING_CURRENCY as CATALOG_PURCHASING_CURRENCY,
      CAT_PR.QUANTITY_RESERVED as CATALOG_QUANTITY_RESERVED,
      IF (CAT_PR.SUBSCRIBE = 'D', 'Y', CAT_PR.SUBSCRIBE) as CATALOG_SUBSCRIBE, CAT_PR.SUBSCRIBE as CATALOG_SUBSCRIBE_ORIG,
      CAT_PR.WIDTH as CATALOG_WIDTH, CAT_PR.LENGTH as CATALOG_LENGTH, CAT_PR.HEIGHT as CATALOG_HEIGHT,
      CAT_PR.MEASURE as CATALOG_MEASURE, CAT_PR.TYPE as CATALOG_TYPE, CAT_PR.AVAILABLE as CATALOG_AVAILABLE,
      CAT_PR.BUNDLE as CATALOG_BUNDLE, CAT_PR.PRICE_TYPE as CATALOG_PRICE_TYPE,
      CAT_PR.RECUR_SCHEME_LENGTH as CATALOG_RECUR_SCHEME_LENGTH, CAT_PR.RECUR_SCHEME_TYPE as CATALOG_RECUR_SCHEME_TYPE,
      CAT_PR.TRIAL_PRICE_ID as CATALOG_TRIAL_PRICE_ID, CAT_PR.WITHOUT_ORDER as CATALOG_WITHOUT_ORDER,
      CAT_PR.SELECT_BEST_PRICE as CATALOG_SELECT_BEST_PRICE,
      IF (CAT_PR.NEGATIVE_AMOUNT_TRACE = 'D', 'N', CAT_PR.NEGATIVE_AMOUNT_TRACE) as CATALOG_NEGATIVE_AMOUNT_TRACE,
      CAT_PR.NEGATIVE_AMOUNT_TRACE as CATALOG_NEGATIVE_AMOUNT_TRACE_ORIG, CAT_VAT.RATE as CATALOG_VAT
      
      	FROM b_iblock B
      	INNER JOIN b_lang L ON B.LID=L.LID
      	INNER JOIN b_iblock_element BE ON BE.IBLOCK_ID = B.ID
      
      	left join b_catalog_product as CAT_PR on (CAT_PR.ID = BE.ID)
      	left join b_catalog_iblock as CAT_IB on ((CAT_PR.VAT_ID IS NULL or CAT_PR.VAT_ID = 0) and CAT_IB.IBLOCK_ID = BE.IBLOCK_ID)
      	left join b_catalog_vat as CAT_VAT on
      		(CAT_VAT.ID = IF((CAT_PR.VAT_ID IS NULL OR CAT_PR.VAT_ID = 0), CAT_IB.VAT_ID, CAT_PR.VAT_ID))
      
      	WHERE 1=1 
      	AND (
      		((((BE.IBLOCK_ID = '2'))))
      		AND ((((CAT_PR.AVAILABLE='Y'))))
      	)
      	AND (((BE.WF_STATUS_ID=1 AND BE.WF_PARENT_ELEMENT_ID IS NULL)))
      

      Таким образом, любое обращение к данным товара (фильтрация, сортировка, выборка одного из полей) приводит к join трех таблиц и выборке всех полей товара.
    3. Уберем фильтрацию, но будем выбирать цену одного из типов (ID типа цены - 1):
      $iterator = \CIBlockElement::GetList(
      	array(),
      	array('IBLOCK_ID' => 2),
      	false,
      	false,
      	array('ID', 'NAME', 'IBLOCK_ID', 'CATALOG_CATALOG_GROUP_ID_1')
      );
      

      Видим, что стало еще хуже:

      SELECT BE.ID as ID,BE.NAME as NAME,BE.IBLOCK_ID as IBLOCK_ID,
      CAT_P1.CATALOG_GROUP_ID as CATALOG_GROUP_ID_1, CAT_P1.ID as CATALOG_PRICE_ID_1,
      CAT_P1.PRICE as CATALOG_PRICE_1, CAT_P1.CURRENCY as CATALOG_CURRENCY_1,
      CAT_P1.QUANTITY_FROM as CATALOG_QUANTITY_FROM_1, CAT_P1.QUANTITY_TO as CATALOG_QUANTITY_TO_1,
      CAT_P1.EXTRA_ID as CATALOG_EXTRA_ID_1,
      'Базовая цена' as CATALOG_GROUP_NAME_1, 'Y' as CATALOG_CAN_ACCESS_1, 'Y' as CATALOG_CAN_BUY_1,
      CAT_PR.QUANTITY as CATALOG_QUANTITY, IF (CAT_PR.QUANTITY_TRACE = 'D', 'Y', CAT_PR.QUANTITY_TRACE) as CATALOG_QUANTITY_TRACE,
      CAT_PR.QUANTITY_TRACE as CATALOG_QUANTITY_TRACE_ORIG, CAT_PR.WEIGHT as CATALOG_WEIGHT,
      CAT_PR.VAT_ID as CATALOG_VAT_ID, CAT_PR.VAT_INCLUDED as CATALOG_VAT_INCLUDED,
      IF (CAT_PR.CAN_BUY_ZERO = 'D', 'N', CAT_PR.CAN_BUY_ZERO) as CATALOG_CAN_BUY_ZERO,
      CAT_PR.CAN_BUY_ZERO as CATALOG_CAN_BUY_ZERO_ORIG, CAT_PR.PURCHASING_PRICE as CATALOG_PURCHASING_PRICE,
      CAT_PR.PURCHASING_CURRENCY as CATALOG_PURCHASING_CURRENCY, CAT_PR.QUANTITY_RESERVED as CATALOG_QUANTITY_RESERVED,
      IF (CAT_PR.SUBSCRIBE = 'D', 'Y', CAT_PR.SUBSCRIBE) as CATALOG_SUBSCRIBE, CAT_PR.SUBSCRIBE as CATALOG_SUBSCRIBE_ORIG,
      CAT_PR.WIDTH as CATALOG_WIDTH, CAT_PR.LENGTH as CATALOG_LENGTH, CAT_PR.HEIGHT as CATALOG_HEIGHT,
      CAT_PR.MEASURE as CATALOG_MEASURE, CAT_PR.TYPE as CATALOG_TYPE, CAT_PR.AVAILABLE as CATALOG_AVAILABLE,
      CAT_PR.BUNDLE as CATALOG_BUNDLE, CAT_PR.PRICE_TYPE as CATALOG_PRICE_TYPE,
      CAT_PR.RECUR_SCHEME_LENGTH as CATALOG_RECUR_SCHEME_LENGTH, CAT_PR.RECUR_SCHEME_TYPE as CATALOG_RECUR_SCHEME_TYPE,
      CAT_PR.TRIAL_PRICE_ID as CATALOG_TRIAL_PRICE_ID, CAT_PR.WITHOUT_ORDER as CATALOG_WITHOUT_ORDER,
      CAT_PR.SELECT_BEST_PRICE as CATALOG_SELECT_BEST_PRICE,
      IF (CAT_PR.NEGATIVE_AMOUNT_TRACE = 'D', 'N', CAT_PR.NEGATIVE_AMOUNT_TRACE) as CATALOG_NEGATIVE_AMOUNT_TRACE,
      CAT_PR.NEGATIVE_AMOUNT_TRACE as CATALOG_NEGATIVE_AMOUNT_TRACE_ORIG, CAT_VAT.RATE as CATALOG_VAT
      FR OM b_iblock B
      INNER JOIN b_lang L ON B.LID=L.LID
      INNER JOIN b_iblock_element BE ON BE.IBLOCK_ID = B.ID
      left join b_catalog_price as CAT_P1 on (CAT_P1.PRODUCT_ID = BE.ID and CAT_P1.CATALOG_GROUP_ID = 1)
      left join b_catalog_product as CAT_PR on (CAT_PR.ID = BE.ID)
      left join b_catalog_iblock as CAT_IB on ((CAT_PR.VAT_ID IS NULL or CAT_PR.VAT_ID = 0) and CAT_IB.IBLOCK_ID = BE.IBLOCK_ID)
      left join b_catalog_vat as CAT_VAT on (CAT_VAT.ID = IF((CAT_PR.VAT_ID IS NULL OR CAT_PR.VAT_ID = 0), CAT_IB.VAT_ID, CAT_PR.VAT_ID))
      WH ERE 1=1
      	AND (
      		((((BE.IBLOCK_ID = '2'))))
      	)
      	AND (((BE.WF_STATUS_ID=1 AND BE.WF_PARENT_ELEMENT_ID IS NULL)))
      

      Если мы попробуем выбрать еще и данные по складам, то ситуация будет аналогичной.

    Подведем промежуточные итоги:

    • Выборки цен, количества на складах в CIBlockElement::GetList необходимо избегать категорически (особенно при наличии сортировки и неважно по каким полям). Эти данные нужно получать отдельными вызовами АПИ. К слову сказать, в штатных компонентах это было сделано еще в версии 17.0.
    • Остается фильтрация и сортировка по полям товара, ценам, складам. Обращение к полям товара по ключам CATALOG_ дает дополнительный join трех таблиц. Обращение к N типам цен или складов - join N+3 таблиц. Мало того, что время запроса увеличивается, так еще можно получить ошибку MySql "Too many tables; MySQL can only use 61 tables in a join".

    До выпуска связки обновлений catalog 18.6.100 + iblock 18.6.200 все вышеописанное относится, в том числе, к штатным компонентам и административным спискам модуля Информационные блоки (особенно в режиме совместного просмотра элементов и разделов, смотрите примечание ниже). После выхода данных обновлений в CIBlockElement::GetList доступны новые возможности работы с товарами.

    Примечание: ранее для всех больших инфоблоков мы рекомендовали не использовать режим совместного просмотра по причине большого расхода памяти. С выходом версии 18.5.5 модуля Информационные блоки эта проблема решена - инфоблок со 100 тысячами элементов спокойно выводится в административном разделе в этом режиме (расход памяти снижен примерно в 20 раз).

      С версий catalog 18.6.100 + iblock 18.6.200

    С версии iblock 18.6.200 изменяются ключи метода. По всем ключам возможна фильтрация, сортировка, выборка.

    Поля товара


    С версии catalog 20.0.200 добавились поля товара

    Теперь вызов метода с фильтрацией по доступности выглядит так:

    $iterator = \CIBlockElement::GetList(
    	array(),
    	array('IBLOCK_ID' => 2, '=AVAILABLE' => 'Y'),
    	false,
    	false,
    	array('ID', 'NAME', 'IBLOCK_ID')
    );
    

    а в запросе только то, что просили, и join только один:

    SELECT  BE.ID as ID,BE.NAME as NAME,BE.IBLOCK_ID as IBLOCK_ID
    
    FROM b_iblock B
    INNER JOIN b_lang L ON B.LID=L.LID
    INNER JOIN b_iblock_element BE ON BE.IBLOCK_ID = B.ID
    left join b_catalog_product as PRD on (PRD.ID = BE.ID)
    
    	WHERE 1=1 
    AND (	
    	((((BE.IBLOCK_ID = '2'))))
    	AND ((((PRD.AVAILABLE='Y'))))
    )
    AND (((BE.WF_STATUS_ID=1 AND BE.WF_PARENT_ELEMENT_ID IS NULL)))
    

    Сделаем выборку размеров и веса доступных простых товаров:

    $iterator = \CIBlockElement::GetList(
    	array(),
    	array('IBLOCK_ID' => 2, '=AVAILABLE' => 'Y', '=TYPE' => 1),
    	false,
    	false,
    	array('ID', 'NAME', 'IBLOCK_ID', 'WEIGHT', 'WIDTH', 'HEIGHT', 'LENGTH')
    );
    

    Запрос получается следующим:

    SELECT  BE.ID as ID,BE.NAME as NAME,BE.IBLOCK_ID as IBLOCK_ID ,
    PRD.WEIGHT as WEIGHT, PRD.WIDTH as WIDTH, PRD.HEIGHT as HEIGHT, PRD.LENGTH as LENGTH
    
    FROM b_iblock B
    INNER JOIN b_lang L ON B.LID=L.LID
    INNER JOIN b_iblock_element BE ON BE.IBLOCK_ID = B.ID
    left join b_catalog_product as PRD on (PRD.ID = BE.ID)
    
    WHERE 1=1 
    AND (
    	((((BE.IBLOCK_ID = '2'))))
    	AND ((((PRD.AVAILABLE='Y'))))
    	AND ((((PRD.TYPE = '1'))))
    )
    AND (((BE.WF_STATUS_ID=1 AND BE.WF_PARENT_ELEMENT_ID IS NULL)))
    
    Поля цен (без привязки к конкретному типу цены)


    Поля цен (с указанием типа цены)


    Поля складов

      Примеры выборок

    Фильтрация по цене любого типа:

    $iterator = \CIBlockElement::GetList(
    	array(),
    	array('IBLOCK_ID' => 2, '=AVAILABLE' => 'Y', '=TYPE' => 1, '>PRICE' => 500),
    	false,
    	false,
    	array('ID', 'NAME', 'IBLOCK_ID', 'WEIGHT', 'WIDTH', 'HEIGHT', 'LENGTH')
    );
    

    Фильтрация для типа цены с кодом 1 (обычно это базовая цена):

    $iterator = \CIBlockElement::GetList(
    	array(),
    	array('IBLOCK_ID' => 2, '=AVAILABLE' => 'Y', '=TYPE' => 1, '>PRICE' => 500, '=PRICE_TYPE' => 1),
    	false,
    	false,
    	array('ID', 'NAME', 'IBLOCK_ID', 'WEIGHT', 'WIDTH', 'HEIGHT', 'LENGTH')
    );
    

    Или в таком варианте:

    $iterator = \CIBlockElement::GetList(
    	array(),
    	array('IBLOCK_ID' => 2, '=AVAILABLE' => 'Y', '=TYPE' => 1, '>PRICE_1' => 500),
    	false,
    	false,
    	array('ID', 'NAME', 'IBLOCK_ID', 'WEIGHT', 'WIDTH', 'HEIGHT', 'LENGTH')
    );
    

    Теперь можно делать фильтры, ранее недоступные. Выбрать все товары, имеющие цены любого типа от 500 до 1000:

    $iterator = \CIBlockElement::GetList(
    	array(),
    	array('IBLOCK_ID' => 2, '=AVAILABLE' => 'Y', '=TYPE' => 1, '>=PRICE' => 500, '<=PRICE' => 1000),
    	false,
    	false,
    	array('ID', 'NAME', 'IBLOCK_ID')
    );
    

    Или только цены типов с кодом 1,4,5:

    $iterator = \CIBlockElement::GetList(
    	array(),
    	array('IBLOCK_ID' => 2, '=AVAILABLE' => 'Y', '=TYPE' => 1, '>=PRICE' => 500, '<=PRICE' => 1000, '@PRICE_TYPE' => [1,4,5]),
    	false,
    	false,
    	array('ID', 'NAME', 'IBLOCK_ID')
    );
    

    Выбрать все товары с ценами в любой валюте, эквивалентными диапазону от 100 до 200 USD:

    $iterator = \CIBlockElement::GetList(
    	array(),
    	array('IBLOCK_ID' => 2, '=AVAILABLE' => 'Y', '=TYPE' => 1, '>=PRICE' => 100, '<=PRICE' => 200, 'CURRENCY_FOR_SCALE' => 'USD'),
    	false,
    	false,
    	array('ID', 'NAME', 'IBLOCK_ID')
    );
    

    Выбрать товары, которых на любом складе не больше 3:

    $iterator = \CIBlockElement::GetList(
    	array(),
    	array('IBLOCK_ID' => 2, '<=STORE_AMOUNT' => 3),
    	false,
    	false,
    	array('ID', 'NAME', 'IBLOCK_ID')
    );
    

    Выбрать товары, которых на 17-м складе от 5 до 7:

    $iterator = \CIBlockElement::GetList(
    	array(),
    	array('IBLOCK_ID' => 2, '<=STORE_AMOUNT' => 7, '>=STORE_AMOUNT' => 5, 'STORE_NUMBER' => 17),
    	false,
    	false,
    	array('ID', 'NAME', 'IBLOCK_ID')
    );
    

    Либо в таком виде:

    $iterator = \CIBlockElement::GetList(
    	array(),
    	array('IBLOCK_ID' => 2, '<=STORE_AMOUNT_17' => 7, '>=STORE_AMOUNT_17' => 5),
    	false,
    	false,
    	array('ID', 'NAME', 'IBLOCK_ID')
    );
    

    Фильтрация товаров по наличию на определенных складах:

    if(!empty($arParams['STORES'])){ 
    	$GLOBALS[$arParams['FILTER_NAME']]['@STORE_NUMBER'] => $arParams['STORES'];
    	$GLOBALS[$arParams['FILTER_NAME']]['>STORE_AMOUNT'] = 0;
    }

    Другой пример фильтрации товаров по наличию на определенных складах:

    if(!empty($arParams['STORES'])){
    
    	$storesFilter = [
    		'LOGIC'=>'OR'
    	];
    		foreach ($arParams['STORES'] as $store_id){
    			$storesFilter[] = ['STORE_NUMBER' => intval($store_id),'>STORE_AMOUNT'=>0];
    		}
    
    	$GLOBALS[$arParams['FILTER_NAME']][] = $storesFilter;
    }
    

    Заключение

    После установки обновлений catalog 18.6.100 + iblock 18.6.200 настоятельно рекомендуется перевести свои компоненты и скрипты на новые ключи. Увеличение производительности прямо пропорционально числу товаров в каталоге. Так, при тестах на разделе из 1,5 тысяч товаров прирост скорости выполнения составил порядка 30%. Штатные компоненты ([comp include_62980]catalog.section[/comp], [comp include_62981]catalog.element[/comp], [comp include_62986]catalog.top[/comp]), а также все компоненты наследники \Bitrix\Iblock\Component\Base переведены на новые фильтры.



    Пользовательские типы свойств заказа

    Свои типы свойств

    В системе имеются следующие стандартные типы свойств: Строка, Число, Да/Нет, Перечисление, Файл, Дата и Местоположение. Но вы можете добавлять свои типы свойств и самостоятельно определять их внешний вид. Таким образом, у покупателя при оформлении заказа будет спрашиваться какое-то значение, которое вы сами запрограммируете. Для этого вам необходимо выполнить следующие действия:

    • Унаследовать класс пользовательского типа:
      class MyType extends \Bitrix\Sale\Internals\Input\Base
      {
      	protected static function getEditHtmlSingle($name, array $input, $value){...} 
      	protected static function getErrorSingle(array $input, $value){...}
      	static function getSettings(array $input, $reload){...}
      }
      
    • Подключить тип свойства к системе - тип подключается на событии registerInputTypes:
      \Bitrix\Main\EventManager::getInstance()->addEventHandler(
      	'sale',
      	'registerInputTypes',
      	'myFunction'
      );
      
    • В обработчике события зарегистрировать свой тип свойства с помощью метода Manager::register, где указывается ваш класс-обработчик и имя вашего типа:
      public function myFunction(\Bitrix\Main\Event $event)
      {
      	\Bitrix\Sale\Internals\Input\Manager::register(
      		"myType",
      		array(
      			'CLASS' => '\MyNamespace\MyType',
      			'NAME' => 'Мой тип',
      		)	
      	);
      }
      
    • [ds]Описать JS-класс[/ds][di]Иногда при разработке компонента его шаблон необходимо наделить js-функциональностью, событиями и прочим.

      Подробнее ...[/di] для работы со свойством и [ds]подключить[/ds][di]Перед написанием JS-кода встает резонный вопрос - где его хранить?

      Подробнее ...[/di] его:
      BX.Sale.Input.Manager.MyType = MyType;
      BX.Sale.Input.Utils.extend(MyType, BX.Sale.Input.BaseInput);
      BX.Sale.Input.Manager.register('myType', MyType);
      
      function MyType(name, settings, value, publicO)
      {
      	MyType.__super__.constructor.call(this, name, settings, value, publicO);
      }
      
      MyType.prototype.createEditorSingle = function (name, value)
      {
      	...
      };
      
      MyType.prototype.afterEditorSingleInsert = function (item)
      {
      	...
      };
      
      MyType.prototype.setValueSingle = function (item, value)
      {
      	...
      };
      
      MyType.prototype.getValueSingle = function (item)
      {
      	...
      };
      
      MyType.prototype.setDisabledSingle = function (item, disabled)
      {
      	...
      };
      
      MyType.prototype.addEventSingle = function (item, name, action)
      {
      	...
      };
      

    Важно! Название типа myType должно быть уникальным в рамках всей системы.

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

    Обратите внимание! Поддержку созданного пользовательского свойства в компоненте оформления заказа [comp include_146775]sale.order.ajax[/comp] необходимо делать самостоятельно.

    Пример создания типа свойств String

    Наследуем класс, подключаем тип свойства к системе и регистрируем его:

    class StringInput extends \Bitrix\Sale\Internals\Input\Base 
    {
    	public static function getEditHtmlSingle($name, array $input, $value)
    	{
    		if ($input['MULTILINE'] == 'Y')
    		{
    			$attributes = static::extractAttributes($input,
    				array('DISABLED'=>'', 'READONLY'=>'', 'AUTOFOCUS'=>'', 'REQUIRED'=>''),
    				array('FORM'=>1, 'MAXLENGTH'=>1, 'PLACEHOLDER'=>1, 'DIRNAME'=>1, 'ROWS'=>1, 'COLS'=>1, 'WRAP'=>1));
    
    			return '<textarea name="'.$name.'"'.$attributes.'>'.htmlspecialcharsbx($value).'</textarea>';
    		}
    		else
    		{
    			$attributes = static::extractAttributes($input,
    				array('DISABLED'=>'', 'READONLY'=>'', 'AUTOFOCUS'=>'', 'REQUIRED'=>'', 'AUTOCOMPLETE'=>'on'),
    				array('FORM'=>1, 'MAXLENGTH'=>1, 'PLACEHOLDER'=>1, 'DIRNAME'=>1, 'SIZE'=>1, 'LIST'=>1, 'PATTERN'=>1));
    
    			return '<input type="text" name="'.$name.'" value="'.htmlspecialcharsbx($value).'"'.$attributes.'>';
    		}
    	}
    
    	/**
    	 * @param $name
    	 * @param array $input
    	 * @param $value
    	 * @return string
    	 */
    	public static function getFilterEditHtml($name, array $input, $value)
    	{
    		return static::getEditHtmlSingle($name, $input, $value);
    	}
    
    	public static function getErrorSingle(array $input, $value)
    	{
    		$errors = array();
    
    		$value = trim($value);
    
    		if ($input['MINLENGTH'] && strlen($value) < $input['MINLENGTH'])
    			$errors['MINLENGTH'] = Loc::getMessage('INPUT_STRING_MINLENGTH_ERROR', array("#NUM#" => $input['MINLENGTH']));
    
    		if ($input['MAXLENGTH'] && strlen($value) > $input['MAXLENGTH'])
    			$errors['MAXLENGTH'] = Loc::getMessage('INPUT_STRING_MAXLENGTH_ERROR', array("#NUM#" => $input['MAXLENGTH']));
    
    		if ($input['PATTERN'] && !preg_match($input['PATTERN'], $value))
    			$errors['PATTERN'] = Loc::getMessage('INPUT_STRING_PATTERN_ERROR');
    
    		return $errors;
    	}
    
    	static function getSettings(array $input, $reload)
    	{
    		$settings = array(
    			'MINLENGTH' => array('TYPE' => 'NUMBER', 'LABEL' => Loc::getMessage('INPUT_STRING_MINLENGTH'), 'MIN' => 0, 'STEP' => 1),
    			'MAXLENGTH' => array('TYPE' => 'NUMBER', 'LABEL' => Loc::getMessage('INPUT_STRING_MAXLENGTH'), 'MIN' => 0, 'STEP' => 1),
    			'PATTERN'   => array('TYPE' => 'STRING', 'LABEL' => Loc::getMessage('INPUT_STRING_PATTERN'  )),
    			'MULTILINE' => array('TYPE' => 'Y/N'   , 'LABEL' => Loc::getMessage('INPUT_STRING_MULTILINE'), 'ONCLICK' => $reload),
    		);
    
    		if ($input['MULTILINE'] == 'Y')
    		{
    			$settings['COLS'] = array('TYPE' => 'NUMBER', 'LABEL' => Loc::getMessage('INPUT_STRING_SIZE'), 'MIN' => 0, 'STEP' => 1);
    			$settings['ROWS'] = array('TYPE' => 'NUMBER', 'LABEL' => Loc::getMessage('INPUT_STRING_ROWS'), 'MIN' => 0, 'STEP' => 1);
    		}
    		else
    		{
    			$settings['SIZE'] = array('TYPE' => 'NUMBER', 'LABEL' => Loc::getMessage('INPUT_STRING_SIZE'), 'MIN' => 0, 'STEP' => 1);
    		}
    
    		return $settings;
    	}
    }
    
    \Bitrix\Sale\Internals\Input\Manager::register('STRING', array(
    	'CLASS' => '\StringInput',
    	'NAME' => \Bitrix\Main\Localization\Loc::getMessage('INPUT_STRING'),
    ));

    Описываем и подключаем JS-класс:

    BX.Sale.Input.Manager.StringInput = StringInput;
    BX.Sale.Input.Utils.extend(StringInput, BX.Sale.Input.BaseInput);
    BX.Sale.Input.Manager.register('STRING', StringInput);
    
    function StringInput(name, settings, value, publicO)
    {
    	StringInput.__super__.constructor.call(this, name, settings, value, publicO);
    }
    
    StringInput.prototype.createEditorSingle = function (name, value)
    {
    	var s, size = 5, settings = this.settings;
    
    	if ((s = settings.MIN) && s.toString().length > size)
    		size = s;
    
    	if ((s = settings.MAX) && s.toString().length > size)
    		size = s;
    
    	if ((s = settings.STEP) && s.toString().length > size)
    		size = s;
    
    	var element = document.createElement('input');
    	element.type  = 'text';
    	element.name  = name;
    	element.value = value;
    	element.size  = size;
    
    	BX.Sale.Input.Utils.applyBooleanAttributesTo(element, settings, BX.Sale.Input.Utils.globalBooleanAttributes, {DISABLED:'', READONLY:'', AUTOFOCUS:'', REQUIRED:'', AUTOCOMPLETE:'on'});
    	BX.Sale.Input.Utils.applyValueAttributesTo(element, settings, BX.Sale.Input.Utils.globalValueAttributes, {FORM:1, LIST:1, PLACEHOLDER:1});
    	this.applyEventAttributesTo(element, settings, BX.Sale.Input.Utils.globalEventAttributes);
    
    	return [element];
    };
    
    StringInput.prototype.afterEditorSingleInsert = function (item)
    {
    	item[0].focus();
    };
    
    StringInput.prototype.setValueSingle = function (item, value)
    {
    	item[0].value = value;
    };
    
    StringInput.prototype.getValueSingle = function (item)
    {
    	var element = item[0];
    	return element.disabled ? null : element.value;
    };
    
    StringInput.prototype.setDisabledSingle = function (item, disabled)
    {
    	item[0].disabled = disabled;
    };
    
    StringInput.prototype.addEventSingle = function (item, name, action)
    {
    	BX.Sale.Input.Utils.addEventTo(item[0], name, action);
    };
    


    Пользовательские ограничения

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

    Таким образом, например, службу доставки можно настроить так, что она будет работать только для некоторого местоположения, только для заказов такой-то стоимости,веса или выбрать другие ограничения из стандартных ограничений.

    Вы можете дополнить стандартный набор ограничений своими собственными ограничениями. Для этого следует в зависимости от ваших нужд использовать события инициализации ограничений:

    • для служб доставок onSaleDeliveryRestrictionsClassNamesBuildList:
      Bitrix\Main\EventManager::getInstance()->addEventHandler(
      	'sale',
      	'onSaleDeliveryRestrictionsClassNamesBuildList',
      	'myDeliveryFunction'
      );
      
    • для платежных систем onSalePaySystemRestrictionsClassNamesBuildList:
      Bitrix\Main\EventManager::getInstance()->addEventHandler(
      	'sale',
      	'onSalePaySystemRestrictionsClassNamesBuildList',
      	'myPayFunction'
      );
      
    • для касс onSaleCashboxRestrictionsClassNamesBuildList:
      Bitrix\Main\EventManager::getInstance()->addEventHandler(
      	'sale',
      	'onSaleCashboxRestrictionsClassNamesBuildList',
      	'myCashboxFunction'
      );
      
    • для компаний onSaleCompanyRulesClassNamesBuildList:
      Bitrix\Main\EventManager::getInstance()->addEventHandler(
      	'sale',
      	'onSaleCompanyRulesClassNamesBuildList',
      	'myCompanyFunction'
      );
      

    В обработчиках событий соответственно следует возвращать ваш класс ограничений:

    • для служб доставок:
      function myDeliveryFunction()
      {
      	return new \Bitrix\Main\EventResult(
      		\Bitrix\Main\EventResult::SUCCESS,
      		array(
      			'\MyDeliveryRestriction' => '/bitrix/php_interface/include/mydelrestriction.php',
      			)
      	);
      }
      
    • для платежных систем:
      function myPayFunction()
      {
      	return new \Bitrix\Main\EventResult(
      		\Bitrix\Main\EventResult::SUCCESS,
      		array(
      			'\MyPayRestriction' => '/bitrix/php_interface/include/mypayrestriction.php',
      		)
      	);
      }
      
    • для касс:
      function myCashboxFunction()
      {
      	return new \Bitrix\Main\EventResult(
      		\Bitrix\Main\EventResult::SUCCESS,
      		array(
      			'\MyCashboxRestriction' => '/bitrix/php_interface/include/mycashboxrestriction.php',
      		)
      	);
      }
      
    • для компаний:
      function myCompanyFunction()
      {
      	return new \Bitrix\Main\EventResult(
      		\Bitrix\Main\EventResult::SUCCESS,
      		array(
      			'\MyCompanyRestriction' => '/bitrix/php_interface/include/mycompanyrestriction.php',
      		)
      	);
      }
      

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

    use Bitrix\Sale\Delivery\Restrictions;
    use Bitrix\Sale\Internals\Entity;
    
    class MyDeliveryRestriction extends Restrictions\Base
    {
    	public static function getClassTitle()
    	{
    		return 'по лунным суткам';
    	}
    
    	public static function getClassDescription()
    	{
    		return 'доставка будет выводится только в указанном диапазоне лунных суток';
    	}
    
    public static function check($moonday, array $restrictionParams, $deliveryId = 0)
    {
    	if ($moonday < $restrictionParams['MIN_MOONDAY'] || $moonday > $restrictionParams['MAX_MOONDAY'])
    		return false;
    
    	return true;
    }
    protected static function extractParams(Entity $shipment)
    {
    	$json = file_get_contents('http://moon-today.com/api/index.php?get=moonday');
    	$res = json_decode($json, true);
    	return !empty($res['moonday']) ? intval($res['moonday']) : 0;
    }
    public static function getParamsStructure($entityId = 0)
    	{
    		return array(
    			"MIN_MOONDAY" => array(
    				'TYPE' => 'NUMBER',
    				'DEFAULT' => "1",
    				'LABEL' => 'Минимальные сутки'
    			),
    			"MAX_MOONDAY" => array(
    				'TYPE' => 'NUMBER',
    				'DEFAULT' => "30",
    				'LABEL' => 'Максимальные сутки'
    			)
    		);
    	}
    }
    


    Пользовательские правила компаний

    Вы можете дополнить стандартный набор правил компаний своими собственными правилами. Для этого следует использовать событие onSaleCompanyRulesClassNamesBuildList:

    Bitrix\Main\EventManager::getInstance()->addEventHandler(
    	"sale",
    	"onSaleCompanyRulesClassNamesBuildList", 
    	"myCompanyRulesFunction"
    );
    

    В обработчике события следует вернуть ваш класс правил:

    function myCompanyRulesFunction()
    {
    	return new \Bitrix\Main\EventResult(
    		\Bitrix\Main\EventResult::SUCCESS,
    		array(
    			'\MyCompanyRules' => '/bitrix/php_interface/include/mycompanyrules.php',
    		)
    	);
    }
    

    Описывая уже само правило, вы можете определять какие-то собственные условия. Например, в примере приведено правило автоназначения компании в зависимости от лунных суток:

    use Bitrix\Sale\Services\Base;
    use Bitrix\Sale\Internals\Entity;
    
    class MyCompanyRules extends Base\Restriction
    {
    	public static function getClassTitle()
    	{
    		return 'по лунным суткам';
    	}
    
    	public static function getClassDescription()
    	{
    		return 'компания будет использоваться только в указанном диапазоне лунных суток';
    	}
    
    public static function check($params, array $restrictionParams, $serviceId = 0)
    {
    	if ($params < $restrictionParams['MIN_MOONDAY'] || $params > $restrictionParams['MAX_MOONDAY'])
    		return false;
    
    	return true;
    }
    protected static function extractParams(Entity $entity)
    {
    	$json = file_get_contents('http://moon-today.com/api/index.php?get=moonday');
    	$res = json_decode($json, true);
    	return !empty($res['moonday']) ? intval($res['moonday']) : 0;
    }
    public static function getParamsStructure($entityId = 0)
    	{
    		return array(
    			"MIN_MOONDAY" => array(
    				'TYPE' => 'NUMBER',
    				'DEFAULT' => "1",
    				'LABEL' => 'Минимальные сутки'
    			),
    			"MAX_MOONDAY" => array(
    				'TYPE' => 'NUMBER',
    				'DEFAULT' => "30",
    				'LABEL' => 'Максимальные сутки'
    			)
    		);
    	}
    }

    Кастомизация типов дополнительных услуг

    К службам доставки можно добавлять дополнительные услуги, оказываемые клиентам при доставке товаров. Так, например, вы можете спросить у клиента нужно ли товар упаковать в подарочную упаковку.

    В случае, если вас не устраивают стандартные типы дополнительных услуг, то вы можете добавлять свои собственные типы. Для этого следует использовать событие onSaleDeliveryExtraServicesClassNamesBuildList, которое регистрирует пользовательские типы услуг:

    EventManager::getInstance()->addEventHandler(
    	'sale',
    	'onSaleDeliveryExtraServicesClassNamesBuildList',
    	'myFunction'
    );	
    

    Зарегистрируйте свой класс, который реализует ваш кастомный тип:

    class MyService extends \Bitrix\Sale\Delivery\ExtraServices\Base
    {
    	public function getClassTitle()
    	{
    			return "Моя услуга";
    	}
    	...
    }
    

    Обработчик события должен вернуть список ваших классов типов дополнительных услуг и путей к ним:

    public static function myFunction(Main\Event $event)
    {
    	return new Main\EventResult(
    		Main\EventResult::SUCCESS,
    		array(
    			'MyService' = > 'folder/myservice.php',
    		)
    	);
    }
    

    В результате ваш класс включится в работу и будет реализован интерфейс услуг в соответствии с вашими настройками.

    Класс наследуется от базового Base, которые размещается в директории: /bitrix/modules/sale/lib/delivery/extra_services.
    Там же вы сможете найти примеры штатных типов услуг.


    Кастомизация служб доставок

    Кастомизация

    Средства системы позволяют кастомизировать и добавлять свои собственные службы доставки, причем в магазине на ядре D7 они представляют из себя классы. Следовательно, можно использовать механизм наследования. Для создания собственной службы доставки необходимо создать класс - наследник базового \Bitrix\Sale\Delivery\Services\Base.

    Пример наследования для служб доставок:

    class SimpleHandler extends \Bitrix\Sale\Delivery\Services\Base
    {
    	protected static $isCalculatePriceImmediately = true;
    		protected static $whetherAdminExtraServiceShow = true;
    	
    	/**
    	* @param array $initParams
    	* @throws \Bitrix\Main\ArgumentTypeException
    	*/
        
    	public function __construct(array $initParams)
    	{
    		parent::__construct($initParams);
    	}
    }
    
    

    Система будет искать обработчик в следующих директориях:

    self::$handlersDirectories = array(
    	'LOCAL' = > '/local/php_interface/include/sale_delivery',
    	'CUSTOM' = > '/bitrix/php_interface/include/sale_delivery',
    	'SYSTEM' = > '/bitrix/modules/sale/handlers/delivery'
    )
    

    Вы можете ограничить использование службы доставки по каким-либо параметрам. Например, по максимальному весу или размеру. Для этого применяйте стандартные ограничения. Если же необходимо что-то особенное, то создайте собственные типы ограничений.

    Также для служб доставок можно задать дополнительные услуги как стандартные, так и собственно созданные.

    Дополнительно стоит отметить, что для служб доставок имеется событие onSaleDeliveryServiceCalculate, которое позволяет вмешаться в расчеты стоимости доставки (например, вы можете увеличить стоимость доставки на 100 единиц):

    EventManager::getInstance()->addEventHandler(
    	'sale',
    	'onSaleDeliveryServiceCalculate',
    	'myCalc'
    );
    
    function myCalc(\Bitrix\Main\Event $event)
    {
    	/** @var Delivery\CalculationResult $baseResult */
    	$baseResult = $event->getParameter('RESULT');
    	$shipment = $event->getParameter('SHIPMENT');
    	
    	$price = $baseResult->getDeliveryPrice() + 100;
    	$baseResult->setDeliveryPrice($price);
    	
    	$event->addResult(
    		new EventResult(
    			EventResult::SUCCESS, array('RESULT' => $baseResult)
    		)
    	);
    }
    

    Важно! При создании службы доставки учитывайте тот факт, что сайт может быть как в кодировке utf-8, так и в кодировке cp-1251. Если производится обмен данными со службой доставки, то необходимо правильно менять кодировку при отправлении и получении данных. Здесь вам поможет метод \Bitrix\Main\Text\Encoding::convertEncoding().

    Примечание: о работе с REST служб доставки читайте [ds]в отдельной главе[/ds][di] В данной главе рассмотрим принципы работы с REST служб доставки:
    • Настройка обработчика службы доставки;
    • Настройка службы доставки;
    • Настройка дополнительных услуг для службы доставки;
    • Работа со свойствами отгрузки;
    • Типовой сценарий работы REST служб доставки: работа с доставкой в центре продаж.
      Стартовой точкой для менеджера может являться функционал принятия оплаты ("Принять
      оплату в сделке") или создание дела на доставку.


    Подробнее...[/di].

    Примеры служб доставок

    В качестве примера служб доставок лучше всего подходят следующие:

    • \Sale\Handlers\Delivery\SimpleHandler (/bitrix/modules/sale/handlers/delivery/simple/handler.php) - простейший пример обработчика.
    • \Sale\Handlers\Delivery\SpsrHandler (/bitrix/modules/sale/handlers/delivery/spsr/handler.php) - вариант посложнее с использованием всех возможностей текущей архитектуры.

    Для служб доставок существует механизм автоматического отслеживания идентификаторов отправления (трэкинг-номеров) (пример, как это реализовано для службы доставки СПСР: \Sale\Handlers\Delivery\SpsrTracking).

    Рекомендации

    Для запросов к сервису службы доставки рекомендуется использовать встроенный класс \Bitrix\Main\Web\HttpClient вместо сторонних расширений. Оптимальный формат обмена - json, так как возможно использовать встроенный класс \Bitrix\Main\Web\Json.

    При обмене информацией с сервисами служб доставок зачастую необходимо передавать идентификаторы местоположений. Сопоставление идентификаторов местоположений интернет-магазина с идентификаторами местоположений служб доставок - задача нетривиальная. Как пример, используйте \Sale\Handlers\Delivery\Spsr\Location::mapStepless().

    Чтобы не порождать лишних запросов к службе доставки и не замедлять работу сайта, желательно при возможности кешировать полученную от сервисов служб доставок информацию. Однако, делать это надо аккуратно во избежание побочных эффектов. Используйте \Sale\Handlers\Delivery\Spsr\Cache.

    В случае возникновения ошибок и для отладки желательно записывать события, связанные с получением информации от служб доставок в системный журнал. Для этих целей используйте класс \CEventLog.



    Кастомизация платежных систем

    Средства системы позволяют кастомизировать и добавлять свои собственные платежные системы, причем в магазине на ядре D7 они представляют из себя классы. Следовательно, можно использовать механизм наследования:

    • если необходимо сделать платежную систему, похожую на входящую в состав продукта, то можно унаследоваться от соответствующего класса;
    • если требуется написать платежную систему с нуля, то можно унаследоваться от базового класса Bitrix\Sale\PaySystem\BaseServiceHandler или \Bitrix\Sale\PaySystem\ServiceHandler (первый вариант подходит для платежных систем, которые не являются автоматизированными, например, квитанция Сбербанка).

      Чаще всего при кастомизации используется класс \Bitrix\Sale\PaySystem\ServiceHandler, который является наследником класса Bitrix\Sale\PaySystem\BaseServiceHandler. Класс \Bitrix\Sale\PaySystem\ServiceHandler поддерживает методы, реализовав которые возможна обработка ответа от платежной системы.

    Собственный обработчик платёжной системы необходимо добавлять в пространство имён \Sale\Handlers\PaySystem\, иначе он не подключится.

    Пример наследования для платежных систем:

    class YandexHandler extends ServiceHandler implements IReturn, IHold
    {
    	public static function initiatePay(Payment $payment)
    	{
    		$params = array('URL' = > $this->getUrl($payment, 'pay'));
    		$this->setExtraParams($params);
            
    		return $this->showTemplate($payment, "template");
    	}
    
    	public static function getIndicativeFields()
    	{
    		return array('BX_HANDLER' => 'YANDEX');
    	}
    }
    

    Система будет искать обработчик в следующих директориях:

    protected static $handlerDirectories = array(
    	'CUSTOM' =>  путь берется из опции path2user_ps_files (по умолчанию "/php_interface/include/sale_payment/")
    	'LOCAL' => '/local/php_interface/include/sale_payment/',
    	'SYSTEM' => '/bitrix/modules/sale/handlers/paysystem/'
    	'SYSTEM_OLD' => '/bitrix/modules/sale/payment/'
    )
    

    Важно помнить!

    Если при копировании не изменить имя (оставить /bitrix/php_interface/include/sale_payment/yandex), то в настройках платежных систем можно будет использовать только кастомный обработчик. Системный (тот, который копировался) не будет доступен, то есть кастомный обработчик подменяет системный.

    Из этого вытекает следующее: если при копировании системного обработчика в свое пространство имен его имя меняется, то необходимо переименовать класс. Например, если мы скопировали системный yandex в /bitrix/php_interface/include/sale_payment/yandexnew, то в файле handler.php наследование должно быть так:

    class YandexNewHandler extends PaySystem\BaseServiceHandler

    Обратите внимание! Имя папки обработчика не может содержать слово handler, т.к. оно присутствует в названии самого класса внутри обработчика. То есть в приведенном примере в имени /bitrix/php_interface/include/sale_payment/yandexnew конечная папка yandexnew не должна содержать слово handler. Название папки должно быть в нижнем регистре.

    Ограничения использования платежной системы

    Вы можете ограничить использование платежной системы по каким-либо параметрам. Например, по службе доставки. Для этого применяйте стандартные ограничения. Если же необходимо что-то особенное, то создайте собственные типы ограничений.

    Требования к файлу .description.php обработчика платежной системы

    Структура массива с описанием настроек обработчика платежной системы хранится в переменной $data и имеет вид:

    $data = array(
    	'NAME' => 'название_платежной_системы',
    	'SORT' => 500,
    	'CODES' => array( // массив параметров, необходимых для настройки
    		"КОД_ПАРАМЕТРА" => array(
    			"NAME" => 'НАЗВАНИЕ_ПАРАМЕТРА',
    			"DESCRIPTION" => 'ОПИСАНИЕ_ПАРАМЕТРА',
    			'SORT' =>100,
    			'GROUP' => 'КОД_ГРУППЫ',
    			'DEFAULT' => array( // значение по умолчанию
    				'PROVIDER_KEY' => 'КЛЮЧ', // тип значения: (PAYMENT, ORDER, SHIPMENT, USER, COMPANY, VALUE)
    				'PROVIDER_VALUE' => 'DATE_BILL' // значение: поля из конкретной сущности, либо произвольное значение
    			)
    		),
    		...
    	)
    );
    

    Для вывода описания при создании обработчика необходимо объявить переменную $description:

    $description = array(
    	'MAIN' => 'ОПИСАНИЕ ОБРАБОТЧИКА'
    );
    


    Кастомизация шаблона платежной системы

    Весь HTML, который порождают платежные системы, вынесен отдельно в понятие, похожее на шаблоны компонентов. Таким образом, системы имеют некие шаблоны, которые вы можете менять независимо от них самих. Например, если вам не нравится стандартное приглашение, которое выводится некоторой платежной системой, то вы можете переопределить только этот HTML и выводить свое собственное приглашение, не кастомизируя при этом сам обработчик системы.

    Для этого нужно скопировать шаблон из папки /bitrix/modules/sale/handlers/paysystem/<имя_платежной_системы>/template/ в папку /bitrix/templates/<шаблон_сайта>/payment/<имя_платежной_системы>/template/ и отредактировать его так, как вам необходимо. За поиск HTML-шаблона отвечает метод \Bitrix\Sale\PaySystem\BaseServiceHandler::searchTemplate().


    Собственный обработчик онлайн-кассы

    Не подходит стандартный обработчик онлайн-кассы? Используйте API продукта и напишите свой обработчик. Для этого вам необходимо:

    • Унаследовать класс \Bitrix\Sale\Cashbox\Cashbox и реализовать необходимые методы:
      Примечание: дополнительно вы можете использовать следующие интерфейсы:
      • \Bitrix\Sale\Cashbox\IPrintImmediately - необходим для отправки чека на печать сразу же после его создания;
      • \Bitrix\Sale\Cashbox\ICheckable - необходим, если требуется запрашивать информацию о результатах печати чека.
      use \Bitrix\Sale\Cashbox\Cashbox,
      	\Bitrix\Sale\Cashbox\Check,
      	\Bitrix\Sale\Cashbox\IPrintImmediately,
      	\Bitrix\Sale\Cashbox\ICheckable;
      
      class CashboxCustom extends Cashbox implements IPrintImmediately, ICheckable
      {
      	/**
      	 * @param Check $check
      	 * @return array
      	 */
      	public function buildCheckQuery(Check $check)
      	{
      		// построение запроса с информацией по чеку
      	}
      
      	/**
      	 * @param $id
      	 * @return array
      	 */
      	public function buildZReportQuery($id)
      	{
      		// построение запроса на печать z-отчета
      		// если печать z-отчета не требуется, возвращается пустой массив
      	}
      	
      	public function printImmediately(Check $check)
      	{
      		// алгоритм отправки чека на печать
      	}
      	
      	public function check(Check $check)
      	{
      		// алгоритм запроса состояния чека
      	}
      
      	/**
      	 * @return string
      	 */
      	public static function getName()
      	{
      		// название обработчика
      		return Localization\Loc::getMessage('SALE_CASHBOX_CUSTOM_TITLE');
      	}
      	
      	/**
      	 * @param array $data
      	 * @throws Main\NotImplementedException
      	 * @return array
      	 */
      	protected static function extractCheckData(array $data)
      	{
      		// извлечение данных по чеку дальнейшего сохранения
      	}
      	public static function getVersion() ?: float
      	{
      		// версия ФФД, с которой работает обработчик
      		return null; 
      	}
      }
    • Подключить обработчик кассы к системе с помощью события OnGetCustomCashboxHandlers. Обработчик события должен возвращать массив вида: array(полное_имя_класса => путь_к_файлу):
      AddEventHandler("sale", "OnGetCustomCashboxHandlers", 'myCashboxFunction');
      
      function myCashboxFunction()
      {
      	return new \Bitrix\Main\EventResult(
      		\Bitrix\Main\EventResult::SUCCESS,
      		array(
      		'\CashboxCustom' => '/bitrix/php_interface/include/cashboxcustom.php',
      		)
      	);
      }
      

    В результате в административном разделе сайта в настройках кассы появится ваш обработчик.

    Список ссылок по теме:



    Принцип печати чеков через платёжную систему

    В [ds]предыдущем уроке[/ds][di] Не подходит стандартный обработчик онлайн-кассы? Используйте API продукта и напишите свой обработчик.

    Подробнее...[/di] Вы узнали, как написать собственный обработчик онлайн-кассы. В этом уроке ознакомьтесь с алгоритмом, по которому происходит печать чеков через платёжную систему:

    1. Создание оплаты

      В процессе создания оплаты подготавливаются данные для чека. Обычно это:

      • состав корзины;
      • система налогообложения;
      • НДС;
      • тип чека.

      Эти данные отправляются вместе с запросом на создание оплаты.

      В системе на этом этапе чек не создаётся, так как покупатель ещё не выполнил оплату.

    2. Оплата

      После того, как покупатель выполнил оплату, платёжный шлюз отправляет об этом уведомление.

      При обработке уведомления меняется статус оплаты на Оплачено.

    3. Чек

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

      • определяется тип чека, который нужно напечатать (совпадает с типом, который был передан при создании платежа);
      • вызывается метод buildCheckQuery, который подготавливает данные по чеку;
      • выбирается подходящая касса для печати;
      • вызывается метод printImmediately. Так как данные по чеку уже есть в платёжном шлюзе, они не передаются повторно;
      • чек в статусе Печатается добавляется в систему.

    4. Статус чека

      После того, как чек добавится в систему, получить данные о статусе этого чека можно либо через агент \Bitrix\Sale\Cashbox\Manager::updateChecksStatus(), либо менеджер может вручную запросить статус из интерфейса.

      Примечание: Если первый чек был чеком "полной оплаты", на этом этапе работа с чеком завершается. Если чек был другого типа ("аванс" или "предоплата"), нужно напечатать второй закрывающий чек в момент отгрузки заказа.

    5. Печать закрывающего чека

      В момент отгрузки заказа начинают собираться данные для второго чека:

      • определяется тип чека, который нужно напечатать (в этом случае тип "полная оплата");
      • вызывается метод buildCheckQuery, который подготавливает данные по чеку;
      • выбирается подходящая касса для печати;
      • вызывается метод printImmediately, и теперь данные по чеку отправляются платежному провайдеру;
      • чек в статусе Печатается добавляется в систему.

    6. Статус закрывающего чека

      После того, как чек добавится в систему, получить данные о статусе этого чека можно либо через агент \Bitrix\Sale\Cashbox\Manager::updateChecksStatus(), либо менеджер может вручную запросить статус из интерфейса.

    Список ссылок по теме:



    Работа с REST служб доставки

    В данной главе рассмотрим принципы работы с REST служб доставки:

    • Настройка обработчика службы доставки;
    • Настройка службы доставки;
    • Настройка дополнительных услуг для службы доставки;
    • Работа со свойствами отгрузки;
    • Типовой сценарий работы REST служб доставки: работа с доставкой в центре продаж. Стартовой точкой для менеджера может являться функционал принятия оплаты ("Принять оплату в сделке") или создание дела на доставку.

    Процесс создания и настройки службы доставки

      Настройка обработчика службы доставки

    Обработчик службы доставки представляет из себя шаблон, по которому в дальнейшем можно будет создавать экземпляры служб доставки. Поэтому перед созданием службы доставки необходимо добавить обработчик с помощью метода sale.delivery.handler.add:

    {
    	"CODE":"uber",
    	"NAME":"Uber",
    	"DESCRIPTION":"Uber Description",
    	"SETTINGS":{
    		"CALCULATE_URL":"http://gateway.bx/calculate.php",
    		"CREATE_DELIVERY_REQUEST_URL":"http://gateway.bx/create_delivery_request.php",
    		"CANCEL_DELIVERY_REQUEST_URL":"http://gateway.bx/cancel_delivery_request.php",
    		"HAS_CALLBACK_TRACKING_SUPPORT":"Y",
    		"CONFIG":[
    			{
    			"TYPE":"STRING",
    			"CODE":"SETTING_1",
    			"NAME":"Setting 1"
    			},
    			{
    			"TYPE":"STRING",
    			"CODE":"SETTING_2",
    			"NAME":"Setting 2"
    			}
    		]
    	},
    	"PROFILES":[
    		{
    		"NAME":"Taxi",
    		"CODE":"TAXI",
    		"DESCRIPTION":"Taxi Delivery"
    		},
    		{
    		"NAME":"Cargo",
    		"CODE":"CARGO",
    		"DESCRIPTION":"Cargo Delivery"
    		}
    	]
    }
    

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

    При создании обработчика также необходимо указать URL для веб-хуков, по которым будут происходить обращения в случаях:

    Если интеграция с заказами на доставку не предполагается, то CREATE_DELIVERY_REQUEST_URL и CANCEL_DELIVERY_REQUEST_URL можно не передавать. В этом случае флаг HAS_CALLBACK_TRACKING_SUPPORT должен быть установлен в значение "N".

    При создании обработчика службы доставки есть возможность задать набор доступных параметров (CONFIG), значения которых могут быть уникально заданы для каждого экземпляра конкретной службы доставки. Параметры могут быть использованы для хранения API ключей, номеров договоров и других авторизационных данных для конкретного экземпляра службы доставки.

    Создание обработчика возможно только при наличии как минимум одного профиля доставки (PROFILES).

    Также доступны методы обновления и удаления существующих обработчиков и получения списка обработчиков.

      Настройка службы доставки

    После успешного создания обработчика службы доставки можно приступить к добавлению экземпляра самой службы доставки с помощью метода sale.delivery.add:

    {
    	"REST_CODE":"uber",
    	"NAME":"Uber Taxi",
    	"DESCRIPTION":"Uber Taxi Description",
    	"LOGOTYPE":"/9j/4AAQSkZJRgABAQEBLAEsAAD/2wBDAAgGBgcGBQgHBwcJCQgKDBQNDAsLDBkSEw8UHRofHh0aHBwgJC4nICIsIxwcKDcpLDAxNDQ0Hyc5PTgyPC4zNDL/2wBDAQkJCQwLDBgNDRgyIRwhMjIyMjIyMjIyMjIyMjIyMjIyMjIyMjIyMjIyMjIyMjIyMjIyMjIyMjIyMjIyMjIyMjL/wgARCAIVAyADASIAAhEBAxEB/8QAGwABAAIDAQEAAAAAAAAAAAAAAAYHAwQFAQL/xAAaAQEAAwEBAQAAAAAAAAAAAAAAAwQFBgEC/9oADAMBAAIQAxAAAAGfgAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAHz4+tPUw4lrP8Y2dPkY3jIxjIxjIxjIxjIxjIxjIxjIxjIxjIxjIxjIxjIxjIxjIxjIxjIxjIxjIxjIxjIxjIxjIxjIxjIxjIxjIxjIxjIxjJ9YXre3eJ93oe0xZdyqH34AAAAAAAAAAAAA5W9yceyGLZAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA++zw+hqQbo3qgAAAAAAAAAAAAGho7mnzN4KcgAAADT2qsvxWSrZeisnaqyyKcm6M6ZrbMFtfEuVs04LJzVhLK/1KRlWGPJwZfnpK2bFeyfqtN/49sYYtkfPrVVs3Ktkq2Fp/WjvYtkPj3W+IhxditZKtkvln5opK8mwEH1j1+fBtOCyVbLXxZe1XFj50wU5AAAAGxr5ZvnsDrM8AAAAAAAAAAAADnae5p8xfCp9gAAAfNWWnVm1WDYrLIreyMqxujCtILOoLow8QdDTSyJyylLKRzV1we9wbPxCB1NBv6G/F7Yw5LRfP18+qsHY5oFj72jvcjohH7BOL2uL1WeFj5lUrikr5q8FKThwacwboaYaMO9Y9cWPhWwypwAAAGXFll87A63OAAAAAAAAAAAAA52nuafMXwqfYABz67vxWoqtajtKrPu0CrFpvVWWRt1z4shVYtSC8WcEGWm9VZLJPFPEtVWLU4MJ7hwlpvVWb9iaB0VVvFqfNW+nytN6qxaY096uNLxaiqx2eLOe16qxaYi0rikW8WoqsTiDdyblWLTeq6sfn134tRVYtRVlpU5fRSkAAZcWWXzsDrc4AAAAAAAAAAAADnae5p8xfCp9gAc6u7ErveqBqQe2pVdqY1kMayraya21q+mNyqnMGnOdN3Bz1xEpbErsUXHS0nd4Xdrfc4HLX3P6HPl+a6HW573z3xag4/SDxW+lu6XXZwSeTrt8Tt8rfCv9xSKyqK9NRC5H251BZ1z1wM6bQrmxq53agasC1KrtTGs+jGsgAMuLLL52B1ucAAAAAAAAAAAABztPc0+YvhU+wAOdXdpaOlBXaxFyOvLU53RoTBQlVtZOhdjrlYjRgruc7e3VkzDLnRKW61j4rNYjWr133ZPmi+tsY1lz+h8fflWrEbdWu/bDHRGDbArfSsb53KtdrEfXmr28ObGshF9RSK2Zr69eu1iJ/iLTrT3MuwFX70K5tLR0oa7WIuRV3anP6NCYKEoADLiyy+dgdbnAAAAAAAAAAAAAc7T3NPmL4VPsAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAABlxZZfOwOtzgAAAAAAAAAAAAOdp7mnzF8Kn2ABx4FPIFvVPXjRh9eD14PXg9eD2z6utHJscOFTWF2Pjx6vRePR49Hj0ePR7atVWrj2YvEZdEbcYXYgDNkNVsa4BsWdWFn4tmJRaUxa/F7YVfXZbjiiVxY+WpIDlRyzsZSrLiN+x63sjEtBlzgAMuLLL52B1ucAAAAAAAAAAAABztPc0+YvhU+wAOPAp7AugputybY0oYJo3HCCCA6W3LJSU/zbLrkw2hV9oZFjiQuaQux8OnzLYvxQBbIqbBcAo7y5q5I+D21KrtTGsxeJS2JXI2zrWNdig65BUy1vCqtS4OeVCDPZ1Y2diWolF5RF78P1eFH3hcjVTa1UnD3NPZLpBUnI6nLN6x64sfCthlTgAMuLLL52B1ucAAAAAAAAAAAABztPc0+YvhU+wAOPAp7AugprYqe2NKHtQibwgggLJk8VlBHa4sauTDaFaWVlWOJDZnE5vj4tSrbPvxdzW2IMSrdpboFuYMvyU3h7vEPm0azsvGsxiKSyM3I8dgQGfXYpW8jBJfKW2C4udUuY+H0MtlVvZGJaiUck8bvw/F20tdFyP2FzQV3IpEGplrI4XyG9Y9cWPhWwypwAGXFll87A63OAAAAAAAAAAAAA52nuafMXwqfYAHHgU9gXQU1sVPbGlD2oRN4QQQFjSmLSgj1b2HXgsqtbKybHHh8tic/wAfVn1XaV+LsQec8cqjdmXYOs+uMQbje+H1Z1Y2bkWY1GJPF7UWCxK7sW7HKoxJtIqHYsP0rVZMENEGxY9cWPjWYtHpFGb0X1dVK3Pbj++F16vLB6tQ28cmo7yqg4gN6x64sfCthlTgAMuLLL52B1ucAAAAAAAAAAAABztPc0+YvhU+wAOPAp7AugprYqe2NKHtQibwgggLJlEXlJFK4sitz6s2r7QyLHEhkzhdj4y2nU9sX4u157DiXqk3C0YRM8hR3vc4Jls+rLUxrMaiEsiVyNY9cWPdilPvvBO981gLQqXa4hi9+RtWXWNnYlqJRyRxe/DmuukLwuR/FWWtVJw7WqmTFmRqS+FHN7RN6x64sfCthlTgAMuLLL52B1ucAAAAAAAAAAAABztPc0+YvhU+wAOPAp7AugprUquf6UMzhc055Trcyk7k2tskSrqYQ88tCr7QyLHEhc0hdj4WxU9sX4u1A55CyAt/fJ32cWUgMLlkTPbUqu1MazF4lLYlcjWPXFj3YpXFZVFitn2Ph9+HyDPZ1Y2diWolF5RF78PtiV0uR2LC+cH18iw/quh3+AG9Y9cWPhWwypwAGXFll87A63OAAAAAAAAAAAAA52nuafMXwqfYAHHgU9gXQU3Q57Shu7JU9gHYeeH1q8uvjU1w8tCr7QyLHEhc0hdj4WxU9o34pC1hstb4NzDw4KaeqHtqVXamNZi8SlsSuRrHrix7sUrAA5fU5ZUIM9nVjZ2JaiUXlEXvwhcjAAAA3rHrix8K2GVOAAy4ssvnYHW5wAAAAAAAAAAAAHO09zT5i+FT7AA48CnsC6CmGlCB9eeAADy0Kws/IscSFzSF2Ph56vxePR49AAHtqVZaeLZi8SlsSuxvr5XYvt8D7fA+/PkAZ7OrKzcS1EovKYtfhC5GAAABvWPXNjYVsMqcABlxZZfOwOtzgAAAAAAAAAAAAOdp7mnzF8Kn2AB49evHo8ejx6PHo8ejz0PHo8ejx6PHo8ejx6PPQ8ejx6PHo8ejx6PHo89Dx6PHo8ejx6PHo8ejz0A8AAMuLLL52B1ucAAAAAAAAAAAABztPV4mBbkqNK/3JUaElRoSVGhJUaElRoSVGhJUaElRoSVGhJUaElRoSVGhJUaElRoSVGhJUaElRoSVGhJUaElRoSVGhJUaElRoSVGhJUaElRoSVGhJUaElRoSVGhJUaElRoSVGhJcsV3ZPJqOjpAAAAAAAAAAAAAQSEWrVQAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA7/AnpNQAAAAAAAAAAADEZWjgOrVM60itm9ogAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAzGW4In1juuTnN9gzgAAAAAAAADn9DhHxhAAD54EhEE1rE9K5WMK5WMK5WMK5WMK5WMK5WMK5WMK5WMK5WMK5WMK5WMK5WMK5WMK5WMK5WMK5WMK5WMK5WMK5WMK5WMK5WMK5WMK5WMK5WMK5WMK5WMK5WMK5WMK5WMK5WMK5zz/wi8izAABkxjpdOP90yAAAAAAAAAwanSHEwSIRlI8RwXYxnLb+M1Gf4Mb3wAAAAAAAAAAAAAAAAAAAAAAAAAAHp4+8hgbeQ0HTyHId3KcDP3By9raAAAAAAAAAAAAAAAAD5+hi+NganzujQ+eiOZ89Ucn57A4vnbHD+e8OB5IBHkhEd8kYjaSCNpII2kgjaSCNpII2kgjnsiEeSER/3vjg+90cT3tDj+9ccr3qDm/XQGl9bY1/rMPj7AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAD//xAAyEAAABQEGBQMDBAMBAAAAAAAAAQIDBAUQERITFDQVIDIzQDAxNQYhYBYkQUMiI1Cg/9oACAEBAAEFAv8AxgqfIgbyzGNQxqGNQxqGNQxqGNQxqGNQxqGNQxqGNQxqGNQxqGNQxqGNQxqGNQxqGNQxqGNQxqGNQxqGNQxqGNQxqGNQxqGNQxqGNQxqGNQxqGNQxqGNQxqGNQxqGNQxqGNQxqGNQxqGNQxqGNQxqGNQxqGNQxqGNQxqBOrIJkAjJReeZkkluGv8DSo0mhZLLznV41fgiVGhRHeXmOqwt/g0dX28yR6urjjVxxq441ccEZKK1b7TatXHGrjjVxwh1t3kWtDZauONXHGrjhMllSuTVxxq441ccauOCMlFat9ptWrjjVxxq44Q627yLWltOrjjVxxq44KSwpXqMn/s8yR1emftyRNnbVd7bRum2rbO2FvbT9uSHs7arvraNyVbZWw976jXc8yR1emftyRNnbVd7bRum2rbO2FvbT9uSHs7arvraNyVbZWw976jXc8yR1emftyRNnbVd7bRum2rbO2FvbT9uSHs7arvraNyVbZWw976jXc8yR1emftyRNnbVd7bRum2rbO2FvbT9uSHs7arvraNyVbZWw976jXc8yR1c87Y3mLzF5gjO+4hcQuIXEJR/u7zF5i8xS/vCuIXELiFY+yrzF5i8xSvvMuIXELiE0v2V5i8xeYIzvuIXELiFxCWf7y8xeYvMUv7wriFxC4hWPsLzF5i8xS/vMuIXELiEwv2d5i8xeYvMF7c7Xc8yR1c87Y2l78kveW0rZW1nqtpO8tnbG0vfkl7y2lbK2s8lK3ts3Zche3O13PMkdXPO2Npe/JL3ltK2VtZ6raTvLZ2xtL35Je8tpWytrPJSt7bN2XIXtztdzzJHVzztjaXvyS95bStlbWeq2k7y2dsbS9+SXvLaVsrazyUre2zdlyF7c7Xc8yR1c6kpWnQxRoYo0MUaGLyqhx1q0MUaGKNDFDbaGk2usNPDQxRoYo0MUNxmWVWqSladDFGhijQxRoYvKqHHWrQxRoYo0MUNtoaTa6w08NDFGhijQxQ3FYaVapJLToYo0MUaGKNDF9FrueZI6vwZrueZI6vwZrueZI6ueprU3B1cgauQNXIGrkDVyBq5A1cgauQNXIGrkDVyBq5Aa+7NYWtuLqpA1UgaqQNVIGqkDVSBqpA1UgaqQNVIGqkBMl/EKy640jVyBq5A1cgauQNXIGrkDVyBq5A1cgauQNXIDEp85ArLrjatVIBSpF+IxiMYjGIxiMYjE9L7Y1UgaqQIUh5U30Gu55kjq56psLhcLhcLhcLhcLhcLhcGuzWNrcLhcLhcLhcLhcLhcCL/IVnouFwuFwuCGsQuZBtJuuFwuDBfuBWeq4JIsWWgZaBLqrUWTx1oQ5kWaMtAWw04241lO3CFvfQa7nmSOrnqexsapkt9rg84SIb8S2PAkym+DzhIgSYrdjXZrG1sap0t5vhU4cKnDhU4Lp8tAMrjtLqFZ6LG47zpaKUNFKC4km448hI0kgjciSEWsbgVnqCeqyr/KCI6bEuyqFhqYhb30Gu55kjq56nsbKR8WPqL2soGwFdLFDMiNIa7NY2tlJ+L5H4jElNRpioR2F1Cs9FlA2Nn8WVD42xjcCs9QT1WVf5QR0G7JsqKsdRELe+g13PMkdXPU9jZSPix9Re1lA2F4rW1/qDXZq+1IrxlqFJ+MDj7TI10YJWlZB1tLzTzC2XCQoxhMjFZ6CSahgUKDsiBquGYgZiRjbE9aVQDQoEhRhpJk+Kz1ZahhNKrJ1HelTP0/IFPpSIShJfTGjGZqUIW99BrueZI6uep7GykfFj6i9rKDsBWdr/AFJ+xN9qr/eMMBYaV8YYrxXqwkIkpcd6yrJwVL3Cf8FCsdJ/cywpFB2Qrl2iUeJX9pFefUd1w6iZ/wAXxVzuPBebf2dL25HXW2W6lUTmrshb30Gu55kjq56nsbKR8WPqL2soOwIVvaf1f0t9uqdn+DK8Ur700x9QWIQa5HsQqTmZUT7Zdv8Amrj+RQdkQrplollcr+5vuf1oH9THUKt3PvdfhOyRVo0Z+JUGJpipRtVCthb30Gu55kjq56nsbKR8WPqL2soWwFcvOEof0t9ur7YXoFL+N/iZBRMHAGBHp0eK4J81MOOk7wfbLt/zVjIk9KBQdkJMZqW2dEhmODxMRUWGQltJZfR7/wBTHUKuV54iMESTCh7Cr/KU+TpZtlVjaadZC3voNdzzJHVz1PY2Uj4sfUXtZQNhcK99oAJRpDXarG1IzIZhilfem2XC6yp0lagR3GZmYQq4xV1GSPeygbG4XWXWVMz4gR3GajUI53Pis9WYoYzM/cYRV/lBSZOpgitRs6HZC3voNdzzJHVz1PY2Uj4sfUXtZQNgK/sLGuzWNrZSfixW5L0dXEpoj1mU04hZONirxyjzwXUKz0WUDYirSnYkXjc0cbmjjU0OuqedsY3ArPUE9VlX+UFEk5MwGRKKZHOLKELe+g13PMkdXPU9jZRlX0sfUKf9VlCK6nD6gV+zsa7NY2tlJ+LH1F12Ur4wfUJf7AXUKz0WUDYiv7DmY3ArPUC+x/qCMP1BGE59MqYCM0qT9QMYf1BGFUmMTViFvfQa7nmSOrnqexs+n3r2RNilMivRH4640GRKXHZTHYH1A9ifsa7NY2tlJ+LFdYdeVopQj0iU+tttLTYr7mKYC6hWeiygbEV4jODhUMKhhUMKrWNwKz1enC3voNdzzJHVz1PY2QpRw5TbiXW+SQ+iMw+8qQ/Y12axtbKT8XyuuoYakvqkyAXUKz0WUDY8tS+NsY3ArPV6cLe+g13PMkdXPU9jbAqTsI49UiSCIyMGoklIq8Vgpk52a5a12axtbKW80mm6hkahkahkKlx0h6tRGim1B6aqwuoVnosoGx5al8bYxuBWer04W99BrueZI6uep7HlIzIGZnzNdmsbX0y6hWeiy8yGJQxKGJQxKGJQxKtY3ArPV6cLe+g13PMkdXPU9j6jXZq+29MuoVno9NjcCs9Xpw976DXc8yR1ehcLhcLhcLhcLhcLhcLv+m13PMkdX4M13PMkdX4M13PMqU5MV7jDY4w2OMNjjDY4w2OMNjjDY4w2OMNjjDY4w2OMNjjDY4w2OMNjjDY4w2OMNjjDY4w2OMNjjDY4w2OMNjjDY4w2OMNjjDY4w2OMNjjDY4w2OMNjjDY4w2OMNjjDY4w2OMNjjDY4w2OMNjjDY4w2OMNjjDY4w2OMNjjDY4w2OMNjjDY4w2OMNjjDY4w2OMNjjDY4w2OMNjjDY4w2OMNjjDY4w2OMNjjDY4w2OMNjjDYiVRD0rzPqFH+X4NRkY6n5lXj6iB+DfT7FyPNqkLRyfwSNHXKfZaSwz5JuISDlIByzByHDD6dQ3JirjK/AmmlvLiRyiIJ9wgUpQKUkE82rxXJNwU4tfMaSUT1LSoLgSUDIeIZLoyXRkujJdGS6Ml0ZLoyXRkujJdGS6Ml0ZLoyXRkujJdGS6Ml0ZLoyXRkujJdGS6Ml0ZLoyXRkujJdGS6Ml0ZLoyXRkujJdGS6Ml0ZLoyXRkujJdGS6Ml0ZLoyXRkujJdGS6Ml0ZLoyXRkujJdGS6Ml0ZLoyXRkujJdGS6Ml0ZLoTDkKDVKMNtIZTypWpIRKBHeXguMqb/EUNqWEJwI8JTKFA4gOO4QNCi/CSSZgmHDBRDCY7afJNKTBsNmDioGkGlUNM4MlwYFF/2cKhlODTuDSrBRAUVAKO2QJCS/42FIy0GMhsadsaVsaVI0hDSDSGNKoaVY0zg07g07gyHBkuDKcGWsZaxgUMKhhMXGLjFxi4xcYuMXGLjFxi4xhMYVDAoZaxlrGU4MlwZLgyHBp3BpnBpVjSrGkUNININIkaVA0zYyGxlNjCn/wAf//EACsRAAAEBAUFAQACAwAAAAAAAAABAgMREyAyBBAUMVESITBBYSJQgDNCUv/aAAgBAwEBPwH+miGP+h0kIEIEIEIEIEIEIEIEIEIEIEIEIEIEIEIEIEIEIEIEIEIEIEIEIEIEIEIEIEIEOkgtgj2BlDsfnYT/ALfwb6e0fO1YVTrnQNQYbcNZ5OL6SiNQYQ8alQyUcCiNQYJ8456gxqDBdyyU+ZHAagwhXUmOTjppOA1BhtfWVS7T87VhVYj1lh98n7cmb8nLTyTvSnbJdx5M2ZP3ZYfapVp+dqwqHHeg4DUfB/mGn+iEnuNR8HVN/I0/0S5f6Go+Cb1/kaf6JHT3iNR8Go+DT/Rp/onw7QGo+CV1/oaf6Jkv8jUfB0Tf0NP9EZPYaj4G3OuhVp+dqwqMRdlh/eWI2LJi7J6zJu4slWnSrfJu0snrzyYtyxG+WH90KtPztWFQ40ajiNOoNNmjfJ1BrLsNOoNtGk45OJ6kwGnUEsmRxyMokNOoadWZsHEadQSUChktk1KiNOoNp6Shk62ajGnUGmzRvQq0/O1YX8Gq0/O1YVD5/oRMRMRMRMRMMH3MPH+hExExExExExh/YeM+sRMRMRMRMRMMHuHTPrMRMRMRMRMRMMbUKtPztWFQ/dVh9zD19WH9h6+rD+w7edWH2oVafnasKh+6pj2HrqsP7D19WH9h286sPtQq0/O1YVD91WH3D19WH9h6+rDh286sPtQq0/O1YVD91WH3MPX1Yf2Hr6sP7Dt51YfahVp+dqwqH7qsPuYevqw/sPX1Yf2Hbzqw+1CrT87VhUP3VYfcw9fVh/Yevqw/sO3nVh9qFWn52rCofuqw+5h6+rD+w9fVh/YdvOrD7UKtPztWF/BqtPztuJJMBNRyJqORNRyJqORNRyJqORNRyJqORNRyJqORNRyJqORNRyJqORNRyJqORNRyJqORNRyJqORNRyJqORNRyJqORNRyJqORNRyJqORNRyJqORNRyJqORNRyJqOQp1MP7+f/xAA2EQAAAwQHBgYCAgIDAAAAAAAAAQIDBAUzERMUFSBRUhASMDRxgSExMkJioSNBIlCA8DVDYf/aAAgBAgEBPwH/AAzMyIqTDxElGdDIKbtFeahWLzFYvMVi8xWLzFYvMVi8xWLzFYvMVi8xWLzFYvMVi8xWLzFYvMVi8xWLzFYvMVi8xWLzFYvMVi8xWLzFYvMVi8xWLzFYvMVi8xWLzFYvMVi8wTZoXkoMIktPg08SCFkst5PHibf/AKi/o4a3NK6s/I+O/HS8KxObol4ppPyF1I1B8c0sEkZHsdGBN2m6YupGoPMPSyZGsj2MWdY0JGYupGoLhiEpNVO26kahdSNQWW6oy2MYahbMlU+YupGoPLImTU0FsdHFLdnvmYupGoPjsTBRER4nc6GqevHfZ6sUJ9/bZFfQnZDJ3bZEOXV/v72Ok9OxtLV0wtZh7HWSnpsf+YV/v62QyR32RWYXTEwmp68d9nqwOrlaE71NAun5/Q/4/wCW92F7fD7G/eH8PTQLp+f0Kiw/lpp/Qvb4fYtVs/BRRSLp+f0LDZ/zb1NAvb4fYvKs/Hu+fgLp+f0Lp+f0L2+H2L2+H2LtrP573mLp+f0LfUfi3aaPAXt8PsWS1/npopF0/P6FfYfxUUi9vh9irt/8/TQLp+f0Ht0s9HjTTgYTU9eO+z1YIVLPrsi3s77IV61bInI77IfzCdj3IVsYzE9cLKWWx6nq67HDl07InP7bIVLPrsi3s74GE1PXjvs9WBzfEMEGlRC9WWRh9ekt6N39bHJ5SwUZqF6ssjD2/IbM90i2OzUmTUlmL1ZZGG0RZtGZpIvPYzVurJQvVlkYvVlke1ETZpSRUGL1ZZGGyyW0NRfvY7RBDJkSDIXqyyMPbcmzTeLY5viGCTJRC9WWRh9ekt6N0vLAwmp68d9nq/o2E1PXjvs9WCGoSbHxL9irRkKtGQq0ZCrRkKtGQiiSIk0CHJSbDxIVachVpyFWnIVachVpyEVSRbtH/ocEJNgVJCrRkKtGQq0ZCrRkKtGQiiSJSaA4oSbumkhVoyFWjIVaMhVoyFWjIRRJE0KjAwmp68d9nqwQuT3xRb0pENkYot7O4h/LliivqSHDl04orMLpgYTU9eO+z1YIXJ74ooXgkQ6Rii3t7iH8uWKK+aQ48unFFZhdMDCanrx32erBC5PfFFfQkQ2RiivtDhy5Yor5kHKQnFFZhdMDCanrx32erBC5PfFFvSkQ2Rii3s7iH8uWKK+pIcOXTiiswumBhNT1477PVghcnvii3pSIbIxRb2dxD+XLFFfUkOHLpxRWYXTAwmp68d9nqwQuT3xRb0pENkYot7O4h/LliivqSHDl04orMLpgYTU9eO+z1YIXJ74ot6UiGyMUW9ncQ/lyxRX1JDhy6cUVmF0wMJqevHfZ6v6NhNT14706NltlKSkWF40iwvGkWF40iwvGkWF40iwvGkWF40iwvGkWF40iwvGkWF40iwvGkWF40iwvGkWF40iwvGkWF40iwvGkWF40iwvGkWF40iwvGkWF40iwvGkWF40iwvGkWF40iwvGkWF40iwvGkWF40iwvGkWF40iwvGkMnNuTQjNP+fn/8QAPxAAAQICBgcECQMDBAMAAAAAAQACAzMQESAxMpISITBxcoGRQEFRcwQTImBhgqGxwTRCUiNDUBRioKJTY9H/2gAIAQEABj8C/wCGD7OtXrEViKxFYisRWIrEViKxFYisRWIrEViKxFYisRWIrEViKxFYisRWIrEViKxFYisRWIrEViKxFYisRWIrEViKxFYisRWIrEViKxFYisRWIrEViKxFYisRWIrEViKxFYisRWIrEViKvXtBav8AAVlfD3DrH+A+A9xawq/eQt7a0bWdD6qfD6qfD6qfD6oEGsGxoviNafAlT4fVT4fVT4fVHQe11XgbFb3Bo+Knw+qnw+qnw+qDWxWEnursz4fVT4fVT4fVT4fVAg1g2NF8RrT4EqfD6qfD6qfD6o6D2uq8DYre4NHxU+H1U+H1U+H1QDYrCT8dqO2jaGzB4BY+UWI3Kx84sQeKwbMHhFg8IsRuVj5hYg8W1b20btobMHgFj5RYjcrHzixB4rBsweEWDwixG5WPmFiDxbVvbRu2hsweAWPlFiNysfOLEHisGzB4RYPCLEblY+YWIPFtW9tG7aGzB4BY+UWI3Kx84sQeKwbMHhFg8IsRuVj5hYg8W1b20bthG4VeVeVeUNZVwVwVwVwUbjKvKvKvK1/yKuCuCuCg1fFXlXlXla/4lXBXBXBRuFXlXlXlXlXBXBXBXBRuIq8q8q8rX/Iq4K4K4KDV8VeVeVeVr/iVcFcFcFG4VeVeVeVedi3to3bCNw2BZjcZsfMbEHnY+Q2I3DsY3GbHzGxB52PlNiNw7VvbRu2EbhsCzG4zY+Y2IPOx8hsRuHYxuM2PmNiDzsfKbEbh2re2jdsI3DYFmNxmx8xsQedj5DYjcOxjcZsfMbEHnY+U2I3DtW9tG7YFrhW03hSGqQ1SGqS2yXOgtJN6kNUhqkNWjDaGjwsD1jA6rxUhqkNUhq0ocMNNgtcKwe5SGqQ1SGqS2yXOgtJN6kNUhqkNWjDbojwsD1jA6q6tSGqQ1SGrShww02C1wrBvUhqkNUhqkN2Le2jd7jt7aN3uO3to3bB7mOLTWNYU+JmU+JmU+JmU+JmU+JmU+JmU+JmU+JmU+JmU+JmU+JmU+JmTOEJhY4tOn3FT4mZT4mZT4mZT4mZT4mZT4mZT4mZT4mZT4mZT4mZT4mZD+tEzUQtB7m6zcVPiZlPiZlPiZlPiZlPiZlPiZlPiZlPiZlPiZlPiZlPiZlDBjRKtIfuog6D3NrruKnxMyH9eJmV6vV6vV6vXrYUaJo940rlPiZlPiZlBDoryNK7S2Le2jdsH7xtWcITOPaCiDvNqvuorZYh8Qog86BvWBvRYG9E+D/pQ7R761+j+qOg0B4vaQsDeicwsbU4VXJ8M3tNVEHi2Le2jdsH7xSIkOFW03HSCk/8AYJvrmaOldrp04MPSbXVepP8A2C040PRbXVfSzhCZx0iJDgktNxrC/TnqF+nPUL9OeoXtejxOQrVRvsCiDvNNcOE948WtX6aLkX6aLkTWCBFyqpvosTKg4ejxdd40U4mBEDR36NMPiFEHnQN9Mbl9qIURvc6mPvog8Wxb20btg/eKYG4/ej0fnS7zDQwf+wfYqsUM4QmcdMDd+bNUWGHfHvWm32oJ7/CkUQd5pf5lkBekcBph8Qog86BvpjcvtRDYLy4Uxz/uqog8Wxb20btg/eKYG4/ej0fnS7zDRD8z8FHfQzhCZxrVRA3fmgesiNbX4lT4eZVscHD4UOhvFbXCop7HftNVArFEHeVqof5lGtYx1WMdVib1UcBwJ0D30w6/5CiDzVyFY76XxmxIYDvFTYX1XrHO04vj4UPiu/aESbzrog8Wxb20btg/eKYG4/ej0fnS7zDRD8z8FHei7wTK/wCITONaDear0lA3fmiBrqFRXsu1psRpuPtjxFMVzT4V9FpOch3tNEHeVoDUAtRrKf5lDa//ACDuRK5KpVXNC0mOuWk46lDINbS4UQj366l7R1oNGsV2i+I4NaO8rRbWILbh4/GmDxbFvbRu2D94pgbj96PR+dLvMP2oZ5n4KO9HemcIUPjUTxQ/gFBPw/K5KBzoLG3u1IUekkd2romocVEDiUSh/mLkm1iv+oiFyQR3o7kN6Z5go9H5p1V9etBo8dZpdCfp6TfBqcIWlW3xFD2fuHtN32IPFsW9tG7YP3imBuP3o9H50u8w0NA/mEGjuR3pvCEzjWm3mrioFXh+aG6byNFTYn0Re0Ev/k6gu/uHCE8m+pNQ4qIWqvWUa73UP8ygMi16INeoqstfmVdT8yB0X5lHhswtdUEdyG9M8wUQh366l7Y1rVXqpjcvsmRP23O3UuqwP9oUweLYt7aN2wfvFMDcfvR6Pzpd5hobr/uCjUmbgmca1LuUA/D82jHhPc897XfhalrXOiFvNL/MtRx/uVYWtQ+IUQea8UB8aY3L7UNrPts9l1HrBiha+XfTB4ti3to3bB+8UwNx+9Ho/Ol3mGhvmClnCEzjpgbvzRB9VEcyuuupfqXoGI/1jO8FNe3C4Vih2iKmv9oUCiDvNL/Moa+EQHadWsLG3Ksbcqxtyp0R+J2s0w+IUQedA30xuX2o9WcMXVzoINxUSF3A6t1EHi2Le2jdsH7xTC+FY+tEB3g4imvxeaIbfF/4pZwhM46YG780ej7jTAr/AI0QD8DQKIO80v8AMob5gtw+IUQedAUuL9FLi/RPjMBAd40BwvGsIaUKJX31VKXF+iY+Gx7XDUa6IPFsW9tG7YP3imLB72nSFDoR1HuPgVoxIThy1INZDNXe4jUEyE25oqohwR+wVnnSzhCZx0wN35og+rhvfVXhC/TRcqAdDMNne5ybDbhaKhQxn8WUCiDvNL/MobUP7gWF3RYXdFhd0WE9KYfEKIPPaQeLYt7aN2wfvFLYo1i5w+Ca9hra7WDZdFiHUE+K+9xrpZwhM46YG782nRIhqa1PjO/caBRB3ml/mWvSOA0w+IUQee0g8Wxb20btg/eLFWKEb2rVFDXfxdqWpVk1I1P9Y7wYq36mi5o7rDOEJnHTBDojAau8qazMprMymszLXHhj5l7LjEPg1e37LBcwUiiDvNL/ADLXpHAaYfEKIPPaQeLYt7aN2wfvFrUSFrNdpnCEzj2gog7zTqJWJ3VYndVid1WJ3VYndViPWmHxCiDz2kHi2Le2jdsH7xtWcITOPaCiDvO0h8Qog89pB4ti3to3e47e2jd7jt7aN3uO3trGuY41tr1KU9SnqU9SnqU9SnqU9SnqU9SnqU9SnqU9SnqU9SnqU9SnqU9SnqU9SnqU9SnqU9SnqU9SnqU9SnqU9SnqU9SnqU9SnqU9SnqU9SnqU9SnqU9SnqU9SnqU9SnqU9SnqU9SnqU9SnqU9SnqU9SnqU9SnqU9SnqU9SnqU9SnqU9SnqU9SnqHDENwLj22A/ePceH/ALQT211WJntD3HiekH93sjt3sj+k/W3/AOe4rYTLz3+CbCZhaKu1a3BagStTQr0WRCSFr1t7ne4egwVleyfbN7liWsBawQsXZama/itbrVThWFXBdo/A3KXXw61KflUp+VSn5VKflUp+VSn5VKflUp+VSn5VKflUp+VSn5VKflUp+VSn5VKflUp+VSn5VKflUp+VSn5VKflUp+VSn5VKflUp+VSn5VKflUp+VSn5VKflUp+VSn5VKflUp+VSn5VKflUp+VSn5VKflUp+VSn5VKflUp+VSn5VKflUp+VSn5VKflUp+VSn5VKflUp+VSn5VKflUp+VSn5VKflUp+VSn5VKflUp+VaoTuepVxn8mrRY2oWtRqXt9VWOxeI8fdHUEG9jwr2XdVdWtbT7k6gVhq3r2ndFdXv7TrAWFd61PV4VwWArCen+ZwlYCrleFretZKwrU0f4a4LAFhVy71eVjWP6LEFiC7l3K76rCsKwFYCsDuiwHosJ6LCeiuKuKuKuKuVyuVyuKuKuKuKwnosJ6LCeiwO6LAVgKwrCrvqu7qu5XhYgsf0WP6LEV3q76rCsAWEf8AD/xAAtEAABAgMGBgIDAQEBAAAAAAABABExUfEQIUFhofAgMHGRscFQgUBg0eGQoP/aAAgBAQABPyH/AMYBIAclgrsDqwUgdFX1XVXVXVXVXVXVXVXVXVXVXVXVXVXVXVXVXVXVXVXVXVXVXVXVXVXVXVXVXVXVXVXVXVXVXVXVXVXVXVXVXVXVXVXVXVXVXVXVXVXVXVXVXVXVXVXVXVXVXVXVXVi09VgfYE+CcfAHJLkbvuwD9Ddyn2I4j8+7BlfoowgQwgfzXhom4fo7hGF4/NPyc2jVRKolUShlgHBGPBiw24FUSqJVErOEr5uC9djOTKiVRKolXGEgF88NEqiVRKolDLAOCMeDFhtwKolUSqJWeNXzcGDIXJlRKolUSjJSYADfzWs6783TczTcO4S4IvBaz34IeyPIzRc1QN/75BaTzdV8Tmm4dwlwReC1nvwQ9keRmi5qgb/3yC0nm6r4nNNw7hLgi8FrPfgh7I8jNFzVA3/vkFpPN1XxOabh3CXBH4LWe/BD2R5GaLmqBv8A3yC0nm6r4bArCrCrCzETVIVIVIVIRgIEtMzVYVYVYQgBBziKkKkKkKULoLpKsKsKsI3BeGN9KkKkKkIQMAAUrCrCrCYJk1SFSFSFSEQABIE5VhVhVhCAEHOIqQqQqQr6BlukqwqwqwjLJOMb6VIVIVIQgYAAvwVYVYVYVYUDkar4nI1Q4dgnwQ+C0HrwR9kOQEDrycQ+Aj24cG66cGo8MDpyNV8TkaocOwT4IfBaD14I+yHICB15OIfAR7cODddODUeGB05Gq+JyNUOHYJ8EPgtB68EXZDkBA68nEPgI9uHBuunBqPDA5Gq+FwbYBiY8EzLXDloE5HE8EzN64XZwFSw9JwTM3dNZxLgG2CYljyZmbLQJyOPBMyy467ODwrRwTM3DtZxwD7BMBx5szM6r9kzVfsmar4XBUCwRjFV0q6VdKulXSrpV0q6VdKulXSrpESSXJIT9IoGAHawKqNVGqjVRqo1UaqNVGqjVRqo0XMp7CGdLl08FVaqtVWqrVVqq1VaqtVWqrVVo3kQiCV99l2mnvB4Ks0dOJ1n+6z/dZ/us/wB1n+6z/dCGh92f2GSrNVmj5aLkTHk6r4XNgmmSCZIJkgmSCZIJkgmSCZIJkgmSCZIIgxuC2SSB9iBTJBMkEyQTJBMkEyQTJBMkEyQTJBMkEKAIiwQdhcEyQTJBMkEyQTJBABzBWbmfNGXYiSZIJkgmSCBBH+9gguDD1TJBQGIFTCphHbdEAO4eSy/b/ENAaOHfrmqYT4NCuYppIdndIpkggEIQ8nVfC5vE7RLsOCy+7NDijEAu3S0oaF40X/ay+7NBDwYLTf8AVhgVsklsMjbeOHBearc3tbm9rc3tA3ZT9BEIAgIghuDWCzYJC0nFpZwQ6qpVUrkEY30XewM6YeC45UcSyRsBbs87NB62aNbuMlhYGIHzBuItGCMRPcCzQeTqvhc3idu0TWar1t2iQszWUMXIYtFGBWySWwyNus+XDlLzLnQp+pMYHE5H+26wWbBIWk3UeAgXUQiCcwTl4F1DIvK7jxW7POzQetmjW7jJYGdw2trbw8V3qzQeTqvhc3idu0TWar1t2iQWQIn6RA0CMCnQaC3GRRCYHKyx0dAwjcQXlYeAhBcIF/gWf5CcW2wALTk5EII7XZrDw4vs2CQUKOiIEsLpFa54CIxi4QRdgGZCJcE9C6J0KlEZ9AXAgg7OMkBcC6ZR/C6P92aD1QIHudULlly1mUzBz3ACzkGCYMYdNkIZwCZwCOG5HHOzQeTqvhc3idu0TWar1tgtjCwe2QNAgBbo6oXMUepBPmCPgou8KZXoIMWREkj/AKIlnAuiBNIzI+QMAEM9E3BsVFXAUYdQTNuTwWIu1BxJC4t1yG8MWifJGGgy3mQUfMEMfA8AoTBFNdu2Cb5kbwCncXjFcmhOAXL/AFQJYkIErBoUIV+AvTiC5PxEVOhEPhKXkSmt0Hk6r4XN4nbtE1mq9bdikUf0uxAoCGx0W9SWn+ChsZq/CWFcmAgwew+y9ye8keiBYghAae4DMpmMghh19IEMeEAvNsMxKDrerTeZBepAXIYGfIpnIBYNoWUaZR9dFDfZrH6CxcG8gmIEfsIBIK0CHdHvBB0c5gCQ1dYywcf0ttwaDydV8Lm8Tt2iazVetsdsYLcViJL2OBRgAhxEZlbHRb5JGx8ngou8Z4E7hdCjEYA7yUXSRdpgRc16MWGjQ9kOORdgjgnUA3E8tHoiPDkRJmvNsMxjojreXTlF7Atc8BbighFgJenBwOdT6aeSOic60Loi8hfBa2ihvs1j9BZFD0IxCzEIy7RIG/qiYKJbjInqLEfube1vi2pxHe3QeTqvhc3idu0TWar1t2iQWQp95f4GwAXRUVifEthkURciFkl9J12C/AZkAwZEfSzFdSIdCxevOc/b0ivRI65IogwIZcRY1pjgZIkk5LmzWPAXUs19nVciHCKNNw9QiPDFNDoIoVxBuyM1iVoPVBi8jqCFygDIDNEIE3GSx0vaFA9rH8O6/r6t0Hk6r4XN4nbtE1mq9bdokLN0kbDArZJLYZG3WfKwdtnOQW5CNgeuYdsijZuATI2XZDaGcdbNYLNgkLdY8CwhYAd5cxVHqj1T6N2CZwBrdnnZoPWzRrdxksvnuOzD2LBlOBiJojZrMoWaDydV8Lm8TtYmwVhjhO4D/LTvBGjQerADEO9isMCtkkthkbdZ8rNhytMnHf0bBTzeos1gs2CQt1jwLN0kePZ52aD1sJ1IuqH/AEqH/SbMEwjuAFh0WM5IrAhwGP3VD/pBXGSiLxh7s0Hk6r4XN4naMidCH/RrYaZy+UQRoUzvF0KASugyJreTzsAWRet/ga2GBWySWwyNus+VhrbD3S0FXayqSLDIITrAAyFggRndSf8ALNYLNgkLdY8CwRkMgZFV4q8VeIgDkI6rdnnZoPXmaDydV8Lm8TtFMx1BRQIojDHhbwDhiTILEizLKwwK2SS2GRt1ny4myqclXBheaQwFmsFmwSFuseByRbPOzQevM0Hk6r4XN4nwMOHjlOokgB4ZpAHIEZJqIJkshYEcL7WCHj9Bv6NpgVsklsMjaJ6CcBBiVRyo5Ucgb9eFHsthu7lBTgU8IMzM26wWbBIW6x4HJFs87NB68zQeTqvhc3ifFo6FlETqL8JgVsklsMjawkmEgmEgmEhxawWbBIWgZguhZV4q8VeKvFXiJgxKOq3Z52aD15mg8nVfC5vE+YYFbJJbzIpjIpjIpjIpjIpjIpjIpjIpjIpjIpjIpjIoTAMRZsEgmTJkyZMmTJkyZbPOzQeqYyTGSYyTGSYyTGSYyTGSYyTGSYyQ9jydV8NjJDsmSHZMkOyZIdkyQ7Jkh2TJDsmSHZMkOyZIdkyQ7Jkh2tYSTCSYSTCSYSTCSYSTCSYSTCSYSTCVjJhIJhIJhIJhIJhIJhIJhIJhIJhIJhIJhIJhIWsJBMJBMJBMJBMJBMJBMJBMJBMJBMJBMJBMJcnVfsmar9kzVfmnyN8ITVeCrwVeCrwVeCrwVeCrwVeCrwVeCrwVeCrwVeCrwVeCrwVeCrwVeCrwVeCrwVeCrwVeCrwVeCrwVeCrwVeCrwVeCrwVeCrwVeCrwVeCrwVeCrwVeCrwVeCrwVeCrwVeCrwVeCrwVeCrwVeCrwVeCrwVeCrwVeCrwVeCrwVeCrwVeCrwTH2DkiX5sJ4H0P8Af0d8NcR7N7/NKzvpUdH/AEcorF9Yjr4/OKQ3gSyn+ih9zcAYkoMjMn5UMCE/gLArqVgUdAnNnnAzCYouwBcf9/QwssnYdU0j4Fc+XRTZ1CDitEdpl6hAvnco/iDMiBTQUcmlhxG4hYgi4o8XHroQjGZISGPdKulXSrpV0q6VdKulXSrpV0q6VdKulXSrpV0q6VdKulXSrpV0q6VdKulXSrpV0q6VdKulXSrpV0q6VdKulXSrpV0q6VdKulXSrpV0q6VdKulXSrpV0q6VdKulXSrpV0q6VdKukXuWf9EcQABuvTV2VjxEb9DOwOJEABHB/BIcMixuyX6iTwp4K/B2H4d7kAZi5fyQUMDoKgY+v0mJroF78LCA6FMBAAAw/Iiq6hYFHS5EwI/aP+gI4Z0Q4nQoh9CMcNGIiD8wDQL9IGQD4R1KBohRP4BBRamTqVBz9fDE0e0jHdlEuFE+MfaOR+0cNeY7IlgijWbm96yu5ZdNohZuxKTw4E6VGVOVIVIWY7LMdlmOyzHZUhUhU5Ubhgo2KDYGfW8QsumSjN77LoSBYgjJkGIkTT+0A4igHhQBDspqHYQAEB/3/wD/2gAMAwEAAgADAAAAEPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPOU4gQQQQQQQQQQQQQQQQQQQQQQQQQQQQQUM9vPPPPPPPPPPPPPKAwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwww4vPPPPPPPPPPPPPIQwwwwyABQ0gAwxwBgxQACQ6wCgwwCgwwwwx/PPPPPPPPPPPPPAAwwww6AFAwgAA1wFw1QAKg6wKAwgKAwwww1vPPPPPPPPPPPPPAAwwxDYgFjSgBjXAEDXQAJDYAJDQgBjQAww1vPPPPPPPPPPPPPAAww6AKQ1QFg1wAgwwAgQwQAwwQKwwwKgww1vPPPPPPPPPPPPPAAww8waA1gVg0wQAw4VQwyASwyAag0AYgww1vPPPPPPPPPPPPPAAwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwww1vPPPPPPPPPPPPPAAww6QAAAAGqQAAAAKssMOOMKwJCADACQww1vPPPPPPPPPPPPPAAwww0CAJJPQVDDAAKaBIIEAKK1KEPIKAww1vPPPPPPPPPPPPPAAwwwwKAHNON5MJKAJ9PBBNLComPHIAKAww1vPPPPPPPPPPPPPAAwwwwKAOEFreJCJHBO0PCFPFVsIFDAKAww1vPPPPPPPPPPPPPAAwwwwKABIP0ZDJHIGSgLMIMCq1KFKAKAww1vPPPPPPPPPPPPPAAwww1ODHCPQVPAPIKaAKABAKK8IAMIKAww1vPPPPPPPPPPPPPAAwww0GNPAPQVPOJIKaAPPKAKKwAAAAKAww1vPPPPPPPPPPPPPAAwwxwAEAANRXPPDDK7DDDHDIJTDDDDIAww1vPPPPPPPPPPPPPAAww4QQQQQQccccccYYQQQQQQYQQQQQYQww1vPPPPPPPPPPPPPDzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzz3PPPPPPPPPPPPPPAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAPPPPPPPPPPPPOOHAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAABPMOPPPPPPPPPNHPPPKAAAAAAAAAAAAAAAAAAAAAAAAAAAAAANPPPLHPPPPPPPPPDLOOPOMPPPPPPPPPPPPPPPPPPPPPPPPPPOOMNNDHHPPPPPPPPPPPPPPPDHPDDDLHPLHPDDDDPDHPPDHHPDHLPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPP/EACoRAAEDAgMHBQEBAAAAAAAAAAEAEWEgMRBxoSFAQbHB4fAwUFGB8YCR/9oACAEDAQE/EP4zAJLBAAf/AAgOwCiUSiUSiUSiUSiUSiUSiUSiUSiUSiUSiUSiUSiUSiUSiUSJbgIE+yUQuJ649pfXsYSzceuLVBGMLqFHAIwM2UKEDGBzhwUKLANjCoUbBwI2WUKMA8GWChRyEioHBG6i/wC2F3Dn4WPOFGs5jA2xs5Ya7Cx5xw5OF3OrSHcxYDryfsv8N93T/wALMP8AS8n7I8M3FP8Awvluy8n7I8Izp/4ToW1eT9l4P2T/AMJ/4X0kryfsn3Gfan/hfHdv1eT9l8pk/wDCE1/peT9ltWxmo0h3MWMsOlQOThe+ueGqw0lN3PDTYeDLDn4WcsOlRpDuYEgKkCcPlgEBJAn+OBCApAh8iNmDwFIFIMSyLhSBHGXDA0AqQI77AKIKkCcPlRpD7kNIdzBBsngplMplMpkYuFGBsVMplMplMiJ2oQBMVMplMplMjEOQjBUymUymUyMk3+aNIdzHLrF6rpK5VYnkyqu50aQ7mOXUe2t9JXKrK0u50aQ7mOXVcV6suVWGsXc6NIdzHLrF6rpK5VYnkyqu50aQ7mOXWL1XSVyqxPJlVdzo0h3McusXqukrlVieTKq7nRpDuY5dYvVdJXKrE8mVV3OjSHdAyZMmTekyZMmTeppD64KR2+xySSSSSSSSSSSSSSSSEmAeH9+f/8QAKhEAAQIDCAMBAQADAQAAAAAAAQARYZGxECAhMaHB0fBBUXEwgVCA8eH/2gAIAQIBAT8Q/wBMyIjAI4wx78n567kiTmP9KjZlRsyo2ZUbMqNmVGzKjZlRsyo2ZUbMqNmVGzKjZlRsyo2ZUbMqNmVGzKjZlRsyo2ZUbMqNmVGzKjZlRsyo2ZUbMqNmVGzKIuQf0o2PqeRz3FApHB/cwYueJ2G8v8G8L0QP/o/cziFBexjDBlF+FHaIqYuWxsdGwZ8P4o7REykhvXkgWCJFgTKO0QyHgCfHgWBR2ijtEED4JFhFmCAPhR2iKk4DagGx2MFyMFHaInYuHx+3iFj0r++u2F7J082a7azP+qiyios1os1ylgzt1prZp1LKaiyo2s7UTe0Cv767YXDlyRbJ9wndOUDlOwab5pnThHKDyPm/iCd05QzMEzZ+Xx9ekzpwiOBHO7Njkw9e07pygf2LMztFzRM6cJni+Z8nwfJO6cpwx6TTOnCZ04T/AAczNk+PtO6cpn9ydnbyzGqZ04RyUOzs2Gbj16TunKGd9ru2cMfXtM6cI5M+Bs4w9p3TlfRnhsmifdzQK/vrthc60BZm6eLNNvZQb2VVDZoTZplbDlbpRSzWq2V1TZSb2dKAs6P5c0Cv767YXDoCSXwb0IrpDlYfEYs4txYBIlw2C6Q5TywLg4tGNg83AfKIIXSHKOMcG8c2ANZAgyK6Q5R/4XNpY4QA8crpDlZaxE2GzEh8m8kn2ukOU1sgMBjYdoXL4N6+rpDlY/Bizby3FzQK/vrth/g9Ar++u2FwyIDi8QChZBQsgoWQULIKFkE38DE5fxBBAcT4UFIKCkFBSCgpBQUgibTf8IxgJx8RKhZBQsgoWQULIKFkE1hsCiAAnHxEqBkFAyCgZBQMgoGQQFBsNzc0Cv767YXM36oL2pOyyfpvZunhV9Te0p2VVU3u1E3NAr++u2FzN+qC8Av+zsmAG9m8GfrJV9TeHGgULf0qb3aibmgV/fXbC5m/VBe1B2WT9N4HAiOyFg/am9oTVG5vtTe7UTc0Cv767YXM36oL2pOyyfpvEQenhESUxqbxliB2VVU3u1E3NAr++u2FzN+qC9qTssn6b2bp4VfU3tKdlVVN7tRNzQK/vrthczfqgvak7LJ+m9m6eFX1N7SnZVVTe7UTc0Cv767YXM36oL2pOyyfpTp06dOs3Twq2pvaU7KqqU6dOnTrpRNzQK/vrthcdOU5TlOU5tdOnTp7HTlOU5TlObHTlOU5TlObugV/d4ACYevqiZjlRMxyomY5UTMcqJmOVEzHKiZjlRMxyomY5UTMcqJmOVEzHKiZjlRMxyomY5UTMcqJmOVEzHKiZjlRMxyomY5UTMcqJmOVEzHKiZjlRMxyomY5UTMcqJmOVEzHKiZjlRMxyomY5UTMco6hgR69/f8Afz//xAAtEAABAgMGBgMBAQEBAQAAAAABABExUfAhQWGRwfEQIDBxgaFAsdFg4VCQoP/aAAgBAQABPxD/AOMAiABaSSwCLFyms/0j9gjIWRL+lbtW7Vu1btW7Vu1btW7Vu1btW7Vu1btW7Vu1btW7Vu1btW7Vu1btW7Vu1btW7Vu1btW7Vu1btW7Vu1btW7Vu1btW7Vu1btW7Vu1btW7Vu1btW7Vu1btW7Vu1btW7Vu1btW7Vu1btW7Vu1btRCzCA6txY6bEGDzh/wG0we1bkybHWZ/g7Jg3i491DAPg/z55SltGxGf8AC3QURMSRd3A4+ac0m99/n8OYo0BjWPzbfuF1CQASSwEeUoIIIfYhdwBgQeSwzgY2DAseQIIIIQxgMFpB25Bk5gzg5gHPIEEETDjKJJAchIAcwHKEEEEPswu4CYPISBwBjYMCx5AggrqQslpB27Hktd4GcHMA55AggnwuHCUgOq1rhJZPp82nx6ntfpGPJTZOSu48lGlyKpLkev8Aoq7j7j65aNLkqcjyQd3IxaY8lfxV3UpMPm02PU9r9Ix5KbJyVXHko0uRRJcj1/0Vdx9x9ctGlyVOR5IO7kYtMeSv4q7qUmHzabHqe1+kY8lNk5KrjyUaXIokuR6/6Ku4+4+uWjS5KnI8kHdyMWmPJX8Vd1KTD5tNj1Pa/SJDxTiacTTiapsnJTcU4mnE04mqNLkVSSOJpxNOJo8v9FCHH3H0nE04mnE04mqNLkqcinE04mnE1aHdyNg6YpxNOJpxNHXzV3UpMPm02PQMgoJBfaO4Rd1q3at2rDlvzQZ0q2YtmLZiFRgAAEAE3at2rdqEgjYOYia2YtmLZixRfvrZFu1btW7UQ8L0xOIzLZi2YtmIWMGxAAItC3at2rdqwfL81s1bMWzFsxCgwQAIAWrdq3at2oTiNg5iJrZi2YtmLFUOvvFbtW7Vu1CJEJJxmWzFsxbMQuIZIAELdq3at2oOidOvQHQpMPm02PQ9v9hGPGtTQh0LqDMclWnyKZPke5+wr+Ppvvlqk+SnzHJ6PIweVfv4iI7r0H10KTD5tNj0Pb/YRjxrU0IdC6gzHJVp8imT5HufsK/j6b75apPkp8xyejyMHlX7+IiO69B9dCkw+bTY9D2/2EQXgmMkxkhNVFCHIKcxkmMkxkqDMclSmjGSYyTGSrk+R7n7CYvBMZJjJAfG++UaS9MZJjJMZKnzHJ6KWMkxkmMlB5V9i8ExkmMkAXHdegProUmHzabHoXb1QBIrbT+rbT+rbT+oEAgZBcWH95TzJCi5Ik2rbT+rbT+rbT+pvklcXMTyF0AkGBii1uAW2n9W2n9W2n9RYZybl3REcByXblyBIrbT+rbT+rbT+oCIIfFsD+8pp1hRcjEm1baf1baf1baf1DoNK4uYnkvOWwMTO1uAW2n9W2n9W2n9VszTMuxiI8hLLoUBuK20/q20/q20/q20/qAYMIdCkw+bTY/w9Jh82mx+OxkUxkUxkUxkUxkUxkUxkUxkUxkUxkUxkUxkfg+F45/C8Lx0qTD5tNj0AlKSYHF7Qq71Vd6qu9VXeqrvVV3qq71Vd6qu9VXeqrvVO1ftHDFALkkg5R2+QwyITi5VDqqh1VQ6qodVUOqqHVVDqqh1VQ6qodVUOqDZOCLjF78BChzPZBjtFVzqq51Vc6qudVXOqrnVVzqq51Vc6qudVXOqJOXDAEXBtRie6Fo5lsUWOxtVY6oRFkhm90XDzd73veVkyDJHEilcfVY6qsdUGroaAkQTb0aTD5tNj0ABAQCL3tWzLZlsy2ZbMtmWzLZlsy2ZbMsCRuVQkQgSAQ2KtmWzLZlsy2ZbMtmWzLZlsy2Zeq1iODxAGeEbMtmWzLZlsyMhhXsLU9d4hRgYKLAStmWzLZkLl1hRie6aMM4Ypsy9AjEKitFRWiIwYcNhA9ooPBxC9l26qhREjWBiIXqitEZcBYeYGNyNbKsiJA/p1syDKBGSruhSYfNpsehWZeIrhkkcAEg2EvEHhiGJEwbDjxFojiGIUM7OAJDEDeOGIGgEK7IEgMRMAeHoFVCReh4wB/kFAHIgQMQeOLERrReKAOWIZiomFGIdwbeSjTHLsdLggKRwI2qrdEQAk2NMEywMxiCGwV5xRjzuT8b3DxZkRf0Hsc5LMAONelRie6r0+CgTHH3+HP8AAcAWdAjAglBGCCWw8qUfZ4UzFXdCkw+bTY9Csy8L+IGvy5b8BMsHDdxGltUC58V6BVQkXoeEI8rBkZWLYjOy2jNTM6JIM+r2Bjwo0xy7BuCZIDrADguiAQG8MgJsdWggRhr2RAAhi7GCEgpsJJCGPCvSoxPdV6fBQJjj7/DtimR4EnwAT4QRghmgIGBF7RwKZiruhSYfNpsehWZeF/EDX5cl+bLkInAJtkMYaq9JegUNkPEIFxG9xZsU7zIAVpfYLogkXEDEcAJRCRE5ox7oywP2InuSWbvdxweq4GQN7zEe4T3EFO7Fn8hj5TDtuEgHTnICaC0RF/HYUIOyJuHlHaQA5YLBTpDisKwgGWB7RG9AjqLOFb5KtJsvDw9pyNndxDnFAYDWdtRQLYYFWt3RVZkSYLKzNnGHtN+YIXEKMT3VsYqKAhMGDwE6MBYMREX8bo+Vzo7BrkHLWu34Ra9isNGLXNpmbpW8DpB2R2wdywTyx5oi5OZ4UzFXdCkw+bTY9Csy8L+IGvy4ydk4BGmbEIgPMzId+0gjgjEACD4or0kIt2MJvL8QDwSRBysW4ILAgg8SAROGXrOb3Mgrdh7UEyCOckngE5CYDAsTfLFGFLIjoC1ll5RJykYE4exRC+YTFFiyBIEDcU8IpAVYEAzOHDgC6yaM5ggKGINoIvdkW5wGQADkt9BE5AiQbMQ9lxTEiFyTIwqJAMkIcAJyMC0SVaq4EK2a9yt7BIBJAsPijnwRAReWAogFYCEAaZ4oMEA5AQQtFZnawY4koCUgoBYQJ4hOIxiwHPgAhLVZIg9mwiaNjx2NhwQWBzwS1xQYsG7Th3NyJ2mICLGcOfCIkHoco0dOwDsJnAWp8YizQTHaAutvPGmYq7oUmHzabHoVmXhfxA1+XHco/ipmQ3ZEAUwIYBFk0xAyRVUGVNtYKnd4MokEgIAkDHDuisMhICL9EF5l5nZ6RjhiC4R8tjF7H6g0S4YfsFH2FJMCICQYkh7BXv8A3XoPoL6f0qzJPrP3w9yu7RFHKydYY6AZWlmToIvcP0Vn+B/3/wBKsyI1u6buEgN0ehxY/wCET9yQCvtFgw4nzMZgAYvIhAbHz8kSHFpe0W9xwFdGg5ZJAHcP5L1340zFXdCkw+bTY9Csy8L+IGvy4xa5WhkzJTix2BixY3YyQtYQzC5r83BC6OCC8RVUeVEAFiDBR2OAubsb3wKeOTDdYqJ1EHIKC4mOSLidxR1EQAseReYEMhHAQiDFoiFpYCWQAADuz4r6z9IVJ/cA4SRPZkaMmgiRLknyvf8AuvQfQTiBYSQfwh3MLmCWFrBOCMgYgO5PngFvB7SRDadi1gIusEfoQSSwB2ACI3xKNxzFViu1hIDtxSigJrgeMw2m+PCvv/pVmRGt3QQJYcQtcEaaDiRie4Q6MpgQIIYjgA71mnLgxhh2Xvrm7NtwyfBbwQLh+ALjTiwOOQ7wRxpmKu6FJh82mx6FZl4X8QNflyX5mAJRCsRaAIFnDbkYLQQ4KIkguSJnYXoVPgmBQMbSpkU4hIYAgAAgEFyQXGLXoBcHBgAhdeR2FvlMGci8EXIG4y9MCJvGF7xJCxmY0EFOGDAAMAhfLktQdxaiHckkkl3xR8G9rY8COCiRJPAHGU5uggJG1AAQSJCD3cJXXGwkhEBVu47ABkDjAgUeYEQABgEPIeZ0IVa4S5JRscR1kAAABBoo6nLoAHsIYMbGvQAXJe+xe+vGwwUzgkSQ9jD3fgJ9TkWiTsDJuJTMVd0KTD5tNj0KzLwv4ga/LnnzfoFVCReh4QjxYDxpQGICx3BmVuf4TCFraLzABB7uFjwYQDj74NTAYQCRAjwJbHhRpjn2VzFxQAuIbD2HIsttYLFoEsBYLoca9KjE91Xp8FAmOPv8O7NRrcA4T8+wcAEzBUAIYhDKNoJijys7g8KZiruhSYfNpsehWZeIBUTN4/TgIYLkPZI4jwoAqmBqFwMqDDs5++HoFVCReh4QjxYUGfCIolES4A8gIem4CiRkdgTXhRpjrbK5uvSoxPdV6fAMjAJeC/GrVsZvUADo7EiI4HliwLguDmFYstsIsLWeB+FUMoqQR4CbQch4UzFXdCkw+bTY9Csy8QNmAjEjY5cAFK1PDxBdrjgShJoSAJYkAYhHRMAQHvJJj2FpTl5BAoleWJLnzwC4IARcQAA5nB6BVQkXoeEI8WAdaMdoi12hAqu9E8gwaOvL0mUBMrAJ2gGHBy8XMJRbIM+FGmOfZXM43mMT41UWiqLRVFoioAWkkAHrjXpUYnuq9PqKZiruhSYfNpsehWZeJCDoGMMGMCMQEBP0jYBqHLajGYcFMJNiOAHoAbAgAwAAHjh6BVQkXoeEI87AQgyd9CZJsAvJQ2XUHu1Z4AAOFGmOnsr0SSMeFelRie6r0+opmKu6FJh82mx6FZl5C6CgzG8t70faAoctKBmVth8EoLNUCThEQfiEHtR+BIPOMIefCEGPL4977CfDcfQKqEi9DwhFNmGlE4Eqr9VV+qq/VFoUA+7VkE7RtYtAe1bMAEvTPYYXAcaNMdPZXokkY8K9KjE91Xp9RTMVd0KTD5tNj0KzLym0MbQgjCpHD0V7Sr7cvoFVCReh4xJED4WwLYFsCAAgAPHLRpjl2NocWOHoqotVUWqqLVVFqqi1REQLCCYH3xr0qMT3Ven1FMxV3QpMPm02PQpMqYyOSYyOSYyOSYyOSYyOSYyOSYyOSYyOSYyOSYyOSYyOSAwDA3KoSISWgTZgraFtC2hbQtoW0LaFtC2hbQtoXpJYjgJLQWOkck6RyTpHJOkck6RyTpHJOkck6RyTpHJOkck6RyQGGe1hRie6ElkE4WKYjJYjJYjJYjJYjJYjJYjJYjJYjJYjJYjJGhnsd1d0KTD5tNj0CAQxAIxWzFsxbMWzFsxbMWzFsxbMWzFsxbM4EAxAKwmSwmSwmSwmSwmSwmSwmSwmSwmSwmSwmSwGXAgYgHuFti2xbYtsW2LbFti2xbYtsW2LaOBAMQD3C2hbQtoW0LaFtC2hbQtoW0LaEAQDLo0mHzabH+HpMPm02P8AD0mHzT+UAEADg1qrfVVvqq31Vb6qt9VW+qrfVVvqq31Vb6qt9VW+qrfVVvqq31Vb6qt9VW+qrfVVvqq31Vb6qt9VW+qrfVVvqq31Vb6qt9VW+qrfVVvqq31Vb6qt9VW+qrfVVvqq31Vb6qt9VW+qrfVVvqq31Vb6qt9VW+qrfVVvqq31Vb6qt9VW+qrfVVvqq31Vb6qt9VW+qrfVVvqq31Vb6qt9VW+qrfVVvqq31Vb6pqlGfAtG1uyu+YcAkxJZA/hxLVYbMYew+aAX6BgWm0wdy9F2h/DN2WCm8jn2sDy+aQCCCAQbij2GwCw4kxF2DSP8LaCq2FjyQ9lhemleB7XnElycT8qP5J3OQVjm+zPaN9wH8UEBw9UO3Zw9q6kEJ/4z1iZYMv4ONDa4MyuCOSARGMCQS8qEmGEVZnbyS1VjYswEdAFK629oEA4IIN4+IDgbDAHCaKFyVwtkHMI7rPwMQixSW23gMR7RGDLoByt9JsERTBUDoqB0VA6KgdFQOioHRUDoqB0VA6KgdFQOioHRUDoqB0VA6KgdFQOioHRUDoqB0VA6KgdFQOioHRUDoqB0VA6KgdFQOioHRUDoqB0VA6KgdFQOioHRUDoqB0VA6KgdFQOioHRUDoqB0VA6KgdFQOioHRUDoqB0VA6KgdFQOioHRUDoqB0VA6KgdFQOioHRAxYOU3IeAY3IDMgr6CrUnuVg8AoZD9pERTJiT35nNhwNmUEGA8hYjuPxAwBOCLx8EBCgQyMTwwA9mMuk5mc05mc05mc05mc05mc05mc05mc05mc05mc05mc05mc05mc05mc05mc05mc05mc05mc05mc05mc05mc05mc05mc05mc05mc05mc05mc05mc05mc05mc05mc05mc05mc05mc05mc05mc05mc05mc05mc05mc05mc05mc05mc05mc05mc05mc05mc05mc05mc05mc05mc05mc05mc05mc05mc05mc05mc05mc05mc05mc05mc05mc05mc+kNg5vHYHlAHQJ5n4ZslHFekK0kHcewrXBVxZZ75Nv4k03eQqSEwBGW4I57KaCTFeT+oICAABAAQ+R6R0p1cg3mfonR9wA/YQ7ymRQb+WcaKEdg6qOk8D9L3UEjADuGTiY/6riYQBMAT2C9fBo/Z5Q32o32ihT4M5OiuQMP1K9AJA0UycYqLJMCgAAw/4hAIYh1EF3FRw+KiQ+zj6UE7A0RA9j/KLg+7HRG6D3FXI8/6Rucyjdk7uERwP5fiIYF7f5RnvgiGft+yIdMj9RF/j+ogUI/2IjjmkRxzCII5hM/vW8Vu1bsW+lvJbyW8luxbtW8VutAkM0gaGcQLDPIH/AEIGXBfxCBv3H6geXv8AssO8EDRH3QFiPz/EDRHmdELzNLRj/SF/4Q/UDEdmGiDj3wjRRDuiUCN3JOq0U0IY9hQBgDsP/f8A/9k=",
    	"CURRENCY":"RUB",
    	"SORT":"500",
    	"ACTIVE":"Y",
    	"CONFIG":[
    		{
    		"CODE":"SETTING_1",
    		"VALUE":"SETTING_1 value"
    		},
    		{
    		"CODE":"SETTING_2",
    		"VALUE":"SETTING_2 value"
    		}
    	]
    }
    
    • Символьный код обработчика (REST_CODE) обязателен.
    • Логотип с изображением службы доставки можно передать в виде base64-encoded строки.
    • При создании конкретного экземпляра доставки можно указать значение параметров конфигурации (CONFIG).

    В случае успешного создания службы доставки будет получен её числовой идентификатор. Пример ответа:

    {
    	"result":{
    		"parent":{
    			"ID":"622",
    			"PARENT_ID":null,
    			"NAME":"Uber Taxi",
    			"ACTIVE":"Y",
    			"DESCRIPTION":"Uber Taxi Description",
    			"SORT":"500",
    			"CURRENCY":"RUB",
    			"LOGOTYPE":"954"
    			},
    	"profiles":[
    			{
    			"ID":"688",
    			"PARENT_ID":"622",
    			"NAME":"Taxi",
    			"ACTIVE":"Y",
    			"DESCRIPTION":"Taxi Delivery",
    			"SORT":"500",
    			"CURRENCY":"RUB",
    			"LOGOTYPE":null
    			},
    			{
    			"ID":"689",
    			"PARENT_ID":"622",
    			"NAME":"Cargo",
    			"ACTIVE":"Y",
    			"DESCRIPTION":"Cargo Delivery",
    			"SORT":"500",
    			"CURRENCY":"RUB",
    			"LOGOTYPE":null
    			}
    		]
    	},
    	"time":{
    		"start":1642404734.307061,
    		"finish":1642404734.582061,
    		"duration":0.27500009536743164,
    		"processing":0.08100008964538574,
    		"date_start":"2022-01-17T09:32:14+02:00",
    		"date_finish":"2022-01-17T09:32:14+02:00"
    		}
    }
    

    В ответе возвращается корневая служба доставки, а также все созданные профили. Корневая служба доставки используется как контейнер для служб доставки конкретных профилей (их список указывался при создании обработчика). В процессе настройки (настройка дополнительных услуг, привязка свойств и т.п.) понадобятся идентификаторы профилей корневой службы доставки. Получить список созданных профилей можно либо из ответа после создания корневой службы, либо позже (например, методом sale.delivery.getList):

    {
    	"FILTER":{
    		"PARENT_ID":622
    	}
    }
    

    Пример ответа:

    {
    	"result":[
    		{
    		"ID":"688",
    		"PARENT_ID":"687",
    		"NAME":"Taxi",
    		"ACTIVE":"Y",
    		"DESCRIPTION":"Taxi Delivery",
    		"SORT":"500",
    		"CURRENCY":"RUB",
    		"LOGOTYPE":null
    		},
    		{
    		"ID":"689",
    		"PARENT_ID":"687",
    		"NAME":"Cargo",
    		"ACTIVE":"Y",
    		"DESCRIPTION":"Cargo Delivery",
    		"SORT":"500",
    		"CURRENCY":"RUB",
    		"LOGOTYPE":null
    		}
    	],
    	"time":{
    		"start":1638544721.243672,
    		"finish":1638544721.621672,
    		"duration":0.37800002098083496,
    		"processing":0.019999980926513672,
    		"date_start":"2021-12-03T17:18:41+02:00",
    		"date_finish":"2021-12-03T17:18:41+02:00"
    	}
    }
    

    Видно, что в процессе создания корневой службы доставки создались две дочерние службы для профилей с идентификаторами 688 и 689. Именно они и будут использоваться для дальнейшей настройки. Корневая служба является лишь контейнером для служб доставок профилей.

    Если в дальнейшем понадобится указать настройки или загрузить кастомный логотип для конкретной службы доставки профиля, необходимо воспользоваться методом sale.delivery.update.

    Описание остальных методов для работы со службами доставок можно посмотреть в разделе Службы доставки.

      Настройка дополнительных услуг службы доставки

    Если тот или иной профиль подразумевает наличие дополнительных услуг, их можно добавить с помощью метода sale.delivery.extra.service.add.

    • Пример добавления дополнительной услуги типа checkbox (да / нет) "Доставка до двери":
      {
      	"DELIVERY_ID":688,
      	"ACTIVE":"Y",
      	"CODE":"door_delivery",
      	"NAME":"Door Delivery",
      	"TYPE":"checkbox",
      	"PRICE":59.99,
      	"SORT":100
      }
      
    • Пример добавления дополнительной услуги типа enum (список) "Тип груза":
      {
      	"DELIVERY_ID":688,
      	"ACTIVE":"Y",
      	"CODE":"cargo_type",
      	"NAME":"Cargo Type",
      	"TYPE":"enum",
      	"ITEMS":[
      		{
      		"TITLE":"Small Package(s)",
      		"CODE":"small_package",
      		"PRICE":129.99
      		},
      		{
      		"TITLE":"Documents",
      		"CODE":"documents",
      		"PRICE":69.99
      		}
      	],
      	"SORT":100
      }
      

    При создании дополнительной услуги можно указать символьный код (CODE), чтобы затем идентифицировать выбранные дополнительные услуги при получении запросов к веб-хукам расчета стоимости доставки и создания заказа на доставку. В ответе возвращается числовой идентификатор созданной дополнительной услуги. Его также можно использовать как альтернативу символьному коду для идентификации выбранных дополнительных услуг при запросах к веб-хукам.

    Полный список методов для работы с дополнительными услугами можно посмотреть в разделе Дополнительные услуги.

      Работа со свойствами отгрузки

    Если для расчета стоимости доставки и/или создания заказа на доставку необходимо предоставлять дополнительную информации, то сделать это можно путем прикрепления свойств к службам доставки профилей. Ниже приведён пример прикрепления двух свойств типа "Адрес" (Откуда и Куда) и свойства типа "Строка" для передачи текстового комментария. Для создания свойств отгрузки можно воспользоваться методом sale.shipmentproperty.add.

    Пример создания свойства для указания адреса отгрузки:

    {
    	"fields":{
    		"personTypeId":"3",
    		"propsGroupId":"11",
    		"name":"Address From",
    		"active":"Y",
    		"sort":"100",
    		"description":"",
    		"type":"ADDRESS",
    		"required":"Y",
    		"isAddressFrom":"Y"
    	}
    }
    

    Пример создания свойства для указания адреса доставки:

    {
    	"fields":{
    		"personTypeId":"3",
    		"propsGroupId":"11",
    		"name":"Address To",
    		"active":"Y",
    		"sort":"100",
    		"description":"",
    		"type":"ADDRESS",
    		"required":"Y",
    		"isAddressTo":"Y"
    	}
    }
    

    Пример создания свойства для указания комментария:

    {
    	"fields":{
    		"personTypeId":"3",
    		"propsGroupId":"11",
    		"name":"Comments",
    		"active":"Y",
    		"sort":"100",
    		"description":"",
    		"type":"STRING",
    		"required":"N"
    	}
    }
    

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

    Тип плательщика (personTypeId) можно получить методом sale.persontype.list. Свойство нужно создавать отдельно для каждого требуемого типа плательщика (физ. лицо, юр. лицо).

    Группу свойств (propsGroupId) можно получить методом sale.propertygroup.list.

    Каждое созданное свойство теперь нужно привязать к службе доставки профилей. Сделать это можно с помощью вызова метода sale.propertyRelation.add.

    Пример привязки:

    {
    	"fields":{
    		"propertyId":"437",
    		"entityId":"688",
    		"entityType":"D"
    	}
    }
    

    В данном примере привязывается свойство с идентификатором 437 к службе доставки профилей с идентификатором 688.

    Если планируется создать несколько служб доставки, которые разделяют некий общий набор свойств (к примеру, им всем нужны адреса "Откуда" и "Куда", а также комментарий), то создавать отдельные свойства для каждой службы доставки не требуется. Можно просто привязать новые службы доставки к уже существующим свойствам. Получить список свойств отгрузки можно с помощью метода sale.shipmentproperty.list.

    На этом процесс установки и настройки завершен. Следующим этапом рассмотрим процесс использования установленной и настроенной службы доставки.


    Процесс использования службы доставки в сценариях центра продаж

    Рассмотрим один из полноценных типовых сценариев работы REST служб доставки - работу с доставкой в центре продаж. Стартовой точкой для менеджера тут может являться функционал принятия оплаты («Принять оплату в сделке») или создание дела на доставку.

      Работа со стороны менеджера

    Предварительный расчет стоимости доставки

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

    При расчете доставки отправляется запрос на URL, указанный в значении свойства CALCULATE_URL обработчика службы доставки. В запросе передаются все необходимые для расчета стоимости параметры (свойства отгрузки, вес, стоимость товаров, требуемые дополнительные услуги, значения настроек службы доставки и т.д.). В ответе служба доставки должна сообщить предварительную стоимость доставки в валюте заказа. В случае, если расчет стоимости невозможен, служба доставки должна сообщить текст ошибки, который будет показан менеджеру (см. веб-хук предварительного расчета стоимости доставки).

    Если обработчик службы доставки поддерживает возможность создания заказов на доставку и их дальнейшего отслеживания (значение параметра HAS_CALLBACK_TRACKING_SUPPORT у обработчика выставлено в "Y"), то после создания отгрузки будет создано [dw]дело[/dw][di] [/di] на доставку данной отгрузки.

    Создание заказа на доставку

    Посредством дела на доставку менеджер имеет возможность инициировать процесс создания заказа на доставку. При клике на Заказать доставку ([dw]Request Delivery[/dw][di] [/di]) отправляется [dw]запрос[/dw][di] [/di] на URL, указанный в значении свойства CREATE_DELIVERY_REQUEST обработчика службы доставки. В запросе, помимо информации о самой отгрузке, присутствует информация о контактах отправителя и получателя для связи с ними.

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

    После успешного создания заказа на доставку дело на доставку будет выглядеть следующим образом:

    Отмена заказа на доставку

    Менеджер имеет возможность в любой момент попытаться отменить ранее созданный заказ до тех пор, пока он не был завершен службой доставкой. При клике на Отменить заявку (Cancel Request) отправляется запрос на URL, указанный в значении свойства CANCEL_DELIVERY_REQUEST обработчика службы доставки. В случае, если заказ может быть отменен, служба доставки отменяет его и сообщает об успешной отмене в ответе. Дело на доставку в этом случае возвращается в начальный вид, и менеджер имеет возможность повторно оформить заказ на доставку. В случае, если отмена заказа невозможна, служба доставки должна сообщить причину, по которой отмена невозможна. [dw]Причина[/dw][di] [/di] будет выведена менеджеру при попытке отмены заказа (см. веб-хук отмены заказа на доставку).

      Работа со стороны службы доставки

    Обновление заказа на доставку

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

    Сразу после создания заказа на доставку требуется как минимум обновить его статус, чтобы менеджер понимал, что в данный момент происходит с заказом. Например, если поиск исполнителя по заказу занимает некоторое время, то можно сообщить об этом менеджеру путем обновления [dw]статуса заказа[/dw][di] [/di] методом sale.delivery.request.update:

    {
    	"DELIVERY_ID":723,
    	"REQUEST_ID":"4757aca4931a4f029f49c0db4374d13d",
    	"STATUS":{
    		"TEXT":"Searching performer",
    		"SEMANTIC":"process"
    	}
    }
    

    Предположим, что исполнитель заказа найден, и теперь требуется зафиксировать в заказе [dw]новый статус и информацию об исполнителе[/dw][di] [/di]. Для этого можно воспользоваться функционалом набираемых свойств для заказа на доставку (метод sale.delivery.request.update):

    {
    	"DELIVERY_ID":723,
    	"REQUEST_ID":"4757aca4931a4f029f49c0db4374d13d",
    	"STATUS":{
    		"TEXT":"Performer found",
    		"SEMANTIC":"process"
    	},
    	"PROPERTIES":[
    		{
    		"NAME":"Car",
    		"VALUE":"Gray Skoda Octavia, a777zn"
    		},
    		{
    		"NAME":"Driver",
    		"VALUE":"John Smith"
    		},
    		{
    		"NAME":"Phone Number",
    		"VALUE":"+79097996161",
    		"TAGS":[
    			"phone"
    			]
    		}
    	]
    }
    

    Набор свойств может быть произвольным. Если требуется, чтобы значение свойства было обработано особенным образом, то можно отметить его тегом. В примере выше значение номера телефона исполнителя отмечено тегом "phone". Это позволяет вывести его как ссылку и открывать либо через IP телефонию (если она настроена у клиента), либо стандартными средствами браузера.

    Допустим, служба доставки передала заказ исполнителю (отгрузила заказ), и теперь нужно [dw]обновить его статус[/dw][di] [/di] методом sale.delivery.request.update:

    {
    	"DELIVERY_ID":723,
    	"REQUEST_ID":"4757aca4931a4f029f49c0db4374d13d",
    	"STATUS":{
    		"TEXT":"Parcel on its way",
    		"SEMANTIC":"process"
    	}
    }
    

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

    • добавление новых свойств и значений (OVERWRITE_PROPERTIES = N, это режим по умолчанию)
    • перезапись всего набора значений свойств (OVERWRITE_PROPERTIES = Y)

    Когда заказ на доставку завершен, то необходимо его финализировать методом sale.delivery.request.update:

    {
    	"DELIVERY_ID":723,
    	"REQUEST_ID":"4757aca4931a4f029f49c0db4374d13d",
    	"FINALIZE":"Y"
    }
    

    Дело на доставку в этом случае переходит в [dw]статус выполненного[/dw][di] [/di], и его отмена со стороны менеджера уже невозможна.

    Отправка сообщений по заказу на доставку

    Предположим, что в процессе выполнения заказа на доставку необходимо оповестить ответственного за доставку менеджера или грузополучателя о том или ином событии. Допустим, требуется сообщить клиенту о факте того, что посылка передана в службу доставки, и о времени доставки (метод sale.delivery.request.sendmessage):

    {
    	"DELIVERY_ID":723,
    	"REQUEST_ID":"4757aca4931a4f029f49c0db4374d13d",
    	"ADDRESSEE":"RECIPIENT",
    	"MESSAGE":{
    		"SUBJECT":"We will soon deliver your parcel to you!",
    		"BODY":"Your order has been dispatched and will be delivered to you in 23 minutes. Thank you!",
    		"STATUS":{
    			"MESSAGE":"Dispatched",
    			"SEMANTIC":"success"
    		}
    	}
    }
    

    [dw]Оповещение[/dw][di] [/di] будет отправлено клиенту по одному из каналов связи, настроенных в CRM. Это может быть SMS-оповещение или сообщение в один из мессенджеров (например, WhatsApp). Для отправки сообщения менеджеру необходимо установить адресата сообщения (ADDRESSEE) в значение "MANAGER".

    Отправка сообщений по заказу на доставку

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


    Примеры

    Пример создания собственной службы доставки

    Чтобы добавить собственную службу доставки, следует создать файл handler.php и в нем написать класс <Ваше_название>Handler для вашей службы доставки. Файл должен быть размещен в отдельной директории <ваше_название> в /local/php_interface/include/sale_delivery/ или в том разделе, который задан в настройках модуля Интернет-магазин с помощью параметра Путь к собственным обработчикам расширенных систем доставки (по умолчанию используется /bitrix/php_interface/include/sale_delivery/). Из данного файла система подключает вашу службу доставки автоматически.

    Например, добавим службу доставки, зависящую от веса.

    Для этого файл handler.php расположим в директории /bitrix/php_interface/include/sale_delivery/custom/ и в нем составим описание для класса соответственно CustomHandler. Класс наследуем от базового класса служб доставки:

    <?
    namespace Sale\Handlers\Delivery;
    
    use Bitrix\Sale\Delivery\CalculationResult;
    use Bitrix\Sale\Delivery\Services\Base;
    
    class CustomHandler extends Base
    {
    	public static function getClassTitle()
    		{
    			return 'Доставка по весу';
    		}
    		
    	public static function getClassDescription()
    		{
    			return 'Доставка, стоимость которой зависит только от веса отправления';
    		}
    		
    	protected function calculateConcrete(\Bitrix\Sale\Shipment $shipment)
    		{
    			$result = new CalculationResult();
    			$price = floatval($this->config["MAIN"]["PRICE"]);
    			$weight = floatval($shipment->getWeight()) / 1000;
    		
    			$result->setDeliveryPrice(roundEx($price * $weight, 2));
    			$result->setPeriodDescription('1 день');
    		
    			return $result;
    		}
    		
    	protected function getConfigStructure()
    		{
    			return array(
    				"MAIN" => array(
    					"TITLE" => 'Настройка обработчика',
    					"DESCRIPTION" => 'Настройка обработчика',"ITEMS" => array(
    						"PRICE" => array(
    									"TYPE" => "NUMBER",
    									"MIN" => 0,
    									"NAME" => 'Стоимость доставки за грамм'
    						)
    					)
    				)
    			);
    		}
    		
    	public function isCalculatePriceImmediately()
    		{
    			return true;
    		}
    		
    	public static function whetherAdminExtraServicesShow()
    		{
    			return true;
    		}
    }
    ?>
    

    В классе определяем необходимые нам методы. Так, методы getClassTitle и getClassDescription содержат название и описание службы доставки. Метод calculateConcrete вызывается при расчете стоимости доставки, принимая в качестве параметра отгрузку. Метод getConfigStructure описывает параметры, которые необходимо спросить в интерфейсе у администратора.


    Примечание: если класс с вашей службой доставки имеет произвольное название, лежит в произвольном файле, то для подключения вашей службы в файл init.php необходимо добавить следующий код:
    function addCustomDeliveryServices()
    {
    	return new \Bitrix\Main\EventResult(
    		\Bitrix\Main\EventResult::SUCCESS,
    		array(
    		'\Sale\Handlers\Delivery\CustomHandler' => '/ваш_путь_до_обработчика/имя_обработчика.php'
    		)
    	);
    }
    
    \Bitrix\Main\EventManager::getInstance()->addEventHandler('sale', 'onSaleDeliveryHandlersClassNamesBuildList', 'addCustomDeliveryServices');
    

    Как добавить логотип сразу при создании службы доставки?

    Дополнительные рекомендации

    • Обязательно учитывайте кодировку сайта. Если производится обмен данными со службой доставки, необходимо при этом правильно менять кодировку данных при отправлении и получении данных. В этом вам поможет метод \Bitrix\Main\Text\Encoding::convertEncoding().
    • При разработке в качестве примеров используйте следующие службы доставки:
      • \Sale\Handlers\Delivery\SimpleHandler (/bitrix/modules/sale/handlers/delivery/simple/handler.php) - простейший пример обработчика.
      • \Sale\Handlers\Delivery\SpsrHandler (/bitrix/modules/sale/handlers/delivery/spsr/handler.php) - вариант посложнее с использованием всех возможностей ядра D7.
    • Если вам необходимо наладить механизм автоматического отслеживания идентификаторов отправления (трэкинг-номеров), то используйте как пример службу доставки СПСР: \Sale\Handlers\Delivery\SpsrTracking.
    • Для запросов к сервису службы доставки рекомендуется использовать встроенный класс \Bitrix\Main\Web\HttpClient вместо сторонних расширений, например curl. Оптимальный формат обмена - json, так как возможно использовать встроенный класс \Bitrix\Main\Web\Json.
    • При обмене информацией с сервисами служб доставок зачастую необходимо передавать идентификаторы местоположений. Сопоставление идентификаторов местоположений интернет-магазина с идентификаторами местоположений служб доставок - задача нетривиальная. Как пример можно использовать \Sale\Handlers\Delivery\Spsr\Location::mapStepless();
    • Чтобы не порождать лишних запросов к службе доставки и не замедлять работу сайта, желательно по возможности кешировать полученную от сервисов служб доставок информацию. Однако, делать это надо аккуратно во избежание побочных эффектов. Как пример: \Sale\Handlers\Delivery\Spsr\Cache.
    • В случае возникновения ошибок и для отладки желательно иметь возможность записывать события, связанные с получением информации от служб доставок, в системный журнал. Для этого можно воспользоваться классом \CEventLog.


    Пример создания заказа через API

    Рассмотрим пример создания кода, с помощью которого будет добавлен заказ в систему. Допустим, у нас есть некие товары, в данном случае заданные массивом:

    $products = array(
    	array('PRODUCT_ID' => 1811, 'NAME' => 'PRODUCT_PROVIDER_CLASS' => '\Bitrix\Catalog\Product\CatalogProvider', 'Товар 1', 'PRICE' => 500, 'CURRENCY' => 'RUB', 'QUANTITY' => 5)
    			);
    

    В первую очередь создадим объект корзины при помощи метода create, в параметрах которого указываем идентификатор сайта, поскольку корзина привязывается к сайту. Затем наполняем корзину, пробегая по массиву товаров. Элемент корзины создается с помощью createItem:

    $basket = Bitrix\Sale\Basket::create(SITE_ID);
    
    foreach ($products as $product)
    	{
    		$item = $basket->createItem("catalog", $product["PRODUCT_ID"]);
    		unset($product["PRODUCT_ID"]);
    		$item->setFields($product);
    	}
    

    Теперь необходимо создать сам заказ. Опять используем метод create, но уже для класса заказа. В параметрах передаем идентификатор сайта и код пользователя (для простоты создадим заказ для администратора). После этого на объекте заказа можем вызывать необходимые методы: например, установить тип плательщика с помощью метода setPersonTypeId. Привязка корзины к заказу осуществляется с помощью метода setBasket:

    $order = Bitrix\Sale\Order::create(SITE_ID, 1);
    $order->setPersonTypeId(1);
    $order->setBasket($basket);
    

    При вызове метода setBasket происходит не только привязка корзины к данному заказу, но еще и пересчет заказа. Заказ актуализируется в соответствии с теми параметрами, которые в него передаются. Таким образом, после вызова данного метода у заказа появляется стоимость заказа, которая в данном случае численно равна стоимости корзины.

    Дальше нужно создать отгрузки. Получаем коллекцию отгрузок с помощью метода getShipmentCollection. Поскольку наш заказ новый, то создается новая коллекция отгрузок (если бы это был существующий заказ, то коллекция отгрузок была взята бы из базы данных). На этой коллекции мы можем создавать конкретные отгрузки с помощью createItem, в который передается объект службы доставки:

    $shipmentCollection = $order->getShipmentCollection();
    $shipment = $shipmentCollection->createItem(
    		Bitrix\Sale\Delivery\Services\Manager::getObjectById(1)
    	);
    

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

    $shipmentItemCollection = $shipment->getShipmentItemCollection();
    
    foreach ($basket as $basketItem)
    	{
    		$item = $shipmentItemCollection->createItem($basketItem);
    		$item->setQuantity($basketItem->getQuantity());
    	}
    

    Примечание: несмотря на то, что мы создаем одну отгрузку, на самом деле будет создано 2 отгрузки. Одна из отгрузок особая, она не видна в административном интерфейсе и является системной. Данная особенность связана с тем, что архитектурно товары не могут оставаться «подвешенными» и обязательно должны быть распределены по каким-то отгрузкам. Если товар не распределен по созданным вами отгрузкам, то он будет лежать в системной отгрузке до тех пор, пока вы его не перераспределите в некоторую вашу отгрузку.

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

    Теперь создадим оплату. С помощью метода getPaymentCollection получаем коллекцию оплат, а с помощью метода createItem создаем конкретную оплату:

    $paymentCollection = $order->getPaymentCollection();
    $payment = $paymentCollection->createItem(
    		Bitrix\Sale\PaySystem\Manager::getObjectById(1)
    	);
    

    Чтобы настроить свойства оплаты, используем метод setField. В нашем примере мы выставим счет на полную стоимость заказа:

    $payment->setField("SUM", $order->getPrice());
    $payment->setField("CURRENCY", $order->getCurrency());
    

    Для системы заказов характерна следующая особенность: если мы производим манипуляции с некоторым объектом заказа, то происходит перестройка всех других связанных с ним объектов. Так, мы ничего не делали с суммой заказа, но мы можем обратиться к ней, чтобы выставить платеж именно на сумму заказа, потому что система посчитала это самостоятельно. Система учла и стоимость корзины, и стоимость доставки, и скидки, и т.д.

    В конце нужно вызвать метод save, чтобы сохранить наш заказ. До вызова этого метода мы манипулируем с объектами в памяти. Именно он сохраняет все в базу данных.

    $result = $order->save();
    	if (!$result->isSuccess())
    		{
    			//$result->getErrors();
    		}
    

    Отметим также, что почти все те методы, которые не возвращают специфические объекты в магазине, всегда возвращают наследника класса result. Таким образом, из любого метода вы можете получить результат его выполнения, посмотреть, был ли он успешный, и получить ошибки, если таковые имелись.

    Наш код для создания заказа готов. После того, как выполним его на сервере, новый заказ добавится в систему:

    Полный код примера создания заказа



    Пример изменения заказа через API

    Рассмотрим небольшой пример изменения заказа через API. Допустим, у нас есть заказ с идентификатором 38:

    Составим код, с помощью которого будет добавлен комментарий и изменен флаг разрешения отгрузки в нашем заказе. Для этого нам достаточно с помощью метода load поднять заказ из базы данных по его идентификатору. Это действие сразу позволит нам манипулировать со всеми свойствами заказа. Так, с помощью метода setField добавим необходимый нам комментарий. Затем с помощью метода getShipmentCollection получим коллекцию отгрузок и для всех отгрузок, кроме системной, пометим флаг разрешения отгрузки (метод allowDelivery). После чего сохраним заказ с помощью метода save. Данный метод сохранит и заказ, и все связанные объекты (сохраняет именно те поля, которые были изменены):

    Bitrix\Main\Loader::includeModule('sale');
    
    $order = \Bitrix\Sale\Order::load(38);
    $order->setField("USER_DESCRIPTION", "Доставить к подъезду");
    
    $shipmentCollection = $order->getShipmentCollection();
    
    /** @var Sale\Shipment $shipment */
    
    foreach ($shipmentCollection as $shipment)
    {
    	if (!$shipment->isSystem())
    		$shipment->allowDelivery();
    }
    
    $result = $order->save();
    if (!$result->isSuccess())
    {
    	//$result->getErrors();
    }
    

    После выполнения вышеуказанного кода через командную php-строку наш заказ будет изменен соответствующим образом:


    Пример разделения оплаты на 2 части

    В уроке мы рассмотрим, как средствами API продукта разделить на части оплату в уже существующем заказе. Допустим, у нас есть заказ с идентификатором 21, а его документ оплаты имеет идентификатор 22:

    Составим код, с помощью которого оплата будет разбита на 2 части.

    • В первую очередь загружаем заказ из базы и получаем коллекцию оплат:
      Bitrix\Main\Loader::includeModule('sale');
      $order = \Bitrix\Sale\Order::load(21);
      $paymentCollection = $order->getPaymentCollection();
      
    • Затем из коллекции выбираем оплату, которую необходимо изменить (напомним, что у нас она имеет идентификатор 22):
      $payment = $paymentCollection->getItemById(22);
      
    • Сумму данной оплаты заменяем, например, на величину стоимости одной из позиций заказа (у нас это 1979 рублей):
      $payment->setField('SUM', 1979);
      
    • Добавляем новую оплату. Минимальный набор полей при создании оплаты - это платежная система (идентификатор и название платежной системы) и сумма. Для примера будем использовать платежную систему [dw]Сбербанк[/dw][di][/di] с идентификатором 7.

      Создать объект оплаты можно двумя способами:

      • передать объект платежной системы в параметре, из которого возьмется вся необходимая информация:
        $service = \Bitrix\Sale\PaySystem\Manager::getObjectById(7);
        $newPayment = $paymentCollection->createItem($service);
        
      • либо установить необходимую информацию по платежной системе явно:
        $newPayment = $paymentCollection->createItem();
        $newPayment->setField('PAY_SYSTEM_ID',7);
        $newPayment->setField('PAY_SYSTEM_NAME', "Сбербанк");
        
    • Устанавливаем сумму новой оплаты:
      $newPayment->setField('SUM', $order->getPrice() - 1979);
      
    • Сохраняем заказ:
      $result = $order->save();
      
      Метод save() сохранит и заказ, и все связанные объекты.

    После выполнения вышеуказанного кода через командную php-строку наш заказ будет иметь 2 оплаты:

    Примечание: разделение оплаты на этапе создания заказа делается аналогично.



    Видео

    Скачать Время Размер файла
    Интернет-магазины, высокие нагрузки, синхронизации бизнес-приложений с веб-системой 15 минут 43 секунды 432 Мб

    Смотреть

    Работа с модулем Push & Pull

    Push & Pull

    Детально о работе и настройке модуля смотрите в [ds]соответствующей главе[/ds][di]Push-уведомления – это небольшие всплывающие окна, которые появляются на экране мобильного телефона или обычного компьютера и сообщают о важных событиях и обновлениях.

    Подробнее ...[/di] курса Администратор.Модули.

    Режимы работы

    Модуль Push & Pull работает в двух режимах:

    • постоянное подключение к специальному серверу Сервер очередей;
    • в режиме опроса сервера (60-20-10).

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

    Второй режим используется когда по каким-то причинам невозможно использовать первый. В таком случае модуль будет каждые 60 секунд обращаться на сервер и проверять, есть ли данные. Если есть, то следующий хит будет через 10 секунд. Если данных больше нет, то хит будет сначала через 20 секунд, а потом через каждые 60 секунд. Тем самым будет создан эффект интерактивности.

    Какой способ выберет клиент на своем сайте (сервер очередей или опрос сервера), работа с модулем останется одинаковой (кроме работы с общим каналом).


    Само API делится на PHP и JS часть, ниже описаны принципы работы с ним.

    Примечание: В документации описаны не все существующие классы и события. Все остальные классы, которые вы можете видеть с помощью модуля LiveAPI являются служебными и не рекомендуются к использованию.

    PHP и JS

    Для начала работы вам необходимо подключить модуль:

    if (!CModule::IncludeModule('pull'))
    	return false;

    и зарегистрировать зависимость на модуль PULL. Регистрируем обработчик зависимости:

    RegisterModuleDependences("pull", "OnGetDependentModule", "your_module", "CYourModulePullSchema", "OnGetDependentModule" );

    Далее создайте свой класс. Код класса

    class CYourModulePullSchema
    {
    	public static function OnGetDependentModule()
    	{
    		return Array(
    			'MODULE_ID' => "your_module",
    			'USE' => Array("PUBLIC_SECTION")
    		);
    	}
    }

    Если ваш код работает в публичной части, необходимо указать:

    'USE' => Array("PUBLIC_SECTION")

    Если кроме публички, нужна еще и админка, то укажите:

    'USE' => Array("PUBLIC_SECTION", "ADMIN_SECTION")

    Перед использованием своего кода необходимо провести проверку подключения методами класса CPullOptions.

    Далее в работе можно использовать классы АПИ:

    Серверная часть (PHP)
    CPullStack Отправка данных.
    CPullWatch Отправка данных подписанным пользователям.
    CPushManager Отправка пуш уведомления.

    JS методы

    Клиентская часть (JS)
    Событие BX.addCustomEvent "Ловушка" для команд Push & Pull.
    BX.PULL.extendWatch Продление подписки.

    Пример

    Код для работы с выше описанными методами PHP

    BX.addCustomEvent("onPullEvent", function(module_id,command,params) {
    	if (module_id == "test" && command == 'check')
    	{
    		console.log('Work!');
    	}
    });

    Мы подписываемся на событие получение команд (onPullEvent), в функции получаем module_id, command, params которые мы указали при отправке команды из PHP, обрабатываем свои команды с учетом вашей логики.

    Пример компонента, который работает с самым сложным методом подписок BX.PULL.extendWatch.


    Оптимизация количества запросов к серверу

    Если проект находится на отдельном сервере, то необходимо использовать настройку путей в модуле Push & Pull, позволяющая современным браузерам ходить напрямую на сервер очередей.

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

    Теперь все новые браузеры обращаются напрямую на сервера. Но из-за особенности работы нашей реализации JS с сервером, им приходится делать дополнительно на каждое подключение OPTIONS запрос. Чтобы избавится от него, необходимо изменить конфигурационные файлы сервера.

    Откройте файл bx/conf/im_subscrider.conf, в описании локейшена location ^~ /bitrix/sub { проверьте есть ли у вас такое условие:

    if ($arg_time) {
    	push_stream_last_received_message_time "$arg_time";
    }

    Если нет, то добавьте. Выглядеть это должно примерно так:

    push_stream_subscriber            long-polling;
    push_stream_allowed_origins "*";
    push_stream_channels_path        $arg_CHANNEL_ID;
    push_stream_last_received_message_tag    $arg_tag;
    if ($arg_time) {
    	push_stream_last_received_message_time "$arg_time";
    }

    После этого воспользуйтесь методом COption::SetOptionString:

    COption::SetOptionString("pull", "nginx_headers", "N");

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


    Push & Pull для гостей

    Запустить Push & Pull для гостей можно, но необходимо реализовать свою логику фиксации сессии за гостем. Для этого необходим уникальный числовой идентификатор: без него невозможно выдать гостю один и тот же адрес канала и невозможно отправлять ему персонализированные команды. С версии Push & Pull 15.5.1 такая возможность появилась.

    Для реализации функционала разработчику необходимо реализовать методы определения гостя, присвоение ему внутреннего числового идентификатора. Допустим этот идентификатор у вас определился как 1. Чтобы не путать авторизованных и не авторизованных пользователей, передавать этот идентификатор в модуль Push & Pull его нужно со знаком минус:

    $guestId = -1;
    
    CModule::IncludeModule('pull');
    CPullStack::AddByUser($guestId, Array(
    	'module_id' => 'test',
    	'command' => 'check',
    	'params' => Array(),
    )); 
    

    Метод который определяет идентификатор, нужно реализовать и выполнить в прологе, до момента инициализации Push & Pull. Для этого необходимо будет зарегистрировать зависимость:

    RegisterModuleDependences("main", "OnProlog", "main", "", "", 2, "local/scripts/pull_hit.php");

    Внутри указанного файла должна быть ваша логика определения и указание константы PULL_USER_ID которая будет использоваться для корректного формирования канала гостю. Для каждого гостя должен быть свой идентификатор!

    $guestId = -1; // эту цифру должна вернуть ваша функция, определения идентификатора для гостя
    define('PULL_USER_ID', $guestId);

    После этого ваши гости могут получать Push & Pull команды наравне с авторизованными.

    Для удобства отладки, вы можете воспользоваться следующими JS командами:

    JS Команды
    BX.PULL.getDebugInfo(); Данная команда отображает состояние подключение к серверам P&P;
    BX.PULL.capturePullEvent(); Данная команда логирует все команды приходящие для данного пользователя;

    Подписка на события модуля

    Подписка

    До версий pull 18.5.7 (для десктопов) и mobile 18.5.10 (для мобильных устройств) для разработчиков существовали три проблемы при подписке на события модуля Push&Pull:

    1. Невозможно было одним кодом реализовать подписку на обоих типах устройств.
    2. В силу первого пункта приходилось обрабатывать много разных команд, получая "лапшу" из if'ов, что, помимо сложной ориентации в коде, выдавало неинформативный результат в отладчике в случае возникновения ошибки (вывод стек-трейса).
    3. Если требовалось обработать всего одну команду, то использование конструкции с одним if или целом классом было слишком многословным.

    Начиная с указанных версий подключение к событиям модуля Push&Pull реализовано с помощью метода BX.PULL.subscribe. Как работать с этим методом, рассказано ниже.

      Подключение библиотеки

    В рамках компонентов которые используются только в браузере, нужно обязательно проверить наличие модуля Push & Pull, а так же подключить библиотеку pull.client:

    • Укажите CoreJS зависимость pull.client в описании вашего расширения или вызовите \Bitrix\Main\UI\Extension::load('pull.client');
    • В рамках компонентов которые используются в мобильном приложении, нужно подключить необходимые зависимости для вашего контекста.

    • Для веб-страницы укажите CoreJS зависимость mobile.pull.client в описании расширения или вызовите \Bitrix\Main\UI\Extension::load('mobile.pull.client');
    • Для JaNative component, укажите зависимость pull/client/events в файле deps.php.
    • Для Offline WebComponent укажите зависимость pull/client/events в файле config.php в разделе deps.

    В рамках мобильного расширения вы можете использовать методы:
    BX.PULL.subscribe(...),
    BX.PULL.extendWatch(...),
    BX.PULL.clearWatch(...),
    BX.PULL.capturePullEvent(),
    BX.PULL.getDebugInfo().


    Для подписки есть три формата, вы можете выбрать для себя подходящий в зависимости от ваших задач.

      Одна команда

    Подписка на одну команду:

    BX.PULL.subscribe({
    	type: BX.PullClient.SubscriptionType.Server,
    	moduleId: 'im',
    	command: 'messageChat',
    	callback: function (params, extra, command) {
    		console.warn('Receive message:', params.message.text)
    	}.bind(this)
    });

    Где:
    type - тип подписки (Server, Client, Online) - можно не указывать, по умолчанию будет тип Server,
    moduleId - модуль отправивший команду
    command - команда на которую осуществляется подписка
    callback - функция обработчик.

    Параметры метода, который будет вызван при наступления события следующие:

    • params - объект, параметры команды,
    • extra - объект, дополнительные данные такие как версия модуля, имя и время сервера, время с момента отправки команды,
    • command - строка, название команды.

    Результатом выполнения метода будет функция c помощью которой вы сможете отписаться от указанной команды в будущем:

    let unsubscibe = BX.PULL.subscribe({...}); // подписка
    unsubscibe(); // отписка

      Несколько команд

    Подписка на множество команд с помощью функции маршрутизатора:

    BX.PULL.subscribe({
    	type: BX.PullClient.SubscriptionType.Server,
    	moduleId: 'im',
    	callback: function (data) {
    		if (data.command == 'messageAdd')
    		{
    			this.doSomething();
    		}
    	}.bind(this)
    });

    Где:
    type - тип подписки (Server, Client, Online) - можно не указывать, по умолчанию будет тип Server,
    moduleId - модуль отправивший команду,
    callback - функция обработчик для всех поступающих команд.

    В параметре data в указанной callback функции будет предоставлен следующий объект:

    {
    	command: '...', // название команды
    	params: {...}, // параметры команды
    	extra: {...} // дополнительные данные такие как версия модуля, имя и время сервера, время с момента отправки команды
    }

    Результатом выполнения метода будет функция c помощью которой вы сможете отписаться от команд модуля в будущем.

    let unsubscibe = BX.PULL.subscribe({...}); // подписка
    unsubscibe(); // отписка

      Класс маршрутизации

    Подписка с помощью класса маршрутизации:

    BX.PULL.subscribe(new CommandHandler(options));

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

    Сам класс маршрутизации выглядит так (обратите внимание: класс написан на ES6, но возможно и применение класса в формате ES5)

    class CommandHandler
    {
    	constructor(options = {})
    	{
    	}
    
    	getModuleId()
    	{
    		return 'im';
    	}
    
    	getSubscriptionType()
    	{
    		return BX.PullClient.SubscriptionType.Server;
    	}
    	
    	getMap()
    	{
    		return {
    			message: this.handleMessage.bind(this), 
    			messageChat: this.handleMessageChat.bind(this),
    			startCall: this.handleStartCall.bind(this),
    		}; 
    	}
    
    	handleMessage(params, extra, command)
    	{
    		console.log('exec command - message', params);
    	}
    
    	handleMessageChat(params, extra, command)
    	{
    		console.log('exec command - messageChat', params);
    	}
    
    	handleStartCall(params, extra, command)
    	{
    		console.log('exec command - startCall', params);
    	}
    }

    Метод getModuleId() должен возвращать идентификатор модуля, команды которого будет обрабатывать данный класс. (Обязательный метод).

    Метод getSubscriptionType() должен возвращать тип подписки (Server, Client, Online). (Не обязательный метод, если не указать, будет тип Server)

    Метод getMap() должен возвращать карту соответствия команды поступающей от сервера и метода который будет его обрабатывать.

      Форматы объекта

    Возможные форматы объекта возвращаемого функцией getMap().

    В формате ссылки на функцию, рекомендованный вариант, т.к. в IDE можно будет быстро перейти к функции просто кликнув на неё:

    {
    	startCall: this.handleStartCall.bind(this),
    }

    В формате строки:

    {
    	startCall: 'handleStartCall',
    }

    В формате callback функции:

    {
    	startCall: function(params, extra, command) {
    		console.log('exec command - startCall', params);
    	}.bind(this),
    }

    Параметры метода, который будет вызван при наступления события следующие:

    • params - объект, параметры команды
    • extra - объект, дополнительные данные такие как версия модуля, имя и время сервера, время с момента отправки команды
    • command - строка, название команды

    Упрощенный вариант описания

    Вы можете упростить описание класса, опустив описание метода getMap(), тогда методы обработки команд должны начинаться со слова handle. Далее должно следовать название команды, где первая буква будет заглавной, например у вас есть команда startCall, в классе должен быть метод handleStartCall.

    class CommandHandler
    {
    	constructor(options = {})
    	{
    	}
    
    	getModuleId()
    	{
    		return 'im';
    	}
    
    	handleMessage(params, extra, command)
    	{
    		console.log('exec command - message', params);
    	}
    
    	handleMessageChat(params, extra, command)
    	{
    		console.log('exec command - messageChat', params);
    	}
    
    	handleStartCall(params, extra, command)
    	{
    		console.log('exec command - startCall', params);
    	}
    }

    Результатом выполнения метода будет функция, c помощью которой вы сможете отписаться от команд модуля в будущем.

    let unsubscibe = BX.PULL.subscribe({...}); // подписка
    unsubscibe(); // отписка

    Гибридный вариант описания

    Вы можете использовать одновременно getMap() и методы по стандартам именования CommandHandler. Такой вариант подойдет, если вы хотите сделать alias к устаревшему формату команд или если вы отправляете команды в формате который невозможно описать в названии метода.

    class CommandHandler
    {
    	constructor(options = {})
    	{
    	}
    
    	getModuleId()
    	{
    		return 'im';
    	}
    	
    	getMap()
    	{
    		return {
    			'Application::send': this.handleApplicationSend.bind(this),
    			messageChatAdd: this.handleMessageChat.bind(this) 
    		}; 
    	}
    
    	handleMessage(params, extra, command)
    	{
    		console.log('exec command - message', params);
    	}
    
    	handleMessageChat(params, extra, command)
    	{
    		console.log('exec command - messageChat', params);
    	}
    
    	handleStartCall(params, extra, command)
    	{
    		console.log('exec command - startCall', params);
    	}
    	
    	handleApplicationSend(params, extra, command)
    	{
    		console.log('exec command - applicationSend', params);
    	}
    }

    Внимание!. Если команда описана в getMap() и у вас есть метод для этой команды названный по стандартам именования CommandHandler, то приоритет вызова будет отдан getMap().

    Результатом выполнения метода будет функция c помощью которой вы сможете отписаться от команд модуля в будущем.

    let unsubscibe = BX.PULL.subscribe({...}); // подписка
    unsubscibe(); // отписка

    Агенты и их использование

    Агенты

    Агенты – технология, позволяющая запускать произвольные PHP функции с заданной периодичностью. Технически - это запись в специальной таблице:
    • какой код надо выполнить,
    • когда выполнить,
    • с каким периодом выполнять,
    • каким способом назначать время следующего запуска агента (строго периодический или нестрого периодический агент).

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

    Как выполнялась проверка наличия агента до версии 20.5.0 Главного модуля.

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

    Кроме этого, не рекомендуется вешать на агенты ресурсоёмкие операции, для них существует фоновый запуск по cron'у.

    Чтобы агент выполнился в заданное время, его необходимо зарегистрировать в системе при помощи метода CAgent::AddAgent. Удалить регистрацию можно с помощью функции CAgent::RemoveAgent.

    Если функция-агент принадлежит модулю, то перед ее выполнением этот модуль будет автоматически подключаться, а именно будет подключаться файл /bitrix/modules/ID модуля/include.php. В этом случае необходимо убедиться, что функция будет доступна после подключения этого файла.

    Если функция не принадлежит ни одному из модулей, то ее необходимо разместить в файле /bitrix/php_interface/init.php. Этот файл автоматически подключается в прологе.

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

    Список используемых в системе агентов приводится на странице Список агентов (Настройки > Настройки продукта > Агенты).

    Периодические и непериодические

    Исторически агенты называются "периодические" и "непериодические", хотя правильнее было бы сказать: "повторяющиеся" и "неповторяющиеся". Сложившиеся названия решили не менять.

    Так же решили не менять и интерфейс настройки агента в административном разделе сайта:

    Тип агента зависит от программиста, который написал код агента. Программист может сделать агента, который повторится бесконечное число раз. Или только 2-3 раза в зависимости от условий. Пример функций агентов повторяющихся бесконечное число раз:

    // пример функций агентов
    
    function TestAgentPeriod()
    {
    	AddMessage2Log( "Периодический BX_CRONTAB:".BX_CRONTAB." BX_CRONTAB_SUPPORT:".BX_CRONTAB_SUPPORT );   
    	return "TestAgentPeriod();";
    }
    
    
    function TestAgentNotPeriod()
    {
    	AddMessage2Log( "Непериодический BX_CRONTAB:".BX_CRONTAB." BX_CRONTAB_SUPPORT:".BX_CRONTAB_SUPPORT );
    	return "TestAgentNotPeriod();";
    }

    Тип агента определяется по способу вычисления времени следующего запуска агента:

    • Периодические, назначенное время следующего запуска вычисляется так:
      Старое назначенное время агента + интервал

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

    • Непериодические, назначенное время следующего запуска вычисляется так:
      Время завершения последнего запуска агента (выполнение return) + интервал

      Например, агент пересчета рейтингов запускается раз в час. Но если за сутки не было ни одного посетителя, то логичнее выполнить агент один раз и сдвинуть назначенное время следующего запуска на час вперед после запуска. В случае строго периодического агента, этот код выполнился бы все 24 раза в сутки.

      Абсолютное большинство агентов непериодические.

    Ограничения

    При использовании технологии учтите, что:

    • Переменная USER в агентах отсутствует. Точнее она может быть создана на сервере хостинга, но нет никакой гарантии, что это будет объект класса CUser.
      При необходимости рекомендуется создавать в агенте объект $USER, что-то с ним сделать и уничтожить.
    • В агентах нельзя проводить авторизацию методом Authorize.
    • Отсутствует константа SITE_ID, так как агент может выполниться и на странице административного раздела.
    • На многоязычных сайтах нельзя заранее узнать какой будет язык.
    • Агенты выполняются в однопоточном режиме с блокировкой на MySQL на 10 минут. Новый вызов агента возможен только после того, как отработает предыдущий вызов. Блокировка БД может потеряться если закроется соединение с ней. 10 минут ожидания это - дополнительная защита от повторного запуска, агентов которые не корректно отработали.

    Дополнительно

    Список ссылок по теме:



    Примеры агентов

      Пример создания агента

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

    Агента создаем на странице Настройки > Настройки продукта > Агенты по команде Добавить агента на контекстной панели:

    О параметрах, значение которых может быть неясно из названия:

    • дата последнего запуска - выводится время последнего запуска (при редактировании агента);
    • дата и время следующего запуска – время старта работы агента, если он не периодический то выполнится 1 раз в это время;
    • модуль - этот модуль будет автоматически подключаться, а именно будет подключаться файл /bitrix/modules/ID модуля/include.php, в этом случае необходимо убедиться, что функция-агент будет доступна после подключения этого файла;
    • функция агента - это основное поле, у нас функция называется testAgent();
    • ID пользователя – это фильтр выполнения на хите для определенного пользователя;

    Сама функция будет выглядеть так:

    function testAgent()
    {
    	mail('mail@gmail.com', 'Агент', 'Агент');
    	return "testAgent();";
    }

    Функцию добавить в файл /bitrix/php_interface/init.php.

    Для активизации агента выполните в php-консоли админки следующий код:

    CAgent::AddAgent("testAgent();");

    Если письмо пришло, то агент работает и можно писать свой функционал.

      Простые примеры агентов

    <?
    // добавим агент модуля "Статистика"
    CAgent::AddAgent(
    	"CStatistic::CleanUpStatistics_2();", // имя функции
    	"statistic",                          // идентификатор модуля
    	"N",                                  // агент не критичен к кол-ву запусков
    	86400,                                // интервал запуска - 1 сутки
    	"07.04.2005 20:03:26",                // дата первой проверки на запуск
    	"Y",                                  // агент активен
    	"07.04.2005 20:03:26",                // дата первого запуска
    	30);
    ?>
    <?
    // добавим агент модуля "Техподдержка"
    CAgent::AddAgent(
    	"CTicket::AutoClose();",  // имя функции
    	"support",                // идентификатор модуля
    	"N",                      // агент не критичен к кол-ву запусков
    	86400,                    // интервал запуска - 1 сутки
    	"",                       // дата первой проверки - текущее
    	"Y",                      // агент активен
    	"",                       // дата первого запуска - текущее
    	30);
    ?>
    <?
    // добавим произвольный агент не принадлежащий ни одному модулю
    CAgent::AddAgent("My_Agent_Function();");
    ?>
    
    <?
    // файл /bitrix/php_interface/init.php
    
    function My_Agent_Function()
    {
    	// выполняем какие-либо действия
    	return "My_Agent_Function();";
    }
    ?>
    <?
    // добавим произвольный агент принадлежащий модулю
    // с идентификатором my_module
    
    CAgent::AddAgent(
    	"CMyModule::Agent007(1)", 
    	"my_module", 
    	"Y", 
    	86400);
    ?>
    
    <?
    // данный агент будет запущен ровно 7 раз с периодичностью раз в сутки, 
    // после чего будет удален из таблицы агентов.
    
    Class CMyModule
    {
    	public static function Agent007($cnt=1) : string
    	{
    		echo "Hello!";
    		if($cnt>=7)
    		return "";
    	return "CMyModule::Agent007(".($cnt+1).");";
       }
    }>

      Курсы валют

    Практичный пример: Обновление курса валют на сайте. Запуск данного агента рекомендуется повесить на cron.

    <?// Обновление курса валют
    
    function AgentGetCurrencyRate()
    {
    	global $DB;
    
    	// подключаем модуль "валют"
    	if(!CModule::IncludeModule('currency'))
    		return "AgentGetCurrencyRate();";
    
    	$arCurList = array('USD', 'EUR');
    	$bWarning = False;
    	$rateDay = GetTime(time(), "SHORT", LANGUAGE_ID);
    	$QUERY_STR = "date_req=".$DB->FormatDate($rateDay, CLang::GetDateFormat("SHORT", SITE_ID), "D.M.Y");
    	$strQueryText = QueryGetData("www.cbr.ru", 80, "/scripts/XML_daily.asp", $QUERY_STR, $errno, $errstr);
    
    	// данная строка нужна только для сайтов в кодировке utf8
    	$strQueryText = iconv('windows-1251', 'utf-8', $strQueryText);
    
    	if (strlen($strQueryText) <= 0)
    		$bWarning = True;
    
    	if (!$bWarning)
    	{
    		require_once($_SERVER["DOCUMENT_ROOT"]."/bitrix/modules/main/classes/general/xml.php");
    		$objXML = new CDataXML();
    		$objXML->LoadString($strQueryText);
    		$arData = $objXML->GetArray();
    		$arFields = array();
    		$arCurRate["CURRENCY_CBRF"] = array();
    
    	if (is_array($arData) && count($arData["ValCurs"]["#"]["Valute"])>0)
    	{
    		for ($j1 = 0; $j1<count($arData["ValCurs"]["#"]["Valute"]); $j1++)
    		{
    			$arFields = array(
    				"CURRENCY" => $arData["ValCurs"]["#"]["Valute"][$j1]["#"]["CharCode"][0]["#"],
    				'DATE_RATE' => $rateDay,
    				'RATE' => DoubleVal(str_replace(",", ".", $arData["ValCurs"]["#"]["Valute"][$j1]["#"]["Value"][0]["#"])),
    				'RATE_CNT' => IntVal($arData["ValCurs"]["#"]["Valute"][$j1]["#"]["Nominal"][0]["#"]),
    			);
    			CCurrencyRates::Add($arFields);
    		}
    
    	}
    }
    
    return "AgentGetCurrencyRate();";
    }?>

    Указанный код добавляется в файл /bitrix/php_interface/init.php. Не забудьте добавить агента AgentGetCurrencyRate(); на странице Настройки > Настройки продукта > Агенты.

      Элементы без цен

    Агент, проверяющий наличие элементов инфоблока без заполненных цен.

      Разработаем функцию, которая проверяет элементы инфоблока на заполнение цены. При наличии таких элементов необходимо отсылать письмо администратору об их количестве и делать соответствующую запись в журнал событий.
    1. Создадим:
      • Почтовое событие и шаблон.
      • «Агента», который будет запускать функцию 1 раз в день.
    2. Убедимся, что агент запускается, производится запись в журнал и приходят письма на почту.
    3. Размещение функции должно быть в отдельном файле, который подключается в init.php.
    function AgentChekPrice()
    {
    	if(CModule::IncludeModule("iblock"))
    	{
    		$arSelect = Array("ID", "NAME", "PROPERTY_PRICE");
    		$arFilter = Array("IBLOCK_ID"=> 2, "PROPERTY_PRICE" => false);
    		$rsResCat = CIBlockElement::GetList(Array(), $arFilter, false, false, $arSelect);
    		$arItems = array();
    		while($arItemCat = $rsResCat->GetNext())
    		{
    			$arItems[] = $arItemCat;
    		}
    	
    		CEventLog::Add(array(
    				"SEVERITY" => "SECURITY",
    				"AUDIT_TYPE_ID" => "CHECK_PRICE",
    				"MODULE_ID" => "iblock",
    				"ITEM_ID" => "",
    				"DESCRIPTION" => "Проверка цен, нет цен для ".count($arItems)." элементов",
    		));
    	
    		if(count($arItems) > 0)
    		{
    			$arFilter = Array(
    					"GROUPS_ID" => Array(2)
    			);
    			$rsUsers = CUser::GetList(($by="personal_country"), ($order="desc"), $arFilter);
    			$arEmail = array();
    			while($arResUser = $rsUsers->GetNext())
    			{
    				$arEmail[] = $arResUser["EMAIL"];
    			}
    
    			if(count($arEmail) > 0)
    			{
    				$arEventFields = array(
    						"TEXT" => "Проверка цен, нет цен для ".count($arItems)." элементов",
    						"EMAIL" => implode(", ", $arEmail),
    				);
    				CEvent::Send("INFO_PRICE", "s1", $arEventFields);
    			}
    		}
    	}
    	
    	return "AgentChekPrice();";
    }


    Запуск агентов из cron

    Об агентах

    Агент может вносить ощутимое ожидание на хите пользователя. Но тут надо взвесить различные факторы. Например, агент выполняющийся 0.5 секунд раз в сутки не причинит ощутимого вреда, а такой же агент раз в 30 минут уже будет досаждать. Всецело можно сказать одно: если агенты занимают несколько десятых долей секунды, то уже стоит задуматься о переносе агентов на cron. "Тяжелым" считается агент, который выполняется более 10 секунд.

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

    Механизм запуска

    Перейдите на страницу Настройки > Инструменты > Командная PHP-строка и исполните следующий код:

    COption::SetOptionString("main", "agents_use_crontab", "Y");
    echo COption::GetOptionString("main", "agents_use_crontab", "N");

    Увидели "Y". С этой секунды на хитах будут исполняться только [ds]периодические агенты[/ds][di]Тип агента зависит от программиста, который написал код агента. Программист может сделать агента, который повторится бесконечное число раз. Или только 2-3 раза в зависимости от условий.

    Подробнее ...[/di].

    Перейдите на страницу Настройки > Настройки продукта > Агенты и настройте показ колонки Периодичность. И отредактируйте нужные вам агенты выставив флажки в Периодичность выполнения: через заданный интервал.

    В cron добавьте команду на выполнение:

    */10 * * * * /usr/bin/php -f /home/bitrix/www/bitrix/modules/main/tools/cron_events.php

    Где: */10 * * * * - означает [dw]раз в десять минут[/dw][di]Обратите внимание, это лишь пример. Если вашему агенту требуется запуск хотя бы раз в 2-3 минуты, то он никогда не будет идти в ногу со временем. Обычно (в зависимости от разрешений хостера и личных предпочтений) частота времени запуска разнится от 1 до 5 минут.[/di].

    Файл для cron в BitrixEnv

    Примечание: Непосредственно перед выполнением задания процедура запуска агентов пытается отменить ограничение:
    @set_time_limit(0);
    ignore_user_abort(true);

    Если set_time_limit разрешен, то время выполнения может превышать то, что стоит в настройках файла php.ini.

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

    Пример кода запуска старых рассылок на кроне.

    Обобщённое решение

    Обобщенное решение для выполнения всех агентов из-под cron.

    Для начала полностью отключим выполнение агентов на хите. Для этого выполним следующую команду в php консоли:

    COption::SetOptionString("main", "agents_use_crontab", "N"); 
    echo COption::GetOptionString("main", "agents_use_crontab", "N"); 
    
    COption::SetOptionString("main", "check_agents", "N"); 
    echo COption::GetOptionString("main", "check_agents", "Y");
    

    В результате выполнения должно быть "NN".

    После этого убираем из файла /bitrix/php_interface/dbconn.php определение следующих констант:

    define("BX_CRONTAB_SUPPORT", true);
    define("BX_CRONTAB", true);

    И добавляем в этот файл:

    if(!(defined("CHK_EVENT") && CHK_EVENT===true))
    	define("BX_CRONTAB_SUPPORT", true);

    Создаем файл проверки агентов и рассылки системных сообщений /bitrix/php_interface/cron_events.php:

    <?php
    $_SERVER["DOCUMENT_ROOT"] = realpath(dirname(__FILE__)."/../..");
    $DOCUMENT_ROOT = $_SERVER["DOCUMENT_ROOT"];
    
    define("NO_KEEP_STATISTIC", true);
    define("NOT_CHECK_PERMISSIONS",true);
    define('BX_NO_ACCELERATOR_RESET', true);
    define('CHK_EVENT', true);
    define('BX_WITH_ON_AFTER_EPILOG', true);
    
    require($_SERVER["DOCUMENT_ROOT"]."/bitrix/modules/main/include/prolog_before.php");
    
    @set_time_limit(0);
    @ignore_user_abort(true);
    
    CAgent::CheckAgents();
    define("BX_CRONTAB_SUPPORT", true);
    define("BX_CRONTAB", true);
    
    if(CModule::IncludeModule('sender'))
    {
    	\Bitrix\Sender\MailingManager::checkPeriod(false);
    	\Bitrix\Sender\MailingManager::checkSend();
    }
    
    require($_SERVER['DOCUMENT_ROOT']."/bitrix/modules/main/tools/backup.php");
    CMain::FinalActions();
    ?>

    И добавляем данный скрипт в cron:

    */1 * * * * /usr/bin/php -f /home/bitrix/www/bitrix/php_interface/cron_events.php

    После этого все агенты и отправка системных событий будут обрабатываться из-под cron, [dw]раз в 1 минуту[/dw][di]Обратите внимание, некоторым системным агентам может быть недостаточно такой частоты запуска.[/di].

    Примечание: Время выполнения можно скорректировать в соответствие с проектом. Кроме того, есть возможность через установку большого значения mail_event_bulk сделать более "быстрой" доставку почтовых уведомлений. Установка проверки раз в минуту вместе с отправкой за раз 100 сообщений, сделает для пользователей незаметным данную задержку.

    Чтобы не увеличивалась очередь отправки почтовых сообщений, рекомендуется изменить параметр отвечающий за количество почтовых событий обрабатываемых за раз. Для этого выполняем в php консоли следующую команду:

    COption::SetOptionString("main", "mail_event_bulk", "20"); 
    echo COption::GetOptionString("main", "mail_event_bulk", "5");

    Если очередной запуск cron_events.php произошёл до завершения работы ранее запущенного скрипта, то запуска агентов не произойдет и скрипт завершит свою работу. (Так как агенты блокируются на время выполнения.) В данном случае обработка ничем не отличается от обработки на хите, новый хит может произойти в тот момент когда еще не отработали агенты на предыдущем.

    Как правило, скрипты выполненные из-под cron, не имеют ограничения на время исполнения. Но если в скриптах используются методы для работы с БД, то можно столкнуться с ошибкой выполнения вложенных скриптов. Для избежания этой ошибки можно подправить значение в dbconn.php:

    // если скрипт выполняется кроном, то лимит подключения к БД - 600 секунд, иначе - 60
    @set_time_limit(php_sapi_name() == "cli" ? 600 : 60);

    Права доступа

    Интерпретатор php из консоли на сервере должен быть запущен с такими же настройками, как и веб-сервер. Иначе возможна ситуация, что при запуске php из-под cli параметры будут отличаться. Это приводит к трудно-отлаживаемым ошибкам.

    Пользователь, из-под которого выполняется скрипт агентов, должен быть тем же самым, что и пользователь веб-сервера. Часто возникают ошибки, связанные с правами, если например, агент создает кеш, а веб-сайт не может его прочитать или удалить.

    Если задание необходимо добавлять от имени другого пользователя (например, root), то в записи crontab нужно указать имя этого пользователя:

    */1 * * * * USERNAME /usr/bin/php -f /home/bitrix/www/bitrix/php_interface/cron_events.php

    Некоторые проблемы

    Стандартная надпись

    Вопрос: А нужно ли при данных настройках отключать в кроне стандартную запись?
    * * * * * bitrix test -f /home/bitrix/www/bitrix/modules/main/tools/cron_events.php && { /usr/bin/php -f /home/bitrix/www/bitrix/php_interface/cron_events.php

    Да, если штатный переделывается на свой скрипт, то стандартный лучше закомментировать.

    Отправка писем агентом через postfix

    После настройки может возникнуть проблема: postfix каждую минуту отправляет письма примерно такого содержания:

    Nov 21 15:39:02 s052d79c2 postfix/pickup[10895]: C15E8BE8D9: uid=600 from=
    Nov 21 15:39:02 s052d79c2 postfix/cleanup[10914]: C15E8BE8D9: message-id=<20201121053902.C15E8BE8D9@example.com>
    Nov 21 15:39:02 s052d79c2 postfix/qmgr[10896]: C15E8BE8D9: from=, size=858, nrcpt=1 (queue active)
    Nov 21 15:39:02 s052d79c2 postfix/local[10916]: C15E8BE8D9: to=, orig_to=, relay=local, delay=0.4, delays=0.4/0/0/0, dsn=2.0.0, status=sent (delivered to mailbox)
    Nov 21 15:39:02 s052d79c2 postfix/qmgr[10896]: C15E8BE8D9: removed

    Решение проблемы следующее:

    Изначально строка запуска скрипта из cron из инструкции выглядит следующим образом:

    */1 * * * * bitrix /usr/bin/php -f /home/bitrix/ext_www/example.com/bitrix/php_interface/cron_events.php

    Соответственно каждый раз при вызове скрипта отправляется письмо. Чтобы отключить вывод, необходимо перенаправить вывод в "никуда", добавив директиву > /dev/null 2>&1 в конец строки.


    Ещё об агентах

    Осторожнее с периодичными агентами

    На проектах с низкой посещаемостью возможна ситуация, когда большое количество агентов может тормозить работу сайта. Монитор производительности показывает низкую (в районе 1) оценку. Статистика выполнения страниц показывает время генерации от 1 секунды и больше, а некоторые страницы вообще отказываются открываться.

    При таких симптомах посмотрите внимательнее статистику выполнения страницы. Если обнаружится что 90% времени генерации страницы занимает пролог сайта, то вероятно проблема в агентах. Достаточно 5-и агентов, у которых выставлено свойство Периодический. При выставлении этого свойства система при пропуске выполнения агента в следующие разы пытается компенсировать пропуски, что приводит к перегрузке сервера.

    Решение: Выключить периодичность агентов.


    События

    Иногда бывает необходимо повлиять на ход выполнения какой-нибудь API функции. Но если ее изменить, то эти изменения будут утеряны при очередном обновлении. Для таких случаев и разработана система событий. В ходе выполнения некоторых API функций, в определённых точках установлены вызовы определённых функций, так называемых обработчиков события.

    Примечание: С обработчиками следует обращаться очень внимательно. Поскольку событийная модель BitrixFramework довольно богатая, то при небрежности в коде обработчика могут появиться трудноуловимые ошибки. Они могут основательно попортить нервы разработчику.

    Какие функции-обработчики должны быть вызваны в каком месте (при каком событии) - нужно устанавливать вызовом функции, регистрирующей обработчики. В данный момент их две: Bitrix\Main\EventManager::addEventHandler и Bitrix\Main\EventManager::registerEventHandler. Сам набор событий для каждого модуля описан в документации по каждому модулю. Вот, например, ссылка на события главного модуля.

    registerEventHandler - функция для регистрации обработчиков, расположенных в модулях и использующихся для взаимодействия между модулями системы. Эту функцию необходимо вызвать один раз при инсталляции модуля, после этого функция-обработчик события будет автоматически вызываться в определённый момент, предварительно подключив сам модуль.

    Удаляется с помощью Bitrix\Main\EventManager::unRegisterEventHandler при удалении модуля.

    Пример

    $eventManager = \Bitrix\Main\EventManager::getInstance();
    
    // функции обработчики модуля компрессии подключаются дважды - в начале и в конце каждой страницы
    $eventManager->registerEventHandler("main", "OnPageStart", "compression", "CCompress", "OnPageStart", 1);
    $eventManager->registerEventHandler("main", "OnAfterEpilog", "compression", "CCompress", "OnAfterEpilog");
    
    // инсталлятор модуля рекламы регистирует пустой обработчик
    // при возникновении события OnBeforeProlog модуль рекламы будет просто подключаться на каждой странице
    // что сделает возможным выполнять его API функции без предварительного подключения в теле страницы
    $eventManager->registerEventHandler("main", "OnBeforeProlog", "advertising");

    Каждый модуль может предоставить другим модулям интерфейс для неявного взаимодействия - набор событий. Такое взаимодействие позволяет сделать модули максимально независимыми друг от друга. Модуль ничего не знает об особенностях функционирования другого модуля, но может взаимодействовать с ним через интерфейс событий.

    AddEventHandler - функция предназначена для регистрации произвольных обработчиков, которые не расположены в модулях. Ее необходимо вызывать до возникновения события на тех страницах, где требуется его обработать. Например, если событие нужно обработать на всех страницах, где оно возникает, то функцию можно вызвать в /bitrix/php_interface/init.php.

    Пример

    // регистрация обработчика в /bitrix/php_interface/init.php
    $eventManager = \Bitrix\Main\EventManager::getInstance();
    $eventManager->addEventHandlerCompatible("main", "OnBeforeUserLogin", "MyOnBeforeUserLoginHandler");
    
    function MyOnBeforeUserLoginHandler($arFields)
    {
    	if(strtolower($arFields["LOGIN"])=="guest")
    	{
    		global $APPLICATION;
    		$APPLICATION->throwException("Пользователь с именем входа Guest не может быть авторизован.");
    		return false;
    	}
    }

    Анонимные функции хорошо подходят для "решить быстро и безболезненно". Чтобы не засорять код именными функциями или классами делаю так:

    use Bitrix\Main\EventManager;
    
    $eventManager = EventManager::getInstance();
    
    $eventManager->addEventHandlerCompatible("main", "OnAfterUserLogin", function(&$fields) {
    // Мой код
    });

    Различия в использовании функций

    Действия, которые вы будете осуществлять с помощью событий должны быть где-то физически прописаны, и они должны быть зафиксированы, что они срабатывают на нужное событие.

    registerEventHandler производит регистрацию в БД, а AddEventHandler в файле init.php. То есть использование первой функции приводит к дополнительной нагрузке на БД. Её лучше использовать в ситуациях, когда выполняемые действия должны быть зафиксированы раз и навсегда и именно в БД.


    Как правило, события делятся по месту возникновения и назначению на следующие группы:

    • Предназначенные для отмены дальнейшего выполнения метода. Например, событие OnBeforeUserDelete позволяет отменить удаление пользователя при заданных условиях (наличие критических связанных объектов), событие OnBeforeUserLogin - запретить авторизацию пользователю;
    • Позволяющие выполниться в определённых методах, при завершении их исполнения. Например, OnAfterUserLogin - после проверки имени входа и пароля, событие OnUserDelete - перед непосредственным удалением пользователя из БД, позволяет удалить связанные объекты;
    • Возникающие во время исполнения страницы, для того чтобы включить свой код в определённые места на странице. Например, OnBeforeProlog, (подробнее см. порядок выполнения страниц).
    Совет от Антона Долганина.

    Если необходимо расширить журнал событий новыми видами событий (например, при действиях с инфоблоками), то нужно использовать обработчик событий OnEventLogGetAuditTypes. Массив, который он вернет, приплюсуется к стандартному массиву в админке. Именно с помощью него дописывает типы модуль Форум, например.

    Список и описание событий, доступных тому или иному модулю размещаются в Документации для разработчика.

    Список ссылок по теме:

    События в D7

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

    $event = new Bitrix\Main\Event("main", "OnPageStart");
    $event->send();

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

    foreach ($event->getResults() as $eventResult)
    {
    	switch($eventResult->getType())
    	{
    		case \Bitrix\Main\EventResult::ERROR:
    			// обработка ошибки
    			break;
    		case \Bitrix\Main\EventResult::SUCCESS:
      		          // успешно
    			$handlerRes = $eventResult->getParameters(); // получаем то, что вернул нам обработчик события
    			break;
    		case \Bitrix\Main\EventResult::UNDEFINED:
    			/* обработчик вернул неизвестно что вместо объекта класса \Bitrix\Main\EventResult его результат по прежнему доступен через getParameters*/
    			break;
    	}
    }

    Для уменьшения количества кода могут быть созданы наследники класса Bitrix\Main\Event для специфических типов событий. Например, Bitrix\Main\Entity\Event делает более комфортной отправку событий, связанных с модификацией сущностей.


    Список ссылок по теме:

    Использование событий

    В главе приводятся примеры работы с событиями.

    В большинстве случаев действия в системе имеют три рода событий:

    • OnBefore;
    • On;
    • OnAfter.

    Важно понимать какой из типов лучше использовать в вашем конкретном случае. События типа OnBefore отрабатывают в любом случае. А вот события типа OnAfter только тогда, когда будет проверена правильность введенных данных (соответствие пароля, e-mail и т.п.). Соответственно, если что-то в данных не так, то событие не отработает.

    Кроме примеров в этой главе можно ещё посмотреть пример использования событий в разделе Модификация тестов и в главе Модули.

    Как написать обработчик события

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

    Анализ вызова события

    Для начала разберемся в событиях как таковых. Теорию вы уже знаете, рассмотрим на практике.

    Вызов обработчиков всегда одинаковый, меняются только переменные и логика обработки ответа. Для анализа необходимо обратиться к исходным кодам. Это можно сделать просмотрев файлы системы, а лучше с использованием модуля Живое АПИ.

    Выберем для примера событие OnBeforeSocNetGroupAdd:

    Количество переменных. В этом событии переменных всего одна ($arFields). Именно столько же переменных нам надо будет вызвать в нашем обработчике. Переменных также может быть две или больше, например в событии OnSocNetGroupAdd:

    Переопределение переменных. Если перед одной из переменных стоит &, значит ее можно переопределить (это называется передача по ссылке).

    Отмена действия. В нашем случае для события OnBeforeSocNetGroupAdd есть такая возможность:, если мы в нашем обработчике сделаем return false, группа создана не будет. А, к примеру, в OnSocNetGroupAdd возможности отмены действия нет. Ибо действие уже произведено.

    Создание обработчика события

    Напомним теорию: для обработки событий в ваших модулях вам надо использовать RegisterModuleDependences. А для обработки в иных случаях вам надо использовать AddEventHandler.

    Имя модуля нам известно (socialnetwork), имя события известно (OnBeforeSocNetGroupAdd), пишем функцию/метод по правилам из теории и не забываем про:

    • количество переменных
    • возможность переопределения
    • отмену действия

    Как узнать что содержится в переменных, какие ключи массива и так далее?

    Делаем вывод на экран переменных с завершением работы в теле функции:

    echo '
    '; print_r($arFields); echo '
    '; die();

    Отмена действий

    Отмена действия с передачей ошибки в систему:

    if ($GLOBALS['USER']->GetID() == 2) {
    	$GLOBALS['APPLICATION']->throwException('Вы не можете создавать группы.');
    	return false;
    }

    Результат

    Мы собрали абстрактный обработчик, который добавляет к описанию группы правило, и запрещает пользователю с ID=2 создавать группы в принципе.

    AddEventHandler('socialnetwork', 'OnBeforeSocNetGroupAdd', 'TestHandler');
    function TestHandler(&$arFields) {
    	$arFields['DESCRIPTION'] .= ' Ругаться матом запрещено!';
    	if ($GLOBALS['USER']->GetID() == 2) {
    		$GLOBALS['APPLICATION']->throwException('Вы не можете создавать группы.');
    		return false;
    	}
    }
    Совет от Антона Долганина: Лучше не плодить функции, а создать класс ваших обработчиков (в идеале по одному классу на каждый модуль), и писать обработчики внутри классов. Например, CForumHandlers::onBeforeTopicAdd();.

    Добавление закладки в социальную сеть

    Как добавить произвольную закладку в социальную сеть без изменения стандартных компонентов? Для этого необходимо написать обработчики специальных событий.

    Обработчики можно расположить в файле /bitrix/php_interface/init.php.

    // Событие происходит при формировании списка дополнительного 
    // функционала соц.сети
    // В обработчике можно изменить или дополнить список
    AddEventHandler("socialnetwork", "OnFillSocNetFeaturesList", "__AddSocNetFeature");
    
    // Событие происходит при формировании списка закладок
    // В обработчике можно изменить список закладок
    AddEventHandler("socialnetwork", "OnFillSocNetMenu", "__AddSocNetMenu");
    
    // Событие происходит в комплексном компоненте при работе в ЧПУ
    // режиме при формировании списка шаблонов адресов страниц 
    // комплексного компонента
    AddEventHandler("socialnetwork", "OnParseSocNetComponentPath", "__OnParseSocNetComponentPath");
    
    // Событие происходит в комплексном компоненте при работе в 
    // не ЧПУ режиме при формировании списка псевдонимов переменных
    AddEventHandler("socialnetwork", "OnInitSocNetComponentVariables", "__OnInitSocNetComponentVariables");
    
    // При формировании списка дополнительного функционала 
    // добавим дополнительную запись superficha
    function __AddSocNetFeature(&$arSocNetFeaturesSettings)
    {
    	$arSocNetFeaturesSettings["superficha"] = array(
    		"allowed" => array(SONET_ENTITY_USER, SONET_ENTITY_GROUP),
    		"operations" => array(
    			"write" => array(SONET_ENTITY_USER => SONET_RELATIONS_TYPE_NONE, SONET_ENTITY_GROUP => SONET_ROLES_MODERATOR),
    			"view" => array(SONET_ENTITY_USER => SONET_RELATIONS_TYPE_ALL, SONET_ENTITY_GROUP => SONET_ROLES_USER),
    		),
    		"minoperation" => "view",
    	);
    }
    
    // При формировании списка закладок добавим дополнительную 
    // закладку для функционала superficha
    function __AddSocNetMenu(&$arResult)
    {
    	// Доступна для показа
    	$arResult["CanView"]["superficha"] = true;
    	// Ссылка закладки
    	$arResult["Urls"]["superficha"] = CComponentEngine::MakePathFromTemplate("/workgroups/group/#group_id#/superficha/", array("group_id" => $arResult["Group"]["ID"]));
    	// Название закладки
    	$arResult["Title"]["superficha"] = "Моя фича";
    }
    
    // При формировании списка шаблонов адресов страниц 
    // комплексного компонента в режиме ЧПУ добавим шаблон 
    // для superficha
    function __OnParseSocNetComponentPath(&$arUrlTemplates, &$arCustomPagesPath)
    {
    	// Шаблон адреса страницы
    	$arUrlTemplates["superficha"] = "group/#group_id#/superficha/";
    	// Путь относительно корня сайта,
    		// по которому лежит страница
    	$arCustomPagesPath["superficha"] = "/bitrix/php_interface/1/";
    }
    
    // Если компонент соц.сети работает в режиме 
    // ЧПУ, то этот обработчик не нужен 
    function __OnInitSocNetComponentVariables(&$arVariableAliases, &$arCustomPagesPath)
    {
    	}
    

    По пути /bitrix/php_interface/1/ должен лежать файл superficha.php, который содержит код страницы:

    <?if(!defined("B_PROLOG_INCLUDED") || B_PROLOG_INCLUDED!==true)die();?>
    <?
    $APPLICATION->IncludeComponent(
    	"bitrix:socialnetwork.group_menu",
    	"",
    	Array(
    		"GROUP_VAR" => $arResult["ALIASES"]["group_id"],
    		"PAGE_VAR" => $arResult["ALIASES"]["page"],
    		"PATH_TO_GROUP" => $arResult["PATH_TO_GROUP"],
    		"PATH_TO_GROUP_MODS" => $arResult["PATH_TO_GROUP_MODS"],
    		"PATH_TO_GROUP_USERS" => $arResult["PATH_TO_GROUP_USERS"],
    		"PATH_TO_GROUP_EDIT" => $arResult["PATH_TO_GROUP_EDIT"],
    		"PATH_TO_GROUP_REQUEST_SEARCH" => $arResult["PATH_TO_GROUP_REQUEST_SEARCH"],
    		"PATH_TO_GROUP_REQUESTS" => $arResult["PATH_TO_GROUP_REQUESTS"],
    		"PATH_TO_GROUP_REQUESTS_OUT" => $arResult["PATH_TO_GROUP_REQUESTS_OUT"],
    		"PATH_TO_GROUP_BAN" => $arResult["PATH_TO_GROUP_BAN"],
    		"PATH_TO_GROUP_BLOG" => $arResult["PATH_TO_GROUP_BLOG"],
    		"PATH_TO_GROUP_PHOTO" => $arResult["PATH_TO_GROUP_PHOTO"],
    		"PATH_TO_GROUP_FORUM" => $arResult["PATH_TO_GROUP_FORUM"],
    		"PATH_TO_GROUP_CALENDAR" => $arResult["PATH_TO_GROUP_CALENDAR"],
    		"PATH_TO_GROUP_FILES" => $arResult["PATH_TO_GROUP_FILES"],
    		"PATH_TO_GROUP_TASKS" => $arResult["PATH_TO_GROUP_TASKS"],
    		"GROUP_ID" => $arResult["VARIABLES"]["group_id"],
    		"PAGE_ID" => "group_superficha",
    		),
    	$component
    );
    ?>
    
    Полезный код страницы...

    Учет регистрации нового пользователя в статистике

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

    Для решения задачи используется обработчик события OnAfterUserRegister. Код обработчика будет таким:

    AddEventHandler("main", "OnAfterUserRegister", "OnUserEmailLoginRegisterHandler"); 
     
    function OnUserEmailLoginRegisterHandler(&$arFields) 
    { 
     
    	if(CModule::IncludeModule("statistic") && intval($_SESSION["SESS_SEARCHER_ID"]) <= 0)
    	{ 
    		$event1 = "register";
    		$event2 = "new_user";
    		$event3 = $arFields["EMAIL"];
    		CStatistic::Set_Event($event1, $event2, $event3);      
    	}
    	return $arFields; 
    }

    В результате в отчетах модуля статистики появятся данные о регистрациях:



    Зацикливание обработчиков событий

    Задача: при изменении элемента инфоблока модифицировать другой элемент. Кейс может быть какой угодно - это и логирование, и деактивация основного товара, когда нет активных предложений, и изменение даты активности связанного элемента. Если создать обработчик, использующий метод CIBlockElement::Update, и повесить его на события OnBeforeIBlockElementUpdate / OnAfterIBlockElementUpdate, то:

    вызов обработчика OnBeforeIBlockElementUpdate/OnAfterIBlockElementUpdate
    	....
    CIBlockElement::Update
    вызов обработчика OnBeforeIBlockElementUpdate/OnAfterIBlockElementUpdate
    	....
    	CIBlockElement::Update
    
    		...
    		итог - 500 (Internal Server Error)

    Причина в том, что происходит рекурсивный вызов обработчика. Ниже приведен код, который избавляет от подобных проблем. В качестве примера взят обработчик OnAfterIBlockElementUpdate.

    class myClass 
    { 
    	protected static $handlerDisallow = false; 
     
    	public static function iblockElementUpdateHandler(&$fields) 
    	{ 
    		/* проверяем, что обработчик уже запущен */
    		if (self::$handlerDisallow) 
    			return; 
    		/* взводим флаг запуска */
    		self::$handlerDisallow = true;           
    		/*  наш код, приводящий к вызову CIBlockElement::Update */ 
    		... 
    		CIBlockElement :: Update (..., ...); 
     
    		/* вновь разрешаем запускать обработчик */ 
    		self::$handlerDisallow = false;
    	} 
    } 

    Пояснение. За основу взят класс, так как подобные решения в основном используются в собственных модулях. В классе имеется статическая булевая переменная - $handlerDisallow. По умолчанию она имеет значение false - нет запрета. В самом начале обработчика необходимо проверять ее значение. Если обработчик уже запущен, она будет равна true и выполнение необходимо прервать. Если же выполнять обработчик можно, необходимо присвоить этой переменной true на время выполнения всего обработчика. В конце необходимо флаг сбросить ($handlerDisallow), иначе до конца хита ваш обработчик не выполнится больше ни разу.

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

    Можно дополнить класс возможностью блокировать работу обработчика "снаружи". Для этого измените тип переменной и добавьте три метода:

    class myClass
    {
    	protected static $handlerDisallow = 0;
    
    	public static function disableHandler()
    	{
    		self::$handlerDisallow--;
    	}
    
    	public static function enableHandler()
    	{
    		self::$handlerDisallow++;
    	}
    
    	public static function isEnabledHandler()
    	{
    		return (self::$handlerDisallow >= 0);
    	}
    
    	public static function iblockElementUpdateHandler(&$fields)
    	{
    		/* проверяем, что обработчик уже запущен */
    		if (!self::isEnabledHandler())
    			return;
    		/* взводим флаг запуска */
    		self::disableHandler();
    		/*  наш код, приводящий к вызову CIBlockElement::Update */
    	...
    	CIBlockElement :: Update (..., ...);
    
    		/* вновь разрешаем запускать обработчик */
    		self::enableHandler();
    	}
    }

    Совместная работа пары событий

    Рассмотрим использование пары обработчиков событий на примере событий метода CCatalogProduct::GetOptimalPrice.

    Метод имеет два события: OnGetOptimalPrice и OnGetOptimalPriceResult, различающихся набором передаваемых параметров. Предположим, что для реализации логики обработчика OnGetOptimalPriceResult необходимо знать список групп пользователя, переданный в метод. Но эти данные доступны только в обработчике OnGetOptimalPrice.

    Необходимо сохранить параметры вызова метода в одном обработчике (onGetOptimalPrice), а использовать в другом (onGetOptimalPriceResult), после чего очистить кеш параметров внутри обработчика. Реализация работы обработчиков "в паре":

    class myClass
    {
    	protected static $handlerDisallow = 0; 
     
    	protected static $handlerParams = array();
    
    	public static function disableHandler()
    		{
    		self::$handlerDisallow--;
    		}
    
    	public static function enableHandler()
    		{
    		self::$handlerDisallow++;
    		}
    
    	public static function isEnabledHandler()
     		  {
    		return (self::$handlerDisallow >= 0);
    		}
    
    	public static function onGetOptimalPrice()
    		{
    		/* проверяем, что обработчик уже запущен */
    		if (!self::isEnabledHandler())
    			return;
    		/* взводим флаг запуска */
    		self::disableHandler();   
    		self::$handlerParams = func_get_args();
    		return true;
    	}
     
    	public static function onGetOptimalPriceResult(&$data)  
    		{  
    		/* 
    		далее логика, использующая данные из self::$handlerParams примерно так
    		if (self::$handlerParams[0] == 17) //первый параметр из вызова CCatalogProduct::GetOptimalPrice
    		$data['RESULT_PRICE']['BASE_PRICE'] = 100;
    		*/
    		self::$handlerParams = array();  // очищаем параметры - в обязательном порядке 
    		/* вновь разрешаем запускать обработчик */ 
    		self::enableHandler();  
    	} 
    }

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


    Данные до и после update

    Описание

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

    В обработчиках нет старых данных в силу того, что если будет реализована передача данных штатно, то это минимум в два раза увеличит число запросов на каждое обновление.

    Решить задачу в D7 достаточно просто, в случае старого API - несколько сложнее, хотя подход одинаков. На D7 проще в силу того что набор имеющихся событий и место их вызова стандартизированы, равно как и данные, доступные в обработчиках. В старом ядре не всегда есть обработчики либо есть только один (обычно вызываемый после обновления).

    D7

    Реальный пример, взятый из API купонов правил корзины. В случае перепривязки купона (смены правила корзины, к которому он относится), необходимо обновить флаг существования купонов как у старого, так и у нового правила. Класс \Bitrix\Sale\Internals\DiscountCouponTable:

    public static function onUpdate(Main\Entity\Event $event)
    {
    	if (!self::isCheckedCouponsUse())
    		return;
    	$data = $event->getParameter('fields');
    	if (isset($data['DISCOUNT_ID']))
    	{
    		$data['DISCOUNT_ID'] = (int)$data['DISCOUNT_ID'];
    		$id = $event->getParameter('id');
    		$couponIterator = self::getList(array(
    			'select' => array('ID', 'DISCOUNT_ID'),
    			'filter' => array('=ID' => $id)
    	));
    	if ($coupon = $couponIterator->fetch())
    	{
    		$coupon['DISCOUNT_ID'] = (int)$coupon['DISCOUNT_ID'];
    		if ($coupon['DISCOUNT_ID'] !== $data['DISCOUNT_ID'])
    			{
    			self::$discountCheckList[$data['DISCOUNT_ID']] = $data['DISCOUNT_ID'];
    			self::$discountCheckList[$coupon['DISCOUNT_ID']] = $coupon['DISCOUNT_ID'];
    			}
    	}
    	unset($coupon, $couponIterator);
    	}
    }  
     
    public static function onAfterUpdate(Main\Entity\Event $event)
    {
    	self::updateUseCoupons();
    }

    Использованы события onUpdate и onAfterUpdate. Первое вызывается перед изменением записи в таблице, но уже после проверки данных и возможной отмены действия. Поскольку функционал реализован в самом классе, нет необходимости регистрировать обработчики отдельно. Для вмешательства в работу чужого класса регистрацию своих обработчиков придется сделать, используя следующий код:

          $eventManager = \Bitrix\Main\EventManager::getInstance();
    	$eventManager->registerEventHandler('модуль', 'событие', 'Ваш_модуль', 'Ваш_класс', 'метод_класса');  
    
    /* событие - \Bitrix\Main\Entity\DataManager::EVENT_ON_UPDATE или \Bitrix\Main\Entity\DataManager::EVENT_ON_AFTER_UPDATE */
    

    Что же делают эти два обработчика? Рассмотрим обработчик onUpdate.

    1. if (!self::isCheckedCouponsUse()) 
      	return; 

      Возможность отключить обработчик. Нужна, например, при деактивации набора купонов либо массовой операции перепривязки (когда использующий API и так знает исходное и новое правило корзины).

    2. $data = $event->getParameter('fields');
      if (isset($data['DISCOUNT_ID'] ) )
      {
      	$data['DISCOUNT_ID'] = (int)$data['DISCOUNT_ID'];
      	$id = $event->getParameter('id');
      	$couponIterator = self::getList(array(
      		'select' => array('ID', 'DISCOUNT_ID'),
      		'filter' => array('=ID' => $id)
      		));
      	if ($coupon = $couponIterator->fetch())
      	{
      		$coupon['DISCOUNT_ID'] = (int)$coupon['DISCOUNT_ID'];
      		if ($coupon['DISCOUNT_ID'] !== $data['DISCOUNT_ID'] )
      		{
      			self::$discountCheckList[$data['DISCOUNT_ID']] = $data['DISCOUNT_ID'];
      			self::$discountCheckList[$coupon['DISCOUNT_ID']] = $coupon['DISCOUNT_ID'];
      		}
      	}
         unset($coupon, $couponIterator);
      }
      

      Проверяется наличие интересующего нас поля (DISCOUNT_ID). Если оно есть, получается из базы старое значение, сравнивается, и, в случае несовпадения, - заносятся старое и новое значение в статическую переменную класса.

    3. Собственно, остался вызов onAfterUpdate, внутри которого проверяется, что переменная с данными не пустая и идет обновление флага существования купонов для старого и нового правил корзины.

    Примечание: Обязательные условия:
    • Оба обработчика должны относится к одному классу.
    • Оба обработчика должны быть статические.
    • Данные хранятся в статической переменной класса.

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

    Старое ядро

    При реализации на API старого ядра задача усложняется - может не оказаться необходимых событий. Единственный выход - либо просить вендора их добавить, либо искать обходные пути. В рамках описываемой задачи предположим, что они есть. Небольшой недостаток есть в том, что в старом ядре может выполниться событие Before и не выполниться After. Рассмотрим на примере CCatalogProduct. У метода Update есть два события - OnBeforeProductUpdate и OnProductUpdate. Создаем класс обработчиков:

    class MyChanger
    {
    	protected static $oldWeight = array();
    
    	public static function OnBeforeUpdate($id, &$fields)
    	{
    		$id = (int)$id;
    		if ($id <= 0)
    		return true;
    		if (isset($fields['WEIGHT'] ) )
    			{
    			$productRes = CCatalogProduct::GetList(array(), array('ID' => $id), false, false, array('WEIGHT') );
    			$product = $productRes->Fetch();
    			if (!empty($product))
    			{
    			if ($product['WEIGHT'] != $fields['WEIGHT'] )
    			self::$oldWeight[$id] = $product['WEIGHT'];
    			}
    		}
    		return true;
    	}
    
    	public static function OnUpdate($id, $fields)
    		{
    		if (isset($fields['WEIGHT'] ) && isset(self::$oldWeight[$id] ) )
    		{
    		/*
    		* необходимые действия
    		*/
    
    	unset(self::$oldWeight[$id] );
    		}
    	}
    }

    Этот класс контролирует изменение веса товара. Задача условна, но код содержит все необходимое для подобных действий.

    Метод MyChanger::OnBeforeUpdate вешается на событие модуля catalog OnBeforeProductUpdate. Если в массиве обновляемых данных есть поле WEIGHT - из базы получается старое значение и если оно отличается, то старое заносится в статическую переменную.

    Метод MyChanger::OnUpdate должен содержать требуемую логику (логирование, отправка писем и так далее). Он должен быть зарегистрирован на событие модуля catalog OnProductUpdate.

    Примечание: перечень параметров обработчика должен соответствовать параметрам, передаваемым в обработчик из метода.

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


    Дополнительно

    Вопрос: Можно ли вызывать функцию AddEventHandler несколько раз для одного и того же события?

    Ответ: Вызывать несколько раз функцию AddEventHandler с одинаковыми первыми двумя параметрами можно. Случаи, когда это не так (возможен лишь один обработчик на событие) очень редки. При повторном вызове желательно указывать четвёртый параметр, который отвечает за очерёдность вызова обработчиков. Если не указать этот параметр, то обработчики будут вызваны в порядке добавления.

    Как обработчику события узнать, какое событие он обрабатывает?

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

    Вопрос: как обработчику события узнать, какое событие он обрабатывает?

    Решение зависит от способа инициализации.

    1. AddEventHandler - сделайте прослойку.
      function OnAdd()
      {
      	RealHandler("add");
      }
      
      function OnUpdate()
      {
      	RealHandler("update");
      }
    2. RegisterModuleDependences - при регистрации добавьте аргумент.
      $TO_METHOD_ARG = Array("argument"=>"OnUserDelete");
      RegisterModuleDependences("main", "OnUserDelete", "forum", "CForum", "OnUserDelete", $sort, $TO_PATH, $TO_METHOD_ARG);
      
      class CForum 
      { 
      	function OnUserDelete($arguments, &$arFields) 
      	{ 
      		//Код обработчика     
      	} 
      }

    Вопрос: Как осуществить поиск не только по названию товаров но и по описанию.

    Решение: Нужно добавить свойство для поиска и в настройках свойства указываем что оно участвует в поиске. Далее:

    // регистрируем обработчик
    AddEventHandler("search", "BeforeIndex", "BeforeIndexHandler");
     // создаем обработчик события "BeforeIndex"
    function BeforeIndexHandler($arFields)
    {
    	if(!CModule::IncludeModule("iblock")) // подключаем модуль
    		return $arFields;
    	if($arFields["MODULE_ID"] == "iblock")
    		{
    		$db_props = CIBlockElement::GetProperty(  
    			// Запросим свойства индексируемого элемента
    			$arFields["PARAM2"],  // BLOCK_ID индексируемого свойства
    			$arFields["ITEM_ID"],  // ID индексируемого свойства
    			array("sort" => "asc"),  // Сортировка (можно упустить)
    			Array("CODE"=>"CML2_ARTICLE"));  // CODE свойства (в данном случае артикул)
    				if($ar_props = $db_props->Fetch())
    				$arFields["TITLE"] .= " ".$ar_props["VALUE"];  // Добавим свойство в конец заголовка индексируемого элемента
    		}
    	return $arFields; // вернём изменения
    } 

    "Ленивые" параметры в событиях

    Чтобы отправить событие выполните следующее:

    use Bitrix\Main\EventManager;
    
    $event = new \Bitrix\Main\Event('moduleName', 'onEventName', array(
    	'data' => array(
    		'name' => 'John',
    		'sex' => 'male',
    		),
    	'datetime' => new \Datetime(),
    ));
    
    $event->send();

    Таким образом отправится событие от модуля moduleName с именем onEventName и данными, которые будут доступны в обработчиках этого события.

    В некоторых ситуациях вычисление данных для отправки требует привлечения дополнительных ресурсов. В такой ситуации сконфигурируйте объект иначе:

    use Bitrix\Main\EventManager;
    
    $lazyEvent = new \Bitrix\Main\Event('moduleName', 'onEventName', function() use ($userId){
        
    //какая-то работа, которая получает данные
    	$groups = loadUserGroups($userId);
        
    	return array(
    		'data' => array(
    			'name' => 'John',
    			'sex' => 'male',
    			'groups' => $groups,
    		),
    		'datetime' => new \Datetime(),
    	);
    });
    
    
    $Lazyevent->send();

    В таком случае вычисление параметров события будет отложено до первого обращения к ним в обработчике. В итоге получается два преимущества:

    1. Если нет обработчиков, то не будут запускаться вычисления.
    2. Если в обработчиках не используются параметры события, то снова не будут запускаться вычисления.

    Примечание: Доступно с версии главного модуля 17.0.0.


    Настройка ЧПУ

    Примечание: Работа с ЧПУ штатными методами описана в курсе Администратор. Базовый. Перед тем как учиться работать с ЧПУ через API обязательно изучите этот раздел.

    Для хранения идентификаторов элементов/разделов информационных блоков удобнее всего использовать поле Символьный код. Например, в ссылке www.myserver.ru/catalog/mobile/nokia_3310/, mobile - это символьный код раздела Мобильные телефоны, а nokia_3310 - символьный код элемента, находящегося в разделе Мобильные телефоны. Символьный код должен быть уникальным и система сама проверяет уникальность.

    В обработчике 404 ошибки необходимо "разобрать" переменную $_SERVER["REQUEST_URI"] на нужные идентификаторы. Для этого в PHP есть ряд полезных функций:

    Например, ссылки вида myserver.ru/users/<Логин пользователя> обрабатываются в файле 404.php так:

    <?
    if(preg_match("~^/users/([a-z_][a-z0-9_]{2,14})/?$­~i",$_SERVER["REQUEST_URI"],$match))
    {
    header("HTTP/1.1 200 OK");
    //делаем выборку по идентификатору
    require($_SERVER["DOCUMENT_ROOT"]."/bitrix/header.­php");
    $res = CUser::GetList($O, $B, Array("LOGIN_EQUAL_EXACT"=>$match[1],"ACTIVE"=>"Y"­));
    //$match[1] содержит логин
    if($arUser = $res->GetNext())
    {
    //выводим данные пользователя
    }
    else
    {
    //ошибка: нет такого пользователя
    }
    require($_SERVER["DOCUMENT_ROOT"]."/bitrix/footer.php");
    }
    else
    {
    header("HTTP/1.1 404 Not Found");
    //ошибка
    }
    ?>
    

    Но жесткая проверка в preg_match не позволит сделать ссылки вида www.myserver.ru/users/user_login/?r1=banner&r2=com­puterra.ru, которые очень необходимы для анализа рекламных компаний. Поэтому, в начале файла 404.php пишем:

    <?$arURI = parse_url($_SERVER["REQUEST_URI"]);
    $_SERVER["REQUEST_URI"] = $arURI["path"];
    if(!empty($arURI["query"]))
    {
    parse_str($arURI["query"],$par);
    foreach($par as $key => $val)
    {
    global $$key;
    $$key = $val;
    }
    }
    ?>
    

    Комплексный компонент и SEF режим

    В комплексные компоненты встроена функция генерации ЧПУ. У этих компонентов всегда есть входной параметр SEF_MODE, который может принимать значения Y и N. Если параметр SEF_MODE равен N, то компонент работает с физическими ссылками и все параметры передает через стандартные параметры HTTP запроса. Например:

    /fld/cat.php?IBLOCK_ID=12&SECTION_ID=371

    Если параметр SEF_MODE равен Y, то компонент генерирует и обрабатывает ссылки на основании шаблонов. Например, он может понять и обработать ссылку:

    /catalog/section/371.php?IBLOCK_ID=12, даже если сам он лежит в файле /fld/cat.php.

    Если параметр SEF_MODE равен Y, то у компонента должен так же присутствовать параметр SEF_FOLDER, который должен содержать путь до папки, с которой работает компонент. Этот путь может как совпадать с физическим путем, так и не совпадать. Например, в компоненте bitrix:catalog, подключенном в файле /fld/cat.php, могут быть установлены параметры SEF_MODE = Y и SEF_FOLDER=/catalog/. Тогда компонент будет отвечать на запросы по пути /catalog/.... По умолчанию редактор должен устанавливать текущий физический путь к редактируемому файлу.

    У комплексного компонента, который может работать в режиме SEF, должен быть определен набор шаблонов путей по умолчанию. Например, в комплексном компоненте bitrix:catalog может быть определен следующий массив:

    $arDefaultUrlTemplatesSEF = array( 
    	"list" => "index.php", 
    	"section" => "section.php?IBLOCK_ID=#IBLOCK_ID#&SECTION_ID=#SECTION_ID#",
    	"element" => "element.php?ELEMENT_ID=#ELEMENT_ID#" 
      );

    Эти шаблоны путей могут быть переопределены с помощью входного параметра комплексного компонента SEF_URL_TEMPLATES, содержащего новый массив всех шаблонов путей или их части.

    При сохранении в редакторе страницы с компонентом, работающим в SEF режиме, создается или обновляется запись в системе urlrewrite. Например, при сохранении файла /fld/cat.php, в котором лежит компонент bitrix:catalog, переключенный в SEF режиме с параметром SEF_FOLDER=/catalog/, в системе urlrewrite создается или обновляется запись типа:

    array( 
    	"CONDITION" => "#^/catalog/#", 
    	"ID" => "bitrix:catalog", 
    	"PATH" => "/fld/cat.php" 
    ),
    • в CONDITION записывается значение параметра SEF_FOLDER, обрамленное символами «#^» и «#»;
    • в ID записывается название компонента;
    • в PATH записывается физический путь к файлу, который сохраняется.

    Если запись с таким PATH и ID уже есть, то она обновляется, если нет – добавляется.

    В run-time при запросе физически не существующей страницы механизм urlrewrite производит поиск соответствующей записи по CONDITION и передает управление на страницу PATH.

    Компонент на странице PATH на основании шаблонов путей выясняет запрашиваемую страницу и восстанавливает переменные, спрятаные в пути.

    Внимание! Обязательное требование к набору шаблонов путей данного компонента - это уникальность каждого шаблона пути без учета параметров и переменных. Это должно проверяться при сохранении страницы в визуальном редакторе.

    То есть, набор шаблонов путей

    Список ссылок по теме:


    Примеры

    Пример

    Новости вида /about/news/23.html (ссылка для печати /about/news/print_23.html) вместо /about/news/detail.php?ID=23 (/about/news/detail.php?ID=23&print=Y)

    • mod_rewrite
      RewriteEngine On
      RewriteBase /
      RewriteRule ^about/news/([0-9]+).html$ about/news/detail.php?ID=$1
      RewriteRule ^about/news/print_([0-9]+).html$ about/news/detail.php?ID=$1&print=Y
      
    • Обработчик 404 ошибки
      <?if(preg_match("~^/about/news/(print_)?([0-9]+).html$~",$_SERVER["REQUEST_URI"],$match))
      {
      header("HTTP/1.1 200 OK");
      $_GET["print"] = (strlen($match[1])>0 ? "Y": "");
      $_REQUEST["ID"] = $match[2];
      include($_SERVER["DOCUMENT_ROOT"]."/about/news/detail.php");
      }
      else
      {
      define("ERROR_404", "Y");
      header("HTTP/1.1 404 Not Found");
      require($_SERVER["DOCUMENT_ROOT"]."/bitrix/header.php");
      $APPLICATION->SetTitle("404 - файл не найден");
      require($_SERVER["DOCUMENT_ROOT"]."/bitrix/footer.php");
      }
      ?>

    Дополнительно

    Как убрать "PHPSESSID=..." из URL?

    Чтобы избавиться от идентификатора сессии в URL, раскомментируйте строку в /.htaccess:

    php_flag session.use_trans_sid off

    Если это не дает результата, необходимо изменить значение параметра session.use_trans_sid на Off непосредственно в php.ini на сервере.

    Удостоверьтесь, также, что значение параметра session.use_cookies установлено в On.


    Как убрать из URL страницы знак вопроса?

    Для этого необходимо выполнить следующие шаги:

    • создать в каталоге /news/ файл .htaccess со следующим содержимым:
      ErrorDocument 404 /news/404.php
    • создать в каталоге /news/ файл 404.php со следующим содержимым:
      <?
      $arrPath = pathinfo($_SERVER["REQUEST_URI"]);
      function initialize_params($url)
      {
      
      if (strpos($url,"?")>0)
      {
      $par = substr($url,strpos($url,"?")+1,strlen($url));
      $arr = explode("#",$par);
      $par = $arr[0];
      $arr1 = explode("&",$par);
      
      foreach ($arr1 as $pair)
      {
      $arr2 = explode("=",$pair);
      global $$arr2[0];
      $$arr2[0] = $arr2[1];
      }
      
      }
      
      }
      
      initialize_params($_SERVER["REQUEST_URI"]);
      require($_SERVER["DOCUMENT_ROOT"]."/bitrix/modules/main/include/prolog_before.php");
      $arr = explode("?",$arrPath["basename"]);
      $fname = $arr[0];
      
      if (strlen(trim($arrPath["extension"]))>0)
      {
      $arr = explode(".",$fname);
      $NEWS_ID = intval($arr[0]);
      
      if ($NEWS_ID>0)
      {
      $ID = $NEWS_ID;
      $APPLICATION->SetTitle("News Details");
      $sapi = php_sapi_name();
      if ($sapi=="cgi") header("Status: 200 OK"); else header("HTTP/1.1 200 OK");
      require_once($_SERVER["DOCUMENT_ROOT"]."/bitrix/modules/iblock/iblock.php");
      CIblock::ShowPanel($IBLOCK_ID, $ID);
      include($_SERVER["DOCUMENT_ROOT"]."/bitrix/php_interface/include/news/news_detail.php"); // интерфейсный скрипт, который вызывается
                                                                                              //и в /news/detail.php
      }
      
      }
      require($_SERVER["DOCUMENT_ROOT"]."/bitrix/modules/main/include/epilog.php");
      ?>

    Модуль Поиск

    Модуль Поиск включает в себя сквозной индекс по разным сущностям, отдельные фильтры по атрибутам сущностей, поддержку стемминга, точный поиск по атрибутам и связанным сущностям.

    Но появилась потребность изменить поиск в связи с новым пользовательским сценарием работы. Это новый фильтр, в котором помимо атрибутов (где мы ищем точно), появился произвольный поиск по строке (где пользователь может набирать произвольные комбинации символов). Таким образом, новый поиск:

    • должен быть интерактивным в конкретной сущности;
    • комбинировать поиск по тексту и фильтр по атрибутам;
    • быть быстрым (так называемым поиском «на кончиках пальцев»);
    • результатом поиска должно являться текущее представление сущности.

    Следовательно, возкникла задача: как эффективно ускорить, изменить и скрестить поиск с фильтрами? Для ее решения было предложено 3 варианта:

    1. Интегрировать текущий модуль поиска с фильтрами по сущностям - реализовать оказалось непросто и поиск не будет быстрым.
    2. Перейти на внешний поисковый индекс (Sphinx, Lucene) - результаты тестирования показали хорошую индексацию по атрибутам, тексту, но сложно скрестить результаты поиска с их представлениями в списке. Дополнительная сложность - это десятки и сотни тысяч индексов для облачных версий Битрикс24.
    3. Полнотекстовый индекс Mysql - оказался подходящим вариантом, о нем поговорим ниже.

    Что такое полнотекстовый индекс Mysql?

    • Индекс строится по одному или нескольким текстовым полям.
    • Mysql разбивает содержимое полей на «слова» и заносит их в отдельные таблицы для построения «обратного» индекса.
    • При построении запроса таблицы с индексами прозрачно «джоинятся» к основному запросу.
    • Поиск возможен на естественном языке или в режиме boolean.

    Как использовать индекс?

    Для этого существует оператор MATCH (col1,col2,...) AGAINST (expr [search_modifier]). У него есть модификаторы, которые позволяют определить нам на каком языке искать:

    • IN NATURAL LANGUAGE MODE - на естественном языке;
    • IN BOOLEAN MODE - в режиме boolean;
    • WITH QUERY EXPANSION - в этом режиме Mysql делает 2 запроса: сначала ищет по первому запросу, находит записи, затем во второй запрос подставляет строчки из первого. Таким образом, сильно расширяется область поиска.

    Запрос обычно выглядит следующим образом:

    SELECT * FROM articles WHERE MATCH (title,body) AGAINST ('database' IN NATURAL LANGUAGE MODE);
    

    Отметим особенности поиска Mysql:

    • При запросе на натуральном языке Mysql сам сортирует результат по релевантности.
    • Двойной поиск с параметром WITH QUERY EXPANSION.
    • Поиск по умолчанию регистронезависимый.
    • В качестве expr может использоваться только литерная строка.

    При поиск в режиме boolean можно использовать дополнительные параметры:

    • + Должно быть
    • - Не должно быть
    • (no operator) Или
    • @distance Расстояние
    • >~< Вес
    • ( ) Группировка
    • * Маска (проставляется только справа)

    Типичный запрос выглядит так:

    SELECT * FROM articles WHERE MATCH (title,body) AGAINST ('+MySQL -YourSQL' IN BOOLEAN MODE);
    

    Быстродействие поиска Mysql:

    • Полнотекстовый индекс существенно оптимизирован: кеширование на модификацию, partitioning (разбиение на несколько таблиц индекса).
    • Быстрее LIKE на один-несколько порядков.
    • Быстрее нашего модуля поиска в 2-10 раз за счет оптимизации.
    • Возможен реально интерактивный поиск.

    Требования к поиску:

    • Только Innodb (c 5.6) и MyISAM.
    • Не поддерживаются таблицы с «партициями».
    • Не поддерживаются иероглифы и некоторые кодировки (ucs2).

    Использование в продукте

    1. Создание индекса:

      В инсталляторе модуля новый файл install_ft.sql с содержимым:

      CREATE fulltext index IXF_B_USER_INDEX_1 on b_user_index (SEARCH_USER_CONTENT);
      
      В install.php код:
      $errors = $DB->RunSQLBatch($_SERVER["DOCUMENT_ROOT"]."/bitrix/modules/main/install/mysql/install_ft.sql");			
      if ($errors === false)			
      {				
      	$entity = \Bitrix\Main\UserTable::getEntity();
      	$entity->enableFullTextIndex("SEARCH_USER_CONTENT");			
      }
      
    2. Индексирование:
      • Можно создать либо текстовую колонку в таблице, либо связанную отдельную таблицу (предпочтительнее для больших существующих таблиц).
      • В индексированную колонку можно записывать объединенные данные с разных колонок (искать «везде»).
      • На одном хите практически невозможно ни заполнить эту колонку, ни построить индекс по имеющимся данным.
      • Рекомендуем пошаговый индексатор на агентах.
    3. Индексатор:
      • Базовый класс Bitrix\Main\Update\Stepper запускает агенты и предоставляет графический интерфейс.
      • Необходимо реализовать метод execute(), который выполняет реальную работу в пошаговом режиме.
      • Метод getHtml() предоставляет интерфейс с аяксовым «хитователем» - желтый прогресс-индикатор.
      • Метод bind() добавляет агента (в обновлении).
    4. Использование:
      • Добавлены новые операторы в построитель запросов:
        "*" => "FT", // partial full text match
        "*=" => "FTI", // identical full text match
        "*%" => "FTL", // partial full text match based on LIKE
        
      • Необходимо учитывать, что индекса может не быть:
        $operation = (LogIndexTable::getEntity()->fullTextIndexEnabled("CONTENT")? '*' : '*%‘);
        
      • В новый фильтр ORM добавлена поддержка match: методы whereMatch(), whereNotMatch(), а также хелпер matchAgainstWildcard().

    Пример обращения к сущности:

    $res = \Bitrix\Main\UserIndexTable::getList(array(
    	"select" => array("SEARCH_ADMIN_CONTENT"),
    	"filter" => array(
    		"*SEARCH_ADMIN_CONTENT" => \Bitrix\Main\Search\Content::prepareStringToken("vad dumbrav"),
        )
    ));
    
    var_dump(\Bitrix\Main\Entity\Query::getLastQuery()) 
    

    Запрос:

    string(216) "SELECT 
    	`main_user_index`.`SEARCH_ADMIN_CONTENT` AS `SEARCH_ADMIN_CONTENT`
    FROM `b_user_index` `main_user_index` 
    
    WHERE MATCH (`main_user_index`.`SEARCH_ADMIN_CONTENT`) AGAINST ('(+inq* +qhzoeni*)' IN BOOLEAN MODE)"; 
    

    Более медленный запрос с LIKE:

    string(277) "SELECT 
    	`main_user_index`.`SEARCH_ADMIN_CONTENT` AS `SEARCH_ADMIN_CONTENT`
    FROM `b_user_index` `main_user_index` 
    
    WHERE ((UPPER(`main_user_index`.`SEARCH_ADMIN_CONTENT`) like '%INQ%' ESCAPE '!' AND UPPER(`main_user_index`.`SEARCH_ADMIN_CONTENT`) like '%QHZOENI%' ESCAPE '!'))"
    

    Особенности по использованию полнотекстового поиска:

    • Минимальная длина слова в индексе (по умолчанию 3). Параметр соединения ft_min_token_size.
    • Словарь стоп-слов. Можно или отключить, или преобразовывать данные методом Bitrix\Main\Search\Content::prepareStringToken().
    • Поиск по части слова справа (звездочка слева). Можно декомпозировать не длинные слова: 123456789, 23456789, 3456789…


    Пользовательские поля

    Пользовательские поля

    Пользовательское поле - инструмент, позволяющий добавлять к объектам системы поля, не предусмотренные штатным функционалом.

    Необходимо отличать Пользовательские поля в модулях системы и свойства используемые в рамках инфоблоков, хотя в формах системы (форма создания/редактирования пользователя, форме создания/редактирования раздела инфоблока и другие) используется термин пользовательские свойства.

    Пользовательские поля это сущность:

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

    Пользовательские поля могут создаваться в неограниченном количестве для каждого объекта. При выборе того или иного типа пользовательского поля становятся доступными дополнительные поля настройки для соответствующего типа. Детально об этом можно узнать в документации.

    Применение пользовательских полей в системе к тем или иным модулям задаётся с помощью объектов, которые необходимо указать при создании поля. Не все модули имеют объекты для пользовательских полей по умолчанию. Разработчик может создавать собственные объекты, но надо понимать, что в методах GetList поддерживаются только системные объекты:

    Штатные объекты пользовательских полей
    Модуль Объект Предназначен дляПродукт
    Главный модуль USER пользователя БУС, КП
    Блоги BLOG_BLOG блога БУС, КП
    BLOG_POSTсообщения в блоге БУС, КП
    BLOG_COMMENTкомментария сообщения БУС, КП
    Задачи TASKS_TASK задач КП
    TASKS_SCRUM_ITEM прикрепления файлов диска КП
    TASKS_TASK_TEMPLATE_CHECKLIST чеклиста в шаблонах КП
    TASKS_TASK_CHECKLIST чеклиста в задачах КП
    TASKS_TASK_TEMPLATE шаблонов КП
    Информационные блокиIBLOCK_N_SECTION секций инфоблока с ID = N БУС, КП
    IBLOCK_N инфоблока с ID = N БУС, КП
    Календарь CALENDAR_EVENT событий календаря КП
    Обучение LEARN_ATTEMPT попыток теста БУС, КП
    Социальная сеть SONET_GROUP групп соцсети БУС, КП
    SONET_COMMENT комментариев БУС, КП
    SONET_LOG логов БУС, КП
    Библиотека документов WEBDAV библиотек документов КП
    Форум FORUM_MESSAGE сообщений форума БУС, КП
    Highload-блоки HLBLOCK_N highload-блока с ID=N БУС, КП
    Торговый каталог PRODUCT товаров БУС, КП
    CAT_STOREскладов БУС, КП
    [ICO_NEW data-adding-timestamp="1702552775"]CAT_STORE_DOCUMENT_A[/ICO_NEW]документов складского учёта «Приход товара на склад» БУС, КП
    [ICO_NEW data-adding-timestamp="1702552775"]CAT_STORE_DOCUMENT_S[/ICO_NEW]документов складского учёта «Оприходование товара» БУС, КП
    [ICO_NEW data-adding-timestamp="1702552775"]CAT_STORE_DOCUMENT_M[/ICO_NEW]документов складского учёта «Перемещение товара между складами» БУС, КП
    [ICO_NEW data-adding-timestamp="1702552775"]CAT_STORE_DOCUMENT_R[/ICO_NEW]документов складского учёта «Возврат товара» БУС, КП
    [ICO_NEW data-adding-timestamp="1702552775"]CAT_STORE_DOCUMENT_D[/ICO_NEW]документов складского учёта «Списание товара» БУС, КП
    [ICO_NEW data-adding-timestamp="1702552775"]CAT_STORE_DOCUMENT_U[/ICO_NEW]документов складского учёта «Отмена резервирования» БУС, КП
    Корзина RECYCLEBIN_DISK элементов корзины КП
    CRM CRM_MAIL_TEMPLATE шаблонов писем КП
    CRM_TIMELINE Таймлайна КП
    CRM_LEAD Лидов КП
    CRM_DEAL Сделок КП
    CRM_COMPANY Компаний КП
    CRM_CONTACT Контактов КП
    CRM_ORDER Заказов КП
    CRM_INVOICE Счетов КП
    CRM_(смарт-процесс) Смарт-процессов КП
    CRM_ACTIVITY действий бизнес-процессов КП
    CRM_QUOTE Коммерческих предложений КП
    CRM_LEAD_SPD Лидов с привязкой к элементам корзины КП
    CRM_DEAL_SPD Сделок с привязкой к элементам корзины КП
    CRM_COMPANY_SPD Компаний с привязкой к элементам корзины КП
    CRM_CONTACT_SPD Контактов с привязкой к элементам корзины КП
    CRM_ORDER_SPD Заказов с привязкой к элементам корзины КП
    CRM_INVOICE_SPD счетов с привязкой к элементам корзины КП
    CRM_(смарт-процесс)_SPD Смарт-процессов с привязкой к элементам корзины КП
    CRM_ACTIVITY_SPD действий БП с привязкой к элементам корзины КП
    CRM_QUOTE_SPD Коммерческих предложений с привязкой к элементам корзины КП
    Роботизация бизнеса (RPA) RPA_(id процесса) робота КП
    RPA_COMMENT комментария к роботу КП

    Примечание: Модули, использующие информационные блоки, могут работать с объектами пользовательских полей модуля Информационные блоки.

    Создание полей

    Создание пользовательских полей из Административной части выполняется на странице Настройки > Настройки продукта > Пользовательские поля либо, что предпочтительнее, с использованием ссылки Добавить пользовательское свойство в тех формах системы, в которых предусмотрено штатное добавление пользовательских свойств:

    • форма добавления/редактирования пользователя;
    • форма добавления/редактирования раздела информационного блока;
    • форма добавления/редактирования блога;
    • форма добавления/редактирования сущности CRM;
    • форма добавления/редактирования RPA процесса.

    Использовать страницу Пользовательские поля можно в случае, если разработчик точно знает, какой идентификатор типа объектов ему нужен.

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

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

    Пользовательские поля могут создаваться с разными типами данных. По умолчанию в системе предусмотрены следующие типы:

    • Число;
    • Да/Нет;
    • Видео;
    • Деньги;
    • Шаблон;
    • Список;
    • Строка;
    • Дата;
    • Дата со временем;
    • Привязка к элементам highload-блоков (Для коробочной версии Битрикс24);
    • Привязка к разделам инф. блоков;
    • Привязка к элементам инф. блоков;
    • Привязка к элементам CRM (Для коробочной версии Битрикс24);
    • Привязка к справочникам CRM (Для коробочной версии Битрикс24);
    • Файл;
    • Целое число.
    • Документ из библиотеки документов (Для коробочной версии Битрикс24);
    • Привязка к сотруднику (Для коробочной версии Битрикс24);
    • Документ истории из библиотеки документов
    • Ссылка
    • Адрес
    • Бронирование ресурсов
    • Версия файла (Диск) (Для коробочной версии Битрикс24)
    • Письмо
    • Содержимое ссылки
    • Файл (Диск) (Для коробочной версии Битрикс24)
    • Опрос

    Как правило, этих типов вполне хватает для работы. Если есть необходимость создания собственных типов данных, то это можно сделать самостоятельно. Пример добавления типов данных "Связь с элементом" и "Связь с элементом в виде списка" (блог).

    Для работы с пользовательскими полями можно использовать События.

    События Главного модуля при работе с пользовательскими полями:

    Событие Вызывается Метод
    OnUserTypeBuildList при построении списка пользовательских полей CUserTypeManager::GetUserType
    OnUserTypeRightsCheck при проверке прав доступа на пользовательские поля GetRights.

    Примеры работы

    Рассмотрим несколько примеров работы с пользовательскими полями.

    Фильтрация и сортировка

    Фильтрация

    Пользовательские поля разделов могут принимать участие в фильтрации.

    $sec_Filter= array(  
    	"IBLOCK_ID" => $IBLOCK_ID, 
    	"DEPTH_LEVEL" => "2", 
    	"!UF_ARC_PAGES" => ""
    );

    Примечание: Фильтрация по пользовательским полям работает только при наличии фильтра по IBLOCK_ID.

    Будут отобраны все разделы, у которых установлено значение свойства UF_ARC_PAGES.

    Фильтрация по значению пользовательского свойства:

    $arSFilter ['=UF_USERS_PROPERTY'] =$users_property_value;

    Сортировка

    Сортировать по пользовательским полям разделов:

    $arSort = array(
    	"UF_RATING"=>"asc",
    	"sort"=>"asc"
          
    );

      Получение значений

    Получить значение пользовательского поля можно с помощью метода GetList соответствующего класса.

    Значение пользовательского поля для пользователя можно получить таким образом:

    $rsUser = CUser::GetByID($user);
    $arUser = $rsUser->Fetch();
    $нужное значение = $arUser['код пользовательского поля']; 

    Чтобы получить значение пользовательского поля определенного пользователя, тип поля – строка, необходимо воспользоваться методом GetList класса CUser. При этом в качестве четвертого аргумента данному методу необходимо передать массив с ключом SELECT, значениями которого являются список кодов пользовательских свойств, которые необходимо получить.

    global $USER;
    $arFilter = array("ID" => $USER->GetID());
    $arParams["SELECT"] = array("UF_USER_CARD_CODE");
    $arRes = CUser::GetList($by,$desc,$arFilter,$arParams);
    	if ($res = $arRes->Fetch()) {
    		echo $res["UF_USER_CARD_CODE"];
    	}

    Если тип пользовательского поля список, то для получения значения (или значений, если возможен множественный выбор) текущего пользователя нужно воспользоваться методом GetList класса CUserFieldEnum.

    global $USER;
    $arFilter = array("ID" => $USER->GetID());
    $arParams["SELECT"] = array("UF_LIST_TASK");
    $arRes = CUser::GetList($by,$desc,$arFilter,$arParams);
    	if ($res = $arRes->Fetch()) {
    		foreach ($res["UF_LIST_TASK "] as $id) {
    			$rsRes= CUserFieldEnum::GetList(array(), array(
    				"ID" => $id,
    			));
    			if($arGender = $rsRes->GetNext())
          		              echo $arGender["VALUE"];
    		}   
    }

    Если необходимо получить список всех значений пользовательского поля объекта USER типа список, то следует воспользоваться следующим кодом:

    global $USER_FIELD_MANAGER;
    $arFields = $USER_FIELD_MANAGER->GetUserFields("USER");
    $obEnum = new CUserFieldEnum;
    $rsEnum = $obEnum->GetList(array(), array("USER_FIELD_ID" => $arFields["UF_LIST_TASK "]["ID"]));
    while($arEnum = $rsEnum->GetNext()){
    	echo $arEnum["VALUE"];
    }

    Для выбора значения пользовательского поля у раздела информационного блока можно воспользоваться методом CIBlockSection:GetList:

    	
    $aSection   = CIBlockSection::GetList( array(), array(
    	'IBLOCK_ID'         => 3,
    	'CODE'          => 'test_section',
    ), false, array( 'UF_DEV2DAY_FIELD' ) )->Fetch();
    

    Примечание: Передача идентификатора инфоблока (IBLOCK_ID) обязательна, иначе выборка пользовательских свойств не будет осуществлена.


    Получение значения пользовательского поля типа файл конкретного раздела инфоблока:

    $iblockID = 1;
    $sectionID = 10; // ID секции
    $propertyCode = 'UF_MY_FILE'; // Код свойства
    
    $rsResult = CIBlockSection::GetList(array("SORT" => "ASC"), array("ID" => $sectionID, "IBLOCK_ID" => $iblockID), false, $arSelect = array("ID", "IBLOCK_ID", $propertyCode));
    	if ($arResult = $rsResult -> GetNext())
    	{
    		print_r($arResult);
    	}

    Так как пользовательские поля можно использовать не только с разделами информационного блока, но и с любыми другими сущностями, то для выбора значений по идентификатору сущности используется класс CUserTypeManager. Экземпляр данного класса уже находится в глобальной переменной $USER_FIELD_MANAGER.

    	
    global $USER_FIELD_MANAGER;
     
    $aSection   = CIBlockSection::GetList( array(), array(
    	'IBLOCK_CODE' => 'shop_news',
    	'CODE' => 'test_section',
    ) )->Fetch();
     
    if( !$aSection ) {
    	throw new Exception( 'Секция не найдена' );
    }
     
    $aUserField = $USER_FIELD_MANAGER->GetUserFields(
    'IBLOCK_3_SECTION',
    $aSection['ID']
    ); // array

    В результате мы получим массив содержащий в себе всю информацию о поле и его значении для конкретного объекта.

    Примечание: Чтобы получить все значения пользовательских полей в параметре arSelect достаточно указать Array("UF_*").

      Использование пользовательских свойств

    Использование пользовательских свойств на примере дополнительных полей в подписке

    Задача: подписчиком будет указываться пол, и в зависимости от этого выбора в письме рассылки она или он получит "Уважаемая" или "Уважаемый" в качестве обращения.

    В решении используется демо дистрибутив с настроенным функционалом рассылки. Для использования описанного функционала на ваших проектах нужно провести работы:

    • Размещение компонента Форма подписки (bitrix:subscribe.form) в шаблоне сайта.
    • Настройка компонента на страницу редактирования подписки пользователя.
    • Создание, при необходимости, рубрик подписки.
    • Создать раздел управления подписками.
    • Размещение компонента Страница рассылок (bitrix:subscribe.index) и настройка ее на страницу редактирования подписки пользователя
    • Создание страницы редактирования подписки пользователя и размещение на ней компонента Страница редактирования подписки (bitrix:subscribe.edit).
    • Настройка модуля Подписка

    Решение. Зададим идентификатор сущности к которой будут привязываться значения дополнительных свойств: MY_SUBSCRIPTION. В качестве уникального идентификатора объектов этой сущности будут выступать b_subscription.ID.

    На странице Пользовательские поля (Настройки > Настройки продукта > Пользовательские поля) откроем форму создания нового поля.

    Заполните поля:

    • Тип данных - Список
    • Объект - MY_SUBSCRIPTION
    • Код поля - UF_GENDER

    Остальные поля не заполняем, нажимаем кнопку Применить.

    На вкладке Список задаем возможные значения: Женский и Мужской. Применяем внесенные изменения.

    Кастомизиция компонента subscribe.edit

    После копирования компонента в свое пространство имён заменяем вызов на странице /personal/subscribe/subscr_edit.php на путь к копированному компоненту.

    Для вывода значения пользовательских свойств подписки в файле component.php после

    $arResult["ALLOW_REGISTER"] = $bAllowRegister?"Y":"N"; 
    добавляем чтение значений из базы данных
    $arResult["USER_PROPERTIES"] = $GLOBALS["USER_FIELD_MANAGER"]->GetUserFields(
    	"MY_SUBSCRIPTION",
    	$arResult["ID"],
    	LANGUAGE_ID
    ); 

    В файле setting.php шаблона выводим примерно следующее:

    <table> 
    <?foreach ($arResult["USER_PROPERTIES"] as $FIELD_NAME => $arUserField):?> 
    	<tr> 
    	<td><?echo $arUserField["EDIT_FORM_LABEL"]?>:</td> 
    	<td><?$APPLICATION->IncludeComponent( 
    		"bitrix:system.field.edit", 
    		$arUserField["USER_TYPE"]["USER_TYPE_ID"], 
    		array(
    			"bVarsFromForm" => false,
    			"arUserField" => $arUserField
    		),
    		null,
    		array("HIDE_ICONS"=>"Y"));?></td> 
    	</tr> 
    <?endforeach;?> 
    </table> 
    
    Для сохранения значений в базе данных в файле component.php после строк
    if($ID>0) 
    { 
    	... 
    	$res = $obSubscription->Update($ID, $arFields); 
    	... 
    } 
    else 
    { 
    	... 
    	$ID = $obSubscription->Add($arFields); 
    	... 
    } 
    добавляем код установки значений свойств
    if($res && $ID > 0) 
    { 
    	global $USER_FIELD_MANAGER; 
    	$arUserFields = $USER_FIELD_MANAGER->GetUserFields("MY_SUBSCRIPTION"); 
    	$arFields = array(); 
    	foreach($arUserFields as $FIELD_ID => $arField) 
    		$arFields[$FIELD_ID] = $_REQUEST[$FIELD_ID]; 
    	$USER_FIELD_MANAGER->Update("MY_SUBSCRIPTION", $ID, $arFields); 
    } 

    Для полноты действий у данного поля в административной части указываем:

    • Обязательное - Да
    • Подпись - Пол

    Создайте новую подписку (или отредактируйте уже существующую) и укажите пол подписчика.

    Теперь надо сделать так, чтобы #GENDER_HELLO# будет заменяться на Уважаемая/Уважаемый в зависимости от пола. Создаем обработчик события BeforePostingSendMail:

    <? 
    // файл /bitrix/php_interface/init.php 
    // регистрируем обработчик 
    AddEventHandler("subscribe", "BeforePostingSendMail", Array("MyClass", "BeforePostingSendMailHandler")); 
    class MyClass 
    { 
    	// создаем обработчик события "BeforePostingSendMail" 
    	function BeforePostingSendMailHandler($arFields) 
    	{ 
    		$rs = CSubscription::GetByEmail($arFields["EMAIL"]); 
    		if($ar = $rs->Fetch()) 
    		{ 
    			global $USER_FIELD_MANAGER; 
    			$arUserFields = $USER_FIELD_MANAGER->GetUserFields("MY_SUBSCRIPTION", $ar["ID"]); 
    
    			if($arUserFields["UF_GENDER"]["VALUE"] == 1) 
    				$arFields["BODY"] = str_replace("#GENDER_HELLO#", "Уважаемая", $arFields["BODY"]); 
    			elseif($arUserFields["UF_GENDER"]["VALUE"] == 2) 
    				$arFields["BODY"] = str_replace("#GENDER_HELLO#", "Уважаемый", $arFields["BODY"]); 
    			else 
    				$arFields["BODY"] = str_replace("#GENDER_HELLO#", "", $arFields["BODY"]); 
    		}
    		else 
    		{ 
    			$arFields["BODY"] = str_replace("#GENDER_HELLO#", "", $arFields["BODY"]); 
    		} 
    		return $arFields; 
    	} 
    } 
    ?>

    Добавление, редактирование, удаление пользовательских свойств и их значений

    За работу с пользовательскими полями в D7 отвечает класс UserField, в старом ядре: CUserTypeEntity.

    Пример добавления пользовательского свойства типа Строка

    /**
    * Добавление пользовательского свойства
    */
    $oUserTypeEntity    = new CUserTypeEntity();
     
    $aUserFields    = array(
    /*
    *  Идентификатор сущности, к которой будет привязано свойство.
    * Для секция формат следующий - IBLOCK_{IBLOCK_ID}_SECTION
    */
    	'ENTITY_ID' => 'IBLOCK_3_SECTION',
    /* Код поля. Всегда должно начинаться с UF_ */
    	'FIELD_NAME' => 'UF_DEV2DAY_FIELD',
    	/* Указываем, что тип нового пользовательского свойства строка */
    	'USER_TYPE_ID' => 'string',
    /*
    * XML_ID пользовательского свойства.
    * Используется при выгрузке в качестве названия поля
    */
    'XML_ID' => 'XML_ID_DEV2DAY_FIELD',
    /* Сортировка */
    'SORT' => 500,
    /* Является поле множественным или нет */
    'MULTIPLE' => 'N',
    /* Обязательное или нет свойство */
    	'MANDATORY' => 'N',
    /*
    * Показывать в фильтре списка. Возможные значения:
    * не показывать = N, точное совпадение = I,
    * поиск по маске = E, поиск по подстроке = S
    */
    	'SHOW_FILTER' => 'N',
    /*
    * Не показывать в списке. Если передать какое-либо значение,
    * то будет считаться, что флаг выставлен.
    */
    	'SHOW_IN_LIST' => '',
    /*
    * Пустая строка разрешает редактирование.
    * Если передать какое-либо значение, то будет считаться,
    * что флаг выставлен.
    
    */
    	'EDIT_IN_LIST' => '',
    	/* Значения поля участвуют в поиске */
    	'IS_SEARCHABLE' => 'N',
    	/*
    * Дополнительные настройки поля (зависят от типа).
    * В нашем случае для типа string
    */
    	'SETTINGS'  => array(
    	/* Значение по умолчанию */
    	'DEFAULT_VALUE' => '',
    	/* Размер поля ввода для отображения */
    	'SIZE'  => '20',
    /* Количество строчек поля ввода */
    	'ROWS'  => '1',
    	/* Минимальная длина строки (0 - не проверять) */
    	'MIN_LENGTH'  => '0',
    /* Максимальная длина строки (0 - не проверять) */
    	'MAX_LENGTH'  => '0',
    	/* Регулярное выражение для проверки */
    	'REGEXP' => '',
    ),
    /* Подпись в форме редактирования */
    	'EDIT_FORM_LABEL'   => array(
    		'ru'    => 'Пользовательское свойство',
    		'en'    => 'User field',
    	),
    /* Заголовок в списке */
    	'LIST_COLUMN_LABEL' => array(
    		'ru'    => 'Пользовательское свойство',
    		'en'    => 'User field',
    	),
    /* Подпись фильтра в списке */
    	'LIST_FILTER_LABEL' => array(
    		'ru'    => 'Пользовательское свойство',
    		'en'    => 'User field',
    ),
    /* Сообщение об ошибке (не обязательное) */
    	'ERROR_MESSAGE'     => array(
    		'ru'    => 'Ошибка при заполнении пользовательского свойства',
    		'en'    => 'An error in completing the user field',
    ),
    /* Помощь */
    	'HELP_MESSAGE'      => array(
    		'ru'    => '',
    		'en'    => '',
    	),
    );
     
    $iUserFieldId   = $oUserTypeEntity->Add( $aUserFields ); // int
    
    

    При удачном добавлении свойства в переменную $iUserFieldId будет возвращен идентификатор нового пользовательского свойства.

    Для создания пользовательских полей других типов замените значение USER_TYPE_ID:

    • enumeration - Список
    • double - Число
    • integer - Целое число
    • boolean - Да/Нет
    • string - Строка
    • file - Файл
    • video - Видео
    • datetime - Дата/Время
    • iblock_section - Привязка к разделам инф. блоков
    • iblock_element - Привязка к элементам инф. блоков
    • string_formatted - Шаблон
    • crm - Привязка к элементам CRM
    • crm_status - Привязка к справочникам CRM

    Пример добавления значений в пользовательские поля типа Список.

    $obEnum = new CUserFieldEnum();
    $obEnum->SetEnumValues($newID, $arAddEnum);
    
    //мы передаем массив, который состоит из таких элементов (наличие n в ключе обязательно)
    $arAddEnum['n'.$i] = array(
    	'XML_ID' => $key,//xml_id
    	'VALUE' => $value,//значение
    	'DEF' => 'N',//по умолчанию
    	'SORT' => $i*10//сортировка
    );

    Обновление пользовательского свойства

    При обновлении пользовательского свойства накладываются ограничения на изменение его типа (USER_TYPE_ID), объекта привязки (ENTITY_ID), кода поля (FIELD_NAME). Это связано с возможными ошибками связей значений и сущностей. Если необходимо изменить одно из этих полей, то нужно сначала создать новое пользовательское свойство, привязать все значения к нему, а затем удалить старое свойство.

    Пример обновления пользовательского свойства:

    $oUserTypeEntity->Update( $iUserFieldId, array(
    	'MANDATORY' => 'Y',
    ) ); // boolean;

    В примере установлено требование обязательности заполнения поля.


    Удаление пользовательского поля

    Необходимо передать идентификатор пользовательского поля:

    	
    $oUserTypeEntity->Delete( $iUserFieldId );   // CDBResult

    Добавление и обновление значений пользовательских полей

    Добавление и обновление реализовано также через класс CUserTypeManager и метод Update.

    global $USER_FIELD_MANAGER;
     
    $aSection   = CIBlockSection::GetList( array(), array(
    	'IBLOCK_CODE'   => 'shop_news',
    	'CODE'          => 'test_section',
    ) )->Fetch();
     
    if( !$aSection ) {
    	throw new Exception( 'Секция не найдена' );
    }
     
    $USER_FIELD_MANAGER->Update( 'IBLOCK_3_SECTION', $aSection['ID'], array(
    	'UF_DEV2DAY_FIELD'  => 'updated value'
    ) ); // boolean

    В случае успешного обновления метод вернет true.

    Добавить пользовательское свойство к разделу инфоблока

    Если необходимо добавить пользовательское свойство к разделу инфоблока через механизм Битрикс-API, нужно использовать следующий код с вариациями:

    $arFields = Array(
    "ENTITY_ID" => "IBLOCK_2_SECTION",
    "FIELD_NAME" => "UF_TITLE",
    "USER_TYPE_ID" => "string",
    "EDIT_FORM_LABEL" => Array("ru"=>"заголовок", "en"=>"title")
    );
    $obUserField  = new CUserTypeEntity;
    $obUserField->Add($arFields);

    Поля к нештатным объектам и новые объекты

    Создание пользовательского поля к нештатным объектам

    Иногда возникает необходимость создавать пользовательские поля к объектам, у которых нет поддержки пользовательских полей по умолчанию. В таком случае, можно самостоятельно создать [dw]пользовательское свойство для этого объекта[/dw][di]Помните при этом, что в методах GetList поддерживаются только системные объекты.[/di].

    Рассмотрим это на примере комментариев блога. Например, у каждого комментария необходимо создавать свойство Рейтинг. Создаем в административной части пользовательское свойство нужного типа (Настройки > Настройки продукта > Пользовательские поля). Заполняем все поля, в поле Объект указываем любое придуманное имя объекта, главное, чтобы оно было уникально. В нашем случае, напишем BLOG_RATING. Для считывания и записи значений пользовательских свойств можно использовать следующие функции:

    function SetUserField ($entity_id, $value_id, $uf_id, $uf_value) //запись значения
    {
    return $GLOBALS["USER_FIELD_MANAGER"]->Update ($entity_id, $value_id,
    Array ($uf_id => $uf_value));
    }
     
    function GetUserField ($entity_id, $value_id, $uf_id) //считывание значения
    {
    $arUF = $GLOBALS["USER_FIELD_MANAGER"]->GetUserFields ($entity_id, $value_id);
    return $arUF[$uf_id]["VALUE"];
    }
     
    // $entity_id - имя объекта (у нас "BLOG_RATING")
    // $value_id - идентификатор элемента (вероятно, ID элемента, свойство которого мы сохраняем или получаем. в нашем случае, это ID комментария)
    // $uf_id - имя пользовательского свойства (в нашем случае UF_RATING)
    // $uf_value - значение, которое сохраняем

    Пример использования:

    SetUserField ("BLOG_RATING", $CommentID, "UF_RATING", $Rating);
    echo "Рейтинг комментария: ".GetUserField ("BLOG_RATING", $CommentID, "UF_RATING");

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

    Создание собственного объекта

    Можно создать любой объект и работать с ним как вам удобно. Пример:

    $GLOBALS["USER_FIELD_MANAGER"]->Update("GRADEBOOK_RESULT", $ID, Array("UF_TEACHERS"=>$arValue));
    $arUserFields = $GLOBALS["USER_FIELD_MANAGER"]->GetUserFields("GRADEBOOK_RESULT", $ID);

    Тестирование проектов

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

    Контрольный список (перечень, таблица, карта; англ. checklist) — список факторов, свойств, параметров, аспектов, компонентов, критериев или задач, структурированных особым образом с целью достижения поставленных задач.

    В процессе тестирования проверяется много важных мелочей, которые повторяются из проекта в проект. Программисты часто говорят, что помнят их - на самом деле опыт показывает, что не помнят, пропускают важные моменты и занимаются "наступанием на грабли". Не зря опытные руководители проектов закрывают этапы/релизы только при наличии оформленных ответственными сотрудниками чеклистов-отчетов: начиная от верстальщиков, заканчивая системными администраторами.

    Чеклисты обычно составляются опытными сотрудниками. По мере накопления интеллектуального багажа компании в базах знаний, развиваются и совершенствуются и чеклисты. Суть проста — сэкономить время и деньги, не позволив сотрудникам непроизвольно (а иногда и произвольно) наступать на грабли 2 и более раз.

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

    • Поиск. Если на сайте используется форма поиска, то её следует протестировать в первую очередь. Обязательно надо проверить, чтобы ссылки в результатах не были битые. (Битые ссылки присутствуют в анонсах, в подробных описаниях, просто в статическом контенте, в настройках инфоблоков.) Если на сайте используется несколько инфоблоков с динамической информацией, то нужно сделать поисковые запросы, чтобы найти по отдельности элементы различных инфоблоков и проверить, корректно ли работают ссылки в результатах поиска.
    • F5. Во всех формах, в которых пользователь может оставлять какие-либо данные (например, комментарии), нужно проверить, не отправляются ли данные снова при нажатии кнопки F5 в браузере.
    • Пользователи и права доступа. Следует протестировать сайт в трёх состояниях: неавторизованным, авторизованным и авторизованным под администратором. Часто бывает, что создавая инфоблок из-под администратора, ему забывают поставить нужные права доступа, и он остается доступным только для администраторов.
    • Карта сайта Не всегда для этой страницы подойдёт стандартный компонент Карта сайта. Если основная информация на сайте – каталог товаров (сделанный, естественно, с помощью модуля инфоблоков), то в карте сайта логично вывести разделы каталога, а не только разделы сайта.
    • Дефолтные шаблоны. Необходимо проверить, не используются ли где-нибудь на сайте дефолтные шаблоны. Может получиться не очень хорошо, если, например, пользователь пытается восстановить пароль, ему на почту уходит письмо со ссылкой на сайт, он нажимает на ссылку и видит не привычный ему дизайн сайта, а пример дефолтного «корпоративного» сайта "1С-Битрикс: Управление сайтом".
    • Стили в визуальном редакторе. Хорошо, когда контент-менеджер, пользуясь визуальным редактором, видит в нём текст, максимально похожий на текст, который отобразится на сайте. То есть для абзацев, заголовков и т.д. свой кегль шрифта, цвет и т.д.
    • Динамическая информация во включаемых областях Если во включаемой области содержится какая-либо динамическая информация, то она может закешироваться, что скорее всего приведет к нежелательному результату. Лучше вообще не использовать динамическую информацию во включаемой области.
    • Резервная копия, обновления. Необходимо обновить Битрикс до актуальной версии, сделать резервную копию сайта и сохранить её на локальный компьютер.
    • Кеш. Следует проверить, включено ли автокеширование. Если оно не включено, то нужно его включить и повторить всё тестирование с самого начала
    • Права доступа. Проверка на корректность прав доступа различных групп пользователей, генерация отчета по правам групп в папках сайта.
    • Ядро системы. Проверка работы ядра и соответствие его оригиналу с генерацией отчета о папках компонентах и модулях не соответствующих оригиналу и генерации отчета о несоответствии версий. Иногда шаловливые ручки позволяют себе править непосредственно компоненты Bitrix Framework и, чтобы не порушить функционал сайта, очень полезно перед тем как обновлять проект узнать что наделано не так и что наделано дополнительно.
    • Неиспользуемые файлы. Часто при разработке или изменениях делают резервные копии и хорошо если делают приставку old а иногда просто весь этот мусор остается, плюс неиспользуемые картинки и т.п.
    • Сторонние подключения. Часто к сайту подключают различные партнерские программы: баннеры, скрипты и т.п., вещи без которых невозможна монетизация. Но при этом может пострадать безопасность. Необходимо проверять подобный функционал на предмет безопасности и наличия уязвимостей (доступ на редактирование, доступ к ядру, возможность внедрения кода, сохранения через подключаемый модуль файлов)
    Из опыта веб-разработчиков.

    Роман Петров: Один из простых способов протестировать на ошибки разработки ваш сайт на "1С-Битрикс: Управление сайтом" - это перед запуском проекта, включить тест производительности на нужное время и на том же сервере запустить 3-4 одновременных команды wget на зеркалирование сайта. Первое обращение создаст кэш страницы, а остальные - обратятся уже к закэшированным файлам. Это позволит очень легко выявить все проблемные странички (мелкие ошибки "по забывчивости", ошибки верстки (пропущенные файлы и др.)) и исправить ошибки.

    Если у вас еще не достаточно опыта для составления собственного чеклиста, то воспользуйтесь штатным инструментом Монитор качества.


    Монитор качества

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

    Для разрешения этой проблемы создан инструмент Монитор качества. Он позволяет решить задачу обеспечения прозрачного и гибкого процесса сдачи веб-проекта клиенту, повышая уровень гарантированного результата и снижая общие риски.

    Монитор качества - инструмент для проверки качества выполненного проекта перед сдачей его заказчику.

    Монитор качества это:

    • Структурированная методика управления качеством внедрения;
    • Система тестов для веб-разработчиков, набор рекомендаций для клиентов;
    • Состоит из 26 обязательных тестов и 40 необязательных;
    • Включает 14 автоматических проверок.

    Монитор качества дает дополнительные возможности разработчикам и клиентам:

      Разработчикам:
    • Систематизация процедуры тестирования;
    • Повышение качества создания интернет-проектов за счет систематизации производства;
    • Формализация отношений с клиентом как на этапе сдачи, так и на этапе поддержки.
      Клиентам:
    • Снижение рисков: чем раньше найдена проблема, тем дешевле ее устранить;
    • Систематизация приемки проекта и запуска его в эксплуатацию: шаги расписаны, можно сосредоточиться на деталях;
    • Формализация и упрощение взаимодействия с разработчиком на этапе поддержки и развития проекта;
    • Снижение затрат на получение качественного результата;
    • Высокая производительность и безопасность веб-решения.

    Тесты представлены в виде дерева, организованного согласно этапам типичного внедрения. Однако их можно выполнять в любом удобном порядке. Тесты состоят из обязательных и необязательных. Некоторые из них могут быть автоматизированными. Автоматизированы, как правило, сложные и рутинные проверки.

    Обязательный тест – критичный тест для качества веб-решения. Может быть пропущен при условии наличия комментария разработчика. В общем списке помечаются черным цветом.

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

    Для сдачи проекта по чеклисту необходимо добиться успешного прохождения обязательных тестов. Необязательные тесты – призваны существенно улучшить качество решения и снизить риски.

    Автоматизированный тест – тест, данные по которому собирает система. Конечное решение в любом случае за разработчиком. Автоматизированный тест можно запустить повторно.

    Сдача проекта

    Сдача проекта

    Сдача проекта производится на странице Монитор качества (Настройки > Инструменты > Монитор качества). При первом открытии отобразится страница с вводной информацией. Запуск тестов произойдёт после нажатия на кнопку , после чего откроется дерево тестов с предложением запуска автотестирования. Автотестирование можно отложить и запустить его позже.

    Тесты

    Автоматизированный тест

    После запуска автоматизированого теста система соберет данные и предложит отобразить тесты, которые по ее мнению пройдены, а какие - нет:

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

    Ручной тест

    Необходимо открыть непройденные или ручные тесты и вручную перевести их в тот или иной статус и дать, при необходимости описание.

    Прохождение тестов

    Тест открывается кликом по его названию. Форма автоматического теста (в форме ручного теста отсутствует кнопка Запустить автотест):

    Если тест не пройден, то по ссылке Подробный отчет можно просмотреть причины непрохождения:

    В закладке Рекомендации можно посмотреть условия теста, и какие параметры в системе нужно проверить и исправить для правильного его прохождения.

    После исправления ошибок можно:

    • запустить повторно автоматический тест;
    • вручную сменить статус ручного теста.
    Примечание: ручной перевод обязательного теста в статус Пропущен требует обязательного добавления комментария.

    После прохождения всех обязательных тестов веб-проект можно успешно сдать, после чего отчет по тестированию попадает в архив.

    Архив

    Отчеты можно просмотреть в любое время, получив подробную информацию по каждому тесту (в том числе и системные сообщения автотестов).

    Примечание: Отчёт можно послать в компанию "1С-Битрикс" для участия в программе монитора качества. Для этого используйте ссылку Направить в 1С-Битрикс и заполните открывшуюся форму.


    Способы использования

    Монитор качества можно использовать в нескольких вариантах.

    Базовое тестирование по чеклисту

    Партнер/Разработчик организует проверку выполненной интеграции по чеклисту и выступает в роли тестировщика. Затем предъявляет клиенту успешно сданный отчет, доступный в административном интерфейсе в разделе Настройки > Инструменты > Контроль качества – в котором все обязательные тесты успешно пройдены.

    Углубленное тестирование по чеклисту

    Разработчик и клиент договариваются о необходимости углубленного тестирования по чеклисту качества высоконагруженного проекта. Партнер выступает в роли тестировщика и добивается успешного прохождения всех (большинства) тестов чеклиста + своих тестов. Клиент обращает внимание на число доступных тестов и число пройденных успешно, просматривая отчет по тестированию в архиве отчетов.

    Внутренняя разработка

    Клиент силами собственной команды разработки осуществляет интеграцию решения. В роли тестировщика выступает команда тестирования клиента. Одно структурное подразделение клиента сдает проект другому подразделению - работу координирует менеджер проекта.

    Итерационное развитие с минимальными рисками

    Партнер оказывает клиенту услугу по доработке функционала действующего веб-проекта, первоначальная интеграция которого была выполнена с использованием Монитора качества. Партнер сдает работу, формируя отчет по тестированию. Заказчик проверяет отчет и следит, чтобы все доработки веб-проекта регистрировались в архиве отчетов.

    Модификация тестов

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

    Разработчики, при необходимости, могут сами добавить свои тесты и разделы в Монитор качества.

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

    • Тесты по SEO-оптимизации;
    • Тест на CodeStyle;
    • Тест на корректность работы биллинга под нагрузкой;
    • и другие.

    Как дополнить штатные тесты

    Сначала нам необходимо описать свои тесты и их разделы. Для этого вешаем обработчик на событие Главного модуля onCheckListGet. Событие вызывается в конструкторе CCheckList с аргументом $arCheckList следующего вида:

    array(2) {
    	["CATEGORIES"]=>
    	array(10) {
    		["QDESIGN"]=>
    		array(0) {
    		}
    		["DESIGN"]=>
    		array(1) {
    		["PARENT"]=>
    		string(7) "QDESIGN"
    		}
    		["MODEL"]=>
    		array(1) {
    		["PARENT"]=>
    		string(7) "QDESIGN"
    		}
    	["STANDART"]=>
    	array(1) {
    		["PARENT"]=>
    		string(7) "QDESIGN"
    		}
    	}
    	["POINTS"]=>
    	array(65) {
    		["QD0010"]=>
    		array(2) {
    			["PARENT"]=>
    			string(6) "DESIGN"
    		["REQUIRE"]=>
    		string(1) "Y"
    		}
    		["QD0020"]=>
    		array(5) {
    			["REQUIRE"]=>
    			string(1) "Y"
    			["PARENT"]=>
    			string(6) "DESIGN"
    			["AUTO"]=>
    			string(1) "Y"
    			["CLASS_NAME"]=>
    			string(10) "CAutoCheck"
    			["METHOD_NAME"]=>
    			string(14) "CheckTemplates"
    	}
    )

    Нетрудно заметить, что CATEGORIES содержит список разделов чеклиста, которые могут быть вложенными, а POINTS - сами тесты.

    Ключ массива CATEGORIES — ID раздела, значение — массив параметров:

    • NAME - наименование раздела;
    • LINKS
    • PARENT - родительский раздел.

    Пример описания раздела:

    $checkList['CATEGORIES']['ITC_QC'] = array(
    	'NAME' => 'Корпоративный тест качества ITConstruct',
    	'LINKS' => ''
    );

    Ключом элементов POINTS является символьный идентификатор теста, а значение представляет собой массив следующих параметров:

    • NAME - наименование теста;
    • DESC - краткое описание теста (вкладка Рекомендации, блок Описание);
    • HOWTO - длинный текст о том, что будет проверяться (вкладка Рекомендации, блок Как тестировать);
    • LINKS - аналогично разделам;
    • PARENT - ID раздела, обязательное;
    • REQUIRE - флаг обязательности теста (Y/N);
    • AUTO - "Y", если является автотестом;
    • CLASS_NAME - имя класса теста (для автотеста);
    • METHOD_NAME - имя метода теста (для автотеста);
    • FILE_PATH - подключение файла теста, если он вынесен в отдельный скрипт (для автотеста). Путь - относительно DOCUMENT_ROOT;
    • PARAMS - массив дополнительных параметров, передаваемых первым аргументом при вызове метода автотеста.
    Примечание: Такие ключи как NAME, DESC и LINKS можно не определять в массиве описания теста, они являются языкозависимыми и жесткое определение их прямо в обработчике лишает возможности локализации.

    Достаточно подключить свой языковой файл примерно с таким содержанием:

    $MESS["CL_ITC_QC_FAVICON_NAME"] = 'Наличие favicon'; 
    $MESS["CL_ITC_QC_FAVICON_DESC"] = 'Проверка наличия favicon - иконки сайта'; 
    $MESS["CL_ITC_QC_FAVICON_LINKS"] ='блаблабла';
    И эти языковые фразы будут подтягиваться в поля NAME, DESC и LINKS теста с кодом ITC_QC_FAVICON. К HOWTO это тоже будет относиться, но пока его можно определить только в массиве описания пункта.

    Пример:

    $checkList['POINTS']['ITC_QC_FAVICON'] = array(
    	'PARENT' => 'ITC_QC',
    	'REQUIRE' => 'Y',
    	'AUTO' => 'Y',
    	'CLASS_NAME' => __CLASS__,
    	'METHOD_NAME' => 'checkFavicon',
    	'NAME' => 'Наличие favicon',
    	'DESC' => 'Проверка наличия favicon - иконки сайта, отображаемой в заголовке вкладки и поисковых системах',
    	'HOWTO' => 'Производится проверка главной страницы сайта на наличие соответствующего мета-тэга. 
    		Если тэг объявлен - проверяется наличие иконки по указанному урлу. 
    		Если не указан - наличие favicon.ico в корне сайта',
    	'LINKS' => 'links'
    );

    Обработчик события может возвращать как изменённый $arCheckList, так и новый массив с разделами и тестами - если категория/тест с неким ID уже существует, то он не заменится, т.е. подкорректировать системные тесты не получится.

    Теперь необходимо объявить метод для автотеста. Для альфа-версии логика теста будет тривиальна:

    $check = file_exists($_SERVER['DOCUMENT_ROOT'] . '/favicon.ico')

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

    $arResult = array(
    	'IN_PROGRESS' => 'Y',
    	'PERCENT' => '42',
    );

    PERCENT служит лишь для визуализации прогресса на странице и никуда не сохраняется для последующего использования. Промежуточные данные для идентификации прогресса шага надо сохранять самим - в сессию, временный файл, базу (в зависимости от объёма данных и прочих условий).

    Если же тест закончен, сообщаем статус массивом, содержащим следующие ключи:

    • STATUS- результат теста. true, если успешно, нечто иное, если тест провалился. В коде проверяется так:

      if ($result['STATUS'] == "true")

    • MESSAGE - разъяснения результата:
      • PREVIEW - краткий текст результата;
      • DETAIL - расширенное объяснение, открываемое во всплывающем окне.

    Готовый метод теста фавиконки имеет вид:

    static public function checkFavicon($arParams)
    {
    	$arResult = array('STATUS' => 'F');
    	$check = file_exists($_SERVER['DOCUMENT_ROOT'] . '/favicon.ico');
    
    	if ($check === true) {
    		$arResult = array(
    			'STATUS' => true,
    			'MESSAGE' => array(
    			'PREVIEW' => 'Favicon найдена - ' . '/favicon.ico',
    			),
    		);
    	} else {
    		$arResult = array(
    			'STATUS' => false,
    			'MESSAGE' => array(
    				'PREVIEW' => 'Favicon не найдена',
    				'DETAIL' => 'Тест очень старался, но так и не смог найти фавыконку. Ну и чёрт с ней',
    			),
    		);
    	}
    
    	return $arResult;
    }

    Результат

    Тест в мониторе качества:

    Справочная информация о тесте:

    Код

    Код обработчика и тесты целиком:

    AddEventHandler('main', 'OnCheckListGet', array('CItcCheckListTests', 'onCheckListGet'));
    
    class CItcCheckListTests
    {
    	static public function onCheckListGet($arCheckList)
    	{
    		$checkList = array('CATEGORIES' => array(), 'POINTS' => array());
    
    		$checkList['CATEGORIES']['ITC_QC'] = array(
    			'NAME' => 'Корпоративный тест качества ITConstruct',
    		'LINKS' => ''
    		);
    
    		$checkList['POINTS']['ITC_QC_FAVICON'] = array(
    			'PARENT' => 'ITC_QC',
    			'REQUIRE' => 'Y',
    			'AUTO' => 'Y',
    			'CLASS_NAME' => __CLASS__,
    			'METHOD_NAME' => 'checkFavicon',
    			'NAME' => 'Наличие favicon',
    			'DESC' => 'Проверка наличия favicon - иконки сайта, отображаемой в заголовке вкладки и поисковых системах',
    			'HOWTO' => 'Производится проверка главной страницы сайта на наличие соответствующего мета-тэга. 
    				Если тэг объявлен - проверяется наличие иконки по указанному урлу. 
    				Если не указан - наличие favicon.ico в корне сайта',
    			'LINKS' => 'links'
    		);
    
    		$checkList['POINTS']['ITC_QC_DENY_DEV'] = array(
    			'PARENT' => 'ITC_QC',
    			'REQUIRE' => 'N',
    			'AUTO' => 'N',
    			'NAME' => 'Закрытие доступа извне к dev-серверу',
    			'DESC' => 'Согласовать с менеджером закрытие доступа ко внутреннему серверу разработок из внешнего мира',
    			'HOWTO' => 'Попинговать с телефона после апдейта днса',
    		);
    
    		return $checkList;
    	}
    
    	static public function checkFavicon($arParams)
    	{
    		$arResult = array('STATUS' => 'F');
    		$check = file_exists($_SERVER['DOCUMENT_ROOT'] . '/favicon.ico');
    
    		if ($check === true) {
    			$arResult = array(
    			'STATUS' => true,
    			'MESSAGE' => array(
    				'PREVIEW' => 'Favicon найдена - ' . '/favicon.ico',
    				),
    			);
    	} else {
     	           $arResult = array(
    			'STATUS' => false,
    			'MESSAGE' => array(
    				'PREVIEW' => 'Favicon не найдена',
    				'DETAIL' => 'Тест очень старался, но так и не смог найти фавыконку. Ну и чёрт с ней',
    				),
    			);
    	}
    
    	return $arResult;
    	}
    }

    Несколько советов

    Цитатник веб-разработчиков.

    Максим Месилов: Если на текущем проекте сбоит штатный функционал, то следует попробовать смоделировать аналогичную ситуацию в демо-лаборатории от 1С-Битрикс. Эта ссылка должна быть всегда под рукой.

    2 способа отладки веб-приложений

    var_dump – способ

    Самый простой вариант использование оператора var_dump(): получение состава переменной, даже будь это объект или массив. Если обернуть вывод этого оператора в <pre>, то будет удобочитаемо.

    FirePHP – способ

    Есть более технологичный и в конечном итоге удобный способ просмотра содержимого переменных. Для этого нам понадобится установленный браузер FireFox, установленное расширение FireBug и установленное расширение FirePHP.

    Загрузите с этого сайта последнюю версию класса для работы с расширением FirePHP и подлючите этот класс к своему движку:

    1. Скопируйте файл fb.php в папку /bitrix/php_interface/
    2. Добавьте в файл /bitrix/php_inteface/init.php строку:
      require_once(‘FirePHPCore/fb.php’);

    Теперь можно использовать логгирование в консоль FireBug. В простейшем варианте это делается так: fb($var), если нужно поставить метку, то fb($var, ‘Label’);

    Видео

    Скачать Время Размер файла
    Тестирование и нагрузочные испытания - как, чем и зачем 17 минут 43 секунды 500 мБ

    Смотреть

    Senior, выше некуда

    Senior - высший уровень квалификации. Программист этого уровня должен уметь:

    • Читать исходный код.
    • Уметь работать с Базой данных.
    • Разбираться в вопросах производительности и безопасности кода.
    • Знать как можно кастомизировать административный раздел Bitrix Framework.
    • Уметь создавать новые компоненты и модули.

    Производительность

    Производительность сайта - комплексное явление. На скорость открытия страниц влияют:

    1. ресурсы сервера;
    2. настройка серверного ПО;
    3. программная платформа (БУС);
    4. прикладная разработка на платформе.

    Провал производительности может быть на одном или более этапов. Прикладная разработка имеет очень большое значение в этом списке, хотя и не единственное. "Кривые руки" могут свести на нет преимущество любой, самой совершенной платформы и "подвесить" любое, самое мощное "железо".

    Цитатник веб-разработчиков.

    Степан Овчинников: Как всегда: универсальность и функциональность против простоты и производительности.

    Особенности веб-программирования

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

    Основная задача веб-сайта (в 99% случаев) – это быстро вернуть посетителю запрошенную страничку и освободить ресурсы для обслуживания других посетителей. Именно для решения этой задачи существуют такие инструменты, как NGINX, lighthttpd, кэширование, PHP-акселераторы и другие. Для большинства сайтов с небольшой посещаемостью таких инструментов, имеющихся в наличии на практически каждом хостинге или почти в любой CMS, вполне достаточно.

    Однако для более посещаемых проектов возникает необходимость дополнительной оптимизации.


    Bitrix Framework достаточно хорошо ведет себя при большой нагрузке. Есть официальные результаты тестирования, с которыми можно познакомиться на сайте компании. Но нам интересен опыт реальной эксплуатации сложных, ресурсоемких проектов.

    Сложным ресурсоемким проектом можно назвать проект, под 30 000 посещений, с количеством элементов в базе под 500 000 и больше, более 40 типов инфоблоков, самих инфоблоков больше 200. В ключевых инфоблоках по 150-200 свойств.

    Что позволяет работать таким проектам:

    • Инфоблоки выборки из которых можно делать на стандартном API;
    • Кэширование.

    Нюансы, на которые надо обращать внимание:

    • При большом количестве свойств возникают проблемы, могут происходить и сбои: проблемы с изменением свойства элемента из административной части, может не выполняться фильтр по значению.
    • Немного неудобно работать в административной части с таким большим кол-вом данных.

    Самое главное это - проектирование инфоблоков. Также нужно быть внимательными с обработчиками по работе с инфоблоками, и с агентами. И при проектировании необходимо знать и учитывать особенности API. Например, выбирать элементы по IBLOCK_CODE или IBLOCK_TYPE хуже, чем по IBLOCK_ID. Это конкретный пример, таких вариаций очень много.

    Сложные и объемные проекты лучше реализовывать на собственных серверах. При этом рекомендуется использовать 2 сервера: отдельно для базы и для статики и кэша.

    Опыт разработчиков проектов на Bitrix Framework показывает, что около 1 миллиона хитов на проекте – это реальный показатель, если уметь конфигурировать MySQL.

    Есть и своеобразная «вилка»: база данных против надежности жесткого диска. Не кешировать данные - хуже для базы и лучше для диска. То есть если используется очень мощный сервер под БД, и он хорошо сконфигурирован, то это может быть реальной альтернативой файловому кешу.

    Примечание: Для снижения нагрузки к базе данных и удобства работы с системой в Bitrix Framework используется хранение разнообразных данных в файловой системе. Подробнее про файлы и их влияние на производительность можно посмотреть в уроке Структура файлов.


    Ссылки по теме:


    Кеширование при проектировании сайта

    Кеширование

    Цитатник веб-разработчиков.

    Максим Месилов: В 1С-Битрикс есть производительная система кеширования. Она используется как в стандартных компонентах системы, так может быть применена и при самостоятельной разработке. Основная её задача - снизить время отклика сайта и повысить его устойчивость при нагрузках.

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

    Закладывая в ТЗ проекта интенсивное использование технологии кеширования, можно максимально ограничить нагрузку на базу данных. Это позволит нашему проекту выдержать в будущем значительные нагрузки.

    Примечание: Возможность использования кеширования в проекте лучше заранее предусмотреть в ТЗ т.к. его внедрение после запуска проекта обходится значительно дороже и сложнее.

    Для реализации кеширования в системе созданы два класса:

    • CPageCache - для кеширования HTML
    • CPHPCache - для кеширования HTML и PHP переменных

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

    Правильное использование кеширования позволяет увеличить общую производительность сайта на порядок. Однако необходимо учитывать, что неразумное использование кеширования может привести к серьезному увеличению размера каталога /bitrix/cache/.

    Пример кеширования HTML

    <?
    // создаем объект
    $obCache = new CPageCache; 
    
    // время кеширования - 30 минут
    $life_time = 30*60; 
    
    // формируем идентификатор кеша в зависимости от всех параметров 
    // которые могут повлиять на результирующий HTML
    $cache_id = $ELEMENT_ID.$IBLOCK_TYPE.$USER->GetUserGroupString(); 
    
    // инициализируем буферизирование вывода
    if($obCache->StartDataCache($life_time, $cache_id, "/")):
    
    	// выбираем из базы параметры элемента инфоблока
    	if($arIBlockElement = GetIBlockElement($ELEMENT_ID, $IBLOCK_TYPE)):
    		echo "<pre>"; print_r($arIBlockElement); echo "</pre>";
    	endif;
    
    	// записываем предварительно буферизированный вывод в файл кеша
    	$obCache->EndDataCache(); 
    
    endif;
    ?>

    В данном примере, метод CPageCache::StartDataCache проверит наличие неистекшего и валидного файла кеша. Если такой файл существует, то он будет подключен и выведен на экран, в противном случае будет включена буферизация. Результаты буферизации будут записаны в файл кеша методом CPageCache::EndDataCache.

    В первом параметре метода CPageCache::StartDataCache задается интервал времени в секундах, в течение которого файл кеша будет считаться валидным и не истекшим (интервал времени отсчитывается с момента создания файла кеша).

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

    Например:

    • Если на результат выполнения кешируемого кода может повлиять авторизация текущего пользователя, то к идентификатору необходимо добавить результат выполнения функции CUser::GetUserGroupString, возвращающей строку c ID групп текущего пользователя.
    • Если в кешируемом коде вы используете постраничную навигацию, то в данный идентификатор обязательно нужно включить результат работы функции CDBResult::NavStringForCache.
    • При использовании сортировки необходимо также помнить, что в идентификатор необходимо включить значения переменных хранящих в себе поле, по которому идет сортировка и порядок сортировки (например, $by.$sort).

    Пример кеширования HTML и PHP переменных

    <?
    // создаем объект
    $obCache = new CPHPCache; 
    
    // время кеширования - 30 минут
    $life_time = 30*60; 
    
    // формируем идентификатор кеша в зависимости от всех параметров 
    // которые могут повлиять на результирующий HTML
    $cache_id = $ELEMENT_ID.$SECTION_ID.$USER->GetUserGroupString(); 
    
    // если кеш есть и он ещё не истек то
    if($obCache->InitCache($life_time, $cache_id, "/")):
    	// получаем закешированные переменные
    	$vars = $obCache->GetVars();
    	$SECTION_TITLE = $vars["SECTION_TITLE"];
    else :
    	// иначе обращаемся к базе
    	$arSection = GetIBlockSection($SECTION_ID);
    	$SECTION_TITLE = $arSection["NAME"];
    endif;
    
    // добавляем пункт меню в навигационную цепочку
    $APPLICATION->AddChainItem($SECTION_TITLE, $SECTION_URL."SECTION_ID=".$SECTION_ID);
    
    // начинаем буферизирование вывода
    if($obCache->StartDataCache()):
    
    	// выбираем из базы параметры элемента инфоблока
    	if($arIBlockElement = GetIBlockElement($ELEMENT_ID, $IBLOCK_TYPE)):
    		echo "<pre>"; print_r($arIBlockElement); echo "</pre>";
    	endif;
    
    	// записываем предварительно буферизированный вывод в файл кеша
    	// вместе с дополнительной переменной
    	$obCache->EndDataCache(array(
    	"SECTION_TITLE" => $SECTION_TITLE
    	)); 
    endif;
    ?>

    Отличие данного примера от предыдущего заключается в том, что помимо HTML также кешируется PHP переменная $SECTION_TITLE. Структура и тип кешируемых PHP переменных могут быть произвольными. Метод CPHPCache::InitCache выполняет проверку на наличие неистекшего и валидного файла кеша. Если данный файл найден, то происходит его подключение, при этом все закешированные переменные будут доступны после использования метода CPHPCache::GetVars. Метод CPHPCache::EndDataCache, помимо буферизированного HTML кода, записывает в файл кеша также PHP переменные.

    Для отключения кеширования на одной странице, необходимо авторизоваться с административными правами и вызвать эту страницу с параметром &clear_cache=Y. Если, авторизовавшись с административными правами, вызвать любую страницу с параметром &clear_cache_session=Y, то кеш будет отключен для всех страниц, которые будут просмотрены в рамках сессии. Файлы кеша можно удалить в административной части на закладке Очистка файлов кеша страницы Настройки > Настройки продукта > Автокеширование.

    Смотрите также:

    Альтернативные способы хранения кеша

    Мы убедились, что использовать технологию кеширования для веб-сайта нужно, значит необходимо предусмотреть кеширование ещё на стадии написания ТЗ.

    Допустим, через определенное время, наш веб-сайт стал популярным ресурсом. Благодаря активному использованию технологии кеширования мы надежно защитили базу данных от высокой нагрузки, и она может выдержать 3-5 кратное превышение нагрузки.

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


    Решения есть:

    memcached

    При использовании memcached временные данные будут храниться в оперативной памяти. Можно выделить для хранения кеша недорогой сервер с несколькими гигабайтами памяти.

    При этом, устаревшие данные будут автоматически вытесняться и наш кеш перестанет "расползаться" по системе, пожирая все больше и больше места. Допустим, мы выделили в memcached 4GB места для кеширования и можем быть уверенными, что больше 4GB кеш не вырастет, а наименее часто используемые данные будут вытесняться (алгоритм LRU). Очень удобно и эффективно.

    Подключение memcached осуществляется в файле dbconn.php.

    Для эффективной работы системы настройки по умолчанию нужно изменить. Примеры:

    // Мемкеш с объемом памяти 2ГБ 8 потоками и работой через tcp
    PORT="11211"
    USER="memcached"
    MAXCONN="10240"
    CACHESIZE="2048"
    OPTIONS="-t 8"
    
    // настройки в dbconn.php
    define("BX_CACHE_TYPE", "memcache");
    define("BX_CACHE_SID", $_SERVER["DOCUMENT_ROOT"]."#01");
    define("BX_MEMCACHE_HOST", "127.0.0.1");
    define("BX_MEMCACHE_PORT", "11211");
    
    // Мемкеш с объемом памяти 2ГБ 8 потоками и работой через сокет
    PORT="11211"
    USER="bitrix"
    MAXCONN="10240"
    CACHESIZE="2048"
    OPTIONS="-t 8 -s /tmp/memcached.sock"
    
    // настройки в dbconn.php
    define("BX_CACHE_TYPE", "memcache");
    define("BX_CACHE_SID", $_SERVER["DOCUMENT_ROOT"]."#01");
    define("BX_MEMCACHE_HOST", "unix:///tmp/memcached.sock");
    define("BX_MEMCACHE_PORT", "0");
    

    Если memcached используется на одной машине, там же, где и находится сам веб-сервер, то использование сокета будет быстрее.

    Ссылки по теме:


    APC

    Подключение APC осуществляется в файле dbconn.php:

    // настройки в dbconn.php
    define("BX_CACHE_TYPE", "apc");
    define("BX_CACHE_SID", $_SERVER["DOCUMENT_ROOT"]."#01");
    

    Подробнее про установку и настройку APC смотрите в документации www.php.net/apc.


    Параметр cacheenginenone

    Очень часто встречающаяся проблема при работе с кешем - в закладке Хранение кеша страницы Настройки > Производительность > Панель производительности появляется параметр cacheenginenone, а кеширование при этом не производится. По сути данный параметр - это указание системы, что выбранный вами способ хранения кеша не работает. Можно сказать, что подходящего способа кеширования для системы нет, поэтому она и не будет выполнять его, как будто оно не включено вовсе.

    Для того, чтобы решить эту проблему, нужно правильно настроить способ кеширования. Например, данный параметр может появиться, если вы указываете, что кеш должен храниться c помощью APC, но данное приложение настроено неправильно или отключено. Вам нужно проверить его настройки или включить его, если требуется.

    Смотрите также:

    Кеширование в собственных компонентах

    Подробное описание кеширования в компонентах описано в уроке Кеширование в собственных компонентах в рамках главы Компоненты.

    Рекомендации по работе с кешированием

    • Оптимизировать проект так, чтобы без использования кеширования сложность и количество запросов было минимально. Например, для облегчения запросов, возможно, потребуется создание специфических для проекта индексов. Также, в некоторых случаях, будет эффективнее разбить один сложный запрос на несколько более простых и легких. Это снизит общую нагрузку к базе и позволит более эффективно использовать кеширование.

      Цитатник веб-разработчиков.

      Юналиев Рамиль: Если разработчик делает 1000 запросов и говорит "и так сойдет, оно же кешируется", то это конечно неправильно, любым инструментом нужно уметь пользоваться.

      Цитатник веб-разработчиков.

      Сербул Александр: Иногда во время разработки активно используются технологии кеширования, однако при возрастании нагрузки база данных подвергается перегрузкам. Почему? Для предупреждения данного риска необходимо сначала обеспечить наиболее оптимальную работу с базой данных с выключенным кешированием, прежде чем его включать (рекомендую менеджерам интернет-проектов взять это на заметку и включить в чеклист контроля качества проекта). Типичный кейс данной "ловушки" такой - при включенном кешировании кастомизированное меню использует 0 SQL-запросов, при выключенном - 5000!

    • Подобрать оптимальное время кеширования с направлением в большую сторону. Малое время заставляет обновляться кеш чаше, для больших объемов кешируемых данных это будет создавать дополнительную (лишнюю) нагрузку на сервер, особенно если данные не меняются.

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

    • При формировании идентификатора кеша необходимо учитывать только нужные параметры, избегая излишней вариативности.
      Неправильный ИД будет приводить к тому, что кеширование будет не эффективно из-за того, что большая часть хитов будет попадать мимо кеша.

      Пример создания кеша для новостей, когда срок активности новости скоро истекает, и кеш достаточно будет создать на весь день всего один раз:

      // Не правильный ИД, так как в данном случае на каждый хит будет создан новый кеш.
      // (При условии что нам надо выбрать все элементы которые на данный день активны.)
      
      $arFilter = array(
      	'IBLOCK_ID' => 19,
      	'ACTIVE' => 'Y',
      	'>=PROPERTY_end_date' => date("Y-m-d H:i:s")
      );
      
      $CACHE_TIME = 86400;
      $CACHE_ID = "some_key_iblock".implode('_', $arFilter);
      $obCache = new CPHPCache;
      
      if($obCache->InitCache($CACHE_TIME, $CACHE_ID, "/"))
      {
      	$arVars = $obCache->GetVars();
      }
      
      // Правильный ИД, так как в данном случае кеш будет создан на весь день.
      // (При условии что нам надо выбрать все элементы которые на данный день активны.)
      $arFilter = array(
      	'IBLOCK_ID' => 19,
      	'ACTIVE' => 'Y',
      	'>=PROPERTY_end_date' => date("Y-m-d 00:00:00")
      );
      
      $CACHE_TIME = 86400;
      $CACHE_ID = "some_key_iblock".implode('_', $arFilter);
      $obCache = new CPHPCache;
      
      if($obCache->InitCache($CACHE_TIME, $CACHE_ID, "/"))
      {
      	$arVars = $obCache->GetVars();
      }
      

    Ссылки по теме:

    Проблемы при кешировании меню

    Большой размер папки с кешем меню

    Ситуация. Выявлен большой размер папки bitrix/managed_cache/MYSQL/menu: 1.9 Gb. На сайте 4 типа сквозных меню, страниц на сайте много. В чём проблема и что делать?

    Причина. На каждую страницу создаётся 1 файл кэша по каждому типу используемого меню. Кроме того, если еще задано кеширование для разных групп, то умножьте это число на количество таких групп. То есть, для каждой страницы у вас будет 4 файла кэша меню (если по группам - умножайте на количество групп). Поэтому такой размер вполне может быть.

    Само по себе такое количество файлов не страшно, если место на диске достаточно. Проблема в том, что акселератор (в нашем случае APC) складывает эти файлы в кеш и кеш переполняется.

    Решение: Исключить файловый кэш из акселератора, убедившись, что в template.php и result_modifier.php нет запросов и тяжелых вычислений. В файлах menu_ext запросы должны кешироваться.

    apc.filters="-/bitrix/cache,-/bitrix/managed_cache/,-/upload/"

    Примечание: Если на сайте меню одного типа в подпапках не переопределяется, то можно при подключении меню указать параметр:
    "CACHE_SELECTED_ITEMS" => "N",
    Это приведет к тому, что при создании файла кеша меню в ключе не будет участвовать url. А расчет выбранного уровня будет происходить после получения данных из кеша.


    Примеры. Кешируем правильно

    Рассмотрим примеры, как можно улучшить производительность проекта.

      Кешируем правильно

    Кеш очень важная составляющая производительности проектов. Чем эффективнее кешируем, чем кеш легче и быстрее, тем меньше ресурсов надо на поддержание проекта.

    Но работа кеша может быть организована не совсем корректно, что приводит к проблемам производительности.

    Комплексный пример: класс кеширования пользователя

    Как неправильно:

    // Класс кеширования пользователя с некорретным кешированием
    class User
    {
    	private $userData;
    	function __construct($userId = false)
    	{
    		if (!$userId)
    		{
    			global $USER;
    			$userId = $USER->GetID();
    		}
    
    		$cntStartCacheId = __CLASS__.'::'.__FUNCTION__.'|'.SITE_ID.'|'.$userId;
    		$cache = new \CXxxCache($cntStartCacheId.'sid0',3600,'user_data');
    		$this->userData = $cache->Init();
    
    		if (null == $this->userData)
    		{
    			$this->putUserData(array("ID"=>$userId));
    			$this->putUserData(\CUser::GetByID($userId)->Fetch());
    			$this->putUserData(array("DEPARTMENT" => $this->getDepartment()));
    
    			$cache->registerTag('USER_NAME_'.$userId);
    			$cache->set($this->userData);
    		}
    	}
    }
    
    Что не так в примере выше:
    1. Общая папка для кеша пользователей
    Такой пример кеша практически не работает на проекте. На портале, где много пользователей, мы постоянно будем [dw]терять кеш[/dw][di]Большое количество хитов попадает в том момент, когда кеш уже скинут. Происходит это из-за того, что кеш пользователей уникальный на каждого человека и его хотелось бы скидывать отдельно. В примере указана общая папка для кеша, а не отдельная для пользователя. Следовательно, когда мы хотим скинуть кеш для пользователя, мы скидываем его его для всех. [/di], что негативно отразиться на его работе.
    2. Время кеширования
    Кеширование сущности в примере на 1 час - неэффективно. Сохранение на малое время может привести к [dw]массовой генерации кеша[/dw][di]Простой пример: сотрудники ушли на обед и вернулись через час, а кеш уже у всех сброшен.[/di]. Такая проблема актуальна для больших компаний и проектов.
    3. Заполнение пользователя методом GetByID
    В кеш попадает большой объем данных о пользователе, а именно много ненужной информации (пароль, подтверждение пароля, настройки синхронизации и т.д.).
    4. Использование Fetch
    При использовании Fetch данные не экранируются и никак не проверяются, что приводит к ошибкам безопасности.

    Как правильно:

    GetID();
    		}
    
    		$cntStartCacheId = __CLASS__.'::'.__FUNCTION__.'|'.SITE_ID.'|'.$userId;
    		$cache = new \CXxxCache(
    			$cntStartCacheId.'sid0',
    			// увеличили время кеширования
    			604800,
    			// путь для ключей кеша сделали зависимым от $userID
    			'user_data/'.substr(md5($userId),2,2).'/'.$userId 
    		);
    
    		$this->userData = $cache->Init();
    
    		if (null == $this->userData)
    		{
    			$this->putUserData(array("ID"=>$userId));
    
    			// Выбираем только нужные поля
    			$this->putUserData(\CUser::GetList(...)->GetNext(true, false));
    			$this->putUserData(array("DEPARTMENT" => $this->getDepartment()));
    
    			$cache->registerTag('USER_NAME_'.$userId);
    			$cache->set($this->userData);
    		}
    	}
    }
    
    • Увеличили время кеширования до недели;
    • Изменили папку хранения кеша (разложили персонализированный кеш по подпапкам);
    • Изменили сбор информации по пользователю с GetByID на [dw]GetList[/dw][di]Достаточно часто в проектах многие используют вызов CIBlockElement::GetById. Простой, удобный метод, когда надо вытащить какое-то поле для элемента инфоблока. Но этот метод тянет все поля и все свойства элемента. В случае инфоблока с большим количеством свойств и большого числа посетителей на сайте этот простой запрос приводит к снижению производительности.

      Подробнее...[/di];
    • Заменили Fetch на GetNext(true, false).

    Время кеширования (cache key and ttl)

    Пример на компоненте, который отображает дни рождения. Установлено большое время кеширования и добавлен дополнительный параметр, который сам компонент никак не отрабатывает и его не обрабатывает шаблон. Но так как в параметры подставлен date, то в 0 часов получим новый ключ у кеша данного компонента.

    IncludeComponent(
    	"bitrix:intranet.structure.birthday.nearest",
    	"widget",
    	Array(
    		"CACHE_TYPE" => "A",
    		"CACHE_TIME" => "86450",
    		"DATE_FORMAT" => "j F",
    		"DETAIL_URL" => "#SITE_DIR#company/personal/user/#USER_ID#/",
    		"DEPARTMENT" => "0",
    		.....
    		"CACHE_DATE" => date('dmy')
    	)
    );
    

    Пример: как в API правильно подставлять ключи

    В таком случае часто проставляют лишние параметры, что приводит к увеличению кеша. (например date без параметров приводит к обновлению кеша каждую секунду).

    <?php
    
    // Пример добавление в ключ кеша метки времени для корректного переключения кеша. Метка может быть и не из времени.
    
    $cache = Bitrix\Main\Data\Cache::createInstance();
    if ($cache->initCache(86450, '/some_key/'.date('myd').'/', '/some_dir/'))
    {
    	$var = $cache->getVars();
    }
    else
    {
    	// Получение данных
    	$cache->startDataCache();
    	$cache->endDataCache($var);
    }
    

    Отключаем сброс ключей

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

    1. Отключите кеширование элементов (сбрасывание тегированного кеша) перед импортом;
    2. Включите его по окончании процесса импорта элементов;
    3. Сбросьте те теги инфоблока, которые сбрасывались в данном случае.
    В таком вариант кеш сбросится один раз после полной загрузки, а не после загрузки каждого элемента.

    Индексация фасетного индекса также может быть отложена (т.е. отключена перед импортом и включена по окончании).

    <?php
    // Отключение сброса тегированного кеша инфоблоков и пересчета фасетного индекса, во время импорта.
    
    Bitrix\Iblock\PropertyIndex\Manager::enableDeferredIndexing();
    Bitrix\Catalog\Product\Sku::enableDeferredCalculation();
    
    \CAllIBlock::disableTagCache($iblockID);
    
    // Импорт элементов 
    
    \CAllIBlock::enableTagCache($iblockID);
    \CAllIBlock::clearIblockTagCache($iblockID);
    
    Bitrix\Catalog\Product\Sku::disableDeferredCalculation();
    Bitrix\Catalog\Product\Sku::calculate();
    
    Bitrix\Iblock\PropertyIndex\Manager::disableDeferredIndexing();
    Bitrix\Iblock\PropertyIndex\Manager::runDeferredIndexing($iblockID);
    

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


    Итоги рекомендаций по кешированию

    ХорошоПлохо
    • Уникальные ключи, в отдельных папках;
    • Только нужные поля в кеше;
    • Максимальное время хранения ключей;
    • Конечные списки в одном ключе;
    • Разумное количество тегов в ключе;
    • Сброс ключей по пути.
    • Закрывать кешем долгие вычисления или страницы;
    • Часто сбрасывать ключи, особенно в большом количестве.

    Внимание! Не забывайте, что управляемый кеш инфоблоков очищается только при вызове CIBlockElement::Update. При изменении, например, свойств с помощью CIBlockElement::SetPropertyValueCode очистки не произойдет. Делаем вручную после изменения:
    if(defined('BX_COMP_MANAGED_CACHE'))
       $GLOBALS['CACHE_MANAGER']->ClearByTag('iblock_id_'.$arParams['IBLOCK_ID']);



    Оптимизация выборки дополнительных данных

    Использование метода GetList

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

    Достаточно часто в проектах многие используют вызов CIBlockElement::GetById. Простой, удобный метод, когда надо вытащить какое-то поле для элемента инфоблока. Но этот метод тянет все поля и все свойства элемента. В случае инфоблока с большим количеством свойств и большого числа посетителей на сайте этот простой запрос приводит к снижению производительности. А если таких запросов несколько десятков в различных result_modifier у разных компонентов? Оказывается, что в совокупности, несмотря на кеширование, эти вещи создают пиковые нагрузки в момент обновления кеша.

    Если уж надо получить название элемента по ID, то лучше воспользоваться GetList с указанием конкретного вытаскиваемого поля элемента.

    Примечание: GetByID внутри вызывает GetList. Разница только в том, что GetByID в любом случае тянет все поля и свойства, а в GetList этот перечень можно контролировать.

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

    Также GetList позволяет выбрать сразу несколько записей, тогда как GetByID только одну.

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

    Примеры повышения производительности

    Важно! Множество однотипных запросов создают большую нагрузку. По возможности, необходимо сократить их количество.

    1. Рассмотрим пример, когда необходимо для каких-либо элементов одного инфоблока (авторы книг) получить дополнительные свойства из другого инфоблока (информация по авторам), например путем изменения шаблона или result modifier.

      В большинстве случаев это делается в цикле, например в таком:

       // неправильный вариант выборки, на каждый элемент делается дополнительный запрос
      foreach($arResult['ITEMS'] as $ikey => $ival)
      {
      	$avtorID = intval($ival['PROPERTIES']['AVTOR']['VALUE']);
      	if($avtorID > 0)
      	{
      		$rs = CIBlockElement::GetByID($avtorID);
      		while($ar = $rs->GetNext())
      		{
      		$arResulr['ITEMS'][$ikey]['AVTOR_INFO'][] = $ar;
      		}
      	}
      }
      

      но это не правильно. Такой цикл создает множество запросов, что снижает производительность.

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

      // правильный вариант, информация по авторам выбирается одним запросом и только нужная
      $avtorID = array();
      
      foreach($arResult['ITEMS'] as $ikey => $ival)
      {
      	$aID = intval($ival['PROPERTIES']['AVTOR']['VALUE']);
      	if($aID > 0)
      	{
      		$avtorID[] = $aID;
      	}
      }
      
      $avtorID = array_unique($avtorID);
      
      
      $rs = CIBlockElement::GetList(
      	array('ID' => 'ASC'),
      	array(
      		'IBLOCK_ID' => XX,
      		'ID' => $avtorID,
      		'ACTIVE' => 'Y'
      	),
      	false,
      	false,
      	array('ID', 'NAME', 'PREVIEW_PICTURE')
      
      );
      while($ar = $rs->GetNext())
      {
      	$arResulr['AVTOR_INFO'][$ar['ID']] = $ar;
      }
      

    2. Если мы можем изменить основной АПИ-вызов, например, используем свой компонент, то данные по авторам из примеров выше можно получить сразу:
      // правильный вариант,
      // информация по книгам и связанная с ними информация по авторам выбирается одним запросом
      $rs = CIBlockElement::GetList(
      	array('ID' => 'ASC'),
      	array(
      		'IBLOCK_ID' => YY,
      		'ACTIVE' => 'Y'
      	),
      	false,
      	false,
      	array(
      		'ID',
      		'NAME',
      		'PREVIEW_PICTURE',
      		'PREVIEW_TEXT',
      		'PROPERTY_AVTOR.NAME',
      		'PROPERTY_AVTOR.PREVIEW_PICTURE',
      	)
      
      );
      while($ar = $rs->GetNext())
      {
      	$arResulr['ITEMS'][] = $ar;
      }
      

    3. Использование более оптимального кода, который сокращает количество запросов.

      Пример: Необходимо проверить для списка определенных пользователей, состоят ли они в заданной группе.

      Типичный код:

      foreach ($arSomeUser as $arUser)
      {
      	$arGroups = CUser::GetUserGroup($arUser['ID']);
      	if(in_array(5, $arGroups))
      	{
      		// Делаем что-то
      	}
      }
      

      В этом случае у нас будет столько запросов, сколько пользователей в списке + один на выборку нужных пользователей из всех возможных (самый первый запрос).

      Если в группе содержится мало пользователей, то правильнее будет использовать следующий подход:

      $rsUser = $arUser->GetList(($by="ID"), ($order="desc"), array('ACTIVE' => 'Y', 'GROUPS_ID' => 5 ....))
      while($arUser = $rsUser->GetNext())
      {
      	// Делаем что то
      }
      

    Выборка и хранение в кеше только нужных данных

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

    Выборка всех данных может потребовать большего времени и также заметно увеличить размер кеша.


    Важные примеры повышения производительности

    1. Часть АПИ-методов может генерировать излишнюю информацию. Лучше сразу ограничить запросы только нужными данными.

      Примеры:

      $rs = CIBlockResult::GetNext(true, false);
      // установка параметра false позволяет ограничить выборку только преобразованными значениями полей.
      $res = CIBlock::GetList(array(), array('TYPE'=>'catalog', 'SITE_ID'=>SITE_ID, 'ACTIVE'=>'Y'), false);
    2. В некоторых случаях метод Fetch работает быстрее, чем GetNext, но он не делает данные безопасными.
    3. Кеширование только нужных данных.

      В самом компоненте нужно ограничить результат работы компонента $arResult только нужными данными.

      Например, компонент должен выводить всего идентификатор элемента и его название, тогда только их и нужно занести в кеш:

      $this->SetResultCacheKeys(array("ID", "NAME"));

      Если мы делаем какие-то операции в result_modifier.php, чтоб потом передать их результат в component_epilog.php, то также понадобится установить ключи в SetResultCacheKeys. Тогда результат наших операций будет также закеширован.

      foreach ($sAr as $key => $arElement)
      {
      $this->__component->arResult["IDS"][] = $arElement["ID"];
      }
      $this->SetResultCacheKeys(array("IDS"))

      Примечание: Если в SetResultCacheKeys не установлены ключи, то тогда, по умолчанию, будет производиться кеширование всех результатов $arResult.

      Ссылки по теме:


    4. Оптимизация размера кеша.

      Если, например, размер файла кеша превышает 1МБ, то возможно происходит избыточное кеширование, что не есть хорошо. В этом случае следует произвести анализ кешируемых данных. Чем больше размер файла кеша, тем меньше его эффективность.

    5. Ограничение объема запрашиваемой информации.

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

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

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

      Пример:

      Параметр nPageSize=20 массива arNavStartParams позволяет отобразить 20 элементов, но он делает 2 запроса к базе:

      1. "COUNT" - получение кол-ва элементов
      2. "SELECT" - непосредственно выборка данных
      

      в то время как параметр nTopCount=4 того же массива создает сразу 1-н запрос вида:

      SELECT * FROM tbl LIMIT 4;  
      

      Использование nTopCount гораздо эффективнее т.к. он не делает дополнительный запрос на получение записей выборки.

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

    Оптимизация запросов к БД

      Пример оптимизации запроса

    Всегда минимизируйте запросы. Например, если в цикле идет запрос к элементу ИБ, то уже необходимо задуматься над минимизацией. Да, это займет больше времени, но вам скажут спасибо клиенты.

    Нельзя:

    foreach($arResult["ORDERS"] as $key => $val)
    {
    	foreach($val["BASKET_ITEMS"] as $vvval)
    	{
    		$rsEls = CIBlockElement::GetByID();
    	}
    }

    Следует:

    $arIDs = array();
    foreach($arResult["ORDERS"] as $key => $val)
    	{
    		foreach($val["BASKET_ITEMS"] as $vvval)
    		{
    			$arIDs[] = $vvval["PRODUCT_ID"];
    		}
    	}
    if(!empty($arIDs))
    {
    	$rsEls = CIBlockElement::GetList(array(), array("ID" => $arIDs));
    	...
    }
    
    foreach($arResult["ORDERS"] as $key => $val)
    {
    	foreach($val["BASKET_ITEMS"] as $vvval)
    	{
    		//наполняем данные, налаживая соответствие ID-ков
    	}
    }

    Фактически, вы сводите порой десятки, если не сотни, запросов к одному.

      Специальные методы

    Если для какого-либо изменения в БД предусмотрен специальный метод, то следует использовать именно его, а не более общий метод изменения БД.

    Хороший пример - модуль интернет-магазина и работа с заказом: можно изменить флаг оплаты заказа путем CSaleOrder::Update, а можно путем CSaleOrder::PayOrder. Так вот, следует применять именно PayOrder, потому что в нем произойдет вызов соответствующих обработчиков.

    Даже если вам надо изменить множество полей (того же заказа) и флаг оплаты, то произведите изменение через PayOrder, а затем уже апдейт остальных полей.


    Как сделать сайт быстрым

    Разработчик в большинстве случаев под ускорением загрузки сайта понимает работу по back-end. Однако посетителю всё равно за сколько миллисекунд открывается страница у разработчика, ему важно как быстро открывается страница у него самого. А тут уже важен целый комплекс показателей. Квалифицированный разработчик должен рассматривать сайт не как кусок программного кода, за который он отвечает, а как конечный комплексный продукт.

    Back-End

    Без отладки программного кода, конечно же, не обойтись. Здесь разработчику в помощь приходит инструмент Монитор производительности, который позволяет профилировать все страницы сайта на предмет кода, SQL запросов. Кроме того, этот модуль даёт рекомендации по тюнингу серверного ПО.

    Далее необходимо заняться кешированием компонентов.

    Следующий шаг - это внедрение технологии Композитный сайт.

    Расположение сайтов на удалённых территориально серверах может серьёзно повлиять на время получения информации пользователем. Особенно это актуально для проектов, ориентирующихся на локальные рынки. Перенос хостинга в таком случае может дать существенный прирост, в ряде случаев отмечалось ускорение до 1 секунды.

    Front-End

    После выполнения работ по back-end'у необходимо переходить к рассмотрению остальных параметров, влияющих на скорость загрузки. В Bitrix Framework есть инструмент - Скорость сайта. Его задача - облегчить разработчику работу по оптимизации загрузки страницы.

    Примечание: Для работы Скорости сайта нужна статистика. После внесения каких-то изменений необходимо выдерживать некоторый период для получения статистически верных данных. Продолжительность периода зависит от посещаемости сайта.

    У этого инструмента есть диаграмма по хитам, где можно посмотреть каждый из этапов загрузки Navigation Timing.

    На этом графике зелёным цветом показана работа back-end'а, а фиолетовым - работа front-end'а. То есть получается, что большую часть времени в загрузке страницы браузер тратит не на back-end, а на front-end.

    Улучшить работу front-end можно уменьшив количество загружаемых ресурсов и их объём.


    Меры по уменьшению числа загружаемых ресурсов:

    • Включив объединение css и js скриптов. В браузерах есть ограничение на количество одновременных соединений с сервером. Как правило это 6, но может доходить до 13, как в IE. То есть, если на странице 70 файлов, то браузер скачает их в несколько приёмов, а не одномоментно. Если объединить эти скрипты, то скорость загрузки увеличится.
    • Включив CDN. Однако к подключению CDN нужно подходить внимательно. Если ваш основной посетитель локализован на определённой территории, то CDN может оказаться замедляющим фактором, если ближайший сервер расположен далеко от пользователей.
    • Используя кеширование ресурсов в браузере (Expires/Cache-Control: max-age, Last-Modified/E-tag). Нужно отдавать заголовки для картинок, css и js-скрипта. Если нет заголовков последней модификации или заголовка Expires, то на каждый хит браузер будет скачивать все ресурсы повторно.

      Если у вас есть дата последней модификации, то это уже хорошо. В этом случае на вторых и последующих хитах браузер будет делать conditional get запрос. То есть спрашивать: а не изменился ли этот ресурс вот с этой даты? И загружать только изменённые ресурсы. Правда и в этом случае приходится делать один дополнительный запрос.

      Чтобы избежать этого запроса достаточно настроить в Apache или NGINX выдачу заголовка Expires. Это, к сожалению, часто забывают сделать.
    • Все картинки для шаблона должны быть в спрайтах. А для мелких картинок лучше вообще использовать Base64.

    Меры по уменьшению объёма загружаемых данных:

    • GZIP-сжатие. Рекомендуется использовать серверное сжатие. Если оно не доступно, то можно использовать модуль Компрессия. Однако с этим модулем надо быть осторожным, его сжатие поддерживается не всеми хостерами.
    • Минификация CSS и Javascript. Это - вырезание пробелов, переводов строк, уменьшение локальных переменных и оптимизация кода (для js).
    • Сжатие картинок специальными утилитами.
    Зачем нужно сжимать данные?

    Дополнительные меры

    • Вынос ресурсов на разные домены, который позволяет обойти ограничение на 6 подключений.
    • Порядок подключения ресурсов на странице: CSS вверху, Javascript – внизу. Оба они блокируют рендеринг страницы до завершения собственной загрузки. Но если css мы не можем убрать в низ страницы: без него она просто не отобразится, то Javascript - вполне убрать вниз или же грузить асинхронно.

      Примечание: Это же относится к Яндекс.Метрике, GoogleAnalitics и все подобные внешние скрипты и инструменты. Несмотря на то, что они все внешние и должны попозже загружаться. Достаточно часто получается что несмотря на то, что эти скрипты грузятся асинхронно, эти файлы могут загрузится раньше рендеринга страницы и, соответственно, как только этот ресурс загрузился, он начинает выполняться. Совет: грузите все внешние инструменты (метрики, счётчики, кнопки Вконтакте и прочие) в BX.ready, то есть на факт того, что страница загрузилась.

    Сторонние инструменты

    Для оптимизации front-end'a в качестве чек-листа можно использовать сторонние инструменты:

    Если долго сохраняется элемент в административном разделе

      Проблема и её признаки

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

    Признаки:

    • долгое (60 секунд и выше) сохранение элемента/товара через форму редактирования в административном разделе (при импорте и обновлении из консоли проблема не наблюдается)
    • анализ на стороне хостера выявляет долгий запрос вида:

      UPDATE b_iblock_element SET 
      	TIMESTAMP_X = TIMESTAMP_X,
      	SHOW_COUNTER_START = ifnull(SHOW_COUNTER_START, now()),
      	SHOW_COUNTER =  ifnull(SHOW_COUNTER, 0) + 1
      WHERE ID=ИД_сохраняемого_элемента
      

    • в некоторых случаях изменения не сохраняются вообще

    Наиболее вероятная причина - обработчик событий, посылающий http(s)-запрос к публичной детальной странице элемента, на которой в настройках компонента включено обновление счетчика просмотров.

    Далее рассмотрим выявление причины подробнее на примере.

      Выявление причины

    1. Выявляем тормозящий запрос.

      В ходе первичного анализа установлено, что тормозящий запрос - это вызов метода ClBlockElement::CounterInc. Однако данный метод вызывается только для увеличения счетчика просмотров в публичных компонентах и не должен тормозить процесс сохранение элементов.

    2. Проверяем, как работает сохранение элемента.

      Открываем код страницы редактирования и видим, что сохранение элемента работает через транзакции. Делается это, чтобы откатить все изменения, если на каком-то этапе сохранения произошла ошибка.

    3. Проверяем вызовы методов.

      Последовательно проверяем все вызовы [dw]методов[/dw][di] CIBlockElement::Update, обновление полей цен и товара, работу с документооборотом. [/di], находящиеся внутри блока транзакции - есть ли на проекте обработчики событий, что именно вызывается в этих методах. Если результат отрицательный - разбираем методы детально и смотрим уже их (например, для метода CIBlockElement::Update таким является вызов CIBlockElement::UpdateSearch, а в нем - CSearch::Index).

      Важно! Учтите, в проекте могут быть и другие обработчики с вызовом api.

      В результате в одном из обработчиков находим http(s)-запрос к публичной детальной странице элемента. А на этой странице [ds]в компоненте[/ds][di] Например, в компонентах Элемент каталога детально или Каталог включена опция Использовать счетчик просмотров.[/di] включено обновление счетчика просмотров. Отключаем его - все тормоза исчезают.

      Примечание: При открытии транзакции строка (а иногда и вся таблица) блокируется от записи. Это одна из причин, почему использование транзакций запрещено в методах api. Все попытки изменения записи, кроме текущей, ставятся в очередь ожидания на уровне базы. Пока транзакция не закрыта или не отменена - очередь ожидает.

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

    Далее в уроке для наглядности рассмотрим ожидаемый план сохранения элемента и то, как на него влияет вызов подобных обработчиков.

      План сохранения данных без/с обработчиком

    Ожидаемый план сохранения данных:

    1. Открываем транзакцию
    2. Обновляем строку в b_iblock_element
    3. Запись блокируется от внешнего изменения (другой хит) на уровне БД
    4. Закрываем транзакцию
    5. Запись разблокируется
    6. Выполняется очередь изменений с других хитов (если успела накопиться)

    План сохранения данных с ранее выявленным обработчиком

    1. Открываем транзакцию
    2. Обновляем строку в b_iblock_element
    3. Запись блокируется от внешнего изменения (другой хит) на уровне БД
    4. Вызывается обработчик
    5. Идет хит на публичную страницу
    6. Пытаемся обновить счетчик просмотров (та же самая строка в b_iblock_element)
    7. ...долгое ожидание...

    8. Прерывание по таймауту. Обработчик завершен
    9. Если время исполнения скрипта не вышло - закрываем транзакцию. Вышло - просто "падаем"
    10. Запись разблокируется
    11. И тут, наконец, выполнится увеличение счетчика просмотров

    Важно! Причины реализации подобных обработчиков могут быть разные. Тем не менее, в идеале на проекте их быть не должно. Это основная и главная рекомендация.

      Что делать, если обработчик нельзя убрать

    Повторимся, что лучше не использовать в своем проекте обработчики событий, посылающие http(s)-запросы к публичной детальной странице элемента и вступающие в конфликт с выполнением транзакций.

    Если по каким-либо причинам от такого обработчика избавиться нельзя, то можно воспользоваться следующими вариантами решения проблемы:

    1. Заменить прямое обращение к публичной странице на добавление в очередь обработки (своя таблица) и агента/скрипт на кроне, который пойдет по очереди и выполнит требуемое (обращение к странице и какие-то действия с результатом). Наиболее предпочтительный способ, однако его реализация требует высокого уровня знаний и квалификации разработчика.
    2. Передавать в строке обращения некий параметр (например,disableCounter=Y). На самой странице параметр вызова компонента USE_ELEMENT_COUNTER (на примере bitrix:catalog или catalog.element) меняем таким образом:
      • было:

        "USE_ELEMENT_COUNTER" => "Y"
        

      • стало:
        "USE_ELEMENT_COUNTER" => (isset($_REQUEST['disableCounter']) && $_REQUEST['disableCounter'] === 'Y') ? 'N' : 'Y'
        

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

    Как снизить нагрузку с помощью API

    Снижаем нагрузку с помощью API

    Фильтры по like

    Часто встречаемая проблема на проектах это фильтры по like.

    Пример в старом API:

    <?php
    
    // Различные вызовы АПИ и запросы которые они генерируют
    
    Bitrix\Main\Loader::includeModule('iblock');
    $rs = CIBlockElement::GetList(
    	[], 
    	['CODE' => 'xxx'], // правильно вариант данного фильтра ['=CODE' => 'xxx'], 
    	false, 
    	false, 
    	['ID', 'CODE', 'NAME']
    );
    
    /*
    SELECT BE.ID as ID,BE.CODE as CODE,BE.NAME as NAME
    FROM
    	b_iblock B
    	INNER JOIN b_lang L ON B.LID=L.LID
    	INNER JOIN b_iblock_element BE ON BE.IBLOCK_ID = B.ID
    WHERE
    	1=1 AND ( ((((BE.CODE LIKE 'xxx')))))
    		AND (((BE.WF_STATUS_ID=1 AND BE.WF_PARENT_ELEMENT_ID IS NULL))
    	)
    */
    
    

    Пример в ORM:

    <?php
    
    Bitrix\Iblock\ElementTable::getList([
    	'select' => ['ID', 'NAME', 'CODE'],
    	'filter' => ['CODE' => 'xxx'] // правильный вариант фильтра 'filter' => ['=CODE' => 'xxx']
    ]);
    
    /*
    SELECT
    	`iblock_element`.`ID` AS `ID`,
    	`iblock_element`.`NAME` AS `NAME`,
    	`iblock_element`.`CODE` AS `CODE`
    FROM `b_iblock_element` `iblock_element`
    WHERE UPPER(`iblock_element`.`CODE`) like upper('xxx')
    */
    

    Для ORM есть решение, это [dw]новый фильтр[/dw][di]В обновлении main 17.5.2 в ORM появился новый фильтр.

    Подробнее...[/di].

    <?php
    
    // С новым фильтром не получится допустить ошибку
    Bitrix\Iblock\ElementTable::query()
    	->setSelect(['ID', 'NAME', 'CODE'])
    	->where('CODE','xxx')->exec();
    
    /*
    SELECT
    	`iblock_element`.`ID` AS `ID`,
    	`iblock_element`.`NAME` AS `NAME`,
    	`iblock_element`.`CODE` AS `CODE`
    FROM `b_iblock_element` `iblock_element`
    WHERE `iblock_element`.`CODE` = 'xxx'
    */
    

    В старых фильтрах необходимо контролировать like.


    Подготовка собственных индексов

    В штатном продукте не предусмотрены все индексы, т.к. все проекты разные. Для улучшения производительности не забывайте предусмотреть создание своих индексов. В этом поможет инструмент [dw]Монитор производительности[/dw][di]Заочно нельзя сказать какие индексы необходимо создавать, надо всегда рассматривать конкретную ситуацию. Индексы нужны для конкретных выборок на конкретных проектах. В зависимости от архитектуры и логики проекта медленные запросы получаются у каждого свои, и для них нужны свои индексы, часто составные.
    Страницы Анализ индексов и Список индексов - инструмент анализа и рекомендаций по созданию индексов.

    Подробнее в курсе Администратор. Базовый[/di].


    Убираем count

    Count очень тяжелая операция и создает больше нагрузки, чем выборка по запросу.

    Проблема очень критична для Rest. Чтобы не использовать Count в случае если вам не нужно [dw]количество элементов[/dw][di]Например вам нужно просто 10 последних записей.[/di] или вы делаете импорт всех записей по фильтру, передавайте параметр start= -1. Подробнее почитать об этом и посмотреть пример можно в документации по REST.


    Предотвращаем срыв конвейера

    Высоконагруженные проекты как правило реализованы с использованием кластера. В кластере есть одна часто встречаемая проблема - срыв конвейера. Конвейер это поток запросов, которые идут последовательно и ядро распределяет их по серверам: на один из слейвов, либо на мастер. Если идет запрос на изменение данных, то конвейер в рамках хита перестает работать и все запросы идут на master, при этом слейвы перестают обслуживать этот хит, оставаясь не занятыми.

    Рекомендации по предотвращению срыва конвейера

    • Не должно быть файлов after_connect*.php в ядре продукта. Эти файлы содержат настройки подключения к базе данных для случаев, когда мы устанавливаем проект на какой-то хостинг;
    • Не использовать SET (в начале хита полностью срывает конвейер);
    • Минимизировать изменения в начале страницы (для отлавливания таких мест в коде поможет модуль Монитор производительности);
    • В ряде случаев изменения можно экранировать и они не сломают конвейер.

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

    <?php
    // Выполнение всех запросов на master сервере, без срыва конвейера
    
    Bitrix\Main\Application::getInstance()->getConnectionPool()->useMasterOnly(true);
    
    // Какие либо обновления и изменения
    
    Bitrix\Main\Application::getInstance()->getConnectionPool()->useMasterOnly(false);
    

    Например, таким образом сейчас обернута работа с тегированным кешем.

    Вебинар Мастер-класс по производительности

    Рассмотренную в уроке тему подробнее смотрите в вебинаре:

    Мастер-класс по производительности. Highload проекты, как их сделать и поддерживать? Техноволна 6 от 08 апреля 2020.



    Примеры оптимизации JS кода

    Разработчики не очень часто задумываются над количеством хитов, когда делают инструмент которым сами не пользуются и не арендуют мощности под это. Они оставляют этот момент на откуп клиентам: "Купят по мощнее сервер и все залетает". Однако профессионализм разработчика состоит в том, чтобы уметь видеть все последствия своих трудов и уметь выбирать решения оптимальные не только для него, как программиста, но и для клиента.

    Где искать ошибку в ajax обработчике при POST запросах

    Пример уменьшения количества хитов рассмотрим на основе десктоп мессенджера Битрикс24.

    Специфика этого инструмента такова, что он может выдавать просто запредельное количество хитов, ни один другой инструмент Bitrix Framework ни делает даже половины хитов мессенджера.

    На Битрикс24 количество пользователей росло чуть ли не по экспоненте, количество хитов тоже, и наступил критичный момент когда игнорировать такие цифры стало нельзя:

    Анализ лога по хитам выявил следующую проблему: в логе было только название обработчика, так как все данные отправлялись POST запросом на сервер. Логировать POST запросы вещь не очень приятная, поэтому нужно было искать другое решение что бы понять легальные ли это запросы или все таки паразитные.

    Решение очень простое: это GET метки. Достаточно выполнять запрос вместе с GET меткой: im.ajax.php?GET_HISTORY, im.ajax.php?UPDATE_STATE и так далее.

    По этим меткам получилось группировать запросы к серверу и выявить узкие места.

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

    После всех этих операций получились вот такие показатели:


    Сессии и cookie

    Про механизм и возможности сессии отлично написано в официальной документации PHP.

    Внимание! Описанные в главе способы работы с сессиями актуальны с версии главного модуля 20.5.0.

    Переменная $_SESSION

    Работать напрямую с $_SESSION - допустимо, но не желательно. Все изменения данных в глобальной переменной будут сохранены, но настоятельно советуем использовать новое API вместо этой переменной.

    Вместо прямого использования переменной лучше использовать объект, возвращаемый методом \Bitrix\Main\Application::getSession():

    $session = \Bitrix\Main\Application::getInstance()->getSession();
    if (!$session->has('foo'))
    {
    	$session->set('foo', 'bar');            
    }
    
    echo $session['foo']; //bar

    Данный объект реализует интерфейс \ArrayAccess, а также \Bitrix\Main\Session\SessionInterface.

    Сессионный кеш (Local Session)

    Иногда возникает задача кешировать данные, которые связаны с текущим пользователем. Конечно, один из вариантов, это использовать сессию, но это не всегда подходит, так как:

    1. сессия не создана для кеширования,
    2. большое количество данных сказывается на скорость работы с сессией,
    3. возникают блокировки хитов.

    Один из альтернативных вариантов, это создать кеш, который привязан к session_id(). По сути, это простая имитация сессии. С версии main 20.5.400 есть новая возможность.

    Пример:

    $localStorage = \Bitrix\Main\Application::getInstance()->getLocalSession('someCategory');
    if (!isset($localStorage['productIds']))
    {
    	$localStorage->set('productIds', [1, 2, 100]);
    	$localStorage->set('price', 42);
    }
    
    var_dump($localStorage->get('productIds'));
    

    Принцип работы

    Принцип работы достаточно прост: при вызове \Bitrix\Main\Application::getLocalSession($name) всегда возвращается экземпляр \Bitrix\Main\Data\LocalStorage\SessionLocalStorage. Это элемент кеша, который автоматически опирается на session_id().

    При этом, если это первое обращение и данных нет, то будет создан пустой контейнер, если же в кеше были данные по $name, то контейнер будет наполнен данными.

    Все SessionLocalStorage сохраняются в конце хита ядром автоматически.

    Внимание! SessionLocalStorage работает на кеше, который описан в настройках .settings.php.

    Примечание: Если кеш файловый, то SessionLocalStorage будет использовать для хранения $_SESSION, так как иначе возникает проблема контроля и удаления устаревших файлов, что может повлиять на работу файловой системы.

    Документация по теме:

    Сессия разделенный режим (hot&cold)

    Введение

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

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

    Общий принцип

    Один из вариантов решения как избежать блокировки - это написать SessionHandlerInterface и не делать там просто блокировку. Но этот вариант не подходит, так как есть большое количество существующих использований в коде Bitrix Framework и партнеров.

    Выбран другой путь:

    1. Данные, которые используются на каждом хите выделены в KernelSession-сессию (hot данные). Это данные для аутентификации, авторизации и другие связанные с ядром.
    2. Всё остальное, что хранится в сессии -- это cold-данные.
    3. KernelSession-сессия неблокирующая и работает по принципу "кто последний, того и тапки".
    4. Всё остальное -- это обычная блокирующая сессия.
    5. Блокирующая сессия стартует только при первом обращении к cold-данным.

    Разделение хранения cold&hot

    Хранилище для KernelSession-сессии -- это шифрованные cookies.

    Хранилище для cold-сессии -- это обычная сессия, принцип работы, как и раньше, поэтому данные могут храниться в Redis, Memcache, БД.

    Настройка хранения

    Чтобы включить разделенный режим сессии, нужно в bitrix/.settings.php изменить session[mode] на separated. И добавить 'kernel' => 'encrypted_cookies', и 'lifetime' => 14400,.

    Примеры можно посмотреть здесь.

    Настройка хранения данных сессии

    Ядро поддерживает четыре варианта для хранения (файлы, redis, database, memcache) данных сессии. Способ хранения описывается в bitrix/.settings.php в секции 'session':

    Файлы

    // bitrix/.settings.php
    return [
    //...        
    	'session' => [
    		'value' => [
    			'mode' => 'default',
    			'handlers' => [
    			'general' => [
    				'type' => 'file',       
    				]           
    			],
    
    		]                   
    	] 
    ];

    Настройка для разделённой сессии:

    // bitrix/.settings.php
    return [
    //...        
    	'session' => [
    		'value' => [
    			'lifetime' => 14400, // +
    			'mode' => 'separated',  // +
    			'handlers' => [
    				'kernel' => 'encrypted_cookies',  // +           
    				'general' => [
    					'type' => 'file',       
    				],
    			],
    		]                   
    	] 
    ];

    Redis

    // bitrix/.settings.php
    return [
    'session' => [
    	'value' => [
    		'mode' => 'default',
    		'handlers' => [
    			'general' => [
    				'type' => 'redis',
    				'port' => '6379',
    				'host' => '127.0.0.1',
    			],
    		],
    	],
    ]

    Кластерное хранения данных сессии

    Отличие от обычной конфигурации заключается лишь в servers дополнительных опциях: serializer, persistent, failover, timeout, read_timeout. Про них можно прочитать в официальной документации.

    Redis в режиме cluster может быть настроен двумя способами:

    1. Мультимастер кластер: N мастеров (и могут быть слейвы у каждого).
    2. Обычный кластер: 1 мастер и N слейвов

    Redis cluster в режиме мультимастер, указываются параметры всех мастеров:

    // bitrix/.settings.php
    return [
    //...        
    	'session' => [
    		'value' => [
    			'mode' => 'default',
    			'handlers' => [
    				'general' => [
    					'type' => 'redis',   
    					'servers' => [
    					[
    						'port' => 6379,
    						'host' => '127.0.0.1',
    					],
    					[
    						'port' => 6379,
    						'host' => '127.0.0.2',
    					],
    					[
    						'port' => 6379,
    						'host' => '127.0.0.3',
    					],
    					'serializer' => \Redis::SERIALIZER_IGBINARY,
    					'persistent' => false,
    					'failover' => \RedisCluster::FAILOVER_DISTRIBUTE,
    					'timeout' => null,
    					'read_timeout' => null,
    					],
    				],           
    			],
    		]                   
    	] 
    ];

    Redis cluster в режиме 1 мастер + N слейвов. Указываются только параметры мастера блок с опциями опускается:

    return [
    	'session' => [
    		'value' => [
    			'mode' => 'default',
    			'handlers' => [
    				'general' => [
    					'type' => 'redis',
    					'servers' => [
    						[
    						'port' => '30015',
    						'host' => '127.0.0.1'
    						],
    					],
    				],
    			],
    		],
    	],
    ];

    Настройка для разделённой сессии:

    // bitrix/.settings.php
    return [
    //...        
    	'session' => [
    		'value' => [
    			'lifetime' => 14400, // +
    			'mode' => 'separated', // +
    			'handlers' => [
    				'kernel' => 'encrypted_cookies',  // +  
    				'general' => [
    					'type' => 'redis',   
    					'port' => '6379',
    					'host' => '127.0.0.1',
    					],           
    				],
    			]                   
    		] 
    ];

    Memcache

    // bitrix/.settings.php
    return [
    //...        
    	'session' => [
    		'value' => [
    			'mode' => 'default',
    			'handlers' => [
    				'general' => [
    					'type' => 'memcache',   
    					'port' => '11211',
    					'host' => '127.0.0.1',
    					],           
    				],
    			]                   
    	] 
    ];

    Кластерное хранения данных сессии

    Если необходимо создать кластер из memcache серверов, то достаточно добавить настройку servers.

    // bitrix/.settings.php
    return [
    //...        
    	'session' => [
    		'value' => [
    			'mode' => 'default',
    			'handlers' => [
    				'general' => [
    					'type' => 'memcache',   
    					'servers' => [
    						[
    							'port' => 11211,
    							'host' => '127.0.0.1',
    							'weight' => 1, //про настройку weight читайте внимательно в документации по memcahe
    						],
    						[
    							'port' => 11211,
    							'host' => '127.0.0.2',
    						],
    					],
    				],           
    			],
    		]                   
    	] 
    ];

    Настройка для разделённой сессии:

    // bitrix/.settings.php
    return [
    //...        
    	'session' => [
    		'value' => [
    			'lifetime' => 14400, // +
    			'mode' => 'separated', // +
    			'handlers' => [
    				'kernel' => 'encrypted_cookies',  // +  
    				'general' => [
    					'type' => 'memcache',   
    					'port' => '11211',
    					'host' => '127.0.0.1',
    						],           
    					],
    				]                   
    		] 
    ];

    Mysql

    Данные хранятся в таблице b_user_session

    // bitrix/.settings.php
    return [
    //...        
    	'session' => [
    		'value' => [
    			'mode' => 'default',
    			'handlers' => [
    				'general' => [
    					'type' => 'database',       
    					]           
    				],         
    			]                   
    		] 
    ];

    Настройка для разделённой сессии:

    // bitrix/.settings.php
    return [
    //...        
    	'session' => [
    		'value' => [
    			'lifetime' => 14400, // +
    			'mode' => 'separated', // +
    			'handlers' => [
    				'kernel' => 'encrypted_cookies',  // +  
    				'general' => [
    					'type' => 'database',       
    					]           
    				],         
    			]                   
    		] 
    ];

    Работа с сессиями

    Продукт допускает работу с сессиями следующим образом:

    Хранение сессий в memcached

    Для включения хранения сессий в memcached в старом ядре необходимо в файле /bitrix/php_interface/dbconn.php установить следующие константы:

    define('BX_SECURITY_SESSION_MEMCACHE_HOST', 'localhost');
    define('BX_SECURITY_SESSION_MEMCACHE_PORT', 11211);

    Либо, в случае использования unix-socket:

    define('BX_SECURITY_SESSION_MEMCACHE_HOST', 'unix:///path/to/memcached.sock');
    define('BX_SECURITY_SESSION_MEMCACHE_PORT', 0);

    После этого, включить в файле [ds]bitrix/.settings.php[/ds][di]Ядро поддерживает четыре варианта для хранения (файлы, redis, database, memcache) данных сессии. Способ хранения описывается в bitrix/.settings.php в секции 'session'.

    Подробнее ...[/di] хранение сессий в базе данных средствами ядра.

    Данный способ хранения сессий дает следующие преимущества:

    • нет необходимости следить за количеством старых сессий на нагруженном проекте;
    • возможность разделять сессии между серверами в кластере;
    • возможность использовать не ожидающую получения блокировки сессию;
    • возможность использовать виртуальные сессии.

    Хранение сессий в БД имеет те же преимущества что и хранение сессий в memcached, но оно значительно более медленное. Поэтому рекомендуем для этой цели использовать memcached взамен БД.

    Неблокирующие сессии

    Одной из проблем больших проектов с множественными Ajax запросами, является частые блокировки хитов одного пользователя на ожидание получения блокировки сессии. Особенно это актуально для "Битрикс24 в коробке", где во многих местах прикрепленные к сущностям файлы отдаются пользователю, после проверки прав на PHP. Поэтому на страницах возможно построение "лесенки", из-за ожидания получения блокировки сессии. Включить не блокирующую сессию можно установкой константы, до подключения ядра продукта:

    define('BX_SECURITY_SESSION_READONLY', true);

    После этого сессия читается из memcached или БД не ожидая получения блокировки. Внутри продукта данная функциональность используется например при отдаче файлов.

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

    Виртуальные сессии

    Неблокирующих сессий достаточно для большинства случаев. Но в некоторых ситуациях, когда сессия нам не нужна совсем, использование неблокирующей сессии - избыточно, так как сессия будет создаваться в случае ее отсутствия. В результате при большом количестве не связанных между собой хитов, будет создано большое количество "мусорных" сессий. Примером таких хитов, являются хиты REST'а.

    Для решения этой проблемы была добавлена Виртуальная сессия. Её суть в том, что сессия создается в памяти, не ждет блокировок и не сохраняется. Для ее включения необходимо установить константу, до подключения ядра продукта.

    define('BX_SECURITY_SESSION_VIRTUAL', true);

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



    Примечание: Если у вас "Битрикс24 в коробке", проект с большим количеством ajax запросов или много файлов отдается с проверкой прав (например в блогах, соцсети, форуме), то лучше использовать хранение сессий в memcached средствами ядра.


    Шифрованные cookies

      Шифрованные куки

    Шифрованные [ds]куки[/ds][di] Cookie - это текстовая строка информации, которую веб-сервер передает в браузер посетителя сайта и которая сохраняется в файле на устройстве посетителя сайта. Как правило, используется для определения уникальности посетителя, времени его последнего визита, личных настроек, уникального идентификатора корзины покупок и т.д.

    Подробнее...[/di] (\Bitrix\Main\Web\CryptoCookie) позволяют отправлять данные пользователю, не раскрывая содержимое и не позволяя изменять данные внутри. Доступно с версии main 20.5.400.

    Конфигурация

    Чтобы ядро могло шифровать данные необходимо указать в настройках /bitrix/.settings.php crypto_key. По умолчанию, в новых дистрибутивах он генерируется автоматически.

    Если он отсутствует, то добавьте его в ручную в файл настроек ядра:

    <?php
    return [
    	//...
    	'crypto' => [
    		'value' => [
    			'crypto_key' => 'mysupersecretphrase',
    			//советуем устанавливать 32-х символьную строку из a-z0-9,
    		],
    		'readonly' => true,
    	]
    
    	//...
    ];
    

      Примеры использования

    Установка Cookie

    Чтобы установить шифрованную cookie, достаточно создать объект, как в сниппете ниже добавить в нужный Response:

    $cookie = new \Bitrix\Main\Web\CryptoCookie('someName', 'secret value');
    \Bitrix\Main\Context::getCurrent()->getResponse()->addCookie($cookie);

    Так как значение cookie ограничено по длине, а данные шифруются и упаковываются в base64, то во избежание потери данных, ядро может создать несколько cookies, в которых будет зашифрованное значение.

    В итоге в http-ответе будет содержаться cookie someName cо значением -crpt-someName_0. И cookie someName_0 уже с шифрованным значением вида DRMg6jrwXO1aUxTvdyBYyT-3_bCqomI9MMN_enurA5abplMm2OiSlNdu_1zgjbkKT_3D3uT8366.

    Чтение Cookie

    Чтобы получить расшифрованное значение cookie, достаточно использовать стандартное API ядра по работе с cookies:

    $httpRequest = \Bitrix\Main\Context::getCurrent()->getRequest();
    
    echo $httpRequest->getCookie('someName');
    //secret value

    Ядро автоматически определяет, что cookie шифрованная или нет, распаковывает значение и дешифрует. Если значение не удаётся расшифровать, то будет получено пустое значение.


    Использование постранички для массивов данных

    Пример реализации выборки из нескольких инфоблоков с постраничной навигацией и сортировкой.

    Задача

    1. Выбрать список элементов из нескольких инфоблоков в таблицу;
    2. Иметь возможность сортировки элементов;
    3. Должна работать постраничная навигация

    Решение

    Этот пример можно использовать при небольшом количестве выбираемых элементов (максимум до 100), естественно с кэшированием.

    Соберём все данные в ассоциативный массив, например вот такой:

    [ITEM] => Array
    	(
    		[0] => Array
    			(
    				[CITY_NAME] => value
    				[CITY_DETAIL_URL] => value
    				[OBJECT_NAME] => value
    				[OBJECT_ID] => 2487
    				[DATE_CREATE] => 02.07.2006
    				[STATUS] => Y
    				[PAID_STATUS] => Y
    				[DETAIL_OBJECT_URL] => value
    			)
    
    		[1] => Array
    			(
    				[CITY_NAME] => value
    				[CITY_DETAIL_URL] => value
    				[OBJECT_NAME] => value
    				[OBJECT_ID] => 2489
    				[DATE_CREATE] => 02.07.2006
    				[STATUS] => Y
    				[PAID_STATUS] => N
    				[DETAIL_OBJECT_URL] => value
    			)

    Теперь нужно отсортировать массив $arResult['ITEM'], для этого пишем класс:

    class CCabinet_SortObject {
    
    	function __cmp_ValueOf($a, $b, $name, $order) {
    		if(is_set($a[$name]) && is_set($b[$name])) {
    			if($order == 'ASC')
    				return ($a[$name]<$b[$name])?true:false;
     			elseif($order == 'DESC')
     				return ($b[$name]>$a[$name])?false:true;
    		}
    	}
    
    	function cmp_STATUS_ASC($a, $b) {
    		return CCabinet_SortObject::__cmp_ValueOf($a, $b, "STATUS", "ASC");
    	}
    
    	function cmp_STATUS_DESC($a, $b) {
    		return CCabinet_SortObject::__cmp_ValueOf($a, $b, "STATUS", "DESC");
    	}
    
    	function cmp_NAME_ASC($a, $b) {
    		return CCabinet_SortObject::__cmp_ValueOf($a, $b, "OBJECT_NAME", "ASC");
    	}
    
    	function cmp_NAME_DESC($a, $b) {
    		return CCabinet_SortObject::__cmp_ValueOf($a, $b, "OBJECT_NAME", "DESC");
    	}
    
    	function cmp_CITY_ASC($a, $b) {
    		return CCabinet_SortObject::__cmp_ValueOf($a, $b, "CITY_NAME", "ASC");
    	}
    
    	function cmp_CITY_DESC($a, $b) {
    		return CCabinet_SortObject::__cmp_ValueOf($a, $b, "CITY_NAME", "DESC");
    	}
    
    	function cmp_DATE_DESC($a, $b) {
    		if ($a["DATE_CREATE"] == $b["DATE_CREATE"]) {
            	return 0;
    	    }
    	    return ($a["DATE_CREATE"] > $b["DATE_CREATE"]) ? -1 : 1;
    	}
    
    	function cmp_DATE_ASC($a, $b) {
    		if ($a["DATE_CREATE"] == $b["DATE_CREATE"]) {
            	return 0;
    	    }
    	    return ($a["DATE_CREATE"] < $b["DATE_CREATE"]) ? -1 : 1;
    	}
    
    }

    Вот пример применения класса:

    usort($arResult['ITEM'], array("CCabinet_SortObject", "cmp_".$arParams['SORT_BY']."_".$arParams['SORT_ORDER']));

    После чего нам нужно разбить массив постранично используя API:

    $rs_ObjectList = new CDBResult;
    $rs_ObjectList->InitFromArray($arResult['ITEM']);
    $rs_ObjectList->NavStart(10, false);
    $arResult["NAV_STRING"] = $rs_ObjectList->GetPageNavString("", 'komka.cabinet');
    $arResult["PAGE_START"] = $rs_ObjectList->SelectedRowsCount() - ($rs_ObjectList->NavPageNomer - 1) * $rs_ObjectList->NavPageSize;
    while($ar_Field = $rs_ObjectList->Fetch())
    {
    $arResult['_ITEM'][] = $ar_Field;
    }

    Работа с БД

    Общая архитектура классов API для работы с базами данных:

    • Пул соединений Bitrix\Main\Data\ConnectionPool управляет соединениями, параметры которых настроены в файле настроек .settings.php. В пуле есть соединение по умолчанию и может быть набор дополнительных именованных соединений. Например, соединений к другой базе данных.
    • Конкретные классы соединений наследуют абстрактный класс Bitrix\Main\DB\Connection. Для разных типов баз данных реализованы разные конкретные классы.
    • Конкретные классы формирования SQL запросов, наследующие Bitrix\Main\DB\SqlHelper. Они помогают сформировать запрос не опускаясь до синтаксиса конкретной базы данных.
    • Конкретные классы для работы с результатом выполнения запроса, наследующие Bitrix\Main\DB\Result.

    Эти классы позволяют работать с базами данных на низком уровне, но это необходимо в небольшом числе случаев. Предпочтительно работать через ORM, которая позволяет программировать только на уровне бизнес-логики.

  • Получение соединения, именованные соединения
  • Разные формы вызова выполнения запроса
  • Получение результатов запроса
  • Работа со временем
  • Работа с несколькими базами данных
  • Поддержка нового типа БД

  • Получение соединения, именованные соединения

    Получить соединение можно через приложения, которое, кроме всего прочего, является точкой входа. С этой точки входа можно получить экземпляры "звездных" объектов для данного приложения, которые нужны всем (или почти всем) страницам или компонентам данного приложения.

    $connection = Bitrix\Main\Application::getConnection();
    $sqlHelper = $connection->getSqlHelper();
    
    $sql = "SELECT ID FROM b_user WHERE LOGIN = '".$sqlHelper->forSql($login, 50)."'";
    
    $recordset = $connection->query($sql);
    while ($record = $recordset->fetch())
    {
    	***

    Разные формы вызова выполнения запроса

    Соответственно, через приложение выполняется запрос в разной форме: просто запрос, запрос с указанием лимита на записи, скалярный запрос либо запрос "вообще".

    $result1 = $connection->query($sql);
    $result2 = $connection->query($sql, $limit);
    $result3 = $connection->query($sql, $offset, $limit);
    
    $cnt = $connection->queryScalar("SELECT COUNT(ID) FROM table");
    
    $connection->queryExecute("INSERT INTO table (NAME, SORT) VALUES ('Название', 100)")

    Получение результатов запроса

    $connection = Bitrix\Main\Application::getConnection();
    $sqlHelper = $connection->getSqlHelper();
    
    $sql = "SELECT ID FROM b_user WHERE LOGIN = '".$sqlHelper->forSql($login, 50)."' ";
    
    $recordset = $connection->query($sql);
    while ($record = $recordset->fetch())
    {
    	***

    Типизированные данные возвращаются сразу в виде типа, а не в виде строк или чисел.

    Модификация результата:

    $connection = \Bitrix\Main\Application::getConnection();
    $recordset = $connection->query("select * from b_iblock_element", 10);
    $recordset->addFetchDataModifier(
    	function ($data)
    	{
    		$data["NAME"] .= "!";
    		return $data;
    	}
    );
    
    while ($record = $recordset->fetch(\Bitrix\Main\Text\Converter::getHtmlConverter()))
    	{
    		$data[] = $record;
    	}

    Результат можно модифицировать, применить конвертер к результатам сразу, например, подготовить к выводу в XML, и так далее.


    Работа со временем

    При проектировании своей БД возникает вопрос, какой тип данных использовать: datetime или timestamp? При работе с БД приведение времени к GMT является минимально необходимым. Чтобы меньше зависеть от настроек сервера, лучше использовать datetime. В этом случае текущее значение времени придется получать на PHP.


    Работа с несколькими базами данных

    В рамках работы ORM можно работать с несколькими базами данных. Для этого в файле /bitrix/.settings.php в секции connections создается несколько записей для работы с базой:

    'default' =>
    	array(
    		'className' => '\\Bitrix\\Main\\DB\\MysqlConnection',
    		'host' => 'localhost', 
    		'database' => 'site', 
    		'login' => 'user', 
    		'password' => 'passwd',
    		'options' => 2,
    	),
    'old_db' =>
    	array(
    		'className' => '\\Bitrix\\Main\\DB\\MysqlConnection',
    		'host' => 'localhost',
    		'database' => 'old_db',
    		'login' => 'user',
    		'password' => 'passwd',
    		'options' => 2,
     ), 

    Таким образом подключены 2 базы: default (по умолчанию Bitrix Framework использует это подключение) и old_db (указаны параметры подключения ко 2-й базе данных). Обращаться к old_db нужно так:

    $connection = Bitrix\Main\Application::getConnection('old_db');
    $sqlHelper = $connection->getSqlHelper();
    
    $sql = "SELECT ID FROM b_user WHERE LOGIN = '".$sqlHelper->forSql($login, 50)."' ";
    
    $recordset = $connection->query($sql);
    while ($record = $recordset->fetch())
    {
    	*** 

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

    'old_db' => array(
    	'className' => '\\Bitrix\\Main\\DB\\MysqlConnection',
    	'host' => 'localhost',
    	'database' => 'old_db',
    	'login' => 'user',
    	'password' => 'passwd',
    	'options' => 2,
    	'include_after_connected' => $_SERVER["DOCUMENT_ROOT"].'/local/php_interface/after_connect_d7_old_db.php',
    ),
    

    Сам файл after_connect_d7_old_db.php

    <?
    $connectionOld = \Bitrix\Main\Application::getConnection("old_db");
    $connectionOld->queryExecute("SET NAMES 'utf8'");
    $connectionOld->queryExecute("SET collation_connection = 'utf8_unicode_ci'");
    ?>

    Для привязки сущности ORM к конкретному соединению переопределите в своем *Table классе метод Bitrix\Main\ORM\Data\DataManager::getConnectionName().


    Поддержка нового типа БД

    В редких случаях возникает необходимость подключения баз данных к которым невозможно использовать встроенный драйвер для PHP (например, у заказчика чрезвычайно устаревшая (или наоборот, слишком свежая) версия СУБД. Чтобы добавить в Bitrix Framework поддержку нового типа БД, необходимо:

    • Создать класс подключения (наследник Bitrix\Main\DB\Connection). В нем определить все базовые операции с БД: подключение, отключение, выполнение произвольного запроса, работу с транзакциями.
    • Создать класс SQL-хелпер (наследник Bitrix\Main\DB\SqlHelper) и возвращать его экземпляр в методе createSqlHelper. Класс предназначен для самой низкоуровневой работы с БД: добавляет экранирование, работает с датами, предоставляет доступ к базовым SQL-функциям и так далее.
    • Создать класс для результата выборки (наследник Bitrix\Main\DB\Result). В нем требуется определить методы-обертки над традиционными функциями работы с результатом выборки.

    Примечание: реальный пример: использование ORM для подключения из PHP к MSSQL в Linux от компании "Интерволга".


    Пример работы с БД

    Практический пример работы с Базой данных через API D7 на основе создания собственного компонента. Создадим компонент:

    <?php
    
    class d7SQL extends CBitrixComponent
    {
    	var $connection;
    	var $sqlHelper;
    	var $sql;
    
    	function __construct($component = null)
    	{
    		parent::__construct($component);
    		$this->connection = \Bitrix\Main\Application::getConnection();
    		$this->sqlHelper = $this->connection->getSqlHelper();
    
    		//Строка запроса. Выбираем все логины, активных пользователей
    		$this->sql = 'SELECT LOGIN FROM b_user WHERE ACTIVE = \''.$this->sqlHelper->forSql('Y', 1).'\' ';
    	}
    
    	/*
    	* Возвращаем все значения
    	*/
    	function var1()
    	{
    		$recordset = $this->connection->query($this->sql);
    			while ($record = $recordset->fetch())
    		{
    			$arResult[]=$record;
    		}
    
    		return $arResult;
    	}
    
    	/*
    	* Возвращаем первые два значения
    	*/
    	function var2()
    	{
    		$recordset = $this->connection->query($this->sql,2);
    		while ($record = $recordset->fetch())
    		{
    			$arResult[]=$record;
    		}
    
    		return $arResult;
    	}
    
    	/*
    	* Возвращаем два значения, отступая два элемента от начала
    	*/
    	function var3()
    	{
    		$recordset = $this->connection->query($this->sql,2,2);
    		while ($record = $recordset->fetch())
    		{
    		$arResult[]=$record;
    		}
    
    		return $arResult;
    	}
    
    	/*
    	* Возвращаем сразу первый элемент из запроса
    	*/
    	function var4()
    	{
    		$arResult = $this->connection->queryScalar($this->sql);
    
    		return $arResult;
    	}
    
    	/*
    	* Выполняем запрос, не возвращая результат, т. е. INSERT, UPDATE, DELETE
    	*/
    	function var5()
    	{
    		$this->connection->queryExecute('UPDATE b_user SET ACTIVE = \'N\' WHERE LOGIN=\'test\' ');//Заменить на UPDATE
    	}
    
    	/*
    	* Модифицируем результат
    	*/
    	function var6()
    	{
    		$recordset = $this->connection->query($this->sql);
    		$recordset->addFetchDataModifier(
    			function ($data)
    			{
    				$data["LOGIN"] .= ": Логин пользователя";
    				return $data;
    			}
    		);
    		while ($record = $recordset->fetch())
    		{
    			$arResult[]=$record;
    		}
    
    		return $arResult;
    	}
    
    	public function executeComponent()
    	{
    		//$this->arResult = $this->var1();
    
    		//$this->arResult = $this->var2();
    
    		//$this->arResult = $this->var3();
    
    		//$this->arResult = $this->var4();
    
    		//$this->var5();
    
    		$this->arResult = $this->var6();
    
    		$this->includeComponentTemplate();
    	}
    };

    В коде объявлены три переменные:

    1. connection - хранит подключение к базе данных;
    2. sqlHelper- хранит объект конкретного класса формирования sql запросов;
    3. sql - sql запрос.

    В конструкторе класса получаем соединение через приложения, которые, кроме всего прочего, являются точкой входа.

    Так же у нас здесь формируется строка запроса: выбираются из таблицы пользователей логины всех пользователей, которые активны, то есть поле ACTIVE установлено в Y. В строке запроса использован метод forSql, который делает входные параметры безопасными. Так же он может ограничить длину строки. В нашем случае он показан для примера: передан Y и указано что длина не должна быть больше одного символа.

    Через приложение выполняется запрос и получаются все значения, соответствующие значению.

    Функция var1: в ней осуществляется запрос и с помощью fetch получаются результаты. Типизированные данные возвращаются сразу в виде типа, а не в виде строк или чисел.

    Возвращаем все значения

    Функция var2. Здесь выполняется тот же самый запрос, но указывается лимит на количество получаемых элементов. В нашем случае 2.

    Возвращаем первые два значения

    Функция var3. Выполняется тот же самый запрос, но указываются два дополнительных параметра. Такая запись означает, то, что возвратятся два элемента. Это последний параметр. И эти элементы возвращаются нам начиная со второй позиции. Это второй параметр. То есть отступаем два элемента и отдаем два, начиная с третьего элемента.

    Возвращаем два значения, отступая два элемента от начала

    Функция var4 - скалярный запрос, то есть когда возвращается первый, единственный результат выборки.

    Возвращаем сразу первый элемент из запроса

    Функция var5 - выполнение запроса, без получения результата. Это нужно в случае INSERT, UPDATE, DELETE.

    Выполняем запрос, не возвращая результат

    Функция var6 - модификация результата. Смотрим . С помощью метода addFetchDataModifier объявляется функцию, которая на вход принимает массив результата для одного элемента и после модификации его возвращает. В нашем случае не сложный пример: просто к полю логин после двоеточия добавляется текст Логин пользователя.

    Модифицируем результат

    В метод fetch можно передать конвертер. Выглядит это так:

    <?
    $record = $recordset->fetch(\Bitrix\Main\Text\Converter::getHtmlConverter())

    Допустимо использовать методы Bitrix\Main\Text\Converter::getHtmlConverter и Bitrix\Main\Text\Converter::getXmlConverter. Соответственно, они подготавливают к выводу в html и в xml. Происходит преобразование специальных символов в html сущности.


    Миграция на MySQL

    Внимание! С 1 января 2017 года поддержка продуктов «1С-Битрикс» на Oracle Database и MS SQL Server стала ограниченной, заказчики не могут скачать обновления продукта платформы и воспользоваться возможностями новых релизов.

    Задачу миграции Базы данных сайта с Oracle или MSSQL на MySQL можно с помощью мигратора. "Переезд" с его помощью возможен с Oracle или MSSQL только в одну сторону, на MySQL.

    Мигратор работает на PHP не ниже версии 5.3.

    Порядок действий при миграции:

    1. Установите драйвера PHP для БД: mysql, oracle, mssql.
    2. Установите на БД MySQL редакцию 1С-Битрикс аналогичную используемой вами. Обновите её до той же версии, что и исходная установка.
    3. В файле config.php укажите данные доступа к исходной и конечной базам данных.

      Пример файла config.php

    4. Запустите конверсию командой "./converter.php convert oracle|mssql mysql".
    5. Внимательно отслеживайте диагностику, думайте дважды перед подтверждением операций. Детали процесса можно изучить в лог-файле.
    6. Если администратор не уверен в том с какими ключами нужно запускать конвертацию, то используйте команду "./converter.php" без параметров.
    7. Вручную скопируйте файлы из старой установки на новую.

    Смена кодировки сайта

    Рассмотрим общий порядок конвертации сайта с кодировки cp1251 в UTF-8.

    Внимание! Прежде чем приступить к конвертации сайта обязательно сделайте резервную копию сайта и базы данных. Настоятельно рекомендуем предварительно потренироваться выполнять конвертацию на отдельной копии сайта. Конвертация сайта сложная операция и каждый случай индивидуален. При её выполнении высока вероятность потерять важные данные, если что-то пойдет не так!

      Общий порядок действий

    Редактировать файлы и вносить правки на сервере можно подключаясь по SSH.

    Общий порядок действий:

    1. В региональных настройках Настройки > Настройки продукта > Языковые параметры > Региональные настройки сменитe кодировку на UTF-8 для всех языков;

    2. mbstring.func_overload до версии 20.100.0 модуля main

    3. Установите в файле настроек php.ini значение default_charset = "utf-8";

      Расположение файла настроек php.ini можно посмотреть заранее в административном разделе на странице [dw]Настройки PHP[/dw][di]Страница Настройки PHP (Настройки > Инструменты > Диагностика > Настройки PHP) служит для отображения информации о текущих настройках PHP.

      Подробнее в курсе Администратор. Базовый[/di] (Loaded Configuration File) или с помощью PHP функции phpinfo().

      Если сайт размещен на Хостинге, возможно понадобится обратиться к хостинг провайдеру для внесения этих настроек.

    4. Добавьте в /bitrix/php_interface/dbconn.php
      define("BX_UTF", true);
      

      В этом же файле удалите строки, относящиеся к кодировке cp1251:

      setlocale(LC_ALL, 'ru_RU.CP1251');
      mb_internal_encoding("Windows-1251");
      
    5. Установите значение 'value' => true для utf_mode в файле /bitrix/.settings.php:
      utf_mode =>
      	array(
      		'value' => true,
      		'readonly' => true,
      	),
      
    6. Перекодируйте всю базу данных в UTF-8. Вероятнее всего придётся обращаться за помощью к администратору сервера.
    7. Установите в файле /bitrix/php_interface/after_connect.php
      $DB->Query("SET NAMES 'utf8'");
      $DB->Query('SET collation_connection = "utf8_unicode_ci"');
      
      и в файле /bitrix/php_interface/after_connect_d7.php
      $this->queryExecute("SET NAMES 'utf8'");
      $this->queryExecute('SET collation_connection = "utf8_unicode_ci"');
      //До версии main 22.0 вместо $this использовалась переменная $connection.
    8. Установите в /.htaccess:
      php_value default_charset utf-8
      
    9. Перекодируйте все файлы сайта в UTF-8.
    10. Сбросьте весь кеш;
    11. Выйдите и зайдите заново на сайт чтобы обновить данные сессии.

      База данных

    Для конвертации базы (БД) потребуется сменить кодировку самой базы, всех её таблиц и всех текстовых полей таблиц. НЕ выполняйте конвертацию БД из административной части. Используйте для этого другие доступные средства.

    В простом случае (без сериализованных данных) перекодировать базу данных и все таблицы можно следующим образом:

    • Изменить кодировку самой базы данных сайта:
      ALTER DATABASE имя_базы_данных charset=utf8;
      
    • Изменить кодировку соединения с базой данных:
      SET NAMES 'utf8'
      
      ALTER DATABASE database_name CHARACTER SET utf8 COLLATE utf8_unicode_ci;
      
    • Выполним запрос, который позволит найти все таблицы базы данных и сформировать запрос на смену кодировки для каждой:
      SELECT CONCAT('ALTER   TABLE `', t.`TABLE_SCHEMA`, '`.`', t.`TABLE_NAME`, '` CONVERT TO CHARACTER SET utf8 COLLATE utf8_unicode_ci;') as sqlcode
      FROM `information_schema`.`TABLES` t
      WHERE 1
      AND t.`TABLE_SCHEMA` = 'имя_базы_данных'
      ORDER BY 1
      ;
      
    • В качестве ответа получим список запросов вида:
      ALTER TABLE `имя_базы_данных`.`имя_таблицы` CONVERT TO CHARACTER SET utf8 COLLATE utf8_unicode_ci;
      
    • Выполните все запросы. База данных и таблицы перекодированы.

    Внимание: Если в базе данных хранятся сериализованные данные, то приведенный выше метод конвертации для них не подойдет. Используйте специальные методы / средства для конвертации таких данных.

      Файлы

    В простом варианте, когда все файлы сайта в кодировке cp1251, перекодировать их в UTF-8 можно выполнив такую команду в корневой папке сайта (для UNIX систем):

    // переходим в корневую папку сайта. Например:
    cd /var/www/html/ 
    
    // выполняем команду для перекодирования файлов
    find . -name '*.php' -type f -exec iconv -fcp1251 -tutf8 -o /tmp/tmp_file {} \; -exec mv /tmp/tmp_file {} \;

    Важно:
    1. Способ не подходит для сайтов на нескольких языках, т.к. в таком случае в структуре будут присутствовать файлы в различных кодировках.
    2. Учитывайте особенности используемого вами Unix. Указанный выше пример может не сработать. В этом случае его надо адаптировать под вашу ОС. Например:
      // выполняем команду для перекодирования файлов
      find ./ -type f -name "*.php" -exec bash -c 'file="$1"; iconv -f cp1251 -t utf8 "$file" > "${file}.tmp" && mv "${file}.tmp" "$file"' _ {} \;

    Использование внешних программ или конвертация файлов вручную

    Часто при использовании внешних программ для конвертации в файлы добавляется специальная последовательность символов, так называемый BOM. Эти символы должны находиться только вначале файла, а поскольку итоговая страница является составной из нескольких php файлов, то спецсимволы появляются в теле страницы. Если делаете конвертацию файлов вручную - не сохраняйте с BOM!

      Бизнес-процессы

    [ICO_NEW data-adding-timestamp="1703770625"]

    Шаблоны бизнес-процессов с переменными, константами и параметрами хранятся в сериализованном и запакованном виде в таблице b_bp_workflow_template, поэтому смена кодировки БД не повлияет на них. Чтобы привести их к нужной кодировке требуется выполнить дополнительные действия.

    Сначала обязательно сделайте копию таблиц шаблонов b_bp_workflow_template одним из приведенных ниже способов:

    1. копированием с помощью SQL-запросов:
      //создаём новую таблицу, аналогичную оригинальной
      CREATE TABLE b_bp_workflow_template_bak LIKE b_bp_workflow_template;
      // копируем данные в созданную таблицу
      INSERT INTO b_bp_workflow_template_bak SELECT * FROM b_bp_workflow_template;
      
    2. созданием полной резервной копии базы данных.

    Следующим шагом выполните скрипт в командной PHP-строке, который изменит кодировку данных:

    cmodule::includemodule("bizproc");
    $connection = \Bitrix\Main\Application::getConnection();
    
    $sql_select = "select * from b_bp_workflow_template";
    $process = $connection->query($sql_select);
    
    while ($r = $process->fetch())
    {
    	$gztemp = $r['TEMPLATE'];
    	$gzvar = $r['VARIABLES'];
    	$gzconst = $r['CONSTANTS'];
    	$gzpar = $r['PARAMETERS'];
    	
    	// Распаковываем данные по БП.
    	$serializedTemplate = @gzuncompress($gztemp);
    	$serializedVariables = @gzuncompress($gzvar);
    	$serializedConstants = @gzuncompress($gzconst);
    	$serializedParameters = @gzuncompress($gzpar);
    	
    	// Рассериализуем данные по БП.
    	$serializedTemplate = @unserialize($serializedTemplate);
    	$serializedVariables = @unserialize($serializedVariables);
    	$serializedConstants = @unserialize($serializedConstants);
    	$serializedParameters = @unserialize($serializedParameters);
    	
    	if ($serializedTemplate === false) continue;
    	
    	// Меняем кодировку данных.
    	$serializedTemplate = $APPLICATION->ConvertCharsetArray(
    		$serializedTemplate,
    		'windows-1251',
    		'utf-8'
    	);
    	$serializedVariables = $APPLICATION->ConvertCharsetArray(
    		$serializedVariables,
    		'windows-1251',
    		'utf-8'
    	);
    	$serializedConstants = $APPLICATION->ConvertCharsetArray(
    		$serializedConstants,
    		'windows-1251',
    		'utf-8'
    	);
    	$serializedParameters = $APPLICATION->ConvertCharsetArray(
    		$serializedParameters,
    		'windows-1251',
    		'utf-8'
    	);
    	
    	$r["TEMPLATE"] = $serializedTemplate;
    	$r["VARIABLES"] = $serializedVariables;
    	$r["CONSTANTS"] = $serializedConstants;
    	$r["PARAMETERS"] = $serializedParameters;
    	
    	// Сохраняем обновленную информацию.
    	CBPWorkflowTemplateLoader::update(
    		$r["ID"],
    		[
    			'TEMPLATE' => $r['TEMPLATE'],
    			'VARIABLES' => $r['VARIABLES'],
    			'CONSTANTS' => $r['CONSTANTS'],
    			'PARAMETERS' => $r['PARAMETERS']
    		],
    		$r,
    		false,
    		false
    	);
    }
    [/ICO_NEW]

      Советы и ссылки

    Основные шаги по конвертации сайта выполнены. Если после конвертации возникают ошибки при открытии сайта, включите режим отладки 'debug' => true в файле /bitrix/.settings.php. Это позволит видеть где и какие возникают ошибки.

    Обязательно выполните [dw]проверку системы[/dw][di]Форма Проверка системы (Настройки > Инструменты > Проверка системы) предназначена для всесторонней проверки соответствия параметров системы, на которой осуществляется функционирование проекта, минимальным и рекомендуемым техническим требованиям продукта.

    Подробнее в курсе Администратор. Базовый.[/di]. По результатам проверки будет видно, что ещё нужно исправить. Пользуйтесь подсказками под знаками вопроса справа.

    Если возникли ошибки с таблицами базы данных (последняя строка проверки), можно посмотреть [dw]логи в журнале[/dw][di][/di]. В конце файла логов будут указаны запросы, с помощью которых можно исправить эти ошибки. Перед началом исправления рекомендуется сделать копию базы данных.

    Список ссылок по теме:



    Кастомизация Административной части

    Административная панель управления – HTML-код, который может быть выведен авторизованному пользователю при наличии у него достаточных прав на операции, представленные на панели управления. HTML-код представляет из себя область с кнопками, в самом верху страницы, каждая из которых предназначена для той или иной операции.

    Подключение панели осуществляется с помощью функции CMain::ShowPanel. Эта функция использует технологию отложенных функций, которая добавляет кнопки в панель непосредственно в теле страницы.

    Добавить кнопку в панель можно с помощью функции CMain::AddPanelButton. Она же используется в скрипте /bitrix/php_interface/include/add_top_panel.php, который автоматически подключается при выводе панели.

    Панель управления имеет два основных режима:

    • Сайт – для работы над содержимым сайта.
    • Администрирование – для полнофункционального управления всем проектом.

    Подробную информацию о работе с элементами управления в интерфейсе Эрмитаж смотрите в курсе Контент-менеджер. Исторически в Bitrix Framework существовало еще два вида административной панели. Оба они похожи между собой (но сильно отличаются от Эрмитажа), вы их можете встретить в давно не обновлявшихся проектах.

    Интерфейс "Эрмитаж" с точки зрения разработчика

    Подключение панели

    После авторизации на сайте для пользователя с соответствующими правами становится доступна панель Панель управления в верхней части страницы. С ее помощью можно:

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

    Код для подключения административной панели задается в служебной области шаблона дизайна сайта сразу после тега <body>.

    <?
    $APPLICATION->ShowPanel();
    ?>
    

    Примечание: Если пользователь не обладает достаточными правами, то панель для него не будет отображена.


    Панель также можно и принудительно отображать для требуемой группы или отдельных пользователей. Для этого необходимо воспользоваться опцией Всегда показывать панель для пользователей в настройках Главного модуля, закладка Настройки.

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

    Примечание: в концепции Эрмитажа основные действия выносятся в toolbar, остальные остаются в контекстном меню.

    Дополнительно

    Добавление кнопок на панель управления

    При создании собственных проектов может возникнуть потребность в создании новых кнопок на Панели управления. Добавление кнопок на панель управления можно осуществить следующим образом:

    <?$APPLICATION->AddPanelButton(
    	Array(
    		"ID" => "ID кнопки", //определяет уникальность кнопки
    		"TEXT" => "Название кнопки",
    		"TYPE" => "BIG", //BIG - большая кнопка, иначе маленькая
    		"MAIN_SORT" => 100, //индекс сортировки для групп кнопок
    		"SORT" => 10, //сортировка внутри группы
    		"HREF" => "URL для перехода", //или javascript:MyJSFunction())
    		"ICON" => "icon-class", //название CSS-класса с иконкой кнопки
    		"SRC" => "путь к иконке кнопки",
    		"ALT" => "Текст всплывающей подсказки", //старый вариант
    		"HINT" => array( //тултип кнопки
    			"TITLE" => "Заголовок тултипа",
    			"TEXT" => "Текст тултипа" //HTML допускается
    		),
    		"HINT_MENU" => array( //тултип кнопки контекстного меню
    			"TITLE" => "Заголовок тултипа",
    			"TEXT" => "Текст тултипа" //HTML допускается
    		),
    		"MENU" => Array(
    			Array( //массив пунктов контекстного меню
    				"TEXT" => "название пункта",
    				"TITLE" => "всплывающая подсказака над пунктом",
    				"SORT" => 10, //индекс сортировки пункта
    				"ICON" => "", //иконка пункта
    				"ACTION" => "Javascript-код",
    				"SEPARATOR" => true, //определяет пункт-разделитель
    				"DEFAULT" => true, //пункт по умолчанию?
    				"MENU" => Array() //массив подменю
    				)
    			)
    		),
    	$bReplace = false //заменить существующую кнопку?
    );	
    ?>
    

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

    • в компоненте
    • на странице
    • в шаблоне сайта
    • на событии OnBeforeProlog

    Результат добавления в шаблон сайта:

    Добавление контекстного меню

    Для добавления пунктов контекстного меню к любой кнопке панели используйте следующий код:

    $APPLICATION -> AddPanelButtonMenu($btnId, $arMenuItem)
    

    где:

    • $btnId – идентификатор кнопки;
    • $arMenuItem – массив пунктов.

    Примечание: пересортировку пунктов согласно индексу сортировки можно выполнить с помощью следующей конструкции:
    "RESORT_MENU" => true

    Toolbar компонента

    Для вывода toolbar'a компонента используйте в component.php следующий код:

    $this->AddIncludeAreaIcons(
    	Array( //массив кнопок toolbar'a
    		Array(
    			"ID" => "Идентификатор кнопки",
    			"TITLE" => "Название кнопки toolbar'a",
    			"URL" => "ссылка для перехода", //или javascript:MyJSFunction ()
    			"ICON" => "menu-delete", //CSS-класс с иконкой
    			"MENU" => Array(
    				//массив пунктов контекстного меню
    			),
    			"HINT" => array( //тултип кнопки
    				"TITLE" => "Заголовок тултипа",
    				"TEXT" => "Текст тултипа" //HTML допускается
    			),
    			"HINT_MENU" => array ( //тултип кнопки контекстного меню
    				"TITLE" => "Заголовок тултипа",
    				"TEXT" => "Текст тултипа" //HTML допускается
    			),
    			"IN_PARAMS_MENU" => true, //показать в контекстном меню
    			"IN_MENU" => true //показать в подменю компонента
    		)
    	)
    );
    //Режим редактирования включён?
    if ($APPLICATION->GetShowIncludeAreas())
    {
    	$this->AddIncludeAreaIcons(Array(
    		//массивы кнопок toolbar'a
    	));
    }

    Страница со списком элементов

    С версии 21.600.0 Главного модуля доступны новые методы объекта грида. Цель их внедрения - упростить и уменьшить объем кода, требующийся для создания своей админской страницы со списком элементов.

    Классы для работы со списками элементов

    Для работы со списками элементов в main есть два класса:

    • [dw]CAdminList[/dw][di]CAdminList.png[/di] - предназначен исключительно для страниц Административного раздела.
    • [dw]CAdminUiList[/dw][di]CAdminUiList.png[/di] (наследник CAdminList) - используется, если административный список выводится в магазине Б24.

    В коде страниц, использующих эти классы, имеется множество обращений к $_REQUEST для получения текущего режима страницы, текущего группового действия, измененных данных и т.п. Например, таких:

    $bExcel = isset($_REQUEST["mode"]) && ($_REQUEST["mode"] == "excel"); // режим выгрузки в Excel
    
    !isset($_REQUEST["mode"]) || $_REQUEST["mode"]=='list' || $_REQUEST["mode"]=='frame' 
    // все, кроме Excel и окна настроек
    
    switch($_REQUEST['action']) // получение идентификатора групповой операции
    

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

    Режим работы грида

    Грид может работать в нескольких режимах:

    • Простой вывод списка элементов (открытие страницы).
    • Обработка быстрого редактирования элементов.
    • Обновление грида без перезагрузки страницы (например, сортировка).
    • Выгрузка в формат для Excel.
    • Показ формы настроек грида.

    Ранее получение текущего режима сводилось к анализу ключа mode в $_REQUEST. Вместо этого предлагается использовать методы объекта грида:

    • Метод
      public function getCurrentMode(): string // получение режима работы
      

      вернет одну из констант класса:

      • public const MODE_PAGE = 'normal'; - обычный вывод страницы;
      • public const MODE_LIST = 'list'; - обновление содержимого грида без полной перезагрузки;
      • public const MODE_ACTION = 'frame'; - обработка результатов быстрого редактирования;
      • public const MODE_EXPORT = 'excel'; - выгрузка в псевдо-Excel;
      • public const MODE_CONFIG = 'settings'; - показ формы настроек.

    Режим определяется в момент инициализации объекта грида.

    Старый код:

    $sTableID = "tbl_user";
    $lAdmin = new CAdminUiList($sTableID);
    $excelMode = ($_REQUEST["mode"] == "excel");
    

    Новый:

    $sTableID = "tbl_user";
    $lAdmin = new CAdminUiList($sTableID);
    $excelMode = $lAdmin->getCurrentMode() === \CAdminList::MODE_EXPORT;
    

    Чтобы каждый раз не писать большой объем кода для сравнения с константой, добавлены вспомогательные методы:

    МетодОписание
    public function isPageMode(): boolВернет true, если страница с гридом просто открывается в браузере.
    public function isExportMode(): boolРежим выгрузки (Excel).
    public function isAjaxMode(): boolРабота в режиме ajax (как обновление грида, так и обработка быстрого редактирования).
    public function isConfigMode(): boolПоказ окна настроек.
    public function isActionMode(): boolРезультаты inline-редактирования.
    public function isListMode(): boolОбновления блока грида без обновления страницы.

    Таким образом, предыдущий пример превращается в такой код:

    $sTableID = "tbl_user";
    $lAdmin = new CAdminUiList($sTableID);
    $excelMode = $lAdmin->isExportMode();
    

    Обработка inline-редактирования элементов

    Типичный код обработки на странице

    if ($lAdmin->EditAction())
    {
    	if (is_array($_REQUEST['FIELDS']))
    	{
    		foreach($_REQUEST['FIELDS'] as $ID=>$arFields)
    	{
    		$ID = (int)$ID;
    		if ($ID <= 0)
    			continue;
    		if (!$lAdmin->IsUpdated($ID))
    			continue;
    		// дальше код, сохраняющий данные одной строки
    		}
    	}
    }
    

    можно заменить на такой:

    if ($lAdmin->EditAction())
    {
    	foreach ($lAdmin->getEditFields() as $ID=>$arFields)
    	{
    		//код, сохраняющий данные одной строки
    	}
    }
    
    • Метод

      public function getEditFields(): array
      

      возвращает массив записей, которые были изменены (с предварительной проверкой, что ключи непустые).

    • Дополнительно (если в гриде используется вывод и редактирование файлов) есть метод:

      public function convertFilesToEditFields(): void
      

      для копирования присланных файлов в общий массив полей. Т.е. предыдущий код слегка расширяем:

      if ($lAdmin->EditAction())
      {
      	$lAdmin->convertFilesToEditFields();
      	foreach ($lAdmin->getEditFields() as $ID=>$arFields)
      	{
      		//код, сохраняющий данные одной строки
      	}
      }
      

    Работа с групповыми операциями

    Типичный код обработки групповых операций:

    if ($arID = $lAdmin->GroupAction()
    {
    	if ($_REQUEST['action_target']=='selected')
    	{
    		// в $arID выбираем идентификаторы всех строки
    	}
    	foreach ($arID as $ID)
    	{
    		switch ($_REQUEST['action']) // получение ID действия
    		{
    		...
    		}
    	}
    }
    

    можно упростить так:

    if ($arID = $lAdmin->GroupAction()
    {
    	$actionId = $lAdmin->getAction();
    	if ($actionId === null)
    	{
    		continue;
    	}
    	if ($lAdmin-> isGroupActionToAll())
    	{
    		// в $arID выбираем все идентификаторы строк
    	}
    	foreach ($arID as $ID)
    	{
    		switch ($actionId) // действия
    		{
    		...
    		}
    	}
    }
    

    МетодОписание
    public function getAction(): ?stringВернет идентификатор группового действия или null, если ничего не передано.
    public function isGroupActionToAll(): boolВернет true, если в гриде для действий был выбран пункт "Для всех".

    Контекстное меню элементов списка

    • Установить HTML-атрибут id для блочного тега:
      <div id="<?=$this->GetEditAreaID("идентификатор_области")?>">
      	<!-- контент блока -->
      </div>
    • В component_epilog.php определить кнопки контекстного меню с помощью метода:
          $APPLICATION->SetEditArea($areaId, $arIcons);

      где:

      • $areaId – идентификатор области с контекстным меню;
      • $arIcons – массив иконок контекстного меню.
    • Метод добавляет кнопку, которая открывает указанный URL в popup-окне:
      $this->AddEditAction(
      	"Идентификатор_области",
      	"URL страницы, которая откроется в popup-окне",
      	"Название кнопки в toolbar",
      	Array(
      		"WINDOW" => array("wight"=>780, "height"=>500),
      		"ICON" => "bx-context-toolbar-edit-icon",
      		"SRC" => "/bitrix/images/myicon.gif"
      	)
      );
    • Метод добавляет кнопку удаления элемента:
      $this->AddDeleteAction(
      	"Идентификатор_области",
      	"URL страницы, удаляющая указанный элемент",
      	"Название кнопки",
      	Array(
      		"CONFIRM" => "Вы действительно хотите удалить этот элемент?",
      
      	)
      );

    Примечание: Для названия кнопки можно использовать такой метод:
    CIBlock::GetArrayByID($params["IBLOCK_ID"], "ELEMENT_EDIT")
    где вторым параметром будут как раз названия для кнопок (ELEMENT_EDIT, ELEMENT_ADD, ELEMENT_DELETE).

    Административные страницы в публичке

    • Метод генерирует Javascript, открывающий URL в popup-окне:
      $APPLICATION->GetPopupLink(Array(
      	"URL"=> "URL страницы, которая откроется в popup-окне",
      	"PARAMS" => Array(
      		"width" => 780,
      		"height" => 570,
      		"resizable" => true,
      		"min_width" => 780,
      		"min_height" => 400
      		)
      	)
      );
    • Метод генерирует кнопки управления элементами и разделами инфоблока:
      CIBlock::GetPanelButtons(
      	$IBLOCK_ID = 0, //ID инфоблока
      	$ELEMENT_ID = 0, //ID елемента инфоблока
      	$SECTION_ID = 0, //ID раздела инфоблока
      	$arOptions = Array(
      		"SECTION_BUTTONS" => true, //генерировать кнопки для управления разделами
      		"SESSID" => false, //добавлять ссылку в авторизованный token
      		"RETURN_URL" => "",
      		"LABELS" => Array() //надписи кнопок, по умолчанию берутся из настроек инфоблока
      	)
      );

    Пользовательские формы редактирования элементов

    Описание

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

    В этом случае в /bitrix/php_interface/include/ следует создать один или два (в зависимости от задачи) дополнительных файла:

    Затем в настройках инфоблока задать пути к этим файлам:

    Файл с формой редактирования элемента

    Создадим в папке /bitrix/php_interface/include/, например, файл iblock_element_edit_my.php, затем скопируем в него код из файла /bitrix/modules/iblock/admin/iblock_element_edit.php от строки:

    <?
    	//////////////////////////
    	//START of the custom form
    	//////////////////////////
    

    до строки:

    	//////////////////////////
    	//END of the custom form
    	//////////////////////////
    

    Важно! Не забудьте в вашем файле подключить необходимые пространства имен (смотрите первые строки файла /bitrix/modules/iblock/admin/iblock_element_edit.php).

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

    • Вы можете удалить ненужные вам поля инфоблока. Для отображения полей формы используются конструкции следующего вида:

      <?
      $tabControl->BeginCustomField("ACTIVE_TO", GetMessage("IBLOCK_FIELD_ACTIVE_PERIOD_TO"), $arIBlock["FIELDS"]["ACTIVE_TO"]["IS_REQUIRED"] === "Y");
      ?>
      	<tr id="tr_ACTIVE_TO">
      		<td><?echo $tabControl->GetCustomLabelHTML()?>:</td>
      		<td><?echo CAdminCalendar::CalendarDate("ACTIVE_TO", $str_ACTIVE_TO, 19, true)?></td>
      	</tr>
      <?
      $tabControl->EndCustomField("ACTIVE_TO", '<input type="hidden" id="ACTIVE_TO" name="ACTIVE_TO" value="'.$str_ACTIVE_TO.'">');
      ?>
      

    • Для отображения в форме свойств элементов инфоблока используется функция _ShowPropertyField():

      <?
      $prop_code = "20"; //идентификатор свойства
      $prop_fields = $PROP[$prop_code];
      $prop_values = $prop_fields["VALUE"];  
      
      $tabControl->BeginCustomField("PROPERTY_".$prop_fields["ID"], $prop_fields["NAME"], $prop_fields["IS_REQUIRED"]==="Y"); 
      ?>
      
      <tr id="tr_PROPERTY_<?echo $prop_fields["ID"];?>">
      <td class="adm-detail-valign-top" width="40%"><?if($prop_fields["HINT"]!=""):
      	?><span id="hint_<?echo $prop_fields["ID"];?>"></span>
      		<script>BX.hint_replace(BX('hint_<?echo $prop_fields["ID"];?>'), '<?echo CUtil::JSEscape($prop_fields["HINT"])?>');</script>
      	<?endif;?><?echo $tabControl->GetCustomLabelHTML();?>:</td>
      	<td width="60%"><?_ShowPropertyField('PROP['.$prop_fields["ID"].']',
      			$prop_fields,
      			$prop_fields["VALUE"],
      			(($historyId <= 0) && (!$bVarsFromForm) && ($ID<=0)),
      			$bVarsFromForm, 50000,
      			$tabControl->GetFormName(),
      			$bCopy);?></td>
      </tr>
      
      <?  
      $tabControl->EndCustomField("PROPERTY_".$prop_fields["ID"], $hidden);  
      ?>
      

    При использовании собственной формы на странице редактирования элемента пропадает кнопка Настроить, позволяющая отсортировать и настроить отображение полей формы элемента.

    Чтобы не добавлять механизм сортировки полей в iblock_element_edit_my.php и не отказываться от стандартной функции, необходимо добавить следующий код в файл:

    <?
    // Кнопка "Настроить"
    $aMenu = array();
    if (false == ((true == defined('BT_UT_AUTOCOMPLETE')) && (1 == BT_UT_AUTOCOMPLETE)))
    {
    	$link = DeleteParam(array("mode"));
    	$link = $GLOBALS["APPLICATION"]->GetCurPage()."?mode=settings".($link <> ""? "&".$link:"");
    	$aMenu[] = array(
    		"TEXT"=>GetMessage("IBEL_E_SETTINGS"),
    		"TITLE"=>GetMessage("IBEL_E_SETTINGS_TITLE"),
    		"LINK"=>"javascript:".$tabControl->GetName().".ShowSettings('".urlencode($link)."')",
    		"ICON"=>"btn_settings",
    	);
       
    	$context = new CAdminContextMenu($aMenu);
    	$context->Show();
    }
    ?>
    

    Важно! Не забудьте указать путь к данному файлу в настройках инфоблока.

    Файл, отвечающий за обработку полей элемента перед его сохранением

    Чтобы изменить сохраняемые поля, необходимо модифицировать одноименные поля в массивах $_POST и $_FILES, значения всех свойств необходимо модифицировать в массиве $PROP.

    Создадим в /bitrix/php_interface/include/, например, файл iblock_element_edit_before_save.php.

    Для проверки, что детальный текст элемента введен, используем следующее условие:

    if (strlen($_POST['DETAIL_TEXT'])<=0)
       $error = new _CIBlockError(2, 'DESCRIPTION_REQUIRED', 'Введите текст статьи');

    Конструктор объекта _CIBlockError принимает три параметра: степень серьезности ошибки, произвольный идентификатор и текст ошибки. Если на странице редактирования определить переменную $error со значением этого объекта, то сохранения внесённых изменений не произойдет. Для того чтобы значения, пришедшие из формы, не потерялись, после инициализации переменной $error также инициализируйте переменную $bVarsFromForm=true. Переменная $bVarsFromForm как раз указывает, что значения в полях необходимо показывать те, которые пришли из формы.

    Для автоматического создания маленькой картинки на основе большой воспользуемся функцией BXIBlockAfterSave. Если ее определить до сохранения элемента, то она автоматически будет вызвана после успешного сохранения элемента. Определим ее в начале файла /bitrix/php_interface/include/iblock_element_edit_before_save.php:

    <?
    function BXIBlockAfterSave($arFields)
    {
    	$dbr = CIBlockElement::GetByID($arFields['ID']);
    	if(($ar = $dbr->Fetch()) && $ar['DETAIL_PICTURE']>0)
    	{
    		$img_path = $_SERVER['DOCUMENT_ROOT'].CFile::GetPath($ar['DETAIL_PICTURE']);
    		$width = 200;
    		$height = 200;
    		list($width_orig, $height_orig) = getimagesize($img_path);
    		if($width && ($width_orig < $height_orig))
    			$width = ($height / $height_orig) * $width_orig;
    		else
    			$height = ($width / $width_orig) * $height_orig;
    			$image_p = imagecreatetruecolor($width, $height);
    			$image = imagecreatefromjpeg($img_path);
    			imagecopyresized($image_p, $image, 0, 0, 0, 0, $width, $height, $width_orig, $height_orig);
    			$new_img_path = tempnam("/tmp", "FOO").".jpg";
    			imagejpeg($image_p, $new_img_path);
    			$be = new CIBlockElement();
    			$be->Update($arFields['ID'], Array('PREVIEW_PICTURE' => CFile::MakeFileArray($new_img_path)), false);
    			@unlink($new_img_path);
    	}
    }
    ?>

    Примечание: в вышеприведенном скрипте на основе большой картинки будет создана маленькая и эта картинка будет подставлена в поле для маленькой картинки. Пример работает только с картинками в формате JPG.

    Приведем полный код страницы /bitrix/php_interface/include/iblock_element_edit_before_save.php:

    <?
    if($REQUEST_METHOD=="POST" && strlen($Update)>0 && $view!="Y" && (!$error) && empty($dontsave) && strlen($_POST['DETAIL_TEXT'])<=0)
       $error = new _CIBlockError(2, "DESCRIPTION_REQUIRED", "Введите текст статьи");
    
    function BXIBlockAfterSave($arFields)
    {
    	$dbr = CIBlockElement::GetByID($arFields['ID']);
    	if(($ar = $dbr->Fetch()) && $ar['DETAIL_PICTURE']>0)
    	{
    		$img_path = $_SERVER['DOCUMENT_ROOT'].CFile::GetPath($ar['DETAIL_PICTURE']);
    		$width = 200;
    		$height = 200;
    		list($width_orig, $height_orig) = getimagesize($img_path);
    		if($width && ($width_orig < $height_orig))
    			$width = ($height / $height_orig) * $width_orig;
    		else
    			$height = ($width / $width_orig) * $height_orig;
    		$image_p = imagecreatetruecolor($width, $height);
    		$image = imagecreatefromjpeg($img_path);
    		imagecopyresized($image_p, $image, 0, 0, 0, 0, $width, $height, $width_orig, $height_orig);
    		$new_img_path = tempnam("/tmp", "FOO").".jpg";
    		imagejpeg($image_p, $new_img_path);
    		$be = new CIBlockElement();
    		$be->Update($arFields['ID'], Array('PREVIEW_PICTURE' => CFile::MakeFileArray($new_img_path)), false);
    		@unlink($new_img_path);
    	}
    }
    ?>

    Важно! Не забудьте указать путь к данному файлу в настройках инфоблока.


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

    Дополнительные материалы


    Кастомизация административной формы заказа

    С версии 16.0.33 модуля Интернет-магазин доступна возможность кастомизации форм просмотра, редактирования и создания заказа. Функционал основан на использовании специальных событий, тем самым вам не требуется вносить правки в ядро продукта и вы не теряете возможность установки обновлений системы.

    Добавление пользовательской закладки

    Чтобы добавить собственную закладку в форму заказа, следует использовать обработчик соответствующего события:

    • OnAdminSaleOrderView - для формы просмотра заказа;
    • OnAdminSaleOrderCreate - для формы создания заказа;
    • OnAdminSaleOrderEdit- для формы редактирования заказа.

    Рассмотрим пример добавления закладки в форму редактирования заказа:

    В файл bitrix/php_interface/init.php необходимо добавить обработчик события OnAdminSaleOrderEdit:

    
    \Bitrix\Main\EventManager::getInstance()->addEventHandler("main", "OnAdminSaleOrderEdit", array("MyTab", "onInit"));
    
    class MyTab
    {
    	public static function onInit()
    	{
    		return array(
    		"TABSET" => "MyTab",
    		"GetTabs" => array("MyTab", "mygetTabs"),
    		"ShowTab" => array("MyTab", "myshowTab"),
    		"Action" => array("MyTab", "myaction"),
    		"Check" => array("MyTab", "mycheck"),
    		); 
    	}
    
    	public static function myaction($arArgs)
    	{
    		// Действие после сохранения заказа. Возвращаем true / false
    		// Сообщение $GLOBALS["APPLICATION"]->ThrowException("Ошибка!!!", "ERROR");
    		return true;
    	}
    	public static function mycheck($arArgs)
    	{
    		// Проверки перед сохранением. Возвращаем true / false
    		return true;
    	}
    
    	public static function mygetTabs($arArgs)
    	{
    		return array(array("DIV" => "edit1", "TAB" => "Моя закладка",
    		"ICON" => "sale", "TITLE" => "Пользовательские настройки",
    		"SORT" => 1));
    	}
    
    	public static function myshowTab($divName, $arArgs, $bVarsFromForm)
    	{
    		if ($divName == "edit1")
    		{
    		?><tr><td width="40%">Мое поле:</td>
    		<td width="60%"><input type="text" name="myfield"></td></tr><?
    		}
    	}
    }
    
    

    В обработчике указываем какие методы будут вызываться. Так с помощью методов mygetTabs и myshowTab определяем добавляемые закладки и их содержимое. Метод mycheck определяет действия, которые будут выполняться перед сохранением заказа, а метод myaction - действия, которые будут выполняться при сохранении заказа.

    Кастомизация информационной панели

    Рассмотрим пример добавления новых строчек в информационную панель:

    Следует использовать обработчик построения информационной панели onSaleAdminOrderInfoBlockShow и из него вернуть параметры, которые нам необходимо добавить:

    \Bitrix\Main\EventManager::getInstance()->addEventHandler('sale', 'onSaleAdminOrderInfoBlockShow', 'onSaleAdminOrderInfoBlockShow');
    
    function onSaleAdminOrderInfoBlockShow(\Bitrix\Main\Event $event)
    {
    	// $order = $event->getParameter("ORDER");
    
    	return new \Bitrix\Main\EventResult(
    			\Bitrix\Main\EventResult::SUCCESS,
    			array(
    				array('TITLE' => 'Параметр 1:', 
    				'VALUE' => 'Значение параметра 1', 'ID' => 'param1'),
    				array('TITLE' => 'Параметр 2::', 
    				'VALUE' => '<a href="http://1c-bitrix.ru">Значение 
    				параметра 2</a>'),
    				),
    			'sale'
    			);
    }
    

    Таким образом, вы можете вернуть необходимую дополнительную информацию, которую будете показывать менеджеру.

    Добавление пользовательских блоков

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

    Чтобы добавить пользовательские блоки в форму просмотра заказа, следует использовать обработчик события OnAdminSaleOrderViewDraggable:

    \Bitrix\Main\EventManager::getInstance()->addEventHandler("main", "OnAdminSaleOrderViewDraggable", array("MyClass1", "onInit"));
    
    class MyClass1
    {
    	public static function onInit()
    		{
    			return array("BLOCKSET" => "MyClass1",
    				"getScripts"  => array("MyClass1", "mygetScripts"),
    				"getBlocksBrief" => array("MyClass1", "mygetBlocksBrief"),
    				"getBlockContent" => array("MyClass1", "mygetBlockContent"),
    				);
    		}
    		
    	public static function mygetBlocksBrief($args)
    		{
    			$id = !empty($args['ORDER']) ? $args['ORDER']->getId() : 0;
    			return array(
    				'custom1' => array("TITLE" => "Пользовательский блок для заказа №".$id),
    				'custom2' => array("TITLE" => "Еще один блок для заказа №".$id),
    				);
    		}
    	
    	public static function mygetScripts($args)
    		{
    			return '<script type="text/javascript">... </script>';
    		}
    		
    	public static function mygetBlockContent($blockCode, $selectedTab, $args)
    		{
    		$result = '';
    		$id = !empty($args['ORDER']) ? $args['ORDER']->getId() : 0;
    		
    		if ($selectedTab == 'tab_order')
    			{
    			if ($blockCode == 'custom1')
    				$result = 'Содержимое блока custom1<br> Номер заказа: '.$id;
    			if ($blockCode == 'custom2')
    				$result = 'Содержимое блока custom2<br> Номер заказа: '.$id;
    			}
    			
    		return $result;
    		}
    }
    

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

    • mygetScripts - нужен, чтобы инициализировать JavaScript, которые вам понадобятся;
    • mygetBlocksBrief - нужен для описания блока;
    • mygetBlockContent - нужен для создания содержимого блока.

    Чтобы добавить пользовательские блоки в форму создания/редактирования заказа, следует использовать обработчики событий OnAdminSaleOrderCreateDraggable/OnAdminSaleOrderEditDraggable:

    \Bitrix\Main\EventManager::getInstance()->addEventHandler("main", "OnAdminSaleOrderEditDraggable", array("MyEditClass", "onInit"));
    \Bitrix\Main\EventManager::getInstance()->addEventHandler("main", "OnAdminSaleOrderCreateDraggable", array("MyEditClass", "onInit"));
    
    class MyEditClass extends MyClass1
    {
    	public static function onInit()
    		{
    			return array(
    				"BLOCKSET" => "MyEditClass",
    				"check" => array("MyEditClass", "mycheck"),
    				"action" => array("MyEditClass", "myaction"),
    				"getScripts" => array("MyEditClass", "mygetScripts"),
    				"getBlocksBrief" => array("MyEditClass", "mygetBlocksBrief"),
    				"getBlockContent" => array("MyEditClass", "mygetBlockContent"),
    			);
    		}
    
    	public static function myaction($args)
    	{
    		// заказ сохранен, сохраняем данные пользовательских блоков
    		// возвращаем True в случае успеха и False - в случае ошибки
    		// в случае ошибки $GLOBALS["APPLICATION"]->ThrowException("Ошибка!!!", "ERROR");
    		return true;
    	}
    
    	public static function mycheck($args)
    	{
    		// заказ еще не сохранен, делаем проверки
    		// возвращаем True, если можно все сохранять, иначе False
    		// в случае ошибки $GLOBALS["APPLICATION"]->ThrowException("Ошибка!!!", "ERROR");
    		return true;
    	}
    
    	public static function mygetBlockContent($blockCode, $selectedTab, $args)
    	{
    		$result = '';
    		$id = !empty($args['ORDER']) ? $args['ORDER']->getId() : 0;
    	
    		if ($selectedTab == 'tab_order')
    			{
    				if ($blockCode == 'custom1')
    					{
    						$result = 'Содержимое блока custom1 для заказа №'.$id.'
    						<br><input name="custom1" value="cust1">';
    					}
    				if ($blockCode == 'custom2')
    					{
    						$result = 'Содержимое блока custom2 для заказа №'.$id.'
    						<br><input name="custom2" value="cust2" 
    						id="custom2_field">';
    					}
    			}
    		
    			return $result;
    	}
    
    	//вставим итоговую стоимость заказа в кастомное поле №2 на странице создания/изменения заказа
    
    	public static function mygetScripts($args)
    	{
    		$id = !empty($args['ORDER']) ? $args['ORDER']->getId() : 0;
    	
    		return '<script type="text/javascript"> BX.ready(function(){
    			BX.Sale.Admin.OrderEditPage.registerFieldsUpdaters({
    				"TOTAL_PRICES": function(prices){
    					var custom = BX("custom2_field");
    					if (custom2_field)
    						custom2_field.value = prices.PRICE_TOTAL;
    				}	
    			});
    		});	</script>';
    	}
    
    }
    

    В обработчик, помимо методов mygetScripts, mygetBlocksBrief и mygetBlockContent, добавляются еще 2 метода: mycheck и myaction, которые вызываются перед сохранением и при сохранении заказа соответственно.


    Создание компонентов

    Цитатник веб-разработчиков.

    Роман Петров: Методы, которые были актуальны 2 года назад - сейчас могут быть неактуальны из-за появления готовых компонентов, делающих то же самое

    Фактически, на сегодняшний момент свой компонент нужно писать лишь тогда, когда нужен абсолютно новый функционал для сайта. Если учесть тот факт, что состав стандартных компонентов довольно большой, то в большинстве случаев написание новых и не требуется. Достаточно расширить функционал уже имеющихся.

    Тем не менее, приходит время, когда разработчик должен научиться создавать свои компоненты.

    Типовая последовательность действий

    • В веб-проекте, при составлении ТЗ и проектировании, выявляют и описывают возможные виды собственных компонентов.
    • Определяется пространство имен собственных компонентов, например, с использованием названия проекта. Системные компоненты Bitrix Framework размещены в пространстве имен bitrix, компоненты проекта могут размещаться в пространстве имен, к примеру, citybank.

      Внимание ! Названия создаваемых компонентов не должны пересекаться со стандартными.

    • Определяется, какой стандартный компонент можно взять за основу для создания собственного. В коде стандартных компонентов много примеров типичного и правильного использования API и техник программирования, поэтому рекомендуется брать их за основу.
    • К каждому компоненту продумывается интерфейс – какие параметры должны быть доступны администратору веб-сайта для редактирования. Например, для компонента, отображающего прогноз погоды, можно в настройки для администратора вынести свойство Адрес веб-сервиса и Таймаут соединения с веб-сервисом и т.п.
    • Определяется, в каком разделе дерева компонентов в визуальном редакторе необходимо разместить созданный компонент.
    • Компонент кодируется. Особое внимание уделяется настройке автокеширования компонента и профилированию его работы – он не выполняет запросы к базе данных в режиме кэширования, выполняет минимальное количество запросов к базе данных при устаревании кэша, хранит в кэше только необходимые данные, использует минимально возможный объем оперативной памяти (не сортирует массивы размером с десятки-сотни мегабайт и т.п).

    Порядок создания

    Выделить необходимый php-код в отдельный файл для того, чтобы использовать его потом в виде вызываемого файла несложно. Но компонент еще нужно подключить в систему с помощью файла описания, который опознается ядром Bitrix Framework. Без этого пользователь не увидит в визуальном редакторе иконку с названием компонента и сможет настраивать его свойства.

    Напомним, что компонент – это выделенный в отдельный файл php-код с законченной функциональностью, файл регистрации компонента в системе и описания его параметров, а также файлы локализации.

    • Регистрация компонента
      • Выделение необходимого php-кода в отдельный файл.
      • Создание файла описания .description.php
      • Размещение файлов в папке в собственном пространстве имен.
    • Задание параметров в коде компонента
    • Локализация
      • Подготовка файлов с текстовыми константами для компонента и файла регистрации: /lang/ru/<имя_компонента>/component.php и /lang/ru/<имя_компонента>/.description.php
      • Внесение изменения в код обоих файлов компонента для использования этих констант (подключение файла локализации делается при помощи функции IncludeTemplateLangFile).

    Важно! Все ключи в $MESS, содержащие название, описание и параметры компонента, а также идентификаторы веток компонента в дереве компонентов визуального редактора должны быть уникальными в рамках всего продукта.

    При создании компонента для Marketplace нужно создавать мастер установки (аналогично модулю).

    Совет от М. Месилова:

    Если на сайте используются компоненты собственной разработки и требуется их большое обновление, то рекомендуется использовать в префиксе номер версии: news.list.v2 так вы не поломаете старые компоненты по всему сайту и у вас будет возможность сделать новый компонент с потерей обратной совместимости.

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

    Примечание: При создании компонента рекомендуется использовать:
    • [ds]SEF режим[/ds][di]В комплексные компоненты встроена функция генерации ЧПУ. У этих компонентов всегда есть входной параметр SEF_MODE, который может принимать значения Y и N. Если параметр SEF_MODE равен N, то компонент работает с физическими ссылками и все параметры передает через стандартные параметры HTTP запроса.

      Подробнее ...[/di]

    а в шаблоне компонента:

    • [ds]JS-класс[/ds][di]Иногда при разработке компонента его шаблон необходимо наделить js-функциональностью, событиями и прочим. Выглядеть это может примерно так:

      Подробнее ...[/di],
    • [ds]буферизацию[/ds][di]Усовершенствованные методы буферизации в шаблоне позволяют более не использовать CBitrixComponentTemplate::EndViewTarget() ввиду того, что конец шаблона вызывает завершение буферизации автоматически.

      Подробнее ...[/di].


    Дополнительные методы

    Дополнительные методы, доступные в компонентах и шаблонах

    В компонентах и шаблонах можно использовать дополнительные методы из класса CComponentEngine.

    string CComponentEngine::MakePathFromTemplate($pageTemplate, $arParams);

    где:
    $pageTemplate - шаблон вида /catalog/#IBLOCK_ID#/section/#SECTION_ID#.php или catalog.php?BID=#IBLOCK_ID#&SID=#SECTION_ID#,
    $arParams - ассоциативный массив замен параметров, в котором ключ - это название параметра, а значение - это значение параметра. Возвращает путь на основании шаблона пути $pageTemplate и массива замен.

    Пример:

    $url = CComponentEngine::MakePathFromTemplate
    ("/catalog/#IBLOCK_ID#/section/#SECTION_ID#.php", 
    	array( 
    		"IBLOCK_ID" => 21, 
    		"SECTION_ID" => 452  
    		) 
    );

    Организация явной связи между компонентами на одной странице комплексного компонента

    Явную связь можно организовывать через возвращаемые значения и входящие параметры этих компонентов.

    Если из компонента comp1 нужно передать данные в компонент comp2, то в конце кода компонента comp1 нужно написать: return данные;

    Подключить comp1 нужно следующим образом:

    $result = $APPLICATION->IncludeComponent(comp1, ...);

    Теперь данные находятся в переменной $result и их можно передать входящими параметрами в comp2.


    Переопределение входящих переменных

    Каждый компонент имеет набор переменных, в которых он принимает извне коды или другие атрибуты запрашиваемых данных. Например, компонент bitrix:catalog.section имеет переменные IBLOCK_ID и SECTION_ID, в которых он принимает и обрабатывает коды каталога и группы товаров соответственно.

    Все компоненты, которые входят в состав комплексного компонента, должны иметь единообразный набор переменных. Например, комплексный компонент bitrix:catalog и все обычные компоненты (bitrix:catalog.section.list, bitrix:catalog.section и т.д.), которыми он управляет, работают с переменными IBLOCK_ID, SECTION_ID, ELEMENT_ID и другими.

    Если разработчик при размещении комплексного компонента на странице хочет переопределить переменные компонента, то он среди входящих параметров компонента должен задать параметр VARIABLE_ALIASES.

    При подключении компонента в режиме SEF (ЧПУ) этот параметр должен иметь вид:

    "VARIABLE_ALIASES" => array( 
    	"list" => array(),
    	"section" => array(
    		"IBLOCK_ID" => "BID",
    		"SECTION_ID" => "ID"
    		),
    	"element" => array(
    	"SECTION_ID" => "SID",
    	"ELEMENT_ID" => "ID"
    	),
    )

    Здесь коды массива соответствуют кодам в массиве шаблонов путей. Для каждого пути могут быть заданы свои переопределения переменных.

    При подключении компонента не в режиме SEF (ЧПУ) этот параметр должен имет вид:

    "VARIABLE_ALIASES" => array(
    	"IBLOCK_ID" => "BID",
    	"SECTION_ID" => "GID",
    	"ELEMENT_ID" => "ID",
    )

    Пример №1:

    Пусть требуется, чтобы компонент bitrix:catalog, лежащий в файле /fld/cat.php, работал с путями:
    /catalog/index.php – для списка каталогов,
    /catalog/section/код_группы.php?ID=код_каталога – для группы товаров,
    /catalog/element/код_товара.php?ID=код_группы – для детальной информации о товаре.

    Во входящих параметрах подключения компонента должны быть установлены следующие параметры:

    "SEF_MODE" => "Y",    
    "SEF_FOLDER" => "/catalog/",
    "SEF_URL_TEMPLATES" => array(
    	"list" => "index.php",
    	"section" => "section/#SECTION_ID#.php?ID=#IBLOCK_ID#",
    	"element" => "element/#ELEMENT_ID#.php?ID=#SECTION_ID#"    
                                ),
    "VARIABLE_ALIASES" => array(
    	"list" => array(),
    	"section" => array(
    		"IBLOCK_ID" => "ID"),
    			"element" => array(
    				"SECTION_ID" => "ID",),    
    

    Пример №2:

    Пусть требуется, чтобы компонент bitrix:catalog, лежащий в файле /fld/cat.php, работал с путями
    /fld/cat.php – для списка каталогов,
    /fld/cat.php?BID=код_каталога&SID=код_группы – для группы товаров,
    /fld/cat.php?ID=код_товара&SID=код_группы – для детальной информации о товаре.

    Во входящих параметрах подключения компонента должны быть установлены следующие параметры:

    "SEF_MODE" => "N",
    "VARIABLE_ALIASES" => array(
    	"IBLOCK_ID" => "BID",
    	"SECTION_ID" => "SID",
    	"ELEMENT_ID" => "ID",
    	),

    Пользовательские движки шаблонизации

    Шаблонизация

    Компоненты могут работать с любыми движками шаблонизации, которые могут быть подключены из PHP. Чтобы добавить новый движок шаблонизации на сайт необходимо определить (или дополнить) глобальную переменную $arCustomTemplateEngines в файле /bitrix/php_interface/init.php. В этой переменной содержится ассоциативный массив, каждый элемент которого имеет вид:

    "код_шаблонизатора" => array(
    	"templateExt" => array("расширение1"[, "расширение2"...]),
    	"function" => "имя_функции_подключения_движка"
    )

    где:

    • код_шаблонизатора - произвольное уникальное в рамках сайта слово,
    • расширениеN - расширение файла, который должен обрабатываться этим движком шаблонизации,
    • имя_функции_подключения_движка - имя функции, которая будет вызываться, если шаблон компонента имеет указанное расширение. Функцию можно разместить в этом же файле /bitrix/php_interface/init.php.

    Например, если на сайте кроме стандартного движка шаблонизации (PHP) требуется использовать Smarty, то в файл /bitrix/php_interface/init.php необходимо добавить следующий код:

    global $arCustomTemplateEngines;
    	$arCustomTemplateEngines = array(
    		"smarty" => array(
    			"templateExt" => array("tpl"),
    			"function" => "SmartyEngine"
    		),
    );

    Тогда при подключении шаблона с расширением tpl будет запускаться не стандартный движок PHP, а функция SmartyEngine, которая должна подключить движок Smarty.

    Синтаксис функций подключения движков следующий:

       function имя_функции_подключения_движка($templateFile, $arResult, $arParams, $arLangMessages, $templateFolder, $parentTemplateFolder, $template)

    где:

    • $templateFile – путь к файлу шаблона относительно корня сайта,
    • $arResult – массив результатов работы компонента,
    • $arParams – массив входных параметров компонента,
    • $arLangMessages – массив языковых сообщений (переводов) шаблона,
    • $templateFolder – путь к папке шаблона относительно корня сайта (если шаблон лежит не в папке, то эта переменная пуста),
    • $parentTemplateFolder - путь относительно корня сайта к папке шаблона комплексного компонента, в составе которого подключается данный компонент (если компонент подключается самостоятельно, то эта переменная пуста),
    • $template – объект шаблона.

    Код функции подключения движка шаблонизации зависит от подключаемого движка.

    Подключение Smarty

    Полный пример подключения движка Smarty

    В файл /bitrix/php_interface/init.php необходимо добавить код:

    global $arCustomTemplateEngines;
    $arCustomTemplateEngines = array(
    	"smarty" => array(
    		"templateExt" => array("tpl"),
    		"function" => "SmartyEngine"
    	)
    );
    
    function SmartyEngine($templateFile, $arResult, $arParams, $arLangMessages, $templateFolder, $parentTemplateFolder, $template)
    {
    	if (!defined("SMARTY_DIR"))
    		define("SMARTY_DIR", "<абсолютный путь к движку Smarty>/libs/");
    
    	require_once('<абсолютный путь к движку Smarty>/libs/Smarty.class.php');
    
    	$smarty = new Smarty;
    
    	$smarty->compile_dir = "<абсолютный путь к движку Smarty>/templates_c/";
    	$smarty->config_dir = "<абсолютный путь к движку Smarty>/configs/";
    	$smarty->template_dir = "<абсолютный путь к движку Smarty>/templates/";
    	$smarty->cache_dir = "<абсолютный путь к движку Smarty>/cache/";
    
    	$smarty->compile_check = true;
    	$smarty->debugging = false;
    
    	$smarty->assign("arResult", $arResult);
    	$smarty->assign("arParams", $arParams);
    	$smarty->assign("MESS", $arLangMessages);
    	$smarty->assign("templateFolder", $templateFolder);
    	$smarty->assign("parentTemplateFolder", $parentTemplateFolder);
    
    	$smarty->display($_SERVER["DOCUMENT_ROOT"].$templateFile);
    }

    Строку <абсолютный путь к движку Smarty> нужно везде заменить на абсолютный путь к движку Smarty в рамках вашей установки. Подробности по установке движка на сайт есть в системе помощи по Smarty.

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

    Подключение XML/XSLT

    Полный пример подключения движка XML/XSLT

    В файл /bitrix/php_interface/init.php необходимо добавить код:

    global $arCustomTemplateEngines;
    $arCustomTemplateEngines = array(
    	"xslt" => array(
    		"templateExt" => array("xsl"),
    		"function" => "XSLTEngine"
    	),
    );
    
    function CreateXMLFromArray($xDoc, $xNode, $ar)
    {
    	foreach($ar as $key=>$val)
    	{
    		if(!is_string($key) || strlen($key)<=0)
    			$key = "value";
    
    	$xElement = $xDoc->createElement($key);
    	if(is_array($val))
    	{
    		CreateXMLFromArray($xDoc, $xElement, $val);
    	}
    	else
    	{
    		$xElement->appendChild($xDoc->createTextNode(iconv(SITE_CHARSET, "utf-8", $val)));
    	}
    	$xNode->appendChild($xElement);
    	}
    	return $xNode;
    }
    
    function XSLTEngine($templateFile, $arResult, $arParams, $arLangMessages, $templateFolder, $parentTemplateFolder, $template)
    {
    	$arResult["PARAMS"] = array(
    		"templateFolder" => $templateFolder,
    		"parentTemplateFolder" => $parentTemplateFolder,
    		"arParams" => $arParams,
    		"arLangMessages" => $arLangMessages
    	);
    
    	$xDoc = new DOMDocument("1.0", SITE_CHARSET);
    	$xRoot = $xDoc->createElement('result');
    	CreateXMLFromArray($xDoc, $xRoot, $arResult);
    	$xDoc->appendChild($xRoot);
    
    	$xXsl = new DOMDocument();
    	$xXsl->load($_SERVER["DOCUMENT_ROOT"].$templateFile);
    
    	$xProc = new XSLTProcessor;
    	$xProc->importStyleSheet($xXsl);
    
    	echo $xProc->transformToXML($xDoc);
    }

    Разработка верстки шаблона компонента

    При разработке компонентов рекомендуем использовать технологию, которая может упростить работу партнерам, студиям при разработке. Разработка с её помощью не обязательна и не нужна всем подряд. Для внесения незначительных изменений в шаблон компонента верстальщику не нужно использовать описываемый ниже файл template.html.php. Этот файл рекомендуется использовать, при новой верстке шаблона компонента и при разработке нового компонента. Для доработки существующей верстки он не годится в полной мере.

    В общем виде веб-технолог должен сделать следующие шаги:

    1. Если используется система контроля версий, то уточнить у разработчика название ветки, компонент и страницу, в которой требуется создать новую верстку. Если такого компонента нет, попросить разработчика подготовить вышеперечисленное. Если разработка ведётся без системы контроля версий, то работу можно вести непосредственно в папке создаваемого компонента.
    2. В папке шаблона компонента создать файл с названием template.html.php. Данный шаблон будет подключаться вместо стандартного template.php, если в /bitrix/php_interface/init.php зарегистрировать свой шаблонизатор:
      global $arCustomTemplateEngines;
      $arCustomTemplateEngines = array(
      	"html" => array(
      		"templateExt" => array("html.php"),
      		"function" => "includeHtmlTemplate",
      		"sort" => 50
      	),
      );
      
      function includeHtmlTemplate($templateFile, $arResult, $arParams, $arLangMessages, $templateFolder, $parentTemplateFolder, $template)
      {
      	return $template->__IncludePHPTemplate($arResult, $arParams, $parentTemplateFolder);
      }

      Таким образом, веб-технолог будет работать с чистым шаблоном template.html.php. После размещения этого кода в init.php во всех компонентах, где в шаблоне есть файл template.html.php, происходит подключение именно его. Если нужно подключить файл template.php, то данный код нужно или удалить или закомментировать.

      Если же веб-технолог и программист работают одновременно на одной установке, то данный код можно подключать только по определенному условию: например, по id авторизованного пользователя.

    3. По окончании работ над template.html.php, разработчик должен интегрировать верстку и удалить временный шаблон.
    4. После этого веб-технолог работает по алгоритму модификации существующей верстки.

    Способы передачи данных между компонентами

    Способы передачи данных между компонентами:

    1. Глобальные переменные, Например:
      $GLOBALS['mycomponent_variable'] = $arResult["ID"];

      Кроме GLOBALS можно использовать $_SESSION при условиях, что:

      • данные небольшого объема;
      • сразу после передачи данные будут удалены из $_SESSION, так как в противном случае будут жить, пока сессия жива.
    2. Класс обертка, например:
      Class GarbageStorage{
      	private static $storage = array();
      	public static function set($name, $value){ self::$storage[$name] = $value;}
      	public static function get($name){ return self::$storage[$name];}
      }
      соответственно, использование:
      \GarbageStorage::set('MyCustomID', $arResult["ID"]); #установить значение
      \GarbageStorage::get('MyCustomID'); #получить значение

    Выбор способа зависит от компонентов и от того что именно вы хотите передать в другой компонент и есть ли необходимые данные в некешируемых файлах (речь идет о component_epilog.php). Использование класса обертки сложнее, но гораздо правильнее, особенно в свете создаваемого нового ядра.


    Простой пример создания компонента

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

    Предварительные действия

    Готовим php-код компонента

    Первым делом сразу напишем php-код, который выполняет, то, что нам нужно.

    <?
    echo date('Y-m-d');
    ?>

    Правда код просто выводит дату и нельзя выбрать другой формат. Лучше поместить в переменную формат вывода даты:

    <?
    $format = 'Y-m-d';
    echo date($format);
    ?>

    И последний штрих — нужно разделить логику и представление:

    <?
    // параметры
    $format = 'Y-m-d';
    // логика
    $d = date($format);
    // представление
    echo $d;
    ?>

    Создаем структуру папок и файлов компонента

    Теперь нужно создать свое пространство имен, например: dv. Для этого надо создать папку /local/components/dv. В ней делаем папку компонента — date.current. И в ней, в свою очередь, создаем два обязательных файла и папку для хранения шаблонов templates. В этой папке должна быть создана папка .default и в ней файл template.php.

    Получаем такую структуру в папке /local/components/dv/date.current:

    • component.php
    • .description.php
    • templates/.default/template.php

    Компонент без входных параметров

    Пока сделаем компонент без возможности задания входного параметра - формата даты.

    Содержимое файлов:

    • component.php
      <? if(!defined("B_PROLOG_INCLUDED") || B_PROLOG_INCLUDED!==true)die();
      	$arResult['DATE'] = date('Y-m-d');
      	$this->IncludeComponentTemplate();
      ?>
    • .description.php
      <? if (!defined("B_PROLOG_INCLUDED") || B_PROLOG_INCLUDED!==true) die(); $arComponentDescription = array(
      	"NAME" => GetMessage("Текущая дата"),
      	"DESCRIPTION" => GetMessage("Выводим текущую дату"),
      );
      ?>
    • templates/.default/template.php
      <? if(!defined("B_PROLOG_INCLUDED") || B_PROLOG_INCLUDED!==true)die();
      	echo $arResult['DATE'];
      ?>

    Как вы могли заметить в каждом из файлов компонента в начале пишется строка if (!defined(“B_PROLOG_INCLUDED”) || B_PROLOG_INCLUDED!==true) die();. Она нужна для того, чтобы данные файлы нельзя было вызвать напрямую из окна браузера.

    В простейшем виде компонент готов — его можно вызвать в коде страниц при помощи конструкции:

    <? $APPLICATION->IncludeComponent(
    	"dv:date.current",
    	".default",
    	Array(
    	),
    	false
    );?>

    Компонент с входными параметрами

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

    Чтобы наш компонент появился в визуальном редакторе нужно дополнить файл описания компонента.

    .description.php:
    <? if (!defined("B_PROLOG_INCLUDED") || B_PROLOG_INCLUDED!==true) die(); 
    $arComponentDescription = array(
    	"NAME" => GetMessage("Текущая дата"),
    	"DESCRIPTION" => GetMessage("Выводим текущую дату"),
    	"PATH" => array(
    		"ID" => "dv_components",
    		"CHILD" => array(
    			"ID" => "curdate",
    			"NAME" => "Текущая дата"
    		)
    ),
    "ICON" => "/images/icon.gif",
    );
    ?>

    Для размещения компонента в дереве компонентов мы добавили элемент массива описания PATH. Таким образом, наш компонент будет показан в отдельной папке. Опционально, можно задать иконку компонента — она будет показываться в дереве и в визуальном редакторе.

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

    <? if (!defined("B_PROLOG_INCLUDED") || B_PROLOG_INCLUDED!==true) die();
     $arComponentParameters = array(
    	"GROUPS" => array(),
    	"PARAMETERS" => array(
    		"TEMPLATE_FOR_DATE" => array(
    			"PARENT" => "BASE",
    			"NAME" => "Шаблон для даты",
    			"TYPE" => "STRING",
    			"MULTIPLE" => "N",
    			"DEFAULT" => "Y-m-d",
    		),
    	),
    );
    ?>

    И изменяем файл с логикой компонента, чтобы он мог использовать параметр, который мы задаем component.php:

    <? if(!defined("B_PROLOG_INCLUDED") || B_PROLOG_INCLUDED!==true)die();
    	$arResult['DATE'] = date($arParams["TEMPLATE_FOR_DATE"]);
    	$this->IncludeComponentTemplate();
    ?>

    Результат

    Мы создали компонент, в самом простом виде. Мы не учитывали мультиязычность, не учитывали возможность создания помощи к компоненту и прелести кеширования компонентов.

    Большая часть заказных компонентов для Bitrix Framework создается путем изменения компонентов, идущих в поставке продуктов. Поэтому очень нужно хорошо изучить стандартные компоненты, перед тем как программировать новые. Скорее всего, задачу, которую вы хотите решить — уже решили разработчики компании 1С-Битрикс.


    Пример создания компонента

    Рассмотрим пример создания компонента для сообщений администратору об ошибке.

    Описание

    С помощью этого компонента можно реализовать функционал, который бы позволял пользователям сообщать ответственным за контент о найденной на сайте ошибке. Ошибка будет высылаться почтовым уведомлением. Алгоритм работы пользователя с компонентом очень простой: если пользователь находит на портале ошибку, то он выделяет текст, нажимает Ctrl+Enter и получает форму:

    Форма заполняется и сообщение отправляется ответственному лицу.

    Создание почтового шаблона

    Так как сообщение об ошибке будет отправлено на почту, то потребуется создать новый почтовый тип.

    • Перейдите на страницу Настройки > Настройки продукта > Почтовые события > Типы почтовых событий.
    • Заполните поля формы:

    • Перейдите на страницу Настройки > Настройки продукта > Почтовые события > Почтовые шаблоны.
    • Нажмите в контекстной панели на Добавить шаблон, откроется форма создания шаблона.
    • Задайте шаблон для созданного типа почтового события:

    Создание компонента

    Создайте в собственном пространстве имен папку feedback.error со следующей структурой:

    • папка /images
      • файл feedback.gif
    • папка /templates
      • папка /.default
        • файл script.js
        • файл template.php
    • файл .description.php
    • файл .parameters.php
    • файл component.php

    Файл feedback.gif - иконка, которая будет отображаться в визуальном редакторе.

    код файла script.js:

    BX.bind(document, "keypress", SendError);
    
    function SendError(event, formElem)
    {
    	event = event || window.event;
    
    	if((event.ctrlKey) && ((event.keyCode == 0xA)||(event.keyCode == 0xD)))
    	{
    		var Dialog = new BX.CDialog({
    			title: "На сайте обнаружена ошибка!!",
    			head: "В чём заключается ошибка?",
    			content: 	'<form method="POST" id="help_form">\
    					<textarea name="error_desc" style="height: 78px; width: 374px;"></textarea>\
    					<input type="hidden" name="error_message"value="'+getSelectedText()+'">\
    		 			<input type="hidden" name="error_url" value="'+window.location+'">\
    					<input type="hidden" name="error_referer" value="'+document.referrer+'">\
    					<input type="hidden" name="error_useragent" value="'+navigator.userAgent+'">\
    					<input type="hidden" name="sessid" value="'+BX.bitrix_sessid()+'"></form>',
    						resizable: false,
    						height: '198',
    						width: '400'});
    
    		Dialog.SetButtons([
    	{
    		'title': 'Отправить',
    		'id': 'action_send',
    		'name': 'action_send',
    		'action': function(){
    			BX.ajax.submit(BX("help_form"));
    			this.parentWindow.Close();
    			}
    	},
    		{
    		'title': 'Отмена',
    		'id': 'cancel',
    		'name': 'cancel',
    		'action': function(){
    			this.parentWindow.Close();
    			}
    	}
    			]);
    			Dialog.Show();
    		}
    }
    function getSelectedText(){
    	if (window.getSelection){
    		txt = window.getSelection();
    	}
    	else if (document.getSelection) {
    		txt = document.getSelection();
    	}
    	else if (document.selection){
    		txt = document.selection.createRange().text;
    	}
    	else return;
    	return txt;
    }
    

    код файла template.php:

    <?
    CUtil::InitJSCore(array('window', 'ajax'));
    ?>

    код файла .description.php:

    <?
    if (!defined("B_PROLOG_INCLUDED") || B_PROLOG_INCLUDED!==true) die();
    
    $arComponentDescription = array(
    	"NAME" => "Send Error",
    	"DESCRIPTION" => "Send Error",
    	"ICON" => "/images/feedback.gif",
    	"PATH" => array(
    		"ID" => "utility",
    	),
    );
    ?>

    код файла .parameters.php:

    <?
    if (!defined("B_PROLOG_INCLUDED") || B_PROLOG_INCLUDED!==true)die();
    
    $arComponentParameters = array();
    ?>

    код файла component.php:

    <?
    if (check_bitrix_sessid() && $_SERVER['REQUEST_METHOD'] == "POST" && !empty($_REQUEST["error_message"]) && !empty($_REQUEST["error_url"]))
    {
    	$arMailFields = Array();
    	$arMailFields["ERROR_MESSAGE"] = trim ($_REQUEST["error_message"]);
    	$arMailFields["ERROR_DESCRIPTION"] = trim ($_REQUEST["error_desc"]);
    	$arMailFields["ERROR_URL"] = $_REQUEST["error_url"];
    	$arMailFields["ERROR_REFERER"] = $_REQUEST["error_referer"];
    	$arMailFields["ERROR_USERAGENT"] = $_REQUEST["error_useragent"];
    
    	CEvent::Send("ERROR_CONTENT", SITE_ID, $arMailFields);
    }
    $this->IncludeComponentTemplate();
    ?>

    Теперь подробнее для разработчиков о том, что находится в файле feedback.error\templates\.default\script.js.

    Объявление функции-обработчика, которая выводится на <body>:

    function SendError(event, formElem)

    Ожидание нажатия Ctrl+Enter:

    if((event.ctrlKey) && ((event.keyCode == 0xA)||(event.keyCode == 0xD)))

    Определение параметров будущего окна и его содержимого:

    var Dialog = new BX.CDialog({
    	title: "На сайте обнаружена ошибка!!",
    	head: "В чём заключается ошибка?",
    	content:    '<form method="POST" id="help_form" action="/bitrix/templates/.default/send_error.php">\
    		<textarea name="error_desc" style="height: 78px; width: 374px;"></textarea>\
    		<input type="hidden" name="error_message"value="'+getSelectedText()+'">\
    		<input type="hidden" name="error_url" value="'+window.location+'">\
    		<input type="hidden" name="sessid" value="'+BX.bitrix_sessid()+'"></form>',
    	resizable: false,
    	height: '198',
    	width: '400'});

    Определение набора кнопок:

    Dialog.SetButtons([
    {
    	'title': 'Отправить',
    	'id': 'action_send',
    	'name': 'action_send',
    	'action': function(){
    		BX.ajax.submit(BX("help_form"));
    		this.parentWindow.Close();
    	}
    },
    {
    	'title': 'Отмена',
    	'id': 'cancel',
    	'name': 'cancel',
    	'action': function(){
    		this.parentWindow.Close();
    	}
    },
    ]);

    Вывод окна:

    Dialog.Show();

    Функция getSelectedText() получает выделенный мышью текст. И далее идет отправка письма в тексте файла component.php:

    if (check_bitrix_sessid() && $_SERVER['REQUEST_METHOD'] == "POST" && !empty($_REQUEST["error_message"]) && !empty($_REQUEST["error_url"]))
    {
    	$arMailFields = Array();
    	$arMailFields["ERROR_MESSAGE"] = trim ($_REQUEST["error_message"]);
    	$arMailFields["ERROR_DESCRIPTION"] = trim ($_REQUEST["error_desc"]);
    	$arMailFields["ERROR_URL"] = trim ($_REQUEST["error_url"]);
    	CEvent::Send("BX", SITE_ID, $arMailFields);
    };

    Список ссылок по теме:


    Компонент интеграции визуального редактора

    Создадим компонент, интегрирующий популярный редактор TinyMCE в Bitrix Framework.

    Загрузите свежую версию редактора с сайта производителя.

    В своем пространстве имен в папке /local создайте структуру папок и файлов:

    • /bitrix/components/tools/;
      • /bitrix/components/tools/editor.tiny.mce/;
        • /bitrix/components/tools/editor.tiny.mce/templates/;
          • /bitrix/components/tools/editor.tiny.mce/templates/.default/;
        • /bitrix/components/tools/editor.tiny.mce/tiny_mce/ - папка для дистрибутива редактора;
        • component.php — логика компонента;
        • .parameters.php — файл для описания входящих параметров.

    Скопируйте скаченный дистрибутив в папку /tiny_mce.

    В файл component.php добавляем код:

    <? if (!defined("B_PROLOG_INCLUDED") || B_PROLOG_INCLUDED!==true) die();
    	$APPLICATION->AddHeadScript($this->__path .'/tiny_mce/tiny_mce.js');
    
    	$sNameTextArea   =  (isset($arParams['TEXTAREA_NAME'])   == false) ? 'content'   : $arParams['TEXTAREA_NAME'];
    	$sIdTextArea   	 =  (isset($arParams['TEXTAREA_ID'])     == false) ? '' 		 : $arParams['TEXTAREA_ID'];
    		if ('' == trim($sIdTextArea))
    		$sIdTextArea = 'content';
    	$sEditorID 		 =  (isset($arParams['INIT_ID'])  	     == false) ? 'textareas' : $arParams['INIT_ID'];
    	$iTextareaWidth  =  (isset($arParams['TEXTAREA_WIDTH'])  == false) ? '100%'      : $arParams['TEXTAREA_WIDTH'];
    	$iTextareaHeight =  (isset($arParams['TEXTAREA_HEIGHT']) == false) ? '300'       : $arParams['TEXTAREA_HEIGHT'];
    	$sText 			 =  (isset($arParams['TEXT']) 			 == false) ? ''       	 : $arParams['TEXT'];
    	?>
    
    <script type="text/javascript">
    
    <? 
    if ($arParams['TYPE_EDITOR'] == 'TYPE_1')
    {
    	?>
    	tinyMCE.init(
    		{
    			language : 'ru',
    			mode 	 : "textareas",
    			//elements : "<?=$sEditorID?>",
    			editor_selector : "<?=$sEditorID?>",
    			theme    : "advanced",
    			plugins  : "safari, spellchecker, upload.images.komka, wordcount, fullscreen",
    			theme_advanced_buttons1 : "formatselect,fontselect,fontsizeselect,bold,italic,underline,link,justifyleft,justifycenter, 
                                           justifyright,pasteword,pastetext,images,|,bullist,numlist,|,undo,redo,|,spellchecker,fullscreen",
    			theme_advanced_buttons2 : "",
    			theme_advanced_buttons3 : "",
    			theme_advanced_toolbar_location   : "top",
    			theme_advanced_toolbar_align      : "left",
    			theme_advanced_statusbar_location : "bottom",
    			theme_advanced_resizing           : false,
    			content_css                       : "<?=$this->__path?>/example.css",
    			height : "<?=$iTextareaHeight?>",
    			spellchecker_languages : '+Русский=ru,English=en',
    			spellchecker_word_separator_chars : '\\s!\"#$%&()*+,-./:;<=>?@[\]^_{|}'
    		}
    	);
    	<? 
    }
    elseif($arParams['TYPE_EDITOR'] == 'TYPE_2')
    {
    	?>
    		tinyMCE.init({
    				language : 'ru',
    				mode 	 : "textareas",
    				editor_selector : "<?=$sEditorID?>",
    				theme    : "advanced",
    				plugins : "safari,spellchecker,pagebreak,style,layer,table,save,advhr,advimage,advlink,emotions,iespell,inlinepopups,insertdatetime,preview,media,searchreplace,print,contextmenu,paste,directionality,fullscreen,noneditable,visualchars,nonbreaking,xhtmlxtras,template,imagemanager,filemanager",
    				theme_advanced_buttons1 : "save,newdocument,|,bold,italic,underline,strikethrough,|,justifyleft,justifycenter,justifyright,justifyfull,|,styleselect,formatselect,fontselect,fontsizeselect",
    				theme_advanced_buttons2 : "cut,copy,paste,pastetext,pasteword,|,search,replace,|,bullist,numlist,|,outdent,indent,blockquote,|,undo,redo,|,link,unlink,anchor,image,cleanup,help,code,|,insertdate,inserttime,preview,|,forecolor,backcolor",
    				theme_advanced_buttons3 : "tablecontrols,|,hr,removeformat,visualaid,|,sub,sup,|,charmap,emotions,iespell,media,advhr,|,print,|,ltr,rtl,|,fullscreen",
    				theme_advanced_buttons4 : "insertlayer,moveforward,movebackward,absolute,|,styleprops,spellchecker,|,cite,abbr,acronym,del,ins,attribs,|,visualchars,nonbreaking,template,blockquote,pagebreak,|,insertfile,insertimage",
    				theme_advanced_toolbar_location : "top",
    				theme_advanced_toolbar_align : "left",
    				theme_advanced_statusbar_location : "bottom",
    				theme_advanced_resizing : true,
    				content_css : "<?=$this->__path?>/example.css",
    				height : "<?=$iTextareaHeight?>",
    				template_external_list_url : "js/template_list.js",
    				external_link_list_url : "js/link_list.js",
    				external_image_list_url : "js/image_list.js",
    				media_external_list_url : "js/media_list.js",
    				template_replace_values : {username : "Some User", staffid : "991234"}
    			}
    		);
    	<? 
    }
    ?>
    
    </script>
    <textarea id="<?=$sIdTextArea?>" class="<?=$sEditorID?>"  name="<?=$sNameTextArea?>" style="width:<?=$iTextareaWidth?>"><?=$sText?></textarea>
    <? $this->IncludeComponentTemplate();?>
    

    Файл .parameters.php

    
    <? if (!defined("B_PROLOG_INCLUDED") || B_PROLOG_INCLUDED!==true) die();
    
    $arComponentParameters = array(
    	"PARAMETERS" => array(
    		"TYPE_EDITOR" => Array(
    			"PARENT" => "SETTINGS",
    			"NAME" => "Режим редактора",
    			"TYPE" => "LIST",
    			"VALUES" => array('TYPE_1' => 'Упрощенные редактор', 'TYPE_2' => 'Полной редактор'),
    		),
    
    		'INIT_ID' => array(
    			"PARENT" => "SETTINGS",
    			"NAME" => "ID редатора (уникальный)",
    			"TYPE" => "STRING",
    			"DEFAULT" => '',
    		),
    		
    		'TEXT' => array(
    			"PARENT" => "SETTINGS",
    			"NAME" => "Контент который нужно вставить в редактор",
    			"TYPE" => "STRING",
    			"DEFAULT" => $_POST['content'],
    		),
    		
    		'TEXTAREA_NAME' => array(
    			"PARENT" => "SETTINGS",
    			"NAME" => "Имя поля TEXTAREA",
    			"TYPE" => "STRING",
    			"DEFAULT" => 'content',
    		),
    		
    		'TEXTAREA_ID' => array(
    			"PARENT" => "SETTINGS",
    			"NAME" => "ID поля TEXTAREA",
    			"TYPE" => "STRING",
    			"DEFAULT" => 'content',
    		),
    		
    		'TEXTAREA_WIDTH' => array(
    			"PARENT" => "SETTINGS",
    			"NAME" => "Ширина редактора",
    			"TYPE" => "STRING",
    			"DEFAULT" => '100%',
    		),
    
    		'TEXTAREA_HEIGHT' => array(
    			"PARENT" => "SETTINGS",
    			"NAME" => "Высота редактора",
    			"TYPE" => "STRING",
    			"DEFAULT" => '300',
    		),
    	)
    );
    ?>

    Важные моменты в коде файла component.php, которые надо пояснить. Подключение собственно редактора:

    <?  $APPLICATION->AddHeadScript($this->__path .’/tiny_mce/tiny_mce.js’); ?>

    Подключение стилей, которые вынесены в папку с компонентом чтобы было удобнее, это настройка уже в js, в коде инициализации:

    content_css : ‘<?=$this->__path?>/example.css’,
    Внимание! если на странице 2 или более редакторов, то мы идентифицируем их по имени класса editor_selector : ‘<?=$sEditorID?>’.

    Собственно сама область текста для которой это все и делается:

    <textarea id=’<?=$sIdTextArea?>’  name=’<?=$sNameTextArea?>’ style=’width:<?=$iTextareaWidth?>’><?=$sText?></textarea>

    Как использовать

    Подключается вот в таком виде:

    <? echo $_POST['content'] ?>
    <? echo $_POST['content2'] ?>
    <form action="" method="post" name="">
    <? $APPLICATION->IncludeComponent("tools:editor.tiny.mce", ".default", array(
    	"TEXT" => $_POST["content"], // контент из запроса который нужно вставить
    	"TEXTAREA_NAME" => "content", // имя поля
    	"TEXTAREA_ID" => "content",         // id поля
    	"TEXTAREA_WIDTH" => "100%",  // ширина
    	"TEXTAREA_HEIGHT" => "300",    // высота
    	"INIT_ID" => "ID" // ID самого редактора
    ),
    false
    );
    ?>
    <input value="submit" name="sub" type="submit" />
    </form>

    Кеширование в собственных компонентах

    Описание

    Для чего нужно кэширование в собственных компонентах

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

    Все дело в производительности веб-проекта при одновременной работе с ним множества пользователей. Если компонент отрабатывает без кэширования за 0.1 сек, выполняя, допустим, 100 запросов к базе данных, то, при одновременной работе 100 пользователей не только резко возрастет нагрузка на сервер базы данных, но и время отработки компонента может вырасти, к примеру, до 5-10 секунд.

    Не менее важный момент, на который стоит обратить внимание – скорость отработки компонента при получении данных из кэша. Если без кэширования компонент отрабатывает за 2 сек. для каждого пользователя, то при использовании кэширования компонент для одного пользователя отработает за 2 сек., а для остальных 100 пользователей в ближайшие полчаса, допустим, будет отрабатывать 0.1 сек.

    При использовании кэширования в собственных компонентах 2.0:

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

    Время кеширования

    Примечание: В Bitrix Framework время кеширования учитывается в секундах.

    Период времени кэширования зависит от типа кеширования. Если используется Авто+Управляемое кэширование – информация будет отдаваться из кэша до тех пор, пока она не поменяется в базе данных и кэш сбросится автоматически. Время кэширования для этого режима должно быть большим, к примеру, 1 год.

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

    Встроенная поддержка кеширования

    В компонентах 2.0 есть встроенная поддержка типичного алгоритма кеширования. Структура компонента с использованием встроенной поддержки кеширования будет примерно такова:

    // Проверка и инициализация входных параметров
    if ($arParams["ID"] <= 0)
    	$arParams["ID"] = 10;
    
    // Если нет валидного кеша (то есть нужно запросить
    // данные и сделать валидный кеш)
    if ($this->StartResultCache())
    {
    	// Запрос данных и заполнение $arResult
    	$arResult = array(
    		"ID" => rand(1, 100)
    	);
    
    	for ($i = 0; $i < 5; $i++)
    		$arResult["FIELDS"][] = rand(1, 100);
    
    	// Если выполнилось какое-то условие, то кешировать
    	// данные не надо
    	if ($arParams["ID"] < 10)
    		$this->AbortResultCache();
    
    	// Подключить шаблон вывода
    	$this->IncludeComponentTemplate();
    }
    
    // Установить заголовок страницы с помощью отложенной
    // функции
    $APPLICATION->SetTitle($arResult["ID"]); 

    Пояснения по коду

    Метод StartResultCache имеет следующее описание: bool $this->StartResultCache($cacheTime = False, $additionalCacheID = False, $cachePath = False)

    где:

    • $cacheTime - время кеширования (если False - подставляется IntVal($arParams["CACHE_TIME"]));
    • $additionalCacheID - от чего дополнительно зависит кеш кроме текущего сайта SITE_ID, имени компонента и входных параметров;
    • $cachePath - путь к файлу кеша (если False - подставляется "/".SITE_ID.<путь к компоненту относительно bitrix/components>).

    Если есть валидный кеш, то метод отправляет на экран его содержимое, заполняет $arResult и возвращает False; если нет валидного кеша, то он возвращает True.

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

    if ($this->StartResultCache(false, $USER->GetGroups()))
    {
    	// Валидного кеша нет. Выбираем данные из 
    	// базы в $arResult
    }

    Если в результате выборки данных (в случае отсутствия валидного кеша) выяснилось, что кешировать данные не надо, то нужно вызвать метод $this->AbortResultCache();. Например, если выяснилось, что новости с таким ID нет, то нужно прервать кеширование и выдать сообщение, что такой новости нет. Если кеширование не прерывать, то злоумышленники смогут забить кешем все отведенное сайту дисковое пространство вызывая страницу с произвольными (в том числе и не существующими) ID.

    Метод $this->IncludeComponentTemplate(); подключает шаблон компонента и сохраняет в кеш-файл вывод и массив результатов $arResult. Все изменения $arResult и вывод после вызова метода подключения шаблона не будут сохранены в кеш.

    Если при исполнении кода компонента мы не вошли в тело условия if ($this->StartResultCache()), то значит для данного компонента, страницы и входных параметров есть валидный кеш. После вызова этого метода HTML из кеша отправлен на вывод и мы имеем заполненный массив $arResult. Здесь можно выполнить некоторые действия. Например, установить заголовок страницы с помощью отложенных функций.

    Если при выполнении некоторых условий нужно очистить кеш компонента (например, компонент знает, что данные изменились), то можно воспользоваться методом $this->ClearResultCache($additionalCacheID = False, $cachePath = False). Параметры этого метода соответствуют одноименным параметрам метода StartResultCache.

    Сложное кеширование

    Если компоненту требуется какое-либо особое кеширование, которое не может быть выполнено с помощью встроенной поддержки кеширования, то можно использовать стандартный класс CPHPCache. Структура компонента с использованием класса CPHPCache будет примерно такова:

    // Проверка и инициализация входных параметров
    if ($arParams["ID"] <= 0)
    	$arParams["ID"] = 10;
    
    $arParams["CACHE_TIME"] = IntVal($arParams["CACHE_TIME"]);
    $CACHE_ID = SITE_ID."|".$APPLICATION->GetCurPage()."|";
    // Кеш зависит только от подготовленных параметров без "~"
    foreach ($this->arParams as $k => $v)
    	if (strncmp("~", $k, 1))
    		$CACHE_ID .= ",".$k."=".$v;
    $CACHE_ID .= "|".$USER->GetGroups();
    
    $cache = new CPHPCache;
    if ($cache->StartDataCache($arParams["CACHE_TIME"], $CACHE_ID, "/".SITE_ID.$this->GetRelativePath()))
    {
    	// Запрос данных и формирование массива $arResult
    	$arResult = array("a" => 1, "b" => 2);
    
    	// Подключение шаблона компонента
    	$this->IncludeComponentTemplate();
    
    	$templateCachedData = $this->GetTemplateCachedData();
    
    	$cache->EndDataCache(
    		array(
    			"arResult" => $arResult,
    			"templateCachedData" => $templateCachedData
    		)
    	);
    }
    else
    {
    	extract($cache->GetVars());
    	$this->SetTemplateCachedData($templateCachedData);
    }

    Пояснения по коду

    Кеш должен зависеть только от подготовленных параметров. То есть от параметров, которые инициализированы нужным образом, приведены к нужному типу (например, с помощью IntVal()) и т.д. В массиве $arParams содержатся как подготовленные параметры, так и исходные параметры (с тем же ключем, но с префиксом "~"). Если кеш будет зависеть от неподготовленных параметров, то злоумышленники смогут забить кешем все отведенное сайту дисковое пространство вызывая страницу с ID равными "8a", "8b", ... (которые после IntVal() дадут 8).

    Метод $this->IncludeComponentTemplate() не запрашивает данные из базы. Но его тоже лучше включить в кешируемую область, так как этот метод производит определенные дисковые операции.

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

    Чтобы вернуть шаблону те данные, которые он просил сохранить в кеше, можно пользоваться методами $this->SetTemplateCachedData($templateCachedData); или CBitrixComponentTemplate::ApplyCachedData($templateCachedData);. Один из этих методов должен быть вызван в той области компонента, которая выполняется в случае наличия валидного кеша. В параметрах ему должны быть переданы те данные, которые шаблон просил сохранить.

    Пример сложного кеширования с классом кеша из D7:

    use Bitrix\Main\Data\Cache;
    
    // Проверка и инициализация входных параметров
    if ($arParams['ID'] <= 0) {
    	$arParams['ID'] = 10;
    }
    
    $arParams['CACHE_TIME'] = intval($arParams['CACHE_TIME']);
    
    $cacheId = implode('|', [
    	SITE_ID,
    	$APPLICATION->GetCurPage(),
    	$USER->GetGroups()
    ]);
    
    // Кеш зависит только от подготовленных параметров без "~"
    foreach ($this->arParams as $k => $v) {
    	if (strncmp('~', $k, 1)) {
    		$cacheId .= ',' . $k . '=' . $v;
    	}
    }
    
    $cacheDir = '/' . SITE_ID . $this->GetRelativePath();
    $cache    = Cache::createInstance();
    
    if ($cache->startDataCache($arParams['CACHE_TIME'], $cacheId, $cacheDir)) {
    	// Запрос данных и формирование массива $arResult
    	$arResult = ['a' => 1, 'b' => 2];
    
    	// Подключение шаблона компонента
    	$this->IncludeComponentTemplate();
    
    	$templateCachedData = $this->GetTemplateCachedData();
    
    	$cache->endDataCache([
    		'arResult'           => $arResult,
    		'templateCachedData' => $templateCachedData,
    	]);
    } else {
    	extract($cache->GetVars());
    	$this->SetTemplateCachedData($templateCachedData);
    }

    Несколько советов


    Если в компоненте используется стандартное кэширование, но при этом не подключается шаблон (по причине ненадобности), нужно использовать:

    if ($this->startResultCache())
    {
    	\\Код который модифицирует $arResult
    	$this->endResultCache();
    }


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

    $cache_id = serialize(array($arParams, ($arParams['CACHE_GROUPS']==='N'? false: $USER->GetGroups()))); 
    $obCache = new CPHPCache; 
    if ($obCache->InitCache($arParams['CACHE_TIME'], $cache_id, '/')) 
    { 
    	$vars = $obCache->GetVars(); 
    	$arResult = $vars['arResult']; 
    } 
    elseif ($obCache->StartDataCache()) 
    { 
    
    	// делаем то, что надо 
    
    	$obCache->EndDataCache(array( 
    		'arResult' => $arResult, 
    	)); 
    } 
    

    Если код написан правильно и в template.php нет «тяжелого» кода, то этот вариант может работать достаточно хорошо.


    Внешняя авторизация

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

    • Первая - пароли пользователей во внешней БД не хранятся в открытом виде, хранится только хэш от них, поэтому после переноса пользователи не смогут авторизоваться по причине несовпадения пароля.
    • Вторая причина заключается в том, что иногда необходимо чтобы база пользователей была единой, т.е. пароли пользователей хранились на одном удаленном сервере и использовались для проверки в нескольких местах, в том числе и в CMS.

    Для решения таких задач в Bitrix Framework, реализована возможность добавить к стандартной встроенной системе авторизации свою внешнюю. Для этого необходимо реализовать несколько шагов, которые разберем на примере внешней авторизации, используя БД пользователей популярного форума PHP BB.

    Для начала создадим файл, например, /bitrix/php_interface/scripts/phpbb.php. В этом файле будет размещён класс с внешним обработчиком, назовем его __PHPBB2Auth:

    class __PHPBB2Auth
    {
    }

    Чтобы во время попытки авторизоваться вызвалась наша функция, необходимо установить обработчик события OnUserLoginExternal, который будет вызываться автоматически каждый раз при вводе пользователем имени входа и пароля, перед встроенной проверкой. Для этого в файле /bitrix/php_interface/init.php воспользуемся функцией AddEventHandler:

    AddEventHandler(
    	"main", 
    	"OnUserLoginExternal", 
    	Array("__PHPBB2Auth", "OnUserLoginExternal"), 100, $_SERVER['DOCUMENT_ROOT'].'/bitrix/php_interface/scripts/phpbb.php'
    );

    В качестве обработчика мы указали метод нашего класса __PHPBB2Auth::OnUserLoginExternal. Обработчик события OnUserLoginExternal принимает в качестве параметра ссылку на массив с полями для проверки:

    define("PHPBB2_TABLE_PREFIX", "phpbb_");
    function OnUserLoginExternal(&$arArgs)
    {
    	$table_user = PHPBB2_TABLE_PREFIX."users";
    	$table_user_group = PHPBB2_TABLE_PREFIX."user_group";
    	extract($arArgs);
    
    	global $DB, $USER, $APPLICATION;
    
    	$strSql = "SELECT * FROM ".
    		$table_user.
    		" WHERE username='".
    		$DB->ForSQL($login).
    		"' AND user_password='".
    		$DB->ForSql(md5($password))."'";
    	$dbRes = $DB->Query($strSql);
    	if($arRes = $dbRes->Fetch())
    	{
    		if($arRes['user_active']!='0')
    		{
    		// имя пользователя и пароль подошел
    		}
    	}
    }

    После того как имя входа и пароль проверены по алгоритму PHPBB, необходимо создать внешнего пользователя во внутренней БД, чтобы к нему можно было привязывать внутренние объекты (новости, голосования и т.п.). Для этого воспользуемся методом CUser::GetList с фильтром по имени входа и коду внешнего источника. Если такой пользователь не существует - создадим его, если существует - обновим информацию о нем.

    $arFields = Array(
    	"LOGIN" => $login,
    	"NAME" => $login,
    	"PASSWORD" => $password,
    	"EMAIL" => $arRes['user_email'],
    	"ACTIVE" => "Y",
    	"EXTERNAL_AUTH_ID"=>"PHPBB2",
    	"LID" => SITE_ID
    	);
    $oUser = new CUser;
    $res = CUser::GetList($O, $B, Array("LOGIN_EQUAL_EXACT"=>$login, "EXTERNAL_AUTH_ID"=>"PHPBB2"));
    if(!($ar_res = $res->Fetch()))
    	$ID = $oUser->Add($arFields);
    else
    {
    	$ID = $ar_res["ID"];
    	$oUser->Update($ID, $arFields);
    }
    if($ID>0)
    {
    	// можно авторизовывать
    	return $ID;
    }

    Теперь у нас есть идентификатор пользователя в нашей БД и можно его вернуть из функции обработчика, чтобы этот пользователь был авторизован системой, но новый пользователь будет анонимным, т.к. не привязан ни к одной из групп. Воспользуемся привязкой в базе PHPBB, чтобы перенести ее в нашу БД до авторизации.

    $USER->SetParam("PHPBB2_USER_ID", $arRes['user_id']);
    $groups_map = Array(
    	/*'PhpBB2 Group ID' => 'Local Group ID',*/
    	'2' => '1'
    	);
    
    $user_groups = Array();
    $dbUserGroup = $DB->Query('SELECT * FROM '.$table_user_group.' WHERE user_id='.$arRes['user_id']);
    while($arUserGroup = $dbUserGroup->Fetch())
    	$user_groups[] = $arUserGroup['group_id'];
    
    if(count($user_groups)>0)
    {
    	$arUserGroups = CUser::GetUserGroup($ID);
    	foreach($groups_map as $ext_group_id => $group_id)
    	{
    		if(in_array($ext_group_id, $user_groups))
    			$arUserGroups[] = $group_id;
    		else
    		{
    		$arUserGroupsTmp = Array();
    		foreach($arUserGroups as $grid)
    			if($grid != $group_id)
    			$arUserGroupsTmp[] = $grid;
    		$arUserGroups = $arUserGroupsTmp;
    		}
    	}
    	CUser::SetUserGroup($ID, $arUserGroups);
    }

    Все, теперь локальный аккаунт пользователя соответствует удаленному, можно возвращать код пользователя и он будет авторизован. Отключим предварительно функцию запомнить меня на этом компьютере, если пользователь нажал на "флажок", т.к. при этом мы не сможем корректно проверить права доступа:

    $arArgs["store_password"] = "N";
    return $ID;

    Для того чтобы зарегистрировать новую внешнюю проверку авторизации в системе, необходимо обработать событие OnExternalAuthList. Добавим в файл /bitrix/php_interface/init.php соответствующий вызов:

    AddEventHandler(
    	"main", 
    	"OnExternalAuthList", 
    	Array("__PHPBB2Auth", "OnExternalAuthList"), 100, $_SERVER['DOCUMENT_ROOT'].'/bitrix/php_interface/scripts/phpbb.php'
         );

    Функция обработчик должна вернуть массив из набора обработчиков с полямим ID и NAME.

    function OnExternalAuthList()
    {
        return Array(
            Array("ID"=>"PHPBB2", "NAME"=>"PhpBB2")
            );
    }

    Теперь на странице редактирования пользователя появится выпадающий список со списком внешних источников авторизации. Приведем полный текст файла /bitrix/php_interface/scripts/phpbb.php. В нем дополнительно реализован обратный механизм: при авторизации пользователя в нашей системе он автоматически авторизуется на форуме.

    <?
    define("PHPBB2_TABLE_PREFIX", "phpbb_");
    
    class __PHPBB2Auth
    {
    	function OnUserLoginExternal(&$arArgs)
    	{
    		////////// <settings> ////////////
    		$table_user = PHPBB2_TABLE_PREFIX."users";
    		$table_user_group = PHPBB2_TABLE_PREFIX."user_group";
    		$groups_map = Array(
    			/*'PhpBB2 Group ID' => 'Local Group ID',*/
    			'2' => '1'
    			);
    		////////// </settings> ////////////
    		extract($arArgs);
    
    		global $DB, $USER, $APPLICATION;
    
    		$strSql = "SELECT * FROM ".
    			$table_user.
    			" WHERE username='".
    			$DB->ForSQL($login).
    			"' AND user_password='".
    			$DB->ForSql(md5($password))."'";
    		$dbRes = $DB->Query($strSql);
    		if($arRes = $dbRes->Fetch())
    		{
    		if($arRes['user_active']!='0')
    		{
    			$arFields = Array(
    			"LOGIN" => $login,
    			"NAME" => $login,
    			"PASSWORD" => $password,
    			"EMAIL" => $arRes['user_email'],
    			"ACTIVE" => "Y",
    			"EXTERNAL_AUTH_ID"=>"PHPBB2",
    			"LID" => SITE_ID
    			);
    			$oUser = new CUser;
    			$res = CUser::GetList($O, $B, 
    				Array("LOGIN_EQUAL_EXACT"=>$login, 
    				"EXTERNAL_AUTH_ID"=>"PHPBB2"));
    			if(!($ar_res = $res->Fetch()))
    				$ID = $oUser->Add($arFields);
    			else
    			{
    			$ID = $ar_res["ID"];
    			$oUser->Update($ID, $arFields);
    			}
    
    			if($ID>0)
    			{
    			$USER->SetParam("PHPBB2_USER_ID", $arRes['user_id']);
    
    			$user_groups = Array();
    			$dbUserGroup = $DB->Query('SELECT * FROM '.
    				$table_user_group.
    				' WHERE user_id='.$arRes['user_id']);
    			while($arUserGroup = $dbUserGroup->Fetch())
    				$user_groups[] = $arUserGroup['group_id'];
    
    			if(count($user_groups)>0)
    				{
    				$arUserGroups = CUser::GetUserGroup($ID);
    				foreach($groups_map as $ext_group_id => $group_id)
    				{
    					if(in_array($ext_group_id, $user_groups))
    					$arUserGroups[] = $group_id;
    				else
    				{
    				$arUserGroupsTmp = Array();
    				foreach($arUserGroups as $grid)
    					if($grid != $group_id)
                                            $arUserGroupsTmp[] = $grid;
    					$arUserGroups = $arUserGroupsTmp;
    				}
    			}
    			CUser::SetUserGroup($ID, $arUserGroups);
    		}
    			$arArgs["store_password"] = "N";
    
                        return $ID;
    				}
    			}
    		}
    	}
    
    	function OnExternalAuthList()
    	{
    		return Array(
    			Array("ID"=>"PHPBB2", "NAME"=>"PhpBB2 Board")
    		);
    	}
    
    	function OnAuthorize(&$arArgs)
    	{
    		extract($arArgs);
    
    		global $DB, $APPLICATION, $USER;
    		$user_id = $USER->GetParam("PHPBB2_USER_ID");
    		if($user_id<=0)
    			return;
    		$table_user = PHPBB2_TABLE_PREFIX."users";
    		$table_sessions = PHPBB2_TABLE_PREFIX."sessions";
    		$table_config = PHPBB2_TABLE_PREFIX."config";
    
    		$dbConfig = $DB->Query("SELECT * FROM ".
    			$table_config.
    			" WHERE config_name
    				IN ('cookie_name', 'cookie_path', 'cookie_domain', 'cookie_secure')");
    		while($arConfig = $dbConfig->Fetch())
    			${$arConfig['config_name']} = $arConfig['config_value'];
    
    		if (isset($HTTP_COOKIE_VARS[$cookie_name . '_sid']) || 
    			isset($HTTP_COOKIE_VARS[$cookie_name . '_data']))
    			$session_id = isset($HTTP_COOKIE_VARS[$cookie_name . '_sid']) ? 
    				$HTTP_COOKIE_VARS[$cookie_name . '_sid'] : '';
    
    		$ip_sep = explode('.', $_SERVER['REMOTE_ADDR']);
    		$user_ip = sprintf('%02x%02x%02x%02x', $ip_sep[0], $ip_sep[1], $ip_sep[2], $ip_sep[3]);
    		$current_time = time();
    		$sql =
    			"UPDATE ".$table_sessions." SET ".
    			"    session_user_id = ".$user_id.", ".
    			"    session_start = ".$current_time.", ".
    			"    session_time = ".$current_time.", ".
    			"    session_page = 0, ".
    			"    session_logged_in = 1 ".
    			"WHERE session_id = '".$DB->ForSQL($session_id)."' ".
    			"    AND session_ip = '".$user_ip."'";
    
    		$r = $DB->Query($sql);
    		if($r->AffectedRowsCount()<=0)
    		{
    		$session_id = md5(uniqid($user_ip));
    		$sql =
    			"INSERT INTO ".
    			$table_sessions.
    			"(session_id, session_user_id, session_start, session_time, session_ip, session_page, session_logged_in)".
    			"VALUES ('".$session_id."', ".$user_id.", ".$current_time.", ".$current_time.", '".$user_ip."', 0, 1)";
    		$DB->Query($sql);
    		}
    
    		$sql =
    			"UPDATE ".$table_user." SET ".
    			"    user_session_time = ".$current_time.", ".
    			"    user_session_page = 0, ".
    			"    user_lastvisit = ".$current_time." ".
    			"WHERE user_id = ".$user_id;
    
    		$DB->Query($sql);
    
    		$sessiondata = Array('userid' => $user_id);
    
    		setcookie($cookie_name.'_data', 
    			serialize($sessiondata), 
    			$current_time + 31536000, 
    			$cookie_path, 
    			$cookie_domain, $cookie_secure);
    		setcookie($cookie_name.'_sid',
    			$session_id, 0, $cookie_path, 
    			$cookie_domain, $cookie_secure);
    	}
    }
    ?>

    Следующие строки необходимо добавить в /bitrix/php_interface/init.php:

    <?
    AddEventHandler(
    	"main", 
    	"OnUserLoginExternal", 
    	Array("__PHPBB2Auth", "OnUserLoginExternal"),100, $_SERVER['DOCUMENT_ROOT'].'/bitrix/php_interface/scripts/phpbb.php'
    	);
    
    AddEventHandler(
    	"main", 
    	"OnExternalAuthList", 
    	Array("__PHPBB2Auth", "OnExternalAuthList"), 100, $_SERVER['DOCUMENT_ROOT'].'/bitrix/php_interface/scripts/phpbb.php'
    	);
    
    AddEventHandler(
    	"main", 
    	"OnAfterUserAuthorize", 
    	Array("__PHPBB2Auth", "OnAuthorize"),100
    	);
    ?>

    В качестве примера ещё один скрипт внешней авторизации - для форума Invision Power Board:

    <?
    define("IPB_TABLE_PREFIX", "ibf_");
    define("IPB_VERSION", "2");
    
    AddEventHandler("main", "OnUserLoginExternal", Array("__IPBAuth", "OnUserLoginExternal"));
    AddEventHandler("main", "OnExternalAuthList", Array("__IPBAuth", "OnExternalAuthList"));
    
    class __IPBAuth
    {
    	function OnUserLoginExternal(&$arArgs)
    	{
    		extract($arArgs);
    
    		////////// <settings> ////////////
    		$table_user = IPB_TABLE_PREFIX."members";
    		$table_converge = IPB_TABLE_PREFIX."members_converge";
    		$groups_map = Array(
    			/*'IPB Group ID' => 'Local Group ID',*/
    			'4' => '1'
    			);
    		////////// </settings> ////////////
    
    		global $DB, $USER, $APPLICATION;
    
    		if(IPB_VERSION == '1')
    		{
    			$strSql = "SELECT * FROM ".$table_user." WHERE name='".$DB->ForSql($login)."' AND password='".md5($password)."'";
    		}
    		else
    		{
    		$strSql =
    			"SELECT t1.* ".
    			"FROM ".$table_user." t1, ".$table_converge." t2 ".
    			"WHERE t1.name='".$DB->ForSql($login)."' ".
    			"    AND t1.email = t2.converge_email ".
    			"    AND t2.converge_pass_hash = MD5(CONCAT(MD5(t2.converge_pass_salt), '".md5($password)."'))";
    		}
    
    		$dbAuthRes = $DB->Query($strSql);
    		if($arAuthRes = $dbAuthRes->Fetch())
    		{
    		$arFields = Array(
    			"LOGIN" => $login,
    			"NAME" => $arAuthRes['title'],
    			"PASSWORD" => $password,
    			"EMAIL" => $arAuthRes['email'],
    			"ACTIVE" => "Y",
    			"EXTERNAL_AUTH_ID"=>"IPB",
    			"LID" => SITE_ID
    			);
    
    		$oUser = new CUser;
    		$res = CUser::GetList($O, $B, Array("LOGIN_EQUAL_EXACT"=>$login, "EXTERNAL_AUTH_ID"=>"IPB"));
    		if(!($ar_res = $res->Fetch()))
    			$ID = $oUser->Add($arFields);
    		else
    		{
    			$ID = $ar_res["ID"];
    			$oUser->Update($ID, $arFields);
    		}
    
    		if($ID>0)
    		{
    			$USER->SetParam("IPB_USER_ID", $arAuthRes['id']);
    
    			$user_group = $arAuthRes['mgroup'];
    			$arUserGroups = CUser::GetUserGroup($ID);
    			foreach($groups_map as $ext_group_id => $group_id)
    			{
    				if($ext_group_id==$user_group)
    				$arUserGroups[] = $group_id;
    			else
    			{
    				$arUserGroupsTmp = Array();
    				foreach($arUserGroups as $grid)
    					if($grid != $group_id)
    					$arUserGroupsTmp[] = $grid;
    				$arUserGroups = $arUserGroupsTmp;
    			}
    			}
    			CUser::SetUserGroup($ID, $arUserGroups);
    			$arArgs["store_password"] = "N";
    
    			return $ID;
    			}
    		}
    	}
    
    	function OnExternalAuthList()
     	   {
    		return Array(
    		Array("ID"=>"IPB", "NAME"=>"Invision Power Board")
    		);
    	}
    }
    ?>

    Для того чтобы данный скрипт начал работать, его необходимо подключить в /bitrix/php_interface/init.php.

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

    Вопросы безопасности при программировании в Bitrix Framework.


    Санитайзер

    Санитайзер - инструмент, анализирующий введённый пользователем html код. Основная задача санитайзера - предотвратить внедрение/вывод на экран потенциально опасного кода в HTML.

    Санитайзер удобно использовать там, где пользователь вводит произвольный html. Например, в визуальном редакторе или при копировании текста из MS Word. Кроме функций контроля введённого кода санитайзер частично отслеживает валидность вёрстки, в частности закрывает незакрытые теги.

    Как фильтровать текст

    Если необходимо отфильтровать текст (содержащий HTML - тэги) введенный пользователем от нежелательных тэгов HTML с помощью санитайзера, то можно это сделать так:

    $Sanitizer = new CBXSanitizer;
    
    $Sanitizer->AddTags( array (
    	'a' = > array('href','id','style','alt'...),
    	'br' => array(),
    	.... ));
    
    $pureHtml = $Sanitizer->SanitizeHtml($html);

    Санитайзер отфильтрует все тэги и атрибуты, которые не содержатся в "белом" списке, сформированном функцией AddTags().

    В санитайзер включены 3 преднастроенных уровня фильтрации:

    SECURE_LEVEL_HIGH (высокий уровень) включает следующий список:

    $arTags = array(
    	'b'        => array(),
    	'br'        => array(),
    	'big'        => array(),
    	'blockquote'    => array(),
    	'code'        => array(),
    	'del'        => array(),
    	'dt'        => array(),
    	'dd'        => array(),
    	'font'        => array(),
    	'h1'        => array(),
    	'h2'        => array(),
    	'h3'        => array(),
    	'h4'        => array(),
    	'h5'        => array(),
    	'h6'        => array(),
    	'hr'        => array(),
    	'i'        => array(),
    	'ins'        => array(),
    	'li'        => array(),
    	'ol'        => array(),
    	'p'        => array(),
    	'small'        => array(),
    	's'        => array(),
    	'sub'        => array(),
    	'sup'        => array(),
    	'strong'    => array(),
    	'pre'        => array(),
    	'u'        => array(),
    	'ul'        => array()
    );

    SECURE_LEVEL_MIDDLE (средний уровень) включает в себя:

    $arTags = array(
    	'a'        => array('href', 'title','name','alt'),
    	'b'        => array(),
    	'br'        => array(),
    	'big'        => array(),
    	'blockquote'    => array('title'),
    	'code'        => array(),
    	'caption'    => array(),
    	'del'        => array('title'),
    	'dt'        => array(),
    	'dd'        => array(),
    	'font'        => array('color','size'),
    	'color'        => array(),
    	'h1'        => array(),
    	'h2'        => array(),
    	'h3'        => array(),
    	'h4'        => array(),
    	'h5'        => array(),
    	'h6'        => array(),
    	'hr'        => array(),
    	'i'        => array(),
    	'img'        => array('src','alt','height','width','title'),
    	'ins'        => array('title'),
    	'li'        => array(),
    	'ol'        => array(),
    	'p'        => array(),
    	'pre'        => array(),
    	's'        => array(),
    	'small'        => array(),
    	'strong'    => array(),
    	'sub'        => array(),
    	'sup'        => array(),
    	'table'        => array('border','width'),
    	'tbody'        => array('align','valign'),
    	'td'        => array('width','height','align','valign'),
    	'tfoot'        => array('align','valign'),
    	'th'        => array('width','height'),
    	'thead'        => array('align','valign'),
    	'tr'        => array('align','valign'),
    	'u'        => array(),
    	'ul'        => array()
    );

    SECURE_LEVEL_LOW (низкий уровень) включает в себя:

    $arTags = array(
    	'a'        => array('href', 'title','name','style','id','class','shape','coords','alt','target'),
    	'b'        => array('style','id','class'),
    	'br'        => array('style','id','class'),
    	'big'        => array('style','id','class'),
    	'blockquote'    => array('title','style','id','class'),
    	'caption'    => array('style','id','class'),
    	'code'        => array('style','id','class'),
    	'del'        => array('title','style','id','class'),
    	'div'        => array('title','style','id','class','align'),
    	'dt'        => array('style','id','class'),
    	'dd'        => array('style','id','class'),
    	'font'        => array('color','size','face','style','id','class'),
    	'h1'        => array('style','id','class','align'),
    	'h2'        => array('style','id','class','align'),
    	'h3'        => array('style','id','class','align'),
    	'h4'        => array('style','id','class','align'),
    	'h5'        => array('style','id','class','align'),
    	'h6'        => array('style','id','class','align'),
    	'hr'        => array('style','id','class'),
    	'i'        => array('style','id','class'),
    	'img'        => array('src','alt','height','width','title'),
    	'ins'        => array('title','style','id','class'),
    	'li'        => array('style','id','class'),
    	'map'        => array('shape','coords','href','alt','title','style','id','class','name'),
    	'ol'        => array('style','id','class'),
    	'p'        => array('style','id','class','align'),
    	'pre'        => array('style','id','class'),
    	's'        => array('style','id','class'),
    	'small'        => array('style','id','class'),
    	'strong'    => array('style','id','class'),
    	'span'        => array('title','style','id','class','align'),
    	'sub'        => array('style','id','class'),
    	'sup'        => array('style','id','class'),
    	'table'        => array('border','width','style','id','class','cellspacing','cellpadding'),
    	'tbody'        => array('align','valign','style','id','class'),
    	'td'        => array('width','height','style','id','class','align','valign','colspan','rowspan'),
    	'tfoot'        => array('align','valign','style','id','class','align','valign'),
    	'th'        => array('width','height','style','id','class','colspan','rowspan'),
    	'thead'        => array('align','valign','style','id','class'),
    	'tr'        => array('align','valign','style','id','class'),
    	'u'        => array('style','id','class'),
    	'ul'        => array('style','id','class')
    );

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

    $Sanitizer = new CBXSanitizer;
    
    $Sanitizer->SetLevel(CBXSanitizer::SECURE_LEVEL_MIDDLE);
    
    $pureHtml = $Sanitizer->SanitizeHtml($html);

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


    Защита от фреймов

    На странице Защита от фреймов (Настройки > Проактивная защита > Защита от фреймов) можно включить/отключить ограничение работы во фрейме, а также задать исключения на действие защиты.

    Запрет на использование кросс-доменных фреймов ссылающихся на страницы ресурса задается установкой заголовка X-Frame-Options в значение SAMEORIGIN.

    X-Frame-Options

    Данный заголовок указывает браузеру, можно ли загружать страницы сайта через <frame>/<iframe>.

    Значение DENY запретит загрузку через фреймы, значение SAMEORIGIN разрешит загрузку, но только если и фрейм, и страница, его загружающая, находятся на одном домене (Same Origin Policy).

    Основная функция данной защиты – предотвращение кликджекинга; в качестве дополнительного бонуса это позволит предотвратить атаку, описанную Ben Schmidt.


    Вирус на сайте

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

    Основные методы взлома

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

    1. Первая и самая распространенная причина - это виртуальный (разделяемый) хостинг. Архитектурно он не обеспечивает полную изоляцию клиентов, поэтому взлом одного сайта на хостинге часто ведет к взлому соседних. К сожалению, практически невозможно защититься от уязвимости хостинга. Единственное, что можно сделать, это вовремя зафиксировать заражение и обратиться к хостеру.
    2. Устаревшее или уязвимое ПО, которое располагается на том же или соседнем аккаунте хостинга. Для устаревших версий в интернете можно найти давно существующие способы взлома. Чтобы уберечься от заражения, не рекомендуется размещать на одном аккаунте хостинга коммерческий сайт и частную страницу работающую на небезопасном ПО.
    3. Чаще всего злоумышленники похищают сохраненные ftp пароли и сохраненные пароли браузера. Поэтому немаловажное значение имеет выбор паролей достаточной сложности, в том числе включающих и спецсимволы. Сильно повысить защищенность сайта может подключение CAPTCHA и двухэтапной авторизации.
    4. Не столь часто, но все-таки случается ситуация, когда низкоквалифицированные программисты (иногда и фрилансеры) наносят вред сайту. К сожалению, защититься от человеческого фактора практически нельзя.

    Ложные срабатывания антивируса

    Работа веб-антивируса основана на эвристическом анализе потенциально опасных частей кода, поэтому возможно и ложное срабатывание антивируса.

    Главным отличием действительно вирусных блоков кода от безопасных состоит в том, что безопасные были добавлены программистом, который и сможет их опознать. Откуда появились вирусные блоки определить не получится. Эта задача может быть достаточно трудоёмкой, однако это самый простой способ.

    Определение безопасности блоков кода с помощью персональных антивирусов может быть достаточно сложной задачей, а может вовсе не дать результата. Такие блоки html-кода не содержат ни троянов, ни вирусов, а содержат лишь ссылки на них, которые могут не вызвать срабатывания антивируса.

    Итак, если выяснено, что блок кода, на который срабатывает веб-антивирус 1С-Битрикс не является вредоносным и не содержит ссылки на загрузку вирусов, то нужно добавить этот блок в исключение. Для этого необходимо взять некоторую строку из этого блока (достаточно длинную и уникальную) и добавить ее в исключение веб-антивируса. В результате, антивирус перестанет срабатывать на любые, обрабатываемые им блоки, содержащие указанную строку.

    Если обнаружен вирус

    Если все-таки выяснено, что блок содержит ссылку на вирус, то меры необходимо принимать незамедлительно. Наличие стороннего кода на сервере говорит о том, что злоумышленники могли получить доступ к файлам на сервере. В подавляющем большинстве случаев наличие вирусных блоков означает, что один из компьютеров, имеющих доступ на сервер через FTP (SSH, SFTP и т.п.), заражен вирусом и с него был похищен пароль от сервера. Поэтому при обнаружении вируса на сайте необходимо выполнить следующие шаги:

    1. Проверить все компьютеры людей, имеющим доступ к сайту (в том числе к панели администрирования 1С-Битрикс), персональным антивирусным программным обеспечением.
    2. После того, как все компьютеры были вылечены, необходимо сменить все пароли на сервере (пароли на FTP, SSH, пароли к базе данных, пароли пользователей, имеющих доступ к административному разделу сайта).
    3. Затем следует вычистить весь сторонний код на сервере. Проще всего для отслеживания изменившихся файлов использовать контроль целостности файлов 1С-Битрикс. Но это только в том случае, если вы заранее озаботились безопасностью и периодически запускаете контроль целостности файлов.
    4. Если контроль целостности не используется, то поиск изменений, оставленных хакером может быть весьма сложной задачей. Для поиска таких изменений, можно провести поиск по всем файлам на сервере, содержащим строки из блока, на который сработал веб-антивирус, поиск и ручная проверка всех недавно изменившихся файлов, проанализировать логи http сервера.

    Чистка сайта от вирусов

    Важно! Перед выполнением скрипта необходимо сделать полную резервную копию.

    Даже если почистить компьютер от вирусов персональным антивирусом, то сайт все равно останется в нерабочем состоянии. Удалить вирусный код вручную очень сложно, к тому же искать зараженные файлы на дальнем хостинге не всегда возможно. Скачивать сайт на локальный компьютер тоже неразумно. Поэтому мы будем пользоваться php-скриптом.

    По сути создаваемый скрипт будет обычным поиском текста в файле, только с добавлением к нему рекурсивного сканирования директорий. Скрипт показывает форму для ввода кода вируса (или иного текста, который надо найти), затем начинает сканировать все файлы с расширением .php в текущей папке и подпапках.

    После выполнения скрипта список найденных файлов сохраняется в файл filelist.txt, затем он выводится на экран.

    Автоматическая замена

    Внимание! Использование автозамены может нарушить работоспособность сайта, поэтому, если решено ее использовать, необходимо учитывать возможные последствия.

    Как можно заметить, после выполнения скрипта создается файл со списком зараженных файлов. Почему бы в таком случае просто не удалить все зараженные элементы? Автоматически удалять вирус тоже очень опасно, потому что есть серьёзная опасность потерять данные из своих скриптов.

    В нашем примере мы будем использовать автозамену. Для ее выполнения добавим кнопку, по нажатию на которую введённый текст будет автоматически удаляться начиная с папки, где лежит сам скрипт, и ниже. Исходные файлы будут сохраняться с расширением .orig.

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

    Скачать полученный скрипт можно здесь.

    Примечание: скрипт написан для php5, на php4 потребуется заменять функцию file_get_contents.


    Защита от троянов

    Практически невозможно найти способ защититься на 100% от троянов. Однако можно воспользоваться бесплатным решением от 1С-Битрикс Поиск троянов, которое может обнаруживать наиболее характерные из них. Решение оформлено в виде модуля и бесплатно доступно в marketplace.

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

    Обнаруженные трояны нужно поместить в карантин, просканировать компьютер на вирусы персональным антивирусным ПО, после чего нужно сменить все пароли и обратиться к хостеру с требованием проанализировать причину взлома.

    Важно! Перед началом сканирования обязательно необходимо проверить целостность ядра используя функционал Монитора качества.


    Модули

    Bitrix Framework имеет модульную структуру. Каждый модуль отвечает за управление определенными элементами и параметрами сайта: информационным наполнением и структурой сайта, форумами, рекламой, рассылкой, распределением прав между группами пользователей, сбором статистики посещений, оценкой эффективности рекламных кампаний и т.д.

    Модуль – это объёмный блок кода, отвечающий за определенную функциональность продукта. Модуль может содержать API всех уровней (включая API доступа к данным, бизнес-логику и API пользовательского интерфейса), HTML верстку, пользовательские интерфейсы, компоненты, роботы, административные страницы и тому подобное.

    Модули системы, главным образом, работают независимо друг от друга. Однако в целом ряде случаев функционал одних модулей основан на возможностях других. Например:

    • Модуль Торговый каталог расширяет возможности модуля Информационные блоки и позволяет выполнять настройку цен товара в зависимости от различных условий, применять к товарам наценку и скидки и т.п.
    • Модуль Документооборот позволяет организовать последовательную коллективную работу с содержимым модулей Информационные блоки и Управление структурой.

    После установки системы список используемых модулей можно просмотреть на странице Управление модулями (Настройки > Настройки продукта > Модули) в административном разделе системы:

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

    Управление уровнем прав пользователей на доступ к модулям системы осуществляется отдельно для каждого модуля на странице его настроек. На этой же странице выполняется управление общими параметрами его работы.

    Страница настроек конкретного модуля может иметь различное число вкладок и полей, в зависимости от функционала. Перейти к ней можно следующими способами:

    • с помощью административного меню: Настройки > Настройки продукта > Настройки модулей > имя_модуля;
    • с помощью кнопки , расположенной на административной панели. Эта кнопка позволяет перейти к настройкам модуля, страницы (формы) которого открыты в текущий момент в основной рабочей области.

    Примечание: Перед использованием модуля необходимо проверить установлен ли он и подключить его при помощи конструкции:
    <?
    use Bitrix\Main\Loader;
    
    if (Loader::includeModule('******')) {
    	// здесь можно использовать функции и классы модуля
    }?>
    где **** - идентификатор модуля.

    Модули и компоненты

    Модули в Bitrix Framework представляют собой модели и контроллеры нижнего уровня (в понятиях MVC), а компоненты - контроллеры верхнего уровня, включающие представления, которые основаны на иерархии файловой структуры сайта. Весь функционал любого сайта, как правило, реализуется на стандартных модулях, но приходится кастомизировать компоненты (или написать свои) для формирования и вывода страниц и подключать их на соответствующих страницах сайта.

    Разработка модулей

    Bitrix Framework допускает разработку пользовательских модулей.


    Модули в D7

    Модули системы (как штатные, так и загруженные из Marketplace) располагаются в системной папке bitrix/modules. В папке /local/modules могут располагаться [dw]пользовательские модули[/dw][di]Партнерские модули отличаются от стандартных модулей следующим...
    Подробнее...[/di]. Такое разделение позволит сторонним разработчикам просто организовать контроль версий своих разработок с сохранением обновляемости продукта стандартной системой обновлений.

    Структура файлов и папок модулей на D7 осталась такой же, как и в старой версии ядра.

    API (классы, логика) модуля располагается в подпапке [dw]/lib[/dw][di]Данная папка не является обязательной, если у вашего класса нет собственных методов.[/di] папки модуля. Например, для Главного модуля путь будет таким: bitrix/modules/main/lib. Из этой же папки API будет автоматически подключаться при соблюдении некоторых правил:

    • Файлы классов должны именоваться в нижнем регистре, при этом название класса равно название файла. Пример: /lib/myclass.php.
    • Файлы должны содержать правильный namespace. Пример: если модуль подключается как:
      Loader::includeModule('company.shotname');
      то в классе должен быть прописан namespace: namespace Company\Shotname.
    • Там где классы модуля используются в административной части - модуль должен быть подключен.

    Структура файлов

    Файлы модуля располагаются в папке /bitrix/modules/ID модуля/. Структура папки:

    • admin/ – каталог с административными скриптами;
      • menu.php – файл с административным меню;
    • classes/ – скрипты с классами модуля;
      • general/ – классы, не зависящие от используемой базы данных;
      • mysql/ – классы, предназначенные для работы только с MySQL;
      • mssql/ – классы, предназначенные для работы только с MS SQL;
      • oracle/ – классы, предназначенные для работы только с Oracle;
    • lang/ID языка/ – каталог с языковыми файлами;
    • lib/ – каталог с файлами (API: классы, логика) нового ядра D7 (может не присутствовать, если у модуля нет собственных методов);
    • install/ – каталог с файлами используемыми для инсталляции и деинсталляции модуля;
      • admin/ – каталог со скриптами подключающими административные скрипты (вызывающие скрипты);
      • js/ – каталог с js-скриптами модуля. Копируются в /bitrix/js/ID_модуля/;
      • db/ – каталог с SQL скриптами для инсталляции/деинсталляции базы данных;
        • mysql/ – SQL скрипты для инсталляции/деинсталляции таблиц в MySQL;
        • mssql/ – SQL скрипты для инсталляции/деинсталляции таблиц в MS SQL;
        • oracle/ – SQL скрипты для инсталляции/деинсталляции таблиц в Oracle;
      • images/ – каталог с изображениями; после инсталляции модуля они должны быть скопированы в каталог /bitrix/images/ID модуля/;
      • templates/ – каталог с компонентами 1.0. (Сохраняется только с целью совместимости версий.);
        • ID модуля/ – каталог с основными файлами компонентов;
        • lang/ID языка/ID модуля/ – каталог с языковыми файлами компонентов;
      • components/пространство имен/имя компонента/ – каталог с компонентами 2.0;
      • themes/имя_модуля/css и картинки для стилей административной панели, если модуль в таковых нуждается (Устаревшая, до версии 12.0);
      • panel/имя_модуля/css и картинки для стилей административной панели, если модуль в таковых нуждается.
      • index.php – файл с описанием модуля;
      • version.php – файл с номером версии. Версия не может быть равной нулю.
    • include.php – файл подключается в тот момент, когда речь идет о подключении модуля в коде, в нем должны находиться включения всех файлов с библиотеками функций и классов;
    • default_option.php – содержит массив с именем $ID модуля_default_option, в котором заданы значения по умолчанию для параметров модуля;

      Примечание: В случае партнерских модулей, в названии которых содержится точка (пример – mycompany.forum) в имени переменной точка будет автоматически заменена на символ подчеркивания.

    • options.php – файл подключается на странице настройки параметров модулей в административном меню Настройки;
    • prolog_admin.php – файл должен подключаться во всех административных скриптах модуля. Обычно в нем определяется константа ADMIN_MODULE_NAME (идентификатор модуля), используемая в панели управления;
    • .settings.php – файл настроек, описывающий настройки модуля, которые можно прочитать через \Bitrix\Main\Config\Configuration::getInstance($module).

    Описание и параметры

    Описание

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

    Основным файлом, используемым системой для манипуляции модулем, является /bitrix/modules/ID модуля/install/index.php. (ID модуля в этом случае - это полный код партнерского модуля, который задается в формате: код_партнера.код_модуля.) Основное назначение этого файла - это размещение в нем класса с именем, совпадающим с ID модуля. (ID модуля здесь используется в формате код_партнера_код_модуля, так как в имени класса точка недопустима.)

    Пример:

    01 <?
    02 	Class mymodule extends CModule
    03 	{
    04 		var $MODULE_ID = "mymodule";
    05 		var $MODULE_NAME;
    06	 
    07 		function DoInstall()
    08 		{
    09 		global $DB, $APPLICATION, $step;
    10 		$APPLICATION->IncludeAdminFile(GetMessage("FORM_INSTALL_TITLE"), $_SERVER["DOCUMENT_ROOT"]."/bitrix/modules/mymodule/install/step1.php");
    11 		}
    12	 
    13 		function DoUninstall()
    14 		{
    15 		global $DB, $APPLICATION, $step;
    16 		$APPLICATION->IncludeAdminFile(GetMessage("FORM_INSTALL_TITLE"), $_SERVER["DOCUMENT_ROOT"]."/bitrix/modules/mymodule/install/unstep1.php");
    17	 
    18 		}
    19 	}
    20 ?>

    Обязательные методы этого класса:

    • DoInstall - запускается при нажатии кнопки Установить на странице Модули административного раздела, осуществляет инсталляцию модуля.
    • DoUninstall - запускается при нажатии кнопки Удалить на странице Модули административного раздела, осуществляет деинсталляцию модуля.

    Необязательный метод этого класса:

    • GetModuleRightList - возвращает список уникальных прав (или ролей) модуля.

    Обязательные свойства объекта этого класса:

    • MODULE_ID - хранит ID модуля (полный код партнерского модуля);
    • MODULE_VERSION - текущая версия модуля в формате XX.XX.XX;
    • MODULE_VERSION_DATE - строка содержащая дату версии модуля; дата должна быть задана в формате YYYY-MM-DD HH:MI:SS;
    • MODULE_NAME - имя модуля;
    • MODULE_DESCRIPTION - описание модуля;
    • MODULE_GROUP_RIGHTS - если задан метод GetModuleRightList, то данное свойство должно содержать Y.

    Примеры

    Пример файла с описанием модуля Веб-формы:

    <?
    global $MESS;
    $PathInstall = str_replace("\\", "/", __FILE__);
    $PathInstall = substr($PathInstall, 0, strlen($PathInstall)-strlen("/index.php"));
    IncludeModuleLangFile($PathInstall."/install.php");
    include($PathInstall."/version.php");
    if(class_exists("form")) return;
    Class form extends CModule
    {
    	var $MODULE_ID = "form";
    	var $MODULE_VERSION;
    	var $MODULE_VERSION_DATE;
    	var $MODULE_NAME;
    	var $MODULE_DESCRIPTION;
    	var $MODULE_GROUP_RIGHTS = "Y";
    
    	function __construct()
    	{
    		$this->MODULE_VERSION = FORM_VERSION;
    		$this->MODULE_VERSION_DATE = FORM_VERSION_DATE;
    		$this->MODULE_NAME = GetMessage("FORM_MODULE_NAME");
    		$this->MODULE_DESCRIPTION = GetMessage("FORM_MODULE_DESCRIPTION");
    	}
    
    	function DoInstall()
    	{
    		global  $APPLICATION;
    		$FORM_RIGHT = $APPLICATION->GetGroupRight("form");
    		if ($FORM_RIGHT=="W")
    		{
    		$step = IntVal($step);
    		if($step<2)
    			$APPLICATION->IncludeAdminFile(GetMessage("FORM_INSTALL_TITLE"),
    			$_SERVER["DOCUMENT_ROOT"]."/bitrix/modules/form/install/step1.php");
    		elseif($step==2)
    			$APPLICATION->IncludeAdminFile(GetMessage("FORM_INSTALL_TITLE"),
    			$_SERVER["DOCUMENT_ROOT"]."/bitrix/modules/form/install/step2.php");
    		}
    	}
    
    	function DoUninstall()
    	{
    		global $APPLICATION;
    		$FORM_RIGHT = $APPLICATION->GetGroupRight("form");
    		if ($FORM_RIGHT=="W")
    		{
    			$step = IntVal($step);
    			if($step<2)
    			$APPLICATION->IncludeAdminFile(GetMessage("FORM_UNINSTALL_TITLE"),
    			$_SERVER["DOCUMENT_ROOT"]."/bitrix/modules/form/install/unstep1.php");
    			elseif($step==2)
    			$APPLICATION->IncludeAdminFile(GetMessage("FORM_UNINSTALL_TITLE"),
    			$_SERVER["DOCUMENT_ROOT"]."/bitrix/modules/form/install/unstep2.php");
    		}
    	}
    
    	function GetModuleRightList()
    	{
    		global $MESS;
    		$arr = array(
    			"reference_id" => array("D","R","W"),
    			"reference" => array(
    				GetMessage("FORM_DENIED"),
    				GetMessage("FORM_OPENED"),
    				GetMessage("FORM_FULL"))
    			);
    		return $arr;
    	}
    }
    ?>

    Пример файла с указанием версии модуля

    <?
    $arModuleVersion = array(
    	"VERSION" => "11.0.4",
    	"VERSION_DATE" => "2011-11-17 14:00:00"
    );
    ?>

    Параметры

    Параметры модуля доступны для изменения в административном интерфейсе на странице Настройки модулей (Настройки > Настройки продукта > Настройки модулей). При выборе модуля на данной странице, система подключает файл /bitrix/modules/my_module_id/options.php, предназначенный для управления параметрами модуля, назначения прав на модуль и т.п.

    Параметры модуля хранятся в базе данных.

    При получении параметров модуля, может использоваться значение по умолчанию, задаваемое в файле /bitrix/modules/my_module_id/default_option.php. В данном файле определяется массив my_module_id_default_option, хранящий значения по умолчанию.

    Пример файла /bitrix/modules/my_module_id/default_option.php:

    <?
    /*
    * Файл local/modules/my_module_id/default_option.php
    */
    $my_module_id_default_option = array(
    	"MY_PARAMETER_ID"  =>  "DEFAULT_VALUE"
    	);
    ?>

    Для работы с параметрами модуля предназначен класс COption. Методы класса:

    • SetOptionString - установка строковых параметров
    • SetOptionInt - установка числовых параметров
    • GetOptionString - получение строковых параметров
    • GetOptionInt - получение числовых параметров
    • RemoveOption - удаление параметра

    Примеры использования

    Строковый параметр

    <?
    // установим строковый параметр
    COption::SetOptionString("my_module_id", "MY_PARAMETER_ID", "VALUE");
    
    // получим строковый параметр
    $value = COption::GetOptionString("my_module_id", "MY_PARAMETER_ID", "DEFAULT_VALUE");
    ?>

    Примечание: При использовании файла default_option.php, заданные в нем значения параметров по умолчанию, не нужно передавать третьим аргументом def при вызове метода GetOptionString.
    Названия параметров, должны быть идентичными в файле default_option.php и в методах класса COption.

    Параметр типа файл. (Загрузка файла в модуле)

    <table>
    <tr> <td width="40%">
    	<?
    	$path_file = COption::GetOptionString("my_module_id", 'CLEVERSCRIPT_IMG_DESCTOP_BTN');
    	CAdminFileDialog::ShowScript
    	(
    		Array(
    			"event" => "BtnClick_0",
    			"arResultDest" => array("FORM_NAME" => "cleverscriptwantcheaper", "FORM_ELEMENT_NAME" => "CLEVERSCRIPT_IMG_DESCTOP_BTN"),
    			"arPath" => array("PATH" => GetDirPath($path_file)),
    			"select" => 'F',// F - file only, D - folder only
    			"operation" => 'S',// O - open, S - save
    			"showUploadTab" => true,
    			"showAddToMenuTab" => false,
    			"fileFilter" => 'jpg,jpeg,gif,png',
    			"allowAllFiles" => true,
    			"SaveConfig" => true,
    		)
    	);
    	?>
    	<?echo Loc::getMessage("T_CLEVERSCRIPT_IMG_DESCTOP_BTN")?>
    </td>
    	<td width="60%">
    		<input type="text" name="CLEVERSCRIPT_IMG_DESCTOP_BTN" size="50" maxlength="255" value="<?echo $path_file?>"> <input type="button" name="browse_0" value="..." onClick="BtnClick_0()">
       </td>
    </tr>
    </table>
    
    

    Дополнительно

    • Option - класс для работы с параметрами модулей, хранимых в базе данных. Аналог класса COption в старом ядре.

    Административные скрипты

    Административные скрипты - это скрипты используемые модулем в административной части системы. Они должны располагаться в каталоге /bitrix/modules/ID модуля/admin/.

    Необходимо учитывать, что напрямую в браузере административные скрипты нельзя вызывать (как в общем-то и любые скрипты каталога /bitrix/modules/). Поэтому для их вызова используются дополнительные одноименные вызывающие скрипты, находящиеся в каталоге /bitrix/admin/. Они, как правило, состоят только из подключения одноименного административного скрипта:

    <?
    require_once($_SERVER["DOCUMENT_ROOT"]."/bitrix/modules/ID модуля/admin/имя скрипта");
    ?>

    При инсталляции модуля, вызывающие скрипты должны быть скопированы из каталога /bitrix/modules/ID модуля/install/admin/ в каталог /bitrix/admin/. В момент деинсталляции модуля вызывающие скрипты должны быть удалены из этого каталога.

    Примечание: Необходимо учитывать, что вызывающие скрипты всех инсталлированных модулей находятся в одном каталоге /bitrix/admin/, поэтому во избежание дублирования, желательно давать им имена начинающиеся с какого-либо префикса характерного только для соответствующего модуля.

    В каждом административном скрипте до подключения визуальной части административного пролога необходимо определять константу ADMIN_MODULE_NAME, необходимую для формирования иконки над заголовком страницы. Константа нужна чтобы при нажатии кнопки Настройки сразу перейти к настройкам модуля. Она обычно задается в prolog.php.

    Пример определения константы:

    define("ADMIN_MODULE_NAME", "statistic");
    

    Языковые файлы для административных скриптов модуля

    Языковые файлы должны располагаться в каталоге /bitrix/modules/ID модуля/lang/ID языка/. Особенностью их расположения внутри этого каталога является то, что они должны располагаться строго по тому же пути относительно каталога /bitrix/modules/ID модуля/, что одноименные файлы в которых они подключаются. В этом случае подключение языковых файлов может осуществляться только функцией IncludeModuleLangFile.

    К примеру для скрипта /bitrix/modules/ID модуля/admin/body/my_script.php языковой файл должен быть расположен: /bitrix/modules/ID модуля/lang/ID языка/admin/body/my_script.php.

    И подключаться кодом:
    IncludeModuleLangFile(__FILE__);

    Административное меню

    Меню административной части выводится стандартной функцией CMain::GetMenuHtmlEx.

    Меню формируется на основе перебора всех файлов /bitrix/modules/ID модуля/admin/menu.php. В каждом таком файле содержится определение массива $aModuleMenuLinks, содержащего пункты меню соответствующего модуля. Все эти массивы затем будут объединены в стандартный массив $arMenuSections, содержащий информацию о всех пунктах меню.

    Образец структуры меню на примере \bitrix\modules\main\admin\menu.php

    $aMenu[] = array(
    	"parent_menu" => "global_menu_settings",
    	"sort" => 1800,
    	"text" => GetMessage("MAIN_MENU_TOOLS"),
    	"title" => GetMessage("MAIN_MENU_TOOLS_TITLE"),
    	"url" => "tools_index.php?lang=".LANGUAGE_ID,
    	"icon" => "util_menu_icon",
    	"page_icon" => "util_page_icon",
    	"items_id" => "menu_util",
    	"items" => array(
    		array(
    			"text" => GetMessage("MAIN_MENU_SITE_CHECKER"),
    			"url" => "site_checker.php?lang=".LANGUAGE_ID,
    			"more_url" => array(),
    			"title" => GetMessage("MAIN_MENU_SITE_CHECKER_ALT"),
    		),
    	array(
    		"text" => GetMessage("MAIN_MENU_FILE_CHECKER"),
    		"url" => "file_checker.php?lang=".LANGUAGE_ID,
    		"more_url" => array(),
    		"title" => GetMessage("MAIN_MENU_FILE_CHECKER_ALT"),
    		),
    	array(
    		"text" => GetMessage("MAIN_MENU_PHPINFO"),
    		"url" => "phpinfo.php?test_var1=AAA&test_var2=BBB",
    		"more_url" => array("phpinfo.php"),
    		"title" => GetMessage("MAIN_MENU_PHPINFO_ALT"),
    		),
    	array(
    		"text" => GetMessage("MAIN_MENU_SQL"),
    		"url" => "sql.php?lang=".LANGUAGE_ID."&del_query=Y",
    		"more_url" => array("sql.php"),
    		"title" => GetMessage("MAIN_MENU_SQL_ALT"),
    		),
    	array(
    		"text" => GetMessage("MAIN_MENU_PHP"),
    		"url" => "php_command_line.php?lang=".LANGUAGE_ID."",
    		"more_url" => array("php_command_line.php"),
    		"title" => GetMessage("MAIN_MENU_PHP_ALT"),
    		),
    	array(
    		"text" => GetMessage("MAIN_MENU_AGENT"),
    		"url" => "agent_list.php?lang=".LANGUAGE_ID,
    		"more_url" => array("agent_list.php", "agent_edit.php"),
    		"title" => GetMessage("MAIN_MENU_AGENT_ALT"),
    		),
    	array(
    		"text" => GetMessage("MAIN_MENU_DUMP"),
    		"url" => "dump.php?lang=".LANGUAGE_ID,
    		"more_url" => array("dump.php", "restore_export.php"),
    		"title" => GetMessage("MAIN_MENU_DUMP_ALT"),
    		),
    (strtoupper($DBType) == "MYSQL"?
    	Array(
    		"text" => GetMessage("MAIN_MENU_REPAIR_DB"),
    		"url" => "repair_db.php?lang=".LANGUAGE_ID,
    		"more_url" => array(),
    		"title" => GetMessage("MAIN_MENU_REPAIR_DB_ALT"),
    		)
    :null
    ),
    ($USER->CanDoOperation('view_event_log')?
    	Array(
    		"text" => GetMessage("MAIN_MENU_EVENT_LOG"),
    		"url" => "event_log.php?lang=".LANGUAGE_ID,
    		"more_url" => array(),
    		"title" => GetMessage("MAIN_MENU_EVENT_LOG_ALT"),
    		)
    :null
    		),
    	),
    );

    Пункты административного меню можно добавлять с помощью события OnBuildGlobalMenu.

    Если вы пишите свой модуль можете использовать /bitrix/modules/ID_модуля/admin/menu.php для добавления пунктов административного меню.

    Старый способ формирования меню

    Взаимодействие модулей

    Модули могут взаимодействовать между собой двумя способами: явно (прямым вызовом) и скрыто (через систему событий).

    Явное взаимодействие

    Явное взаимодействие с помощью API. Подразумевает подключение модуля с помощью метода CModule::IncludeModule(указав в качестве параметра id модуля) с последующим непосредственным вызовом метода класса или функции модуля.

    Пример явного взаимодействия:

    <?
    // подключаем модуль mymodule
    if (CModule::IncludeModule("mymodule"))
    {
    	// выполним его метод
    	CMyModuleClass::DoIt();
    }
    ?>

    Взаимодействие через события

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

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

    Схема работы с событиями. Модуль, инициирующий событие, в том месте кода, где это событие происходит, должен выполнить следующее:

    • Собрать все зарегистрированные обработчики с помощью функции GetModuleEvents.
    • Выполнить их по одному с помощью функции ExecuteModuleEvent, обрабатывая соответствующим образом возвращаемые обработчиками значения.
    В свою очередь, модуль, который хочет выполнить какие-либо действия на это событие, должен:
    • Зарегистрировать в момент инсталляции свой обработчик с помощью функции RegisterModuleDependences.
    • Соответственно необходимо иметь эту функцию-обработчик и убедиться, что скрипт, в котором эта функция находится, подключается в файле /bitrix/modules/ID модуля/include.php.

    Пример взаимодействия

    Ярким примером взаимодействия такого типа является взаимодействие модулей системы с модулем Поиска. Этот модуль не имеет никакой информации о данных других модулей, особенностях их хранения и обработки. Он только предоставляет интерфейс для индексации данных. Любой модуль системы, который должен индексироваться, при инсталляции регистрирует обработчик на событие OnReindex. Каждый такой обработчик в свою очередь возвращает данные для индексации, которые модуль Поиска использует для наполнения своей базы.

    Коды примеров взаимодействия через события:

    <?
    // регистрация обработчика:
    // когда в модуле init_module возникнет событие OnSomeEvent
    // будет вызван метод CMyModuleClass::Handler модуля handler_module
    
    RegisterModuleDependences(
    	"init_module", "OnSomeEvent",
    	"handler_module", "CMyModuleClass", "Handler"
    	);
    ?>
    <?
    // произвольная функция модуля init_module
    // в которой генерируется событие
    
    function MyFunction()
    {
    	// здесь располагается произвольный код 
    	// представляющий из себя событие
    
    	// далее следует сбор зарегистрированных обработчиков
    	$rsHandlers = GetModuleEvents("init_module", "OnSomeEvent");
    	while($arHandler = $rsHandlers->Fetch())
    		{
    		// и их выполнение по одному
    		if(!ExecuteModuleEvent($arHandler, $param1, $param2))
    			{
    			// если обработчик вернет false, 
    			// то например, вернем надпись "I can't do it..."
    			return "I can't do it...";
    			}
    		}
    	return "I have done it!";
    }
    ?>
    <?
    // обработчик
    
    class CMyModuleClass
    {
    	function Handler($param1, $param2)
    	{
    		if($param1=="delete all")
    			return false;
    		return true;
    	}
    }
    ?>

    Установка и удаление

    Установка модуля

    Инсталляция осуществляется в административном интерфейсе на странице Настройки > Настройки продукта > Модули нажатием кнопки Установить.

    При этом будет вызван метод DoInstall класса с именем, представляющим собой ID модуля, где точка заменяется на нижнее подчеркивание. Этот класс должен быть описан в файле /bitrix/modules/ID модуля/install/index.php.

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

    • Регистрация, которая осуществляется с помощью функции RegisterModule.
    • Если модуль обладает административными скриптами, то для их вызова в каталог /bitrix/admin/ должны быть скопированы вызывающие скрипты.
    • Все изображения, используемые модулем, должны быть скопированы в каталог /bitrix/images/ID модуля/.

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

    Удаление модуля

    Деинсталляция осуществляется нажатием кнопки Удалить. При этом будет вызван метод DoUninstall класса с именем совпадающим с ID модуля. Этот класс должен быть описан в файле /bitrix/modules/ID модуля/install/index.php.

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

    • Удаление регистрационной записи, которая осуществляется с помощью функции UnRegisterModule
    • Если модуль обладает административными скриптами, то вызывающие их скрипты должны быть удалены из каталога /bitrix/admin/.
    • Все изображения, используемые модулем, должны быть удалены из каталога /bitrix/images/ID модуля/.

    Примечание: Модули Главный и Управление структурой удалить нельзя.


    Кастомизация и создание модулей

    Цитатник веб-разработчиков.

    Войтенко Андрей: Скажу по своему опыту (3 года): проблем, которые нельзя было решить без правки ядра, не встречал. Понятно, что не считая багов самого Битрикса. Они решались по принципу: ТП и ручная правка.

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

    Внимание! Кастомизация модуля - это модификация ядра системы со всеми вытекающими отсюда последствиями: рисками получить неработоспособную систему после обновления, потерей права на ТП.

    В силу этого кастомизация модуля - не рекомендуемая, чуть ли не запретная операция. Тем не менее, технически такая возможность имеется и в главе будет приведён пример не сложной кастомизации, если возникла такая потребность.

    Создание собственных модулей описано в курсе Маркетплейс Bitrix Framework

    Список ссылок по теме:

    Пример изменения работы модуля

    Внимание! Перед тем как начать модифицировать работу модуля (то есть модифицировать ядро системы) необходимо быть уверенным, что иными средствами стоящую перед вами задачу не решить. При первом же обновлении продукта все добавленные вами изменения затрутся, и вам придется заново вносить их.

    Пример модификации модуля для версии 11.5. В более поздних версиях этот функционал работает по другому.

    Решим задачу выгрузки контактов из корпоративного портала в Outlook 2003. Напомним, что в стандартной поставке «1С-Битрикс: Корпоративный портал» рассчитан на взаимодействие с Outlook 2007.

    За формирование и подготовку данных для Outlook отвечает файл /bitrix/modules/intranet/classes/general/ws_contacts.php.

    Есть две стандартные проблемы:

    • В Outlook 2003 не выгружаются аватарки - вследствие чего сразу же возникает ошибка.
    • Не для всех пользователей выгружается компания, к которой они принадлежат (поле в Outlook - Организация).

    Решаем первую проблему с помощью функции __getRow($arRes, $listName, &$last_change). Комментируем строки установки атрибута картинки:

    /*if ($this->bGetImages && $arRes['PERSONAL_PHOTO'] > 0)
    	{
    		$arImage = CIntranetUtils::InitImage($arRes['PERSONAL_PHOTO'], 100, 100);
    
    		$obRow->setAttribute('ows_Attachments', ';#'.($APPLICATION->IsHTTPS() ? 'https://' : 'http://')
    			.$_SERVER['HTTP_HOST'].$arImage['CACHE']['src'].';#'.CIntranetUtils::makeGUID(md5($arRes['PERSONAL_PHOTO'])).',1;#');
    		$obRow->setAttribute('ows_MetaInfo_AttachProps', '<File Photo="-1">'.$arImage['FILE']['FILE_NAME'].'</File>');
    	}
    	else
    	{*/
    		$obRow->setAttribute('ows_Attachments', 0);
    	//}

    Решаем вторую проблему с помощью функции GetListItemChangesSinceToken($listName, $viewFields = '', $query = '', $rowLimit = 0, $changeToken = ''). В цикле while ($arUser = $obUsers->NavNext()) комментируем все строки, начинающиеся с $arUser['WORK_COMPANY'] (т.е. где просто меняется значение этого атрибута).

    Примечание: В функции GetList происходит формирование схемы аттрибутов. Если перед строчкой: return array('GetListResult' => $data); сделать вывод массива $data, то можно ознакомиться со схемой выгрузки.

    BigData (сервис персонализации)

    В данной главе рассматривается механизм сбора данных для сервиса 1С-Битрикс BigData, поясняется логика его работы и приводится описание API запросов.

    Примечание: информация по подключению сервиса на сайте представлена в главе Персонализация учебного курса Администратор. Бизнес.

    Сбор данных

    Сервис персонализации использует 2 механизма сбора данных:

    1. Клиентский - у посетителей сайта при просмотре товаров собирается информация из браузеров и пересылается в BigData кластер. При использовании стандартного компонента catalog.element генерится следующий java-скрипт:
      <script type="text/javascript">var _ba = _ba || []; _ba.push(["aid", "5b2ab8a860eebf174046f4d0b2ce52a6"]); 
      _ba.push(["host", "mysite.ru"]); _ba.push(["ad[ct][value]", 
      "eyJ1c2VyX2lkIjoiMSIsInByb2R1Y3RfaWQiOiI4IiwiaWJsb2NrX2lkIjoyLCJwcm9kdWN0X3RpdGxlIjoiXHUwNDFmXHUwNDNiXHUwNDM
      wXHUwNDQyXHUwNDRjXHUwNDM1IFx1MDQxMlx1MDQzNVx1MDQ0MVx1MDQzNVx1MDQzZFx1MDQzZFx1MDQ0Zlx1MDQ0ZiBcdTA0MWJcdTA0MzV
      cdTA0MzNcdTA0M2FcdTA0M2VcdTA0NDFcdTA0NDJcdTA0NGMiLCJjYXRlZ29yeV9pZCI6IjciLCJjYXRlZ29yeSI6eyI3IjoiXHUwNDFmXHU
      wNDNiXHUwNDMwXHUwNDQyXHUwNDRjXHUwNDRmIn0sInJlY29tbWVuZGF0aW9uIjowLCJwcmljZSI6IiIsImN1cnJlbmN5IjoiIn0="]);
      _ba.push(["ad[ct][v]", "1"]);(function() {var ba = document.createElement("script"); 
      ba.type = "text/javascript"; ba.async = true;ba.src = (document.location.protocol == 
      "https:" ? "https://" : "http://") + "bitrix.info/ba.js";var s = document.getElementsByTagName("script")[0];
      s.parentNode.insertBefore(ba, s);})();</script>
      
      Если же вы используете свой компонент для карточки товара, но хотите стать участником сервиса, то вам необходимо разместить вызов метода sendData и данные будут уходить в кластер сервиса:
      $productData = array(
      	'product_id' => ... // идентификатор продукта (не товарного предложения, а именно продукта)
      	'product_title' => ... // название продукта
      	'iblock_id' => ... // идентификатор инфоблока продукта
      	'category_id' => ... // идентификатор категории продукта
      	'category' => ... // перечислены все названия ветки категорий продукта
      	'price' => ... // цена
      	'currency' => ... // валюта
      ));
      
      $counterData = array(
      	'item' => base64_encode(json_encode($productData)),
      	'user_id' => ... // идентификатор пользователя на сайте
      	'recommendation' => ... // уникальный идентификатор выданной рекомендации, если просмотр по рекомендации. Для собственного компонента идентификатор получается в момент получения рекомендации из облака.
      	'v' => '2'
      );
      
      \Bitrix\Main\Analytics\Counter::sendData('ct', $counterData);
    2. Cерверный - с помощью внутренних запросов сервера фиксируются следующие операции (события): добавление товара в корзину, оформление заказа, его оплата. Данные сохраняются в таблицу b_counter_data и отправляются агентом в сервис рекомендаций.

    Важно! Пересылаемая информация не содержит персональных данных клиентов.

    Сбор данных для рекомендаций можно отключить в [dw]настройках главного модуля[/dw][di][/di] (Настройки > Настройки продукта > Настройки модулей > Главный модуль).


    Логика работы сервиса

    Сервис рекомендаций в своей работе использует следующие типы событий магазина: просмотр товара, добавление в корзину, оформление заказа, оплата заказа. Вместе с этими событиями на сервер передаются следующие данные:

    • кука пользователя;
    • хеш лицензии;
    • домен;
    • идентификатор товара;
    • название товара;
    • категории товара;
    • идентификатор рекомендации.

    Работа с данными ведется в несколько этапов:

    • На первом этапе все события поступают в единую точку входа bitrix.info, куда в секунду поступает порядка тысячи запросов. (Точка входа реализована на NGINX с модулем Lua.) Входящие данные накапливаются в буфере Kinesis, который хранит данные одни сутки. Затем данные достаются из буфера и обрабатываются большим количеством php-воркеров. Воркеры фильтруют данные и сохраняют их в таблицах хранилища Amazon DinamoDB.

    • На втором этапе данные достаются из хранилища Amazon DynamoDB и при этом, чтобы каждый раз не сканировать базу, используются простые файлы данных, хранящиеся в Amazon S3. Затем выполняется обработка данных: данные обсчитываются с помощью batch-процессинга (используется инструментарий Apache Spark) и с помощью онлайн алгоритмов (Apache Tomcat, Apache Mahout, алгоритмов похожести пользователей, похожести товаров и семантического анализа на основе контента). В результате выбранная и обсчитанная информация в виде рекомендаций выдается из analytics.bitrix.info.

    Метрики качества

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

    • Отношение просмотров по рекомендации к просмотрам, т.е. определяется эффективность размещения компонента.
    • Отношение заказов по рекомендации к просмотрам по рекомендации – этот показатель оценивает качество алгоритма, насколько релевантной оказалась рекомендация покупателю.
    • Отношение заказов по рекомендации ко всем заказам.

    API запросов

    Персональная рекомендация

    Для конкретной [ds]куки[/ds][di] Cookie - это текстовая строка информации, которую веб-сервер передает в браузер посетителя сайта и которая сохраняется в файле на устройстве посетителя сайта. Как правило, используется для определения уникальности посетителя, времени его последнего визита, личных настроек, уникального идентификатора корзины покупок и т.д.

    Подробнее...[/di] дается персональная рекомендация. Используется комбинированный алгоритм определения похожести товаров.

    Запрос:

    https://analytics.bitrix.info/crecoms/v1_0/recoms.php?op=recommend&uid=#кука#&count=3&aid=#хэш_лицензии#

    Параметры:

    • op=recommend;
    • uid – cookie пользователя (нужна исключительно для привязки выданной рекомендации к пользователю, в расчетах не используется );
    • aid – хэш от лицензии сайта;
    • сount – число элементов (размер выдачи).

    Ответ:

    {"id":"24aace52dc0284950bcff7b7f1b7a7f0de66aca9","items":["1651384","1652041","1651556"]}
    

    id - уникальный идентификатор выданной рекомендации, который потом отслеживается в сессии пользователя на сайте и передается вместе со связанным событием: просмотром, добавлением в корзину, оформлением заказа и его оплатой. В случае ошибки сохранения его в базе данных вместо него возвращается false.

    Товары, похожие на данный

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

    Запрос:

    https://analytics.bitrix.info/crecoms/v1_0/recoms.php?op=simitems&aid=#хэш_лицензии#&eid=#id_товара#&count=3&type=combined&uid=#кука#
    

    Параметры:

    • op=simitems;
    • uid – cookie пользователя (нужна исключительно для привязки выданной рекомендации к пользователю, в расчетах не используется );
    • aid – хэш от лицензии сайта;
    • eid – идентификатор товара;
    • type - view|order|combined, если не задать, то по умолчанию выбираются максимально похожие по комбинированному алгоритму (combined);
    • сount – число элементов (размер выдачи).

    Топ товаров на сайте

    Это не персональная рекомендация. Топ товаров на сайте - это топ по продажам, топ по просмотрам и комбинированный тип.

    Запрос:

    https://analytics.bitrix.info/crecoms/v1_0/recoms.php?op=sim_domain_items&aid=#хэш_лицензии#&domain=#домен#&count=50&type=combined&uid=#кука#

    Параметры:

    • op=sim_domain_items;
    • uid – cookie пользователя (нужна исключительно для привязки выданной рекомендации к пользователю, в расчетах не используется );
    • aid – хэш от лицензии сайта;
    • domain – домен сайта;
    • type - view|order|combined, если не задать, то по умолчанию выбираются максимально похожие по комбинированному алгоритму (combined);
    • сount – число элементов (размер выдачи).

    Веб-сервисы

    Веб-служба, веб-сервис (англ. web service) — программная система, идентифицируемая строкой URI, чьи публичные интерфейсы и привязки определены и описаны языком XML. Описание этой программной системы может быть найдено другими программными системами, которые могут взаимодействовать с ней согласно этому описанию посредством сообщений, основанных на XML, и передаваемых с помощью интернет-протоколов.

    Достоинства веб-сервисов:

    Веб-службы обеспечивают взаимодействие программных систем независимо от платформы. Веб-службы основаны на базе открытых стандартов и протоколов. Благодаря использованию XML достигается простота разработки и отладки веб-служб. Использование интернет-протокола HTTP обеспечивает взаимодействие программных систем через межсетевой экран.

    Модуль Веб-сервисов служит для облегчения создания и интеграции с существующими веб-сервисами. Основное свое применение модуль найдет при разработке интеграций с действующими приложениями как внутри компании, так и с внешними уже работающими веб-сервисами.

    Пример создания windows-приложения для добавления новостей

    Описание

    Рассмотрим, как на его основе модуля Веб-сервисы можно сделать простейшее Windows-приложение для добавления новостей при помощи Visual Studio 2012 и .NET Framework 4.5. Для этого можно скачать одну из бесплатных версий Express 2012 for Windows Desktop.

    Создание информационного блока

    Прежде всего, чтобы иметь возможность добавлять новости, необходимо определить информационный блок, в котором они будут храниться. Для примера будем использовать уже имеющийся у нас на сайте инфоблок с ID=3.

    Создание компонента

    Создание компонента веб-сервиса для добавления новостей

    При установке модуля Веб-сервисы создается новый компонент bitrix:webservice.server. Он предназначен для простого создания, тестирования и вывода в читабельном виде информации о ваших веб-сервисах. Для нашего веб-сервиса он будет являться «сервером».

    Сначала создадим свой компонент. Для этого создадим новую папку в /bitrix/components/demo, назовем ее webservice.addnews. Как и любой другой компонент 2.0, компонент веб-сервиса должен содержать стандартные файлы описаний:

    • Файл .description.php:
      <?
      if (!defined("B_PROLOG_INCLUDED") || B_PROLOG_INCLUDED!==true) die();
      
      $arComponentDescription = array(
      	"NAME" => "Веб-сервис добавления новостей",
      	"DESCRIPTION" => "Веб-сервис добавления новостей",
      	"CACHE_PATH" => "Y",
      	"PATH" => array(
      		"ID" => "service",
      			"CHILD" => array(
      				"ID" => "webservice",
      				"NAME" => "Веб-сервис добавления новостей."
      		)
      	),
      );
      ?>
      
    • Файл .parameters.php:
      <?
      if (!defined("B_PROLOG_INCLUDED") || B_PROLOG_INCLUDED!==true) die();
      
      $arComponentParameters = array(
      	"GROUPS" => array(),
      	"PARAMETERS" => array(),
      	);
      ?>
      
    • И исполняемый файл component.php. Создадим первоначально его в следующем виде:
      <?
      if(!defined("B_PROLOG_INCLUDED") || B_PROLOG_INCLUDED!==true) die();
      
      if(!CModule::IncludeModule("webservice") || !CModule::IncludeModule("iblock"))
         return;
         
      // наш новый класс наследуется от базового IWebService
      class CAddNewsWS extends IWebService
      {
      	// метод GetWebServiceDesc возвращает описание сервиса и его методов
      	function GetWebServiceDesc() 
      	{
      		$wsdesc = new CWebServiceDesc();
      		$wsdesc->wsname = "bitrix.webservice.addnews"; // название сервиса
      		$wsdesc->wsclassname = "CAddNewsWS"; // название класса
      		$wsdesc->wsdlauto = true;
      		$wsdesc->wsendpoint = CWebService::GetDefaultEndpoint();
      		$wsdesc->wstargetns = CWebService::GetDefaultTargetNS();
      
      		$wsdesc->classTypes = array();
      		$wsdesc->structTypes = Array();
      		$wsdesc->classes = array();
      
      		return $wsdesc;
      	}
      }
      
      $arParams["WEBSERVICE_NAME"] = "bitrix.webservice.addnews";
      $arParams["WEBSERVICE_CLASS"] = "CAddNewsWS";
      $arParams["WEBSERVICE_MODULE"] = "";
      
      // передаем в компонент описание веб-сервиса
      $APPLICATION->IncludeComponent(
      	"bitrix:webservice.server",
      	"",
      	$arParams
      	);
      
      die();
      ?>
      

    Это минимальное содержимое для определения веб-сервиса. Как видно он содержит наследованный от IWebService класс CAddNewsWS, переопределенный метод GetWebServiceDesc, возвращающий описание сервиса в формате CWebServiceDesc и вызов компонента bitrix:webservice.server, которому в качестве параметров передается описание нашего зарождающегося веб-сервиса.


    Для проверки работоспособности создадим новую страницу, например, ws_addnews.php, разместим на ней новый компонент и сохраним. В публичном разделе результат будет следующим

    Как мы видим, появилось описание нашего веб-сервиса. Но у него нет ни одного метода. Для создания метода нам потребуется добавить в класс новый метод, который принимает на вход в качестве параметров некоторые поля новости, а в качестве результата возвращает ID добавленной новости или ошибку:

    function AddNews($NAME, $DATE, $PREVIEW_TEXT, $DETAIL_TEXT, $KEYWORDS, $SOURCE)
    {
    	$iblock_permission = CIBlock::GetPermission(3);   
    	if ($iblock_permission < "W")
    	{
    		$GLOBALS["USER"]->RequiredHTTPAuthBasic();
    		return new CSOAPFault('Server Error', 'Unable to authorize user.');
    	}
    		$arFields = Array(
    			"IBLOCK_ID"=>3, // инфоблок "Новости магазина"
    			"NAME"=>$NAME,
    			"DATE_ACTIVE_FROM"=>$DATE,
    			"PREVIEW_TEXT"=>$PREVIEW_TEXT,
    			"DETAIL_TEXT"=>$DETAIL_TEXT,
    			"PROPERTY_VALUES" => Array(
    				"KEYWORDS"=>$KEYWORDS,
    				"SOURCE"=>$SOURCE,
    		)
    	);
    	$ib_element = new CIBlockElement();
    	$result = $ib_element->Add($arFields);
    	if($result>0)
    		return Array("id"=>$result);
    
    	return new CSOAPFault( 'Server Error', 'Error: '.$ib_element->LAST_ERROR );
    }
    

    Зарегистрируем новый метод в массиве $wsdesc->classes:

    $wsdesc->classes = array(
    	"CAddNewsWS"=> array(
    		"AddNews" => array(
    			"type"      => "public",
    			"input"      => array(
    				"NAME" => array("varType" => "string"),
    				"DATE" => array("varType" => "string"),
    				"PREVIEW_TEXT" => array("varType" => "string"),
    				"DETAIL_TEXT" => array("varType" => "string"),
    				"KEYWORDS" => array("varType" => "string"),
    				"SOURCE" => array("varType" => "string"),
    				),
    			"output"   => array(
    			"id" => array("varType" => "integer")
    		),
    		"httpauth" => "Y"
    		),
    	)
    );
    

    В массиве содержится название класса и названия методов, с описанием входных и выходных параметров.


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

    Создание приложения

    Теперь можно приступать к завершающему этапу - созданию в Visual Studio простого Windows-приложения. В нашем примере мы будем делать приложение в Express 2012 for Windows Desktop на C#.

    • Создаем новый проект в Visual Studio, в качестве шаблона выбираем Приложение Windows Forms:

    • На вкладке Обозреватель решений добавляем ссылку на службу:

    • В открывшейся форме указываем ссылку на страницу с компонентом в следующем виде: http://_ваш_домен_/ws_addnews.php?wsdl. Нажимаем кнопку Перейти.

      В поле Службы отобразится наш веб-сервис bitrix.webservice.addnews с интерфейсом CAddNewsWSInterface, для которого доступна операция AddNews:

      Переименовываем пространство имен в myws.addnews и нажимаем кнопку OK.

    • В файле конфигурации приложения App.config необходимо добавить настройки basicHttpBinding:
      <binding name="CAddNewsWSBinding">
      	<security mode="TransportCredentialOnly">
      		<transport clientCredentialType="Basic" proxyCredentialType="None"
      			realm="AXIS" />
      		<message clientCredentialType="UserName" algorithmSuite="Default" />
      	</security>
      </binding>
      
    • В настройках ссылки на службу необходимо отметить использование System.Web.Services:

    • Далее приступаем к созданию непосредственно самого окна ввода новости, для этого разместим на форме необходимые поля и кнопку Отправить:

    • Двойным кликом по кнопке открываем обработчик события нажатия на кнопку и размещаем код сохраняющий новость на сайте. Можно использовать обычный код:
      private void button1_Click(object sender, EventArgs e)
      	{
      		CAddNewsWSInterfaceClient news = new CAddNewsWSInterfaceClient();
      		news.ClientCredentials.UserName.UserName = "admin";
      		news.ClientCredentials.UserName.Password = "password";
      		try
      		{
      		long result = news.AddNews(NAME.Text, NEWS_DATE.Text, PREVIEW_TEXT.Text, DETAIL_TEXT.Text, KEYWORDS.Text, SOURCE.Text);
      		MessageBox.Show("Новость №" + result + " успешно добавлена");
      		}
      		catch (System.Web.Services.Protocols.SoapHeaderException exception)
      		{
      			MessageBox.Show("Ошибка добавления новости [" + exception.Message + "]");
      		}
      	}
      
      или асинхронный:
      private async void button1_Click(object sender, EventArgs e)
      	{
      		CAddNewsWSInterfaceClient news = new CAddNewsWSInterfaceClient();
      		news.ClientCredentials.UserName.UserName = "admin";
      		news.ClientCredentials.UserName.Password = "password";
      	try
      	{
      	AddNewsResponse result = await news.AddNewsAsync(NAME.Text, NEWS_DATE.Text, PREVIEW_TEXT.Text, DETAIL_TEXT.Text, KEYWORDS.Text, SOURCE.Text);
      	MessageBox.Show("Новость №" + result.Body.id + " успешно добавлена");
      		}
      			catch (System.Web.Services.Protocols.SoapHeaderException exception)
      				{
      					MessageBox.Show("Ошибка добавления новости [" + exception.Message + "]");
      				}
      	}
      

      Важно! в описании используемых пространств имен необходимо добавить:
      using System.Net; 
      using WindowsFormsApplication1.myws.addnews;
      

    • Компилируем приложение, запускаем, заполняем поля, нажимаем кнопку Отправить:

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

    Исходный код компонента: скачать.
    Исходный код приложения: скачать.

    Примечание: чтобы добавлять новости с картинкой, придется на стороне Windows-приложения читать файл, конвертировать его при помощи System.Convert.ToBase64String в BASE64, а на стороне компонента конвертировать назад функцией base64_decode, сохранять его во временный файл и передавать на вход методу CIBlockElement:Add(), как одно из полей. Помимо этого, нам необходимо знать на сервере как минимум расширение (тип) файла, поэтому вместе с содержимым будем передавать оригинальное имя файла.
    Таким образом вот так будет выглядеть наш класс веб-сервиса в компоненте:
    class CAddNewsWS extends IWebService
    {
    	function AddNews($NAME, $DATE, $PREVIEW_TEXT, $DETAIL_TEXT, $KEYWORDS, $SOURCE, $IMAGE_NAME, $IMAGE_CONTENT)
    	{
    		$iblock_permission = CIBlock::GetPermission(3);
    		if ($iblock_permission < "W")
    		{
    			$GLOBALS["USER"]->RequiredHTTPAuthBasic();
    			return new CSOAPFault('Server Error', 'Unable to authorize user.');
    		}
            
    		$arFields = Array(
    			"IBLOCK_ID"=>3, // инфоблок "Новости магазина"
    			"NAME"=>$NAME,
    			"DATE_ACTIVE_FROM"=>$DATE,
    			"PREVIEW_TEXT"=>$PREVIEW_TEXT,
    			"DETAIL_TEXT"=>$DETAIL_TEXT,
    			"PROPERTY_VALUES" => Array(
    				"KEYWORDS"=>$KEYWORDS,
    				"SOURCE"=>$SOURCE,
    				)
    		);
    	if(strlen($IMAGE_NAME)>0 && strlen($IMAGE_CONTENT)>0)
            {
    		$IMAGE_CONTENT = base64_decode($IMAGE_CONTENT);
    		if(strlen($IMAGE_CONTENT)>0)
    		{
    			$tmp_name = $_SERVER['DOCUMENT_ROOT'].'/bitrix/tmp/'.md5(uniqid(rand(), true)).".tmp";
    			CheckDirPath($tmp_name);
    			$f = fopen($tmp_name, "wb");
    			fwrite($f, $IMAGE_CONTENT);
    			fclose($f);
    			$arFields["DETAIL_PICTURE"] = Array("name"=>$IMAGE_NAME, "tmp_name"=>$tmp_name, "size"=>strlen($IMAGE_CONTENT), "type"=>"image/jpeg");
    			}
    		}
    		$ib_element = new CIBlockElement();
    		$result = $ib_element->Add($arFields);
    		if($tmp_name)
    			@unlink($tmp_name);
    		if($result>0)
    			return Array("id"=>$result);
    		return new CSOAPFault( 'Server Error', 'Error: '.$ib_element->LAST_ERROR );
    	}
    
    	// метод GetWebServiceDesc возвращает описание сервиса и его методов
    	function GetWebServiceDesc()
    	{
    		$wsdesc = new CWebServiceDesc();
    		$wsdesc->wsname = "bitrix.webservice.addnews";
    		$wsdesc->wsclassname = "CAddNewsWS";
    		$wsdesc->wsdlauto = true;
    		$wsdesc->wsendpoint = CWebService::GetDefaultEndpoint();
    		$wsdesc->wstargetns = CWebService::GetDefaultTargetNS();
    		$wsdesc->classTypes = array();
    		$wsdesc->structTypes = Array();
    		$wsdesc->classes = array(
    			"CAddNewsWS"=> array(
    				"AddNews" => array(
    					"type"        => "public",
    					"input"        => array(
    						"NAME" => array("varType" => "string"),
    						"DATE" => array("varType" => "string"),
    						"PREVIEW_TEXT" => array("varType" => "string"),
    						"DETAIL_TEXT" => array("varType" => "string"),
    						"KEYWORDS" => array("varType" => "string"),
    						"SOURCE" => array("varType" => "string"),
    						"IMAGE_NAME" => array("varType" => "string"),
    						"IMAGE_CONTENT" => array("varType" => "string"),
    						),
    					"output"    => array(
    						"id" => array("varType" => "integer")
    					),
    					"httpauth" => "Y"
    				),
    			)
    		);
    
    		return $wsdesc;
    	}
    }
    

    А вот так обработчик нажатия на кнопку в Windows-приложении (IMAGE - это новый контрол на форме, в нем должен быть путь к файлу на диске):

    private void button1_Click(object sender, EventArgs e)
    {
    	Byte[] binaryData;
    	string base64String = "";
    	if(IMAGE.Text.Length>0)
    	{
    		try
    		{
    			System.IO.FileStream imageFile = new System.IO.FileStream(IMAGE.Text, System.IO.FileMode.Open,System.IO.FileAccess.Read);
    			binaryData = new Byte[imageFile.Length];
    			imageFile.Read(binaryData, 0, (int)imageFile.Length);
    			imageFile.Close();
    			base64String = System.Convert.ToBase64String(binaryData, 0, binaryData.Length);
    		}
    		catch (System.Exception exp)
    		{
    			MessageBox.Show("Ошибка чтения картинки [" + exp.Message + "]");
    			return;
    		}
    	}
    
    			CAddNewsWSInterfaceClient news = new CAddNewsWSInterfaceClient();
    			news.ClientCredentials.UserName.UserName = "admin";
    			news.ClientCredentials.UserName.Password = "password";
    			try
    			{
    			long result = news.AddNews(NAME.Text, NEWS_DATE.Text, PREVIEW_TEXT.Text, DETAIL_TEXT.Text, KEYWORDS.Text, SOURCE.Text, IMAGE_FILE.Text, base64String);
    			MessageBox.Show("Новость №" + result + " успешно добавлена");
    		}
    		catch (System.Web.Services.Protocols.SoapHeaderException exception)
    		{
    			MessageBox.Show("Ошибка добавления новости [" + exception.Message + "]");
    		}
    }
    

    Vue.js и Bitrix Framework

    Важно! Работа с Vue.js подробно описана в отдельном учебном курсе Vue.js и Bitrix Framework.

      Что такое Vue.js?

    Vue (произносится /vjuː/, примерно как view) — это среда JavaScript для создания пользовательских интерфейсов. Он построен на основе стандартных HTML, CSS и JavaScript и предоставляет декларативную и основанную на компонентах модель программирования, которая помогает эффективно разрабатывать пользовательские интерфейсы, будь то простые или сложные.

    Vue.JS базируется на идее [ds]виртуального DOM.[/ds][di] DOM (аббревиатура от Document Object Model) — способ представления
    структурного документа с помощью объектов. Это кроссплатформенное и
    языко-независимое соглашение для представления и взаимодействия с
    данными в HTML, XML и т.д.

    Главная проблема DOM — он никогда не был рассчитан для создания динамического
    пользовательского интерфейса (UI). Мы можем работать с ним, используя
    JavaScript и библиотеки наподобие jQuery, но их использование не решает
    проблем с производительностью.

    Вместо того, чтобы взаимодействовать с DOM напрямую, мы работаем с его
    легковесной копией. Мы можем вносить изменения в копию, исходя из
    наших потребностей, а после этого применять изменения к реальному DOM.

    Подробнее...
    Управление DOM-структурой в документации Битрикс. [/di] Это исключает прямое взаимодействие с узлами интерфейса. Первоначальная работа ведется с виртуальной копией (virtual DOM), а уже потом изменения применяются к реальным узлам интерфейса. Параллельно сравнивается реальное дерево DOM и его виртуальная копия, выявляется разница и переписывается только то, что претерпело изменения. Т.е. за короткий промежуток времени накапливаются все изменения, а потом эти изменения применяются к реальным узлам интерфейса (нодам) единым патчем. За счет этого скорость выполнения гораздо выше.

    Vue.js удобно использовать, когда какие-либо данные используются в нескольких компонентах. В таком случае эти данные выносятся в отдельную библиотеку.

    Преимущества Vue:

    • Простой вход, не нужно настраивать окружение, отличная документация;
    • Сочетает в себе плюсы React и Angular;
    • Фреймворк прошел этап становления;
    • Популярен среди разработчиков.

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

    Библиотека Vue входит в состав продукта «1С-Битрикс: Управление сайтом», что дает возможность использования и кастомизации компонентов Vue, встроенных в ядро BitrixFramework. К тому же, решена проблема версионности библиотеки Vue – в рамках текущего окружения BitrixFramework всегда единая версия.

    Примечание: Данное решение не подойдет для проектов, использующих SSR (Server side rendering) и серверную компиляцию (например, для однофайловых компонентов).

    Подробнее с фреймворком (в том числе и с примерами его использования) вы можете ознакомиться [ds]на официальном сайте.[/ds][di] Если вы хотите узнать больше о Vue перед тем как начать, мы создали видео с рассказом об основных принципах работы на примере проекта.

    Если вы — опытный фронтенд-разработчик, и хотите узнать, чем Vue отличается от остальных библиотек или фреймворков, обратите внимание на сравнение с другими фреймворками.



    Подробнее...[/di]

      Почему фреймворк включен в продукт?

    Преимущества и особенности использования обёртки Vue от Битрикса в следующем:

    Мы включили фреймворк в поставку продукта и сделали BitrixVue для простого взаимодействия между Vue и системой BitrixFramework.

    BitrixVue — это расширение для стандартной библиотеки Vue. Оно добавляет новые возможности и при этом не модифицирует стандартное поведение Vue.js.

    В чём же преимущества и особенности её использования?

    Первое: используя BitrixVue, Вы сможете интегрироваться в систему BitrixFramework и эффективно работать с другими компонентами и внутренними системами (такими как локализации и динамическая загрузка компонентов).

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

    Третье: разработчики-партнёры получают возможность кастомизировать и клонировать компоненты в модулях BitrixFramework (где это возможно) без изменения исходного кода продукта.

    И последнее: фреймворк Vue, который является основной частью BitrixVue, не экспортируется в глобальную область видимости. Тем самым она не может создать проблем сторонним приложениям. Они могут спокойно использовать обычное подключение Vue, в том числе использовать его в Webpack, не беспокоясь о конфликтах с библиотекой внутри BitrixFramework.