Вступление Не знаю, как у вас, а у нас самым проблемным местом при разработке интернет-магазина на битриксе является… Так и хочется сказать «битрикс», но нет — это интеграция с 1С:-) Проблемы эти можно свести в одно предложение: «Нам на сайте нужно то-то, но мы можем выгрузить только так, а не как вы просите». Или «Да у нас отличный 1С-программист, конечно сможем!», а через неделю слышится уже что-то вроде «ну не шмогла я, не шмогла». И тогда нам приходится работать с тем, что дают, подстраивая стандартные алгоритмы интеграции с 1С под свои нужды (здесь и далее разговор идёт о CommerceML).
День добрый. Подскажите пожалуйста. В 1Ске позиции поменяю на Снят с продажи, но на сайт все равно данные позиции, после обмена попадают активными, помечаю, на удаление, тоже не спасает особо. Ни программист 1С ни программисты на сайте не могут мне, помочь, как корректно выгружать данные из 1С на сайт(
В нашей компании уже длительное время при разработке сайтов используются так называемые "некешируемые области" — штука, позволяющая исполнять код в любом месте шаблона компонента вне зависимости от кеша. Применяются они, в основном, в интернет-магазинах и корпоративных сайтах для следующих задач:
Отображение актуального количества товара на складе;
Изменение поведения блока "Добавить в корзину" в зависимости от наличия товара в корзине;
Какие-нибудь рейтинги-голосовалки у элементов каталога;
Реализация была довольно простенькая — глобальная переменная, несколько функций и всё. Скопировал файл, подключил и пользуйся. Но пришло время что-то менять! И я решил сделать модуль в маркетплейс для внутренних нужд. А потом ещё раз подумал — и решил сделать его доступным всем. Собственно, вот модуль "Некешируемая область", а тут небольшая документация с примером использования. Внимание. Модуль требует для работы PHP 5.3, хотя в свете выхода будущей версии битрикса это не страшно.
Тесты — это хорошо, а когда есть готовая платформа для тестирования с интерфейсом и корпоратичный чеклист, каждый пункт которого содержит до двух литров крови программиста или менеджера, то почему бы не попробовать сделать свои автоматические тесты качества?
Для автотестов еще можно указать параметр "AUTOTEST_DESC", в который заложить описание работы автотеста (раздел "Как работает автотестирование"). В итоге получаем что-то подобное описание теста:
Для автотестов еще можно указать параметр "AUTOTEST_DESC", в который заложить описание работы автотеста (раздел "Как работает автотестирование"). В итоге получаем подобное описание теста:
Потребовалось для одного проекта скрестить инфоблоки с собственными таблицами, всяческими сквозными фильтрами и сортировками. Да и ещё и работало чтоб быстро. Первое, что приходит на ум - ORM. Но она в инфоблоках пока отсутствует (точнее, отсутствует самая нужная её часть - работа с элементами), поэтому пришлось реализовывать свою прослойку, которой и хочу поделиться - https://github.com/unnamed777/IblockOrm
Краткие характеристики
Доступна вся мощь getlist'а orm и sql-билдера - связывание и подтягивание данных откуда угодно и как угодно (раз, два).
Возможна работа как с заранее описанными сущностями (наследниками обёртки), так и с динамически создаваемыми (аналогично hlblock'ам).
Поддержка стандартной постраничной навигации. Пока в ORM её нет, поэтому пришлось немного костыльнуть, зато поддерживается стандартный system.pagenavigation.
Хелпер для получения всех свойств элементов в выборке (описано ниже).
Только для ИБ 2.0. При особом желании можно и под первые адаптировать, но не было необходимости.
Read only. Создания и модификация элементов через эту обёртку невозможны, для этого есть замечательное классическое апи.
Доступ к элементам по правам - на совести конечного разработчика, обёртка не предлагает никакого функционала, но предоставляет все возможности для его реализации.
Обёртка состоит из трёх классов:
ElementTable - главный класс, наследуется от orm'ного DataManager. Через него происходит вся работа.
MultiplePropertyElementTable - служебная сущность, описывающая множественные свойства инфоблока.
SinglePropertyElementTable - аналогично предыдущему, только для единичных.
Все они находятся под рекомендуемым namespace Vendor\Module, в моём случае - \nav\IblockOrm. Кстати говоря, нагромождение папок в обёртке может показаться странным, но предполагается, что она будет находиться в /local примерно вот так:
Как использовать Для начала зарегистрируем классы в автолоадере (можно и просто заинклудить):
Как видно, синтаксис запросов всё тот же. Основные моменты:
Свойства доступны по коду PROPERTY_CODE, где CODE - символьный код свойства (как в обычном гетлисте).
Если свойства есть в select, то их значения в результате будут находиться в тех же ключах, если не было задано алиасов (в апи инфоблоков, как помните, они находятся в PROPERTY_CODE_VALUE).
Поля при получении экранируются htmlspecialcharsbx(), оригинальные значения лежат в ключах с префиксом ~ (аналогично обычному гетлисту).
По умолчанию ORM выбирает только скалярные поля, поэтому для получения желаемых свойств их надо явно указывать в select. Либо использовать хелпер для получения всех свойств.
Для получения ссылки на элемент надо в select указать DETAIL_PAGE_URL. Это динамически вычисляемое поле, поэтому по умолчанию оно не заполняется.
Постраничная навигация стартует автоматически, если в выборке были указаны и limit, и offset.
При фильтре по множественному свойству элементы могут дублироваться. Чтобы этого избежать, надо добавить в select поле DISTINCT (см. комментарии к посту).
Что внутри Главный класс - ElementTable - это наследник от DataManager с getMap() (метод, описывающий поля сущности), который считывает свойства и строит по ним поля-референсы PROPERTY_*. Референсы эти ведут на:
SinglePropertyElementTable для единичных свойств. На каждый инфоблок (читай класс) присутствует только один референс на SinglePropertyElementTable и он доступен по ключу PROPERTY. Поля референса - это символьные коды свойств. Таким образом, в гетлисте мы можем обращаться к свойствам по ключу PROPERTY.CODE. Предлагаемые же PROPERTY_CODE сделаны просто для удобства и совместимости с обычным апи инфоблоков и являются референсами на поля сущности SingleProperty.
MultiplePropertyElementTable. Поскольку у каждого множественного свойства своя таблица в БД, то и сущностей MultipleProperty создаётся на каждое свойство по одной. Сущности доступны по ключу PROPERTY_CODE_ENTITY. Так как таблицы множественных свойств имеют разные колонки в зависимости от типа свойства (VALUE, VALUE_ENUM, VALUE_NUM), не пренебрегайте фильтр через эту сущность для достижения наилучшей производительности (вообще при работе с любой orm-сущностью советую всегда мониторить генерируемый sql, встречаются подвохи). Для сущности элемента инфоблока PROPERTY_CODE является референсом PROPERTY_CODE_ENTITY.VALUE.
Постраничная навигация Как выше уже упоминал, постраничка стартует автоматически, если указаны limit и offset в запросе. Поскольку в ORM такое понятие отсутствует, пришлось немного извратиться. Объект \Main\DB\Result (результат работы getList()) дополняется свойством oldCDBResult - старым CDBResult, который содержит стандартную информацию о текущей странице, количестве элементов и т.п, и используется для получения кода постранички. С первого раза, полагаю, понять сложно, поэтому вот пример кодом "было/стало".
Так выглядит классическая постраничка в news.list:
Хелпер для получения всех свойств Чтобы получить свойства элемента, можно в явном виде перечислить их в select. Но если их много или же нужна дополнительная информация о них, можно получить сразу все свойства для всех элементов выборки с помощью вспомогательного метода:
Внутри используется магия \CIBlockElement::GetPropertyValuesArray() - та же, что применяется в catalog.section для существенного сокращения количества запросов к БД по сравнению с _CIBlockElement::GetProperties().
Хелпер для получения Query Порой при отладке требуется залезть напрямую в объект запроса, но стандартный датаменеджер сразу возвращает объект результата выборки Bitrix\Main\Db\Result. Поэтому был добавлен ещё один метод, являющийся полной копией DataManager::getList(), но возвращающий Bitrix\Main\Entity\Query вместо Result:
$query = MyIblockTable::getQuery(...)
Пример компонента Если вы смогли дочитать до этого места, то наверняка уже заглянули на гитхаб и увидели там компонент. Это простой пример компонента, использующий для вывода списка элементов orm-обёртку. Изначально это был компонент в рабочем проекте, из которого я выпилил всю лишнюю логику, но не стал менять общей структуры, поэтому он может выглядеть монструозно. Основная логика находится в методах getItems() и formatResult(). А ещё в качестве бонуса там остался закомментированный код для реализации постранички со своей переменной/чпу.
Вместо тысячи слов Просто для демонстрации - пример скомпанованного запроса с фильтром по свойствам инфоблока и сторонней таблице и сортировкой по агрегированному полю сторонней таблицы. Быстро, просто и безболезненно.
Когда хайлоад блок используется чуть менее тривиально, чем простой справочник, появляется необходимость хранить где-то свои методы работы с ним. Самое лучшее место - сам класс HL-блока. Казалось бы - берём базовый класс в соответствии с документацией, наследуем, работаем. Но всё оказывается неможко не так:-)
Следуя документации, пишем код для hl-блока с идентификатором 1 и кодом сущности Test:
namespace nav;
use Bitrix\Highloadblock as HL;
$hlblock = HL\HighloadBlockTable::getById(1)->fetch();
$entity = HL\HighloadBlockTable::compileEntity($hlblock);
$entityClass = $entity->getDataClass();
class TestTable extends \TestTable {
...
}
У сущности Test есть поле PARENT_ID, попробуем выбрать записи с фильтром по нему:
[Bitrix\Main\SystemException] Unknown field definition `PARENT_ID` (PARENT_ID) for Test Entity. (100)
Проверка кода и названия поля ничего не даёт, мало того, та же выборка через $entityClass::getList() успешно отрабатывает.
А теперь немного о том, как работает компиляция сущности в модуле HighloadBlock. Внутри метода объявляется класс с именем ИмяСущностиTable, в котором описывается только одно поле - ID. И тут создаётся объект этого класса, к которому добавляются все поля hl-блока так же, как если бы это было сделано статически в getMap() сущности. Данный экземпляр класса является синглтоном и хранится в датаменеджере ORM по ключу, равному имени класса ("ИмяСущностиTable"). И все будущие обращения к нашей скомпилированной сущности оперируют этим синглтоном.
Таким образом, после вызова HL\HighloadBlockTable::compileEntity() мы имеем:
Базовый класс сущности с только одним полем ID;
Зарегистрированный по имени класса синглтон со всеми полями HL-блока.
Именно по этому всё хорошо работает, пока мы ничего не трогаем.
Теперь взглянем на метод создания/получения синглтона сущности в ORM (\Bitrix\Main\Entity\DataManager):
public static function getEntity()
{
$class = get_called_class();
if (!isset(static::$entity[$class]))
{
static::$entity[$class] = Base::getInstance($class);
}
return static::$entity[$class];
}
В первой же строке видно, как определяется ключ для хранения нашего синглтона - по имени класса, полученного в результате late state binding. Таким образом, становится очевидна причина, по которой выборка в классе-наследнике не знает о пользовательском поле PARENT_ID - порождается новый синглтон без маппинга полей.
Чтобы избежать этой проблемы (либо недомолвки в документации), надо всего лишь в наследуемом классе переопределить getEntity(), чтобы тот возвращал исходный синглтон. Весь код наследования целиком будет выглядеть следующим образом:
namespace nav;
\Bitrix\Main\Loader::includeModule('highloadblock');
use Bitrix\Highloadblock as HL;
\Bitrix\Highloadblock\HighloadBlockTable::compileEntity(array(
'ID' => 1,
'NAME' => 'Test',
'TABLE_NAME' => 'b_custom_test'
));
class TestTable extends \TestTable
{
public static function getEntity()
{
return static::$entity['TestTable'];
}
}
Примерно раз в полгода кто-то из клиентов обязательно сталкивается с проблемой, когда в 1с остаток нулевой, а на сайте ничего не обновляется из-за отсутствия тега Количество в выгрузке (эта проблема возникает в типовых конфигурациях, в модуле обмена от битрикса она решена). А за прошлую неделю ко мне обратились аж двое человек с этой же проблемой. В итоге вместо очередной копипасты облагородил фикс в бесплатный модуль. Внутри всё просто - пара обработчиков, проверка, что у обновляемого товара действительно отсутствует указание количества и обнуление остатка.
Сегодня описывал техподдержке один нетривиальный баг в работе компонентов в режиме ajax, да столько понаписал, что появилось желание посвятить этому багу пост во блоге.
Итак. Как известно, любой компонент битрикса гипотетически может работать в режиме ajax — для этого лишь надо в параметрах подключения компонента указать AJAX_MODE => Y. При наличии этой опции система сама подменяет нужные ссылки и сабмиты форм на js-вызовы (документация).
При клике по ссылке, ведущей на этот же компонент, серверу отправляется асинхронный запрос через XHR. При отправке же данных из формы передача данных происходит через скрытый iframe. Результат запроса в обоих случаях вставляется через innerHTML и, как следствие, js-скрипты при такой вставке не исполняются. Для обхода этого ограничения в битриксе были разработаны специальные методы для отработки javascript в контексте страницы.
Возвращаемся к проблеме. В 10 версии продукта сабмиты форм отлавливались путём добавления атрибута onsubmit к форме (bitrix/modules/main/ajax_tools.php):
function GetFormEvent($container_id)
{
return 'onsubmit="BX.ajax.submitComponentForm(this, \''.htmlspecialchars(CUtil::JSEscape($container_id)).'\') ; "' ;
}
И всё было прекрасно вне зависимости от набора данных, возвращаемых аякс-запросом.
В 11 версии обработчик на сабмит стал вешаться не через атрибут onsubmit, а байндингом с помощью addEventListener, т.е. подписка на событие происходит путем вызова js-кода. Вроде бы всё ок и в преобладающем большинстве случаев оно работает. Однако, из-за особенностей реализации выполнения js-кода, передаваемого компонентом в ответе, возможна ситуация, когда байндинг не будет происходить после получения ajax-ответа и всё будет ломаться. Вот в такой ситуации и оказались читатели одного интернет-СМИ.
Пользователь открывает страницу, на которой размещён компонент с формой и включенным аяксом. Система генерирует js-код, который навешивает обработчик сабмита формы:
Код выполняется, обработчик навешивается. Пользователь нажимает кнопку «Отправить» в форме. Обработчик перехватывает этот вызов, создаёт скрытый ифрэйм, в который сабмитится форма. В этот же момент к ифрэйму добавляется обработчик на событие onload — замена текущего вывода компонента новым html-кодом, пришедшим в качестве ответа:
Таким образом, при получении результата аякс-запроса мы имеем два асинхронных потока выполнения: первый поток в событии load ифрэйма, второй - в самом ифрейме (назовём его ready аналогично jQuery):
ready выполняется, когда html-код (именно код, не ресурсы) страницы полностью загружен в ифрэйм
load выполняется, когда все ресурсы (стили, картинки, т.п.) загрузились. Т.е. фактически, когда вся страница готова к просмотру.
Если в ответе приходит html-код, не обременённый большим количеством ресурсов, всё работает замечательно. Однако, если в теле ответа на ajax-запрос присутствует, например, тяжёлая/долго отдаваемая картинка, происходит нештатная для текущего алгоритма ситуация: отложенный js-код начинает исполняться, а DOM ещё прежний — не произошла подмена старой области вывода компонента новой. Конкретно — не происходит байндинг события на форму, потому что формы с таким ID ещё не существует. Мы натолкнулись на эту проблему при использовании iblock.element.add.form с аяксом, в котором включена капча - если оная не успевается сгенерироваться и отдаться за ~300 миллисекунд, байндинг сабмита не происходит и следующий сабмит формы идёт уже не аяксом, а обычной перезагрузкой страницы, в итоге пользователь видит вывод компонента на пустой странице (т.к. AJAX_CALL=Y).
По этой проблеме был создан тикет, но был получен отлуп — «У компонента iblock.element.add.form нет ajax режима работы». Формально всё верно, т.к. галочки соответствующей у компонента действительно нет.
Однако, есть документация по работе компонентов в режиме ajax, которая регламентирует аякс-режим работы любого правильно сделанного компонента. Что ж, не будем ничего доказывать, копаем сами. Возникновения подобной проблемы можно добиться на любом стандартном компоненте с долго отдаваемой фотографией. Сделать это очень просто — втыкаем на страницу комплексный компонент catalog со включённым компонентом фильтра. В шаблон каталога производим вставку картинки:
<img src="/formtest_delay.php"/>
Содержимое скрипта-«картинки»:
<?php
delay(2);
?>
Отключаем временно показ этой картинки. Открываем каталог (ссылка вырезана цензурой:-)), нажимаем кнопку «Сбросить» в фильтре — каталог нормально отображается. Нажимаем ещё раз — всё ок.
Теперь подключаем картинку, проводим те же самые манипуляции. Нажимаем первый раз «Сбросить» — каталог прогрузился, однако, выскочила js-ошибка вида (копипаст из firebug):
top.BX("bxajaxid_9a85781ad70d7286c61a136298f44d6e_693") is null
[Break On This Error] <meta name="keywords" content="1С-Битр...rix, система управления контентом" />
При повторном нажатии на «Сбросить» страница уже перегружается и на ней отображается вывод компонента без шаблона сайта. Как раз то, что нужно — неведомый баг, который я отлавливал на протяжении полутора дней. Просто, быстро, но печально.
Стабильная повторяемость бага достигнута, теперь бы надо его пофиксить. Часик поразмышляв над нарисованными в тетради схемами вызовов всех функций, приводящих к багу, родилось два вариант Из очевидных решений на ум пришло два:
1. Реализация deferred-обработчиков load+ready для ифрэйма с костылем под обычный xhr.
2. Двухстрочный файлов ядра, позволящий сделать исполнение js-кода, выводимого компонентов, всегда после замены вывода компонента, тем самым решая проблема с асинхронностью. Сурово, но иначе никак. Без копания в ядре нижеприведённый патч вам ничего не скажет, но разобрать проблему и не показать решение было бы подло:-)
Как ни странно, столь развёрнутый разбор проблемы не вызвал никаких возражений со стороны саппорта, поэтому тикет погулял по нескольким ответственным и направился в отдел разработки. Бага признана незначительной, однако надеюсь, что фикс выйдет действительно в ближайших обновлениях системы, а не в далёком и туманном будущем
в 14 версии решается путем вставки в код формы в шаблоне template.php <script type="text/javascript"> top.BX.ajax.__runOnload(); </script> перед <?=$arResult["FORM_FOOTER"]?>
Живая лента — действительно очень клёвая штука для отслеживания рабочей активности. Но иногда приходят они — Обновления. И что-то начинает работать не так, как хотелось бы:-) В этот раз, например, пришло нововведение в модуле соцсетей (12.5.2*) с изменённой логикой вывода уведомлений задач в живую ленту. Если ранее записи попадали только в ленту пользователей, причастных к этой задаче, то теперь список получателей расширен в соответствии с настройкой группы:
У нас, например, любой сотрудник по умолчанию может посмотреть задачи любой открытой группы без вступления в неё. В итоге получилось, что в ЖЛ стали попадать совершенно чужие задачи из всех групп. С одной стороны, вполне логично, с другой — неудобно. На помощь, как всегда, пришло событие — socialnetwork.OnBeforeSocNetLogRightsAdd. Оно позволяет убрать «лишние» права с уведомлений и вернуть всё как было ранее. Конкретно наш кейс решается следующим обработчиком:
static public function disableNotificationForAllUsers($logId, $groupCode)
{
static $arLogEntry;
if (empty($logId)) {
return;
}
if ($arLogEntry['ID'] != $logId) {
$arLogEntry = CSocNetLog::GetByID($logId);
}
// Проверка типа записи - нужна задача из группы
if (!($arLogEntry['EVENT_ID'] == 'tasks' && $arLogEntry['ENTITY_TYPE'] == 'G')) {
return;
}
// "Авторизованные пользователи"
if ($groupCode == 'AU') {
return false;
}
}
После создания любой записи в ЖЛ на неё назначаются права, которые и можно ограничить в событии, возвращая false в нужные моменты (обработчик вызывается для каждого права доступа отдельно). Таким же образом можно не выводить новые статьи в открытых вики групп для всех пользователей, убирать системные уведомления (например, «пользователь вступил в группу» — кейс из жизни, у клиента ими порой вся страница забита) и т.п.
* версия, конечно, не такая и свежая — ещё от конца апреля, но мы не ставим сразу каждый апдейт, если всё хорошо работает.
Korban Michael, код группы для группы соцсети формируется по формату "SG" + ID группы. Если правильно понял ваш вопрос. Если неправильно - посмотрите ./bitrix/modules/main/classes/general/authproviders.php, там можно найти логику формирования кодов.
Стали замечать, что в последние дни слишком часто выводится 502 ошибка на таймвебе. Причём выводится рандомно - на одной и той же странице нажимаем ф5 и видим либо сайт, либо не сайт (хоть в публичке, хоть в админке). А в error_log сваливаются сообщения вида:
PHP Fatal error: Allowed memory size of 268435456 bytes exhausted (tried to allocate 1769104307 bytes) in /home/*/*/public_html/bitrix/modules/main/classes/mysql/database.php on line 0
Посмотрев на путь, заподозрили неладное и стали копать. И, кажется, докопали. В наших проблемах оказался виноват XCache (раньше был друкой кешер, не помню, какой). Всё решается некрасивой, но волшебной строчкой в .htaccess:
php_value xcache.cacher Off
После этого сайт работает стабильно. Написали так же обращение в саппорт с отсылкой на несколько серверов, где наблюдается проблема. На указанных серверах они что-то подкрутили и вроде бы ситуация исправилась. Так что если у вас возникнет такая же неведомая 502ая - попробуйте это временное решение.
Всех людей можно разделить на два типа: тех, кто уже влюбился в новый интерфейс админки битрикса, и тех, кто ещё не забыл старый. Я и некоторые коллеги пока относимся ко второму типу. Поэтому, для своего комфорта, сделал стиль для файрфоксовского stylish, коим хочу поделиться (ну вдруг кто-то тоже является поклонником некрупных интерфейсов:-)). Вот моменты, которые затрагивает этот стиль:
Отступы в формах;
Отступы в фильтрах;
Инпуты, селекты и кнопки;
Тени текстов;
Пункты главного меню;
Пункты подменю.
Всё перечисленное стиль делает поменьше. Скриншот «до» (ссылка):
Да, местами стало менее красиво, местами есть огрехи с симметричностью. И есть один редко появляющийся, а потому немешающий, косячок (кто найдёт больше — я не виноват=)). Но свою цель я достиг. Точнее, две: а) сделать меню поменьше, чтобы больше влезало как по вертикали, так и по горизонтали (это была моя первая претензия к новому интерфейсу); б) уменьшить формы в админке (на это меня побудил внешний вид детальной страницы тикета после обновления КП, некогда бывший вполне себе компактным).
Новый интерфейс больше подходит для презентаций, работать в нём не удобно, информации на экране мало. Решение Андрея немного исправляет ситуацию, но хотелось бы, чтобы об этом задумались разработчики.
Согласен с Зайцев Артемий в том, что в некоторых случаях (например в разговоре по телефону, когда нельзя тыкнуть пальцем) цветвое выделение особенно важных кнопок -- может быть очень полезным.
Группы на сайте создаются не только сотрудниками «1С-Битрикс», но и партнерами компании. Поэтому мнения участников групп могут не совпадать с позицией компании «1С-Битрикс».