воскресенье, 29 июля 2018 г.

Кастомизация bitrix:form.result.new — кое-что изменилось.


Предупреждение: эта статья создана в процессе разработки сайтов в компании Smart Traffic, в которой в настоящее время я работаю в должности разработчика Битрикс, и напечатана с разрешения руководства компании. Первоначально она была опубликована на нашем сайте.
Smart Traffic — интернет-агентство, предлагающее комплексное обслуживание в сфере интернет-маркетинга. Мы - отличная команда профессионалов, рекомендую обращаться к нам, если Вам необходимо разработать сайт на CMS 1С-Битрикс!

* * * 

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

Чаще всего, например, используется кастомизация компонента bitrix:form.result.new, находящегося в составе комплексного компонента bitrix.form. На первый взгляд, ничего необычного — кастомизируем также точно, как и любой компонент. Однако, не все так гладко, практика показывает, что кое-что работает по-другому. Где это прописано в ядре Битрикса — а бог его знает. Да и нужно ли в этом разбираться? Ведь все равно дорабатывать ядро мы не будем, помня о том, что все это затрется при первом же обновлении. Мы будем кастомизировать bitrix:form.result.new методом, похожим на стандартную кастомизацию, но все же отличающимся в некоторых деталях — мы учтем то, что не сработает, и обойдем эти проблемы.

Во-первых, как подключить собственный шаблон bitrix:form.result.new? И это не тот «собственный шаблон», который мы видим на странице настройки формы http://ваш_сайт.рф/bitrix/admin/form_edit.php?lang=ru&ID=ид_формы на вкладке «шаблон формы», где мы можем отметить опцию «использовать свой шаблон формы». Тут, насколько я понимаю, на самом деле собирается шаблон на основе стандартного табличного же шаблона с помощью специального конструктора — меняется количество и типы полей формы, встроенные в разметку стили, затем шаблон сохраняется, суда по всему, в базу данных, а не в файловую структуру сайта. И потом подключается оттуда. Как быть, если нам это не подходит — у нас уже есть сделанная верстальщиком блочная разметка формы, например?

Смотрим внутрь компонента bitrix:form. Видим, что как и в обычном комплексном компоненте, вложенный компоненет bitrix: form.result.new, например, подключается в файле new.php, который имеет такой вид:
<?
if (!defined("B_PROLOG_INCLUDED") || B_PROLOG_INCLUDED!==true) die();
$APPLICATION->IncludeComponent("bitrix:form.result.new", "", $arParams, $component);
?>
Казалось бы, все ок. Создадим собственный шаблон bitrix:form.result.new и подключим его в этом вызове, например, так:

$APPLICATION->IncludeComponent("bitrix:form.result.new", "moy_shablon", $arParams, $component);

Но нет! Не подключает,ся собака страшная! Возможно, это как-то связано с версионностью Битрикса на сайте, и в более новых версиях это возможно. Но у меня в данном конкретном случае использовалась версия 16.5.13 и обновлять ее было не желательно. Как ни бился, собственный шаблон через этот вызов подключить не удалось, всегда и неизменно подключается только дефолтовый шаблон, который лежит по адресу ваш_сайт.рф/bitrix/templates/ваш_шаблон_сайта/components/bitrix/form/ваш_шаблон_комплексного_компонента_форм/bitrix/form.result.new/.default ! Даже если в настройках комплексного компонента bitrix:form убирать опцию "IGNORE_CUSTOM_TEMPLATE" => "Y" — все равно не работает, по крайней мере на этом конкретном сайте! Очевидно, что эта настройка, предназначена, чтобы блокировать подключение тех кастомных шаблонов, которые сделаны в конструкторе в админке и хранятся в базе данных.

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

Итак, чтобы обойти тот момент, что кастомный шаблон bitrx:form.result.new не желает подключаться в вызове файла new.php делаем так — кастомизируем сам шаблон ваш_сайт.рф/bitrix/templates/ваш_шаблон_сайта/components/bitrix/form/ваш_шаблон_комплексного_компонента_форм/bitrix/form.result.new/.default . А на странице настроек шаблона оставим отмеченной опцию «использовать стандартный шаблон». И после кастомизации обычно все работает. Способ кастомизации достаточно подробно описан в интернете, все детали его обсужать не будем. Остановимся только на том моменте, который вызвал — причем не сразу, а со временем, - ошибку работы формы. А именно — первое, что делается очень часто (хотя и не всегда) в кастомном шаблоне — строка кода <?=$arResult[«FORM_HEADER»] ?>, которая отвечает за показ заголовка формы, подменяется на что-то такое:

<form id="bp1" name="<?=$arResult["WEB_FORM_NAME"]?>" action="<?=POST_FORM_ACTION_URI?>" method="POST" enctype="multipart/form-data" class="contact-form" novalidate="novalidate">
<input type="hidden" name="WEB_FORM_ID" value="<?=$arParams["WEB_FORM_ID"]?>">
<?=bitrix_sessid_post()?>

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

https://dev.1c-bitrix.ru/api_help/main/functions/other/bitrix_sessid_post.php

https://dev.1c-bitrix.ru/api_help/main/functions/other/check_bitrix_sessid().php

В данном конкретном случае к тегу form был добавлен id="bp1", который ни для чего другого не служит, кроме как для подключения нужных стилей к самой форме и вложенным в нее элементам разметки. Все остальные строки кода в кастомном шаблоне нас не интересуют в данном случае — они зависят от конкретной реализации разметки шаблона. Итак, разработчик сделал это до того, как сайт попал в мои руки. Сделано было грамотно, и все работало какое-то время, кажется, даже несколько лет. И в один «прекрасный» момент работать перестало. Форма перестала правильно отрабатывать, выбрасывая сообщение, что данные получены, в результаты формы перестало писаться все, что нужно. Ну и понятно, что и почтовый шаблон перестал отрабатывать, потому что форма не пишется вообще. Некоторое время я потратил на то, чтобы 1) проверить все настройки самой формы в админке 2) проверить кастомизированный шаблон bitrix:form.result.new, в котором я не нашел ничего криминального. Затем стал отлаживать форму по частям. Первым делом, перенастроил код — подсунул шаблону .default стандартную табличную форму — и о чудо! Табличная форма отрабатывает нормально! Только вид у нее, естественно, «табличный», то есть никакой вообще. Ну что же, стал я тогда брать значимые куски кода из кастомной формы и подставлять их в стандартный табличный шаблон, ожидая, когда же он помрёт. Повезло практически сразу — подстановка приведенного выше куска, отвечающего за заголовок формы, вместо <?=$arResult[«FORM_HEADER»] ?>, сразу же вызвало ту же самую ошибку — форма перестала отправляться! Попробовал убрать из него из тега form параметр id="bp1" — а вдруг ядро битрикс где-то там внутри себя использует идентификаторы для обработки формы, и мой идентификатор этому мешает? Нет, не помогло. Конечно же, сразу пришло в голову, что что-то изменилось в ядре, что отвечало за обработку форм. И было бы в будущем очень интересно понять — а что же именно. Но — это задача академическая, так сказать. А заказчик сайта ждет, когда же бедные погибшие формы заработают. И ждет чтобы это случилось как можно скорее! Поэтому на этот раз я просто обошел проблему — заменил кастомно прописанный заголовок обратно на стандартный вызов заголовка формы, но для сохранения стилизации заключил его в div с тем же самым id, который до этого был в теге form. Вот как это стало выглядеть:

<div id="bp1">
<?=$arResult["FORM_HEADER"]?>
… собственно код кастомизированной формы …
<?=$arResult["FORM_FOOTER"]?>
</div>

И о чудо! Все заработало, хотя казалось бы это должно было быть одно и тоже, но оказалось, что не одно. Надо сказать, что проблема на этом сайте возникла после одного из обновлений Битрикс. Вот такой вот он Битрикс — странный и загадочный! И не всегда обратно совместимый — особенно если кастомизированный. 

понедельник, 11 июня 2018 г.

ГЕНЕРАЦИЯ ДОКУМЕНТОВ DOCX ИЗ PHP С ПОМОЩЬЮ БИБЛИОТЕКИ PHPWORD

Предупреждение: эта статья создана в процессе разработки сайтов в компании Smart Traffic, в которой в настоящее время я работаю в должности разработчика Битрикс, и напечатана с разрешения руководства компании. Первоначально она была опубликована на нашем сайте.
Smart Traffic — интернет-агентство, предлагающее комплексное обслуживание в сфере интернет-маркетинга. Мы - отличная команда профессионалов, рекомендую обращаться к нам, если Вам необходимо разработать сайт на CMS 1С-Битрикс!
* * *
На первый взгляд, генерация документов docx не представляет какой-то особенной сложности — есть известная библиотека PHPOffice/PHPWord, есть документация с подробным ее описанием. Как говорится, берем и делаем. Не тут-то было.
Итак, поступила задача по доработке одного из сайтов, а именно — при генерации документов в текстовом формате Microsoft получается ошибка. Документ формируется, но не открывается. При попытке открыть файл мы иногда видим окно сообщения, где указано, что в файле присутствуют не закрытые либо не имеющие открывающего тега закрывающие теги (то есть «не открытые»). Причем для разных файлов теги и их расположение разные и некоторые файлы открываются, а некоторые — нет. В чем дело и как быть дальше?
Первым делом, я решил посмотреть, какие теги вызывают проблемы. Для этого нужно распаковать документ docx. Поскольку я пользуюсь для разработки ubuntu linux на рабочем месте, то делаю это, например, командой:
7z a -tzip файл.docx -mx0 ./директория/*
Если же вы используете Windows, воспользуйтесь архиватором 7zip или WinRAR (любым).
Надо сказать, что файл docx на самом деле обыкновенный архив, внутри которого уже после распаковки мы увидим массу разных файлов, служебных и содержательных, преимущественно, это — xml файлы. Среди них находим тот файл, который содержит непосредственно контент — document.xml. Просматриваем те теги, которые указаны в ошибке, и видим, что в качестве текста в узлах, помимо обычного текста, вставлены теги <p>, </p> и <br />, <br>. Откуда же они взялись? Поскольку генератор был написан ранее неизвестным разработчиком, смотрим в него, и видим, что данные для генерации получаются из различных полей элемента инфоблока Битрикс. Подробно это рассматривать неинтересно, у каждого сайта это могут быть другие поля и другой программный код, который получает их для добавления в генерируемый документ.
Достаточно сказать, что этот элемент находится в выводе компонента на странице, где формируется заказ товаров в админке менеджера. В нашем случае проблемное поле получалось в php из поля элемента (товара) «детальное описание». Смотрим в каталоге Битрикс детальные описания вызвавших проблемы товаров, и, действительно, видим, что контент-менеджеры вставили там тексты в формате html с указанными тегами, причем очень часто теги эти, действительно, не закрыты или не открыты — банальная ошибка при написании html. Однако, исправлять вручную это совершенно невозможно — на сайте более 10 тысяч товаров, кроме того, в будущем ошибочное заполнение также не исключено. Следовательно — необходимо предусмотреть программное исправление ошибок, насколько это возможно.
Прежде всего, пишем функцию для закрытия/открытия тегов. Однако, после ее тестирования обнаруживаем пару интересных моментов. Первое — иногда одиночные теги в тексте расставлены контент-менеджером так, что и не поймешь, что он хотел заключить внутрь параграфа, так что и программно функция тоже этого не может вычислить, и тогда наша функция просто закрывает одиночный тег пустым. Получаются пустые параграфы. С этим мы просто вынуждены смириться. В конце концов, при необходимости — контент-менеджер откроет отдельную карточку товара и сам поправит нужный параграф. Ни программист, ни даже никакая самая лучшая программа мысли читать не умеет. Второе — теги переноса строк <br> и <br /> попадают в текст узлов xml как часть текста, а не как реальные переносы строк, и это тоже вызывает ошибки — внутри текста генератор документа просто не знает, что с ними делать. Поэтому я добавил в функцию замену их и пустых параграфов на перенос строки «\n». Кроме того, нам не нужны были ссылки в формируемых документах, поэтому их я тоже удалил. Получилась такая функция:
function closetags($text)
{
// Выбираем абсолютно все теги
if (preg_match_all("/<([/]?)([wd]+)[^>/]*>/", $text, $matches, PREG_SET_ORDER))
{
$stack = array();
foreach ($matches as $k => $match)
{
$tag = strtolower($match[2]);
if (!$match[1])
// если тег открывается добавляем в стек
$stack[] = $tag;
elseif (end($stack) == $tag)
// если тег закрывается, удаляем из стека
array_pop($stack);
else
// если это закрывающий тег, который не открыт, открываем
$text = '<'.$tag.'>'.$text;
}
while ($tag = array_pop($stack))
// закрываем все открытые теги
$text .= '</'.$tag.'>';
}

$text = str_replace("<br />", "\n", $text);
$text = str_replace("<br>", "\n", $text);
$text = str_replace("<p></p>", "\n", $text);


//удаляем ссылки из документов
$pattern = "/<a href=[\"|']([^\"]*)[\"|']>(.*)<\/a>/iU";
return preg_replace($pattern, "", $text);
}

Теперь все хорошо, формируются все документы docx, независимо от того, есть ошибки в тегах в дополнительном описании товара в каталоге или нет их. Однако оказалось, что это еще не вся проблема.
Формируемый документ состоит не только из данных, получаемых из поля дополнительного описания, обрабатывается еще несколько разных полей с информацией. И вот между ними в нужных местах должны быть переносы строк, а их нет — текст получается в одну строку. При этом, что интересно, в отдельных местах текста переносы работают, а других — нет. Открываем генератор текста из php, и смотрим, какие строки за это отвечают. Находим два варианта формирования переноса стандартным для PHPWord способом:
1) $section→addTextBreak();

2) $textRun = $section→addTextRun();

то есть, вначале создаем текст, чтобы добавлять в него все, что потребуется, а затем делаем:
 $textRun→addTextBreak();

И вот второй-то способ АБСОЛЮТНО не работает.
Странно — идем на сайт PHPWord и ищем документацию.
Генерация документов docx из php с помощью библиотеки PHPWord

На самом верху страницы https://phpword.readthedocs.io/en/latest/elements.html#breaks видим гордые плюсики для Text Break для всех типов элементов! Однако не поторопились ли разработчики? Может быть, все дело в том, что наш генератор разрабатывался довольно давно, и библиотека устарела с тех пор, как ее поставили на этот сайт? Действительно, в описаниях релизов за несколько последних лет можно найти, что по крайней мере один раз исправлен баг в Text Break, может быть, это он? Ничего подобного — обновляю библиотеку PHPWord на сайте до самой последней версии, и все равно перенос не работает!
В итоге пришлось полностью переработать генератор документа docx, исключив из него вообще Text Run и воспользовавшись только Section, с которым перенос работал нормально.
Вывод: не всегда следует верить задокументированным функциям даже у хорошо известных и проверенных программных продуктов. Конечно, вполне возможно, что на работоспособность повлияли какие-то особенные настройки php именно на данном конкретном сервере на данном сайте — но такие моменты тоже должны быть поштучно выявлены разработчиками и отражены в документации для PHPWord или любой другой программы.