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

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

Вкладки

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

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

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

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

  • Ключевое смысловое слово, фраза.
  • Термины, названия элементов интерфейса: кнопки, вкладки, поля, горячие клавиши, формат или название файла.
  • Название курса, глав курса, разделов и страниц сайтов. (Как правило, название глав или курсов даются ссылкой, но иногда это бывает излишним).
Примеры использования жирного шрифта
Пример названия курса или главы | Изучение курса позволяет освоить основные методы администрирования системы, а так же пополнить знания по темам, изученным в курсе Контент-менеджер.
Пример названий элементов интерфейса: | При этом у компонента Список новостей нужно настроить входные параметры так, чтобы он мог формировать ссылки на страницу детальной новости (с кодом новости). Для этого нужно в группе параметров Шаблоны ссылок в поле URL страницы детального просмотра задать путь к этой странице, а также название параметра, в котором будет передаваться код новости для показа.
Пример ключевой фразы, слова | При регистрации сервера обязательно укажите Домен для NTLM авторизации. Он должен полностью (включая регистр) совпадать с названием домена.

и Курсив и подчёркивание используются для выделения:

  • Названия продуктов компании 1С-Битрикс, других программ и технологических платформ.
  • Названия методов, классов, функций и событий, если нет их описания в API доке. (Если описание есть, то название метода или класса оформляется ссылкой на страницу документации.)
  • Пространство имён в новом ядре D7.
  • Ключевое слово, фраза, термин, если использование жирного шрифта не желательно по каким-то причинам.

Примеры использования курсива
Пример названия метода: | Усовершенствованные методы буферизации в шаблоне позволяют более не использовать CBitrixComponentTemplate::EndViewTarget() ввиду того, что конец шаблона вызывает завершение буферизации автоматически. (Если страница метода в документации есть, то будет так: Перед подключением компонента ко всем значениям параметров применяется функция htmlspecialcharsEx )
Пример названий продуктов и платформ: | Чтобы научиться эффективно работать в Bitrix Framework, нужно не сравнивать то, что вы знаете по другим системам, а стараться понять как то или иное реализуется в этой системе. В плане обучения «сравнительный» подход не работает. Просто отвлекитесь от старых знаний и изучите новую систему, используя только знания PHP и сайтостроения, а не сравнивая идеологии и технологии.
Пример пространства имён в D7: | При необходимости модуль может организовывать подпространства внутри своего пространства имен. Например, Bitrix\Main\IO, Bitrix\Forum\SomeName\SomeNameTwo.

Серый фон текста используется для выделения:

  • путей в рамках файловой системы.
  • атрибутов, тегов html, параметров функций, переменных, значения полей и настроек и листинга кода, который нет смысла помещать в стиль для листинга.
Примеры использования серого фона
Пример выделения путей в рамках файловой системы: | Результаты кеширования сохраняются в виде файлов в каталоге /bitrix/cache/.
Пример выделения атрибутов, тегов и так далее: | Если в компоненте написать код: $arParams = & $arSomeArray; то переменная $arParams будет отвязана от члена класса компонента и привязана к массиву $arSomeArray.

Синий курсив с > используется для подсветки путей в продукте. То есть пути выглядят так же как они оформлены в административном отделе системы.

Примеры использования синего курсива
Пример оформления путей в административном отделе системы: Файлы кеша можно удалить в административной части на закладке Очистка файлов кеша страницы Настройки > Настройки продукта > Автокеширование.

Синий шрифт используется для подсветки УРЛ без создания собственно ссылки - псевдо УРЛ.

Примеры использования синего шрифта
Пример псевдо УРЛ: При помощи access_token приложение совершает запросы к REST-сервису до его истечения.
https://имя_портала.bitrix24.ru/rest/user.current?auth=код_авторизации

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

Примеры использования цветного шрифта
Цветной шрифт:
  • Настроить инфоблок на контроллере:

    ........

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

    ......

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

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

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

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

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

Примечание. В ядре D7 настройки кеширования производятся в специальном файле

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

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

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

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

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

  • Нужно пояснить какой-то термин.
    Пример: Как правило, все сайты имеют возможность авторизации из публичного раздела .
  • Дать наглядное представление о команде интерфейса.
    Пример: При включённом режиме Правки откройте на редактирование элемент инфоблока , например, новость.
  • Дать дополнительную, но не относящуюся напрямую к выполняемым работам, информацию.
    Пример: Этот инструмент создаст ссылку с одним единственным слешом после домена сайта .
  • Дать описание и ссылку на страницу компонента в пользовательской документации.
    Пример: Простую фотогалерею в публичном разделе выводит комплексный компонент Фотогалерея 2.0 .
Внимание! Всплывающие подсказки не работают в файлах формата CHM. Интернет на мобильных устройствах на сегодняшний день - не проблема, используйте эти файлы документации только в крайнем случае.

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

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

Задание окна для открытия ссылки при редактировании в режиме кода (для ознакомления)

В коде каждый из пунктов этого списка задается с помощью атрибута target тега <a>. Атрибут target может принимать соответственно следующие значения:

  • _blank;
  • _parent;
  • _self;
  • _top.

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

Видеоролики

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

© «Битрикс», 2001-2008, «1C-Битрикс», 2008-2020 1С-Битрикс


Что такое Bitrix Framework?
2 / 382
Цитатник веб-разработчиков.

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

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

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

Продукт «1С-Битрикс: Корпоративный портал» - готовый продукт, позволяющий создать корпоративный портал компании, с возможностью доработки штатного функционала под потребности компании.

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

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

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

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

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

  • Продукты, созданные на Bitrix Framework
  • Решения на базе продуктов
  • Виртуальная лаборатория для изучения продуктов и решений Bitrix Framework
  • Маркетплейс сайт продуктов и решений от партнёров 1С-Битрикс.


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

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

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

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

Мы выбрали подход кейсов в обучении, стажеры получают 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 могут устаревать в силу постоянного развития системы. Отдел документации отслеживает это развитие, но задержки с внесением изменений в курсы могут быть.

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

Видео для разработчиков


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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

Чтобы отключить подобное отношение к файлу необходимо:

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

Отсутствие кнопки Разблокировать возможно в двух случаях:

  1. Файл лежит не локально, а на сетевом ресурсе.
  2. Если файл лежит на локальном диске, но путь к нему содержит спецсимволы (# и прочие).
Cвойства файла

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

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

Онлайн Курсы

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

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

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

FAQ

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

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

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

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

Видео

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

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

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

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


Сообщество разработчиков
5 / 382
Цитатник веб-разработчиков.

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

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

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

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

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

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


Для тех, кто переходит на Bitrix Framework с других платформ
6 / 382
Цитатник веб-разработчиков.

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

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

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

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

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

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

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

  • Пример "переезда" сайта с неизвестной CMS на "1С-Битрикс: Управление сайтом"
  • В социальной сети компании "1С-Битрикс" есть группа OpenSource.Конвертер, в которой можно познакомиться с опытом других разработчиков по миграции сайтов с других систем на Bitrix Framework.

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. Компонент получает данные откуда-то и выводит их в нужном виде. За правильный вывод отвечает шаблон компонента.

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

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


Глоссарий
7 / 382

Что есть что?

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

  • Административная часть
  • Публичная часть
  • Режим Правка
  • Сайт
  • Шаблон сайта
  • Раздел сайта
  • Компоненты
  • Путь к компоненту
  • Включаемые области
  • Навигационная цепочка
  • Индексная страница (файл)
  • Пользователь
  • Группа пользователей
  • Почтовое событие
  • Тип почтового события
  • Почтовый шаблон
  • Путь относительно корня
  • Полный путь
  • Абсолютный путь
  • DocumentRoot
  • Система обновлений
  • Язык
  • Языковой файл
  • Многоязычный интерфейс
  • Локализация
  • Мета-тэг
  • Доменное имя (домен)
  • Хост
  • IP адрес
  • Сессия
  • Время в Unix-формате
  • Права в Unix системах
  • Пролог
  • Эпилог
  • Тело страницы
  • Шаблон страницы
  • Сабмит
  • HTML-безопасный вид
  • Связывание переменных
  • Дамп
  • cron (крон)
  • Буферизация
  • Постоянное соединение (persistent)
  • Accept-Language
  • Кастомизация
  • API
  • Экземпляр программы
  • Портал

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

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

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

  • Что важно помнить при работе над сайтом:
    1. Нельзя править на "боевом" сайте. Необходимо вести разработку на копии сайта с использованием режима Установка для разработки
    2. Если в силу каких-то причин приходится править на живом сайте, то нужно всегда иметь доступ к FTP или SSH, так как административная часть из-за ошибок в коде может оказаться недоступной.
    3. Всегда должен быть наготове свежий бекап.
    4. Для случая, когда доработки значительны, когда работает команда из нескольких человек крайне рекомендуется использовать систему контроля версий .
  • Если вы хотите внести какие-то изменения в работе сайта, то:
    • Сначала формализуйте свои требования на листе бумаги, а не кидайтесь править код.
    • После этого заново просмотрите все случаи использования на сайте модифицируемого вами блока. Убедитесь, что всё логично. Очень часто делают небольшие, казалось бы, изменения, а потом оказывается, что страдает связанный функционал, о котором не подумали или забыли.
    • После того, как вы формализовали потребности в изменениях, посмотрите, какие сущности эти требования затрагивают.
    • Только после этого подумайте, какие средства использовать для достижения своих целей.
  • Способы внесения изменений и желательный порядок их применения:
    • сначала попытайтесь сделать это редактированием шаблона самого сайта и файлов CSS;
    • если предыдущее невозможно, то попытайтесь сделать это средствами редактирования страницы сайта;

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

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

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

  • Если вы сопровождаете живой сайт и заказчик просит провести работы по доделке-расширению функционала, то сначала проверьте, а пользуются ли вообще этим функционалом. Проверить можно так:
    • Статистика посещаемости - Внутренняя статистика, Яндекс.Метрика, Google Analytics или Liveinternet
    • Артефакты внутри Bitrix Framework: результаты веб-форм, заказы, скачивания (в Яндекс.Метрике есть нужный отчёт и отлично работает) и т.д.
    • Карты кликов в Яндекс.Метрике.
    Если блок мёртвый, то лучше его вообще убрать или переформулировать ТЗ .

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

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

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


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

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

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

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

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


Типовой порядок действий
10 / 382
Цитатник веб-разработчиков.

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

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

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

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


Теория. Сайт в понятии Bitrix Framework
11 / 382

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

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

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

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

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

Структура

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

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

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


Теория. Что такое страница
12 / 382
  • Структура
  • Шаблоны
  • Свойства
    •

    Структура

    Страница представляет из себя 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 соответствующего каталога (раздела сайта). Свойства страницы задаются, как правило, либо в теле страницы, либо между служебной частью и визуальной частью пролога.

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


Теория. Язык и языковые файлы
13 / 382

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

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

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

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

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

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

Языковой файл - 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";
?>

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


Теория. Выбор кодировки сайта
14 / 382
Цитатник веб-разработчиков.

Зайцев Артемий: Если есть возможность делать в 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
Возможность сделать сайты в разной кодировке по системе многосайтовости. | Невозможно. Все сайты на одном ядре должны быть в одной кодировке.
Поддержка на различных хостингах | При работе с Bitrix Framework необходимо подключение опции php mbstring.func_overload в значении большем или равном 2. Это разрешается не на всех хостингах. | Работает на любых хостингах.
Размещение продуктов на виртуальной машине BitrixVM. | По умолчанию. | Требует дополнительных действий по настройке.
Разные мелочи
Взаимодействие с WordPress (блог-клиенты, trackback и ping'и) | Есть | Нет
Поддержка большинством редакторов | Требуется редактор, который поддерживает кодировку UTF-8 без BOM. | Нет проблем.

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

  • Конвертация сайта из cp1251 в UTF-8 (блог)
  • Конвертация сайта из cp1251 в UTF-8 (учебный курс)


Теория. Техническое задание на сайт
15 / 382
Цитатник веб-разработчиков.

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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


Базовый шаблон
16 / 382
Цитатник веб-разработчиков.

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

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

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

    • Управление шаблоном дизайна сайта в курсе Разработчик Bitrix Framework
    • Верстка под битрикс и создание шаблона – особенности, проблемы, рекомендации от Алексея Валеева


Теория. Что такое Шаблон сайта
17 / 382
Шаблон дизайна - это внешний вид сайта, в котором определяется расположение различных элементов на сайте, художественный стиль и способ отображения страниц. Включает в себя программный 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.

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

Несколько типовых примеров композиций
  1. Header
  2. Work Area
  3. Footer

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

Статическая информация, которая не нуждается (либо редко нуждается) в замене, как правило, размещается в статических зонах 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.

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

  • Применение шаблона дизайна в курсе Контент-менеджер
  • Шаблоны дизайна в курсе Администратор. Базовый
  • Шаблон сайта в Документации для разработчиков.
  • Интеграция - раздел об интеграции дизайна на сайт.


Теория. Создание и управление шаблоном
18 / 382

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

  • Создать новый;
  • Отредактировать существующий шаблон;
  • Импортировать/Экспортировать шаблон;

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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


Теория. Разработка шаблона дизайна
19 / 382

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

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

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

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

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

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

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

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

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

Примечание: При создании шаблона сайта возможно использование различных программных условий, влияющих на отображение тех или иных элементов шаблона для различных разделов сайта. В этом случае для раздела сайта нужно определить некоторое свойство , значение которого будет проверяться в шаблоне сайта:
<?if ($APPLICATION->GetProperty(“SECT_PROP”)=="Y"):?>

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

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

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

  • При подготовке графического дизайна следует заранее разметить линию раздела дизайна на пролог (header.php) и эпилог (footer.php).
  • Следует выделить основные элементы дизайна, для последующей модификации таблицы стилей: шрифты, цвета заливки и т.п.
  • Разрабатывая дизайн меню различных уровней, желательно выделять повторяющиеся элементы - для упрощения создания шаблона меню и дальнейшего управления этими меню.
  • Для облегчения сопровождения различных языковых версий сайта по возможности следует использовать вместо графических элементов текстовые.
  • При нарезке графического дизайна и подготовке HTML шаблона, необходимо заранее предусмотреть место расположения основных компонентов системы управления сайтом. Выделить области меню, рекламные области, области размещения дополнительных форм. Рекомендуется верстать компоненты независимо от основного шаблона. Чтобы в будущем, при перемещении компонента в другое место или другой шаблон, не поехала верстка.
  • Для удобства отладки верстки, рекомендуется отключить сжатие и объединение css и js файлов .
  • Размещать графические изображения, относящиеся к шаблону сайта, следует в папке /bitrix/templates/<имя_шаблона>/images.
  • Каскадные стили, используемые в шаблоне, рекомендуется разделять на две таблицы стилей, хранящиеся в двух разных файлах. Оба файла находятся в директории /bitrix/templates/<идентификатор_шаблона>/. Файл styles.css содержит стили для представления информационного содержания страницы на сайте. Файл template_styles.css содержит стили для отображения текстов в самом шаблоне дизайна.

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

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

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

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

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

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

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

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

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

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


Теория. Включаемые области
20 / 382

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

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

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

Управление включаемыми областями

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

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

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

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

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

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

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

Размещение включаемой области

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

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

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

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

Создание и редактирование включаемой области

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

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

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

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

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

  • непосредственно из публичного раздела сайта в режиме правки;

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


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

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

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

<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 не будет специально перекрыт у кого-то из нижележащих разделов.


Теория. Цепочка навигации
22 / 382

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

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

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

Описание построения простейшей цепочки навигации без кастомизации приведено в курсе Контент-менеджер.

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


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

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

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

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

  • из публичного раздела (см. курс Контент-менеджер);
  • из административного раздела (см. курс Администратор. Базовый).

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

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

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

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

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

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

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

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

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

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

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


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

Как добавить свой пункт в цепочку навигации?

С помощью функции 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"],


Теория. Меню сайта
25 / 382

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

Общие понятия о работе с меню даются в курсе Контент-менеджер. На основе меню строится карта сайта. Подробный пример создания карты сайта приведен в курсе Контент-менеджер.

Типы меню

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

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

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

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

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

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

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

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

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

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

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

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

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

Карта сайта

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

Создание карты сайта

Подробный пример создания карты сайта приведен в курсе Контент-менеджер.


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

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

  • выделение 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 позволяет также создавать меню динамического типа . Т.е. массив данных таких меню генерируется автоматически на основании некоторых данных, получаемых с помощью программного кода. Данный код должен храниться в папке соответствующего раздела сайта в файле с именем .<тип меню>.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 меню на экран.


Шаблоны меню
27 / 382

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

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

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

  • 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 - полный доступ (право на прямое редактирование файла и право на изменение прав доступа на данный файл);
  • ADDITIONAL_LINKS - массив дополнительных ссылок для подсветки меню;
  • 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/.

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

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

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


Управление меню
28 / 382

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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


Примеры создания меню
29 / 382
  • Предварительные операции
  • Древовидное меню
  • Одноуровневое древовидное меню
  • Многоуровневое древовидное меню
    •
  • Выпадающее меню
  • Построение меню из информационных блоков
  • Два меню рядом
  • Графическое меню
    •

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

    Для примера мы создадим в структуре сайта новый раздел 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) и на контекстной панели используйте команду Добавить меню кнопки Добавить. В открывшейся форме перейдите выберите тип меню равный Подменю, после чего отредактируйте нужные пункты меню.

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

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

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

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

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

    До версии 10.5
    • Откройте для редактирования файл css используемого шаблона компонента.

      • В конце описания стилей есть несколько групп, в названия которых включено *Items text. В этих стилях нужно добавить строки. Рассмотрим, как это сделать на примере группы стилей /*Items text color & size */ файла css шаблона Горизонтальное многоуровневое выпадающее меню.

      В исходном варианте эта группа имеет следующее содержание:

      #horizontal-multilevel-menu li a,
#horizontal-multilevel-menu li:hover li a,
#horizontal-multilevel-menu li.jshover li a,
#horizontal-multilevel-menu li:hover li:hover li a,
#horizontal-multilevel-menu li.jshover li.jshover li a,
#horizontal-multilevel-menu li:hover li:hover li:hover li a,
#horizontal-multilevel-menu li.jshover li.jshover li.jshover li a,
#horizontal-multilevel-menu li:hover li:hover li:hover li:hover li a,
#horizontal-multilevel-menu li.jshover li.jshover li.jshover li.jshover li a,
#horizontal-multilevel-menu li:hover li:hover li:hover li:hover li:hover li a,
#horizontal-multilevel-menu li.jshover li.jshover li.jshover li.jshover li.jshover li a

      Для добавления еще одного уровня необходимо добавить еще две строки:

      #horizontal-multilevel-menu li:hover li:hover li:hover li:hover li:hover li:hover li a,
#horizontal-multilevel-menu li.jshover li.jshover li.jshover li.jshover li.jshover li.jshover li a

      Дополнив их соответственно :hover li - в одной строке и .jshover li - в другой строке.

    • Дополните аналогичным способом остальные группы стилей, имеющих в своем названии *Items text.
    • Сохраните внесенные изменения.

    Теперь вы можете использовать глубину вложений равную 5.

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

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

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

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

    Построение меню из динамических элементов - информационных блоков - позволяет снять с контент-менеджера часть нагрузки по поддержке сайта. Не нужно будет выполнять работы по актуализации меню в связи с появившимися новыми разделами и страницами. Для решения этой задачи необходимо использовать компонент Пункты меню (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. Вы увидите созданное новое меню:

    Красным цветом выделены пункты, созданные в рамках главного меню (массив $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 для указания картинки, выводимой, если отсутствует картинка для пункта меню или если для него не указаны параметры. Вам нужно использовать собственное имя этой картинки. И другой путь, если вы сохраняете картинки в другой папке.

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

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


Примеры решения частных задач в меню
30 / 382
  • Подсветка ссылок
  • Как сделать, чтобы отдельный пункт меню открывался в новом окне?
  • Отображение пункта меню для определенных групп пользователей
  • Отображение пункта меню неавторизованным пользователям
  • Отображение пункта меню при нахождении в определенном разделе сайта
  • Отображение определенных пунктов только на главной странице и еще в одном внутреннем разделе
  • Осуществление вывода пункта меню, ссылающегося на определенный товар, связанный с товаром, открытым на данной странице
  • Всплывающие подсказки для пунктов меню
  • Как поставить картинки рядом с пунктами меню?
  • Разные картинки-пункты меню для разных языков
  • Отображение собственного, не такого как в шаблоне, меню для определенных разделов сайта
  • Как сделать меню 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 '<b>проверка отложенного меню!</b>';
    // ....здесь происходит вывод меню компонентом или другим способом.... //
    $APPLICATION->SetPageProperty('menu', ob_get_clean() );
}

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

    Меню и кеш

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

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


Теория. Рекламные области
31 / 382

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

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

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

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

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

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

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

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


Типы рекламы
32 / 382

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

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

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

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

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


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

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

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

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

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

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

  • ключевые слова баннера;
  • ключевые слова страниц сайта:
    • желательные (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), заданные для страницы.


Теория. Использование прав доступа
34 / 382

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

  • Для управления показом пунктов меню

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

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

  • Для управления шаблоном меню

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

    <?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, используются только для меню сайта.

  • Для управления шаблоном сайта

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

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

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

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

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

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

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

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

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

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


Теория. Разные языки сайта.
35 / 382

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

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

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

Перед изучением главы рекомендуется познакомится с Управлением языковыми сообщениями курса Администратор. Базовый.

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

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

  • Пользовательская документация по модулю "Перевод"


Механизм реализации
36 / 382

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

  • В папке шаблона сайта создается папка с именем /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')

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


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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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


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

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

Тем не менее, есть технология замены любых языковых фраз продукта. Решение не идеальное, но оно дает довольно большой простор для полета фантазии разработчика. Суть технологии в том, что языковые фразы продукта после подключения языкового файла заменяются на фразы, определенные разработчиком. Список замен должен находиться в файле /bitrix/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'] = "Проактивка";
?>

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

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


Теория. Настройка дополнительных элементов
39 / 382

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

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

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

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

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

Чтобы произвести настройку внешнего вида файла, подключаемого при закрытии публичной части сайта, следует скопировать файл: /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-скрипта. Такое решение использовалось для модуля статистики.
Во всех актуальных версиях продукта такой вариант более не используется.


Примеры работы и решения проблем
40 / 382
Цитатник веб-разработчиков.

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

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

  • Применение шаблона дизайна (курс Контент-менеджер)
  • Шаблоны дизайна (курс Администратор. Базовый)

  • Простые примеры применения шаблонов в зависимости от различных условий
  • Подключение 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".


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

Усовершенствованные методы буферизации в шаблоне позволяют более не использовать 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

  • Cmain::AddViewContent($view, $content, $pos)
  • Cmain::ShowViewContent($view)

где:

  • $view – идентификатор буферизируемой области;
  • $content – буферизируемый контент;
  • $pos – сортировка вывода контента.

Примечание: одному идентификатору $view может соответствовать несколько буферов. Последовательность вывода контента определяется сортировкой $pos.


Разработка шаблонов страниц
42 / 382

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

Шаблоны страниц и редактируемых областей хранятся в папке /page_templates/, находящейся в каталоге соответствующего шаблона сайта или в папке /bitrix/templates/.default/, если предполагается использование одинаковых шаблонов страниц для всех шаблонов сайта.

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

Детально о создании шаблонов страниц описано в курсе Администратор. Базовый


Руководство по оформлению HTML/CSS кода
43 / 382

Руководство - это правила для разработчиков компании 1С-Битрикс, которое является обязательным при разработке продуктов Bitrix Framework. Раздел описывает правила оформления и форматирования HTML и CSS кода. Его цель - повысить качество кода и облегчить совместную работу и поддержку инфраструктуры разработчиками.

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


Общие правила оформления кода
44 / 382

Отступы

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

Настройка PHPStorm:
Показ невидимых символов: Preferences > Editor > General > Appearence > Show Whitespaces.
Табуляция вместо пробелов: Preferences > Editor > Code Style > (PHP|HTML|CSS|JavaScript) > Use tab character.

Кодировка

UTF-8 фактически стал стандартом, хотя клиенты Bitrix Framework иногда установливают продукт в кодировке ANSI.

Перевод строки

Строки должны заканчиваться только символом перевода на новую строку (LF) - стандарт для текстовых файлов в Unix-системах. Не используйте комбинацию символов возврата каретки/перевода строки (CRLF), как это принято в Windows.

Настройка PHPStorm:
Перевод строки для новых файлов: Editor > Code Style > Line separator (for new files) > Unix and OS X \n.
В нижней статусной строке показывается, какой перевод строки используется для текущего файла (должен быть LF).

Максимальная длина строки

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

Настройка PHPStorm:
Показ правой границы: Editor > General > Appearence > Show right margin
Ширина правой границы: Editor > Code Style > Right margin (columns

Регистр

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

Пробелы в конце строки

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

Настройка PHPStorm:
Удаление пробелов перед сохранением файла: Editor -> General -> Strip Trailing Spaces on Save.

Комментарии

Все комментарии должны быть на английском языке.


Правила оформления HTML
45 / 382

Семантика

Использование семантики облегчает восприятие 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-pos">Бухгалтер</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: url(images/sprite.svg);
    }

    .crm-lead-log {
        background: url(images/arrow_left.svg);
    }

    Хорошо:

        .crm-lead-history {
        background: url(images/crm-lead-sprite.svg);
    }

    .crm-lead-log {
        background: url(images/crm-lead-arrow-left.svg);
    }


Правила форматирования HTML
46 / 382

Новая строка для каждого блочного или табличного элемента

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

Плохо:
<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
47 / 382

Каскадные селекторы

Любой блок на странице должен описываться селектором класса. Не используйте каскадные селекторы так как они нарушают принцип независимости компонентов. Это же правило относится к селекторам прямого потомка (>) и соседнего элемента (+).

Исключения:

  • Селекторы состояний (selected, 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
48 / 382
  • При создании правила для нескольких селекторов помещайте каждый селектор на отдельной строке.
  • Перед открывающей скобкой ставьте один пробел.
  • Внутри блока объявлений помещайте каждое объявление на отдельной строке.
  • Добавляйте один уровень отступов перед каждым объявлением.
  • Ставьте пробел после двоеточия внутри объявления.
  • Всегда ставьте точку с запятой после последнего объявления в блоке.
  • Ставьте закрывающую скобку на одной вертикальной линии с первым символом в селекторе.
  • Ставьте пробел после каждой запятой в объявлениях со множественным значением.
  • Разделяйте правила пустой строкой.
  • Сортируйте свойства по принципу: свойства, сильно влияющие на элемент, в начале, а самые незначительно влияющие - в конце:
    • 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);
        }


Тестирование верстки (чеклист)
49 / 382

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

  • Проверка верстки на соответствие макету (Pixel Perfect).
  • Браузеры:
    • Chrome
    • Firefox
    • IE11
    • IE Edge
    • Safari
  • Минимальное и максимальное разрешение экрана.
  • Переполнение блоков:
    • Длинный текст без пробелов
    • Длинный текст с пробелами
  • Удаление необязательных блоков, которые могут отсутствовать.
  • Retina-экран.
  • Картинки разных размеров и пропорций в допустимых пределах, которые будет допускать код.


Особенности интеграции с Битрикс
50 / 382

Шаблон сайта и компоненты

В рамках продукта верстку можно разделить на два вида:

  • Верстка компонентов
  • Верстка шаблона сайта

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

Шаблон сайта определяет внешний вид сайта. Включает в себя HTML и CSS для layout'а страницы (header, footer, колонки), оформление текста и другие элементы, общие для всех страниц сайта.

Контекст

Контексты продуктов и шаблонов сайтов

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

  • Стандарт HTML (HTML5, HTML4, XHTML)
  • Standard Mode и/или Quirks Mode
  • Гибкость верстки
  • Устойчивость к внешним стилям
  • Тестирование
  • Кросс-браузерность

Существует следующие контексты использования верстки:

  • Корпоративный портал. Данный продукт поставляется с двумя шаблонами дизайна - Bitrix24 и Light. Компоненты должны хорошо отображаться в обоих шаблонах. В сложных ситуациях отдавайте приоритет шаблону 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 и нашей документации, то возможна ситуация, когда "плывёт" дизайн.


Верстка для мобильных устройств
51 / 382

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.


Приёмы верстки
52 / 382

В этом разделе собраны типовые подходы к решению частых задач верстки.

Использование оптимальных изображений под разные браузеры и устройства

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 устройства. Также можно указывать разрешения экранов.


Простейший пример внедрения дизайна с табличной вёрсткой
53 / 382

Для повторения описанного примера, знакомящего с принципами внедрения шаблона дизайна в Bitrix Framework, рекомендуется использовать этот макет шаблона. Шаблон - тестовый, вопросы кроссбраузерности при его создании не рассматривались. Все работы выполнялись в Firefox.

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

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

При создании примера подразумевалось, что пользователь знаком с PHP, HTML и CSS. Поэтому детальное описание причин удалений или добавления того или иного кода опускается. Считаем, что вы сами способны понять эти моменты. Перед началом работы настойчиво рекомендуется познакомиться с понятием о шаблоне дизайна сайта в рамках Bitrix Framework.

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

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


Создание шаблона
54 / 382

Создание шаблона – не самый сложный шаг в интеграции дизайна, но требует внимательности.

Форма создания шаблона

  • Перейдите в раздел Настройки > Настройки продукта > Сайты > Шаблоны сайтов. В Рабочей области откроется форма Шаблоны сайтов.
  • Выполните команду Добавить шаблон на Контекстной панели. Откроется форма создания нового шаблона. В этой форме в поле:
    • ID - введите название шаблона. В нашем примере это будет test.
    • Название - введите название шаблона. (Может быть и на кириллице и на латинице), например, тоже test.
    • Описание - можно вставить ваш комментарий к шаблону.
    • Порядок - значение определяет порядок отображения шаблона в общем списке шаблонов.

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

Назначение шаблона для сайта

Назначьте созданный шаблон для тестового сайта.


Добавление кода тестового дизайна в шаблон
55 / 382

Служебная часть

Задание внешнего вида шаблона происходит в поле Внешний вид шаблона сайта.

Вставьте в поле следующий служебный код для шаблона:
  • Верхняя часть:
    <?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# нажав на кнопку с соответствующим названием . Он должен стоять строго перед меткой <!-- #Begin_Article -->.

Примечание: Найти тот или иной нужный участок кода на большой странице бывает достаточно сложно, но в исходном коде страницы нашего тестового шаблона расставлены специальные метки для облегчения поиска. Их общий вид:
<!-- #Begin******** -->
<!-- #End******** -->
где под звездочками будет стоять название функции, которую имитирует данный код. В нашем конкретном случае это будет название _Article. То есть метка в данном случае будут иметь вид:
<!-- #Begin_Article -->

Теперь можно сохранять созданный шаблон. До сих пор это было невозможно, так как был не задан разделитель #WORK_AREA#.


Добавление графики и стилей
56 / 382

Для полного и правильного отображения дизайна нужно добавить графику и стили.

Графика

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

  • Откройте папку, где расположен шаблон (/bitrix/templates/test).
  • Создайте в ней папку /images.

    Примечание: Поместить картинки можно и в корневой папке /images. Но размещение изображений по предлагаемому нами варианту имеет одно преимущество. При копировании (или экспорте) шаблона вам не придется заново импортировать картинки в новый шаблон. Копия создастся сразу с папкой с картинками.

  • Загрузите графические файлы из архива дизайна. Файлы размещены в папке Сайт\izo\

Теперь нам осталось только изменить пути до картинок в кодах шаблона. В исходном дизайне пути до картинок прописаны как izo/. Поменяйте их на bitrix/templates/test/images/.

Примечание: Ручная замена утомительна. Произведите такую замену в текстовом редакторе с функцией автоматической замены (Bred2, AkelPad и подобных, но не в MS Word).

  • Сохраните изменения.
  • Откройте сайт с помощью кнопки Предпросмотр.

Вы увидите, что теперь все картинки, кроме картинок в колонках Блоги и Галерея, а так же картинки в теле статьи «О прикорме», отображаются. При этом не прорисованы границы, и на сайте отображен текст (перечисление разделов), видео и картинки не предусмотренные в дизайне.

Картинки в колонках Блоги и Галерея, а так же картинка в теле статьи «О прикроме» не отображаются потому, что они не загружены на сайт. (Они были расположены не в папке izo.) На это можно не обращать внимание, так как все равно нам удалять участки кода с этими картинками и заменять их на компоненты «1С-Битрикс: Управление сайтом».

К ненужному тексту, видео и картинкам мы еще вернемся позже. (Кстати, появившийся «самостоятельно» текст может быть совсем не таким, как на нашей иллюстрации – это зависит от того какому сайту с каким содержанием вы применили шаблон.)

Стили

  • Откройте на редактирование шаблон сайта на закладке Стили шаблона.

    Вы увидите, что поле Файл стилей шаблона (template_styles.css) пустое, то есть не задано никаких стилей. Стили применяемые в проекте указаны в файле Сайт\izo\code.css.

  • Перенесите стили из файла в это поле.
  • Сохраните изменения.

Результат:


Интеграция дизайна в систему
57 / 382

Шаблон сайта на четверть создан. «На четверть» - это потому что в имеющемся у нас на данном моменте шаблоне есть только html-код, но отсутствуют функции вызова программных компонентов. А их интеграция – это существенный объем работы.

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

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

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

Интегрировать код любого компонента в код шаблона в Bitrix Framework можно следующими способами:

  • Вставкой кода из пользовательской документации;
  • С помощью визуального редактора напрямую (невозможно с версии 14.0, так как отключено использование редактора при редактировании шаблонов сайта).

Внедрение компонентов в дизайн можно, в принципе, производить в любом порядке. Мы начнем с компонента авторизации. Но сначала разберёмся с тем, что же такое Рабочая область.


#WORK_AREA# – Рабочая область
58 / 382

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

Поясним суть Рабочей области #WORK_AREA#. При создании шаблона мы загружали готовую нарезку дизайна, в которой предусмотрен текст в центральной части, колонка в правой части (фотогалерея), левая колонка с анонсами блогов. На данный момент все они: текст, левая и правая колонки входят что-то в Header, что-то в Footer. Если при формировании шаблона левая и правая колонки могут то включаться в Footer или Header, то исключаться из него, но основной текст всегда будет в #WORK_AREA#.

Если вы создавали сайт для тестов на установке демо-дистрибутива по способу описанному здесь, то на вашем сайте отобразятся именно те лишние элементы, которые мы называли ранее. Это происходит потому, что используется та же папка для сайта, что и в демо-версии. Соответственно, отображаются элементы уже размещённые на странице index.php.

  • Скопируйте в буфер обмена текст статьи, которая у нас сейчас видна на шаблоне. Только текст, без картинки!
  • С Публичной части откройте для редактирования индексную страницу сайта в визуальном редакторе. (Либо откройте для редактирования файл index.php в корне сайта)
  • Удалите из страницы все тексты и ярлыки компонентов. Кстати, этим действием вы удалили весь тот «непрошенный» текст, видео и картинки, которые появились у нас на сайте после вставки дизайна в шаблон.
  • В самом начале страницы введите текст: Тестовый проект. Главная страница.
  • В поле редактора вставьте текст из буфера обмена.
  • Сохраните внесенные изменения.

Вы увидите, что статья у нас теперь отражена дважды: один раз как часть #WORK_AREA# (то, что мы только что добавили), второй раз как часть Footer’а (текст, который стоял раньше):

Вот из Footer’а нам и надо текст убрать.

  • Удалите текст из кода шаблона сайта, вместе со строкой и ячейкой. Метки:
    <!-- #Begin_Article -->
<!-- #End_Article -->
  • Сохраните внесенные изменения.
  • Теперь при просмотре сайта с публички текст будет отображаться только один раз и он выводится из Рабочей области #WORK_AREA#.


Авторизация
59 / 382

Переход на страницу авторизации в «1С-Битрикс: Управление сайтом» можно реализовать как с помощью html, так и с помощью компонента авторизации. Мы опишем оба способа.

Авторизация с помощью HTML кода

За авторизацию в html-коде тестового дизайна отвечает код:

<img width="4" height="4" src="/bitrix/templates/test/images/punkt_top.gif" />
       <b><a class="text" href="#"><font size="1" color="#6e6e6e">Регистрация</font></a>
<img width="4" height="4" src="/bitrix/templates/test/images/punkt_top.gif" />
       <b><a class="text" href="#"><font size="1" color="#6e6e6e">Вход</font></a></b>

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

<!-- #Begin_Auth -->
<!-- #End_Auth -->

Чтобы реализовать авторизацию в html:

  • Замените знак # во втором теге <a class="text" href="#"> на ссылку на папку /auth.
  • Замените в первой ссылке знак # на /auth.php?register=yes
  • Сохраните внесенные изменения.

Авторизация встроена. Можете закончить сессию и вновь авторизоваться для проверки работы.

Компонент авторизации

Интеграция компонента авторизации не сложна, но требует внимания.

  • Удалите описанный выше html-код из общего кода шаблона. Метки:
    <!-- #Begin_Auth -->
<!-- #End_Auth -->
  • Откройте страницу компонента авторизации в пользовательской документации.
  • Скопируйте в буфер вызов кода компонента.
  • Вставьте код между указанными метками.
  • Настройте компонент:
    • У параметра 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" => "register.php",
      "FORGOT_PASSWORD_URL" => "",
     "PROFILE_URL" => "profile.php",
     "SHOW_ERRORS" => "Y"
     )
);?>
  • Сохраните внесенные изменения.

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

Какие расхождения мы видим?

  • Не соответствуют местами порядок ссылок. Сначала Войти, потом – Регистрация, вместо обратного, как в дизайне;
  • Само слово Войти не соответствует утвержденному Вход;
  • Не соответствуют шрифты и их форматирование в надписях;
  • Не соответствуют картинки.
  • всплывающее окно авторизации тоже «не вписывается» в дизайн.


Кастомизация шаблона компонента
60 / 382

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

Прежде всего, оговоримся, что не все неточности дизайна мы будем исправлять в этом примере. Порядок ссылок невозможно изменить, не модифицируя 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.

  • В диалоге с кодом шаблона найдите ссылки на картинки, используемые в шаблоне. Так как в теги картинки включен 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="<?=$templateFolder?>/images/punkt_top.gif" border="0" alt="">
    Третья:
    <input type="image" src="<?=$templateFolder?>/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;}
  • Сохраните внесенные изменения.

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

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


Меню сайта
61 / 382

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

  • Левое вертикальное, классическое;
  • Нижнее горизонтальное;
  • Верхнее горизонтальное – частичное.

Для выполнения работ вам нужно будет создать два типа меню, которых нет в дистрибутиве по умолчанию. Эти типы меню будут использоваться в примере. В примере их назвали podmenu и down.

Работу начнем с левого вертикального меню.


Левое меню
62 / 382

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

<!-- #Begin_Left_Menu -->
<!-- #End_Left_Menu -->

Добавление компонента

  • Скопируйте код, имитирующий меню в отдельный текстовый файлик. Он нам понадобится.
  • Удалите из кода шаблона код меню.
  • Откройте страницу документации с описанием компонента Меню (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-код, объясняющий системе, где искать название раздела и ссылку на него.
  • Структура шаблона

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

    Действие | код
    Проверка включения | <?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?>

    Просматривая структуру шаблона можно увидеть, как отображаются в 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;
  • Так как у нас по дизайну не предусмотрены другие стили оформления (активной ссылки, посещенной ссылки, подсветки ссылки), то все остальные стили просто удалите.
  • Сохраните внесенные изменения.

Результат работы:


Нижнее горизонтальное меню
63 / 382

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

Добавление компонента

  • Откройте для редактирования шаблон тестового сайта.
  • Скопируйте в отдельный текстовый файл а затем удалите код таблицы, расположенной между метками:
    <!-- #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">.
  • Сохраните внесенные изменения.

Кастомизация стилей

  • Откройте для редактирования стили шаблона компонента.
  • Отредактируйте файл стилей по аналогии с тем, как это делалось в левом меню.

Результат работы:


Верхнее частичное меню
64 / 382

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

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

  • Разместите код вызова компонента меню между метками:
    <!-- #Begin_part_menu -->
<!-- #End_part_menu -->>
  • Скопируйте шаблон компонента и файл css из шаблона нижнего меню;
  • Замените в шаблоне название картинки (punkt_top на punkt_red.)
  • Сохраните внесенные изменения.
  • Создайте меню.


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

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

Размещение компонента

  • Откройте для редактирования шаблон тестового сайта.
  • Найдите и удалите код, имитирующий цепочку навигации на шаблоне сайта, расположенную между
    <!-- #Begin_nav -->
<!-- #End_nav -->
  • Перенесите на место между метками код вызова компонента Навигационная цепочка (breadcrumb) из документации.

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

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

Примечание: последнюю ссылку можно выделять с помощью CSS, используя псевдокласс: last-child.

  • Скопируйте шаблон Цепочки навигации.
  • Откройте для редактирования файл CSS.
  • Используя за основу файл CSS левого меню, отредактируйте отображение Цепочки навигации.


Компонент Новые сообщения блогов
66 / 382

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

Размещение компонента

  • Откройте для редактирования шаблон тестового сайта.
  • Найдите и удалите код, имитирующий вывод новых сообщений блога, расположенный между:
    <!-- #Begin_blog -->
<!-- #End_blog -->
  • Разместите между метками код вызова компонента Новые сообщения (blog.new_posts)
  • Сохраните внесенные изменения.

Настройка компонента

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

  • Перейдите в Панель управления на страницу Сервисы > Блоги > Группы блогов. Откроется форма Группы блогов.
  • Переопределите какую-нибудь группу блогов на ваш тестовый сайт.
  • Сохраните внесенные изменения.
  • Перейдите в Панель управления на страницу Сервисы > Блоги > Блоги. Откроется список блогов, имеющихся в дистрибутиве.
  • Привяжите один или несколько блогов к выбранной выше группе.

Перейдя в публичку, вы увидите, что блоги появились:

  • Вызовите диалог настройки свойств компонента.
  • В поле Шаблон пути к странице с сообщением блога группы необходимо настроить путь до страницы с сообщениями. Если используется дистрибутив по умолчанию, то путь будет вида:/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 с целью формирования картинки в нужном размере.

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


Счетчик посещений
67 / 382

Для любого проекта – посещаемость очень важна. Посещения страниц регистрируются счетчиками. Счетчики можно установить внешние, скажем, от Yandex’а. А можно поставить счетчик внутренний, системный.

Примечание: Если быть точным, то системный счетчик устанавливать не надо. Он начинает работу сразу по инсталляции 1С-Битрикс: Управление сайтом на ваш компьютер и всю информацию по посещениям вы можете посмотреть в разделе Веб-аналитика Административного раздела (Аналитика > Сводная статистика). А вот вывести данные счетчика для просмотра посетителями сайта можно, если установить соответствующий компонент.

  • Откройте для редактирования шаблон тестового сайта.
  • Скопируйте код вызова компонента Таблица статистики из документации.
  • Разместите его сразу за меткой <!-- #End_blog -->
  • Сохраните внесенные изменения.

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

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


Баннер
68 / 382

Для установки баннера в 1С-Битрикс: Управление сайтом предусмотрен специальный компонент, который так и называется Баннер. Место для него в шаблоне тегами предусмотрено, хотя зрительно в «подвале» оно не видно. Ячейка для баннера расположена между ячейками с копирайтом автора дизайна и ячейкой с логотипом клуба.

  • Откройте для редактирования шаблон тестового сайта.
  • После метки <!-- #Begin_banner --> введите любой символ, например число 123. Это нужно для того, чтобы стал виден компонент. Ненастроенный компонент не выводит баннер и поэтому он не будет отображаться при просмотре сайта с публички.
  • Скопируйте из документации код вызова компонента Баннер и разместите между метками:
    <!-- #Begin_banner -->
<!-- #End_banner -->

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


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

Правая колонка

Тестовый шаблон рисовался в тот момент, когда в 1С-Битрикс: Управление сайтом не было показа фотогалереи во всплывающем окне. Предложенный дизайнером на тот момент вариант сегодня - не лучший вариант. Кроме того, использование правой колонки можно не ограничивать фотографиями. Ведь в разных разделах сайта может потребоваться вывод разных данных в этой колонке. Если такая потребность возникнет, то придётся создавать новый шаблон с новым компонентом (и соответственно впоследствии поддерживать его). Мы поступим по-другому. Создадим в этом месте Включаемую область и при необходимости будем наполнять её нужным контентом в нужном разделе.

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

  • Откройте для редактирования шаблон сайта.
  • Удалите коды, имитирующие фотогалерею в правой таблице, расположенные между
    <!-- #Begin_right_Column -->
<!-- #End_right_Column -->
  • Добавьте в это место код вызова компонента Включаемая область из документации.
  • Сохраните внесенные изменения.
  • Скопируйте и откройте для редактирования шаблон области.
  • Из исходного кода дизайна возьмите код заголовка области между метками
    <!-- #Begin_right_Column -->
<!-- #Begin_Foto -->
  • Сохраните внесенные изменения.

Теперь правая колонка у нас выглядит так:

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

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

  • Включаемая область - это файл с именем sect_inc.php, расположенный в соответствующей директории сайта. В нашем случае - в корне сайта. Откройте этот файл для редактирования.
  • Перенесите в этот файл код вызова компонента Список фото (со слайдером) (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 и разные шаблоны
70 / 382

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)
71 / 382

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

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

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

Скачать тестовые материалы.

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

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

Подразумевается, что пользователь знаком с PHP, HTML и CSS, поэтому детальное описание причин удаления или добавления того или иного кода опускается. Считаем, что Вы сами способны понять эти моменты. Перед началом работы настойчиво рекомендуем ознакомиться с понятием шаблона дизайна сайта в рамках Bitrix Framework.

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

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


Подготовка к работе
72 / 382

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

Скачать тестовые материалы.

В архиве макета сайта содержатся:

  • папки со стилями шаблона
  • папка с js-файлами
  • папка с картинками
  • html-макет индексной страницы сайта (index.html)
  • файл с главными стилями шаблона (styles.css)

Разархивируйте скачанный архив с тестовыми материалами (doc.zip).

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

Просмотр видео в наших курсах в формате CHM невозможен. Ролик можно просмотреть на сайте компании «1С-Битрикс».

Это дизайн главной страницы сайта-лендинга. Она состоит из основной заставки с названием сайта, навигации по странице (верхнее горизонтальное меню, кнопки РЫБЫ.НЕТ и Узнать больше), фотогалереи, блока новостей, контактной информации и копирайта.

Пропишем требования, которым должен соответствовать сайт с внедрённым шаблоном.

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

  • Основная заставка и навигация должны располагаться в шапке сайта (header).
  • Копирайт должен располагаться в подвале сайта (footer).
  • Новости сайта нужно выводить из инфоблока новостей с помощью компонентов. Для страницы детального вывода новости создать упрощенный шаблон.
  • Остальную информацию считать статической и разместить на индексной странице (тогда контент-менеджер сможет её изменять в публичном разделе, включив режим Правки).
  • В качестве дополнительного задания: создать нижнее меню.


Разделение шаблона на header, footer и work_area
73 / 382

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

  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.


Шаблон сайта (создание папки шаблона)
74 / 382

Создание шаблона – первый шаг в интеграции дизайна. Рассмотрим подробнее на примере, как это сделать.

Добавление кода шаблона / создание папки шаблона

  • Откроем исходный код страницы index.html, выделим весь код и скопируем его в буфер обмена.
  • На сайте перейдём в раздел Настройки > Настройки продукта > Сайты > Шаблоны сайтов. Кликнем по кнопке Добавить шаблон, и в Рабочей области откроется форма Шаблоны сайтов.
  • Выполним команду Добавить шаблон на Контекстной панели. В открывшейся форме создания нового шаблона укажем идентификатор шаблона и остальные параметры (описание понятно интуитивно).
  • Вставим скопированный код в поле Внешний вид шаблона сайта.
  • Теперь нужно определить шапку сайта (header) и подвал (footer) - это те области, которые будут неизменны на каждой странице сайта.

    В нашем примере шапка сайта будет содержать основную заставку с названием сайта и навигацию по странице (верхнее горизонтальное меню, кнопки РЫБЫ.НЕТ и Узнать больше) - т.е. заканчиваться на теге </header>. Подвал сайта будет содержать только копирайт, т.е. начинаться с блока <!-- Footer -->.

    Выделим и удалим весь код между шапкой (header) и подвалом сайта (footer).

    Таким образом, мы удалили весь код (контент сайта) между тегом </header> и блоком <!-- Footer -->. Вместо удаленного кода пропишем #WORK_AREA#:

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

После выполнения вышеперечисленных действий будет создана папка этого шаблона, включающая отдельные файлы шапки сайта (header.php), подвала сайта (footer.php) и файлы с автоматически подключаемыми скриптами и стилями (Контент > Структура сайта > Файлы и папки > bitrix > templates):

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

Загрузка файлов шаблона

Загрузите в папку созданного шаблона следующие папки из архива (стили шаблона, картинки и js-файлы):

  • css
  • img
  • js
  • vendor

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

Примечание: вместо папки bitrix можно использовать папку local (подробнее в уроке ).


Шаблон сайта (настройка и подключение)
75 / 382

Ранее мы создали папку шаблона сайта. Однако для корректного отображения шаблона на сайте нужно еще адаптировать вёрстку, подключив все стили и 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>

  • Во все строках, где есть обращение к файлам, добавим в начале пути константу <?=SITE_TEMPLATE_PATH?> и слеш /.

    Рассмотрим на примере блока <!-- 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 с комментариями

<? 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'>

  <!-- Plugin CSS -->
    <link href="<?=SITE_TEMPLATE_PATH?>/vendor/magnific-popup/magnific-popup.css" rel="stylesheet">

  <!-- Theme CSS - Includes Bootstrap -->
    <link href="<?=SITE_TEMPLATE_PATH?>/css/creative.min.css" rel="stylesheet">

</head>

<body id="page-top">

<div id="panel">
    <? $APPLICATION->ShowPanel(); ?> <!-- Отображение административной панели внизу страницы -->
</div>

  <!-- Navigation -->
  <nav class="navbar navbar-expand-lg navbar-light fixed-top py-3" id="mainNav">
    <div class="container">
      <a class="navbar-brand js-scroll-trigger" href="#page-top">РЫБЫ.НЕТ</a>
      <button class="navbar-toggler navbar-toggler-right" type="button" data-toggle="collapse" data-target="#navbarResponsive" aria-controls="navbarResponsive" aria-expanded="false" aria-label="Toggle navigation">
        <span class="navbar-toggler-icon"></span>
      </button>
      <div class="collapse navbar-collapse" id="navbarResponsive">
        <ul class="navbar-nav ml-auto my-2 my-lg-0">
          <li class="nav-item">
            <a class="nav-link js-scroll-trigger" href="#home">Главная</a> <!-- перемещение по клику к блоку About Section -->
          </li>
          <li class="nav-item">
            <a class="nav-link js-scroll-trigger" href="#news">Новости</a> <!-- перемещение по клику к блоку Новости -->
          </li>
          <li class="nav-item">
            <a class="nav-link js-scroll-trigger" href="#portfolio">Галерея</a> <!-- перемещение по клику к блоку Галерея -->
          </li>
          <li class="nav-item">
            <a class="nav-link js-scroll-trigger" href="#contact">Контакты</a> <!-- перемещение по клику к блоку Контакты -->
          </li>
        </ul>
      </div>
   </div>
  </nav>

  <!-- Masthead -->
  <header class="masthead">
    <div class="container h-100">
      <div class="row h-100 align-items-center justify-content-center text-center">
        <div class="col-lg-10 align-self-end">
          <h1 class="text-uppercase text-white font-weight-bold">Рыбный совхоз "Корпач"</h1>
          <hr class="divider my-4">
        </div>
        <div class="col-lg-8 align-self-baseline">
          <p class="text-white-75 font-weight-light mb-5">Одно из лучших мест для рыбалки</p>
          <a class="btn btn-primary btn-xl js-scroll-trigger" href="#home">Узнать больше</a><!-- Перемещение по клику к блоку УЗНАТЬ БОЛЬШЕ -->
        </div>
      </div>
    </div>
  </header>

Примечание: css-стили в шапке сайта можно подключить с помощью метода Asset::addCss() нового ядра D7.

Пример файла header.php с комментариями

<?
if (!defined('B_PROLOG_INCLUDED') || B_PROLOG_INCLUDED !== true) die(); //Защита от подключения файла напрямую без подключения ядра
use Bitrix\Main\Page\Asset; //Подключение библиотеки для использования  Asset::getInstance()->addCss()
global $USER;
?>
<!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> <!-- Отображение заголовка страницы -->
    <?
    Asset::getInstance()->addJs(SITE_TEMPLATE_PATH . "/vendor/jquery/jquery.min.js");
$APPLICATION->ShowHead();
    Asset::getInstance()->addCss(SITE_TEMPLATE_PATH . "/vendor/fontawesome-free/css/all.min.css");
    Asset::getInstance()->addCss("https://fonts.googleapis.com/css?family=Merriweather+Sans:400,700&selection.subset=cyrillic");
    Asset::getInstance()->addCss("https://fonts.googleapis.com/css?family=Merriweather:300,300i,400,400i,700,700i&display=swap&subset=cyrillic");
    Asset::getInstance()->addCss(SITE_TEMPLATE_PATH . "/vendor/magnific-popup/magnific-popup.css");
?>

</head>

<body id="page-top">

<div id="panel">
    <? $APPLICATION->ShowPanel(); ?> <!-- Отображение панели администратора -->
</div>

  <!-- Navigation -->
  <nav class="navbar navbar-expand-lg navbar-light fixed-top py-3" id="mainNav">
    <div class="container">
      <a class="navbar-brand js-scroll-trigger" href="#page-top">РЫБЫ.НЕТ</a>
      <button class="navbar-toggler navbar-toggler-right" type="button" data-toggle="collapse" data-target="#navbarResponsive" aria-controls="navbarResponsive" aria-expanded="false" aria-label="Toggle navigation">
        <span class="navbar-toggler-icon"></span>
      </button>
      <div class="collapse navbar-collapse" id="navbarResponsive">
        <ul class="navbar-nav ml-auto my-2 my-lg-0">
          <li class="nav-item">
            <a class="nav-link js-scroll-trigger" href="#home">Главная</a> <!-- перемещение по клику к блоку About Section -->
          </li>
          <li class="nav-item">
            <a class="nav-link js-scroll-trigger" href="#news">Новости</a> <!-- перемещение по клику к блоку Новости -->
          </li>
          <li class="nav-item">
            <a class="nav-link js-scroll-trigger" href="#portfolio">Галерея</a> <!-- перемещение по клику к блоку Галерея -->
          </li>
          <li class="nav-item">
            <a class="nav-link js-scroll-trigger" href="#contact">Контакты</a> <!-- перемещение по клику к блоку Контакты -->
          </li>
       </ul>
      </div>
    </div>
 </nav>

  <!-- Masthead -->
 <header class="masthead">
    <div class="container h-100">
      <div class="row h-100 align-items-center justify-content-center text-center">
        <div class="col-lg-10 align-self-end">
          <h1 class="text-uppercase text-white font-weight-bold">Рыбный совхоз "Корпач"</h1>
          <hr class="divider my-4">
        </div>
        <div class="col-lg-8 align-self-baseline">
          <p class="text-white-75 font-weight-light mb-5">Одно из лучших мест для рыбалки</p>
          <a class="btn btn-primary btn-xl js-scroll-trigger" href="#home">Узнать больше</a><!-- Перемещение по клику к блоку УЗНАТЬ БОЛЬШЕ -->
        </div>
      </div>
    </div>
  </header>

2. Редактирование подвала сайта (footer.php)

Подвал сайта почти не нуждается в изменениях . Необходимо только в строках, обращающихся к файлам, добавить в начале пути константу <?=SITE_TEMPLATE_PATH?> со слешем / и сохранить изменения.

Пример отредактированного файла footer.php

<!-- Footer -->
  <footer class="bg-light py-5">
    <div class="container">
      <div class="small text-center text-muted">Copyright © 2019 - Start Bootstrap<br/>Copyright © Blackrock Digital LLC. Code released under the MIT license.</div>
    </div>
   </div>
  </footer>

  <!-- 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>

  <!-- Plugin JavaScript -->
  <script src="<?=SITE_TEMPLATE_PATH?>/vendor/jquery-easing/jquery.easing.min.js"></script>
  <script src="<?=SITE_TEMPLATE_PATH?>/vendor/magnific-popup/jquery.magnific-popup.min.js"></script>

  <!-- Custom scripts for this template -->
  <script src="<?=SITE_TEMPLATE_PATH?>/js/creative.min.js"></script>

</body>

</html>

3. Подключение шапки и подвала (index.php), добавление контента

  • Необходимо подключить на странице вывод файлов header.php и footer.php.

    Для этого в файле главной страницы сайта index.php пропишем следующие конструкции:

    • первая строка: <?require($_SERVER["DOCUMENT_ROOT"]."/bitrix/header.php");?>
    • последняя строка: <?require($_SERVER["DOCUMENT_ROOT"]."/bitrix/footer.php");?>
  • Добавим статический контент.

    Откроем файл с подготовленной вёрсткой (index.html) и скопируем код между шапкой (header) и подвалом сайта (footer) - тот, который мы в пункте 1 заменяли на #WORK_AREA#, то есть контент сайта.

    Настроим пути к картинкам. Пропишем в существующих путях уже знакомую константу <?=SITE_TEMPLATE_PATH?>/.

Пример отредактированного файла index.php с комментариями

<?require($_SERVER["DOCUMENT_ROOT"]."/bitrix/header.php");?>
<body id="page-top">

  <!-- About Section -->
  <section class="page-section bg-primary" id="home">
    <div class="container">
      <div class="row justify-content-center">
        <div class="col-lg-8 text-center">
          <h2 class="text-white mt-0">О прикорме</h2>
          <hr class="divider light my-4">
          <p class="text-white-50 mb-4">Споры о том, на что ловить и чем лучше прикармливать, наверное, никогда не закончатся. Одни предпочи-тают новомодные прикор-мки, изготовленные фирма-ми, специализи-рующимися на их производ-стве; другие ни за что не променяют кастрюлю с душистым варевом, над которой они колдовали не один час, словно баба-яга над зельем. Но речь не об этом. Я не буду давать советы и рецепты, а выскажусь по поводу некоторых заблуждений. В большинстве статей пишут, что рыбу можно перекормить большим количеством прикормки. Но никто не пишет конкретных цифр - сколько "много", а сколько "мало" или "достаточно". Потому что никто не знает и никогда не узнает, сколько же это "много". Для одного и килограмм - уже много, а для другого и ведра кажется мало. Но причина не в этом. Давайте на проблему посмотрим с другой стороны.

        </div>
      </div>
    </div>
  </section>

 <!-- Services Section -->
  <section class="page-section">
    <div class="container">
      <h2 class="text-center mt-0">Почему мы?</h2>
      <hr class="divider my-4">
    </div>
  </section>

  <!-- Portfolio Section -->
  <section id="portfolio">
    <div class="container-fluid p-0">
      <div class="row no-gutters">
        <div class="col-lg-4 col-sm-6">
            <a class="portfolio-box" href="<?=SITE_TEMPLATE_PATH?>/img/1.jpg">
            <img class="img-fluid" src="<?=SITE_TEMPLATE_PATH?>/img/1.jpg" alt="">
            <div class="portfolio-box-caption">
              <div class="project-name">Прекрасные пейзажи</div>
            </div>
          </a>
        </div>
        <div class="col-lg-4 col-sm-6">
          <a class="portfolio-box" href="<?=SITE_TEMPLATE_PATH?>/img/2.jpg">
           <img class="img-fluid" src="<?=SITE_TEMPLATE_PATH?>/img/2.jpg" alt="">
            <div class="portfolio-box-caption">
             <div class="project-name">Крупная рыба</div>
            </div>
         </a>
        </div>
       <div class="col-lg-4 col-sm-6">
          <a class="portfolio-box" href="<?=SITE_TEMPLATE_PATH?>/img/3.jpg">
            <img class="img-fluid" src="<?=SITE_TEMPLATE_PATH?>/img/3.jpg" alt="">
            <div class="portfolio-box-caption">
              <div class="project-name">Тихие места</div>
            </div>
          </a>
        </div>
        <div class="col-lg-4 col-sm-6">
          <a class="portfolio-box" href="<?=SITE_TEMPLATE_PATH?>/img/4.jpg">
            <img class="img-fluid" src="<?=SITE_TEMPLATE_PATH?>/img/4.jpg" alt="">
            <div class="portfolio-box-caption">
              <div class="project-name">Дзен</div>
            </div>
         </a>
        </div>
        <div class="col-lg-4 col-sm-6">
          <a class="portfolio-box" href="<?=SITE_TEMPLATE_PATH?>/img/5.jpg">
            <img class="img-fluid" src="<?=SITE_TEMPLATE_PATH?>/img/5.jpg" alt="">
            <div class="portfolio-box-caption">
              <div class="project-name">Оснащение плавсредствами</div>
            </div>
          </a>
        </div>
        <div class="col-lg-4 col-sm-6">
          <a class="portfolio-box" href="<?=SITE_TEMPLATE_PATH?>/img/6.jpg">
            <img class="img-fluid" src="<?=SITE_TEMPLATE_PATH?>/img/6.jpg" alt="">
            <div class="portfolio-box-caption p-3">
             <div class="project-name">Хорошее настроение</div>
            </div>
           </a>
        </div>
      </div>
    </div>
  </section>

  <!-- Call to Action Section -->
  <section class="page-section bg-dark text-white" id="news">
    <div class="container text-center">
      <h2 class="mb-0">Новости</h2>
         <hr class="divider my-4">
    </div>
    <div class="container text-center">
        <div class="row justify-content-center">
            <div class="col-lg-4 text-center">
                <div class="card bg-secondary border border-dark">
                    <img class="card-img-top" src="<?=SITE_TEMPLATE_PATH?>/img/1.jpg" alt="News image cap">
                    <div class="card-body ">
                    <h5 class="card-title">News title
                        <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>
            <div class="col-lg-4 text-center">
                <div class="card bg-secondary border border-dark">
                    <img class="card-img-top" src="<?=SITE_TEMPLATE_PATH?>/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>
            <div class="col-lg-4 text-center">
                <div class="card bg-secondary border border-dark">
                    <img class="card-img-top" src="<?=SITE_TEMPLATE_PATH?>/img/3.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>
        </div>
    </div>

  </section>

  <!-- Contact Section -->
  <section class="page-section" id="contact">
    <div class="container">
      <div class="row justify-content-center">
        <div class="col-lg-8 text-center">
          <h2 class="mt-0">Связаться с нами</h2>
            <hr class="divider my-4">
       </div>
      </div>
      <div class="row">
        <div class="col-lg-4 ml-auto text-center mb-5 mb-lg-0">
          <i class="fas fa-phone fa-3x mb-3 text-muted"></i>
          <div>+1 (202) 555-0149</div>
        </div>
        <div class="col-lg-4 mr-auto text-center">
          <i class="fas fa-envelope fa-3x mb-3 text-muted"></i>
          <!-- Make sure to change the email address in anchor text AND the link below! -->
          <a class="d-block" href="mailto:contact@yourwebsite.com">contact@yourwebsite.com</a>
        </div>
      </div>
    </div>
  </section>

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

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

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

Примечание: Всегда можно изменить шапку или подвал сайта, перенеся требуемую часть кода из индексной страницы в файл header.php или footer.php.

Пример: перенос блока "Связаться с нами" в footer.php
Пример:

Перенесём блок Связаться с нами:

  • Откроем индексную страницу сайта index.php, где сейчас располагается этот блок, скопируем его код в буфер обмена и удалим из этого файла;
  • Откроем файл footer.php и вставим в самом начале скопированный код.
  <!-- Contact Section -->
  <section class="page-section" id="contact">
    <div class="container">
      <div class="row justify-content-center">
        <div class="col-lg-8 text-center">
          <h2 class="mt-0">Связаться с нами</h2>
            <hr class="divider my-4">
       </div>
      </div>
      <div class="row">
        <div class="col-lg-4 ml-auto text-center mb-5 mb-lg-0">
          <i class="fas fa-phone fa-3x mb-3 text-muted"></i>
          <div>+1 (202) 555-0149</div>
        </div>
        <div class="col-lg-4 mr-auto text-center">
          <i class="fas fa-envelope fa-3x mb-3 text-muted"></i>
          <!-- Make sure to change the email address in anchor text AND the link below! -->
          <a class="d-block" href="mailto:contact@yourwebsite.com">contact@yourwebsite.com</a>
        </div>
      </div>
    </div>
  </section>

<!-- Footer -->
  <footer class="bg-light py-5">
    <div class="container">
      <div class="small text-center text-muted">Copyright © 2019 - Start Bootstrap<br/>Copyright © Blackrock Digital LLC. Code released under the MIT license.
    </div>
    </div>
  </footer>

  <!-- 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>

  <!-- Plugin JavaScript -->
  <script src="<?=SITE_TEMPLATE_PATH?>/vendor/jquery-easing/jquery.easing.min.js"></script>
  <script src="<?=SITE_TEMPLATE_PATH?>/vendor/magnific-popup/jquery.magnific-popup.min.js"></script>

  <!-- Custom scripts for this template -->
  <script src="<?=SITE_TEMPLATE_PATH?>/js/creative.min.js"></script>

</body>

</html>

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


Список новостей: кастомизация шаблона компонента
76 / 382

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

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

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

  • Список новостей (news.list) - его разместим на главной странице сайта для вывода анонсов новостей;
  • Новость детально (news.detail) - для вывода детальной новости нужно будет создать новую страницу со своим шаблоном.

В этом уроке изменим дизайн компонента Список новостей. Рассмотрим пошагово:

1. Копирование шаблона компонента

Скопируйте шаблон компонента Список новостей. Это можно сделать двумя способами:

  • В рамках файловой системы копированием папки Контент > Структура сайта > Файлы и папки > bitrix > templates > [ваш шаблон] > components > bitrix > [название компонента] > [название шаблона компонента].
  • Средствами интерфейса системы, разместив на странице компонент и скопировав его с помощью команды Копировать шаблон компонента (при включённом режиме Правка). Копировать лучше в текущий шаблон сайта.

В данном примере скопирован шаблон 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

    <?if(!defined("B_PROLOG_INCLUDED") || B_PROLOG_INCLUDED!==true)die();
$this->setFrameMode(true);
?>

<!-- Call to Action Section -->
<section class="page-section bg-dark text-white" id="news">
    <div class="container text-center">
        <h2 class="mb-0">Новости</h2>
        <hr class="divider my-4">
    </div>
    <div class="container text-center">
        <div class="row justify-content-center">
            <? 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')));
                ?>
            <div class="col-lg-4 text-center">
                <div class="card bg-secondary border border-dark">
                            <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; ?>
                                <a href="<? echo $arItem["DETAIL_PAGE_URL"] ?>" class="btn btn-primary">Подробнее</a>
                            </div>
                </div>
            </div>
            <? endforeach; ?>
        </div>
    </div>
</section>


Детальный просмотр новости: шаблон страницы и компонента
77 / 382

В предыдущем уроке мы настроили вывод списка новостей, а в этом уроке займемся страницей детального просмотра новости:

  • создадим шаблон страницы;
  • разместим и кастомизируем компонент Новость детально (news.detail) .

1. Создание шаблона страниц для вывода детальной информации

Шаблон главной страницы уже подготовлен. Теперь нужно подготовить шаблон страницы вывода детальной информации.

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

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

В нашем примере сделаем шаблон для страниц с подробной информацией минималистичным, оставив только копирайт в подвале сайта. Поэтому файл 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. Применение шаблона к странице

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

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

3. Размещение и настройка компонента

  • На созданной странице разместим и настроим компонент Новость детально (news.detail).
  • Теперь на главной странице сайта откроем настройки размещенного ранее компонента Список новостей. В разделе Шаблоны ссылок пропишем URL страницы детального просмотра.

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

4. Кастомизация компонента

  • Скопируем шаблон компонента Новость детально по аналогии с уроком.
  • В папке скопированного шаблона компонента news.detail откроем в режиме редактирования файл template.php.
  • Оформим детальный показ новости в том же стиле, что и показ анонса новостей. Для этого в файле исходной вёрстки страницы index.html скопируем часть кода, отвечающую за стиль блока Новости.
  • После первой строки <? if (!defined('B_PROLOG_INCLUDED') || B_PROLOG_INCLUDED !== true) die(); ?> (защита от подключения файла напрямую без подключения ядра) вставим скопированный ранее код вёрстки. Не забудьте поставить закрывающий тег </section>.
  • В коде шаблона компонента, который скопировали, оставим только те части, которые отвечают за вывод картинки и текста.

    • Итоговый код кастомизированного шаблона компонента news.detail

    <?if(!defined("B_PROLOG_INCLUDED") || B_PROLOG_INCLUDED!==true)die();
$this->setFrameMode(true);
?>

<section class="page-section bg-dark text-white" id="news">
    <div class="container text-center">
        <h2 class="mb-0">Подробнее о новости</h2>
        <hr class="divider my-4">
    </div>

<div class="news-detail">
    <?if($arParams["DISPLAY_PICTURE"]!="N" && is_array($arResult["DETAIL_PICTURE"])):?>
        <img
            class="detail_picture"
            border="0"
            src="<?=$arResult["DETAIL_PICTURE"]["SRC"]?>"
            width="<?=$arResult["DETAIL_PICTURE"]["WIDTH"]?>"
            height="<?=$arResult["DETAIL_PICTURE"]["HEIGHT"]?>"
            alt="<?=$arResult["DETAIL_PICTURE"]["ALT"]?>"
            title="<?=$arResult["DETAIL_PICTURE"]["TITLE"]?>"
            />
    <?endif?>
    <?if($arParams["DISPLAY_NAME"]!="N" && $arResult["NAME"]):?>
        <h3><?=$arResult["NAME"]?></h3>
    <?endif;?>
    <?if($arResult["NAV_RESULT"]):?>
        <?if($arParams["DISPLAY_TOP_PAGER"]):?><?=$arResult["NAV_STRING"]?><br /><?endif;?>
        <?echo $arResult["NAV_TEXT"];?>
        <?if($arParams["DISPLAY_BOTTOM_PAGER"]):?><br /><?=$arResult["NAV_STRING"]?><?endif;?>
    <?elseif(strlen($arResult["DETAIL_TEXT"])>0):?>
        <?echo $arResult["DETAIL_TEXT"];?>
    <?else:?>
        <?echo $arResult["PREVIEW_TEXT"];?>
    <?endif?>
        <div style="clear:both"></div><br />
</div>

</section>

Теперь осталось в настройках компонента news.detail установить подготовленный нами шаблон и проверить результат.


Меню: кастомизация шаблона компонента
78 / 382

До этого момента навигация на нашем тестовом сайте-лендинге осуществлялась с помощью html-якорей, что довольно удобно для переходов на "длинной" странице.

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

В этом уроке разберём создание статического нижнего меню, выводящегося компонентом Меню (понятия "нижнее", "верхнее" - условны, и влияют только на названия файлов и отображение при создании нового пункта; т.е. рассмотренное в данном уроке меню можно расположить в любой части сайта: и в шапке, и в подвале).

Итак, для создания меню выполним следующие шаги:

1. Кастомизация шаблона компонента Меню

  • Скопируйте шаблон компонента Меню. Это можно сделать двумя способами:
    • В рамках файловой системы копированием папки Контент > Структура сайта > Файлы и папки > bitrix > templates > [ваш шаблон] > components > bitrix > [название компонента] > [название шаблона компонента].
    • Средствами интерфейса системы, разместив на странице компонент и скопировав его с помощью команды Копировать шаблон компонента (при включённом режиме Правка).
  • Задайте скопированному шаблону новое название (переименуйте папку шаблона). Например, 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 - выравнивание блока по центру по оси ОХ.

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

  • Кроме того, у нас нет необходимости в специализированных стилях и картинках исходного шаблона компонента Меню. Поэтому можно удалить из папки нового шаблона файлы style.css, style.min.css и папку images.

2. Размещение компонента Меню в подвале сайта.

  • Скопируем код вызова компонента. Это можно сделать следующими способами:
    • скопировать вызов из документации;
    • разместить на странице компонент Меню, и в совмещенном режиме просмотра скопировать код вызова.
  • Добавим скопированный код вызова в файл footer.php (Контент > Структура сайта > Файлы и папки > bitrix > templates > [шаблон вашего сайта] ):

    В кавычках указывается название используемого шаблона компонента. В нашем случае bottom_menu.

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

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


Управление служебными данными шаблона
79 / 382

Кодировка страниц и формат отображения дат

Bitrix Framework поддерживает кодировку UTF-8, в которой на одной странице могут сочетаться различные языки – от русского до иероглифов, что позволяет не заботиться о кодировке страниц. Рекомендуем вам разработку сайтов именно в кодировке UTF-8, что позволит вам избежать проблем с настройками однобайтных кодировок.

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

Управление метаданными

Основной целью использования метаданных является оптимизация сайта для поисковых систем. Поисковые системы используют метаданные для индексации документов сайта.

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

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

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

Одним из самых важных элементов сайта на сегодняшний день с позиции SEO-продвижения является заголовок окна браузера (тэг <title>). Практически повсеместно требуется, чтобы заголовок окна браузера отличался от заголовка страницы (тэг <H1>). В «1C-Битрикс» включена поддержка этого требования.

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


Теория. Управление кодировкой страниц
80 / 382

Одной из важных особенностей 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 >


Теория. Управление заголовком документа
81 / 382

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

Управление

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

Примечание: Установка дополнительного заголовка окна веб-браузера осуществляется с помощью зарезервированного в продукте свойства 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() или задаваться в публичной части сайта. Подробнее смотрите на странице Примеры работы.

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


Примеры работы
82 / 382

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

В результате мы должны получить примерно следующий результат:

Выполним следующее:

  • Зададим заголовок страницы из интерфейса или с помощью функции $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"); ?>


Теория. Управление стилями
83 / 382

Таблицы стилей представляют собой совокупность правил, применяемых для оформления определенных элементов на страницах сайта. Технология каскадных стилей (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) выполняется на странице редактирования шаблона дизайна (Настройки > Настройки продукта > Сайты > Шаблоны сайтов) на закладке Стили сайта. Важным элементом при создании таблицы стилей страниц является создание названий стилей. Названия следует создавать только для тех стилей, которые планируется использовать при редактировании страниц в режиме визуального HTML-редактора (секция Описания стилей).

Стили будут доступны в визуальном редакторе из выпадающего списка под именами, определенными в данной форме. Заданные здесь названия будут храниться в файле идентификатор_шаблона>/.styles.php (файл с именами стилей).

Создание таблицы стилей шаблона дизайна (файл template_styles.css) выполняется на закладке Стили шаблона формы редактирования шаблона сайта

Настройка таблицы стилей для шаблона сайта

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

  • Работа со стилями в битрикс (статья)


Механизм реализации
84 / 382

Таблицы стилей подключаются к шаблону сайта в области пролога с помощью функции 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-редакторе
85 / 382

Для визуального редактора присутствует возможность к уже имеющимся стандартным стилям добавлять собственные и помещать их в соответствующую панель редактора. Чтобы стили отображались в меню, файл 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". Пока что собственные стили можно помещать только в уже существующие категории.

Теперь созданный нами стиль отобразится в меню стилей.

Теперь созданный стиль можно применять из панели стилей.


Пример таблицы стилей для шаблона сайта
86 / 382

Стили для левого меню:

.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;}


Теория. Управление значениями метаданных
87 / 382

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

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

Управление значениями метаданных через визуальный интерфейс

Чтобы иметь возможность управлять значениями метаданных, предварительно необходимо создать соответствующие свойства в настройках модуля Управление структурой (Настройки > Настройки продукта > Настройки модулей >Управление структурой):

Важно! Названия типов свойств, используемых для управления метаданными страниц, должны совпадать с названиями мета-тегов в языке 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 для элементов каталога и динамически подставлять их в код страницы.

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

  • Свойства страниц и мета-теги


Интеграция компонентов
88 / 382

Интеграция компонентов

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

Антон Долганин:

Я для себя выделил примерно так градацию создания магазинов (не учитывая супер-пупер уникальные):

  • 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-код шаблона.

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

  • Пользовательская документация с описанием компонентов.
  • Подключение компонентов в курсе Контент-менеджер.


Теория. Что такое компонент
89 / 382

Компоненты

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

Роман Петров: Начиная работать с 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С-Битрикс: Управление сайтом. Если встретился такой анахронизм, обратитесь к документации.


Теория. Простые и комплексные компоненты
90 / 382
Цитатник веб-разработчиков.

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

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

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

Простые компоненты

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

Комплексные компоненты

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

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

Комплексные компоненты разрешают следующие проблемы:

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

Алгоритм работы комплексного компонента таков:

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

Пример

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

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

Чтобы задать ссылку на страницу детальной новости, нужно задать путь к этой странице, а так же название параметра, в котором будет передаваться код новости для показа. То же название параметра нужно задать и во входных параметрах компонента детальной новости, чтобы он знал, где брать код новости для показа. Даже в данном максимально упрощённом случае настройки не так просты. А если это набор из десятков компонентов форума?

Более удобной альтернативой для сборщика сайта будет использование комплексного компонента новостей. Этот компонент, например, можно просто установить на страницу index.php. Согласованием ссылок и параметров будет заниматься сам комплексный компонент. От разработчика сайта никаких дополнительных действий не потребуется.

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

Таким образом реализуется паттерн MVC:

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

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

  • Комплексный компонент на пальцах от Антона Долганина (блог)


Теория. Структура компонента
91 / 382

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

Структура

Важно: файлы компонентов нельзя использовать по отдельности. Компонент - это единое целое, он обладает свойством неделимости.

Структура файлов

Папка компонента может содержать следующие подпапки и файлы:

  • подпапку /lang, в которой расположены файлы языковых сообщений (переводов) компонента. В ней также могут размещаться папки помощи /help.
  • подпапку /templates, в которой расположены шаблоны вывода (отображения) компонента. Эта подпапка может отсутствовать, если у компонента нет шаблонов вывода.
  • файл component.php, который содержит логику (код) компонента. Задача этого файла - сформировать из полученных параметров ($arParams) массив $arResult, который впоследствии попадет в шаблон компонента. Этот файл должен присутствовать в папке компонента, если только логика компонента не размещена в файле class.php.
  • файл .description.php, который содержит название, описание компонента и его положение в дереве логического размещения (для редактора). Этот файл должен всегда присутствовать в папке компонента. Его отсутствие не скажется на работе компонента, но размещение компонента через визуальный редактор станет невозможным.
  • файл .parameters.php, который содержит описание входных параметров компонента для редактора. Если у компонента есть входные параметры, то этот файл должен присутствовать в папке компонента.
  • файл class.php для поддержки ООП-компонентов. В этом файле так же может размещаться логика (код) компонента.
  • файл script.js, может подключаться из шаблона, а может из кода компонента .
  • любые другие папки и файлы с ресурсами, необходимыми компоненту, например, папка /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 - время кеширования обратной адресации.

Внимание! Нельзя делать подсказки про запас (создать, но оставить пустыми): перестанут показываться настройки компонента.


Теория. Структура комплексного компонента
92 / 382

Комплексный компонент служит для организации целого раздела сайта (форум, каталог и т.п.). Для вывода данных он подключает обычные компоненты. По сути, он является контроллером (менеджером) простых компонентов. Комплексный компонент определяет на основании 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 в подключении компонента - объект, представляющий текущий компонент. Он передается в вызов подключения компонента. Таким образом, подключаемый компонент будет знать, что он подключается из комплексного компонента. Соответственно, он сможет пользоваться ресурсами комплексного компонента, вызывать его методы и т.п.


Теория. Размещение в системе и подключение компонента
93 / 382

Размещение

Компоненты в Bitrix Framework должны храниться только в определенных местах:

  • в папке /bitrix/components/bitrix (по умолчанию);
  • в папке /bitrix/components/собственное пространство имен.
  • в папке /local/components/ (рекомендуется для собственных компонентов сторонних разработчиков)

Все компоненты находятся в папке /bitrix/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).


Теория. Описание компонента
94 / 382

В файле .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" для комплексного компонента, для простых компонентов не имеет значения.


Теория. Параметры компонента
95 / 382

В файле .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' и так далее), то массив выводится в том порядке, как сформирован . Если же ключи могут быть приведены к числовым - массив значений выводится отсортированный по ключам.

  • STRING - текстовое поле ввода.
  • CHECKBOX - да/нет.
  • CUSTOM - позволяет создавать кастомные элементы управления.
  • FILE - выбор файла.

    Пример реализации
    Пример реализации в компоненте bitrix:player:
    $ext = 'wmv,wma,flv,vp6,mp3,mp4,aac,jpg,jpeg,gif,png';
Array(
   "PARENT" => "BASE_SETTINGS",
   "NAME" => 'Выберите файл:',
   "TYPE" => "FILE",
   "FD_TARGET" => "F",
   "FD_EXT" => $ext,
   "FD_UPLOAD" => true,
   "FD_USE_MEDIALIB" => true,
   "FD_MEDIALIB_TYPES" => Array('video', 'sound')
);
    В данном случае можно будет выбрать файл из медиабиблиотеки или из файловой структуры. Результатом будет строка - путь к файлу от корня сайта.

  • 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" => "массив внутренних названий переменных, которые могут использоваться в шаблоне"
)

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


Теория. Шаблоны компонента
96 / 382
Цитатник веб-разработчиков.

Роман Петров: Не все знают, что у стандартных компонентов "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

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

$arTemplateDescription = array(
    "NAME" => GetMessage("ADV_BANNER_NAME"),
    "DESCRIPTION" => GetMessage("ADV_BANNER_DESC"),
);
?>
    • файл .parameters.php, который содержит описание дополнительных входных параметров шаблона для визуального редактора.
      Пример файла .parameters.php

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

if (!CModule::IncludeModule("advertising"))
    return;

$arTypeFields = Array("-" =>GetMessage("ADV_SELECT_DEFAULT"));
$res = CAdvType::GetList($by, $order, Array("ACTIVE" => "Y"),$is_filtered, "Y");
while (is_object($res) && $ar = $res->GetNext())
{
    $arTypeFields[$ar["SID"]] = "[".$ar["SID"]."] ".$ar["NAME"];
}

$arTemplateParameters = array(
    "GROUPS" => array(
        "SLIDE_SETTINGS" => array("NAME" => GetMessage("ADV_SLIDE_SETTINGS"), "SORT" => "150"),
        "NAV_SETTINGS" => array("NAME" => GetMessage("ADV_NAV_SETTINGS"), "SORT" => "250")
        ),
    "PARAMETERS" => array(
        "TYPE" => Array(
            "NAME" => GetMessage("ADV_TYPE"),
            "PARENT" => "BASE",
            "TYPE" => "LIST",
            "DEFAULT" => "",
            "VALUES" => $arTypeFields,
            "ADDITIONAL_VALUES" => "N"
        ),
        "NOINDEX" => array(
            "NAME" => GetMessage("adv_banner_params_noindex"),
            "PARENT" => "BASE",
            "TYPE" => "CHECKBOX",
            "DEFAULT" => "N",
        ),
        "QUANTITY" => array(
            "NAME" => GetMessage("ADV_QUANTITY"),
            "PARENT" => "BASE",
            "TYPE" => "STRING",
            "DEFAULT" => "1"
        ),
        "CACHE_TIME" => Array("DEFAULT"=>"0"),
    )
);

if ($templateProperties['NEED_TEMPLATE'] == 'Y')
{
    $templates = array('-' => GetMessage("ADV_NOT_SELECTED"));
    $arTemplates = CComponentUtil::GetTemplatesList('bitrix:advertising.banner.view');
    if (is_array($arTemplates) && !empty($arTemplates))
    {
        foreach ($arTemplates as $template)
            $templates[$template['NAME']] = $template['NAME'];
    }

    $arTemplateParameters['PARAMETERS']['DEFAULT_TEMPLATE'] = array(
        "NAME" => GetMessage("ADV_DEFAULT_TEMPLATE"),
        "PARENT" => "BASE",
        "TYPE" => "LIST",
        "VALUES" => $templates,
        "DEFAULT" => '',
        "ADDITIONAL_VALUES" => "N"
    );

    unset($templateProperties['NEED_TEMPLATE']);
}
    • файл template.ext, который и является собственно шаблоном. Расширение ext зависит от того, какой движок шаблонизации нужно подключать. По умолчанию расширение равно php. Этот файл должен обязательно присутствовать.
    • любые другие папки и файлы с ресурсами, необходимыми шаблону компонента. Например, папка image, содержащая изображения, необходимые шаблону.

    Шаблон комплексного компонента

    Шаблон комплексного компонента содержит все те же папки, что и шаблон простого компонента, и дополнительно:

    • шаблоны простых компонентов, которые входят в состав комплексного. Эти шаблоны располагаются в папках вида /пространство_имен/название_простого_компонента/ относительно папки шаблона комплексного компонента.
    • простые компоненты, входящие в состав комплексного, подключаются на шаблонах страниц комплексного компонента.

    Как система ищет шаблон

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

    • Если используется шаблон сайта из папки \local , то система сначала ищет шаблон компонента в папке /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();

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

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

    Копирование шаблона выполняется по команде Копировать шаблон компонента при включенном режиме Правка:

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

    При отмеченной опции Перейти к редактированию шаблона сразу будет осуществлен переход на страницу редактирования шаблона компонента. (В версиях продукта до 9.5 переход к редактированию осуществляется с помощью менеджера файлов либо при помощи команды [image] Редактировать шаблон компонента.)

    Редактирование шаблона допускает добавление в него логики действий, но такую модификацию лучше выносить в файлы 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?>

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

    • Пользовательские движки шаблонизации


Типичные ошибки
97 / 382
Цитатник веб-разработчиков.

TeppopucT: И правда, все проблемы в руках! Ищите ошибки в коде!!! Все теги должны отвечать стандартам. А Битрикс, подхватит!!! Который раз помогает очистка кода. Пусть и ручная работа, и кропотливая, но с достойным финалом!

Не удалось обнаружить код вызова компонента

Довольно распространенная ошибка, когда вы в режиме редактирования пытаетесь отредактировать параметры какого-то компонента на странице. Хоть в коде и присутствует строка $APPLICATION->IncludeComponent() (вызов компонента), всё равно иногда появляется ошибка Не удалось обнаружить код вызова компонента. К сожалению, универсального решения данной проблемы нет.

Ошибка может возникать из-за разных причин:

  • Код вызова компонента не взят в отдельные <? ?>.

    Решение: проверить отделенность кода компонента от другого php-кода на странице.

    То есть, если у вас на странице php-код в таком виде:

    <?
php-код

компонент

php-код
?>

    то будет ошибка.

    Необходимо, чтобы было так:

    <?
php-код
?>

<?
компонент
?>

<?
php-код
?>

    Можно также попробовать вставить такую конструкцию перед вызовом компонента: <?/**/?>.

  • Ошибки в html коде на странице.

    Решение: проверить валидность html кода, убрать все html-комментарии со страницы.

  • Несоответствие кодировки файла с проектом в целом.

    Решение: проверить кодировку файла. Проверить на ошибки на странице Проверка системы. В случае ошибки с кодировкой установить настройки PHP:

    Для сайта с кодировкой windows-1251:

    php_value mbstring.func_overload 0
php_value mbstring.internal_encoding cp1251

    Для сайта с кодировкой UTF-8:

    php_value mbstring.func_overload 2
php_value mbstring.internal_encoding utf-8

    Начиная с PHP 5.2.9 php_value mbstring.func_overload 2 в .htaccess не работает...
    В общем случае это настраивается через панель хостинга или с помощью техподдержки хостинга. На некоторых хостингах реализована возможность указывать mbstring.func_overload в .htaccess для более новых версий PHP.

    На собственном сервере параметр mbstring.func_overload может быть установлен в httpd.conf или php.ini.

  • Несоответствие между владельцем файла и пользователем под которым система файлы редактирует.

    Решение: проверить права пользователя.

  • Взаимное влияние аналогичных компонентов.

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


Кастомизация шаблонов компонентов
98 / 382
Цитатник веб-разработчиков.

Рамиль Юналиев: По себе знаю, что не хватало примеров работы именно с шаблонами, потому что с них начинал. Если программисты "въедут" в эти шаблоны, то там уже пойдет...

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

Кастомизация штаного шаблона компонента, как правило, преследует две цели:

  • Приведение формы вывода данных компонента в соответствие с дизайном сайта;
  • Организация вывода данных компонента в виде, недоступном в стандартном варианте.

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

Детально с кастомизацией шаблонов можно познакомиться в разделе Кастомизация шаблона.


Теория. Кастомизация шаблона
99 / 382

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

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

Иван, веб-разработчик:

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. Организация вывода данных компонента в виде, недоступном в стандартном варианте.

Пользовательские шаблоны компонента - шаблоны, которые изменены под нужды конкретного проекта. Они должны лежать в папках шаблонов портала (т.е. в /local/templates/шаблон_сайта/). При копировании шаблона компонента средствами системы, они будут расположены по следующему пути: /local/templates/шаблон_сайта/components/namespace/название_компонента/название_шаблона.

Примечание: Теоретически шаблон можно расположить в каталоге /bitrix/templates/текущий_шаблон_сайта/components/. Но в этом случае возникают нюансы, которые нужно учитывать. Предположим, что в этой папке оказались шаблоны двух компонентов с общей начальной частью в названии. Например: catalog (комплексный) и собственно catalog.section (простой). При этом в шаблоне комплексного есть еще простой catalog.section. В этом случае для catalog.section нужно использовать единый шаблон с именем, отличным от .default. Иначе он не будет подхвачен.

Копировать шаблон можно следующими способами:

  • В рамках файловой системы копированием папки /bitrix/components/bitrix/_нужный_компонент_/templates/ в папку /local/templates/шаблон_сайта/components/namespace/название_компонента/_название_шаблона.
  • Средствами интерфейса системы с помощью команды Копировать шаблон компонента (при включённом режиме Правка):

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

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

При кастомизации шаблонов можно использовать буферизацию.

В главе приведены некоторые примеры кастомизации шаблонов.

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

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

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


Теория. Модификация шаблона простого компонента в составе комплексного
100 / 382

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

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

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

  • не теряются обновления;
  • легче решать проблемы через техподдержку (ТП не занимается решением проблем, возникающих в работе кастомного кода если явно не обозначена ошибка в работе API);
  • в комплексных компонентах стандартно реализован AJAX.

В качестве примера возьмем компонент Социальная сеть. Задача: заменить форму редактирования описания группы (textarea) на визуальный редактор с возможностью вставки html.

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

Как сделать

  • Скопируйте шаблон компонента
  • Сделайте замену в шаблоне:
    <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",
   )
);
?>

Получаем результат. Было:

Стало:

При этом весь остальной код остаётся стандартный, т.е. будет обновляться и поддерживаться компанией "1С-Битрикс".


Пример. Вывод голосования
101 / 382

Задача:

  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
);?>


Пример. Добавление типа отсутствия
102 / 382

Примечание: С версии модуля 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
103 / 382

Если встаёт необходимость использовать для шаблона компонента какие-то внешние файлы css, то реализовать это можно следующими способами.

До версии 15.5.1

Системной поддержки такой потребности "из коробки" не существовало. Задача решалась одним из трёх способов:

  1. Подключение инлайном:

    <link href="/local/styles.css" type="text/css" rel="stylesheet" />

  2. Код в самом компоненте:

    $APPLICATION->SetAdditionalCss("/local/styles.css");

  3. Код в файле component_epilog.php:

    <?if(!defined("B_PROLOG_INCLUDED") || B_PROLOG_INCLUDED!==true)die();
global $APPLICATION;
$APPLICATION->SetAdditionalCss("/local/styles.css");

С версии 15.5.1

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

Для подключения такого файла в template.php необходимо прописать:

$this->addExternalCss("/local/styles.css");
$this->addExternalJS("/local/liba.js");


Пример. Редактирование шаблона меню
104 / 382

Пример редактирования шаблона на основе компонента меню

Выделите в 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/.
Внимание! Вы можете использовать стили компонента и каталог шаблона компонента для хранения стилей и дополнительных файлов. Это позволит вам переносить шаблон компонента между проектами.


Создание структуры сайта
105 / 382
Цитатник веб-разработчиков.

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

Создание структуры сайта производится в соответствии с ТЗ на сайт.

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

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

Примечание: разработчик должен взять за правило: на проекте должны быть вменяемые тексты на страницах 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) | нет

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

  • Создание новых страниц и разделов в интерфейсе Эрмитаж в курсе Контент-менеджер
  • Управление структурой проекта в курсе Контент-менеджер


Теория. Структура файлов
106 / 382

Структура файлов

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

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

Вся система целиком лежит в каталоге /bitrix/, в него входят следующие подкаталоги и файлы:

  • /admin/ - административные скрипты;
  • /cache/ - файлы кэша;
  • /activities/ - папки действий для бизнес-процессов;
  • /components/ - папка для системных и пользовательских компонентов;
  • /gadgets/ - папки гаджетов;
  • /js/ - файлы javascript модулей;
  • /stack_cache/ - файлы кеша "с вытеснением";
  • /themes/ - темы административного раздела;
  • /wizards/ - папки мастеров;
  • /images/ - изображения используемые как системой в целом, так и отдельными модулями;
  • /managed_cache/ - управляемый кеш;
  • /modules/ - каталог с модулями системы, каждый подкаталог которого имеет свою строго определённую структуру;
  • /php_interface/ - вспомогательный служебный каталог, в него входят следующие каталоги и файлы:
    • dbconn.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 - файл используемый модулем Статистика для выдачи сообщения роботу при превышении им лимита активности;
  • и другие служебные файлы и папки.

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


Настройка инфоблоков
107 / 382

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

Схема каталога товаров которую необходимо построить на сайте:

  • Группа 1
    • Группа 1.1
      • Свой фильтр по свойствам
    • Группа 1.2
      • Свой фильтр по свойствам

Возможные способы реализации

Первый способ. Все товары в одном инфоблоке. Информационный блок расположен на первом уровне (Группа 1).

Плюсы:

  • иерархия, которой можно управлять из 1С;
  • легко управляемая структура каталогов в рамках сайта;

Минусы:

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

Второй способ. Товары размещены в нескольких инфоблоках. Информационные блоки расположены на втором уровне (Группа 1.1; Группа 1.2 и так далее).

Плюсы:

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

Минусы:

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

После выбора схемы реализации нужно создать тип информационного блока, собственно информационный блок, задать его свойства и наполнить контентом через импорт (csv, xml, 1C) или вручную.

Последний шаг: настройка параметров компонента на созданный информационный блок.

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

  • Инфоблоки в курсе Контент-менеджер
  • Пример создания каталога товаров в курсе Администратор. Бизнес.


Теория. Инфоблоки
108 / 382

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

Информационные блоки - ключевой момент Bitrix Framework. Практически всё, что делается в системе в той или иной мере завязано на этот модуль, даже если это и не отображается явно.

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

Инфоблоки - сущность, которая в физической структуре БД создает 4 таблицы, не меняющиеся при изменении структуры данных: типы объектов, экземпляры объектов, свойства объектов и значения свойств объектов.

Плюсы такого подхода:

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

Минусы такого подхода:

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

Особенности упорядочивания элементов по разделам

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

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

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

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

Интеграция ORM в информационных блоках

С версии 19.0.0 модуля iblock добавлена поддержка ORM при работе с элементами инфоблоков.

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

  • Обсуждение на партнерском форуме Архитектура в "больших" магазинах
  • Статья Информационная архитектура сайта
  • Документация для разработчиков по модулю "Информационные блоки"


Теория. Работа с инфоблоками штатными средствами
109 / 382

Порядок работы

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

  • Внимательное продумывание структуры инфоблоков.
  • Создание нужного типа инфоблоков с настройкой параметров.
  • Создание самих инфоблоков с настройкой параметров.
  • Создание структуры внутри инфоблока.
  • Создание элементов инфоблока
  • Создание физической страницы (в случае использования комплексного компонента) или страниц (при использовании простых компонентов) и размещение на ней компонента (компонентов) с последующей настройкой его свойств.
  • Кастомизация работы компонента под потребности ТЗ и дизайна сайта (кастомизация шаблона компонента, использование файлов result_modifier.php или component_epilog.php, кастомизация собственно компонента).
  • Настройка отображения данных инфоблока в административной части под нужды обычного пользователя. Как показывает практика, почти никто из Контент-менеджеров не желает ознакомиться с этой возможностью и не умеет перенастраивать отображение структуры инфоблоков под себя.

Штатные возможности

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

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

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

Настройки журналирования - вкладка в настройках ИБ.

Свойства инфоблоков

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

  • Строка - значение свойства задается в виде текстовой строки;
  • Число - значение свойства задается в виде числа;
  • Список - значение свойства выбирается из списка;
  • Файл - в качестве значения свойства используется файл;
  • Привязка к разделам - с помощью данного свойства можно задать связь между элементом данного инфоблока и разделами другого информационного блока;
  • Привязка к элементам - задание связи между элементами информационных блоков «поштучно»;
  • HTML/текст - значение свойства задается в виде текста с HTML-тегами;
  • Привязка к элементам по XML_ID - привязка хранится как строка и значением является XML_ID привязанного элемента;
  • Привязка к карте Google Maps - задается связь между элементом инфоблока и компонентом Google Map
  • Справочник - задается связь между элементом инфоблока и 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).


Кеширование
110 / 382
Цитатник веб-разработчиков.

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

Производительность

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

  • обращения к этому массиву информации на чтение или на запись порождают конкурентные запросы;
  • запросы сами по себе быстрые, но их такое число, что БД начинает выстраивать из них очередь;
  • запросы медленные и тяжёлые, и к тому же очень частые.

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

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

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

Кеширование

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

Для реализации этого созданы два класса:

  • CPageCache - класс для кеширования HTML результата выполнения скрипта;
  • CPHPCache - класс для кеширования PHP переменных и HTML результата выполнения скрипта.

Система Bitrix Framework включают в себя разные технологии кеширования:

  • Кеширование компонентов (или Автокеширование) - все динамические компоненты, которые используются для создания веб-страниц, имеют встроенную поддержку управления кешированием.

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

  • Неуправляемое кеширование - возможность задать правила кеширования ресурсоемких частей страниц. Результаты кеширования сохраняются в виде файлов в каталоге /bitrix/cache/. Если время кеширования не истекло, то вместо ресурсоемкого кода будет подключен предварительно созданный файл кеша.

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

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

  • Управляемый кеш - автоматически обновляет кеш компонентов при изменении данных. Для часто обновляемого большого массива данных использование управляемого кеша неоправданно.
  • HTML кеш лучше всего включить на какой-нибудь редко изменяющийся раздел с регулярным посещением анонимных посетителей. Технология проста в эксплуатации, не требует от пользователя отслеживать изменения, защищает дисковой квотой от накрутки данных и самовосстанавливает работоспособность при превышении квоты или изменении данных. (Считается устаревшей, рекомендуется использовать технологию Композитный сайт.) В плане многосайтовости поддерживает только многосайтовость на одном домене.
  • Кеширование меню. Для кеширования меню применяется специальный алгоритм, который учитывает тот факт, что большая часть посетителей - это незарегистрированные пользователи.

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

Основные настройки кеширования расположены на странице Настройки кеширования (Настройки > Настройки продукта > Автокеширование).

Примечание: В ядре D7 настройки кеширования производятся в специальном файле.

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

  • Настройки кеширования в курсе Администратор. Базовый.
  • Класс CPHPCache в Документации для разработчика
  • Класс CPageCache в Документации для разработчика
  • Класс Cache в Документации D7


Кеширование компонентов (Автокеширование)
111 / 382
Цитатник разработчиков.

Денис Шаромов: Периодически приходится наблюдать картину: хостер отключает аккаунт за большую нагрузку на сервер, клиент обращается к нам в техподдержку, мы видим, что кеширование компонентов не используется. Клиент объясняет это так: "мне сказали ваши партнёры-разработчики компонентов, что для этого компонента нельзя включать кеширование т.к. оно в битриксе криво работает".

На самом деле проблема в неумении работать с кешированием.

Одним из видов кеширования в Bitrix Framework является кеширование компонентов.

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

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

Внимание! При использовании режима Автокеширования, обновление информации, выводимой компонентами, происходит в соответствии с параметрами отдельных компонентов.

Управление автокешированием располагается на закладке Кеширование компонентов (Настройки > Настройки продукта > Автокеширование):

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

Чтобы обновить содержимое закешированных объектов на странице, вы можете:

  1. Перейти на нужную страницу и обновить ее содержимое, используя кнопку Сбросить кеш на панели инструментов.
  2. В режиме Правки сайта использовать кнопки для очистки кеша в панели отдельных компонентов.
  3. Использовать автоматический сброс кеша по истечении времени кеширования, для чего в настройках компонента выбрать режим кеширования Кешировать или Авто + Управляемое.
  4. Использовать автоматический сброс кеша при изменении данных, для чего в настройках компонента выбрать режим кеширования Авто + Управляемое.
  5. Перейти к настройкам выбранных компонентов и перевести их в режим работы без кеширования.

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


Добавление произвольного PHP кода
112 / 382
Внимание! PHP-код непосредственно в теле страницы не рекомендуется!
Использование php-кода в теле страницы считается дурным тоном при разработке сайта и говорит о низкой квалификации разработчика.

В рабочую область страницы также может быть добавлен произвольный PHP-код, который в режиме редактирования в визуальном редакторе будет отображаться в виде значка [image]. Если установить курсор на этот значок, то в панели Свойства отобразится непосредственно сам код:

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

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

  • Работа с PHP скриптом в курсе Администратор. Базовый.


Дополнительно
113 / 382

Дополнительный материал, не попадающий в другие разделы.


Инструкция получения ключа API Google
114 / 382

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

В июне 2018 года Google запустил новый функционал Google Maps Platform, а c июля 2018 года вступил в силу новый тарифный план Google с оплатой за использование API карт, маршрутов и мест (Maps, Routes or Places).

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

  1. Войдите в свой аккаунт Google или же создайте новый;
  2. Перейдите по адресу https://cloud.google.com/maps-platform/;
  3. Нажмите кнопку Get Started .
  4. В открывшемся попапе выберите Карты и Адреса и нажмите Включить.
  5. В выпадающем списке выберите + Create a new project, введите произвольное название и нажмите NEXT:

  6. Настройте свою систему оплаты за использование API. Создайте платежный аккаунт .
  7. Выберите страну, условия использования и нажмите Принять и продолжить .
  8. Введите данные банковской карточки . Google при этом проверит карточку: снимет с карточки 1$ и вернет его обратно через некоторое время.

    Когда подключен платежный аккаунт, то на него ежемесячно начисляется бесплатно использование карт, маршрутов или мест на сумму в размере 200 долларов США. Это примерно до 28 000 запросов. Счет на оплату будет выставлен только после того, как использование Google Maps Platform превысит ежемесячный лимит в 200 долларов .

  9. В консоли выберите пункт меню API и сервисы -> Библиотека , а в библиотеке выберите и подключите необходимые сервисы: Google Maps JavaScript API, Google Maps Geocoding API, Google Maps Places API.
  10. Выберите пункт меню API и сервисы -> Учетные данные и создайте их. При создании следует выбирать Ключ API .
  11. Через несколько секунд ключ будет создан .
  12. Перед его использованием примените ограничения для него. Минимально необходимые ограничения: протокол (HTTP) и источник перехода (ваш адрес портала в виде http://ваш_битрикс24.ru/*).

Теперь вы можете использовать этот ключ в настройках вашего портала.

API-ключи понадобятся для работы обновленного поля «Адрес»
Чтобы в поле «Адрес» в реквизитах компаний автоматически выводились подсказки от Google или чтобы можно было указывать адрес непосредственно на карте:

понадобится 2 ключа:

  • Ключ с правами на Google Maps JavaScript API, Places API, Geocoding API для использования в браузере пользователя.
  • Ключ с правами на Google Places API, Geocoding API для использования на сервере.

Для первого ключа (использование в браузере) задайте ограничения по HTTP-источникам перехода, укажите источник перехода и допустимые API:

Для второго ключа (использование на сервере) задайте соответствующие ограничения по IP-адресам.

Ввести эти ключи нужно будет в настройках модуля:

Для просмотра уже созданных ключей перейдите по адресу https://console.developers.google.com/apis/credentials.


Middle, Средний уровень подготовки
115 / 382

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

  • Обновлять систему.
  • Делать бекап и уметь восстанавливать сайт.
  • Настраивать систему через файл /bitrix/.settings.php.
  • Работать с компонентами: кастомизация и кеширование.
  • Работать с инфоблоками штатными методами и с помощью API.
  • Работать с агентами и событиями.
  • Уметь пользоваться отложенными функциями.
  • Уметь управлять SEF.
  • Уметь работать с ORM.
  • Знать и использовать Пользовательские поля
  • Понимать как можно тестировать готовые проекты.


Обновление, бекап и восстановление
116 / 382

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

  • Обновление системы. Обновление системы необходимо для получения нового функционала, созданного вендром и для закрытия уязвимостей и обнаруженных багов.
  • Резервное копирование. Наличие бекапа - не просто "хороший тон" в работе. Это и гарантия того, что вам не придётся всё делать заново, с нуля.
  • Перенос сайта на хостинг. После завершения разработки необходимо перенести сайт с локальной установки на выбранный клиентом хостинг.


Ядро D7
117 / 382

Ядро D7

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

Антон Долганин: D7 упрощает работу, значительно. Можно забыть про богомерзкое копирование осточертевших getlist из модуля в модуль. В общем, все низкоуровневое убрали под капот - круто, спасибо.
Цель разработки - создание нового программного ядра на новом технологическом уровне с избавлением от "наслоений" устаревших технологий.

Принцип совместимости, от которого компания "1С-Битрикс" не имеет права отказаться, обязывал выполнять большой объём работ, не направленных непосредственно на развитие Bitrix Framework. Это прямо влияло на скорость и качество разработки самой платформы, и косвенно влияло на распространение продуктов компании на рынке.

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

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

Основные отличия от старого ядра

  • Базы данных
    • Поддерживаются базы данных: MySQL, MS SQL, Oracle, NoSQL.
    • Отказ от неэффективного драйвера MSSQL ODBC, поддерживается только native драйвер.
    • Используется ORM (построитель запросов) c noSQL.
  • ООП
    • Сильное зацепление. Весь код, относящийся к какой-то определённой области должен быть сосредоточен в одном месте, в одном классе, в одном наборе классов.
    • Компоненты с ООП (class.php) – возможность писать более структурированный код компонента и возможность наследования.
  • Разработка
    • Единообразный код. Все одинаковые вызовы называются одинаково, имеют одинаковые наборы параметров, возвращают унифицированные данные. То есть GetList пользователей не отличается от GetList'а групп пользователей.
    • Поддержка пространств имен.
    • Новые единые правила форматирования кода с жёстким контролем на уровне разработки.
    • Отказ от глобальных переменных
    • Поддержка exceptions.
    • Поддержка новых типов: дата, время, файлы. Неформатированные данные заменяются классами с методами. Значения таких типов являются объектами с методами форматирования и т.п.
    • Библиотека классов.
    • Унифицированные события. Возможность модификации и интеграции при помощи обработчиков.
    • Автозагрузка (autoload). Все сущности системы находятся в заранее определённых местах, соответственно, поддерживается автозагрузка без каких-то дополнительных действий со стороны разработчика.
    • Специализированные обработчики (классы, сущности) для разных ситуаций – типы приложений (http, cli).
    • Отложенная загрузка языковых файлов. Файлы из папкок /lang не подключаются одновременно с подключением компонента, они загружаются при первом запросе языковой фразы.
    • Провайдеры объектов для основных операций (кеш, лог).


Настройка параметров ядра
118 / 382

Особый файл

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

Настройки в новом ядре выполняются в файле /bitrix/.settings.php. Напомним, что в старом ядре аналогичные настройки выполнялись в файле /bitrix/php_interface/dbconn.php. Файл .settings.php структурно сильно отличается от прежнего dbconn.php.

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

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

Иногда бывают ситуации, что файл .settings.php отсутствует. Его можно создать в автоматическом режиме, если выполнить в командной строке : 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
    •

    Секция cache

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

    До версии 18.5.200 действовал такой формат записи:

      'cache' => array (
    'value' => array (
      'type' => 'files',
    ),
    'readonly' => false,
  ),

    В версии 18.5.200 формат записи был изменён одновременно с возможностью использовать в кешировании Redis. Оба формата на данный момент работоспособны, но вендор настойчиво рекомендует использовать новый вариант записи.

    Примеры нового формата записи для разных способов кеширования.

    Redis:

    'cache' => array(        'value' => array(
          'type' => array(
              'class_name' => '\\Bitrix\\Main\\Data\\CacheEngineRedis',
              'extension' => 'redis'
          ),
          'redis' => array(
              'host' => '127.0.0.1',
              'port' => '6379',
          ),
          'sid' => $_SERVER["DOCUMENT_ROOT"]."#01"
      ),

  ),

    Memcache

      'cache' => array(
      'value' => array(
          'type' => array(
              'class_name' => '\\Bitrix\\Main\\Data\\CacheEngineMemcache',
              'extension' => 'memcache'
          ),
          'memcache' => array(
              'host' => '127.0.0.1',
              'port' => '11211',
          ),
          'sid' => $_SERVER["DOCUMENT_ROOT"]."#01"
      ),
  ),

    Apc

      'cache' => array(
      'value' => array(
          'type' => array(
              'class_name' => '\\Bitrix\\Main\\Data\\CacheEngineApc',
              'extension' => 'apc'
          ),
          'sid' => $_SERVER["DOCUMENT_ROOT"]."#01"
      ),
  ),

    Кеш в файлах

      'cache' => array(
      'value' => array(
          'type' => array('class_name' => '\\Bitrix\\Main\\Data\\CacheEngineFiles',),
          'sid' => $_SERVER["DOCUMENT_ROOT"]."#01"
      ),
"
  ),

    XCache

      'cache' => array(
      'value' => array(
          'type' => array(
              'class_name' => '\\Bitrix\\Main\\Data\\CacheEngineXCache',
              'extension' => 'xcache'
          ),
          'sid' => $_SERVER["DOCUMENT_ROOT"]."#01"
      ),
  ),

    Параметр Значение
    type В качестве значения можно задать:
    • memcache
    • apc
    • xcache
    • files
    • redis
    • 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

    <?php
return array (
  'cache' => array(
     'value' => array (
        'type' => 'memcache',
        'memcache' => array(
            'host' => 'unix:///tmp/memcached.sock',
            'port' => '0'
        ),
        'sid' => $_SERVER["DOCUMENT_ROOT"]."#01"
     ),
  ),
);
?>

    В базовом файле .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

    Параметры соединения с базой данных и другими источниками данных. Задается имя класса и параметры соединения.

    '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) ядра.

    Параметр | Значение
options | Задаются флаги постоянного соединения и отложенности соединения с базой. Например: Connection::PERSISTENT == 1 Connection::DEFERRED == 2 Можно записывать их комбинации с помощью битовых операций. Например 3 - это и PERSISTENT, и DEFERRED.
handlersocket | В ключе указывается, какое соединение использовать для чтения (ключ read). Необходимо создать подключение, где будут указаны класс, хост и порт. В примере кода выше установлен параметр read, у которого указано значение handlersocket. А ниже собственно описание для соединения handlersocket.
className | имя класса, в которой собственно реализуется работа с конкретным типом БД.
host | имя хоста, где находится база данных. можно указать с портом
database | имя базы
login | логин пользователя базы данных
password | пароль пользователя базы данных

    Примечания:
    В рамках ORM возможна работа с несколькими базами данных.
    Для использования соединения handlersocket должна быт установлена библиотека HSPHP - PHP HandlerSocket client

    Корневая секция

    В корневой секции размещаются настройки общего характера.

    Параметр | Значение
disable_iconv | Запрещает использование библиотеки iconv. Аналог константы BX_ICONV_DISABLE в старом ядре.

    Секция pull

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

    Пример кода секции и таблица параметров

    'pull' => Array(
       'value' =>  array(
           'path_to_listener' => "http://#DOMAIN#/bitrix/sub/";,
           'path_to_listener_secure' => "https://#DOMAIN#/bitrix/sub/";,
           'path_to_modern_listener' => "http://#DOMAIN#/bitrix/sub/";,
           'path_to_modern_listener_secure' => "https://#DOMAIN#/bitrix/sub/";,
           'path_to_mobile_listener' => "http://#DOMAIN#:8893/bitrix/sub/";,
           'path_to_mobile_listener_secure' => "https://#DOMAIN#:8894/bitrix/sub/";,
           'path_to_websocket' => "ws://#DOMAIN#/bitrix/subws/",
           'path_to_websocket_secure' => "wss://#DOMAIN#/bitrix/subws/",
           'path_to_publish' => 'http://127.0.0.1:8895/bitrix/pub/',
           'nginx_version' => '3',
           'nginx_command_per_hit' => '100',
           'nginx' => 'Y',
           'nginx_headers' => 'N',
           'push' => 'Y',
           'websocket' => 'Y',
           'signature_key' => '1111111111',
           'signature_algo' => 'sha1',
           'guest' => 'N',
       ),
   ),
    Параметр | Значение
path_to_listener | путь для подключения к серверу (http) для получения команд
path_to_listener_secure | путь для подключения к серверу (https) для получения команд
path_to_modern_listener | путь для подключения к серверу для современных браузеров (http) для получения команд
path_to_modern_listener_secure | путь для подключения к серверу для современных браузеров (https) для получения команд
path_to_websocket | путь для подключения к серверу для современных браузеров с поддержкой веб-сокетов (http) для получения команд
path_to_websocket_secure | путь для подключения к серверу для современных браузеров с поддержкой веб-сокетов (https) для получения команд
path_to_publish | путь для публикации команд со стороны сервера
path_to_publish_web | путь для публикации команд со стороны браузера (http)
path_to_publish_web_secure | путь для публикации команд со стороны браузера (https)
nginx_version | версия сервера очередей
nginx_command_per_hit | количество каналов в который будут отправлены одинаковые данные за один хит
nginx | включен ли сервер очередей
nginx_headers | нужно ли отправлять специальные заголовки
push | включен ли сервер очередей
websocket | активны ли веб-сокеты
signature_key | секретный ключ-подпись сообщений
signature_algo | алгоритм шифрования подписи
guest | модуль активен ли для гостей
enable_protobuf | включен ли новый формат сообщений
limit_max_payload | максимальный размер сообщения в байтах
limit_max_messages_per_request | максимальное количество сообщений за один хит
limit_max_channels_per_request | максимальное количество каналов за один хит

    Секция http_client_options

    Секция задаёт опции по умолчанию для класса Bitrix\Main\Web\HttpClient.

    Параметр | Значение
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-сертификатов производиться не будет

    Пример настройки:

      '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

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

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

    • Пример скрипта для редактирования настроек. (блог)


Подключения к Redis, Memcache
119 / 382

Для создания подключения в файле настроек 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 можно прочитать в официальной документации.

Кластер

Если необходимо создать кластер из redis серверов, то достаточно добавить настройку servers.

'connections' => [
    'value' => [
      'default' => [
        'className' => \Bitrix\Main\DB\MysqliConnection::class,
        //... настройки существующего подключения в БД
      ],
      'custom.redis' => [
        'className' => \Bitrix\Main\Data\RedisConnection::class,
        '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,
      ],
    ],
    'readonly' => true,
  ]

Про настройки вариантов serializer, persistent, failover, timeout, read_timeout можно прочитать в официальной документации.

Использование

Чтобы получить экземпляр соединения, достаточно обратиться по имени соединения, используя метод \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');


Теория. Модули в D7
120 / 382

Модули системы (как штатные, так и загруженные из Marketplace) располагаются в системной папке bitrix/modules. В папке /local/modules могут располагаться пользовательские модули. Такое разделение позволит сторонним разработчикам просто организовать контроль версий своих разработок с сохранением обновляемости продукта стандартной системой обновлений.

Структура файлов и папок модулей на D7 осталась такой же, как и в старой версии ядра.

API (классы, логика) модуля в новом ядре располагается в подпапке /lib папки модуля. Например, для Главного модуля путь будет таким: bitrix/modules/main/lib. Из этой же папки API будет автоматически подключаться при соблюдении некоторых правил.

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


Теория. Пространства имён
121 / 382

Пространства имён

Понятие пространств имен позволяет давать элементам системы более четкие имена, избавиться от множества префиксов имен, а также избежать потенциальных конфликтов. Все классы, поставляемые в стандартном дистрибутиве, должны находиться в пространстве имен 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");

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

  • PHP Namespace.
  • PHP: пространства имен (документация PHP).


Теория. Исключения
122 / 382

Исключения

В новом ядре используется механизм исключений (exceptions).

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

Примеры

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

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

Если метод ожидает id пользователя, а вы передаёте строку, то это - исключение, так как метод не знает что делать со строкой в данном случае.

Если метод GetList принимает фильтр timestamp, а разработчик написал tymestamp, то это будет исключением.

Иерархия исключений

Все исключения D7 наследуются от встроенного в PHP класса \Exception, который присутствует в PHP начиная с версии 5.1. У данного класса есть не переопределяемые методы getMessage(), getCode(), getFile(), getLine(), getTrace(), getTraceAsString(), а так же переопределяемый метод __toString().

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

  • \Exception
    • Bitrix\Main\SystemException - базовый класс всех системных исключений
      • \Bitrix\Main\IO\IoException - базовый класс всех исключений файлового ввода-вывода
        • \Bitrix\Main\IO\FileDeleteException - исключение при удалении файла
        • \Bitrix\Main\IO\FileNotFoundException - отсутствие требуемого файла
        • \Bitrix\Main\IO\FileOpenException - исключение при открытии файла
        • \Bitrix\Main\IO\InvalidPathException - не корректный путь
        • \Bitrix\Main\IO\FileNotOpenedException - файл не открыт
      • \Bitrix\Main\Config\ConfigurationException - ошибка в конфигурации
      • Bitrix\Main\Security\SecurityException - ошибка безопасности
        • \Bitrix\Main\Security\Sign\BadSignatureException - исключения ошибок подписи.
      • \Bitrix\Main\ArgumentException - базовый класс исключений, связанных с входящими параметрами методов
        • \Bitrix\Main\ArgumentNullException - параметр должен быть не пустым
        • \Bitrix\Main\ArgumentOutOfRangeException - параметр вне допустимого диапазона
        • \Bitrix\Main\ArgumentTypeException - параметр не допустимого типа
      • \Bitrix\Main\DB\Exception - базовый класс для исключений БД
        • \Bitrix\Main\DB\ConnectionException - исключение при соединении
        • \Bitrix\Main\DB\SqlException - исключение при выполнении запроса
      • \Bitrix\Main\NotSupportedException - вызывается, если функционал не поддерживается
      • \Bitrix\Main\NotImplementedException - вызывается, если функционал должен поддерживаться, но пока не реализован
    • \Bitrix\Main\ObjectPropertyException - исключение выводится когда свойства объекта не валидны.
    • \Bitrix\Main\ObjectNotFoundException - выводит исключение когда объект не существует.
    • \Bitrix\Main\ObjectException - исключение выводится, если объект не может быть создан.
    • \Bitrix\Main\LoaderException - исключение в загрузчике

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, где выводится сообщение об ошибке штатной функцией. Выполнение скрипта при этом не будет прервано:


Теория. Приложения и контекст
123 / 382

Приложение - это объект, отвечающий за инициализацию ядра.

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

Любой конкретный класс приложения является наследником абстрактного класса 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
124 / 382

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

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

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

При выводе ошибки на экран (если режим отладки отсутствует), подключается файл /error.php .

Пример файла error.php

<table cellpadding="1" cellspacing="0" width="35%" bgcolor="#9C9A9C">
    <tr>
        <td><table cellpadding="5" cellspacing="0" width="100%">
            <tr>
                <td bgcolor="#FFFFFF" align="center"><FONT face="Verdana, Arial, Helvetica, sans-serif" size="-1">
                <font color="#FF0000"><b><?echo "Нет такого фильтра!"?></b></font><br>Проверьте имя и попробуйте снова.</font></td>
            </tr>
        </table></td>
    </tr>
</table>

В этом файле вы можете вывести ошибку в дизайне сайта, а также установить код статуса HTTP (например, "500 Internal Server Error").


Теория. Highload-блоки
125 / 382

Поскольку модуль Информационные блоки считается очень "тяжелым" для создания легких справочников или хранения большого количества данных (когда модуль может вести себя не очень оптимально), то появилась потребность в создании аналога инфоблоков (модуль 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, который может заметно снизить нагрузку на СУБД.


Архитектура модуля
126 / 382

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

Поскольку модуль работает на новом ядре, то доступ к данным будет предоставляться на основе ORM.

Разнесение данных может быть не только по таблицам одной базы данных, но в принципе оно может быть и по разным базам данных. Таким образом, становится возможным горизонтальное масштабирование проекта, когда одни объекты живут на одном сервере, а другие - на другом.

В модуле Highload-блоки добавлена поддержка концепции NoSQL на основе базы данных MySQL. Соединение с базой данных предусмотрено как с помощью расширения handlersocket для MySQL, так и без него. Если используется handlersocket, то необходимо произвести дополнительные настройки.

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 и собрав плагин, либо установить PerconaServer или MariaDB, в которых он включён по умолчанию.

При обращении к Highloadblock ($obj = $entityClass::getById( $arData["ID"] )->fetch();) через HandlerSocket происходит вызов HS API (обращение open_index и find в MySQL) и обработка результата вызова приложением.

HandlerSocket запускает внутри БД пул потоков работающих в асинхронном режиме. (Аналогично работает NGINX.) Если обычно MySQL для каждого клиентского соединения поднимает один поток (thread) и работает внутри его, то в случае с HandlerSocket вводится пул потоков c использованием мультиплексирующих системных вызовов pool/select, поэтому, например, 5 потоков могут обработать сотни тысяч запросов.

Допустим, что в проекте сконфигурирована поддержка 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 (поле должно быть обязательно заполнено), иначе значения свойства "справочник" не сохраняются у элемента.

Несколько примеров работы с 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();


Теория. API
127 / 382

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

Сокращения, не являющиеся общепринятыми (в Битриксе), использовать нельзя.


Практика. Некоторые классы
128 / 382

Конфигурация

Расположено в пространстве имен \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 на примере местоположений
129 / 382

Типы местоположений

Добавление типа местоположения:

$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);
}


Сервис Локатор
130 / 382

Описание

Сервис локатор (локатор служб) - это шаблон проектирования для удобной работы с сервисами приложения. Подробнее можно прочитать в статье.

Идея сервиса в том, что вместо создания конкретных сервисов напрямую (с помощью 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.

 [
        '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 и правилам описания конфигураций.

 [
        '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');
            },
        ]


Регистрация локатора
131 / 382

Регистрация сервиса

Регистрация сервиса через файлы настроек bitrix/.settings.php

Прежде чем обращаться к сервису его необходимо зарегистрировать и один из способов это использование файлов настроек .settings.php. Все необходимые сервисы перечисляются в секции 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 и правилам описания конфигураций.

 [
        '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');
            },
        ]


Теория. AJAX
132 / 382

Термины

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

Соглашения

  • При вызове все имена параметров регистроЗАвисимые
  • При вызове все имена действий регистроНЕзависимые
  • Полное имя действия из модуля генерируется по шаблону 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


Теория. Контроллер
133 / 382

Контроллеры - это часть 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:

  • Redirect
  • JSON
  • Типовой JSON
  • Файл b_file
  • Ресайзенное изображение
  • Архив
  • Компонент

Создание классов-действий

Есть возможность создавать классы-действия, которые унаследованы от \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;
    }


Теория. AJAX и компонент
134 / 382

Для обработки аяксовых запросов в компоненте можно использовать два подхода:

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-запросы из JavaScript'a
135 / 382

Как в 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(), в нём будет массив распакованных данных.

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

  • Аякс-запрос из JavaScript'a к этому компоненту


Практика. Постраничная навигация
136 / 382

Чтобы организовать в аякс-действии постраничную навигацию, нужно в параметрах метода внедрить \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 вычисление общего количества требуется всегда, а для обычного аякса - это необязательно. Куда производительней и удобнее сделать отдельное аякс-действие для получения количества записей по определенном фильтру.


ORM
137 / 382

ORM (англ. Object-relational mapping, рус. Объектно-реляционное отображение) - технология программирования, которая связывает базы данных с концепциями объектно-ориентированных языков программирования, создавая «виртуальную объектную базу данных». (Wikipedia)

Введение

В старом ядре на каждую сущность программируется свой GetList, Update, Add, Delete.

Недостатки такой идеологии:

  • разный набор параметров;
  • разный синтаксис полей фильтров;
  • события могут быть или не быть;
  • иногда разный код под разные БД (Add).

Цели в новом ядре:

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

Для реализации этих целей были введены понятия:

  1. Cущности (Bitrix\Main\Entity\Base);
  2. Поля сущностей (Bitrix\Main\Entity\Field и его наследники);
  3. Датаменеджер (Bitrix\Main\Entity\DataManager).

Сущность описывает таблицу в БД, в т.ч. содержит поля сущностей. Датаменеджер производит операции выборки и изменения сущности. На практике же работа в основном ведется на уровне датаменеджера.

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

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


Концепция, описание сущности
138 / 382

При создании интернет-проектов на платформе 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) добавлена поддержка SqlExression значений для пользовательских полей в 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\*. Использовать можно оба варианта, правильней - через объекты.


Операции с сущностями
139 / 382

Для операций записи используются три метода уже описанного нами класса: 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)

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

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

    $eventManager = Main\EventManager::getInstance();
$eventManager->addEventHandler(
    "module_name",
    "\SomePartner\MyBooksCatalog\Book::onBeforeUpdate",
    <ваш callback>
);

    Форматирование значений

    Иногда может возникнуть необходимость хранить данные в одном формате, а работать с ними в программе уже в другом. Самый распространенный пример: работа с массивом и его сериализация перед сохранением в БД. На этот случай предусмотрены параметры поля '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;
    }
}

    Скопировав этот код, вы можете поэкспериментировать со всеми описанными выше возможностями.


Объекты
140 / 382

Для начала использования объектов достаточно иметь лишь описанную сущность. Просто замените fetch на fetchObject в своем коде:

$book = \Bitrix\Main\Test\Typography\BookTable::getByPrimary(1)
    ->fetchObject();

Теперь $book - это полноценный объект сущности Book, наделенный множеством методов по манипуляции с собственными данными и отношениями с другими сущностями.


Класс объекта
141 / 382

Все объекты сущностей являются наследниками класса 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, поскольку эти имена уже используются базовым классом.


Именованные методы
142 / 382

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

$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, пришлось бы описывать нужные поля и методы вручную.


Приведение типов
143 / 382

В объектах действует строгое приведение значений к типу поля. Это значит, что числа всегда будут числами, а строки - строками:

$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)
144 / 382
  • 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)
145 / 382
  • 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);
    Все операции по изменению значения приводят к изменениям только во время сеанса. Чтобы зафиксировать изменения в базе данных, объект нужно сохранить - об этом следующий урок.


Проверки (isFilled, isChanged, has)
146 / 382
  • 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


Состояние объекта
147 / 382

Объект может принимать 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);


Создание и редактирование (save, new)
148 / 382

Для фиксации изменений объекта в базе данных используется метод 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)
149 / 382

Для удаления используется метод 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)
150 / 382

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

$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)
151 / 382

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

// изначально у нас есть только 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)
152 / 382

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

Для полей отношений работают уже описанные выше методы 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
153 / 382

Интерфейс доступа к объекту как к массиву может помочь обеспечить обратную совместимость при переходе с массивов на объекты:

$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';
// вызовет исключение


Коллекции
154 / 382

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

Как и в случае с Объектами, для начала использования Коллекции объектов достаточно иметь лишь описанную сущность. Используйте fetchCollection вместо привычного fetchAll или зацикленного fetch:

$books = \Bitrix\Main\Test\Typography\BookTable::getList()
    ->fetchCollection();

$books - коллекция объектов класса Book, наделенная методами для групповых манипуляций с объектами. Также коллекции используются в Отношениях.


Класс коллекции
155 / 382

Действует абсолютно та же логика с префиксом 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 давать подсказки, облегчая работу разработчика.


Доступ к элементам коллекции
156 / 382
  • 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

    •

                            © «Битрикс», 2001-2008, «1C-Битрикс», 2008-2020

                            1С-Битрикс


Групповые действия
157 / 382

Коллекции позволяют совершать групповые операции над содержащимися в ней элементами.

  • 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.


Восстановление коллекции
158 / 382

Восстановление коллекции из готовых данных работает так же, как это сделано в Объектах:

// восстановление по первичному ключу
$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']
]);

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


Отношения
159 / 382

На примере схемы отношений тестовых сущностей Book, Author и Publisher рассмотрим все комбинации и направления отношений: 1:N, 1:1, N:M.

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


1:N
160 / 382

В нашей тестовой вселенной книга может принадлежать и издаваться строго в одном издательстве. Получаем отношение "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
161 / 382

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


N:M
162 / 382

Примитивные отношения без вспомогательных данных

У книги может быть несколько авторов, у автора может быть несколько книг. В таких случаях создается отдельная таблица с двумя полями 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 | 1 | 4
33 | 2 | 0
43 | 2 | 9

Когда есть дополнительные данные (количество книг в наличие), а не только первичные ключи исходных сущностей, такое отношение следует описать отдельной сущностью:

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"
}


Аннотации классов
163 / 382

Большая часть методов Объекта и Коллекции - виртуальные, обрабатываются через magic вызов __call. В то же время они сделаны для интуитивно понятных и говорящих именованных методов, и без автокомплита в IDE их ценность резко снижается.

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

Для генерации таких аннотаций используется 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


Обратная совместимость
164 / 382

С выходом объектов в модуле main версии 18.0.4 изменились некоторые внутренние механизмы ORM. Ситуации достаточно редкие и не совсем корректные изначально, по сути появилось больше строгости в их обработке.

  • Имена полей теперь регистронезависимы. До этого можно было описать два поля LAST_NAME и last_name, и это были бы ли разные поля. Теперь же такая сущность не сможет инициализироваться. Изменение связано с именованными методами в Объектах.
  • В выборке нельзя присвоить одноименный алиас в другом регистре, например, getList(['select' => ['id' => 'ID']]).
  • Поле BooleanField раньше допускало пустую строку в качестве значения, что могло привести к дальнейшему ошибочному трактованию значения. Теперь пустая строка недопустима, можно указывать true/false или значения values, указанные в конфигурации поля.

В объектах пока что не поддерживаются:

  • Поля с сериализацей, поскольку в них существует противоречие: тип поля указывается как StringField или TextField, а фактически в нем хранится массив, что не соответствует заявленному типу. В ближайших обновлениях мы планируем добавить новый тип поля ArrayField.


Выборка данных
165 / 382

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


getList
166 / 382

Чтобы новое 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 и отношения с другими сущностями затронуты не будут - их всегда нужно указывать явно.

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 строгое сравнение с NULL по ID
  • '!==NAME' => null строгое сравнение с NULL по NAME
Внимание! Если не указывать явно оператор сравнения =, то по умолчанию будет выполнен LIKE. В данном случае использован код построения фильтров из модуля Инфоблоков, который по историческим причинам подразумевает такое поведение.

Для полей типа int ставится:
- до выхода объектного ORM : = (сравнение по равенству, массив разворачивается в набор условий 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 Entity\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 Entity\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 Entity\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.


Короткие вызовы
167 / 382

В дополнении к 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
168 / 382

Все параметры 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 уже нет.


Выбор данных из хранимых процедур вместо таблиц
169 / 382

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()”.

Примечание: Пример разработан компанией Интерволга.


Взаимосвязи между сущностями (устаревший вариант)
170 / 382

Примечание: в уроке описан вариант отношений до версии 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
171 / 382

В обновлении 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)

Список операторов находится в \Bitrix\Main\Entity\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');


Автоматическая генерация ORM-классов
172 / 382

Автоматическая генерация

Для использования генератора ORM классов перейдите на страницу Настройки > Настройки продукта > Настройки модулей > Монитор производительности (модуль Монитор производительности (perfmon) должен быть установлен) и на вкладке Генератор таблетов отметьте поле Разрешить генерацию таблетов для 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 классов перейдите на страницу Настройки > Производительность > Таблицы (должен быть установлен модуль Монитор производительности (perfmon)) и добавьте в адресную строку GET-параметр orm=y. Адрес будет выглядеть так: http://_ваш_сайт_/bitrix/admin/perfmon_tables.php?lang=ru&orm=y.

После перезагрузки страницы станет доступен пункт в меню - ORM.

После включения генератора таблетов на странице Настройки > Производительность > Таблицы в меню действий станет доступен пункт 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
173 / 382

При использовании 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.


Постраничная навигация
174 / 382

Как реализовать постраничную навигацию в ядре 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 в информационных блоках
175 / 382
С версии 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-параметров раздела и дочерних сущностей (подразделов и элементов);
  • сброс тегированного кеша;
  • установка прав доступа на раздел и дочерние сущности;
  • проверка дисковой квоты для файловых полей;
  • индексация модулем поиска;
  • привязка свойств к разделам;
  • логирование операций с разделами.

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


Концепция и архитектура
176 / 382

Каждый инфоблок является самостоятельным типом данных со своим собственным набором свойств. В ORM он представляется отдельной сущностью:

Имя классов сущности включает в себя значение нового поля из настроек инфоблока Символьный код API. За счет этого кода обеспечивается уникальность классов вне зависимости от ID и среды окружения.

Важно! Чтобы начать использовать ORM для конкретного инфоблока, ему необходимо задать через административный интерфейс Символьный код API (поле API_CODE). Это строка от 1 до 50 символов, начинающаяся с буквы и состоящая из латинских букв и цифр.

Свойства - это не просто скалярные значения, а отношения с отдельными мини-сущностями с двумя ключевыми полями: VALUE и DESCRIPTION. Единичные свойства представляются в элементе инфоблока полем Reference, множественные - OneToMany:

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

Подробнее о базовых типах свойств
В обычном случае сущность свойства состоит из двух полей - VALUE (тип зависит от свойства) и, если определено настройками свойства, DESCRIPTION (StringField).

Сущность | VALUE | DESCRIPTION | Дополнительное поле
Строка | StringField | StringField | Нет дополнительных полей.
Число | IntegerField | StringField | Нет дополнительных полей.
Список | IntegerField | StringField | ITEM Reference (\Bitrix\Iblock\PropertyEnumerationTable)
Файл | IntegerField | StringField | FILE Reference (\Bitrix\Main\FileTable)
Привязка к элементу | IntegerField | StringField | ELEMENT Reference (сущность элемента инфоблока)
Привязка к разделу | IntegerField | StringField | SECTION Reference (\Bitrix\Iblock\SectionTable)

Ориентироваться среди большого количества свойств поможет механизм аннотаций . При индексации модуля 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 инфоблока.


Чтение и запись
177 / 382

Для получения значений свойств достаточно указать их имена в запросе:

$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();

Для создания нового объекта можно использовать как конструктор соответствующего класса,

$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();

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


События и кастомные типы свойств
178 / 382

События

Для подписи на события сущности инфоблоков можно использовать штатные механизмы 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(
        ...
    );
}


Работа с компонентами
179 / 382
Цитатник веб-разработчиков.

Антон Долганин: Строжайше запрещено писать любую PHP-логику на обычной странице сайта. Любой php-код должен инкапсулироваться в компоненты. Максимум, что позволяется - использовать подключение скриптов с помощью IncludeFile, но это если разработчик полностью отдает себе отчет в том, что этого делать нельзя и это будет исправлено в ближайшее время, или того действительно требует логика проекта (единичные случаи).

Компонент – основной способ вывода информации в Bitrix Framework. Соответственно именно работа с ним дает максимальные возможности по изменению условий вывода данных и изменению (добавлению) функционала системы.

Рекомендуемое соотношение задач и способов их решений:

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

Достаточно часто задачу приходится решать комбинацией методов. То есть, например, редактировать шаблон и добавлять код в result_modifier.php.

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

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


Теория. Переменные в компоненте 2.0
180 / 382

Переменная, описание | Использование в файлах *.php
 | component | template | result_modifier | component_epilog | class
arParams Параметры, чтение/изменение, затрагивает одноименный член класса компонента. | да | да | да, изменения отразятся на arParams в файле template.php | да | да
arResult Результат, чтение/изменение, затрагивает одноименный член класса компонента | да | да | да | да, изменение не затрагивает одноименный член класса компонента | да
APPLICATION доступна, можно не объявлять как global | да | да | да | да |
USER доступна, можно не объявлять как global | да | да | да | да |
DB доступна, можно не объявлять как global | да | да | да | да |
this ссылка на текущий: | компонент | шаблон | шаблон | компонент | Объект компонента
componentPath путь к вызванному компоненту от DOCUMENT_ROOT | да | да |  | да | да
componentName имя вызванного компонента | да |  |  |  | да
componentTemplate шаблон вызванного компонента | да |  |  |  | да
parentComponentPath если компонент вызван в составе другого компонента, идут отсылки на родительский компонент | да |  |  |  | да
parentComponentName если компонент вызван в составе другого компонента, идут отсылки на родительский компонент | да |  |  |  | да
parentComponentTemplate если компонент вызван в составе другого компонента, идут отсылки на родительский компонент | да |  |  |  | да
templateName имя шаблона компонента |  | да |  | да |
templateFile путь к файлу шаблона от DOCUMENT_ROOT |  | да |  | да |
templateFolder путь к папке с шаблоном от DOCUMENT_ROOT |  | да |  | да |
templateData массив для записи, передающий данные из template.php в файл component_epilog.php, данные кешируются, т.к. файл component_epilog.php исполняется на каждом хите. |  | да |  | да |
component ссылка на: | да, текущий компонент | да, текущий шаблон |  | да, $this | да, $this

Примечание: Компонент получает все параметры вызова следующим образом:
  1. В ключах, начинающихся с ~, данные содержатся в исходном виде (т.е. без всякой обработки).
    Если это комплексный компонент или в шаблоне компонента вызывается другой и часть параметров передается ему, то необходимо передавать значение ключей с ~.
  2. В ключах без ~ данные приведены к безопасному виду с помощью метода htmlspecialcharsEx. Если ключ содержит массив, то будут обработаны строковые ключи массива (тоже с помощью htmlspecialcharsEx).


Теория. Классы компонентов
181 / 382

Поддержка классов

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

Максим Смирнов:

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

Поддержка классов компонентов реализована в виде файла /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())//startResultCache используется не для кеширования html, а для кеширования arResult
        {
            $this->arResult["Y"] = $this->sqr($this->arParams["X"]);
            $this->includeComponentTemplate();
        }
        return $this->arResult["Y"];
    }
};

Теперь из обоих компонентов можно удалить файлы component.php.


Теория. result_modifier.php
182 / 382

Файл 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.

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


Примеры решения задач
183 / 382

Несколько примеров решения задач с помощью файла 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;
        }
    }
}

    Еще примеры:

    • Иерархичное меню и result_modifier


Пример. Выборка из Информационного блока
184 / 382

Типичная ошибка

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

<?
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?
185 / 382

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

Как сделать возможность вставки видео при публикации новостей на сайте? На первый взгляд это может показаться сложно. Но фактически все легко реализуемо. Идея состоит в том, чтобы для прикреплённого к новости файла подключать компонент Медиа проигрыватель (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
186 / 382
Файл 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'));
    // сохраним их в копии arResult, с которой работает шаблон
    $arResult['MY_TITLE'] = $cp->arResult['MY_TITLE'];
    $arResult['IS_OBJECT'] = $cp->arResult['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");

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

  • Особенности использования эпилога компонента (блог)


Пример. Компонент в элементе ИБ
187 / 382

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

  • Обозначьте маркер для замены на текущий опрос. Пусть этот маркер будет #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-режим подразумевает очистку буфера вывода. В результате, из-за вложенности буфера вывода, вы можете получить неадекватный вывод компонента.


Пример. Исключение шаблона компонента из кэша
188 / 382

Универсальный способ исключения шаблона компонента из кэша

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

Идея решения проста: переместить шаблон компонента в эпилог компонента:

  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.


Теория. Кеширование компонентов
189 / 382

Автокеширование

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

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

Устройство и место хранения

Кеш компонентов хранится в файлах в папке /bitrix/cache.

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

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

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

Непосредственно после добавления/изменения элемента инфоблока в административной части кеш публичных компонентов не сбрасывается. Образно это объясняется тем, что инфоблок Новости "не знает", где выводятся новости в публичной части и сколько компонентов их отображает. Это не проблема если время кеширования выставлено правильно.

Структурная схема

Структурная схема работы компонента

В $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));
   }
}

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

  • Настройки кеширования в курсе Администратор. Базовый.
  • Класс CPHPCache в Документации для разработчика
  • Класс CPageCache в Документации для разработчика
  • Класс Cache в Документации D7


Сache Dependencies (тегированный кеш)
190 / 382

Теги кеша

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

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

Базовый код тегирования кеша:

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 для элементов, разделов и инфоблоков.

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


Пример. Добавление своего тега
191 / 382

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

Способ 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').


Пример. Сортировка в компонентах
192 / 382

Для выполнения сортировки в компоненте news.list или catalog.section компоненту необходимо передать параметры 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:

<?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 или news.list необходимо четко понимать, что редактировать сортировку данных в компоненте через визуальный редактор будет нельзя.

Перед вызовом компонента разместить код:

<?
$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,
        ... // еще настройки компонента
    );
);?>


Пример. Использование событий
193 / 382

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

Прямое назначение ответственного в Техподдержке

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

Есть два варианта решения данной задачи:

  • Модифицируется код стандартного компонента support.wizard в файлах component.php и .description.php. Также, для большей гибкости, можно установить переменную-флаг в файле .parameters.php.

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

  • Достаточно будет использовать События и модифицировать шаблон.

Теория Событий вам знакома. Вкратце повторимся: в код ядра, обычно в начало и конец вызова системной функции, разработчики уже вставили вызов системной функции, в нашем случае это: CTicket::ExecuteEvents. Если мы хотим дополнить эту функцию своей, то в файле /bitrix/php_intrface/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.


Теория. Кастомизация компонентов
194 / 382

Кастомизация стандартного компонента - копирование стандартного компонента в собственное пространство имён и изменение логики его работы с целью изменения/добавления функционала.

Большинство задач в Bitrix Framework реализуется через компоненты, и в шаблоне компонента вы оперируете массивами $arResult - это результат работы компонента (данные) и $arParams - это входные параметры.

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

  • Создать новое пространство имён компонентов в папке /local/components/, например создать директорию /local/components/my_components/.
  • В созданную папку необходимо скопировать папку с компонентом, который хотите изменить (копировать из папки /bitrix/components/bitrix/).
  • Изменить компонент под текущие задачи.
    • изменить описание компонента на свое в файлах .description.php и /lang/ru/.description.php;
    • исправить файлы .parameters.php и component.php, модифицировав (добавив необходимый) функционал с помощью API продукта;
  • Отредактировать шаблон компонента под текущие задачи.
  • Очистите кеш визуального редактора. В результате в визуальном редакторе отобразится кастомизированный компонент.

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

    Примечание: Обновление кеша визуального редактора делается на закладке Компоненты:

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


Простой пример кастомизации компонента
195 / 382

Компонент news.list при большом числе элементов может существенно тормозить генерацию страницы. Задача – оптимизировать работу компонента. Одним из вариантов оптимизации может стать удаление ссылки на детальный текст новости в виде части текста (останется ссылка в виде названия новости).

  • Скопируйте компонент в свое пространство имен.
  • В коде скопированного компонента удалите строки:
    "DETAIL_TEXT",
"DETAIL_TEXT_TYPE",
    и
    if($bGetProperty)
    $arSelect[]="PROPERTY_*";
  • Сохраните внесенные изменения.
  • Примените вместо стандартного свой собственный компонент.

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


Модификация простого компонента в составе сложного
196 / 382

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

Например, необходимо в компоненте 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
197 / 382

Тип параметров 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 не забывайте добавлять сообщения в этот файл.

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

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


Ещё пара примеров работы
198 / 382

Каким образом можно вывести капчу в своём компоненте?

Пример использования 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())
{
   if (($curPage != "/index.php"))
        {
            ....
        }
}


Инфоблоки, работа с ними
199 / 382
Цитатник веб-разработчиков.

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. Кроме этого советуем познакомиться со следующими статьями:

  • Товары и CIBlockElement::GetList
  • Выводим свойства элемента инфоблока правильно
  • Бесплатный модуль для расширения возможностей работы с инфоблоками


Теория. Инфоблоки 2.0
200 / 382

При создании информационных блоков рекомендуется хранить свойства инфоблока в отдельной таблице, причем все значения свойств одного элемента хранятся в одной строке. Эта технология называется Инфоблоки 2.0 и позволяет существенно ускорить работу системы, а также снять ряд ограничений в предыдущей версии инфоблоков. Например, теперь нет необходимости в дополнительном запросе CIBlockElement::GetProperty при выборе значений свойств функцией CIBlockElement::GetList.

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

Возможности инфоблоков 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 невозможен при числе свойств более 50.

Большое преимущество инфоблоков 2.0 – возможность использования составных индексов. Однако достаточно редка ситуация, когда выполняется фильтр по = и по нескольким полям одновременно.

Уровень информационного блока.

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

При редактировании инфоблока доступна ссылка на "конвертер" между типами хранения. Пользоваться им надо с большой осторожностью, т.к. продолжительность процесса конвертации зависит от общего объема значений свойств инфоблока. В течение всего процесса инфоблок находится в несогласованном состоянии (часть значений перенесена, а часть нет). На тестовой конфигурации для MySQL версии скорость была порядка 1500 элементов за 20-ти секундный шаг.

В классе 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())
    }
}
?>


Теория. Инфоблоки в Документообороте
201 / 382

При работе в режиме документооборота создаётся два элемента инфоблока: один "конечный" с пустым 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.


Теория. Фильтрация
202 / 382

Фильтрация

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

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"));
?>
  • "%" - подстрока;
  • "!%" - не подстрока.
<?
// найти элементы у которых в названии есть символ процента "%"
$res = CIBlockElement::GetList(Array(), Array("%NAME"=>"%"));
?>
  • "><" - между;
  • "!><" - не между.

В качестве аргумента данные типы фильтров принимают массив вида 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");

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

  • Основные функции, в которых можно использовать описанную фильтрацию:
    • CIBlockElement::GetList
    • CIBlockSection::GetList
    • CIBlockSection::GetMixedList
  • Типы фильтров


Фильтрация элементов инфоблока без компонента фильтра
203 / 382

Если для публикации информационного блока используются простые компоненты, то можно сделать фильтрацию элементов без использования компонента Фильтр и без кастомизации компонента, с помощью которого выводится список элементов. Такая фильтрация основана на использовании параметра Имя массива со значениями фильтра для фильтрации (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
204 / 382

Начиная с версии 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.


Теория. Фасетный поиск
205 / 382

Умный фильтр

Компонент Умный фильтр доступен в продукте, начиная с версии 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 остается малая часть, которая выполняется быстро и эффективно.


План действий при проблемах
206 / 382

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

  • Ставьте перед собой конкретную цель. Так как процесс оптимизации, улучшения – бесконечный. Например: каждая страница с каталогом ваших товаров должна открываться за определенное время, скажем 0,2 секунды. Только установка таких конкретных целей и их решение может быть эффективна, в отличие от неясных и формализованных пожеланий, "чтобы лучше работало".
  • С помощью страницы отладки (Настройки > Производительность > SQL Запросы) найти и убрать лишние запросы.

    Лишний запрос – это:

    • Запрос в цикле (вынести из цикла в переменную).
    • Запросы, которые добирают данные в цикле. (Лучше собрать данные в фильтре, а потом одним запросом вывести данные, потом дополнительный цикл – разложить данные) в этом случае запросы не зависят линейно от количества элементов.
  • Убрать из запросов неиспользуемые данные ($arSelect). Точно указывать поля, которые впоследствии будут использоваться. Это в разы повышает производительность, потому что такое четкое указание используемых данных означает для базы данных сокращение объемов сортировки. База данных производит такую сортировку не на жестком диске, а в оперативной памяти.
  • Оценить возможность использования PROPERTY_* в сочетании с инфоблоками 2.0. Инфоблоки 2.0 хранят свои свойства в отдельной таблице. Эта таблица при упоминании PROPERTY_* присоединяется на этапе выборки. И до выборки значения свойств присоединение этих свойств не происходит.

    Когда это не применимо? Например, при малой выборке из большого числа элементов (10 новостей из нескольких тысяч записей). Момент этот не очень однозначный, и малозависим от разработчика. Причина в том, что результат выборки из инфоблоков и инфоблоков 2.0 – разный. В простом инфоблоке при выборке записи начинают размножаться. А инфоблоки 2.0 возвращают массив значения свойства. Если код в шаблоне не предусматривает эту ситуацию, то смена обычных инфоблоков на инфоблоки 2.0 приведет только к тому, что шаблон «развалится».

  • Посмотреть план исполнения самых тяжелых запросов и добавить/удалить индексы.

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

  • Если долго сохраняется элемент в административном разделе.


Практика. Работа с элементами, разделами и свойствами
207 / 382

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


Работа с пользовательскими свойствами инфоблоков
208 / 382

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

  • Получить значения всех свойств элемента, зная его 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, в параметрах настройки списка элементов выбрать это свойство.

    Как удалить файл в свойстве элемента инфоблока

    Обновить любое свойство можно с помощью методов:

    • Update
    • SetPropertyValues
    • SetPropertyValuesEx

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

    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, ну и дополнительно фильтруем, какой файл надо удалить.


Примеры работы с множественными свойствами
209 / 382

Задача 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']);


Копирование значений полей элементов в свойства
210 / 382

Рассмотрим пример функции, которая копирует значения полей элементов инфоблока ($_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 не установлен" );
        }
    }

Дополнительная информация

  • Документация по методу CIBlockElement::SetPropertyValues


Получение суммы значений полей связанных инфоблоков
211 / 382

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

Решение возможно как с использованием модуля Интернет-магазин, так и без него. При использовании интернет-магазина все инфоблоки должны иметь свойства торгового каталога с указанием цены в соответствующих полях. Если используется редакция без интернет-магазина, то все поля исходных инфоблоков должны иметь тип число и (в рамках данного решения) иметь код 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);
   }
}
?>


Вывод свойств элемента инфоблока
212 / 382

Задача

Выбрать свойство(-а) элемента инфоблока и вывести его на экран.

Решение

Решение первой части банально: метод GetProperty класса CIBlockElement подробно описан в документации.

Решение второй части. Возьмём свойство типа HTML\текст. Для этого свойства нельзя просто вывести его значение (ключ VALUE), т.к. это — массив, содержащий «сырое» значение и его тип (HTML или текст). Всего один вызов метода GetDisplayValue класса CIBlockFormatProperties:

$arResult['DISPLAY_PROPERTIES'][$pid] = CIBlockFormatProperties::GetDisplayValue($arResult, $prop, 'news_out');

Теперь в шаблоне мы можем писать так:

echo $arResult['DISPLAY_PROPERTIES'][$pid]['DISPLAY_VALUE'];

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


Практика. Копирование инфоблока
213 / 382

В 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 импорта информационных блоков.

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

Скрипт должен быть размещен в корне сайта.

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

  • Бесплатное решение Инфоблоки, инструменты от Антона Долганина.


Некоторые ошибки при работе с инфоблоками
214 / 382

Ошибка типа:

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. Читайте внимательно описание используемого метода.


Практика. Ограничение области поиска разделом
215 / 382

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

Поиск по разделу инфоблока

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

  • Связать поисковый индекс и набор разделов, к которым привязан элемент.

    Для этого в файле /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):

    Полный код страницы

    <?
require($_SERVER["DOCUMENT_ROOT"]."/bitrix/header.php");
$APPLICATION->SetTitle("Поиск");

global $arSectionFilter;
$arSectionFilter = array("PARAMS" => array("iblock_section" => 12));

?><?$APPLICATION->IncludeComponent("bitrix:search.page", "tags", array(
      "RESTART" => "N",
      "CHECK_DATES" => "Y",
      "arrWHERE" => array(),
      "FILTER_NAME" => "arSectionFilter",
      "arrFILTER" => array(
         0 => "iblock_books",
      ),
      "arrFILTER_iblock_books" => array(
         0 => "6",
      ),
      "SHOW_WHERE" => "N",
      "PAGE_RESULT_COUNT" => "10",
      "CACHE_TYPE" => "A",
      "CACHE_TIME" => "3600",
      "TAGS_SORT" => "NAME",
      "TAGS_PAGE_ELEMENTS" => "20",
      "TAGS_PERIOD" => "",
      "TAGS_URL_SEARCH" => "",
      "TAGS_INHERIT" => "Y",
      "FONT_MAX" => "50",
      "FONT_MIN" => "10",
      "COLOR_NEW" => "000000",
      "COLOR_OLD" => "C8C8C8",
      "PERIOD_NEW_TAGS" => "",
      "SHOW_CHAIN" => "Y",
      "COLOR_TYPE" => "Y",
      "WIDTH" => "100%"
   ),
   false
);?> <?require($_SERVER["DOCUMENT_ROOT"]."/bitrix/footer.php");?>

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

Поиск по разделу и его подразделам

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

  • В файле 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
216 / 382
Цитатник веб-разработчиков.

Хан Эрли: В конечном счёте качество сайта определятся не платформой, а мастерством разработчика.

Программирование в Bitrix Framework не представляет особой сложности. Это обычное программирование на PHP. Особенности, конечно, есть, но это не те особенности, которые делают из "программистов на Битрикс" какую-то особую касту. Всё в пределах досягаемости для неленивого ума.

В этой главе освещаются вопросы особенности программирования на Bitrix Framework.

Видео | Время | Размер файла
Continuous Integration - от простого к сложному | 10 минут 35 секунд | 260 Мб
Подводные камни разработки - чего делать нельзя | 10 минут 56 секунд | 305 Мб
Программная архитектура веб-систем на Битриксе: от простого сайта до веб-кластера | 28 минут 36 секунд | 700 Мб
Особенности проектирования под Битрикс | 37 минут 34 секунды | 605 Мб
Запуск и эксплуатация веб-проектов (аудиозапись), Презентация. | 1 час 38 минут | 98 Мб (звук)


Командная PHP-строка
217 / 382

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

Инструмент расположен в административной части сайта по следующему пути: Настройки > Инструменты > Командная PHP-строка и имеет адрес /bitrix/admin/php_command_line.php.

Вот как выглядит результат выполнения кода, использующего функции класса CUser Главного модуля:

Примечание: Аналогичный инструмент есть и для работы с БД.


Организация разработки
218 / 382

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


Система контроля версий
219 / 382

Организовать сопровождение проекта с помощью системы контроля версий не сложно, если ограничиваться файлами. Для этого можно использовать, например, Mercurial - кроссплатформенную распределённую систему управления версиями, разработанную для эффективной работы с очень большими репозиториями кода. Рекомендуется использовать надстройку с графическим интерфейсом.

Простая схема репозиториев

В схеме три элемента:

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

В ходе работы изменения из Копии для разработки переносятся в Центральный репозиторий, а с него уже на Боевой сайт.

Как ведётся работа

Разработчики изменили какие-то файлы. Ответственное за коммиты лицо использует команду Hg Commit, появляется диалог, в котором отражено где и что изменилось:

Красный текст - удаление, зелёный - добавление. После проверки изменений нажимаем кнопку Commit и изменения отображаются в среде Mercurial:

Далее пишем изменения на Центральный репозиторий и переходим на "боевой" сайт. Изменения на "боевом" сайте вносятся уже из командной строки. Так же необходимо проверить вносились ли изменения на самом "боевом" проекте и, при необходимости, перенести их на Центральный репозиторий и на Копии для разработки.

Примечание: Детально о работе системы контроля версий можно узнать из видео. (С 1 часа 21-ой минуты)

Сложности

Версия ядра на сервере сайта и на сервере Копии для разработчиков могут отличаться. Поэтому ядро исключается из системы контроля версий. Технически это делается набором правил для файла настройки .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
220 / 382

Папка для своих доработок

Чтобы сделать жизнь разработчиков проектов удобнее, в рамках работ по новому ядру D7 с версии главного модуля 14.0.1 основные файлы проекта вынесены из папки /bitrix в папку /local. Это позволит изолировать изменяющиеся файлы проекта от папки продукта. По сути, в исключения достаточно будет добавить одну папку /bitrix.

Какие папки обрабатываются в /local?
  • activities - действия БП;
  • components - компоненты;
  • gadgets - гаджеты рабочего стола;
  • modules - модули;
  • php_interface - init.php, папка user_lang;
  • templates - шаблоны сайтов, шаблоны компонентов, шаблоны страниц;
  • blocks - блоки Сайтов24.

При обработке папок приоритет всегда у папки /local перед /bitrix. Это означает, что если в /local/templates/ и /bitrix/templates/ будут находиться шаблоны сайта с одинаковым названием, то подключится шаблон из /local.

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

Перенос проекта

Как старый проект перенести в папку /local.

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

  1. Для начала следует воспользоваться Монитором качества в нём есть проверка на модификацию файлов ядра. Этот тест поможет определить какие файлы были изменены.
  2. Далее создаете папку /local, в ней не создаете файл /local/php_interface/ init .php, а лишь /local/php_interface/constants.php, /local/php_inteface/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
221 / 382

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
222 / 382

Командный интерфейс реализован на основе библиотеки symfony/console. Перед началом использования убедитесь, что установили зависимости через composer.

Исполняемый файл находится в папке bitrix:

$ cd bitrix
$ php bitrix.php

Для удобства вы можете создать символическую ссылку без постфикса php:

$ chmod +x bitrix.php
$ ln -s bitrix.php bitrix
$ ./bitrix

Список доступных команд "из коробки":

  • orm:annotate

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


Немного теории PHP
223 / 382

Не считаем наших читателей ламерами в PHP, тем не менее слегка напомним теорию. А заодно и зададим стандарты написания кода в Bitrix Framework.


Замечания по $arParams и $arResult
224 / 382

$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 запросы
225 / 382

Если на сайте не существующие страницы обрабатываются с помощью опции 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-запросом. Все необходимые переменные будут соответствующим образом установлены.


Правила написания кода
226 / 382
Цитатник веб-разработчиков.

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

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

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

  • Форматирование кода
    • Длина строки
    • Правила переноса строки
    • Пробелы и табуляция
    • Форматирование подчиненности
    • Правила расстановки фигурных скобок
    • Использование тернарного оператора "?:"
    • Выражения
    • Инструкции if, else, while и т.п.
    • Сложные инструкции
    • Форматирование массивов
    • Пустые строки
    • Пробелы
    • Прочее
  • Соглашение об именовании
    • Общие понятия
    • Именование переменных
    • Именование методов
    • Именование классов
  • Комментарии
    • PHPDoc
  • Другое
    • Магические числа
    • Инструменты автоматического форматирования
      • php_beautifier

1. Форматирование кода

1.1. Структурирование текста

1.1.1. Длина строки

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

1.1.2. Правила переноса строки

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

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

Пример 1: для строки

$arAuthResult = $USER->ChangePassword($USER_LOGIN, $USER_CHECKWORD, $USER_PASSWORD, $USER_CONFIRM_PASSWORD, $USER_LID);

допустимым будет следующий вариант переноса

$arAuthResult = $USER->ChangePassword($USER_LOGIN, $USER_CHECKWORD,
     $USER_PASSWORD, $USER_CONFIRM_PASSWORD, $USER_LID);

Пример 2: для строки

if(COption::GetOptionString("main", "new_user_registration", "N")=="Y" && $_SERVER['REQUEST_METHOD']=='POST' && $TYPE=="REGISTRATION" && (!defined("ADMIN_SECTION") || ADMIN_SECTION!==true))

допустимым будет следующий вариант переноса

if (COption::GetOptionString("main", "new_user_registration", "N") == "Y"
     && $_SERVER['REQUEST_METHOD'] == 'POST' && $TYPE == "REGISTRATION"
     && (!defined("ADMIN_SECTION") || ADMIN_SECTION !== true))

1.1.3. Пробелы и табуляция

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

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

1.1.4. Форматирование подчиненности

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

Пример:

function func()
{
     if (condition)
     {
          while (condition2)
          {

          }
      }
}

1.1.5. Правила расстановки фигурных скобок

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

Пример:

if ($condition)
{
   ...
}

1.1.6. Использование тернарного оператора "?:"

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

Пример: (условие ? funct1() : func2());

1.2. Инструкции, выражения

1.2.1. Выражения

Желательно, чтобы в каждой строчке присутствовало только одно выражение.

Пример. Неправильно писать так:

$a = $b; $b = $c; $c = $a;

Правильно писать так

$a = $b;
$b = $c;
$c = $a;

1.2.2. Инструкции "if", "else", "while" и т.п.

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

Пример. Неправильно писать так:

if ($a == 0) $a = 10;
else{
$a = 5;
$b = 10;}

Правильно писать так:

if ($a == 0)
{
  $a = 10;
}
else
{
  $a = 5;
  $b = 10;
}

1.2.3. Сложные инструкции

Сложные инструкции следует разбивать по строкам. Например,

if(COption::GetOptionString("main", "new_user_registration", "N")=="Y" && $_SERVER['REQUEST_METHOD']=='POST' &&
     $TYPE=="REGISTRATION" && (!defined("ADMIN_SECTION") || ADMIN_SECTION!==true))

Можно записать как:

if (COption::GetOptionString("main", "new_user_registration", "N") == "Y"
     && $_SERVER['REQUEST_METHOD'] == 'POST' && $TYPE == "REGISTRATION"
     && (!defined("ADMIN_SECTION") || ADMIN_SECTION !== true))
{
}

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

if((!(defined("STATISTIC_ONLY") && STATISTIC_ONLY && substr($APPLICATION->GetCurPage(), 0,
     strlen(BX_ROOT."/admin/"))!=BX_ROOT."/admin/")) && COption::GetOptionString("main", "include_charset", "Y")=="Y"
     && strlen(LANG_CHARSET)>0)

Можно записать так:

$publicStatisticOnly = False;
if (defined("STATISTIC_ONLY")
  && STATISTIC_ONLY
  && substr($APPLICATION->GetCurPage(), 0, strlen(BX_ROOT."/admin/")) != BX_ROOT."/admin/")
{
     $publicStatisticOnly = True;
}
if (!$publicStatisticOnly && strlen(LANG_CHARSET) > 0
     && COption::GetOptionString("main", "include_charset", "Y") == "Y")
{
}

или так:

if (!defined("STATISTIC_ONLY") || ! STATISTIC_ONLY
  || substr($APPLICATION->GetCurPage(), 0, strlen(BX_ROOT."/admin/")) == BX_ROOT."/admin/")
{
  if (strlen(LANG_CHARSET) > 0 && COption::GetOptionString("main", "include_charset", "Y") == "Y")
  {
  }
}

1.2.4. Форматирование массивов

Массивы, которые записываются в несколько строк, следует форматировать следующим образом:

$arFilter = array(
  "key1" => "value1",
  "key2" => array(
      "key21" => "value21",
      "key22" => "value22",
  )
);

1.3. Пустые строки и пробелы

1.3.1. Пустые строки

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

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

1.3.2. Пробелы

После запятой должен быть пробел. После точки с запятой, если она не последняя в строке (например, в инструкции for), должен быть пробел. Перед запятой или точкой с запятой пробелы не ставятся. Все операторы должны быть отделены пробелом от операндов с обеих сторон. Замена пробела символом табуляции не допускается.

Пояснения:

  • Один пробел используется в объявлении методов после запятой, но не перед скобками: TestMethod($a, $b, $c);

    Примеры неправильного использования:

    • TestMethod($a,$b,$c);
    • TestMethod( $a, $b, $c );
  • Так же одиночный пробел используют для выделения операторов: $a = $b * $c / $d;

    Пример неправильного использования: $a=$b*$c/$d;

  • Также пробелы используются при форматировании циклов:for ($i = 0; $i < 10; $i++).

    Пример неправильного использования: for($i=0;$i<10;$i++)

  • Табуляция всегда используется для отступов, но никогда не используется вместо пробелов.

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

    $arArray = array(
 "key1" =>→"value1",
 "key2" =>→"value2",
);
    Примечание: присутствие или отсутствие пробела после if правилами не регламентируется.

1.4. Прочее

В сложных выражениях рекомендуется группировать операции с помощью скобок вне зависимости от того, требует это приоритет операций или нет. Пример: $r = $a + ($b * $c);

2. Соглашение об именовании

2.1. Общие понятия

Не используйте подчеркивание для отделения слов внутри идентификаторов, это удлиняет идентификаторы и затрудняет чтение.

Старайтесь давать переменным, методам и пр. "говорящие" названия. Предпочтительно использовать имена, которые ясно и четко описывают предназначение и/или смысл сущности.

Старайтесь делать имена идентификаторов как можно короче (но не в ущерб читабельности).

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

2.2. Именование переменных

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

Пример: $testCounter, $userPassword.

2.3. Именование методов

  • Очевидность из названия действия, которое будет совершать функция\метод.
  • Использование префиксов: is (обозначение вопроса), get (получить значение), set (установить значение).

Пример: isFileWriteable()

2.4. Именование классов

  • В имени должна обозначаться сущность, описываемая классом.
  • В имени должно быть не более трех слов.
  • Нельзя использовать знак подчеркивания ('_').
  • В качестве разделителей слов в имени следует использовать заглавные буквы, строчные - для остальной части.

Пример: class SaleAffiliateAccount

3. Комментарии

Комментарий должны быть на английском языке и носить содержательный характер.

3.1. PHPDoc

Необходимо описывать в стиле PHPDoc все классы и их публичные методы.

Пример:

/**
 * Gets a value that indicates whether a directory exists in the file system
 *
 * @param string $path - Path to the directory
 * @return bool - True if the directory exists, false - otherwise
 */

4. Другое

4.1. Магические числа

В коде не должно быть магических чисел. Пример плохого кода:

$task->Update($taskId, array('STATUS' => 3), 1);

Пример хорошего кода:

$task->Update($taskId, array('STATUS' => CTaskStatus::New), TASK_PERMISSIONS_WRITE);

4.2. Инструменты автоматического форматирования

4.2.1. php_beautifier

1. Установить пакет php_beautifier (ubuntu):

  • sudo aptitude install php_beautifier или sudo aptitude install php-pear и sudo pear install PHP_Beautifier-0.1.15
  • cd
  • hg clone https://bitbucket.org/qMBQx8GH/bitrix_php_beautifier
  • sudo ln -s Bitrix_php_beautifier/Bitrix.filter.php /usr/share/php/PHP/Beautifier/Filter/Bitrix.filter.php

Настроить редактор:

  • Выбрать пункт меню Settings - Configure Kate...
  • Выбрать пункт настроек Plugins и отметить галочку рядом с Text Filter
  • Нажать OK
  • Теперь в меню Tools появился пункт Filter Text

Использование:

  • Выделяем строки (или Ctrl-A для всего текста)
  • Tools - Filter Text
  • Вводим (или выбираем из истории): php_beautifier -t -f - -l 'Lowercase Bitrix'
  • Нажимаем OK.

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

  • Стандарты оформления кода PHP


Теория. Архитектура продукта
227 / 382
Цитатник веб-разработчиков.

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

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

Архитектура программного обеспечения - (англ. software architecture) — это структура программы или вычислительной системы, которая включает программные компоненты, видимые снаружи свойства этих компонентов, а также отношения между ними.

Архитектура Bitrix Framework решает следующие задачи:

  • Преемственность. Каждый новый релиз продуктов поддерживает все предыдущие решения и технологии. Это позволяет осуществлять переход на новые версии продуктов сайтов, созданных на практически любой предыдущей версии.
  • Единство принципов работы с любой версией и любым решением на базе системы.
  • Безопасность. Архитектура позволяет создать достаточный уровень безопасности для сайтов любой направленности.
  • Масштабируемость. Не наложено никаких ограничений на развитие проектов по мере роста контента, сервисов, числа пользователей.
  • Производительность. Скорость работы системы зависит от качества настройки ее элементов, то есть в большей степени на производительность влияет уровень подготовки разработчика проекта, возможности хостинга.
  • Возможность развития системы усилиями сторонних разработчиков. Архитектура не накладывает никаких ограничений на создание собственных модулей, компонентов, решений.
Цитатник веб-разработчиков.

Степан Овчинников: Я убежден, что логика, представления и данные в Битриксе разделены самым разумным для задач CMS образом.

Архитектура MVC для Bitrix Framework

Шаблон MVC для Bitrix Framework:

  • Модель - это API;
  • Представление - это шаблоны;
  • Контроллер - это компонент.

Сплошные линии - прямые связи, пунктир - косвенные связи.

Немного теории
MVC (Model-view-controller, «Модель-представление-поведение», «Модель-представление-контроллер») — архитектура программного обеспечения, в которой модель данных приложения, пользовательский интерфейс и управляющая логика разделены на три отдельных компонента, так, что модификация одного из компонентов оказывает минимальное воздействие на другие компоненты.

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

  • Модель (Model). Модель предоставляет данные (обычно для Представления), а также реагирует на запросы (обычно от Контроллера), изменяя своё состояние.
  • Представление (View). Отвечает за отображение информации (пользовательский интерфейс).
  • Поведение (Controller). Интерпретирует данные, введённые пользователем, и информирует модель и представление о необходимости соответствующей реакции.

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

Структура

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 файл, состоящий из пролога, тела страницы (основной рабочей области) и эпилога. Формирование страницы сайта производится динамически на основе используемого шаблона страницы, данных выводимых компонентами и статической информации, размещенной на странице.

    Видео по теме:

    • Программная архитектура веб-систем на Битриксе: от простого сайта до веб-кластера 28 минут 36 секунд, 700 Мб
    • Особенности проектирования под Битрикс 37 минут 34 секунды, 605 Мб


Теория. Права доступа
228 / 382

В системе 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-го уровня (уровень прав в рамках логики модуля):

  • права;
  • роли.

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

Модули, в которых поддерживаются роли, можно увидеть в фильтре Модуль на странице Настройки > Пользователи > Уровни доступа в Административном разделе. Во всех остальных модулях и во всех остальных настройках системы используются права.

Пример:

  • Права. Если вы принадлежите группам для которых в модуле Статистика заданы права Полный административный доступ и например, Просмотр статистики без финансовых показателей, то вы будете обладать максимальным правом - Полный административный доступ.
  • Роли. Если вы принадлежите к группам для которых в модуле Техподдержка заданы роли Клиент техподдержки и Демо-доступ, то вы одновременно будете обладать возможностями этих двух ролей. Т.е. вы сможете видеть все обращения в режиме демо-доступа и одновременно с этим можете создавать свои обращения как клиент техподдержки.

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

  • Управление правами доступа в курсе Контент-менеджер
  • Управление доступом в курсе Администратор. Базовый


Теория. Файлы и База данных
229 / 382

Файлы и База данных

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

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

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 - параметры соединения с базой;
    • 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 - файл используемый модулем Статистика для выдачи сообщения роботу при превышении им лимита активности;
  • и другие служебные файлы и папки.

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


Работа с базами данных
230 / 382
Цитатник веб-разработчиков.

Антон Долганин:

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

  • Невозможно сделать стандартно без большой нагрузки на 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 .

Разработчик должен уметь оценивать уровень задач. Для действительно серьезных проектов есть Oracle, MS SQL а так же кластеризация My SQL.

Что есть в Bitrix Framework для облегчения жизни простого программиста?

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

В составе административной части системы есть инструменты проверки БД и её оптимизации. Эти действия автоматизированы и вмешательство разработчика в них исключено.

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

Используйте настраиваемую из административной части кластеризацию БД на стандартных механизмах MySQL.

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

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

Денис Шаромов: Есть миф, что длинный запрос долго исполняется. В действительности, всё зависит от того, какой объем данных приходится обработать базе для исполнения запроса. Разбор собственно запроса занимает ничтожно малую часть времени. Основное время занимает фильтрация и сортировка. Время фильтрации зависит от параметров фильтра и индексов, а сортировка - от числа записей в выборке.

Работа с БД

Перед тем как начать работать с БД усвойте, что:

Внимание! Прямое обращение к базе данных в рамках Bitrix Framework не приветствуется. Более того, если речь идет о системных таблицах самого Bitrix Framework, это не просто не приветствуется, это не поддерживается. Необходимо с ними работать через API системы, так как физическая структура БД может измениться, а работа даже самого древнего API гарантирована.

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

Для работы с собственными таблицами используйте методы глобальной переменной $DB (класс CDatabase).

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

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

  • План действий при проблемах
  • Переезжаем на MSSQL
  • Кардинально ускоряем Bitrix+MSSQL (блог)
  • Проблема с deadlock на MSSQL (блог)
  • Пример оптимизации работы MySQL
  • 11 «рецептов приготовления» MySQL в Битрикс24


Теория. Отложенные функции
231 / 382

Отложенные функции

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

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

Внутри отложенной функции можно подключать компоненты, но при этом необходимо вручную подключать файлы CSS и js.

Примечание: Есть ряд новых функций, которые могут работать в условиях кеширования (SetViewTarget, EndViewTarget). Но такие функции новые, ещё не описаны в документации и их надо рассматривать скорее как исключение, чем как правило.

Алгоритм работы

  1. Любой исходящий поток из PHP скрипта буферизируется.
  2. Как только в коде встречается одна из следующих функций:
    • CMain::ShowTitle,
    • CMain::ShowCSS,
    • CMain::ShowNavChain,
    • CMain::ShowProperty,
    • CMain::ShowMeta,
    • CMain::ShowPanel

    или другая функция, обеспечивающая откладывание выполнения какой-либо функции, то:

    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
232 / 382

Файл 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 рекомендуется размещать этот файл в папке /local по пути /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 модуля сайтов.


Теория. Языковые файлы
233 / 382
Цитатник веб-разработчиков.

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

Языковой файл - представляет из себя 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']; // Июня
?>


Работа с языковыми файлами
234 / 382

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

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

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

  • IncludeModuleLangFile - в административных скриптах;
  • IncludeTemplateLangFile - в компонентах;

Для удобства поиска и дальнейшей модификации языковых фраз можно пользоваться параметром страницы 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. Кроме того, при таком подключении языковые фразы будут работать как в кеше, так и "мимо" кеша.


Теория. Гаджеты и их создание
235 / 382
Гаджет – особый программный элемент, выполняющий функцию вывода определенных данных.

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

  • Контент-менеджер;
  • Администратор Корпоративного портала.

Для отображения гаджетов используется компонент Рабочий стол (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-библиотека
236 / 382
Цитатник веб-разработчиков.

Рамиль Юналиев: Почему-то многие неохотно используют битриксовую библиотеку аякса, почти всегда делая предпочтения в сторону Jquery или других js библиотек. Несомненно то, что Jquery очень неплохая библиотека и отлично работает с ajax, но чтобы свободно разрабатывать сайты на 1с-Битрикс надо знать родное API.

Документация по JS-библиотеке Bitrix предполагает уверенные знания разработчика в JavaScript.

Javascript-библиотека Битрикс очень разнообразна. Основным (главным) классом является класс BX. Также к услугам разработчика представлено достаточно большое количество расширений. Например, расширение по работе с датами.

При работе с JS в Bitrix Framework можно использовать как штатную библиотеку, так и сторонние библиотеки. При использовании сторонних библиотек, их предварительно необходимо подключить. Подробнее про подключение внешнего JS-кода вы можете прочитать тут.

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

  • Документация по JS-библиотеке Bitrix Framework
  • Как правильно организовать хранение языковых фраз для javascript-кода
  • О том, как не нужно работать с JavaScript и jQuery
  • Современный учебник JavaScript


Подключение JS-кода
237 / 382

Варианты расположения кода

Перед написанием JS-кода встает резонный вопрос - где его хранить?

Существуют следующие варианты:

  • Если вы разрабатываете JS-код для компонента и данный код больше нигде не применяется, разумнее расположить файл script.js в шаблоне самого компонента.
  • Если JS-код общий для всей публичной части, разумнее его разместить в шаблоне сайта. Как правило, такие JS-файлы хранятся в подпапке шаблона js/ и подключаются в самом шаблоне методом: Bitrix\Main\Page\Asset::getInstance()->addJs(); Но этот путь не подходит, если шаблонов сайта несколько, или вы разрабатываете код для административной части, или публичный шаблон запрещено менять по причине его обновлений компанией Битрикс (например, коробочный шаблон Битрикс24).
  • В случае, описанном выше, применяется метод хранения файлов по пути /bitrix/js/<ваша уникальная папка>/file.js. Этот же метод размещения стоит избрать в случае создания вами своего модуля.

Регистрация и подключение библиотек

Остановимся поподробнее на последнем варианте. Естественно, вы можете также разместить код в шаблоне сайта через Bitrix\Main\Page\Asset::getInstance()->addJs();. Но более правильным решением будет третий подход.

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

$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) в нужном вам месте (например, в шаблоне компонента).

Ошибка слияния файлов

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

<?php
AddEventHandler('main', 'OnEpilog', function(){
    $arJsConfig = array(
        'custom_main' => array(
            'js' => '/bitrix/js/custom/main.js',
            'css' => '/bitrix/js/custom/main.css',
            'rel' => array(),
        ),
        'custom_crm' => array(
            'js' => '/bitrix/js/custom/crm.js',
            'css' => '/bitrix/js/custom/crm.css',
            'rel' => array(),
        ),
    );
    foreach ($arJsConfig as $ext => $arExt) {
        \CJSCore::RegisterExt($ext, $arExt);
    }
    if (!crm) {
        CUtil::InitJSCore(array('custom_main'));
    } else {
        CUtil::InitJSCore(array('custom_crm'));
    }
});

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

Чтобы этого не произошло нужно для папок указать префиксы: не /bitrix/js/custom/crm.js, а /bitrix/js/custom_crm/crm.js


JS-класс к шаблону компонента
238 / 382

Иногда при разработке компонента его шаблон необходимо наделить 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-расширение медиаплеера
239 / 382

Примеры работы на 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();
});


Примеры кастомизации публичной части
240 / 382

Описание

Задачи добавления функционала в визуальную часть действующего проекта - частая просьба клиента. Например, просьба добавить кнопку в карточку задачи, группы или сотрудника. Для "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);
    }
});

Допускается такой способ создания кнопки:

//создаем кнопку
var newButton = BX.create('span', {
    html: 'Скачать как PDF'
});

Добавление в меню

Аналогичную команду Скачать как 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
241 / 382

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

Подключение в PHP

CJSCore::Init("date");

Вызов в Javascipt

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");


Типовые ошибки и советы
242 / 382

Небольшой список типовых ошибок и советов по работе с 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)
243 / 382

Расширение (экстеншн, 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 — бандлы для браузера
    • bundle.config.js — конфигурационный файл для сборщика
    • config.php — конфигурационный файл экстеншна
    • lang — локализации
    • test — тесты
    • @types — файлы *.d.ts

Обязательные элементы: src, dist, bundle.config.js, config.php.

Необязательные элементы (директории): lang, test, @types.

Если у вас установлен @bitrix/cli, то структуру расширения можно создать , выполнив команду 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

    Директория должна содержать вложенные директории и файлы Mocha -тестов. Для каждого файла должна создаваться директория с именем тестируемого файла и вложенным в нее файлом с тестами в формате <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
244 / 382

Описание

@bitrix/cli — консольный инструмент Битрикс-разработчика. Основная цель — упростить и автоматизировать разработку фронтенда для проектов на 1С-Битрикс: Управление Сайтом и Битрикс24.
Требования для его нормальной работы, версии:
Node: 9.11.2
NPM: 5.6.0.

@bitrix/cli - это набор консольных команд:

  1. bitrix build для сборки и транспиляции ES6+ кода в кросс-браузерный ES5;
  2. bitrix test для запуска Mocha -тестов;
  3. bitrix create для быстрого создания расширения («экстеншна»).

Примечание: В первую очередь, @bitrix/cli предназначен для работы расширениями («экстеншнами»), шаблонами сайта и шаблонами компонентов.

Установка

  • NPM:

    $ npm install -g @bitrix/cli

  • YARN:

    $ yarn global add @bitrix/cli

Конфигурация

  • Базовая конфигурация:

    module.exports = {
    input: './src/app.js',
    output: './dist/app.bundle.js',
};

  • Все параметры:

    module.exports = {
    // Файл, для которого необходимо выполнить сборку.
    // Обычно это './src/<extension>.js
    // Необходимо указать относительный путь
    input: string,

    // Путь к бандлу, который будет создан в результате сборки
    // Обычно это ./dist/<extension_name>.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>,
    },
};

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

Сборка

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

$ bitrix build

Сборщик рекурсивно найдет все файлы bundle.config.js и выполнит для каждого конфига сборку и транспиляцию.

Дополнительные параметры:

  • --watch, -w

    Режим отслеживания изменений. Пересобирает бандлы после изменения исходных файлов.

    $ bitrix build --watch

  • --test, -t

    Режим непрерывного тестирования. Тесты запускаются после каждой сборки. Обратите внимание, сборка с параметром --test выводит в отчете только статус прохождения тестов — прошли или не прошли, полный отчет выводит только команда bitrix test.

    $ bitrix build --test

  • --modules, -m

    Сборка только указанных модулей. Параметр поддерживается только в корневой c модулями local/js и bitrix/modules. В значении укажите имена модулей через запятую, например:

    $ bitrix build --modules main,ui,landing

  • --path, -p

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

    $ bitrix build --path ./main/install/js/main/loader

Запуск тестов

Запускает Mocha-тесты и выводит подробный отчет о прохождении тестов.

Примечание: Тестами считаются JS файлы, расположенные в директории ./test относительно файла bundle.config.js. В момент запуска тестов исходный код и код тестов на лету обрабатывается сборщиком и после чего выполняется. Поэтому тесты можно писать на ES6+.

Дополнительные параметры:

  • --watch, -w

    Режим отслеживания изменений. Запускает тесты после изменения исходных файлов и кода тестов.

    $ bitrix test --watch

  • --modules, -m

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

    $ bitrix test --modules main,ui,landing

  • --path, -p

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

    $ bitrix test --path ./main/install/js/main/loader.

Создание расширения

Для создания расширения ("экстеншна"):

  • Перейдите в директорию local/js/ {module}
  • Выполните команду 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");.

Видео от разработчика

Просмотр видео в наших курсах в формате CHM невозможен. Ролик можно просмотреть на сайте компании «1С-Битрикс».


@bitrix/cli: сборка проекта с NPM
245 / 382

В предыдущем уроке мы познакомились с консольным инструментом @bitrix/cli . В этом уроке рассмотрим, как осуществить сборку проектов с NPM .

Сборка проекта с NPM

  1. Создайте package.json.

    В нем объявляются внешние зависимости, необходимые для работы приложения. Например, если приложение использует React, Lodash или какую-то другую библиотеку, то эти библиотеки должны быть описаны в package.json с указанием версий и описанием вашего проекта.

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

    Быстро создать package.json можно с помощью команды npm init. Она запустит мастер сборки, и вам останется только ответить на вопросы. Команду необходимо выполнить в директории экстеншна.

  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,
    },
};


Вложенные библиотеки
246 / 382

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

Полный путь в файловой системе:

`/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
247 / 382

Использование ES6

С 2019 г. все новые расширения Bitrix CoreJS будут написаны на ES6.

Использование в старых браузерах

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

  • Полифилы

    В расширениях, использующих код ES6, необходимо подключить зависимость на библиотеку main.polyfill.complex.

  • Транспиляция

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

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


Работа с магазином
248 / 382
Цитатник веб-разработчиков.

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

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

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

  • События интернет-магазина
  • Кастомизация административной формы заказа
  • Работа с D7 на примере местоположений
  • Глава Интернет-магазин в курсе Администратор. Бизнес


Товары и CIBlockElement::GetList
249 / 382

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 изменяются ключи метода. По всем ключам возможна фильтрация, сортировка, выборка.

Поля товара
Ключ | Описание
TYPE Тип товара. Значения:
  • 1 - Простой товар
  • 2 - Комплект
  • 3 - Товар с предложением
  • 4 - Предложение
AVAILABLE | Доступность.
BUNDLE | Наличие набора.
QUANTITY | Доступное количество.
QUANTITY_RESERVED | Зарезервированное количество.
QUANTITY_TRACE | Включен количественный учет (с учетом значения "по умолчанию" в настройках модуля) - Y/N.
QUANTITY_TRACE_RAW | Включен количественный учет (необработанное значение) - Y/N/D.
CAN_BUY_ZERO | Разрешена покупка «в минус» (с учетом значения "по умолчанию" в настройках модуля) - Y/N.
CAN_BUY_ZERO_RAW | Разрешена покупка «в минус» (необработанное значение) - Y/N/D.
SUBSCRIBE | Разрешена подписка на уведомления о поступлении товара (с учетом значения "по умолчанию" в настройках модуля) - Y/N.
SUBSCRIBE_RAW | Разрешена подписка на уведомления о поступлении товара (необработанное значение) - Y/N/D.
VAT_ID | Идентификатор НДС товара.
VAT_INCLUDED | Признак «НДС включен в цену».
PURCHASING_PRICE | Закупочная цена.
PURCHASING_CURRENCY | Валюта закупочной цены.
BARCODE_MULTI | Множественность штрихкодов.
WEIGHT | Вес.
WIDTH | Ширина.
LENGTH | Длина.
HEIGHT | Высота.
MEASURE | Идентификатор единицы измерения.
PAYMENT_TYPE | Для продажи подписки.
RECUR_SCHEME_LENGTH | Для продажи подписки.
RECUR_SCHEME_TYPE | Для продажи подписки.
TRIAL_PRICE_ID | Для продажи подписки.
WITHOUT_ORDER | Для продажи подписки.

С версии catalog 20.0.200 добавились поля товара

Ключ | Описание
PRODUCT_BARCODE | Штрихкод.
PRODUCT_BARCODE_STORE | Привязка штрихкода к складу.
PRODUCT_BARCODE_ORDER | Привязка штрихкода к заказу.

Теперь вызов метода с фильтрацией по доступности выглядит так:

$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)))
Поля цен (без привязки к конкретному типу цены)

Ключ | Описание | Для чего доступен
PRICE_TYPE | Идентификатор типа цены. | Фильтр.
PRICE | Цена. | Фильтр.
CURRENCY | Валюта. | Фильтр.
QUANTITY_TO | Минимальное количество товара. | Фильтр.
QUANTITY_FROM | Максимальное количество товара. | Фильтр.
EXTRA_ID | Идентификатор наценки. | Фильтр.
SCALED_PRICE | Цена в базовой валюте. | Фильтр.
QUANTITY_RANGE_FILTER | Количество товара, для которого искать цены. | Только фильтр, вспомогательное.
CURRENCY_FOR_SCALE | Фильтрация по цене с указанием валюты (т.е. когда необходимо отфильтровать цены в любых валютах с учетом курса). | Только фильтр, вспомогательное.

Поля цен (с указанием типа цены)

Ключ | Описание | Для чего доступен
PRICE_ИДцены | Цена. | Фильтр, сортировка, выборка.
CURRENCY_ИДцены | Валюта. | Фильтр, сортировка, выборка.
QUANTITY_TO_ИДцены | Минимальное количество товара. | Фильтр, сортировка, выборка.
QUANTITY_FROM_ИДцены | Максимальное количество товара. | Фильтр, сортировка, выборка.
EXTRA_ID_ИДцены | Идентификатор наценки. | Фильтр, сортировка, выборка.
SCALED_PRICE_ИДцены | Цена в базовой валюте. | Фильтр, сортировка, выборка.
QUANTITY_RANGE_FILTER_ИДцены | Количество товара, для которого искать цены. | Только фильтр, вспомогательное.
CURRENCY_FOR_SCALE_ИДцены | Фильтрация по цене с указанием валюты (т.е. когда необходимо отфильтровать цены в любых валютах с учетом курса). | Только фильтр, вспомогательное.

Поля складов

Ключ | Описание
STORE_NUMBER | Идентификатор склада.
STORE_AMOUNT | Количество на складе.
STORE_AMOUNT_ИДсклада | Количество на конкретном складе.

Примеры выборок

Фильтрация по цене любого типа:

$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;
}

Заключение

После установки обновлений catalog 18.6.100 + iblock 18.6.200 настоятельно рекомендуется перевести свои компоненты и скрипты на новые ключи. Увеличение производительности прямо пропорционально числу товаров в каталоге. Так, при тестах на разделе из 1,5 тысяч товаров прирост скорости выполнения составил порядка 30%. Штатные компоненты ( catalog.section , catalog.element , catalog.top ), а также все компоненты наследники \Bitrix\Iblock\Component\Base переведены на новые фильтры.


Пользовательские типы свойств заказа
250 / 382

Свои типы свойств

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

  • Унаследовать класс пользовательского типа:
    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' => 'Мой тип',
        )
    );
}
  • Описать JS-класс для работы со свойством и подключить его:
    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 должно быть уникальным в рамках всей системы.

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

Пример создания типа свойств String

Наследуем класс, подключаем тип свойства к системе и регистрируем его:

class StringInput extends \Bitrix\Sale\Internals\Input\Base
{
    protected 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);
    }

    protected 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);
};


Пользовательские ограничения
251 / 382

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

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

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

  • для служб доставок 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' => 'Максимальные сутки'
            )
        );
    }
}


Пользовательские правила компаний
252 / 382

Вы можете дополнить стандартный набор правил компаний своими собственными правилами. Для этого следует использовать событие 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' => 'Максимальные сутки'
            )
        );
    }
}


Кастомизация типов дополнительных услуг
253 / 382

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

В случае, если вас не устраивают стандартные типы дополнительных услуг, то вы можете добавлять свои собственные типы услуг. Для этого следует использовать событие 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.
Там же вы сможете найти примеры штатных типов услуг.


Кастомизация служб доставок
254 / 382

Кастомизация

Средства системы позволяют кастомизировать и добавлять свои собственные службы доставки, причем в магазине на ядре 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().

Примеры служб доставок

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

  • \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.


Кастомизация платежных систем
255 / 382

Средства системы позволяют кастомизировать и добавлять свои собственные платежные системы, причем в магазине на ядре 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' => 'ОПИСАНИЕ ОБРАБОТЧИКА'
);


Кастомизация шаблона платежной системы
256 / 382

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

Для этого нужно скопировать шаблон из платежной системы /bitrix/modules/sale/handlers/paysystem/<имя_платежной_системы>/template/ в шаблон сайта /bitrix/templates/<шаблон_сайта>/payment/<имя_платежной_системы>/template/ и отредактировать его так, как вам необходимо. За поиск HTML-шаблона отвечает метод \Bitrix\Sale\PaySystem\BaseServiceHandler::searchTemplate().


Собственный обработчик онлайн-кассы
257 / 382

Не подходит стандартный обработчик онлайн-кассы? Используйте API продукта и напишите свой обработчик. Для этого вам необходимо:

  • Унаследовать класс \Bitrix\Sale\Cashbox\Cashbox и реализовать необходимые методы:
    Примечание: дополнительно вы можете использовать следующие интерфейсы:
    • \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)
    {
        // извлечение данных по чеку дальнейшего сохранения
    }
}
  • Подключить обработчик кассы к системе с помощью события OnGetCustomCashboxHandlers. Обработчик события должен возвращать массив вида: array(полное_имя_класса => путь_к_файлу):
    function myCashboxFunction()
{
    return new \Bitrix\Main\EventResult(
       \Bitrix\Main\EventResult::SUCCESS,
       array(
           '\CashboxCustom' => '/bitrix/php_interface/include/cashboxcustom.php',
       )
   );
}

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


Примеры
258 / 382


Пример создания собственной службы доставки
259 / 382

Чтобы добавить собственную службу доставки, следует создать файл 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'
        )
    );
}

$eventManager->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
260 / 382

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

$products = array(
    array('PRODUCT_ID' => 1811, 'NAME' => 'Товар 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. Таким образом, из любого метода вы можете получить результат его выполнения, посмотреть, был ли он успешный, и получить ошибки, если таковые имелись.

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

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

Bitrix\Main\Loader::includeModule('sale');
Bitrix\Main\Loader::includeModule('catalog');

$products = array(
    array('PRODUCT_ID' => 1811, 'NAME' => 'Товар 1', 'PRICE' => 500, 'CURRENCY' => 'RUB', 'QUANTITY' => 5)
            );

$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);
    }

$order = Bitrix\Sale\Order::create(SITE_ID, 1);
$order->setPersonTypeId(1);
$order->setBasket($basket);

$shipmentCollection = $order->getShipmentCollection();
$shipment = $shipmentCollection->createItem(
        Bitrix\Sale\Delivery\Services\Manager::getObjectById(1)
    );

$shipmentItemCollection = $shipment->getShipmentItemCollection();

/** @var Sale\BasketItem $basketItem */

foreach ($basket as $basketItem)
    {
           $item = $shipmentItemCollection->createItem($basketItem);
        $item->setQuantity($basketItem->getQuantity());
    }

$paymentCollection = $order->getPaymentCollection();
$payment = $paymentCollection->createItem(
        Bitrix\Sale\PaySystem\Manager::getObjectById(1)
    );
$payment->setField("SUM", $order->getPrice());
$payment->setField("CURRENCY", $order->getCurrency());

$result = $order->save();
    if (!$result->isSuccess())
        {
            //$result->getErrors();
        }

Примечание: $item->setQuantity на каждом элементе корзины приводит к пересчету стоимости заказа. Для обхода этого на каждом элементе установите количество через метод setFieldNoDemand, а потом на заказе вызвите refreshOrderPrice.


Пример изменения заказа через API
261 / 382

Рассмотрим небольшой пример изменения заказа через 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 части
262 / 382

В уроке мы рассмотрим, как средствами 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);
  • Добавляем новую оплату. Минимальный набор полей при создании оплаты - это платежная система (идентификатор и название платежной системы) и сумма. Для примера будем использовать платежную систему Сбербанк с идентификатором 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 оплаты:

Примечание: разделение оплаты на этапе создания заказа делается аналогично.


Видео
263 / 382

Скачать | Время | Размер файла
Интернет-магазины, высокие нагрузки, синхронизации бизнес-приложений с веб-системой | 15 минут 43 секунды | 432 Мб

Смотреть

Просмотр видео в наших курсах в формате CHM невозможен. Ролик можно просмотреть на сайте компании «1С-Битрикс».


Работа с модулем Push & Pull
264 / 382

Push & Pull

Детально о работе и настройке модуля смотрите в соответствующей главе курса Администратор.Модули.

Режимы работы

Модуль 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.


Оптимизация количества запросов к серверу
265 / 382

Если проект находится на отдельном сервере, то необходимо использовать настройку путей в модуле 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 для гостей
266 / 382

Запустить 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(); | Данная команда логирует все команды приходящие для данного пользователя;


Подписка на события модуля
267 / 382

Подписка

До версий 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(); // отписка


Агенты и их использовние
268 / 382

Агенты

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

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

Как выполнялась проверка наличия агента до версии 20.5.0 Главного модуля.
В самом начале загрузки каждой страницы (непосредственно перед открытием сессии и событием OnPageStart) система автоматически проверяет, есть ли агент, который нуждается в запуске, и исполняет его в случае необходимости.

Примечание: С версии 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 минут ожидания это - дополнительная защита от повторного запуска, агентов которые не корректно отработали.

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

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

  • Примеры использования агентов
  • Выполнение всех агентов на cron. Чтобы почта была на хитах (сообщение в блоге)


Примеры агентов
269 / 382

Пример создания агента

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

Агента создаем на странице Настройки > Настройки продукта > Агенты по команде Добавить агента на контекстной панели:

О параметрах, значение которых может быть неясно из названия:

  • дата последнего запуска - выводится время последнего запуска (при редактировании агента);
  • дата и время следующего запуска – время старта работы агента, если он не периодический то выполнится 1 раз в это время;
  • модуль - этот модуль будет автоматически подключаться, а именно будет подключаться файл /bitrix/modules/ID модуля/include.php, в этом случае необходимо убедиться, что функция-агент будет доступна после подключения этого файла;
  • функция агента - это основное поле, у нас функция называется testAgent();
  • ID пользователя – это фильтр выполнения на хите для определенного пользователя;

Сама функция будет выглядеть так:

function testAgent()
{
        mail('mail@gmail.com', 'Агент', 'Агент');
        return "testAgent();";
}

Функцию добавить в файл /bitrix/php_interface/init.php.

Если письмо пришло, то агент работает и можно писать свой функционал.

Простые примеры агентов

<?
// добавим агент модуля "Статистика"
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
{
   function Agent007($cnt=1)
   {
      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
270 / 382

Об агентах

Агент может вносить ощутимое ожидание на хите пользователя. Но тут надо взвесить различные факторы. Например, агент выполняющийся 0.5 секунд раз в сутки не причинит ощутимого вреда, а такой же агент раз в 30 минут уже будет досаждать. Всецело можно сказать одно: если агенты занимают несколько десятых долей секунды, то уже стоит задуматься о переносе агентов на cron. "Тяжелым" считается агент, который выполняется более 10 секунд.

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

Механизм запуска

Перейдите на страницу Настройки > Инструменты > Командная PHP-строка и исполните следующий код:

COption::SetOptionString("main", "agents_use_crontab", "Y");
echo COption::GetOptionString("main", "agents_use_crontab", "N");

Увидели "Y". С этой секунды на хитах будут исполняться только периодические агенты .

Перейдите на страницу Настройки > Настройки продукта > Агенты и настройте показ колонки Периодичность. И отредактируйте нужные вам агенты выставив флажки в Периодичность выполнения: через заданный интервал.

В cron добавьте команду на выполнение:

/usr/bin/php -f /var/www/bitrix/modules/main/tools/cron_events.php

Выставьте периодичность, например: */10 * * * * - что означает раз в десять минут .

Примечание: Непосредственно перед выполнением задания процедура запуска агентов пытается отменить ограничение:
@set_time_limit(0);
ignore_user_abort(true);

Если set_time_limit разрешен, то время выполнения может превышать то, что стоит в настройках файла php.ini.

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

Пример кода запуска старых рассылок на кроне.
Запуск рассылок старого модуля Подписка и рассылки (не E-mail маркетинг) можно осуществить следующим образом:
if (CModule::IncludeModule("subscribe"))
{
CPostingTemplate::Execute();
      $cPosting = new CPosting;
      $cPosting->AutoSend();
}

Обобщённое решение

Обобщенное решение для выполнения всех агентов из-под 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);
CEvent::CheckEvents();

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, раз в 1 минуту .

Примечание: Время выполнения можно скорректировать в соответствие с проектом. Кроме того, есть возможность через установку большого значения 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 параметры mbstring.func_overload и internal_encoding отличаются. Это приводит к трудно-отлаживаемым ошибкам.

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

Если задание необходимо добавлять от имени другого пользователя (например, root), то в записи crontab нужно указать имя этого пользователя:

*/1 * * * * USERNAME /usr/bin/php -f /home/bitrix/www/bitrix/php_interface/cron_events.php


Ещё об агентах
271 / 382

Осторожнее с периодичными агентами

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

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

Решение: Выключить периодичность агентов.


События
272 / 382

Иногда бывает необходимо повлиять на ход выполнения какой-нибудь 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
273 / 382

В ядре 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 делает более комфортной отправку событий, связанных с модификацией сущностей.

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

  • События в ORM.


Использование событий
274 / 382

В главе приводятся примеры работы с событиями.

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

  • OnBefore;
  • On;
  • OnAfter.

Важно понимать какой из типов лучше использовать в вашем конкретном случае. События типа OnBefore отрабатывают в любом случае. А вот события типа OnAfter только тогда, когда будет проверена правильность введенных данных (соответствие пароля, e-mail и т.п.). Соответственно, если что-то в данных не так, то событие не отработает.

Кроме примеров в этой главе можно ещё посмотреть пример использования событий в разделе Модификация тестов и в главе Модули.


Как написать обработчик события
275 / 382

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

Анализ вызова события

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

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

Выберем для примера событие 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();.


Добавление закладки в социальную сеть
276 / 382

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

Обработчики можно расположить в файле /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
);
?>

Полезный код страницы...


Учет регистрации нового пользователя в статистике
277 / 382

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

Для решения задачи используется обработчик события 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;
}

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


Зацикливание обработчиков событий
278 / 382

Задача: при изменении элемента инфоблока модифицировать другой элемент. Кейс может быть какой угодно - это и логирование, и деактивация основного товара, когда нет активных предложений, и изменение даты активности связанного элемента. Если создать обработчик, использующий метод 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();
   }
}


Совместная работа пары событий
279 / 382

Рассмотрим использование пары обработчиков событий на примере событий метода 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
280 / 382

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

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

Решить задачу в случае использования 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). Для массовых операций необходима реализация блокировки обработчиков, получения требуемых данных одним запросом перед пакетом операций и принудительного исполнения своего кода после завершения пакета.


Дополнительно
281 / 382

Вопрос: Можно ли вызывать функцию 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; // вернём изменения
}


"Ленивые" параметры в событиях
282 / 382

Чтобы отправить событие выполните следующее:

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(),
    );
});

$event->send();

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

  1. Если нет обработчиков, то не будут запускаться вычисления.
  2. Если в обработчиках не используются параметры события, то снова не будут запускаться вычисления.

Примечание: Доступно с версии главного модуля 17.0.0.


Настройка ЧПУ
283 / 382

Примечание: Работа с ЧПУ штатными методами описана в курсе Администратор. Базовый. Перед тем как учиться работать с ЧПУ через API обязательно изучите этот раздел.

Для хранения идентификаторов элементов/разделов информационных блоков удобнее всего использовать поле Символьный код. Например, в ссылке www.myserver.ru/catalog/mobile/nokia_3310/, mobile - это символьный код раздела Мобильные телефоны, а nokia_3310 - символьный код элемента, находящегося в разделе Мобильные телефоны. Символьный код должен быть уникальным и система сама проверяет уникальность.

В обработчике 404 ошибки необходимо "разобрать" переменную $_SERVER["REQUEST_URI"] на нужные идентификаторы. Для этого в PHP есть ряд полезных функций:

  • preg_match
  • parse_url
  • pathinfo
  • strpos
  • parse_str

Например, ссылки вида 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=computerra.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 режим
284 / 382

В комплексные компоненты встроена функция генерации ЧПУ. У этих компонентов всегда есть входной параметр 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 на основании шаблонов путей выясняет запрашиваемую страницу и восстанавливает переменные, спрятаные в пути.

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

То есть, набор шаблонов путей

"section" => "section/#SECTION_ID#.php?IBLOCK_ID=#IBLOCK_ID#",
"element" => "element/#ELEMENT_ID#.php"

является допустимым, а набор шаблонов путей

"section" => "#SECTION_ID#.php?IBLOCK_ID=#IBLOCK_ID#",
"element" => "#ELEMENT_ID#.php"

допустимым не является.

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

  • HTTP POST запросы


Примеры
285 / 382

Пример

Новости вида /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");
?>


Модуль Поиск
286 / 382

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

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

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

Следовательно, возкникла задача: как эффективно ускорить, изменить и скрестить поиск с фильтрами? Для ее решения было предложено 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…


Пользовательские поля
287 / 382

Пользовательские поля

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

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

Пользовательские поля это сущность:

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

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

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

Штатные объекты пользовательских полей
Модуль | Объект | Описание
Главный модуль | USER | для пользователя
Блоги | BLOG_BLOG | для блога
 | BLOG_POST | для сообщения в блоге
 | BLOG_COMMENT | для комментария сообщения
Задачи | TASKS_TASK | для задач
Информационные блоки | IBLOCK_N_SECTION | для секций инфоблока с ID = N
 | IBLOCK_N | Для инфоблока с ID = N
Календарь | CALENDAR_EVENT | для событий календаря
Обучение | LEARN_ATTEMPT | для попыток теста
Социальная сеть | SONET_GROUP | для групп соцсети
Библиотека документов | WEBDAV | для библиотек документов
Форум | FORUM_MESSAGE | для сообщений форума
Highload-блоки | HLBLOCK_N | для highload-блока с ID=N

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

Создание полей

Создание пользовательских полей из Административной части выполняется на странице Настройки > Настройки продукта > Пользовательские поля либо, что предпочтительнее, с использованием ссылки Добавить пользовательское свойство в тех формах системы, в которых предусмотрено штатное добавление пользовательских свойств:

  • форма добавления/редактирования пользователя;
  • форма добавления/редактирования раздела информационного блока;
  • форма добавления/редактирования блога

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

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

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

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

  • Число;
  • Да/Нет;
  • Видео;
  • Шаблон;
  • Список;
  • Строка;
  • Дата;
  • Дата со временем;
  • Привязка к элементам highload-блоков;
  • Привязка к разделам инф. блоков;
  • Привязка к элементам инф. блоков;
  • Привязка к элементам CRM;
  • Привязка к справочникам CRM;
  • Файл;
  • Целое число.
  • Документ из библиотеки документов
  • Привязка к сотруднику
  • Документ истории из библиотеки документов
  • Опрос

Как правило, этих типов вполне хватает для работы. Если есть необходимость создания собственных типов данных, то это можно сделать самостоятельно. Пример добавления типов данных "Связь с элементом" и "Связь с элементом в виде списка" (блог).

Для работы с пользовательскими полями можно использовать События.

События Главного модуля при работе с пользовательскими полями:

Событие | Вызывается | Метод
OnUserTypeBuildList | при построении списка пользовательских полей | CUserTypeManager::GetUserType
OnUserTypeRightsCheck | при проверке прав доступа на пользовательские поля | GetRights.


Примеры работы
288 / 382

Рассмотрим несколько примеров работы с пользовательскими полями.

Фильтрация и сортировка

Фильтрация

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

$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())
    {
        echo '
', mydump($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;
    }
}
?>

  • Расширение пользовательских типов для пользовательских полей главного модуля. (блог)
  • Как прикрутить пользовательские свойства к своему модулю (блог)
  • Добавление типов данных "Связь с элементом" и "Связь с элементом в виде списка" (блог)


Добавление, редактирование, удаление пользовательских свойств и их значений
289 / 382

За работу с пользовательскими полями отвечает класс 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);


Поля к нештатным объектам и новые объекты
290 / 382

Создание пользовательского поля к нештатным объектам

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

Рассмотрим это на примере комментариев блога. Например, у каждого комментария необходимо создавать свойство Рейтинг. Создаем в административной части пользовательское свойство нужного типа (Настройки > Настройки продукта > Пользовательские поля). Заполняем все поля, в поле Объект указываем любое придуманное имя объекта, главное, чтобы оно было уникально. В нашем случае, напишем 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);


Тестирование проектов
291 / 382

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

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

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

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

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

  • Поиск. Если на сайте используется форма поиска, то её следует протестировать в первую очередь. Обязательно надо проверить, чтобы ссылки в результатах не были битые. (Битые ссылки присутствуют в анонсах, в подробных описаниях, просто в статическом контенте, в настройках инфоблоков.) Если на сайте используется несколько инфоблоков с динамической информацией, то нужно сделать поисковые запросы, чтобы найти по отдельности элементы различных инфоблоков и проверить, корректно ли работают ссылки в результатах поиска.
  • F5. Во всех формах, в которых пользователь может оставлять какие-либо данные (например, комментарии), нужно проверить, не отправляются ли данные снова при нажатии кнопки F5 в браузере.
  • Пользователи и права доступа. Следует протестировать сайт в трёх состояниях: неавторизованным, авторизованным и авторизованным под администратором. Часто бывает, что создавая инфоблок из-под администратора, ему забывают поставить нужные права доступа, и он остается доступным только для администраторов.
  • Карта сайта Не всегда для этой страницы подойдёт стандартный компонент Карта сайта. Если основная информация на сайте – каталог товаров (сделанный, естественно, с помощью модуля инфоблоков), то в карте сайта логично вывести разделы каталога, а не только разделы сайта.
  • Дефолтные шаблоны. Необходимо проверить, не используются ли где-нибудь на сайте дефолтные шаблоны. Может получиться не очень хорошо, если, например, пользователь пытается восстановить пароль, ему на почту уходит письмо со ссылкой на сайт, он нажимает на ссылку и видит не привычный ему дизайн сайта, а пример дефолтного «корпоративного» сайта "1С-Битрикс: Управление сайтом".
  • Стили в визуальном редакторе. Хорошо, когда контент-менеджер, пользуясь визуальным редактором, видит в нём текст, максимально похожий на текст, который отобразится на сайте. То есть для абзацев, заголовков и т.д. свой кегль шрифта, цвет и т.д.
  • Динамическая информация во включаемых областях Если во включаемой области содержится какая-либо динамическая информация, то она может закешироваться, что скорее всего приведет к нежелательному результату. Лучше вообще не использовать динамическую информацию во включаемой области.
  • Резервная копия, обновления. Необходимо обновить Битрикс до актуальной версии, сделать резервную копию сайта и сохранить её на локальный компьютер.
  • Кеш. Следует проверить, включено ли автокеширование. Если оно не включено, то нужно его включить и повторить всё тестирование с самого начала
  • Права доступа. Проверка на корректность прав доступа различных групп пользователей, генерация отчета по правам групп в папках сайта.
  • Ядро системы. Проверка работы ядра и соответствие его оригиналу с генерацией отчета о папках компонентах и модулях не соответствующих оригиналу и генерации отчета о несоответствии версий. Иногда шаловливые ручки позволяют себе править непосредственно компоненты Bitrix Framework и, чтобы не порушить функционал сайта, очень полезно перед тем как обновлять проект узнать что наделано не так и что наделано дополнительно.
  • Неиспользуемые файлы. Часто при разработке или изменениях делают резервные копии и хорошо если делают приставку old а иногда просто весь этот мусор остается, плюс неиспользуемые картинки и т.п.
  • Сторонние подключения. Часто к сайту подключают различные партнерские программы: баннеры, скрипты и т.п., вещи без которых невозможна монетизация. Но при этом может пострадать безопасность. Необходимо проверять подобный функционал на предмет безопасности и наличия уязвимостей (доступ на редактирование, доступ к ядру, возможность внедрения кода, сохранения через подключаемый модуль файлов)
Из опыта веб-разработчиков.

Роман Петров: Один из простых способов протестировать на ошибки разработки ваш сайт на "1С-Битрикс: Управление сайтом" - это перед запуском проекта, включить тест производительности на нужное время и на том же сервере запустить 3-4 одновременных команды wget на зеркалирование сайта. Первое обращение создаст кэш страницы, а остальные - обратятся уже к закэшированным файлам. Это позволит очень легко выявить все проблемные странички (мелкие ошибки "по забывчивости", ошибки верстки (пропущенные файлы и др.)) и исправить ошибки.

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


Монитор качества
292 / 382

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

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

Монитор качества - инструмент для проверки качества выполненного проекта перед сдачей его заказчику.

Монитор качества это:

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

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

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

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

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

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

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

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


Сдача проекта
293 / 382

Сдача проекта

Сдача проекта производится на странице Монитор качества (Настройки > Инструменты > Монитор качества). При первом открытии отобразится страница с вводной информацией. Запуск теста произойдёт после нажатия на кнопку [image], после чего откроется дерево тестов с предложением запуска автотестирования. Автотестирование можно отложить и запустить его позже.

Тесты

Автоматизированный тест

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

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

Ручной тест

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

Прохождение тестов

Тест открывается кликом по его названию. Форма автоматического теста (в форме ручного теста отсутствует кнопка Запустить автотест):

Если тест не пройден, то по ссылке Подробный отчет можно просмотреть причины непрохождения:

В закладке Рекомендации можно посмотреть условия теста, и какие параметры в системе нужно проверить и исправить для правильного прохождения теста.

После исправления ошибок можно:

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

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

Архив тестов

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

Примечание: Отчёт можно послать в компанию "1С-Битрикс" для участия в программе монитора качества. Для этого используйте ссылку Направить в 1С-Битрикс и заполните открывшуюся форму.


Способы использования
294 / 382

Монитор качества можно использовать в нескольких вариантах.

Базовое тестирование по чеклисту

Партнер/Разработчик организует тестирование выполненной интеграции по чеклисту и выступает в роли тестировщика. Затем предъявляет клиенту успешно сданный отчет, доступный в административном интерфейсе в разделе Настройки > Инструменты > Контроль качества – в котором все обязательные тесты успешно пройдены.

Углубленное тестирование по чеклисту

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

Внутренняя разработка

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

Итерационное развитие с минимальными рисками

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


Модификация тестов
295 / 382

Система допускает модификацию тестов под нужды разработчика используя штатный механизм Событий 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;
    }
}


Несколько советов
296 / 382
Цитатник веб-разработчиков.

Максим Месилов: Если на текущем проекте сбоит штатный функционал, то следует попробовать смоделировать аналогичную ситуацию в демо-лаборатории от 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’);


Видео
297 / 382

Скачать | Время | Размер файла
Тестирование и нагрузочные испытания - как, чем и зачем | 17 минут 43 секунды | 500 мБ

Смотреть

Просмотр видео в наших курсах в формате CHM невозможен. Ролик можно просмотреть на сайте компании «1С-Битрикс».


Senior, выше некуда
298 / 382

Senior - высший уровень квалификации. Программист этого уровня должен уметь:

  • Читать исходный код.
  • Уметь работать с Базой данных.
  • Разбираться в вопросах производительности и безопасности кода.
  • Знать как можно кастомизировать административный раздел Bitrix Framework.
  • Уметь создавать новые компоненты и модули.


Если нет описания API
299 / 382

Живое описание АПИ

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

Дмитрий Яковенко: Метод как всегда прекрасен и незадокументирован.

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

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

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

Для работы в таких условиях единственный выход у разработчика – смотреть сам код. В помощь для таких случаев есть специальный бесплатный модуль Живое описание АПИ, который сканирует текущие файлы ядра и выводит список доступных API функций и событий всех модулей.

Возможности модуля:

  • Модуль доступен только пользователю с правами администратора системы.
  • Все модули сканируются последовательно один раз, после этого рядом с live_api.php появляется файл live_api.data.php, который содержит данные о функциях;
  • Можно выбрать не только модуль, но и интересующий класс;
  • В исходном коде имеющиеся функции и методы Bitrix Framework отображены в виде ссылок, которые ведут на их исходный код этих функций и методов.

Как работать?

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

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

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

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

По клику на функцию открывается её описание в новом окне. Код функции читается прямо из файла, при этом уже скрипты не парсятся, вся необходимая информация передаётся в URL.

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

Примечание: Есть и онлайн версия этого модуля, созданная Антоном Долганиным.

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


Производительность
300 / 382

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

  1. ресурсы сервера;
  2. настройка серверного ПО;
  3. программная платформа (БУС);
  4. прикладная разработка на платформе.

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

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

Степан Овчинников: Как всегда: универсальность и функциональность против простоты и производительности.


Теория. Особенности веб-программирования
301 / 382

Одно из отличий веб-программирования от 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 используется хранение разнообразных данных в файловой системе. Подробнее про файлы и их влияние на производительность можно посмотреть в уроке Структура файлов.

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

  • Оптимизация веб-проектов (Группа по вопросам производительности веб-проектов)
  • Производительность сайта в картинках (Блог)
  • График времени генерации страниц сайта (Блог)
  • Оптимизация времени генерации страницы (Блог)
  • Количество запросов и время выполнения функции (Сообщество разработчиков)
  • MySQL, InnoDB, Монитор производительности (Оптимизация работы MySQL)
  • Семинар «Битрикс Хайлоад» — как это было, презентации, видео (habrahabr.ru)


Кеширование при проектировании сайта
302 / 382

Кеширование

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

Максим Месилов: В 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, то кеш будет отключен для всех страниц, которые будут просмотрены в рамках сессии. Файлы кеша можно удалить в административной части на закладке Очистка файлов кеша страницы Настройки > Настройки продукта > Автокеширование.

Смотрите также:

  • Класс CPageCache
  • Класс CPHPCache

Альтернативные способы хранения кеша

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

Допустим, через определенное время, наш веб-сайт стал популярным ресурсом. Благодаря активному использованию технологии кеширования мы надежно защитили базу данных от высокой нагрузки, и она может выдержать 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 используется на одной машине, там же, где и находится сам веб-сервер, то использование сокета будет быстрее.

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

  • memcached.org

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, но данное приложение настроено неправильно или отключено. Вам нужно проверить его настройки или включить его, если требуется.

Смотрите также:

  • Инструкция по настройке кеширования (Документация для разработчиков)
  • Memcached (в составе веб-кластера) (Курс Администратор. Модули)

Кеширование в собственных компонентах

Подробное описание кеширования в компонентах описано в уроке Кеширование в собственных компонентах в рамках главы Компоненты.

Рекомендации по работе с кешированием

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

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

    Юналиев Рамиль: Если разработчик делает 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();
}

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

  • Кеширование компонентов
  • Технологии кластеризации 1С-Битрикс - эффективное кэширование в memcached (Кэширование в составе кластера)
  • Кеш не панацея (MySQL), Сколько запросов к БД оптимально, когда кешировать не стоит, и прочие моменты (Советы веб-разработчиков)
  • Битрикс и большой кэш по ID (www.yunaliev.ru)
  • Кэш [bitrix api] (www.yunaliev.ru)
  • Тегированный кеш (main 9.1.0) (Блог)
  • Класс CPHPCache (Документация для разработчиков)
  • Кэш не панацея (обсуждение темы в группе Академия 1С-Битрикс выпускников)


Проблемы при кешировании меню
303 / 382

Большой размер папки с кешем меню

Ситуация. Выявлен большой размер папки 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. А расчет выбранного уровня будет происходить после получения данных из кеша.


Примеры. Кешируем правильно
304 / 382

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

Кешируем правильно

  • Комплексный пример: класс кеширования пользователя;
  • Время кеширования (cache key and ttl);
  • Как в API правильно подставлять ключи;
  • Отключаем сброс ключей;
  • Итоги рекомендаций по кешированию.

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

Но работа кеша может быть организована не совсем корректно, что приводит к проблемам производительности.

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

Как неправильно:

// Класс кеширования пользователя с некорретным кешированием
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. Общая папка для кеша пользователей
Такой пример кеша практически не работает на проекте. На портале, где много пользователей, мы постоянно будем терять кеш , что негативно отразиться на его работе.
2. Время кеширования
Кеширование сущности в примере на 1 час - неэффективно. Сохранение на малое время может привести к массовой генерации кеша . Такая проблема актуальна для больших компаний и проектов.
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 на GetList ;
  • Заменили 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']);


Оптимизация выборки дополнительных данных
305 / 382

Использование метода 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())
{
        // Делаем что то
}


Выборка и хранение в кеше только нужных данных
306 / 382

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

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

Важные примеры повышения производительности

  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.

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

    • Файл component_epilog.php

  4. Оптимизация размера кеша.

    Если, например, размер файла кеша превышает 1МБ, то возможно происходит избыточное кеширование, что не есть хорошо. В этом случае следует произвести анализ кешируемых данных. Чем больше размер файла кеша, тем меньше его эффективность.

  5. Ограничение объема запрашиваемой информации.

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

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

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

    Пример:

    Параметр nPageSize=20 массива arNavStartParams позволяет отобразить 20 элементов, но он делает 2 запроса к базе:

    1. "COUNT" - получение кол-ва элементов
2. "SELECT" - непосредственно выборка данных

    в то время как параметр nTopCount=4 того же массива создает сразу 1-н запрос вида:

    SELECT * FROM tbl LIMIT 4;

    Использование nTopCount гораздо эффективнее т.к. он не делает дополнительный запрос на получение записей выборки.

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


Оптимизация запросов к БД
307 / 382

Пример оптимизации запроса

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

Нельзя:

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, а затем уже апдейт остальных полей.


Как сделать сайт быстрым
308 / 382

Разработчик в большинстве случаев под ускорением загрузки сайта понимает работу по back-end. Однако посетителю всё равно за сколько миллисекунд открывается страница у разработчика, ему важно как быстро открывается страница у него самого. А тут уже важен целый комплекс показателей. Квалифицированный разработчик должен рассматривать сайт не как кусок программного кода, за который он отвечает, а как конечный комплексный продукт.

Back-End

Без отладки программного кода, конечно же, не обойтись. Здесь разработчику в помощь приходит инструмент Монитор производительности, который позволяет профилировать все страницы сайта на предмет кода, SQL запросов. Кроме того, этот модуль даёт рекомендации по тюнингу серверного ПО.

Далее необходимо заняться кешированием компонентов.

Следующий шаг - это внедрение технологии Композитный сайт.

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

Frond-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).
  • Сжатие картинок специальными утилитами.
Зачем нужно сжимать данные?
Скорости передачи данных сейчас велики. Зачем сжимать данные, борясь за байты? Сжать css - файл уменьшается с 10 до 8 kb, сжатие картинки даёт 3 kb. Что даёт такой выигрыш?

Ответ в протоколе TCP/IP и понятие Round Trip Time - время между отправкой запроса от клента серверу и получением обратного ответа. Когда браузер соединяется с сервером, они договариваются между собой о размере окна, в котором они будут обмениваться данными. У каждого клиента это окно разное, более того, оно может меняться в процессе передачи данных. Одно окно передаётся за один Round Trip Time. Если окно - 8 kb, а файл 10 kb, то нужно два Round Trip Time, то есть в два раза больше времени.

Дополнительные меры

  • Вынос ресурсов на разные домены, который позволяет обойти ограничение на 6 подключений.
  • Порядок подключения ресурсов на странице: CSS вверху, Javascript – внизу. Оба они блокируют рендеринг страницы до завершения собственной загрузки. Но если css мы не можем убрать в низ страницы: без него она просто не отобразится, то Javascript - вполне убрать вниз или же грузить асинхронно.

    Примечание: Это же относится к Яндекс.Метрике, GoogleAnalitic и все подобные внешние скрипты и инструменты. Несмотря на то, что они все внешние и должны попозже загружаться. Достаточно часто получается что несмотря на то, что эти скрипты грузятся асинхронно, эти файлы могут загрузится раньше рендеринга страницы и, соответственно, как только этот ресурс загрузился, он начинает выполняться. Совет: грузите все внешние инструменты (метрики, счётчики, кнопки Вконтакте, ФБ и прочие) в BX.ready, то есть на факт того, что страница загрузилась.

Сторонние инструменты

Для оптимизации front-end'a в качестве чек-листа можно использовать сторонние инструменты:

  • Google PageSpeed Insights
  • Audits в Chrome Developers Tools
  • YSlow


Если долго сохраняется элемент в административном разделе
309 / 382

Проблема и её признаки

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

Признаки:

  • долгое (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. Проверяем вызовы методов.

    Последовательно проверяем все вызовы методов , находящиеся внутри блока транзакции - есть ли на проекте обработчики событий, что именно вызывается в этих методах. Если результат отрицательный - разбираем методы детально и смотрим уже их (например, для метода CIBlockElement::Update таким является вызов CIBlockElement::UpdateSearch, а в нем - CSearch::Index).

    Важно! Учтите, в проекте могут быть и другие обработчики с вызовом api.

    В результате в одном из обработчиков находим http(s)-запрос к публичной детальной странице элемента. А на этой странице в компоненте включено обновление счетчика просмотров. Отключаем его - все тормоза исчезают.

    Примечание: При открытии транзакции строка (а иногда и вся таблица) блокируется от записи. Это одна из причин, почему использование транзакций запрещено в методах api. Все попытки изменения записи, кроме текущей, ставятся в очередь ожидания на уровне базы. Пока транзакция не закрыта или не отменена - очередь ожидает.

Вывод: при редактировании элемента мы блокируем элемент от других изменений, но обработчик вступает в конфликт (пытается обратиться к обновленному счетчику). В результате элемент, созданный в административной части сайта, сохраняется долго (или может не сохраниться вообще).

Далее в уроке для наглядности рассмотрим ожидаемый план сохранения элемента и то, как на него влияет вызов подобных обработчиков.

План сохранения данных без/с обработчиком

Ожидаемый план сохранения данных:

  1. Открываем транзакцию
  2. Обновляем строку в b_iblock_element
  3. Запись блокируется от внешнего изменения (другой хит) на уровне БД
  4. Закрываем транзакцию
  5. Запись разблокируется
  6. Выполняется очередь изменений с других хитов (если успела накопиться)

План сохранения данных с ранее выявленным обработчиком

  1. Открываем транзакцию
  2. Обновляем строку в b_iblock_element
  3. Запись блокируется от внешнего изменения (другой хит) на уровне БД
  4. Вызывается обработчик
  5. Идет хит на публичную страницу
  6. Пытаемся обновить счетчик просмотров (та же самая строка в b_iblock_element)

    7. ...долгое ожидание...

  7. Прерывание по таймауту. Обработчик завершен
  8. Если время исполнения скрипта не вышло - закрываем транзакцию. Вышло - просто "падаем"
  9. Запись разблокируется
  10. И тут, наконец, выполнится увеличение счетчика просмотров

Важно! Причины реализации подобных обработчиков могут быть разные. Тем не менее, в идеале на проекте их быть не должно. Это основная и главная рекомендация.

Что делать, если обработчик нельзя убрать

Повторимся, что лучше не использовать в своем проекте обработчики событий, посылающие 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
310 / 382

Снижаем нагрузку с помощью 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 есть решение, это новый фильтр .

<?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.

Подготовка собственных индексов

В штатном продукте не предусмотрены все индексы, т.к. все проекты разные. Для улучшения производительности не забывайте предусмотреть создание своих индексов. В этом поможет инструмент Монитор производительности .

Убираем count

Count очень тяжелая операция и создает больше нагрузки, чем выборка по запросу.

Проблема очень критична для Rest. Чтобы не использовать Count в случае если вам не нужно количество элементов или вы делаете импорт всех записей по фильтру, передавайте параметр 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.

Просмотр видео в наших курсах в формате CHM невозможен. Ролик можно просмотреть на сайте компании «1С-Битрикс».


Примеры оптимизации JS кода
311 / 382

Разработчики не очень часто задумываются над количеством хитов, когда делают инструмент которым сами не пользуются и не арендуют мощности под это. Они оставляют этот момент на откуп клиентам: "Купят по мощнее сервер и все залетает". Однако профессионализм разработчика состоит в том, чтобы уметь видеть все последствия своих трудов и уметь выбирать решения оптимальные не только для него, как программиста, но и для клиента.

Где искать ошибку в ajax обработчике при POST запросах

Пример уменьшения количества хитов рассмотрим на основе десктоп мессенджера Битрикс24.

Специфика этого инструмента такова, что он может выдавать просто запредельное количество хитов, ни один другой инструмент Bitrix Framework ни делает даже половины хитов мессенджера.

На Битрикс24 количество пользователей росло чуть ли не по экспоненте, количество хитов тоже, и наступил критичный момент когда игнорировать такие цифры стало нельзя:

Анализ лога по хитам выявил следующую проблему: в логе было только название обработчика, так как все данные отправлялись POST запросом на сервер. Логировать POST запросы вещь не очень приятная, поэтому нужно было искать другое решение что бы понять легальные ли это запросы или все таки паразитные.

Решение очень простое: это GET метки. Достаточно выполнять запрос вместе с GET меткой: im.ajax.php?GET_HISTORY, im.ajax.php?UPDATE_STATE и так далее.

По этим меткам получилось группировать запросы к серверу и выявить узкие места.

Были определены самые популярные метки, оптимизировано время исполнения, а часть из них полностью переделано таким образом чтобы не дергать сервер лишний раз (были созданы агенты, которые отправляли данные с помощью модуля Push & Pull).

После всех этих операций получились вот такие показатели:


Сессии и cookie
312 / 382

Про механизм и возможности сессии отлично написано в официальной документации PHP.

Внимание! Описанные в главе способы работы с сессиями актуальны с версии главного модуля 20.5.0.


Переменная $_SESSION
313 / 382

Работать напрямую с $_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)
314 / 382

Иногда возникает задача кешировать данные, которые связаны с текущим пользователем. Конечно, один из вариантов, это использовать сессию, но это не всегда подходит, так как:

  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, так как иначе возникает проблема контроля и удаления устаревших файлов, что может повлиять на работу файловой системы.

Документация по теме:

  • Документация D7: SessionLocalStorage


Сессия разделенный режим (hot&cold)
315 / 382

Введение

По умолчанию сессия в 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,.

Примеры можно посмотреть здесь.


Настройка хранения данных сессии
316 / 382

Ядро поддерживает четыре варианта для хранения (файлы, 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. Про них можно прочитать в официальной документации.

// 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,
                    ],
                ],
            ],
        ]
    ]
];

Настройка для разделённой сессии:

// 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',
                ]
            ],
        ]
    ]
];


Работа с сессиями
317 / 382

Продукт допускает работу с сессиями следующим образом:

Хранение сессий в 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);

После этого, включить в модуле проактивной защиты, хранение сессий в базе данных. В результате получаем хранение сессий в memcached средствами ядра.

Данный способ хранения сессий дает следующие преимущества:

  • нет необходимости следить за количеством старых сессий на нагруженном проекте;
  • возможность разделять сессии между серверами в кластере;
  • возможность использовать не ожидающую получения блокировки сессию;
  • возможность использовать виртуальные сессии.

Хранение сессий в БД имеет те же преимущества что и хранение сессий в memcached, но оно значительно более медленное. Поэтому рекомендуем для этой цели использовать memcached взамен БД.

Неблокирующие сессии

Одной из проблем больших проектов с множественными Ajax запросами, является частые блокировки хитов одного пользователя на ожидание получения блокировки сессии. Особенно это актуально для "Битрикс24 в коробке", где во многих местах прикрепленные к сущностям файлы отдаются пользователю, после проверки прав на PHP. Поэтому на страницах возможно построение "лесенки", из-за ожидания получения блокировки сессии. Включить не блокирующую сессию можно установкой константы, до подключения ядра продукта:

define('BX_SECURITY_SESSION_READONLY', true);

После этого сессия читается из memcached или БД не ожидая получения блокировки. Внутри продукта данная функциональность используется например при отдаче файлов.

Важно помнить, что при использование данной константы по завершению хита, сессия не будет записана. Это может привести к потере данных сохраненных в рамках хита в сессии.

Виртуальные сессии

Неблокирующих сессий достаточно для большинства случаев. Но в некоторых ситуациях, когда сессия нам не нужна совсем, использование неблокирующей сессии - избыточно, так как сессия будет создаваться в случае ее отсутствия. В результате при большом количестве не связанных между собой хитов, будет создано большое количество "мусорных" сессий. Примером таких хитов, являются хиты REST'а.

Для решения этой проблемы была добавлена Виртуальная сессия. Её суть в том, что сессия создается в памяти, не ждет блокировок и не сохраняется. Для ее включения необходимо установить константу, до подключения ядра продукта.

define('BX_SECURITY_SESSION_VIRTUAL', true);

Особо стоит обратить внимание на то, что данный тип сессии никак не сохраняется. В продукте используется при обработке rest запросов.

Примечание: Если у вас "Битрикс24 в коробке", проект с большим количеством ajax запросов или много файлов отдается с проверкой прав (например в блогах, соцсети, форуме), то лучше использовать хранение сессий в memcached средствами ядра.


Шифрованные cookies
318 / 382

Шифрованные куки

Шифрованные куки (\Bitrix\Main\Web\CryptoCookie) позволяют отправлять данные пользователю, не раскрывая содержимое и не позволяя изменять данные внутри. Доступно с версии main 20.5.400.

Конфигурация

Для того, чтобы ядро могло шифровать данные необходимо указать в настройках /bitrix/.settings.php crypto_key. По умолчанию, в новых дистрибутивах он генерируется автоматически.

Если он отсутствует, то добавьте его в ручную в файл настроек ядра:

 [
        'value' => [
          'crypto_key' => 'mysupersecretphrase', //советуем устанавливать 32-х символьную строку из a-z0-9
        ],
        'readonly' => true,
      ],
      //...
   ];

Примеры использования

Установка Cookie

Чтобы установить шифрованную cookie, достаточно создать объект, как в сниппете ниже добавить в нужный Response:

$cookie = \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 шифрованная или нет, распаковывает значение и дешифрует. Если значение не удаётся расшифровать, то будет получено пустое значение.


Использование постранички для массивов данных
319 / 382

Пример реализации выборки из нескольких инфоблоков с постраничной навигацией и сортировкой.

Задача

  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;
}


Добавление своих методов REST API
320 / 382

При создании собственных приложений для коробочных редакций возникает потребность в добавлении новых методов для них в REST API.

Порядок действий:

  1. Регистрируется обработчик события OnRestServiceBuildDescription модуля rest. Обработчик возвращает массив вот такой структуры::
    return array(
   'имя_scope' => array(
      'имя_метода' => array(
         'callback' => обработчик_метода,
         'options' => array(),
      ),
      'имя_метода' => array(
         'callback' => обработчик_метода,
         'options' => array(),
      ),
   )
);

    Имя scope - произвольное. Если регистрируется собственный scope, то он будет доступен в списке разрешений для локальных приложений. Для добавления методов в существующий scope, укажите имя метода. Если добавляются методы, доступные всем приложениям, то вместо имени указывается константа \CRestUtil::GLOBAL_SCOPE.

    Имя метода также произвольное, допускается использование как верхнего, так и нижнего регистра. Но рекомендуется придерживаться нижнего регистра. Традиционное именование - имя_scope.имя_сущности.действие.

    Обработчик метода - стандартный PHP псевдо-тип callback. Анонимные функции пока не поддерживаются.

  2. Функция-обработчик получит на вход 3 параметра:
    • ассоциативный массив данных вызова без авторизационных параметров;
    • параметр для возврата постранички;
    • экземпляр класса \CRestServer, из которого получаются некоторые полезные данные.
  3. Функция-обработчик может:
    • либо вернуть ответ (массив или скалярное значение), которые будут приведены к json- или xml-виду,
    • либо бросить исключение, которое будет перехвачено и отображено в виде REST-ошибки.

    Если требуется указать HTTP-статус ошибки, то воспользуйтесь классом \Bitrix\Rest\RestException. Если при этом ранее ядром было сгенерировано старое исключение ($APPLICATION->ThrowException()), то оно перезапишет ошибку в ответе.

  4. К моменту вызова функции-обработчика проверки прав доступа уже выполнены, а объект $USER уже проинициализирован с авторизацией привязанного к токену пользователя.

    Пример:

    class RestTest
{
   public static function OnRestServiceBuildDescription()
   {
      return array(
         'sigurdtest' => array(
            'sigurdtest.test' => array(
               'callback' => array(__CLASS__, 'test'),
               'options' => array(),
            ),
         )
      );
   }

   public static function test($query, $n, \CRestServer $server)
   {
      if($query['error'])
      {
         throw new \Bitrix\Rest\RestException(
            'Message',
            'ERROR_CODE',
            \CRestServer::STATUS_PAYMENT_REQUIRED
         );
      }

      return array('yourquery' => $query, 'myresponse' => 'My own response');
   }
}

AddEventHandler('rest', 'OnRestServiceBuildDescription', array('\RestTest', 'OnRestServiceBuildDescription'));

    http://cp.sigurd.bx/rest/sigurdtest.test?auth=qcdfzzjtp8hvcbjl42eg93wuw5n0mvsb&test=222

HTTP/1.1 200 OK

Content-Type: application/json; charset=utf-8

{
    "result": {
       "myresponse": "My own response",
       "yourquery": {
           "test": "222"
       }
   }
}

http://cp.sigurd.bx/rest/sigurdtest.test.xml?auth=qcdfzzjtp8hvcbjl42eg93wuw5n0mvsb&error=1

HTTP/1.1 402 Payment Required

Content-Type: text/xml; charset=utf-8

<?xml version="1.0" ?>
<response>
   <error>ERROR_CODE</error>
   <error_description>Message</error_description>
</response>

Как определить обработчик отсутствующего в описании REST-метода "на лету"

Для этого используйте событие onFindMethodDescription, вызываемое перед отдачей ошибки METHOD_NOT_FOUND. Оно позволяет подставить описание метода "на лету".

Как работать с навигацией в своих методах реста

\Bitrix\Main\Loader::includeModule('rest');
class ApiTest extends \IRestService
{
    public static function OnRestServiceBuildDescription()
    {
        return array(
            'apitest' => array(
                'api.test.list' => array(
                    'callback' => array(__CLASS__, 'getList'),
                    'options' => array(),
                ),
            )
        );
    }

    public static function getList($query, $nav, \CRestServer $server)
    {
        $navData = static::getNavData($nav, true);

        $res = \Bitrix\Main\UserTable::getList(
            [
                'filter' => $query['filter']?:[],
                'select' => $query['select']?:['*'],
                'order' => $query['order']?:['ID' => 'ASC'],
                'limit' => $navData['limit'],
                'offset' => $navData['offset'],
                'count_total' => true,
            ]
        );

        $result = array();
        while($user = $res->fetch())
        {
            $result[] = $user;
        }

        return static::setNavData(
            $result,
            array(
                "count" => $res->getCount(),
                "offset" => $navData['offset']
            )
        );
    }
}

AddEventHandler('rest', 'OnRestServiceBuildDescription', array('\ApiTest', 'OnRestServiceBuildDescription'));

На примере ORM класс работы с пользователями показывается как зарегистрировать свой метод для работы с таблицами пользователей. Если вам необходимо сделать постраничную навигацию для методов не ORM-классов, например, для getNavData(), то вторым параметром передайте: false ($navData = static::getNavData($nav, false);), тогда вернётся навигация соответствующая методам старого ядра.

Пример запроса:

/rest/api.test.list?order[ID]=ASC&filter[<ID]=1000&select[]=ID&select[]=NAME&start=200

Так же необходимо выставление прав доступа в вашем локальном приложении для scope примера apitest:


Работа с БД
321 / 382

Общая архитектура классов 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',
),

    Поддержка нового типа БД

    В редких случаях возникает необходимость подключения баз данных к которым невозможно использовать встроенный драйвер для PHP (например, у заказчика чрезвычайно устаревшая (или наоборот, слишком свежая) версия СУБД. Чтобы добавить в Bitrix Framework поддержку нового типа БД, необходимо:

    • Создать класс подключения (наследник Bitrix\Main\DB\Connection). В нем определить все базовые операции с БД: подключение, отключение, выполнение произвольного запроса, работу с транзакциями.
    • Создать класс SQL-хелпер (наследник Bitrix\Main\DB\SqlHelper) и возвращать его экземпляр в методе createSqlHelper. Класс предназначен для самой низкоуровневой работы с БД: добавляет экранирование, работает с датами, предоставляет доступ к базовым SQL-функциям и так далее.
    • Создать класс для результата выборки (наследник Bitrix\Main\DB\Result). В нем требуется определить методы-обертки над традиционными функциями работы с результатом выборки.

    Примечание: реальный пример: использование ORM для подключения из PHP к MSSQL в Linux от компании "Интерволга".


Пример работы с БД
322 / 382

Практический пример работы с Базой данных через 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 получаются результаты. Типизированные данные возвращаются сразу в виде типа, а не в виде строк или чисел.

Возвращаем все значения

    /*
     * Возвращаем все значения
     */
    function var1()
    {
        $recordset = $this->connection->query($this->sql);
        while ($record = $recordset->fetch())
        {
            $arResult[]=$record;
        }

        return $arResult;
    }

Функция var2. Здесь выполняется тот же самый запрос, но указывается лимит на количество получаемых элементов. В нашем случае 2.

Возвращаем первые два значения

    /*
     * Возвращаем первые два значения
     */
    function var2()
    {
        $recordset = $this->connection->query($this->sql,2);
        while ($record = $recordset->fetch())
        {
            $arResult[]=$record;
        }

        return $arResult;
    }

Функция var3. Выполняется тот же самый запрос, но указываются два дополнительных параметра. Такая запись означает, то, что возвратятся два элемента. Это последний параметр. И эти элементы возвращаются нам начиная со второй позиции. Это второй параметр. То есть отступаем два элемента и отдаем два, начиная с третьего элемента.

Возвращаем два значения, отступая два элемента от начала

    /*
     * Возвращаем два значения, отступая два элемента от начала
     */
    function var3()
    {
        $recordset = $this->connection->query($this->sql,2,2);
        while ($record = $recordset->fetch())
        {
            $arResult[]=$record;
        }

        return $arResult;
    }

Функция var4 - скалярный запрос, то есть когда возвращается первый, единственный результат выборки.

Возвращаем сразу первый элемент из запроса

    /*
     * Возвращаем сразу первый элемент из запроса
     */
    function var4()
    {
        $arResult = $this->connection->queryScalar($this->sql);

        return $arResult;
    }

Функция var5 - выполнение запроса, без получения результата. Это нужно в случае INSERT, UPDATE, DELETE.

Выполняем запрос, не возвращая результат

    /*
     * Выполняем запрос, не возвращая результат, т. е. INSERT, UPDATE, DELETE
     */
    function var5()
    {
        $this->connection->queryExecute('UPDATE b_user SET ACTIVE = \'N\' WHERE LOGIN=\'test\' ');//Заменить на UPDATE
    }

Функция var6 - модификация результата. Смотрим . С помощью метода addFetchDataModifier объявляется функцию, которая на вход принимает массив результата для одного элемента и после модификации его возвращает. В нашем случае не сложный пример: просто к полю логин после двоеточия добавляется текст Логин пользователя.

Модифицируем результат

    /*
     * Модифицируем результат
     */
    function var6()
    {
        $recordset = $this->connection->query($this->sql);
        $recordset->addFetchDataModifier(
            function ($data)
            {
                $data["LOGIN"] .= ": Логин пользователя";
                return $data;
            }
        );
        while ($record = $recordset->fetch())
        {
            $arResult[]=$record;
        }

        return $arResult;
    }

В метод fetch можно передать конвертер. Выглядит это так:

<?
$record = $recordset->fetch(\Bitrix\Main\Text\Converter::getHtmlConverter())

Допустимо использовать методы Bitrix\Main\Text\Converter::getHtmlConverter и Bitrix\Main\Text\Converter::getXmlConverter. Соответственно, они подготавливают к выводу в html и в xml. Происходит преобразование специальных символов в html сущности.


Миграция на MySQL
323 / 382

Внимание! С 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

    <?php

/*
 * Config for Oracle/MSSQL => MySQL converter.
 *
 * @date 12/02/2016
 * @author Alexander Serbul
 * @copyright Bitrix
 */

error_reporting(E_ALL);
ini_set('memory_limit', '1024M');

$config = array(
    'logFile' => '/tmp/migrate_log.txt',//log file (important for diagnostics)
    'logMigrateQueries' => false,//save queries as is to log file
    'confirmTableDeletion' => true,//
    'confirmTableCreation' => true,//
    'skipCheckTruncation' => false,//skip checking of data truncation (slow)
    'storeMigrationStatus' => false,//remember table migration status
    'statusFolder' => '/tmp/mg_status',//where to save status about table migration

    'encFrom' => 'UTF-8',//oracle data encoding
    'encTo' => 'UTF-8',//mysql data encoding

    'db' => array(

        'oracle' => array(
            'connectionString'	=> 'localhost/XE',
            'user'				=> 'bitrix1',
            'password'			=> '123456',
            'database'			=> 'bitrix1',
            'ignoreTables'		=> array(//which source tables to ignore
                'B_FILE_ACTION',
                'B_SALE_BIZVAL_CODE_1C',
                'B_SALE_DELIVERY',
                'B_SALE_DELIVERY_HANDLER',
                'B_SALE_BIZVAL_PERSONTYPE'
            )
        ),

        'mssql' => array(

            'serverName'		=> 'WIN-HO7VTHGJ7ED',
            'connectionInfo'	=> array(
                "UID"=>"sa",
                "PWD"=>"123456",
                "Database"=>"bitrix2",
                //"CharacterSet" => "UTF-8",
                'ReturnDatesAsStrings'=>true
                //'ConnectionPooling' => false,
                //'MultipleActiveResultSets' => false
            ),
            'ignoreTables'		=> array(
                'B_FILE_ACTION',
                'B_SENDER_POSTING_LOCK',
                'B_POSTING_LOCK',
                'B_SALE_BIZVAL_CODE_1C',
                'B_FAVORITE_LANG',
                'B_LEARN_EXCEPTION_LOG',
                'eshop_brand_reference_uf_intm',
                'eshop_brand_reference_uf_tstringm'
            )
        ),

        'mysql' => array (
            'host'		=> 'localhost',
            'port'		=> 3306,
            'user'		=> 'root',
            'password'	=> '',
            'database'	=> 'bitrix2',
            'ignoreTables'		=> array()
        )

    )

);

  4. Запустите конверсию командой "./converter.php convert oracle|mssql mysql".
  5. Внимательно отслеживайте диагностику, думайте дважды перед подтверждением операций. Детали процесса можно изучить в лог-файле.
  6. Если администратор не уверен в том с какими ключами нужно запускать конвертацию, то используйте команду "./converter.php" без параметров.
  7. Вручную скопируйте файлы из старой установки на новую.


Смена кодировки
324 / 382

Как перевести сайт с кодировки win1251 в UTF-8

Общий порядок действий:

    1. Перекодировать всю базу данных в UTF-8 (вероятнее всего придётся обращаться за помощью к администратору сервера).

    2. Перекодировать все файлы сайта в UTF-8 (можно сделать своими силами).

    3. В файл /bitrix/php_interface/dbconn.php добавить строки:

    define("BX_UTF", true);

    4. В файл /.htaccess добавить строки:

    php_value mbstring.func_overload 2
php_value mbstring.internal_encoding UTF-8

Перекодировать все файлы сайта в UTF-8 (второй пункт) можно выполнив команду через SSH в корневой папке сайта:

find . -name '*.php' -type f -exec iconv -fcp1251 -tutf8 -o /tmp/tmp_file {} \; -exec mv /tmp/tmp_file {} \;

Список ссылок по теме:

  • Конвертация сайта из cp1251 в UTF-8 (блог)


Кастомизация Административной части
325 / 382

Административная панель управления - HTML-код, который может быть выведен авторизованному пользователю при наличии у него достаточных прав на операции, представленные на панели управления. HTML-код представляет из себя область с кнопками, в самом верху страницы, каждая из которых предназначена для той или иной операции.

Подключение панели осуществляется с помощью функции CMain::ShowPanel. Данная функция использует технологию отложенных функций, что позволяет добавлять кнопки в панель непосредственно в теле страницы.

Добавить кнопку в панель можно с помощью функции CMain::AddPanelButton. Эту же функцию можно использовать в скрипте /bitrix/php_interface/include/add_top_panel.php, который автоматически будет подключен при выводе панели.

Панель управления имеет два основных режима:

  • Сайт - режим для работы над содержимым сайта.
  • Администрирование - административный раздел для полнофункционального управления всем проектом.

Подробную информацию о работе с элементами управления в интерфейсе Эрмитаж смотрите в курсе Контент-менеджер. Исторически в Bitrix Framework существовало еще два вида административной панели. Оба они похожи между собой (но сильно отличаются от Эрмитажа), вы их можете встретить в давно не обновлявшихся проектах.

  • Эрмитаж с точки зрения разработчика


Интерфейс "Эрмитаж" с точки зрения разработчика
326 / 382

Подключение панели

После авторизации на сайте для пользователя с соответствующими правами становится доступна панель Панель управления в верхней части страницы. С ее помощью можно:

  • управлять параметрами текущего раздела;
  • перейти к редактированию текущей страницы и включаемых областей;
  • добавить и изменить меню текущего раздела;
  • настроить параметры компонентов;
  • быстро перейти в административный раздел сайта;
  • и многое другое.
Примечание: С подробным описанием интерфейса панели вы можете ознакомиться в курсе Контент-менеджер.

Код для подключения административной панели задается в служебной области шаблона дизайна сайта сразу после тега <body>.

<?
$APPLICATION->ShowPanel();
?>

Примечание: Если пользователь не обладает достаточными правами, то панель для него не будет отображена.

Панель также можно и принудительно отображать для требуемой группы или отдельных пользователей. Для этого необходимо воспользоваться опцией Всегда показывать панель для пользователей в настройках Главного модуля, закладка Настройки.

В данном случае панель будет отображаться, но набор кнопок на ней будет зависеть от прав пользователя.

Примечание: в концепции Эрмитажа основные действия выносятся в toolbar, остальные остаются в контекстном меню.

Дополнительно

  • Нотификации для собственных модулей
  • Добавляем нотификатор в Битрикс 12 (блог)
  • Удаляем Стикеры и Рейтинги из меню Админки (блог)


Добавление кнопок на панель управления
327 / 382

При создании собственных проектов может возникнуть потребность в создании новых кнопок на Панели управления. Добавление кнопок на панель управления можно осуществить следующим образом:

<?$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

Результат добавления в шаблон сайта:


Добавление контекстного меню
328 / 382

Для добавления пунктов контекстного меню к любой кнопке панели используйте следующий код:

$APPLICATION -> AddPanelButtonMenu($btnId, $arMenuItem)

где:

  • $btnId – идентификатор кнопки;
  • $arMenuItem – массив пунктов.

Примечание: пересортировку пунктов согласно индексу сортировки можно выполнить с помощью следующей конструкции:
"RESORT_MENU" => true


Toolbar компонента
329 / 382

Для вывода 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
    ));
}


Контекстное меню элементов списка
330 / 382
  • Установить 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).


Административные страницы в публичке
331 / 382
  • Метод генерирует 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() //надписи кнопок, по умолчанию берутся из настроек инфоблока
    )
);


Пользовательские формы редактирования элементов
332 / 382

Форма добавления/изменения элементов информационных блоков является одной из самых часто используемых, а в интернет-магазинах или информационных изданиях эта форма однозначно является самой популярной в административном разделе. И несмотря на то, что внешний вид и поля формы изменяются в зависимости от настроек информационного блока, а также можно настроить вид формы редактирования элементов стандартными средствами системы, для специфических задач, иногда этого недостаточно.

В этом случае в /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);
        }
}
?>

Важно! Не забудьте указать путь к данному файлу в настройках инфоблока.

Иногда бывает необходимо выполнить изменения совсем другого характера, например, форма ввода и изменения одновременно несколько картинок, в этом случае необходимо просто создать свою новую страницу и добавить ее в административное меню.

Дополнительные материалы

  • Создание формы редактирования
  • Создание списка элементов


Кастомизация административной формы заказа
333 / 382

С версии 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 - нужен, чтобы инициализировать JAVA-скрипты, которые вам понадобятся;
  • 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, которые вызываются перед сохранением и при сохранении заказа соответственно.


Создание компонентов
334 / 382
Цитатник веб-разработчиков.

Роман Петров: Методы, которые были актуальны 2 года назад - сейчас могут быть неактуальны из-за появления готовых компонентов, делающих то же самое

Фактически, на сегодняшний момент свой компонент нужно писать лишь тогда, когда нужен абсолютно новый функционал для сайта. Если учесть тот факт, что состав стандартных компонентов довольно большой, то в большинстве случаев написание компонентов и не требуется, достаточно расширить функционал уже имеющихся.

Тем не менее, приходит время, когда разработчик должен научиться создавать свои компоненты.

Типовая последовательность действий

  • В веб-проекте, при составлении ТЗ и проектировании, выявляют и описывают возможные виды собственных компонентов.
  • Определяется пространство имен собственных компонентов, например, с использованием названия проекта. Системные компоненты Bitrix Framework размещены в пространстве имен bitrix, компоненты проекта могут размещаться в пространстве имен, к примеру, citybank.

    Внимание ! Названия создаваемых компонентов не должны пересекаться со стандартными.

  • Определяется, какой стандартный компонент можно взять за основу для создания собственного компонента. В коде стандартных компонентов много примеров типичного и правильного использования API и техник программирования, поэтому рекомендуется брать их за основу.
  • К каждому компоненту 2.0 продумывается интерфейс - какие параметры компонента должны быть доступны администратору веб-сайта для редактирования. Например, для компонента, отображающего прогноз погоды, можно в настройки для администратора вынести свойство Адрес веб-сервиса и Таймаут соединения с веб-сервисом и т.п.
  • Определяется, в каком разделе дерева компонентов в визуальном редакторе необходимо разместить данный компонент.
  • Компонент кодируется. Особое внимание уделяется настройке автокеширования компонента и профилированию его работы - он не должен выполнять запросы к базе данных в режиме кэширования, выполняет минимальное количество запросов к базе данных при устаревании кэша, хранит в кэше только необходимые данные, использует минимально возможный объем оперативной памяти (не сортирует массивы размером с десятки-сотни мегабайт и т.п).

Порядок создания собственно компонента

Выделить необходимый php-код в отдельный файл для того, чтобы использовать его потом в виде вызываемого файла несложно. Но компонент еще нужно подключить в систему с помощью файла описания, который опознается ядром Bitrix Framework, в результате чего пользователь видит в визуальном редакторе иконку с названием компонента и может настраивать его свойства.

Напомним, что компонент – это выделенный в отдельный файл php-код с законченной функциональностью, файл регистрации компонента в системе и описания его параметров, а также файлы локализации.

  • Регистрация компонента
    • Выделение необходимого php-кода в отдельный файл.
    • Создание файла описания .description.php
    • Размещение файлов в папке в собственном пространстве имен.
  • Задание параметров в коде компонента
  • Локализация
    • Подготовка файлов с текстовыми константами для компонента и файла регистрации: /lang/ru/<имя_компонента>/component.php и /lang/ru/<имя_компонента>/.description.php
    • Внесение изменения в код обоих файлов компонента для использования этих констант (подключение файла локализации делается при помощи функции IncludeTemplateLangFile).

Важно! Все ключи в $MESS, содержащие название, описание и параметры компонента, а также идентификаторы веток компонента в дереве компонентов визуального редактора должны быть уникальными в рамках всего продукта.

При создании компонента для Marketplace нужно создавать мастер установки (аналогично модулю).

Совет от М. Месилова:

Если на сайте используются компоненты собственной разработки и требуется их большое обновление, то рекомендуется использовать в префиксе номер версии: news.list.v2 так вы не поломаете старые компоненты по всему сайту и у вас будет возможность сделать новый компонент с потерей обратной совместимости.

Созданные для веб-проекта собственные компоненты могут использоваться как основа для новых компонентов, а также, по причине модульной структуры и "отчуждаемости от проекта" - эффективно использоваться в других веб-решениях.

Примечание: При создании компонента рекомендуется использовать:
  • SEF режим

а в шаблоне компонента:

  • JS-класс ,
  • буферизацию .

  • Что круче, штатные компоненты или самописные? (блог)


Дополнительные методы
335 / 382

Дополнительные методы, доступные в компонентах и шаблонах

В компонентах и шаблонах можно использовать дополнительные методы из класса 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.


Переопределение входящих переменных
336 / 382

Каждый компонент имеет набор переменных, в которых он принимает извне коды или другие атрибуты запрашиваемых данных. Например, компонент 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",
                           ),


Пользовательские движки шаблонизации
337 / 382

Шаблонизация

Компоненты могут работать с любыми движками шаблонизации, которые могут быть подключены из 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);
}


Разработка верстки шаблона компонента
338 / 382

При разработке компонентов рекомендуем использовать технологию, которая может упростить работу партнерам, студиям при разработке. Разработка с её помощью не обязательна и не нужна всем подряд. Для внесения незначительных изменений в шаблон компонента верстальщику не нужно использовать описываемый ниже файл 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. После этого веб-технолог работает по алгоритму модификации существующей верстки.


Способы передачи данных между компонентами
339 / 382

Способы передачи данных между компонентами:

  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). Использование класса обертки сложнее, но гораздо правильнее, особенно в свете создаваемого нового ядра.


Простой пример создания компонента
340 / 382

В качестве примера сделаем компонент, который выводит текущую дату и время. Причем формат вывода даты и времени задается выбором в свойствах. В реальных условиях такой компонент будет неинтересен. Но нам важно понять, как выполняется разработка компонента. В более сложных случаях все реализуется похожим образом.

Предварительные действия

Готовим 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С-Битрикс.


Пример создания компонента
341 / 382

Рассмотрим пример создания компонента для сообщений администратору об ошибке.

Описание

С помощью этого компонента можно реализовать функционал, который бы позволял пользователям сообщать ответственным за контент о найденной на сайте ошибке. Ошибка будет высылаться почтовым уведомлением. Алгоритм работы пользователя с компонентом очень простой: если пользователь находит на портале ошибку, то он выделяет текст, нажимает 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("BX", 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);
};

Список ссылок по теме:

  • Философия двух компонент (блог)
  • Супер компонент для любых проектов (блог)


Компонент интеграции визуального редактора
342 / 382

Создадим компонент, интегрирующий популярный редактор 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>


Кеширование в собственных компонентах
343 / 382

Описание

Для чего нужно кэширование в собственных компонентах

Казалось бы, проще всего обращаться напрямую через 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 нет «тяжелого» кода, то этот вариант может работать достаточно хорошо.


Внешняя авторизация
344 / 382

Иногда для авторизации (проверки имени входа и пароля) пользователя бывает необходимо использовать свои алгоритмы проверки и(или) внешние БД для хранения пользователей. Например, уже имеется база пользователей и необходимо дать им возможность авторизовываться на сайте под управлением 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.


Безопасность
345 / 382

Вопросы безопасности при программировании в Bitrix Framework.


Санитайзер
346 / 382

Санитайзер - инструмент, анализирующий введённый пользователем 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.


Защита от фреймов
347 / 382

На странице Защита от фреймов (Настройки > Проактивная защита > Защита от фреймов) можно включить/отключить ограничение работы во фрейме, а также задать исключения на действие защиты.

Запрет на использование кросс-доменных фреймов ссылающихся на страницы ресурса задается установкой заголовка X-Frame-Options в значение SAMEORIGIN.

X-Frame-Options

Данный заголовок указывает браузеру, можно ли загружать страницы сайта через <frame>/<iframe>.

Значение DENY запретит загрузку через фреймы, значение SAMEORIGIN разрешит загрузку через фреймы, но только если и фрейм, и страница, его загружающая, находятся на одном домене (Same Origin Policy).

Основная функция данной защиты — предотвращение кликджекинга; в качестве дополнительного бонуса это позволит предотвратить атаку, описанную Ben Schmidt.


Вирус на сайте
348 / 382

Под вирусом на сайте понимается определенный зашифрованный JavaScript-код, который размещается в коде страницы сайта. При его выполнении формируется так называемый iframe (HTML-элемент, позволяющий включить при отображении содержимое одной страницы в другую). Вставленный iframe указывает, как правило, на зараженную страницу, которая уже содержит более опасный код, использующий различные уязвимости браузеров для загрузки и запуска исполняемых файлов вирусов. В данной главе будет рассмотрено как с ними бороться.

Основные методы взлома

Причин заражения сайта может быть достаточно много, однако можно выделить несколько наиболее распространенных причин:

  1. Первая и самая распространенная причина - это виртуальный (разделяемый) хостинг. Архитектурно он не обеспечивает полную изоляцию клиентов, поэтому взлом одного сайта на хостинге часто ведет к взлому соседних. К сожалению, практически невозможно защититься от уязвимости хостинга. Единственное, что можно сделать, это вовремя зафиксировать заражение и обратиться к хостеру.
  2. Устаревшее или уязвимое ПО, которое располагается на том же или соседнем аккаунте хостинга. Для устаревших версий в интернете можно найти давно существующие способы взлома. Чтобы уберечься от заражения, не рекомендуется размещать на одном аккаунте хостинга коммерческий сайт и частную страницу работающую на небезопасном ПО.
  3. Чаще всего злоумышленники похищают сохраненные ftp пароли и сохраненные пароли браузера. Поэтому немаловажное значение имеет выбор паролей достаточной сложности, в том числе включающих и спецсимволы. Сильно повысить защищенность сайта может подключение CAPTCHA и двухэтапной авторизации.
  4. Не столь часто, но все-таки случается ситуация, когда низкоквалифицированные программисты (иногда и фрилансеры) наносят вред сайту. К сожалению, защититься от человеческого фактора практически нельзя.

Ложные срабатывания антивируса

Работа веб-антивируса основана на эвристическом анализе потенциально опасных частей кода, поэтому возможно и ложное срабатывание антивируса.

Главным отличием действительно вирусных блоков кода от безопасных состоит в том, что безопасные были добавлены программистом, который и сможет их опознать. Откуда появились вирусные блоки определить не получится. Эта задача может быть достаточно трудоёмкой, однако это самый простой способ.

Определение безопасности блоков кода с помощью персональных антивирусов может быть достаточно сложной задачей, а может вовсе не дать результата. Такие блоки html-кода не содержат ни троянов, ни вирусов, а содержат лишь ссылки на них, которые могу не вызвать срабатывания антивируса.

Итак, если выяснено, что блок кода, на который срабатывает веб-антивирус 1С-Битрикс не является вредоносным и не содержит ссылки на загрузку вирусов, то нужно добавить этот блок в исключение. Для этого необходимо взять некоторую строку из этого блока (достаточно длинную и уникальную) и добавить ее в исключение веб-антивируса. В результате, антивирус перестанет срабатывать на любые, обрабатываемые им блоки, содержащие указанную строку.

Если обнаружен вирус

Если все-таки выяснено, что блок содержит ссылку на вирус, то меры необходимо принимать незамедлительно. Наличие стороннего кода на сервере говорит о том, что злоумышленники могли получить доступ к файлам на сервере. В подавляющем большинстве случаев наличие вирусных блоков означает, что один из компьютеров, имеющих доступ на сервер через FTP (SSH, SFTP и т.п.), заражен вирусом и с него был похищен пароль от сервера. Поэтому при обнаружении вируса на сайте необходимо выполнить следующие шаги:

  1. Проверить все компьютеры людей, имеющим доступ к сайту (в том числе к панели администрирования 1С-Битрикс), персональным антивирусным программным обеспечением.
  2. После того, как все компьютеры были вылечены, необходимо сменить все пароли на сервере (пароли на FTP, SSH, пароли к базе данных, пароли пользователей, имеющих доступ к административному разделу сайта).
  3. Затем следует вычистить весь сторонний код на сервере. Проще всего для отслеживания изменившихся файлов использовать контроль целостности файлов 1С-Битрикс. Но это только в том случае, если вы заранее озаботились безопасностью и периодически запускаете контроль целостности файлов.
  4. Если контроль целостности не используется, то поиск изменений, оставленных хакером может быть весьма сложной задачей. Для поиска таких изменений, можно провести поиск по всем файлам на сервере, содержащим строки из блока, на который сработал веб-антивирус, поиск и ручная проверка всех недавно изменившихся файлов, проанализировать логи http сервера.


Чистка сайта от вирусов
349 / 382

Важно! Перед выполнением скрипта необходимо сделать полную резервную копию.

Даже если почистить компьютер от вирусов персональным антивирусом, то сайт все равно останется в нерабочем состоянии. Удалить вирусный код вручную очень сложно, к тому же искать зараженные файлы на дальнем хостинге не всегда возможно. Скачивать сайт на локальный компьютер тоже неразумно. Поэтому мы будем пользоваться php-скриптом.

По сути создаваемый скрипт будет обычным поиском текста в файле, только с добавлением к нему рекурсивного сканирования директорий. Скрипт показывает форму для ввода кода вируса (или иного текста, который надо найти), затем начинает сканировать все файлы с расширением .php в текущей папке и подпапках.

После выполнения скрипта список найденных файлов сохраняется в файл filelist.txt, затем он выводится на экран.

Автоматическая замена

Внимание! Использование автозамены может нарушить работоспособность сайта, поэтому, если решено ее использовать, необходимо учитывать возможные последствия.

Как можно заметить, после выполнения скрипта создается файл со списком зараженных файлов. Почему бы в таком случае просто не удалить все зараженные элементы? Автоматически удалять вирус тоже очень опасно, потому что есть серьёзная опасность потерять данные из своих скриптов.

В нашем примере мы будем использовать автозамену. Для ее выполнения добавим кнопку, по нажатию на которую введённый текст будет автоматически удаляться начиная с папки, где лежит сам скрипт, и ниже. Исходные файлы будут сохраняться с расширением .orig.

Примечание: в текстовое поле нужно будет вводить вирусный код целиком, иначе он удалится не полностью.

Скачать полученный скрипт можно здесь.

Примечание: скрипт написан для php5, на php4 потребуется заменять функцию file_get_contents.


Защита от троянов
350 / 382

Практически невозможно найти способ защититься на 100% от троянов. Однако можно воспользоваться бесплатным решением от 1С-Битрикс Поиск троянов, которое может обнаруживать наиболее характерные из них. Решение оформлено в виде модуля и бесплатно доступно в marketplace.

Данный скрипт сканирует сайт от корня в поисках подозрительных файлов. Скрипт составит список подозрительных по его мнению файлов, а также покажет причину, по которой файл признан подозрительным. Далее можно будет посмотреть файлы и переименовать их, чтобы троян стал не опасен.

Обнаруженные трояны нужно поместить в карантин, просканировать компьютер на вирусы персональным антивирусным ПО, после чего нужно сменить все пароли и обратиться к хостеру с требованием проанализировать причину взлома.

Важно! Перед началом сканирования обязательно необходимо проверить целостность ядра используя функционал Монитора качества.


Модули
351 / 382

Bitrix Framework имеет модульную структуру. Каждый модуль отвечает за управление определенными элементами и параметрами сайта: информационным наполнением и структурой сайта, форумами, рекламой, рассылкой, распределением прав между группами пользователей, сбором статистики посещений, оценкой эффективности рекламных кампаний и т.д.

Модуль - это модель данных и API для доступа к этим данным. Статические методы классов модуля могут вызываться в компонентах, шаблонах, других модулях. Также внутри контекста Bitrix Framework могут создаваться экземпляры классов.

Модули системы, главным образом, работают независимо друг от друга. Однако в целом ряде случаев функционал одних модулей основан на возможностях других. Например:

  • Модуль Торговый каталог расширяет возможности модуля Информационные блоки и позволяет выполнять настройку цен товара в зависимости от различных условий, применять к товарам наценку и скидки и т.п.
  • Модуль Документооборот позволяет организовать последовательную коллективную работу с содержимым модулей Информационные блоки и Управление структурой.

После установки системы список используемых модулей можно просмотреть на странице Управление модулями (Настройки > Настройки продукта > Модули) в административном разделе системы:

Для экономии дискового пространства неиспользуемые модули рекомендуется удалить, при этом дистрибутив модуля остается в системе, и он в любое время может быть снова установлен. При деинсталляции некоторых модулей система предлагает сохранить накопленные модулем данные (таблицы модуля). Если вы в дальнейшем планируете использовать эти данные, то при удалении модуля необходимо отметить соответствующую опцию.

Управление уровнем прав пользователей на доступ к модулям системы осуществляется отдельно для каждого модуля на странице его настроек. На этой же странице выполняется управление общими параметрами работы модулей.

Страница настроек конкретного модуля может иметь различное число вкладок и полей, в зависимости от функционала модуля. Перейти к ней можно следующими способами:

  • с помощью административного меню: Настройки > Настройки продукта > Настройки модулей > имя_модуля;
  • с помощью кнопки [image], расположенной на административной панели. Данная кнопка позволяет перейти к настройкам модуля, страницы (формы) которого открыты в текущей момент в основной рабочей области.

Примечание: Перед использованием модуля необходимо проверить установлен ли он и подключить его при помощи конструкции:
<?
use Bitrix\Main\Loader;

if (Loader::includeModule('******')) {
    // здесь можно использовать функции и классы модуля
}?>
где **** - идентификатор модуля.

Модули и компоненты

Модули в Bitrix Framework представляют собой модели и контроллеры нижнего уровня (в понятиях MVC), а компоненты - контроллеры верхнего уровня, включающие представления, которые основаны на иерархии файловой структуры сайта. Весь функционал любого сайта, как правило, реализуется на стандартных модулях, но приходится кастомизировать компоненты (или написать свои) для формирования и вывода страниц и подключать их на соответствующих страницах сайта.

Разработка модулей

Bitrix Framework допускает разработку пользовательских модулей.


Структура файлов
352 / 382

Файлы модуля располагаются в папке /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.php - файл может подключаться во всех административных скриптах модуля. Обычно в нем определяется константа ADMIN_MODULE_NAME (идентификатор модуля), используемая в панели управления;
  • .settings.php - файл настроек модуля, описывающий настройки модуля, которые можно прочитать через \Bitrix\Main\Config\Configuration::getInstance($module).


Описание и параметры
353 / 382

Описание

Каждый модуль должен быть корректно описан в системе для того, чтобы система знала, как с этим модулем работать. Некорректно описанные модули могут привести к полной или частичной неработоспособности системы (например, может не работать система обновлений).

Основным файлом, используемым системой для манипуляции модулем, является /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 form()
    {
        $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/ID модуля/options.php, предназначенный для управления параметрами модуля, назначения прав на модуль и т.п.

Параметры модуля хранятся в базе данных.

При получении параметров модуля, может использоваться значение по умолчанию, задаваемое в файле /bitrix/modules/ID модуля/default_option.php. В данном файле определяется массив $ID модуля_default_option, хранящий значения по умолчанию.

Пример файла /bitrix/modules/ID модуля/default_option.php:

<?
$support_default_option = array(
    "SUPPORT_DIR"                => "#SITE_DIR#support/",
    "SUPPORT_MAX_FILESIZE"       => "100",
    "ONLINE_INTERVAL"            => "900",
    "DEFAULT_VALUE_HIDDEN"       => "N",
    "NOT_IMAGE_EXTENSION_SUFFIX" => "_",
    "NOT_IMAGE_UPLOAD_DIR"       => "support/not_image",
    "DEFAULT_AUTO_CLOSE_DAYS"    => "7"
    );
?>

Для работы с параметрами модуля предназначен класс 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");
?>

Параметр типа файл. (Загрузка файла в модуле)

<table>
<tr> <td width="40%">
       <?
       $path_file = Option::get($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>


Административные скрипты
354 / 382

Административные скрипты - это скрипты используемые модулем в административной части системы. Они должны располагаться в каталоге /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__);


Административное меню
355 / 382

Меню административной части выводится стандартной функцией 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 для добавления пунктов административного меню.

Старый способ формирования меню

Структура массива $aModuleMenuLinks:

Array
(
    [0] => Array
        (
            [0] => заголовок пункта меню
            [1] => ссылка на пункте меню
            [2] => Array
                (
                    [0] => дополнительный ссылка для подсветки пункта меню 1
                    [1] => дополнительный ссылка для подсветки пункта меню 2
                    ...
                    [N] =>  дополнительный ссылка для подсветки пункта меню N
                )
            [3] => Array
                (
                    [ALT] => текст всплывающей подсказки на пункте меню
                    [SECTION_ID] => уникальный идентификатор раздела меню,
                                    принимает значение равное ID модуля,
                                    либо одно из следующих:
                                       FAVORITE - раздел "Избранное"
                                       GENERAL - раздел "Пользователи"
                                       MAIN - раздел "Настройки системы"

                    [SEPARATOR] => "Y" - пункт меню является заголовком раздела меню
                    [SORT] => сортировка раздела меню
                              относительно других разделов меню (только если SEPARATOR=Y)
                    [ICON] => ссылка на маленькую иконку используемую
                              в заголовке раздела меню (только если SEPARATOR=Y)
                    [BIG_ICON] => ссылка на большую иконку для использование
                                  на странице "Рабочий стол" (только если SEPARATOR=Y)
                    [INDEX_PAGE] => ссылка на иконке BIG_ICON (только если SEPARATOR=Y)
                )

        )
    [1] => Array( -//- )
    [2] => Array( -//- )
    ...
    [M] => Array( -//- )
)

Пример файла /bitrix/modules/support/admin/menu.php определяющего меню модуля "Техподдержка":

<?
// подключим языковой файл
IncludeModuleLangFile(__FILE__);

// определим права текущего пользователя
$SUP_RIGHT = $APPLICATION->GetGroupRight("support");

// если доступ не запрещён то
if($SUP_RIGHT>"D")
{
    // добавляем пункты меню в зависимости от прав

    $aModuleMenuLinks[] = Array(
            GetMessage("SUP_M_SUPPORT"),
            "",
            Array(),
            Array(
                "SEPARATOR"  => "Y",
                "SORT"       => 1000,
                "ICON"       => "/bitrix/images/support/mnu_support.gif",
                "BIG_ICON"   => "/bitrix/images/support/support.gif",
                "INDEX_PAGE" => "/bitrix/admin/ticket_desktop.php?lang=".LANGUAGE_ID."&set_default=Y"
                )
            );

    if ($SUP_RIGHT>="T")
    {
        $aModuleMenuLinks[] = Array(
                GetMessage("SUP_M_REPORT_TABLE"),
                "/bitrix/admin/ticket_desktop.php?lang=".LANGUAGE_ID."&set_default=Y",
                Array("/bitrix/admin/ticket_desktop.php"),
                Array("ALT"=>GetMessage("SUP_M_REPORT_TABLE_ALT"))
                );
    }

    $aModuleMenuLinks[] = Array(
            GetMessage("SUP_M_TICKETS"),
            "/bitrix/admin/ticket_list.php?lang=".LANGUAGE_ID."&set_default=Y",
            Array(
                "/bitrix/admin/ticket_list.php",
                "/bitrix/admin/ticket_edit.php",
                "/bitrix/admin/ticket_message_edit.php"
                ),
            Array("ALT"=>GetMessage("SUP_M_TICKETS_ALT"))
            );

    if ($SUP_RIGHT>="T")
    {
        $aModuleMenuLinks[] = Array(
                GetMessage("SUP_M_REPORT_GRAPH"),
                "/bitrix/admin/ticket_report_graph.php?lang=".LANGUAGE_ID."&set_default=Y",
                Array("/bitrix/admin/ticket_report_graph.php"),
                Array("ALT"=>GetMessage("SUP_M_REPORT_GRAPH_ALT"))
                );
    }

    if ($SUP_RIGHT>="V")
    {
        $aModuleMenuLinks[] = Array(
                GetMessage("SUP_M_CATEGORY"),
                "/bitrix/admin/ticket_dict_list.php?lang=".LANGUAGE_ID."&find_type=C&set_filter=Y",
                Array(
                    "/bitrix/admin/ticket_dict_edit.php?find_type=C",
                    "/bitrix/admin/ticket_dict_list.php?find_type=C"
                    )
                );
    }
}
?>


Взаимодействие модулей
356 / 382

Модули могут взаимодействовать между собой двумя способами: явно (прямым вызовом) и скрыто (через систему событий).

Явное взаимодействие

Явное взаимодействие с помощью 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("anothermodule", "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;
    }
}
?>


Установка и удаление
357 / 382

Установка модуля

Инсталляция модуля осуществляется в административном интерфейсе на странице Настройки > Настройки продукта > Модули нажатием кнопки Установить.

При этом будет вызван метод 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 модуля/.

Примечание: Главный модуль и модуль Управление структурой удалить нельзя.


Кастомизация и создание модулей
358 / 382
Цитатник веб-разработчиков.

Войтенко Андрей: Скажу по своему опыту (3 года): проблем, которые нельзя было решить без правки ядра, не встречал. Понятно, что не считая багов самого Битрикса. Они решались по принципу: ТП и ручная правка.

Создание нового модуля или изменение работы штатного модуля в Bitrix Framework требуется не часто, львиная доля проблем решается с помощью компонентов и их кастомизацией.

Внимание! Кастомизация модуля - это модификация ядра системы со всеми вытекающими отсюда последствиями: рисками получить неработоспособную систему после обновления, потерей права на ТП.

В силу этого кастомизация модуля - не рекомендуемая, чуть ли не запретная операция. Тем не менее, технически такая возможность имеется и в главе будет приведён пример не сложной кастомизации, если возникла такая потребность.

Создание собственных модулей описано в курсе Маркетплейс Bitrix Framework

Список ссылок по теме:

  • Конструктор модулей для Marketplace
  • Собираем обновление собственного модуля
  • Запись вебинара по Marketplace


Пример изменения работы модуля
359 / 382
Внимание! Перед тем как начать модифицировать работу модуля (то есть модифицировать ядро системы) необходимо быть уверенным, что иными средствами стоящую перед вами задачу не решить. При первом же обновлении продукта все добавленные вами изменения затрутся, и вам придется заново вносить их.

Пример модификации модуля для версии 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, то можно ознакомиться со схемой выгрузки.


Vue.js и Bitrix Framework
360 / 382

Что такое Vue.js?

Vue.js (произносится /vjuː/, примерно как view) — это фреймворк для создания пользовательских интерфейсов. В отличие от фреймворков-монолитов, Vue создан пригодным для постепенного внедрения. Его ядро в первую очередь решает задачи уровня представления (view), что упрощает интеграцию с другими библиотеками и существующими проектами. С другой стороны, Vue полностью подходит и для создания сложных одностраничных приложений (SPA, Single-Page Applications).

Vue.JS базируется на идее виртуального DOM. Это исключает прямое взаимодействие с узлами интерфейса. Первоначальная работа ведется с виртуальной копией (virtual DOM), а уже потом изменения применяются к реальным узлам интерфейса. Параллельно сравнивается реальное дерево DOM и его виртуальная копия, выявляется разница и переписывается только то, что претерпело изменения. Т.е. за короткий промежуток времени накапливаются все изменения, а потом эти изменения применяются к реальным узлам интерфейса (нодам) единым патчем. За счет этого скорость выполнения гораздо выше.

Vue.js удобно использовать, когда какие-либо данные используются в нескольких компонентах. В таком случае эти данные выносятся в отдельную библиотеку.

Преимущества Vue:

  • Простой вход, не нужно настраивать окружение, отличная документация;
  • Сочетает в себе плюсы React и Angular;
  • Фреймворк прошел этап становления;
  • Популярен среди разработчиков.

Vue.js реализует все современные подходы к разработке пользовательских интерфейсов и является легким в освоении, гибким и высоко интегрируемым со сторонними технологиями фреймворком.

Библиотека Vue входит в состав продукта «1С-Битрикс: Управление сайтом», что дает возможность использования и кастомизации компонентов Vue, встроенных в ядро BitrixFramework. К тому же, решена проблема версионности библиотеки Vue - в рамках текущего окружения BitrixFramework всегда единая версия.

В этой главе вы узнаете, как лучше всего (и проще) интегрировать Vue в BitrixFramework (CoreJS, локализации, события, наследование).

Примечание: Данное решение не подойдет для проектов, использующих SSR (Server side rendering) и серверную компиляцию (например, для однофайловых компонентов).

Подробнее с фреймворком (в том числе и с примерами его использования) вы можете ознакомиться на официальном сайте.


Начало работы
361 / 382

Перед началом работы рекомендуем ознакомиться с фреймворком на сайте разработчика.

Подключение и запуск

Для начала работы подключите Bitrix CoreJS расширение ui.vue в вашем компоненте или на php-странице с помощью \Bitrix\Main\UI\Extension::load("ui.vue");

Для работы с Vue после подключения библиотеки в глобальной области видимости будет доступен объект BX.Vue.

По умолчанию, Vue запускается в режиме production без подсказок. Чтобы на вашей установке работали все подсказки, необходимо в /bitrix/php_interface/init.php добавить следующую константу:

define('VUEJS_DEBUG', true);
Примечание: В коде ES6 (при использовании Bitrix JS CLI ) Vue подключается через конструкцию import {Vue} from 'ui.vue'. После этого можно обращаться к Vue без префикса BX.

Функции

Функции, из которых состоит объект:

  • Создание экземпляра Vue (в терминах Vue это называется приложение)

    BX.Vue.create(params) - аналог оригинального вызова new Vue(params) (документация)

  • Регистрация компонента

    BX.Vue.component(id, params) - аналог оригинального вызова Vue.component(id, params) (документация)

  • Проверка регистрации компонента

    BX.Vue.isComponent(id) - возвращает true в случае, если такой компонент зарегистрирован.

  • Регистрация глобальной директивы

    BX.Vue.directive(id, params) - аналог оригинального вызова Vue.directive(id, params) (документация)

  • Фильтрация локализаций для вставки в шаблон

    BX.Vue.getFilteredPhrases(phrasePrefix) (документация)

  • Отправка события в Единую шину событий (Event Bus)

    BX.Vue.event.$emit(...) (документация)

    BX.Vue.event.$on(...) (документация)

  • Клонирование компонента

    BX.Vue.cloneComponent(id, sourceId, mutations) (документация)

  • Метод для кастомизации компонента партнером

    BX.Vue.mutateComponent(id, mutations) (документация)

Создание приложения

Создание своего первого приложения

Нужно разместить dom-элемент на странице, где в будущем будет отрисован Vue-компонент.

В самом простом виде это выглядит так:

    <div id="vue-application"></div>
    <script>
        BX.Vue.component('bx-component', {
             template: '<div>Мое первое Vue приложение</div>'
        });

        BX.Vue.create({
            el: '#vue-application',
            template: '<bx-component/>'
        });
    </script>
Примечание: el может быть как текстовым атрибутом, так и ссылкой на DOM-элемент, если он хранится в переменной.
Содержимым template всегда указывайте ваш корневой компонент, чтобы его можно было переиспользовать в другом месте или модицифировать партнерам.

Существующий проект

Использование Vue в уже существующем проекте

Компонент во Vue - это автономное приложение, которое выполняет единственную задачу (например, показывает список смайлов или отображает список пользователей).

Хоть компоненты и могут иметь логику, взаимодействие между ними и взаимодействие с сервером лучше выносить за их пределы.

Рассмотрим на примере поля ввода сообщения (допустим, комментарий к задаче). Такой компонент должен содержать в себе логику по работе именно поля ввода текста (уметь выделять текст и т.д.), но логику сохранения результатов в базу данных он не должен реализовывать. На это есть множество причин, одна из них - этот компонент можно переиспользовать в разных местах. Поэтому наполнять его лишними знаниями не стоит, так как это усложнит его сопровождение и использование.

Лучшим решением будет внешний управляющий скрипт. Он с помощью событий будет отлавливать действие пользователя и уже самостоятельно взаимодействовать с сервером.

Управляющий скрипт

Именно в нем вызывается BX.Vue.create и заставляет Vue отрисовать шаблон в нужном месте. Это выглядит примерно так:

    class TaskManager
    {
        attachTemplate()
        {
            this.rootNode.innerHTML = '';
            this.rootNode.appendChild(document.createElement('div'));

            let restClient = BX.rest;
            let pullClient = BX.PULL;

            this.templateEngine = BX.Vue.create({
                el: this.rootNode.firstChild,
                template: '<bx-component/>',
                beforeCreate()
                {
                    this.$bitrixRestClient = restClient;
                    this.$bitrixPullClient = pullClient;
                },
                destroyed()
                {
                    this.$bitrixRestClient = null;
                    this.$bitrixPullClient = null;
                }
            });
        }

        detachTemplate()
        {
            if (!this.templateEngine)
            {
                this.templateEngine.$destroy();
            }

            return true;
        }
    }

В этой конструкции обратите внимание на следующие элементы:

В el лучше указывать не корневой элемент, куда требуется вставить Vue, а создать новый пустой элемент (пустую ноду). Это позволит применять анимацию плавного показа и скрытия (если такие действия есть в шаблоне).

В beforeCreate можно объявить следующие переменные:

  • this.$bitrixRestClient = restClient; - для предоставления компонентам возможности отправить rest-запрос;
  • this.$bitrixPullClient = pullClient - для доступа к Push&Pull клиенту.

При использовании переменных внутри компонентов вместо this указывается this.$root, и обязательно нужно проверять их наличие перед вызовом.

Если переменные объявлены при создании, необходимо удалить в методе destroyed.

Все взаимодействия управляющего скрипта и шаблона должны проходить через событийную модель Vue (глобальное событие или событие приложения).

Так отправляется событие внутрь приложения:

this.templateEngine.$emit('onComponentAction', {name: 'ComponentName'});
BX.Vue.event.$emit('onComponentAction', {name: 'ComponentName'});

Так можно подписаться на эту команду внутри приложения:

this.$root.$on('onComponentAction', this.onComponentAction);
BX.Vue.event.$on('onComponentAction', this.onComponentAction);

Если приложение не предполагает пользовательского ввода, можно управлять им с помощью Внешнего хранилища данных Vuex.


Базовые знания
362 / 382

Базовые знания

Чтобы начать работать с Vue.js, необходимо понимать, как выполнить 4 несложных действия:

  1. Как вставить переменную в шаблон:

    template: `
    <div>
        <h1>{{headerText}}</h1>
        <span>{{bodyText}}</span>
    </div>
`,

  2. Как использовать условный рендеринг:

    template: `
    <div>
        <h1>{{headerText}}</h1>
        <span v-if="isBasicsSlide">{{basicsText}}</span>
        <span v-else>{{otherText}}</span>
    </div>
`,

  3. Как показать коллекцию элементов:

    template: `
    <div>
        <h1>{{headerText}}</h1>
        <div v-for="slide in slides">
            {{slide.text}}
        </div>
    </div>
`,

  4. Кaк взаимодействовать между представлением и логикой:

    new Vue({
    el: '#app',
    data:
    {
        headerText: 'Hello, TechDay!',
        slides: [
            'Talk about Vue Basics.',
            'Talk about events features.',
            'Talk about other features.'
        ],
        slideText: ' ',
        slideIndex: 0,
    },
    methods:
    {
        nextSlide()
        {
            this.slideIndex++;

            if (typeof this.slides[this.slideIndex] === 'undefined')
            {
                this.slideIndex = 0;
            }

            this.slideText = this.slides[this.slideIndex];
        }
    },
    template:	`
        <div>
            <h1>{{headerText}}</h1>
            <div>{{slideText}}</div>
            <button @click="nextSlide">Next</button>
        </div>
    `,
});


Компоненты в Vue
363 / 382

Компоненты (общая информация)

Компонент в Vue (далее в главе просто компонент) - это автономное приложение, которое выполняет единственную задачу (например, умеет показывать список смайлов или отображает список пользователей).

Компоненты Vue.js — это переиспользуемые экземпляры Vue со своим именем.

Подробнее о всех возможностях компонентов вы можете прочитать в документации Vue Основы компонентов и Продвинутые компоненты.

Примеры работы:

Определим новый компонент, назовем его bx-button-counter.

BX.Vue.component('bx-button-counter',
{
    data() {
        return {
            count: 0
        }
    },
    template: '<button @click="count++">Счётчик кликов — {{ count }}</button>'
})

Его можно использовать как пользовательский тег внутри корневого экземпляра Vue, созданного с помощью BX.Vue.create({...}) или в шаблоне другого компонента.

<div id="vue-application"></div>
<script>
    BX.Vue.component('bx-button-counter',
    {
        data()
        {
            return {
                count: 0
            }
        },
        template: '<button @click="count++">Счётчик кликов — {{ count }}</button>'
    });

    BX.Vue.create({
        el: '#vue-application',
        template: '<bx-button-counter/>'
    });
</script>

Так как компоненты Vue.js - это переиспользуемые экземпляры Vue, то они принимают те же опции, что и BX.Vue.create, такие как data, computed, watch, methods, хуки жизненного цикла.

Единственными исключениями будут несколько специфичных для корневого экземпляра опций, например, el.

Переиспользование компонентов

Компоненты можно переиспользовать неограниченное количество раз:

<div id="vue-application"></div>
<script>
    BX.Vue.component('bx-button-counter',
    {
        data()
        {
            return {
                count: 0
            }
        },
        template: '<button @click="count++">Счётчик кликов — {{ count }}</button>'
    });

    BX.Vue.create({
        el: '#vue-application',
        template: `
            <div class="buttons">
                <bx-button-counter/>
                <bx-button-counter/>
                <bx-button-counter/>
            </div>
        `
    });
</script>

Приложение организуется в виде дерева вложенных компонентов:

Например, могут быть компоненты для заголовка, боковой панели, зоны контента, каждый из которых может содержать другие компоненты для навигационных ссылок, постов блога и т.д.

Для использования этих компонентов в шаблоне они должны быть зарегистрированы, чтобы Vue узнал о них. Есть два типа регистрации компонентов: глобальная и локальная. В рамках экосистемы Битрикс используется только глобальная регистрация, подробнее о создании и причинах глобальной регистрации описано в уроке Создание компонентов.


Создание компонентов
364 / 382

Создание компонентов

Глобальные компоненты располагаются в модуле ui в директории /modules/ui/install/js/ui/vue/components/. Компоненты, относящиеся к вашему модулю, вы можете располагать в директории /modules/<module>/install/js/<some_path>.

Все компоненты должны быть в виде расширений СoreJS в новом формате. В общих чертах ваш компонент будет выглядеть следующим образом:

/some-component/config.php
/some-component/lang/<...>/config.php
/some-component/module-name.extension-path.some-component.js

Cодержимое файла config.php:

<?
if (!defined("B_PROLOG_INCLUDED") || B_PROLOG_INCLUDED !== true)
{
   die();
}

return array(
   'js' => Array(
      '/bitrix/js/module-name/extension-path/some-component/module-name.extension-path.some-component.js',
   ),
   'rel' => array('ui.vue'),
   'skip_core' => true,
   'bundle_js' => 'your-bundle-name'
);

В зависимостях обязательно должен быть ui.vue. Если не используются библиотеки CoreJS внутри директивы, нужно указать ключ 'skip_core' => true

Содержимое файла module-name.extension-path.some-component.js:

(function(window)
{
   "use strict";

   /**
    * Module name
    * Some Component Vue component
    *
    * @package bitrix
    * @subpackage moduleName
    * @copyright 2001-2019 Bitrix
    */

   const BX = window.BX;

   BX.Vue.component('bx-moduleName-someComponent',
   {
        /**
         * @emits 'sendEvent`' {text: string}
         */

        /**
         * @listens 'onModuleNameSomeComponentEvent' {} (global)
         */

        props:
        {
            ...,
            doSomethingEventName: { default: 'onModuleNameSomeComponentDoSomethingEvent' },
        },

        data()
        {
            return {
                ...
            }
        },

        created()
        {
            BX.Vue.event.$on('onModuleNameSomeComponentEvent', this.onDoSomething);
            ...
        },

        beforeDestroy()
        {
            BX.Vue.event.$off('onModuleNameSomeComponentEvent', this.onDoSomething);
            ...
        },

        methods:
        {
            onDoSomething(event)
            {
                ...
            }
        },

        ...,

        computed:
        {
            ...,

            localize(state)
            {
                return BX.Vue.getFilteredPhrases('BX_MODULE-NAME_COMPONENT-PREFIX_');
            }
        },

        template: `
            <div class="some-component">
            </div>
        `

   });

})(window);

Все параметры компонентов описаны в документации Vue Основы компонентов и Продвинутые компоненты.

Ниже остановимся только на особенностях работы в рамках системы Bitrix Framework.

Имя компонента должно начинаться с префикса bx- для исключения пересечения с директивами внешних разработчиков, далее указываются название модуля и название компонента.

Компоненты VueJS написаны таким способом, чтобы при беглом взгляде можно было сразу понять, какие используются входные параметры (внешние переменные props), какие используются внутренние данные (внутренние переменные data). К сожалению, для компонентов Vue не предусмотрен описательный формат для событий, генерируемых компонентом во внешнюю среду (emits), и для событий, которые слушаются компонентом из внешней среды (listens). Однако в рамках экосистемы Битрикс нужно описывать их в формате комментариев JSDoc в начале компонента.

Важно! Если ваш компонент создает или ожидает события, обязательно описывайте их в начале компонента, чтобы другие разработчики знали о их наличии, даже не вникая в код.

/**
 * @emits 'sendEvent`' {text: string}
 */

/**
 * @listens 'onModuleNameSomeComponentEvent' {} (global)
 */

Подробнее о событиях и слушателях, а также о правилах их оформления смотрите в уроке События и слушатели в компонентах.

Все js-компоненты Битрикс, как их php-версии, должны иметь поддержку локализаций. Для этого необходимо подготовить вычисляемое свойство computed.localize:

computed:
    {
        localize(state)
        {
            return BX.Vue.getFilteredPhrases('BX_MODULE-NAME_COMPONENT-PREFIX_');
        }
    }

Так как все вычисляемые свойства попадают в шаблон и по умолчанию реактивны, необходимо использовать функцию BX.Vue.getFilteredPhrases для добавления в шаблонизатор только нужных фраз и отключения для них реактивности (отслеживания изменений). Подробнее о локализации можно прочитать в соответствующем уроке.

Компоненты Битрикс, в отличие от обычных компонентов Vue, поддерживают возможность клонирования разработчиками и модификации (мутации) партнерами, поэтому при регистрации компонентов необходимо указывать не Vue.component(...), а метод BX.Vue.component(...) (все параметры полностью совпадают с синтаксисом Vue).


События и слушатели в компонентах
365 / 382

События и слушатели

Компоненты в рамках Vue - автономные структуры, у которых логика и данные находятся в предсказуемых местах. Зачастую даже при беглом взгляде можно сразу понять, какие используются входные параметры (props) и какие используются внутренние переменные (data).

Чтобы компоненты могли взаимодействовать друг с другом, в Vue существует возможность распространять события с помощью .$emit('eventName', eventParams) и подписываться (слушать) через .$on('eventName', this.eventCallback);.

Для компонентов Vue не предусмотрен описательный формат для событий, генерируемых компонентом во внешнюю среду (emits), а также для тех событий, которые компонент слушает из внешней среды (listens). Однако в рамках системы Bitrix Framework вам следует описывать их в формате комментариев JSDoc в начале компонента.

emits

<з>События в компонентах (emits)

Примечание: Если ваш компонент создает событие компонента (локальное событие), необходимо его описать в начале компонента, для того чтобы другие разработчики (не вникая в код вашего компонента) знали, что при его использовании в верстке они могут подписаться на событие.

Описание JSDoc для события компонента this.$emit('sendMessage', {text: 'Some text'}):

/**
 * @emits 'sendMessage' {text: string}
 */

Имя события компонента обычно не содержит префикса on, так как потом оно используется как атрибут при вызове вашего компонента и должно органично смотреться в верстке.

Если используется глобальное событие или событие приложения и о нем должны знать другие разработчики, их также можно описать, поставив метку (global) или (application).

Описание JSDoc для глобального события BX.Vue.event.$emit('onComponentInit', {name: 'ComponentName'}):

/**
 * @emits 'onComponentInit' {name: string} (global)
 */

Описание JSDoc для события приложения this.$root.$emit('onComponentInit', {name: 'ComponentName'}):

/**
 * @emits 'onComponentInit' {name: string} (application)
 */

Отличия событий

Отличия глобальных событий, событий приложений и событий компонента

События компонента (локальные события) создаются компонентом, и поймать их может либо текущий компонент, либо родительский компонент, повесив обработчик в виде html-атрибута в шаблоне:

template: `
    <div>
        <bx-component @sendMessage="onSendEventHandle"/>
    </div>
`

По сути, это информирование вышестоящего компонента о выполнении какого-либо события, чтобы он мог исполнить какую-то свою логику.

Глобальные события и события приложения используются для того, чтобы выполнить какое-либо действие в другом компоненте (вызов может быть в разных частях приложения и на разном уровне вложенности).

Между Глобальными событиями и событиями приложения тоже есть разница (забегая вперед - предпочтительнее пользоваться событиями приложения).

В одном экземпляре Vue (в терминах Vue это называется приложение) могут находиться десятки разных компонентов. Чтобы наладить между ними общение, достаточно использовать функцию this.$root.$emit('sendMessage', {text: 'Some text'}) для отправки события и this.$root.$on('sendMessage', this.eventCallback); для подписки на это событие.

this.$root означает, что вы отправляете событие в корневой компонент и подписываетесь на получение этого события тоже в корневом компоненте. Обычно этого достаточно, но если на странице используются несколько экземпляров Vue (например, мессенджер и всплывающие уведомления), то в этом случае не получится наладить между ними общение, т.к. они находятся в совершенно разных приложениях.

Для решение этой задачи используется подход Event Bus - Единая шина событий. Она не привязана к конкретному экземпляру Vue и позволяет обмениваться событиями, даже находясь за пределами экземпляров Vue.

Важно! После выпуска вашего компонента клиентам вы не сможете изменить тип события, т.к. это может сломать логику других приложений, но иногда это требуется. В таком случае просто продублируйте вызов в нужном вам контексте.

listeners

Слушатели в компонентах (listeners)

Примечание: Если ваш компонент слушает возникновение глобального события или события приложения, обязательно опишите его в начале компонента, чтобы другие разработчики (не вникая в ваш код сразу) знали, что компонент умеет реагировать на внешние события.

В имени события должен быть префикс вашего модуля, название компонента и название самого действия.

Описание JSDoc для подписки на глобальное событие BX.Vue.event.$on('onModuleNameSomeComponentEvent', this.onInsertText);:

/**
 * @listens 'onModuleNameSomeComponentEvent' {} (global)
 */

Описание JSDoc для подписки на событие приложения this.$root.$on('onModuleNameSomeComponentEvent', this.onInsertText);:

/**
 * @listens 'onModuleNameSomeComponentEvent' {} (application)
 */

Подписку на такие события вам нужно сформировать в методе компонента created и не забыть отписаться от события в методе beforeDestroy:

BX.Vue.component('bx-component', {
    /**
     * @listens 'onModuleEventName' {text: string} (global)
     */
    created()
    {
        BX.Vue.event.$on('onModuleEventName', this.onEventNameHandle);
    },
    beforeDestroy()
    {
        BX.Vue.event.$off('onModuleEventName', this.onEventNameHandle);
    },
    ...
});

Если разработчик захочет вызвать ваше событие, он должен воспользоваться методом BX.Vue.event.$emit('onModuleNameSomeComponentEvent', {text: 'Some text'}) (для глобально события) и this.$root.$emit('onModuleNameSomeComponentEvent', {text: 'Some text'}) (для события приложения).

Примеры

  • Пример работы событий
  • Пример объединения компонентов
  • Пример создания универсального компонента

Пример работы событий

Давайте рассмотрим на примере мессенджера:

Синим показан компонент выбора смайлов (smiles), красным показан компонент ввода и отправки нового сообщения (textarea).

Оба компонента полностью независимы и не взаимодействуют напрямую друг с другом (они даже не знают о существовании друг друга).

Компонент textarea подразумевает, что в него могут внешние компоненты публиковать текст, для этого он слушает событие onTextareaInsertText. На этом его знания об окружающем мире заканчиваются. Это значит, что он может использоваться в совершенно разных контекстах и не привязан к текущей логике.

Компонент smiles так же может использоваться в разных контекстах, поэтому сам напрямую никому не отправляет данные, а формирует событие onSmileClick при клике на смайл. При этом совсем не важно, кто будет обрабатывать это событие.

Пример объединения компонентов

Компоненты объединены общим пространством. Это может быть родительский компонент или, в более сложных случаях, управляющих скрипт или единая шина событий.

В примере, приведенном чуть выше, два компонента объединены общим компонентом. Он подписывается на событие компонента smile, обрабатывает результат клика и отправляет эти данные через формирование события на обработчик компонента textarea.

Выглядит это так:

// Компонент кнопок со смайлами
BX.Vue.component('bx-smiles', {
    /**
     * @emits 'smileClick' {text: string} (global)
     */
    methods: {
        buttonClick(event)
        {
            this.$emit('smileClick', {text: event.target.innerHTML})
        }
    },
    template: `
        <div>
            <button @click="buttonClick"> :) </button>
            <button @click="buttonClick"> :( </button>
            <button @click="buttonClick"> :D </button>
        </div>
    `
});

// Компонент вывода результата
BX.Vue.component('bx-textarea',
{
    /**
     * @listens 'onTextareaInsertText' {text: string} (global)
     */
    data()
    {
        return {
            selectedSmile: ''
        }
    },
    created()
    {
        BX.Vue.event.$on('onTextareaInsertText', this.onInsertText);
    },
    beforeDestroy()
    {
        BX.Vue.event.$off('onTextareaInsertText', this.onInsertText);
    },
    methods:
    {
        onInsertText(event = {})
        {
            this.selectedSmile = event.text;
        }
    },
    template: `
        <div>
            <div v-if="selectedSmile">Был выбран смайл: <b>{{selectedSmile}}</b></div>
        </div>
    `
});

// Связывающий компонент
BX.Vue.component('bx-messenger',
{
    methods:
    {
        onSmileClickEvent(event = {})
        {
            BX.Vue.event.$emit('onTextareaInsertText', {text: event.text})
        }
    },
    template: `
        <div>
            <bx-smiles @smileClick="onSmileClickEvent"/>
            <bx-textarea/>
        </div>
    `
});

BX.Vue.create({
    el: '#vue-application',
    template: `<bx-messenger/>`
});

В данном примере использовались глобальные события, но вполне возможно было использовать и события приложения.

Пример создания универсального компонента

Если компонент универсальный и может быть использован в разных местах, то могут возникнуть проблемы: кто-то отправит событие и это событие отработает во всех компонентах данного типа.

Рассмотрим на примере: есть две кнопки, первая публикует один смайл, другая публикует другой смайл - результат нажатия этих двух кнопок публикуется в компонент вывода результата (универсальный).

Как решить озвученную проблему? Нужно сделать название события динамическим и передавать его через входные параметры компонента.

Для таких событий нужно немного по-другому формировать JSDoc @listens. Вместо названия события необходимо указать, в какой переменной хранится значение, в формате props.varEventName, где varEventName это название переменной из объекта props.

BX.Vue.component('bx-result',
{
    /**
     * @listens props.resultEventName {text: string} (global)
     */
    props:
    {
        resultEventName: { default: '' } // Название переменной намеренно оставлено пустым, чтобы избежать проблемы одинакового именования
    },
    data()
    {
        return {
            result: ''
        }
    },
    created()
    {
        if (this.resultEventName)
        {
            BX.Vue.event.$on(this.resultEventName, this.onInsertText);
        }
    },
    beforeDestroy()
    {
        if (this.resultEventName)
        {
            BX.Vue.event.$off(this.resultEventName, this.onInsertText);
        }
    },
    methods:
    {
        onInsertText(event = {})
        {
            this.result = event.text;
        }
    },
    template: `
        <div>
            <div v-if="result">Был выбран смайл: <b>{{result}}</b></div>
        </div>
    `
});

BX.Vue.component('bx-button-one',
{
    /**
     * @emits 'buttonClick' {text: string} (global)
     */
    methods:
    {
        buttonClick(event)
        {
            this.$emit('buttonClick', {text: event.target.innerHTML})
        }
    },
    template: '<button @click="buttonClick"> :) </button>'
});

BX.Vue.component('bx-button-two',
{
    /**
     * @emits 'buttonClick' {text: string} (global)
     */
    methods:
    {
        buttonClick(event)
        {
            this.$emit('buttonClick', {text: event.target.innerHTML})
        }
    },
    template: '<button @click="buttonClick"> :( </button>'
});

BX.Vue.component('bx-example',
{
    methods:
    {
        buttonClickEvent1(event = {})
        {
            BX.Vue.event.$emit('onResultButton1', {text: event.text})
        },
        buttonClickEvent2(event = {})
        {
            BX.Vue.event.$emit('onResultButton2', {text: event.text})
        },
        clear(event = {})
        {
            BX.Vue.event.$emit('onResultButton1', {text: ''});
            BX.Vue.event.$emit('onResultButton2', {text: ''});
        }
    },
    template: `
        <div>
            <bx-button-one @buttonClick="buttonClickEvent1"/>
            <bx-button-two @buttonClick="buttonClickEvent2"/>
            <bx-result resultEventName="onResultButton1"/>
            <bx-result resultEventName="onResultButton2"/>

            <button @click="clear">Очистить данные</button>
        </div>
    `
});

BX.Vue.create({
    el: '#vue-application',
    template: `<bx-example/>`
});


Работа с локализациями
366 / 382

Работа с локализациями

Все Vue-компоненты в рамках Bitrix Framework, как и их стандартные php-компоненты, должны иметь поддержку локализаций. Для этого необходимо подготовить вычисляемое свойство computed.localize:

computed:
{
    localize(state)
    {
        return BX.Vue.getFilteredPhrases('PREFIX_');
    }
}

Метод BX.Vue.getFilteredPhrases прочитает объект BX.message и выберет только те фразы, которые относятся к вашему компоненту (по префиксу) и добавит их в шаблонизатор.

В шаблоне вы будете использовать их в таком формате:

template: `
    <div :title="localize.PREFIX_NAME_TITLE">
        <div>{{localize.PREFIX_NAME}}</div>
        <div v-html="localize.PREFIX_NAME_WITH_HTML"></div>
    </div>
`

Обратите внимание, если вам требуется вставить локализацию вместе с html-разметкой, используйте для этого v-html. Однако в таком случае вы должны быть уверены, что во фразе нет XSS-инъекций.

В таком случае, вам нужно будет использовать следующую конструкцию:

computed:
{
    localize(state)
    {
        return Object.freeze({
            ...BX.Vue.getFilteredPhrases('PREFIX_'),
            'PREFIX_NAME_WITH_HTML': removeAllXssFunction(BX.message('PREFIX_NAME_WITH_HTML'))
        });
    }
}

Инъекции в шаблон

Почему нельзя использовать BX.message внутри шаблона, например, так?

template: `
    <div :title="'BX.message('PREFIX_NAME_TITLE')">
        <div>${BX.message('PREFIX_NAME')}</div>
    </div>
`

Можно, но такой подход (инъекции в шаблон) не принят в сообществе Vue. Все данные, которые появляются в шаблоне, должны быть внутри шаблонизатора либо как свойства, либо как вычисляемые свойства.

Кроме того, необходимо будет всегда отслеживать, чтобы во фразах не было XSS-уязвимостей.

Если по какой-то причине вы не хотите использовать функцию BX.Vue.getFilteredPhrases, то можно собрать справочник фраз самостоятельно, используя следующий формат:

computed:
{
    localize(state)
    {
        return Object.freeze({
            'PREFIX_NAME_TITLE': BX.message('PREFIX_NAME_TITLE'),
            'PREFIX_NAME': BX.message('PREFIX_NAME')
        })
    }
}

Подробнее о интерполяции можно прочитать в документации Vue Интерполяции.

Подробнее о вычисляемых свойствах вы можете прочитать в документации Vue Вычисляемые свойства.


Создание нового компонента на основе существующего (клонирование)
367 / 382

Новый из старого

Создание нового компонента на основе существующего. Бывает, что компонент подходит, но за исключением одного действия (например, нужно, чтобы при выборе смайла он вставлялся не как :smile:, а как [smile]) или же не подходит шаблон компонента по умолчанию. При этом остальная работа компонента устраивает (например, выборка данных из базы, действия, события).

В таком случае проще всего клонировать компонент и изменить в нем только то, что нужно.

Важно! Клонируя компонент, вы наследуете поведение этого компонента. Хотя мы и стараемся соблюдать обратную совместимость везде, где это возможно, вам необходимо проверять свои мутации после обновлений продукта (чтобы убедиться, что ваш компонент работает именно так, как вы предполагали).

Модернизация

Как модернизировать компонент (мутация)

Схема похожа на схему создания нового компонента.

Клон компонента необходимо расположить в директории своего модуля /modules/<module>/install/js/<some_path>.

Клон должен быть в виде расширения СoreJS в новом формате. В общих чертах клон будет выглядеть следующим образом:

/some-component/config.php

/some-component/lang/<...>/config.php

/some-component/module-name.extension-path.some-component.js

Cодержимое файла config.php:

<?
if (!defined("B_PROLOG_INCLUDED") || B_PROLOG_INCLUDED !== true)
{
   die();
}

return array(
   'js' => Array(
      '/bitrix/js/module-name/extension-path/some-component/module-name.extension-path.some-component.js',
   ),
   'rel' => array('module.source.component'),
   'skip_core' => true,
   'bundle_js' => 'your-bundle-name'
);

В зависимостях нужно указать существующий компонент, чтобы он был подгружен перед клонированием.

Содержимое файла module-name.extension-path.some-component.js:

(function(window)
{
   "use strict";

   /**
    * Module name
    * Some Component Vue component
    * Clone of module.source.component
    *
    * @package bitrix
    * @subpackage moduleName
    * @copyright 2001-2019 Bitrix
    */

   const BX = window.BX;

   BX.Vue.cloneComponent('bx-moduleName-someComponent', 'bx-source-component-name',
   {
        template: `
            <div class="some-component">
                {{ result }}
            </div>
        `
   });

})(window);

Синтаксис метода: BX.Vue.cloneComponent(id, sourceId, mutations), где:

  • id - идентификатор нового компонента
  • sourceId - идентификатор компонента, с которого будет снята копия
  • mutations - параметры, которые будут заменены в оригинальном компоненте

Подробнее о мутациях

Параметры этого массива полностью совпадают с массивом params в момент создания компонента, все ключи описаны в документации Vue Основы компонентов и Продвинутые компоненты.

В массиве mutations нужно указать только те ключи, которые требуется изменить.

Рассмотрим пример: пусть нужно заменить метод sendText в оригинальном компоненте. Тогда объект mutations будет выглядеть так:

{
    methods:
    {
        sendText()
        {
            ...
        }
    }
}

Все остальные функции останутся на местах и они не будут затронуты. Если нужно добавить новый метод, это делается так же, как и замена существующего.

Правила мутации применяются ко всему дереву параметров, таким как methods, params, template.

  • Мутация строковых значений

    Строковые значения могут быть заменены на новые с возможностью вставить предыдущий текст в текст нового. Для этого в новом тексте нужно указать название этого параметра со знаками номера по обе стороны от переменной. Например, это применимо для параметра template: чтобы в новом компоненте можно было использовать шаблон старого, добавьте в новый текст #PARENT_TEMPLATE#.

    Представим, что шаблон ранее имел вид:

    {
    template: "<span>123</span>"
}

    Допустим, вы хотите использовать старый шаблон в своем новом шаблоне:

    {
    template: "<div>#PARENT_TEMPLATE#</div>"
}

    Итоговой версткой после отрисовки компонента будет:

    <div><span>123</span></div>

  • Мутация объектов

    Объекты (такие как params, methods, computed) могут быть расширены или заменены новыми значениями.

    Кроме того, при замене уже существующих параметров в объектах есть возможность получить доступ к предыдущему значению, написав слово parent у названия ключа. При этом первую букву ключа нужно будет перевести в верхний регистр. Например, если было вычисляемое свойство dateText, то после его изменения к предыдущему значению можно обратиться, используя parentDateText.

    Рассмотрим пример замены метода. Допустим, есть метод sendText, который отправляет текст вышестоящему компоненту:

    {
    methods:
    {
        sendText(text)
        {
            this.emit('send', text);
        }
    }
}

    Вас устраивает работа метода, но вы хотите модифицировать текст перед отправкой:

    {
    methods:
    {
        sendText(text)
        {
            // модифицируем текст
            text = '['+text+']';

            // вызываем родительский метод но уже с другим текстом
            this.parentSendText(text);
        }
    }
}

    Рассмотрим пример замены вычисляемого свойства на примере локализаций.

    Вот вычисляемое свойство, которое формирует данные локализаций в оригинальном компоненте:

    {
    computed:
    {
        localize()
        {
            return BX.Vue.getFilteredPhrases('IM_MESSENGER_MESSAGE_');
        },
    },
}

    Вам нужно добавить свои, не потеряв при этом оригинальные:

    {
    computed:
    {
        localize()
        {
            return Object.assign({},
                this.parentLocalize,
                BX.Vue.getFilteredPhrases('IMOL_MESSAGE_')
            );
        },
    },
}

    Важно! Этот метод отработает корректно, даже если в оригинальном компоненте не было локализаций, т.к. Object.assign пропустит объединение с undefined свойством.

Примечание: Все остальные значения будут заменены полностью на новые.

Пример клонирования

Пусть есть компонент, которые выводит значения от 1 до 10 в обратном порядке,в виде черного текста. Клонируем его несколько раз.

Первый клон будет выводить цифры в цвете, для четных будет красный цвет, а нечетных зеленый.

Второй клон будет выводить цифры в цвете, в случайном порядке.

BX.Vue.component('bx-digits', {
    data()
    {
        return {
            digits: [1,2,3,4,5,6,7,8,9,10]
        }
    },
    computed:
    {
        preparedDigits()
        {
            return this.digits.reverse();
        }
    },
    template: `
        <div>
            <span v-for="digit in preparedDigits" style="padding: 0 5px;">{{digit}}</span>
        </div>
    `
});

BX.Vue.cloneComponent('bx-digits-color', 'bx-digits', {
    template: `
        <div>
            <template v-for="digit in preparedDigits">
                <span v-if="digit % 2" style="color: green; padding: 0 5px;">{{digit}}</span>
                <span v-else style="color: red; padding: 0 5px;">{{digit}}</span>
            </template>
        </div>
    `
});

BX.Vue.cloneComponent('bx-digits-color-order', 'bx-digits', {
    computed:
    {
        preparedDigits()
        {
            return this.shuffleArray(this.digits)
        }
    },
    methods:
    {
        shuffleArray(array)
        {
            for (let i = array.length - 1; i > 0; i--)
            {
                const j = Math.floor(Math.random() * (i + 1));
                [array[i], array[j]] = [array[j], array[i]];
            }
            return array;
        }
    },
    template: `
        <div>
            <template v-for="digit in preparedDigits">
                <span v-if="digit % 2" style="color: green; padding: 0 5px;">{{digit}}</span>
                <span v-else style="color: red; padding: 0 5px;">{{digit}}</span>
            </template>
        </div>
    `
});

BX.Vue.create({
    el: '#vue-application',
    template: `
        <div>
            <div>Оригинальный компонент</div>
            <bx-digits/>
            <br/>
            <div>Клонированный компонент - цвета</div>
            <bx-digits-color/>
            <br/>
            <div>Клонированный компонент - цвета и порядок</div>
            <bx-digits-color-order/>
        </div>
    `
});

Оригинальный компонент
10 9 8 7 6 5 4 3 2 1

Клонированный компонент - цвета
10 9 8 7 6 5 4 3 2 1

Клонированный компонент - цвета и порядок
4 1 3 2 10 5 6 9 8 7

Пример использования предыдущих значений при клонировании

Базовый компонент выводит числа от 1 до 10 в обратном порядке, при клике на цифру она окрашивается сначала в зеленый, потом в красный цвет.

Клонированный компонент наследует поведение предыдущего и вносит следующие коррективы:

  1. шаблон вывода обрамляется красной рамкой
  2. при клике помимо изменения цвета и шрифта добавляется цвет для заднего фона
BX.Vue.component('bx-digits',
{
    data()
    {
        return {
            digits: [1,2,3,4,5,6,7,8,9,10]
        }
    },
    computed:
    {
        preparedDigits()
        {
            return this.digits.reverse();
        }
    },

    methods:
    {
        changeColor(event)
        {
            if (event.target.style.color == "green")
            {
                event.target.style.color = "red";
            }
            else
            {
                event.target.style.color = "green";
            }
        }
    },

    template: `
        <div>
             <span v-for="digit in preparedDigits" @click="changeColor" style="padding: 0 5px; cursor: pointer">
                {{digit}}
            </span>
        </div>
    `
});

BX.Vue.cloneComponent('bx-digits-parent', 'bx-digits', {
    computed:
    {
        // заменяем существующие в оригинале вычисляемое свойство
        preparedDigits()
        {
            // получаем доступ к предыдущему состоянию свойства через this.parent<НазваниеСвойства>
            let reverseDigit = [].concat(
                this.parentPreparedDigits
            );

            // добавляем к отсортированным значениям цифру ноль в конец.
            reverseDigit.push(0);

            return reverseDigit;
        }
    },

    methods:
    {
        // заменяем существующие в оригинале функцию
        changeColor(event)
        {
            // оставляем предыдущее поведение
            this.parentChangeColor(event);

            // добавляем новое
            if (event.target.style.color == "green")
            {
                event.target.style.backgroundColor = "red";
            }
            else
            {
                event.target.style.backgroundColor = "green";
            }
        }
    },

    template: `
        <div style="border: 1px solid red">
            #PARENT_TEMPLATE#
        </div>
    `
});

BX.Vue.create({
    el: '#vue-application',
    template: `
        <div>
            <div>Оригинальный компонент</div>
            <bx-digits/>
            <br/>
            <div>Добавляем новое поведение компоненту</div>
            <bx-digits-parent/>
        </div>
    `
});


Кастомизация компонентов партнерами
368 / 382

Партнёрская кастомизация

Если перед вами стоит задача не создать новый интерфейс, а внести правки в уже существующий, вам необходимо использовать данный метод.

Техника повторяет технику клонирования, но за одним исключением - вызов метода BX.Vue.mutateComponent изменит уже существующий компонент, а не создаст его копию.

Важно! Вы изменяете оригинальный компонент. Это значит, что все другие компоненты, использующие его, будут также изменены. Хотя мы и стараемся соблюдать обратную совместимость везде, где это возможно, вам необходимо проверять свои мутации после обновлений продукта (чтобы убедиться, что компонент продолжает работать так, как вы предполагали).

Модернизация компонента

Схема похожа на схему клонирования компонента.

Модификатор компонента (мутатор) необходимо расположить в шаблоне сайта (в js файлах шаблона).

Содержимое файла модификатора:

   BX.Vue.mutateComponent('bx-moduleName-someComponent',
   {
        template: `
            <div class="some-component">
                {{ result }}
            </div>
        `
   });

Синтаксис метода: BX.Vue.mutateComponent(id, mutations), где:

  • id - идентификатор компонента для модификации
  • mutations - параметры, которые будут заменены в оригинальном компоненте

Подробнее о заменяемых параметрах

Параметры этого массива полностью совпадают с массивом params в момент создания компонента, все ключи описаны в документации Vue Основы компонентов и Продвинутые компоненты.

В массиве mutations нужно указать только те ключи, которые требуется изменить.

Рассмотрим пример: пусть нужно заменить метод sendText в оригинальном компоненте. Тогда объект mutations будет выглядеть так:

{
    methods:
    {
        sendText()
        {
            ...
        }
    }
}

Все остальные функции останутся на местах и они не будут затронуты. Если нужно добавить новый метод, это делается так же, как и замена существующего.

Правила мутации применяются ко всему дереву параметров, таким как methods, params, template.

  • Мутация строковых значений

    Строковые значения могут быть заменены на новые с возможностью вставить предыдущий текст в текст нового. Для этого в новом тексте нужно указать название этого параметра со знаками номера по обе стороны от переменной. Например, это применимо для параметра template: чтобы в новом компоненте можно было использовать шаблон старого, добавьте в новый текст #PARENT_TEMPLATE#.

    Представим, что шаблон ранее имел вид:

    {
    template: "<span>123</span>"
}

    Допустим, вы хотите использовать старый шаблон в своем новом шаблоне:

    {
    template: "<div>#PARENT_TEMPLATE#</div>"
}

    Итоговой версткой после отрисовки компонента будет:

    <div><span>123</span></div>

  • Мутация объектов

    Объекты (такие как params, methods, computed) могут быть расширены или заменены новыми значениями.

    Кроме того, при замене уже существующих параметров в объектах есть возможность получить доступ к предыдущему значению, написав слово parent у названия ключа. При этом первую букву ключа нужно будет перевести в верхний регистр. Например, если было вычисляемое свойство dateText, то после его изменения к предыдущему значению можно обратиться, используя parentDateText.

    Рассмотрим пример замены метода. Допустим, есть метод sendText, который отправляет текст вышестоящему компоненту:

    {
    methods:
    {
        sendText(text)
        {
            this.emit('send', text);
        }
    }
}

    Вас устраивает работа метода, но вы хотите модифицировать текст перед отправкой:

    {
    methods:
    {
        sendText(text)
        {
            // модифицируем текст
            text = '['+text+']';

            // вызываем родительский метод но уже с другим текстом
            this.parentSendText(text);
        }
    }
}

    Рассмотрим пример замены вычисляемого свойства на примере локализаций.

    Вот вычисляемое свойство, которое формирует данные локализаций в оригинальном компоненте:

    {
    computed:
    {
        localize()
        {
            return BX.Vue.getFilteredPhrases('IM_MESSENGER_MESSAGE_');
        },
    },
}

    Вам нужно добавить свои, не потеряв при этом оригинальные:

    {
    computed:
    {
        localize()
        {
            return Object.assign({},
                this.parentLocalize,
                BX.Vue.getFilteredPhrases('IMOL_MESSAGE_')
            );
        },
    },
}

    Важно! Этот метод отработает корректно, даже если в оригинальном компоненте не было локализаций, т.к. Object.assign пропустит объединение с undefined свойством.

Примечание: Все остальные значения будут заменены полностью на новые.

Пример мутации

Пусть есть компонент, которые выводит значения от 1 до 10 в обратном порядке, в виде черного текста.

Модифицируем его, чтобы он выводил цифры в цвете в случайном порядке. Для четных будет красный цвет, а для нечетных - зеленый.

BX.Vue.mutateComponent('bx-digits',
{
    computed:
    {
        preparedDigits()
        {
            return this.shuffleArray(this.digits)
        }
    },
    methods:
    {
        shuffleArray(array)
        {
            for (let i = array.length - 1; i > 0; i--)
            {
                const j = Math.floor(Math.random() * (i + 1));
                [array[i], array[j]] = [array[j], array[i]];
            }
            return array;
        }
    },
    template: `
        <div>
            <template v-for="digit in preparedDigits">
                <span v-if="digit % 2" style="color: green; padding: 0 5px;">{{digit}}</span>
                <span v-else style="color: red; padding: 0 5px;">{{digit}}</span>
            </template>
        </div>
    `
});

BX.Vue.component('bx-digits',
{
    data()
    {
        return {
            digits: [1,2,3,4,5,6,7,8,9,10]
        }
    },
    computed:
    {
        preparedDigits()
        {
            return this.digits.reverse();
        }
    },
    template: `
        <div>
            <span v-for="digit in preparedDigits" style="padding: 0 5px;">{{digit}}</span>
        </div>
    `
});

BX.Vue.create({
    el: '#vue-application',
    template: `
        <div>
            <div>Компонент после мутации</div>
            <bx-digits/>
        </div>
    `
});

Компонент после мутации
1 6 7 3 10 4 2 8 5 9

Пример использования предыдущих значений при мутации

Базовый компонент выводил числа от 1 до 10 в обратном порядке, при клике на цифру она окрашивалась сначала в зеленый, потом в красный цвет.

Мутируем компонент, наследуя его предыдущее поведение, и внесем следующие коррективы:

  • шаблон вывода обрамляется красной рамкой;
  • при клике помимо изменения цвета и шрифта добавляется цвет для заднего фона.
BX.Vue.component('bx-digits',
{
    data()
    {
        return {
            digits: [1,2,3,4,5,6,7,8,9,10]
        }
    },
    computed:
    {
        preparedDigits()
        {
            return this.digits.reverse();
        }
    },

    methods:
    {
        changeColor(event)
        {
            if (event.target.style.color == "green")
            {
                event.target.style.color = "red";
            }
            else
            {
                event.target.style.color = "green";
            }
        }
    },

    template: `
        <div>
            <span v-for="digit in preparedDigits" @click="changeColor" style="padding: 0 5px; cursor: pointer">
                {{digit}}
            </span>
        </div>
    `
});

BX.Vue.mutateComponent('bx-digits',
{
    computed:
    {
        // заменяем существующие в оригинале вычисляемое свойство
        preparedDigits()
        {
            // получаем доступ к предыдущему состоянию свойства через this.parent<НазваниеСвойства>
            let reverseDigit = [].concat(
                this.parentPreparedDigits
            );

            // добавляем к отсортированным значениям цифру ноль в конец.
            reverseDigit.push(0);

            return reverseDigit;
        }
    },

    methods:
    {
        // заменяем существующие в оригинале функцию
        changeColor(event)
        {
            // оставляем предыдущее поведение
            this.parentChangeColor(event);

            // добавляем новое
            if (event.target.style.color == "green")
            {
                event.target.style.backgroundColor = "red";
            }
            else
            {
                event.target.style.backgroundColor = "green";
            }
        }
    },

    template: `
        <div style="border: 1px solid red">
            #PARENT_TEMPLATE#
        </div>
    `
});

BX.Vue.create({
    el: '#vue-application',
    template: `
        <div>
            <div>Вывод компонента с новым поведением</div>
            <bx-digits/>
         </div>
    `
});


Директивы
369 / 382

Директивы: общая информация

Помимо встроенных директив (таких как v-model и v-show), Vue позволяет использовать ваши собственные.

При этом важно понимать, что основным механизмом создания повторно используемого кода в Vue всё-таки являются компоненты.

Тем не менее, для выполнения низкоуровневых операций с DOM пользовательские директивы могут очень пригодиться.

Например, чтобы после загрузки страницы в поле ввода сразу устанавливался фокус. Штатными средствами такое сделать не получится, поэтому нужно создать свою директиву (специальный атрибут к DOM-элементу).

Это можно сделать двумя способами:

  • зарегистрировать глобальную директиву (для всех компонентов vue):
    // Регистрируем глобальную пользовательскую директиву `v-bx-focus`
Vue.directive('bx-focus', {
  // Когда привязанный элемент вставлен в DOM...
  inserted(element, params)
  {
    // Переключаем фокус на элемент
    element.focus()
  }
})
  • зарегистрировать локально (доступную только в вашем компоненте). Для этого нужно передать опцию directives при определении компонента:
    BX.Vue.component('bx-some-component', {
    ...
    directives: {
        'bx-focus':
        {
            inserted(element, params)
            {
                element.focus()
            }
          }
    },
    ...
})

    • Теперь в шаблонах можно использовать новый атрибут v-bx-focus:

    <input v-bx-focus>

Подробнее о возможностях директив вы можете прочитать в документации.

Про создание глобальных директив для Bitrix Framework вы можете прочитать в уроке Создание глобальных директив.


Создание глобальных директив
370 / 382

Создание глобальных директив

Перед созданием глобальной директивы рекомендуем проанализировать её необходимость: действительно ли она будет полезна другим компонентам? Если нет, то лучше обойтись созданием локальной директивы (см. предыдущий урок ).

Штатные глобальные директивы располагаются в модуле ui в директории /modules/ui/install/js/ui/vue/directives/.

Все директивы должны быть в виде расширений СoreJS в новом формате. В общем случае ваша директива будет выглядеть следующим образом:

/some-directive/config.php
/some-directive/ui.vue.directives.some-directive.js

Cодержимое файла config.php:

<?
if (!defined("B_PROLOG_INCLUDED") || B_PROLOG_INCLUDED !== true)
{
   die();
}

return array(
   'js' => Array(
      '/bitrix/js/ui/vue/directives/some-directive/ui.vue.directives.some-directive.js',
   ),
   'rel' => array('ui.vue'),
   'skip_core' => true,
   'bundle_js' => 'ui_vue'.(defined('VUEJS_DEBUG') && VUEJS_DEBUG? '_debug': '')
);

В зависимостях обязательно должен быть ui.vue. Если не используются библиотеки CoreJS внутри директивы, укажите ключ 'skip_core' => true.

Содержимое файла ui.vue.directives.some-directive.js:

(function(window)
{
   "use strict";

   /**
    * Some Directive Vue directive
    *
    * @package bitrix
    * @subpackage ui
    * @copyright 2001-2019 Bitrix
    */

   /*
      Example of usage:
      ...
    */

   const SomeClassForDirectiveLogic =
   {
      ...
   };

   BX.Vue.directive('bx-some-directive',
   {
      bind(element, params)
      {
         ...
      }
   });

})(window);

Внутри файла указывается пример использования директивы или ссылка на документацию в аннотации JsDoc.

Если требуется сложная логика, можно вынести её за пределы директивы. Если её можно повторно использовать в других модулях и компонентах, рекомендуется вынести за пределы данного расширения.

Для регистрации директивы используется метод BX.Vue.directive (синтаксис использования полностью совпадает с Vue.directive ).

Имя директивы должно начинаться с префикса bx- для исключения пересечения с директивами внешних разработчиков.


Директива Image LazyLoad
371 / 382

Директива Image LazyLoad

C помощью директивы v-bx-lazyload можно сделать "ленивую подгрузку изображений" в рамках ваших компонентов.

Имя расширения для зависимости

ui.vue.directives.lazyload

Доступно с ui 18.5.0.

Примеры использования

Для избежания эффекта скачков скролла и контента важно понимать, что указываемые изображения должны быть одинакового размера.

Этого можно достичь путем подготовки одинаковых изображений или благодаря указанию фиксированного размера через стили.

Примеры

В свойстве src (placeholder) указывается путь до плейсхолдера (т.е. до легкого изображения, которое будет показано во время загрузки основного изображения или если оно скрыто).

В свойстве data-lazyload-src прописывается путь до основного изображения, которое требуется подгрузить, когда изображение будет в поле видимости.

В свойстве data-lazyload-error-src указывается путь до изображения, которое требуется показать в случае, когда основное изображение не удалось загрузить. Если это поле не указать, то у изображения останется placeholder src:

<img v-bx-lazyload
    class="bx-module-element"
    src="https://.../placeholder.png"
    data-lazyload-src="https://.../targetImage.png"
    data-lazyload-error-src="https://.../errorImage.png"
/>

По умолчанию, при показе и скрытии изображения из поля видимости, изображение заменяется обратно на placeholder. Если такое поведение не требуется - укажите свойство data-lazyload-dont-hide:

<img v-bx-lazyload
    class="bx-module-element"
    src="https://.../placeholder.png"
    data-lazyload-dont-hide
    data-lazyload-src="https://.../targetImage.png"
    data-lazyload-error-src="https://.../errorImage.png"
/>

Также можно использовать краткую версию синтаксиса, не указывая placeholder src и error src. Тогда при возникновении ошибки или скрытия изображения из поля видимости будет использовано изображение размером 1х1 пиксель.

Для работы в таком режиме обязательно должны быть указаны размеры изображения через стили.

<img v-bx-lazyload
    class="bx-module-element"
    data-lazyload-src="https://.../targetImage.png"
/>

Во время работы директивы на элемент будут автоматически проставляться разные классы:

bx-lazyload-watch - класс означает что изображение отслеживает изменения состояния

bx-lazyload-loading - класс означает что изображение в стадии загрузки

bx-lazyload-success - класс означает что изображение успешно загрузилось

bx-lazyload-error - класс означает что при загрузки изображения возникла ошибка

bx-lazyload-hidden - класс означает что изображение в данный момент скрыто

При использовании короткого синтаксиса и классов открывается возможность проставлять изображения для placeholder, ошибки и загрузки через стили.

Для классов успешной загрузки и ошибки возможно переопределить стандартные названия на свои. Для этого используются следующие свойства:

data-lazyload-error-class - название класса при возникновении ошибки

data-lazyload-success-class - название класса при успешной загрузке

<img v-bx-lazyload
    class="bx-module-element"
    data-lazyload-src="https://.../targetImage.png"
    data-lazyload-error-class="bx-module-element-error"
    data-lazyload-success-class="bx-module-element-success"
/>

Особенности работы

Данный компонент работает на основе IntersectionObserver инновационной технологии, которая позволяет отслеживать показ и скрытие элементов DOM.

В параметрах этого метода нельзя указать отступы, которые нужно учитывать для срабатывания факта видимости (например, начать загружать изображение в тот момент, когда до него осталось 100 пикселей). Однако этот метод работает значительно быстрее традиционных способов (отслеживания скролла), и производители браузеров рекомендуют использовать именно его (при его наличии).


Внешнее хранение данных Vuex
372 / 382

Внешнее хранение данных Vuex

Vuex - это паттерн управления состоянием и библиотека для приложений на Vue.js. Он служит центральным хранилищем данных для всех компонентов приложения и обеспечивает предсказуемость изменения данных при помощи определённых правил. В результате мы получаем упорядоченную структуру и легкое взаимодействие с данными на разных уровнях.

Кроме того, Vuex интегрируется с официальным расширением инструментов разработчика Vue, предоставляя "из коробки" такие возможности, как " машину времени " при отладке и экспорт/импорт слепков состояния данных.

Vuex реализует архитектурный подход Flux , построенный на однонаправленном потоке данных.

Главный смысл заключается в том, что при возникновении действия представление не изменяет исходные данные самостоятельно, а передает хранилищу свое "желание изменить", а уже хранилище после изменения данных передает новую версию в представление.

Видео

Подробнее о Vuex и о паттерне управления состоянием можете прочитать в статье на сайте Vuex или посмотрев видео.

Просмотр видео в наших курсах в формате CHM невозможен. Ролик можно просмотреть на сайте компании «1С-Битрикс».

Пример

Начнем с примера, когда это не нужно использовать. Допустим, создается компонент, данные которого используются только им. Например, компонент вывода смайлов:

Данный компонент состоит из списка всех смайлов и галерей. Эти данные больше не планируется нигде использовать - значит, хранить их нужно внутри компонента Vue (выносить их в отдельное хранилище Vuex избыточно).

Теперь перейдем к другому примеру - мессенджер.

Мессенджер состоит из нескольких автономных компонентов:

  • список чатов (зеленый)
  • панель чата (желтый)
  • список сообщения (синий)

В каждом из них есть отображение пользователя (красные блоки), и тут возникают несколько вопросов:

  • как получить данные в самом глубоком компоненте?
  • как сделать, чтобы после загрузки аватара в одном блоке он изменился во всех?
  • если разные компоненты умеют получать новые данные, то как их синхронизировать между собой?
  • какой компонент главный?

В таком случае данные лучше вынести во внешнее хранилище Vuex и получать к ним доступ из любой части приложения, в том числе и к изменению (без риска что-либо не учесть, т.к. изменение будет происходить через специальные методы изменения внутри хранилища).

Дополнительный плюс - "источнику данных" не нужно думать, где используются эти данные, или организовывать систему событий для их обновления. Данные автоматически будут синхронны во всех компонентах, которые их используют.

Примечание: Для использования возможностей "Машины времени" и импорта/экспорта слепков состояний необходимо установить специальное расширение для Google Chrome - Vue DevTools.


Vuex: подключение и использование
373 / 382

Vuex: подключение и использование

Для начала работы необходимо подключить Bitrix CoreJS расширение ui.vue.vuex в вашем компоненте или на php-странице с помощью \Bitrix\Main\UI\Extension::load("ui.vue.vuex");

При создании нового экземпляра нужно указать, какое хранилище планируется использовать, если у вас одно хранилище:

const store = BX.Vuex.store(
{
    state:
    {
        data: {a: 1, b: 2, c: 3}
    },
    actions:
    {
        change(store, payload)
        {
            store.commit('changeData', payload);
        }
    },
    mutations:
    {
        changeData(state, params)
        {
            if (typeof params.a === 'number')
            {
                state.data.a = params.a;
            }
            if (typeof params.b === 'number')
            {
                state.data.b = params.b;
            }
            if (typeof params.c === 'number')
            {
                state.data.c = params.c;
            }
        }
    }
});

const templateEngine = BX.Vue.create({
    el: this.rootNode.firstChild,
    store: store,
    template: '<bx-root-component/>',
});

Если у вас несколько независимых хранилищ, используйте модульную систему:

const store = BX.Vuex.store(
{
    modules:
    {
        dataStore1:
        {
            namespaced: true,
            state:
            {
                data: {a: 1, b: 2, c: 3}
            },
            actions:
            {
                change(store, payload)
                {
                    store.commit('changeData', payload);
                },
            },
            mutations:
            {
                changeData(state, params)
                {
                    if (typeof params.a === 'number')
                    {
                        state.data.a = params.a;
                    }
                    if (typeof params.b === 'number')
                    {
                        state.data.b = params.b;
                    }
                    if (typeof params.c === 'number')
                    {
                        state.data.c = params.c;
                    }
                }
            }
        },
        dataStore2:
        {
            namespaced: true,
            state:
            {
                data: {d: 4, e: 5, f: 6}
            },
            actions:
            {
                change(store, payload)
                {
                    store.commit('changeData', payload);
                },
            },
            mutations:
            {
                changeData(state, params)
                {
                    if (typeof params.d === 'number')
                    {
                        state.data.d = params.d;
                    }
                    if (typeof params.e === 'number')
                    {
                        state.data.e = params.e;
                    }
                    if (typeof params.f === 'number')
                    {
                        state.data.f = params.f;
                    }
                }
            }
        },
    }
});

При использовании модульной системы обратите внимание, что параметры внешних вызовов у функций хранилища поменяются.

При выполнении действий (actions) вместо обычного вызова:

store.dispatch('changeData', {a: 11})

Укажите действие вместе с namespace:

store.dispatch('dataStore1/changeData', {a: 11})

При выполнении изменения данных (mutation) вместо обычного вызова:

store.commit('changeData', {b: 22})

Укажите мутацию вместе с namespace:

store.commit('dataStore1/changeData', {a: 22})

Для использования данных хранилища в шаблоне компонентов необходимо, чтобы компонент задекларировал использование этих значений в шаблоне через вычисляемые свойства.

Чтобы вручную не прописывать все элементы структуры, воспользуйтесь вспомогательной функцией mapState:

BX.Vue.component('bx-button-counter', {
            computed:
            {
                counterName(state)
                {
                    return state.data1.c+' шт.';
                },
                ...BX.Vuex.mapState({
                    data1: state => state.dataStore1.data,
                    data2: state => state.dataStore2.data
                })
            },
            template: '<div>Счётчик кликов — {{ counterName }}</div>''
        });

Подробнее об этом можно прочитать в документации.


Маршрутизация внутри приложения - VueRouter
374 / 382

Маршрутизация внутри приложения - VueRouter

Vue Router — официальная библиотека маршрутизации для Vue.js. Она глубоко интегрируется с ядром Vue.js, что позволяет создавать SPA-приложения с лёгкостью.

Включает следующие возможности:

  • Вложенные маршруты/представления
  • Модульная конфигурация маршрутизатора
  • Доступ к параметрам маршрута, query, wildcards
  • Анимация переходов представлений на основе Vue.js
  • Удобный контроль навигации
  • Автоматическое проставление активного класса CSS для ссылок
  • Режимы работы HTML5 history или хэш, с авто-переключением в IE9
  • Настраиваемое поведение прокрутки страницы

Подключение на PHP-странице

Для начала работы подключите Bitrix CoreJS расширение ui.vue.router в вашем компоненте или на php-странице с помощью \Bitrix\Main\UI\Extension::load("ui.vue.router");

Как использовать маршрутизатор

<div id="vue-application"></div>
<script>
    // 1. Определяем компоненты для маршрутов.
    // Они могут быть импортированы из других файлов
    const Foo = { template: '<div>foo</div>' };
    const Bar = { template: '<div>bar</div>' };

    // 2. Определяем несколько маршрутов
    // Каждый маршрут должен указывать на компонент.
    // "Компонентом" может быть как конструктор компонента, созданный
    // через `Vue.extend()`, так и просто объект с опциями компонента.
    // Мы поговорим о вложенных маршрутах позднее.
    const routes = [
      { path: '/foo', component: Foo },
      { path: '/bar', component: Bar }
    ];

    // 3. Создаём экземпляр маршрутизатора и передаём маршруты в опции `routes`
    // Вы можете передавать и дополнительные опции, но пока не будем усложнять.
    const router = BX.VueRouter.create({
      routes // сокращённая запись для `routes: routes`
    });

    // 4. Создаём и монтируем корневой экземпляр приложения.
    // Убедитесь, что передали экземпляр маршрутизатора в опции
    // `router`, чтобы позволить приложению знать о его наличии.
    BX.Vue.create({
        el: '#vue-application',

        router,  // сокращённая запись для `router: router`

        template: `
            <div>
                <h3>VueRouter</h3>
                <div>
                    <router-link to="/foo">Перейти к Foo</router-link>
                    <router-link to="/bar">Перейти к Bar</router-link>
                </div>
                <router-view></router-view>
            </div>
        `
    });
</script>


Приёмы и хитрости
375 / 382

Приёмы и хитрости

Если данные не требуют реактивности (например, константы или языковые фразы), то для содержащего такие данные объекта необходимо указывать, что он неизменяемый. Это поможет шаблонизатору меньше отслеживать данные, и в масштабах всего приложения сэкономит ресурсы.

computed:
{
    applicationConstants(state)
    {
        return Object.freeze({
            const1: 'value 1',
            const2: 'value 2',
        })
    }
}

При использовании циклов v-for у корневых элементов цикла всегда нужно указывать ключ :key - это позволит шаблонизатору правильно отрабатывать возможные анимации и улучшит переиспользование dom-дерева при изменении входных данных. Подробнее в статье Использование Key в списках.

<template v-for="operator in operators">
    <div class="bx-livechat-user" :key="operator.id">
        <div class="bx-livechat-user-name">{{operator.name}}</div>
    </div>
</template>

Если используется анимация появления (выезд контента из-за пределов области видимости), то не используйте директивы автофокуса, так как браузер моментально установит фокус на элементе за пределами видимости и внешне анимация будет выглядеть не так, как ожидалось.

directives:
{
    focus:
    {
        inserted(element, params)
        {
            element.focus();
        }
    }
},


BigData (сервис персонализации)
376 / 382

В данной главе рассматривается механизм сбора данных для сервиса 1С-Битрикс BigData, поясняется логика его работы и приводится описание API запросов.

Примечание: информация по подключению сервиса на сайте представлена в уроке Персонализация учебного курса Администратор. Бизнес.


Сбор данных
377 / 382

Сервис персонализации использует 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 и отправляются агентом в сервис рекомендаций.

Важно! Пересылаемая информация не содержит персональных данных клиентов.

Сбор данных для рекомендаций можно отключить в настройках главного модуля (Настройки > Настройки продукта > Настройки модулей > Главный модуль).


Логика работы сервиса
378 / 382

Сервис рекомендаций в своей работе использует следующие типы событий магазина: просмотр товара, добавление в корзину, оформление заказа, оплата заказа. Вместе с этими событиями на сервер передаются следующие данные:

  • кука пользователя;
  • хеш лицензии;
  • домен;
  • идентификатор товара;
  • название товара;
  • категории товара;
  • идентификатор рекомендации.

Работа с данными ведется в несколько этапов:

  • На первом этапе все события поступают в единую точку входа bitrix.info, куда в секунду поступает порядка тысячи запросов. (Точка входа реализована на NGINX с модулем Lua.) Входящие данные накапливаются в буфере Kinesis, который хранит данные одни сутки. Затем данные достаются из буфера и обрабатываются большим количеством php-воркеров. Воркеры фильтруют данные и сохраняют их в таблицах хранилища Amazon DinamoDB.

  • На втором этапе данные достаются из хранилища Amazon DynamoDB и при этом, чтобы каждый раз не сканировать базу, используются простые файлы данных, хранящиеся в Amazon S3. Затем выполняется обработка данных: данные обсчитываются с помощью batch-процессинга (используется инструментарий Apache Spark) и с помощью онлайн алгоритмов (Apache Tomcat, Apache Mahout, алгоритмов похожести пользователей, похожести товаров и семантического анализа на основе контента). В результате выбранная и обсчитанная информация в виде рекомендаций выдается из analytics.bitrix.info.

Метрики качества

Дополнительно используются следующие метрики качества сервиса рекомендаций:

  • Отношение просмотров по рекомендации к просмотрам, т.е. определяется эффективность размещения компонента.
  • Отношение заказов по рекомендации к просмотрам по рекомендации – этот показатель оценивает качество алгоритма, насколько релевантной оказалась рекомендация покупателю.
  • Отношение заказов по рекомендации ко всем заказам.


API запросов
379 / 382

Персональная рекомендация

Для конкретной куки дается персональная рекомендация. Используется комбинированный алгоритм определения похожести товаров.

Запрос:

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 – число элементов (размер выдачи).


Веб-сервисы
380 / 382

Веб-служба, веб-сервис (англ. web service) — программная система, идентифицируемая строкой URI, чьи публичные интерфейсы и привязки определены и описаны языком XML. Описание этой программной системы может быть найдено другими программными системами, которые могут взаимодействовать с ней согласно этому описанию посредством сообщений, основанных на XML, и передаваемых с помощью интернет-протоколов.

Достоинства веб-сервисов:

Веб-службы обеспечивают взаимодействие программных систем независимо от платформы. Веб-службы основаны на базе открытых стандартов и протоколов. Благодаря использованию XML достигается простота разработки и отладки веб-служб. Использование интернет-протокола HTTP обеспечивает взаимодействие программных систем через межсетевой экран.

Модуль Веб-сервисов служит для облегчения создания и интеграции с существующими веб-сервисами. Основное свое применение модуль найдет при разработке интеграций с действующими приложениями как внутри компании, так и с внешними уже работающими веб-сервисами.


Пример создания windows-приложения для добавления новостей
381 / 382

Описание

Рассмотрим, как на его основе модуля Веб-сервисы можно сделать простейшее 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 + "]");
            }
}


Мобильное приложение BitrixMobile
382 / 382
BitrixMobile 3.0 - технология создания кроссплатформенных мобильных приложений.

Гибридные мобильные приложения имеют доступ к функциям смартфона:

  • GPS;
  • Информация об устройстве;
  • Акселерометр;
  • Файловая структура;
  • Адресная книга.

BitrixMobile прибавляет к этим возможностям широкий набор нативных интерфейсов, чтобы приложение работало быстрее и выглядело привычнее для пользователя. Разработка ведется полностью на HTML5/Javascript/CSS. Взаимодействие с функциями телефона и нативным интерфейсом осуществляется через функции специальной JS-библиотеки - BXMobileApp.

В рамках курса «BitrixMobile - создание кроссплатформенных мобильных приложений» подробно рассмотрены возможности библиотеки BXMobileApp, которая позволяет создавать сложные бизнес-приложения на BitrixMobile.


Разработчик Bitrix Framework
0 / 382
Цитатник веб-разработчиков .

Евгений Смолин: Сам кувыркался с непонятками (пока курсы учебные не прочитал и руками не попробовал то, что там написано). Битрикс из тех систем, где изучение "методом тыка" не очень эффективно без предварительного изучения учебных курсов. Уважаемые новички, потратьте немного своего драгоценного времени, пройдите пару-тройку учебных курсов и масса вопросов просто испарится - там есть ответы на множество вопросов.

Курс для разработчиков - продолжение линейки учебных курсов по Bitrix Framework. Получение сертификата по курсу рекомендуется после успешной сдачи тестов по всей линейке курсов, так как без понятия о работе Контент-менеджера и Администратора создание успешных сайтов будет затруднено.

Чтобы научиться программировать в Bitrix Framework, нет необходимости изучать всю линейку курсов. Но есть моменты, которые необходимо знать разработчикам о системе, они раскрыты в начальных курсах:

  • Интерфейс программы - в главе Элементы управления курса Контент-менеджер.
  • Компоненты 2.0 (начальные сведения) в главе Компоненты 2.0 (начальные сведения) курса Контент-менеджер.
  • Информационные блоки - в главе Информационные блоки (начальные сведения) курса Контент-менеджер.
  • Управление доступом к файлам, элементам контента, модулям и другие права доступа в главе Управление доступом курса Администратор. Базовый.
  • Работа с инструментами системы - в главе Работа с инструментами курса Администратор. Базовый.
  • Модуль Поиск - в главе Поиск курса Администратор. Базовый.
  • Вся информация по администрированию модулей размещена в курсах:
    • Администрирование. Модули - модули "1С-Битрикс: Управление сайтом"
    • Администратор. Бизнес - модули "1С-Битрикс: Управление сайтом", связанные с коммерческой деятельностью в Интернете.
    • Администратор "1С-Битрикс: Корпоративный портал" - модули "1С-Битрикс: Корпоративный портал"

    Как построен курс

    Общепринятая градация квалификации разработчиков в рамках курса обозначает что:

    • Junior сможет создавать простые сайты работая со штатными компонентами и модифицируя их шаблоны.
    • Middle разработчик может работать с API Bitrix Framework.
    • Senior умеет работать над производительностью и безопасностью сайтов, создавать свои модули и компоненты.

    Начальные требования к подготовке

    Для успешного изучения курса и овладения мастерством разработки сайтов на Bitrix Framework необходимо владеть (хотя бы на начальном уровне):

    • основами PHP, баз данных;
    • основами HTML, CSS.

    Тесты

    После изучения курса вам будет предложено пройти тесты на сертификацию. При успешной сдаче последовательности тестов на странице Моё обучение можно просмотреть результат обучения и загрузить сертификат в формате PDF.

    Комментарии к статьям

    На каждой странице курса авторизованный на сайте посетитель может дать комментарий к содержимому страницы. Комментарий - не форум, там не ведётся обсуждений или разъяснений. Это инструмент для сообщений нам об ошибках, неточностях. Для отправки комментария воспользуйтесь расположенной в правом нижнем углу окна браузера кнопкой:

    Что дальше?

    Одновременно с изучением курса Разработчик Bitrix Framework вам придётся обращаться к информации о других технологиях Bitrix Framework. Эта информация размещена в следующих курсах:

  • Технология Композитный сайт - ускорение работы сайта.
  • Разработка и эксплуатация высоконагруженных проектов - для тех кто делает "тяжёлые" проекты.
  • Бизнес-процессы - настройка и создание бизнес-процессов.
  • Бот платформа Битрикс24 - приложения, основанных на чатах в веб-мессенджере.
  • Маркетплейс Битрикс24 - приложения для коробочной и облачной версий Битрикс24.
  • Маркетплейс Bitrix Framework - расширение функционала проектов на основе Bitrix Framework с помощью сторонних модулей и решений.
  • Многосайтовость - система многосайтовости и принципы работы с многосайтовой конфигурацией.

Для преподавания оффлайн

Если данный курс берётся в качестве основы для оффлайного преподавания, то рекомендуемая продолжительность: 5 дней (40 академических часов).

Если нет интернета

Скачать материалы курса в формате CHM. Файлы формата CHM обновляются ежемесячно, тем не менее, возможно некоторое отставание их от онлайновой версии курса.

Если не видно содержание файла CHM
Внимание! Если вы не видите содержимое файла формата .chm, то причина - настройки безопасности операционной системы: скачанные из интернета файлы находятся под наблюдением системы, которая ограничивает доступ к ним.

Чтобы отключить подобное отношение к файлу необходимо:

  • перейти в свойства файла на вкладку Общие. Внизу у Вас отобразится сообщение о том, что этот файл был заблокирован системой с целью защиты компьютера.

  • нажать кнопку Разблокировать.

Отсутствие кнопки Разблокировать возможно в двух случаях:

  1. Файл лежит не локально, а на сетевом ресурсе.
  2. Если файл лежит на локальном диске, но путь к нему содержит спецсимволы (# и прочие).