[Из песочницы] Как Битрикс чуть Новый Год не погубил

Жили мы весело в небольшой веб-студии, делали сайты-визитки, интернет-магазины и небольшие порталы. Были проекты и на платформе 1С-Битрикс. Мы, конечно, не являлись официальным интегратором Битрикс, но делали работоспособные проекты на сколько позволяли силы и опыт. Казалось бы, какие только компоненты не приходилось нам использовать, но сие чудо отечественных мозгов сумело сделать сюрприз под новый год.Поступил к нам новый заказ — интернет-магазин. К слову, были у нас и еще интернет-магазины, работали они на написанной нами CMS, основанной на Codeigniter фреймворке. Работали неплохо, довольно шустро. Но время брало свое, вышел Laravel4 (как всё просто и чудесно), Yii2 (наконец-то стабильный), Phalcon (Си — это очень быстро) и использовать умерший окончательно CI (кто-нибудь возьмите меня домой) не было больше сил. Переглянувшись с ассистентом, мы сразу поняли, что создавать новый заказ на старой системе совсем не хочется. Были мысли переписать удачные решения интернет-магазина на Yii2, смотрели в сторону Open Cart, CS Cart и PrestaShop, но точку в вопросе поставил заказчик — 1С Битрикс (редакция Бизнес). Светлые надежды аккуратно сложили мыло с веревкой в чемодан и отправились восвояси. С другой стороны, не всё так плохо, подумал я. У нас будет набор готовых качественных решений (ключевое слово — готовых, о чем заказчик был предупрежден), останется лишь внедрить верстку. И спустя пару дней работа закипела.«Ура! Он загрузился и установился!» — воскликнул я, допивая потерянную в счете чашку чая. «Черт возьми, что за куча файлов?!», — подумал git и задумался.

За основу я взял интернет-магазин одежды, который можно установить вместе с Битриксом.

Сидим мы с товарищем в офисе, натягиваем незначительные компоненты и вдруг получаем первый приз от Битрикса. Есть у этой системы возможность объединять и сжимать css и js файлы, подключенные правильным образом. «Где я напортачил» — первая мыль пришедшая в голову, когда jquery перестал подключаться. Судорожно жму Ctrl+Z, отменяя написанный код, но ничего не помогает. В бой идут немыслимые варианты, но и они не приносят успеха. В голове хаос. Ухожу попить чай. Пока меня не было, Битрикс клятвенно умолял вернуться, говорил, что всё простит и заработал. Когда я снова запустил сайт сборка статики была в полном порядке. Магия, подумал я и хотел было уже забыть про это, как на тот же баг напарывается мой товарищ, сидевший в нескольких метрах от меня. Гугление постановило, что люди сталкиваются с подобным, но решения нет.

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

Кому интересно, head часть выглядела так:

head ShowMeta («robots», false, true); $APPLICATION→ShowMeta («keywords», false, true); $APPLICATION→ShowMeta («description», false, true); ?> »/> »/> »/> »/>

AddHeadScript (SITE_TEMPLATE_PATH.»/js/jquery-1.11.1.min.js»);?> AddHeadScript (SITE_TEMPLATE_PATH.»/functions.js»);?>

ShowHeadStrings (); $APPLICATION→ShowHeadScripts (); ?>

<? $APPLICATION->ShowTitle () ?> Следующий подарок был на самом деле последний, но по смыслу он был второй.У нас были разделы каталога, ничего особенного. Переходя в раздел открывались товары из этого раздела. Адреса совершенно стандартные (#SITE_DIR#/catalog/#SECTION_CODE#/). Так вот, при переходе по адресу #SITE_DIR#/catalog/ я наблюдал грустную мордочку в хроме и сообщение о разрыве соединения. Как и положено в /catalog/index.php было подключение комплексного компонента каталога. «Это просто невероятно» — думал я, тогда уже на последнем издыхании 30 декабря. Но не отступал. Была в этом файле одна особенность. В случае ajax запроса нужно было подключить компонент без вывода шапки и подвала, а в обычном режиме с шапкой и подвалом. Сделано это было проверкой:

if ($_REQUEST['ajax']=='Y') Я не сразу догадался, но методом исключения выяснилось, что при отсутствии $_REQUEST[«ajax»] мы получали NOTICE, который почему-то отключал дальнейшую работу Битрикса. При добавлении проверки isset каталог заработал. if (isset ($_REQUEST['ajax']) && $_REQUEST['ajax']=='Y') ТЗ явно указывало на то, что у нас будут использоваться торговые предложения. С ними, честно говоря, раньше не имел дела, но почитав документацию и посмотрев видео уроки понял, что это крутой функционал, который на одном из сайтов мы когда-то делали вручную. Сердце этого функционала — детальная карточка товара и выбор торговых предложений. Естественно от свойств торгового предложения зависит цена и всё из этого вытекающее, а возможно какого-то торгового предложения и вовсе нет, тогда нужно показать вместо кнопки в корзине форму подписки на товар. Я посмотрел стандартный шаблон и обрадовался, когда увидел, что и выбор торгового предложения (с динамическим изменением цены и зависимых блоков) и подписка уже есть и работают, но радость моя была не долгой. За динамическое управление и выбор торговых предложений отвечает js файлик \bitrix\components\bitrix\catalog.element\templates\.default\script.js, 2839 чистого недокументированного яваскрипта, бонусом к нему шел result_modifier.php в компоненте детальной страницы товара, который инициализировал js объект на странице и подготавливал данные.Два дня я пытался адаптировать это чудо к нашей верстке, еще два дня меня терзали мысли в духе «ну может быть это всё-таки возможно». На исходе четвертого дня я сдался и приступил к своей реализации. Сделал на jquery, конечно, в 1С всё сделано через BX.js, которая тоже мало где описана. Реализация с комментариями заняли порядка 200 строк.

Немного поясню, в чем всё-таки была сложность и почему я сразу не стал писать сам. Свойств торгового предложения может быть сколько угодно, комбинация этих свойств образует какое-то торговое предложение, которое завели через админку. Так вот когда пользователь выбирает, например, аккумулятор с емкостью 1000 mA синего цвета производства «Супер фирмы» нам нужно определить:, а какому торговому предложению соответствует эта комбинация выбранных свойств? Найдя торговое предложение нужно либо отобразить цену, проверить скидку и отобразить если нужно, сделать свойства выбранными либо показать подписку на этот товар. Есть ряд других мелочей. Например, при отображении страницы нужно выбрать свойства соответствующие самому дешевому торговому предложению и сделать их выбранными. В корзину, если это торговое предложение, должны добавляться торговые предложения с указанными свойствами.

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

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

Итак, при добавлении торгового предложения в корзину оно добавлялось, но вот свойства этого торгового предложения никак не сохранялись. Это проблему так и не удалось «нагуглить». Решение оказалось странным. После добавления товара в корзину вызвать метод Update и передать свойства еще раз. На этот раз они запоминались.

$code = Add2BasketByProductID ($productID, $QUANTITY, $arRewriteFields, $product_properties);

if (!$code) { $response['status'] = 400; $response['message'] = 'Не удалось добавить товар в корзину'; } else { $response['basket'] = getActualSmallBasket ();

/*fix запоминание какие именно свойства sku были выбраны*/ if (is_array ($productProperties)) { $arFields[«PROPS»] = $productProperties; CSaleBasket: Update ($code, $arFields); } } Следующий неприятный момент был связан с поиском. Результаты поиска, по задумке дизайнера, должны были делиться на три вкладки. Найдено в товарах, найдено в статьях, найдено в новостях. И опять же, казалось бы есть компонент (bitrix: search.page), который умеет искать везде, но массив arResult слегка удивил. Результаты поиска были все вперемешку без особых отличительных признаков статья ли это, товар или вообще раздел. В итоге удалось опереться на странную ячейку в arResult[«PARAM2»], в которой оказались id инфоблоков для данных, а чтобы отсечь разделы из результатов поиска, мы проверяли ячейку ITEM_ID на наличие буквы S, которая явно была присуща разделам. Тут мне совсем надоели непонятные ячейки в arResult, и я кинулся искать, может где-то они все описаны. Но так ничего и не найдя, я спросил на Тостере. Как видно, проблему это не решило.И снова каталог. Как вы помните, мы подключали каталог по-разному, в зависимости ajax это запрос или обычная загрузка страницы. Ajax запросы происходили при использовании сортировки или пагинации. Пагинация кстати была в стиле «Показать еще». Естественно, ее пришлось делать самим, потому что дефолтная работала по-другому. И тут мы сами того не подозревая загнали себя в угол. Битрикс передает параметры пагинации в гет параметрах PAGEN_1, PAGEN_2. Конфигурация и значения этих параметров зависят от количества компонентов, которые используют пагинацию на странице. То есть каждому компоненту по своему параметру для пагинации. И получилось так что при отображении страницы обычным хитом у нас подключался хедер, в котором тоже был компонент использующий пагинацию, а при ajax запросе он у нас не подключался. В результате получилась рассинхронизация параметров пагинации. Чтобы решить эту проблему увы пришлось писать костыль, и просто подставлять высчитанную компонентом пагинацию не получилось.

Какой же магазин без умного фильтра. И у нас он тоже был. Не скажу, что были проблемы с самим фильтром, но вот с фильтрацией по цене была загвоздка. Из-за того, что у нас были торговые предложения, то цена могла быть как в торговых предложениях, так и в товаре. Увы нам не удалось найти информации как заставить компонент умного фильтра видеть цены и различать торговые предложения от просто товаров. На помощь пришли наши «любимые» костыли. Решение виделось следующим: Использовать события при создании и редактировании товаров и заполнять программно два свойства — минимальная и максимальная цена. Для обычных товаров значения этих свойств будут равны. И тут меня тоже ждала подстава. Найти на какие же именно события нужно реагировать с первой попытки не удалось. Не буду томить, попался я на такую особенность: Методы, добавляющие товар и цены для товара являются разными. Сначала добавляется товар, затем цены для него. Событие OnAfterIBlockElementAdd как раз между эти двумя действиями. Итоговые обработчики выглядели так:

/*при измении цен в товарах без торговых предложений*/ AddEventHandler («catalog», «OnPriceUpdate», Array («DiEvent», «OnPriceUpdateHandler»)); AddEventHandler («catalog», «OnPriceAdd», Array («DiEvent», «OnPriceAddHandler»));

/*при измении цен в товарах с торговыми предложениями*/ AddEventHandler («catalog», «OnProductAdd», Array («DiEvent», «OnProductAddHandler»)); AddEventHandler («iblock», «OnAfterIBlockElementUpdate», Array («DiEvent», «OnProductUpdateHandler»));

/** * Обновление цены в ОБЫЧНЫХ товарах без торговых предложений * * @param $id * @param $arFields */ function OnPriceUpdateHandler ($id, $arFields) { self: updateFilterPrice ($arFields['PRODUCT_ID']); }

/** * Добавление цены в ОБЫЧНЫХ товарах без торговых предложений * * @param $id * @param $arFields */ function OnPriceAddHandler ($id, $arFields) { self: updateFilterPrice ($arFields['PRODUCT_ID']); }

/** * Добавление цены при создании товара с торговыми предложениями * * @param $id * @param $arFields */ function OnProductAddHandler ($id, $arFields) { self: updateFilterPrice ($id); }

/** * Добавление цены при обновлении товара с торговыми предложениями * OnProductUpdate какого хрена не работает (( * @param $arFields */ function OnProductUpdateHandler (&$arFields) { if ($arFields['IBLOCK_ID'] == 2) { self: updateFilterPrice ($arFields['ID']); } }

/** * Высчитываем минимальную и максимальную цену товара и заполняет спец свойства * MIN_OFFER_PRICE, MAX_OFFER_PRICE для фильтрации в умном фильтре. * ЦЕНЫ БЕРУТСЯ ИЗ ЦЕНЫ ТИПА BASE! Другие типы цен никак не учитываются. * * @param $PRODUCT_ID id Товара */ public static function updateFilterPrice ($PRODUCT_ID) { $EL = new CIBlockElement ();

//получаем массив торговых предложений товара $arr = CIBlockPriceTools: GetOffersArray ( array ('IBLOCK_ID' => 2), array ($PRODUCT_ID), array (), array (), array (), 0, CIBlockPriceTools: GetCatalogPrices (2, array ('BASE')) );

if (is_array ($arr) && count ($arr) > 0) { $minPrice = null; $maxPrice = 0;

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

foreach ($arr as $offer) { $offerMinPrice = $offer['MIN_PRICE']['VALUE'];

if (is_null ($minPrice)) { $minPrice = $offerMinPrice; } else { if ($offerMinPrice < $minPrice) { $minPrice = $offerMinPrice; } }

if ($offerMinPrice > $maxPrice) { $maxPrice = $offerMinPrice; } }

//обновляем два свойства MIN_OFFER_PRICE, MAX_OFFER_PRICE $EL→SetPropertyValuesEx ($PRODUCT_ID, 2, array ('MIN_OFFER_PRICE'=>$minPrice, 'MAX_OFFER_PRICE'=>$maxPrice,) ); } else { //товар без торговых предложений $priceType = CIBlockPriceTools: GetCatalogPrices (2, array ('BASE')); $cgroup = $priceType['BASE']['SELECT'];

//получаем данные о товаре с информацией по ценам! $result = $EL→GetList (array (), array ('IBLOCK_ID'=>2, 'ID'=>$PRODUCT_ID), false, false, array ('*', $cgroup)); $arrElm = $result→GetNextElement ();

if (is_object ($arrElm)) { $fields = $arrElm→GetFields ();

//информация о цене товара $price = CIBlockPriceTools: GetItemPrices (2, $priceType, $fields);

//обновляем два свойства MIN_OFFER_PRICE, MAX_OFFER_PRICE $EL→SetPropertyValuesEx ($PRODUCT_ID, 2, array ('MIN_OFFER_PRICE'=>$price['BASE']['VALUE'], 'MAX_OFFER_PRICE'=>$price['BASE']['VALUE'],) ); } } } Ну и самой большой подставой я считаю компонент пошагового оформления заказа (sale.order.full). После двух дней интеграции, дизайна, системы EDOST и Робокассы, оказалось, что этот компонент не умеет учитывать никакие скидки. То есть он не мог отображать итоговую сумму с учетом скидки. Вот кстати ссылка на форум от куда я это и узнал. Ответ Юрия Волошина просто шокировал.Ну и хочется подвести итог. Битрикс очень сильно разрекламированный продукт, не оправдавший ожиданий. При его использовании я неоднократно натыкался на суровые ограничения, с которыми заказчик не хотел мириться, хотя и был предупрежден, что Битрикс — это набор готовых решений и изменяя поведение этих решений мы пишем свой второй велосипед, привнося N багов. Чудесный функционал, который компания 1C показывает на презентациях, совместим только с их дефолтным шаблоном. Свой дизайн внедряется со скрипом. Очень большое количество плохо документированных методов, а к некоторым документации и вовсе нет. Гит порой задумывался очень надолго, индексируя множество файлов. Отсутствие версионирования и миграций БД серьезно докучает, когда разработчиков больше одного. Со всем этим можно было бы смириться, если не знать, сколько стоит этот продукт. Я думаю, профи Битрикса скажут, что я просто не умею его готовить. Мой аргумент таков: компания не позаботилась, чтобы информация о том, как его правильно готовить, была доступной и бесплатной как минимум. Скудные форумы и немногословная техподдержка, вот всё, что было у нас.

Возвращаясь к нашему заказу и, собственно, поясняя, почему пост так называется, хочется сказать, что сроки сдачи были сорваны, проект должен был быть сдан до Нового Года. Я работал над этим интернет-магазином до вечера 30 декабря и молился, чтобы не пришлось сидеть 31, а потом еще и в первых числах января. Интеграция дизайна со всеми «хотелками» заказчика в Битрикс весьма не быстрое занятие и весьма неприятное.

© Habrahabr.ru