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

в 9:15, , рубрики: 1С-Битрикс, php, битрикс, Веб-разработка

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

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

«Ура! Он загрузился и установился!» – воскликнул я, допивая потерянную в счете чашку чая. «Черт возьми, что за куча файлов?!», – подумал git и задумался.

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

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

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

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

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

Источник


* - обязательные к заполнению поля


https://ajax.googleapis.com/ajax/libs/jquery/3.4.1/jquery.min.js