Жили мы весело в небольшой веб-студии, делали сайты-визитки, интернет-магазины и небольшие порталы. Были проекты и на платформе 1С-Битрикс. Мы, конечно, не являлись официальным интегратором Битрикс, но делали работоспособные проекты на сколько позволяли силы и опыт. Казалось бы, какие только компоненты не приходилось нам использовать, но сие чудо отечественных мозгов сумело сделать сюрприз под новый год.
Поступил к нам новый заказ – интернет-магазин. К слову, были у нас и еще интернет-магазины, работали они на написанной нами CMS, основанной на Codeigniter фреймворке. Работали неплохо, довольно шустро. Но время брало свое, вышел Laravel4 (как всё просто и чудесно), Yii2 (наконец-то стабильный), Phalcon (Си – это очень быстро) и использовать умерший окончательно CI (кто-нибудь возьмите меня домой) не было больше сил. Переглянувшись с ассистентом, мы сразу поняли, что создавать новый заказ на старой системе совсем не хочется. Были мысли переписать удачные решения интернет-магазина на Yii2, смотрели в сторону Open Cart, CS Cart и PrestaShop, но точку в вопросе поставил заказчик – 1С Битрикс (редакция Бизнес). Светлые надежды аккуратно сложили мыло с веревкой в чемодан и отправились восвояси. С другой стороны, не всё так плохо, подумал я. У нас будет набор готовых качественных решений (ключевое слово — готовых, о чем заказчик был предупрежден), останется лишь внедрить верстку. И спустя пару дней работа закипела.
«Ура! Он загрузился и установился!» – воскликнул я, допивая потерянную в счете чашку чая. «Черт возьми, что за куча файлов?!», – подумал git и задумался.
За основу я взял интернет-магазин одежды, который можно установить вместе с Битриксом.
Сидим мы с товарищем в офисе, натягиваем незначительные компоненты и вдруг получаем первый приз от Битрикса. Есть у этой системы возможность объединять и сжимать css и js файлы, подключенные правильным образом. «Где я напортачил» – первая мыль пришедшая в голову, когда jquery перестал подключаться. Судорожно жму Ctrl+Z, отменяя написанный код, но ничего не помогает. В бой идут немыслимые варианты, но и они не приносят успеха. В голове хаос. Ухожу попить чай. Пока меня не было, Битрикс клятвенно умолял вернуться, говорил, что всё простит и заработал. Когда я снова запустил сайт сборка статики была в полном порядке. Магия, подумал я и хотел было уже забыть про это, как на тот же баг напарывается мой товарищ, сидевший в нескольких метрах от меня. Гугление постановило, что люди сталкиваются с подобным, но решения нет.
Как и в моем случае, спустя какое-то время статика на сайте починилась сама собой. К слову, этот баг у нас всплыл еще по разу в процессе разработки. Суть этого бага, видимо, заключается в том, что по не установленной причине порядок js файлов при объединении путается или некоторые нужные файлы js не попадают в объединение.
Кому интересно, head часть выглядела так:
<head>
<meta charset="utf-8"/>
<?
$APPLICATION->ShowMeta("robots", false, true);
$APPLICATION->ShowMeta("keywords", false, true);
$APPLICATION->ShowMeta("description", false, true);
?>
<link rel="shortcut icon" type="image/x-icon" href="<?= SITE_DIR ?>/favicon.ico"/>
<link rel="stylesheet"
type="text/css"
href="<?= CUtil::GetAdditionalFileURL('https://fonts.googleapis.com/css?family=PT+Sans:400,700,400italic,700italic&subset=latin,cyrillic'
) ?>"/>
<link rel="stylesheet"
type="text/css"
href="<?= CUtil::GetAdditionalFileURL('https://fonts.googleapis.com/css?family=Noto+Sans&subset=latin,cyrillic'
) ?>"/>
<link rel="stylesheet"
type="text/css"
href="<?= CUtil::GetAdditionalFileURL('https://fonts.googleapis.com/css?family=Roboto+Slab:400,300,700&subset=latin,cyrillic'
) ?>"/>
<?
$APPLICATION->ShowCSS(true, true);
?>
<link rel="stylesheet"
type="text/css"
href="<?= CUtil::GetAdditionalFileURL(SITE_TEMPLATE_PATH . "/css/jquery.formstyler.css") ?>"/>
<link rel="stylesheet"
type="text/css"
href="<?= CUtil::GetAdditionalFileURL(SITE_TEMPLATE_PATH . "/css/jquery.nouislider.css") ?>"/>
<link rel="stylesheet"
type="text/css"
href="<?= CUtil::GetAdditionalFileURL(SITE_TEMPLATE_PATH . "/css/jquery.ad-gallery.css") ?>"/>
<link rel="stylesheet"
type="text/css"
href="<?= CUtil::GetAdditionalFileURL(SITE_TEMPLATE_PATH . "/css/keyboard.css") ?>"/>
<?$APPLICATION->AddHeadScript(SITE_TEMPLATE_PATH."/js/jquery-1.11.1.min.js");?>
<?$APPLICATION->AddHeadScript(SITE_TEMPLATE_PATH."/functions.js");?>
<?
$APPLICATION->ShowHeadStrings();
$APPLICATION->ShowHeadScripts();
?>
<!--[if lt IE 9]>
<script src="http://html5shim.googlecode.com/svn/trunk/html5.js"></script>
<![endif]-->
<title><? $APPLICATION->ShowTitle() ?></title>
</head>
Следующий подарок был на самом деле последний, но по смыслу он был второй.
У нас были разделы каталога, ничего особенного. Переходя в раздел открывались товары из этого раздела. Адреса совершенно стандартные (#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 файлик bitrixcomponentsbitrixcatalog.elementtemplates.defaultscript.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, а потом еще и в первых числах января. Интеграция дизайна со всеми «хотелками» заказчика в Битрикс весьма не быстрое занятие и весьма неприятное.
Автор: komandakycto