Класс выгрузки каталога товаров в xml

Для выгрузки каталога товаров в различные площадки, в разных форматах на Marketplace можно подобрать подходящий модуль, и для непрограммиста это будет отличное решение!

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

В данном посте я выложу свою заготовку создания xml-файла с актуальной версией небольшого каталога товаров.

Существует 2 варианта представления каталога товаров на сайте: простой - когда товар и все его торговые предложения имеют один и тот же url-адрес (и частный случай - каталог без торговых предложений) и более сложный - когда торговые предложения имеют собственные url-адреса, отличающиеся от основного товара какой-то дополнительной солью.

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

В данном посте рассмотрю простой вариант, т.к. он встречается чаще  (в моей работе), хотя с точки зрения сео - вариант отдельных адресов может быть и не совсем верно.

В любом случае, принимать решение о том, какой вариант лучше - вам, причем, в каждой конкретной ситуации.

Для начала кладем где-то файл с классом, например сюда: /bitrix/php_interface/classes/ccatalogexport.php следующего содержания:

namespace Pai\Tools;

use \Bitrix\Main\Data\Cache;
use Bitrix\Main\IO,
	Bitrix\Main\Application;
use Bitrix\Main\Loader;

class CCatalogExport
{

	var $CATALOG_IBLOCK;
	var $OFFER_IBLOCK;
	var $EXCLUDE_SECTIONS = [83, 88, 89]; // тут указываем разделы, которые выгружать не нужно
	var $FirmName = 'ТМ Торговая Марка';
	var $url = "https://site.ua";
	var $Scheme;
	var $result;

	function __construct($CATALOG_IBLOCK, $OFFER_IBLOCK)
	{
		$this->CATALOG_IBLOCK = $CATALOG_IBLOCK;
		$this->OFFER_IBLOCK = $OFFER_IBLOCK;
	}

	private function GetSectionsList()
	{
		$result = false;
		$needNavChain = false;
		$cache_params = array('function' => __FUNCTION__, 'IBLOCK_ID' => $this->CATALOG_IBLOCK,
			'EXCLUDE' => $this->EXCLUDE_SECTIONS, $needNavChain);
		$cache_id = md5(serialize($cache_params));
		$cache_dir = __CLASS__;
		$cache = Cache::createInstance();

		$life_time = 60 * 60 * 24 * 30;

		if ($life_time < 0)
		{
			$cache->clean($cache_id, $cache_dir);
		}

		if ($cache->initCache($life_time, $cache_id, $cache_dir))
		{
			$result = $cache->getVars();
		} elseif ($cache->startDataCache() && \Bitrix\Main\Loader::includeModule('iblock'))
		{
			$Sect_list = \CIBlockSection::GetList(array("DEPTH_LEVEL" => "ASC", "ID" => "ASC"),
				array('IBLOCK_ID' => $this->CATALOG_IBLOCK, 'GLOBAL_ACTIVE' => 'Y', '!ID' => $this->EXCLUDE_SECTIONS),
				false, array('ID', 'IBLOCK_ID', 'NAME', 'SECTION_PAGE_URL', 'IBLOCK_SECTION_ID'));
			while ($Section = $Sect_list->GetNext())
			{
				if ($needNavChain)
				{
					$nav = \CIBlockSection::GetNavChain(false, $Section['ID']);
					while ($arSectionPath = $nav->GetNext())
					{
						$Section['SECTIONS_PATH'][$arSectionPath['DEPTH_LEVEL']] = $arSectionPath;
					}
				}

				$result[$Section['ID']] = $Section;
			}
			$cache->endDataCache($result);
		}

		$this->result['SECTIONS'] = $result;
	}

	private function GetIblockElements()
	{
		$result = false;
		$cache_params = array('function' => __FUNCTION__, 'IBLOCK_ID' => $this->CATALOG_IBLOCK,
			'EXCLUDE' => $this->EXCLUDE_SECTIONS);
		$cache_id = md5(serialize($cache_params));
		$cache_dir = __CLASS__;
		$cache = Cache::createInstance();

		$life_time = 60 * 60 * 24;

		if ($life_time < 0)
		{
			$cache->clean($cache_id, $cache_dir);
		}

		if ($cache->initCache($life_time, $cache_id, $cache_dir))
		{
			$result = $cache->getVars();
		} elseif ($cache->startDataCache() && \Bitrix\Main\Loader::includeModule('iblock'))
		{
			$arSelect = Array("ID", "NAME", "IBLOCK_ID", "CATALOG_TYPE", "IBLOCK_SECTION_ID", "DETAIL_PAGE_URL",
				"DETAIL_PICTURE", "PREVIEW_PICTURE", "DETAIL_TEXT");
			$arFilter = Array("IBLOCK_ID" => $this->CATALOG_IBLOCK, "ACTIVE_DATE" => "Y", "ACTIVE" => "Y",
				'!IBLOCK_SECTION_ID' => $this->EXCLUDE_SECTIONS, "CATALOG_TYPE" => array(1, 3));
			$res = \CIBlockElement::GetList(Array(), $arFilter, false, false, $arSelect);
			while ($ob = $res->GetNextElement())
			{
				$arFields = $ob->GetFields();
				/*Loader::includeModule('pai.tools');
				CDebug::pre($arFields,true);*/

				$arFields['CATALOG_QUANTITY'] = intval($arFields['CATALOG_QUANTITY']);
				$price = 0;

				if (intval($arFields['DETAIL_PICTURE']) <= 0 && intval($arFields['PREVIEW_PICTURE']) > 0)
				{
					$arFields['DETAIL_PICTURE'] = $arFields['PREVIEW_PICTURE'];
				}

				if ($arFields['CATALOG_TYPE'] == 3)
				{
					//  у товара есть торговые предложения - их нужно найти :(
					$arOfferSelect = Array("ID", "NAME", "IBLOCK_ID", "CATALOG_QUANTITY");
					if (intval($arFields['DETAIL_PICTURE']) <= 0 && intval($arFields['PREVIEW_PICTURE']) <= 0)
					{
						$arOfferSelect[] = 'DETAIL_PICTURE';
						$arOfferSelect[] = 'PREVIEW_PICTURE';
					}
					$arOfferFilter = Array("IBLOCK_ID" => $this->OFFER_IBLOCK, "PROPERTY_CML2_LINK" => $arFields['ID'],
 "ACTIVE_DATE" => "Y", "ACTIVE" => "Y");
					$resOffer = \CIBlockElement::GetList(Array(), $arOfferFilter, false, false, $arOfferSelect);
					while ($arOfferFields = $resOffer->GetNext())
					{
						$dbProductPrice = \CPrice::GetListEx(
							array(),
							array("PRODUCT_ID" => $arOfferFields['ID'], "CATALOG_GROUP_ID" => 1),
// берем товары с базовой ценой, с ID=1
							false,
							false,
							array("ID", "CATALOG_GROUP_ID", "PRICE", "CURRENCY")
						);
						while ($arPrice = $dbProductPrice->Fetch())
						{
							if (!isset($arFields['PRICE']) || floatval($arFields['PRICE']) > floatval($arPrice['PRICE']))
							{
								$arFields['PRICE'] = floatval($arPrice['PRICE']);
								$arFields['CURRENCY'] = $arPrice['CURRENCY'];
							}
						}

						$arFields['CATALOG_QUANTITY'] += intval($arOfferFields['CATALOG_QUANTITY']);

						if (intval($arFields['DETAIL_PICTURE']) <= 0 && (intval($arOfferFields['DETAIL_PICTURE']) > 0
								|| intval($arOfferFields['PREVIEW_PICTURE']) > 0))
						{
							$arFields['DETAIL_PICTURE'] = (intval($arOfferFields['DETAIL_PICTURE']) > 0)
								? $arOfferFields['DETAIL_PICTURE']
								: $arOfferFields['PREVIEW_PICTURE'];
						}

					}
				} else
				{
					// простой товар
					$dbProductPrice = \CPrice::GetListEx(
						array(),
						array("PRODUCT_ID" => $arFields['ID'], "CATALOG_GROUP_ID" => 1),
// берем товары с базовой ценой, с ID=1
						false,
						false,
						array("ID", "CATALOG_GROUP_ID", "PRICE", "CURRENCY")
					);
					while ($arPrice = $dbProductPrice->Fetch())
					{
						if (!isset($arFields['PRICE']) || floatval($arFields['PRICE']) > floatval($arPrice['PRICE']))
						{
							$arFields['PRICE'] = floatval($arPrice['PRICE']);
							$arFields['CURRENCY'] = $arPrice['CURRENCY'];
						}
					}
				}

				if (floatval($arFields['PRICE']) <= 0) continue;
				if (intval($arFields['DETAIL_PICTURE']) <= 0) continue;

				$arProperties = $ob->GetProperties();
				$arDisplay_properties = [];
                $MorePictures = [];

				foreach ($arProperties as $arProperty)
				{

                    if($arProperty['CODE']=='MORE_PHOTO' && !empty($arProperty['VALUE'])){
						foreach ($arProperty['VALUE'] as $fid){
							$MorePictures[] = \CFile::GetPath($fid);
						}
					}

					if (in_array($arProperty['CODE'], array(
						'MINIMUM_PRICE',
						'MAXIMUM_PRICE',
						'HIT',
						'POPUP_VIDEO',
						'PODBORKI',
						'PRODUCTION',
						'COLOR',
						'SPECIAL_OFFERS',
						'META_DESCRIPTION',
						'META_KEYWORDS',
						'MORE_PHOTO', // ???
						'COLOR_REF2',
						'SALE_TEXT',
						'vote_count',
						'vote_sum',
						'rating',
						'FORUM_TOPIC_ID',
						'FORUM_MESSAGE_CNT',
						'COLOR_VARIANTS',
						'PRODUCT_TYPE',
						'VIDEO_YOUTUBE',
						'EXPANDABLES',
						'ASSOCIATED',

					))) continue;

					$arDisplay_properties[$arProperty['CODE']] = \CIBlockFormatProperties::GetDisplayValue(
						array('ID' => $arFields['ID'], 'NAME' => $arFields['NAME']), $arProperty, '');
				}


				$row = array(
					'id' => $arFields['ID'],
					'available' => (intval($arFields['CATALOG_QUANTITY']) > 0) ? "true" : "false",
					'categoryId' => $arFields['IBLOCK_SECTION_ID'],
					'vendor' => 'Brand name',
					'name' => $arFields['NAME'],
					'url' => $arFields['DETAIL_PAGE_URL'],
					'price' => $arFields['PRICE'],
					'stock' => (intval($arFields['CATALOG_QUANTITY']) > 0) ? 'На складе' : "Нет на складе",
					'params' => [],
					'picture' => \CFile::GetPath($arFields['DETAIL_PICTURE']),
                    'more_pictures' => $MorePictures,
					'description' => ((strlen($arFields['DETAIL_TEXT']) > 0) ?
						'' . PHP_EOL : "")

				);

				foreach ($arDisplay_properties as $arProperty)
				{
					if (is_array($arProperty['DISPLAY_VALUE'])) $arProperty['DISPLAY_VALUE'] =
implode(', ', $arProperty['DISPLAY_VALUE']);
					if (strlen($arProperty['DISPLAY_VALUE']) > 0)
					{
						$row['params'][$arProperty['NAME']] = $arProperty['DISPLAY_VALUE'];
					}
				}

				$result[$arFields['ID']] = $row;

			}
			$cache->endDataCache($result);
		}

		$this->result['PRODUCTS'] = $result;
	}

	function GetFileSheme()
	{
		$Scheme = "<?xml version=\"1.0\" encoding=\"utf-8\"?>" . PHP_EOL;
		$Scheme .= "<price>" . PHP_EOL;
		$Scheme .= "\t<date>" . date(str_replace("_", " ", '"Y-m-dTh:i:s±h:i"')) . "</date>" . PHP_EOL;
		$Scheme .= "\t<firmName>" . $this->FirmName . "</firmName>" . PHP_EOL;
		$Scheme .= "\t<rate>1</rate>" . PHP_EOL;
		$Scheme .= "\t<url>" . $this->url . "</url>" . PHP_EOL;
		$Scheme .= "\t<url>" . $this->url . "</url>" . PHP_EOL;
		$Scheme .= "\t<categories>" . PHP_EOL;
		$Scheme .= "#CATEGORIES#" . PHP_EOL;
		$Scheme .= "\t</categories>" . PHP_EOL;
		$Scheme .= "\t<offers>" . PHP_EOL;
		$Scheme .= "#PRODUCTS#" . PHP_EOL;
		$Scheme .= "\t</offers>" . PHP_EOL;
		$Scheme .= "</price>";

		$this->Scheme = $Scheme;
	}

	function putSectionsToScheme()
	{
		$categories = '';
		foreach ($this->result['SECTIONS'] as $arSection)
		{
			$categories .= "\t\t<category id=\"" . $arSection['ID'] . "\" " .
				(
(intval($arSection['IBLOCK_SECTION_ID']) > 0)
? "parentId=\"" . $arSection['IBLOCK_SECTION_ID'] . "\" "
: "")
				. "portal_url=\"" . $arSection['SECTION_PAGE_URL'] . "\">" . $arSection["NAME"] . "</category>" . PHP_EOL;
		}

		$this->Scheme = str_replace('#CATEGORIES#', TrimEx($categories, PHP_EOL, 'right'), $this->Scheme);
	}

	function putProductsToScheme()
	{
		$productsLine = '';
		foreach ($this->result['PRODUCTS'] as $arProduct)
		{
			$productsLine .= "\t\t<offer id=\"" . $arProduct["id"] . "\" available=\""
. $arProduct["available"] . "\">" . PHP_EOL;
			$productsLine .= "\t\t\t<categoryId>" . $arProduct['categoryId'] . "</categoryId>" . PHP_EOL;
			$productsLine .= "\t\t\t<vendor>" . $arProduct['vendor'] . "</vendor>" . PHP_EOL;
			$productsLine .= "\t\t\t<name>" . $arProduct['name'] . "</name>" . PHP_EOL;
			$productsLine .= "\t\t\t<url>" . $this->url . $arProduct['url'] . "</url>" . PHP_EOL;
			$productsLine .= "\t\t\t<price>" . $arProduct['price'] . "</price>" . PHP_EOL;
			$productsLine .= "\t\t\t<stock>" . $arProduct['stock'] . "</stock>" . PHP_EOL;
			$productsLine .= "\t\t\t<picture>" . $this->url . $arProduct['picture'] . "</picture>" . PHP_EOL;
			$productsLine .= "\t\t\t<description>" . $arProduct['description'] . PHP_EOL . "</description>" . PHP_EOL;

			foreach ($arProduct['params'] as $param => $paramValue)
			{
				$productsLine .= "\t\t\t<param name=\"" . $param . "\">" . $paramValue . "</param>" . PHP_EOL;
			}
			$productsLine .= "\t\t</offer>" . PHP_EOL;
		}

		$this->Scheme = str_replace('#PRODUCTS#', TrimEx($productsLine, PHP_EOL, 'right'), $this->Scheme);
	}

	function putSchemeToFile()
	{
		$file = new IO\File(Application::getDocumentRoot() . "/upload/export/tmp.xml");
		$file->putContents($this->Scheme);
		$file->rename(Application::getDocumentRoot() . "/upload/export/siteCatalog.xml");
		echo 'Ok';
	}

	function process()
	{
		$this->GetFileSheme();
		$this->GetSectionsList();
		$this->putSectionsToScheme();
		$this->GetIblockElements();
		$this->putProductsToScheme();
		$this->putSchemeToFile();

	}

	static function Agent($IBLOCK_ID, $OFFERS_ID)
	{
		$processor = new CCatalogExport($IBLOCK_ID, $OFFERS_ID);
		$processor->process();
		return "\Pai\Tools\CCatalogExport::Agent($IBLOCK_ID,$OFFERS_ID);";

	}

}

Дальше, подключаем данный класс к автозагрузке:

Bitrix\Main\Loader::registerAutoLoadClasses(null, array(
	'Pai\Tools\CCatalogExport' => '/bitrix/php_interface/classes/ccatalogexport.php',
));

Проверяем работу класса запустив скрипт:

$processor = new Pai\Tools\CCatalogExport(,);
$processor->process();

И последним шагом - создаем периодический агент с выполнением функции:

\Pai\Tools\CCatalogExport::Agent($IBLOCK_ID,$OFFERS_ID);

Имейте ввиду, что данный вариант скрипта не подходит для больших каталогов, которые нельзя обработать за один хит. Если у вас большой каталог - стоит разбить его на части с помощью пагинации и информацию о товарах записывать в файл пошагово.

2018-12-27. Создание дополнительной выгрузки на базе основной.

Предположим, вы создали и настроили основную выгрузку, но вам нужна еще одна (а в реальной ситуации - не одна) с незначительными изменениями структуры. Для этого, нужно создать еще один класс с наследованинем от текущего.

Итак, создаем рядом с файлом с основным классом ее один, например, класс CYmlCatalogExport в файле cymlcatalogexport. В init.php добавляем автозагрузку нового класса:

Bitrix\Main\Loader::registerAutoLoadClasses(null, array(
	'Pai\Tools\CCatalogExport' => '/bitrix/php_interface/classes/ccatalogexport.php',
	'Pai\Tools\CYmlCatalogExport' => '/bitrix/php_interface/classes/cymlcatalogexport.php',
));

Сам класс примет такой вид:

namespace Pai\Tools;

use \Bitrix\Main\Data\Cache;
use Bitrix\Main\IO,
	Bitrix\Main\Application;
use Bitrix\Main\Loader;

class CYmlCatalogExport extends CCatalogExport
{
	function GetFileSheme()
	{
		$Scheme = "<?xml version=\"1.0\" encoding=\"utf-8\"?>" . PHP_EOL;
		$Scheme .= "<!DOCTYPE yml_catalog SYSTEM \"shops.dtd\" >".PHP_EOL;
		$Scheme .= "<yml_catalog date=\"".date('Y-m-d H:i')."\">" . PHP_EOL;
		$Scheme .= "<shop>".PHP_EOL;
		$Scheme .= "\t<name>" . $this->FirmName . "</name>" . PHP_EOL;
		$Scheme .= "\t<company>" . $this->FirmName . "</company>" . PHP_EOL;
		$Scheme .= "\t<url>" . $this->url . "</url>" . PHP_EOL;
		$Scheme .= "\t<platform>" . "1С-Битрикс" . "</platform>" . PHP_EOL;
		$Scheme .= "\t<currencies>".PHP_EOL;
		$Scheme .= "\t\t<currency id=\"RUR\" rate=\"1.0\" />".PHP_EOL;
		$Scheme .= "\t</currencies>".PHP_EOL;
		$Scheme .= "\t<categories>" . PHP_EOL;
		$Scheme .= "#CATEGORIES#" . PHP_EOL;
		$Scheme .= "\t</categories>" . PHP_EOL;
		$Scheme .= "\t<offers>" . PHP_EOL;
		$Scheme .= "#PRODUCTS#" . PHP_EOL;
		$Scheme .= "\t</offers>" . PHP_EOL;
		$Scheme .= "</shop>".PHP_EOL;
		$Scheme .= "</yml_catalog>";

		$this->Scheme = $Scheme;
	}

	function putSectionsToScheme()
	{
		$categories = '';
		foreach ($this->result['SECTIONS'] as $arSection)
		{
			$categories .= "\t\t<category id=\"" . $arSection['ID'] . "\" " .
				(
				(intval($arSection['IBLOCK_SECTION_ID']) > 0)
					? "parentId=\"" . $arSection['IBLOCK_SECTION_ID'] . "\" "
					: "")
				. "portal_url=\"" . $arSection['SECTION_PAGE_URL'] . "\">" . $arSection["NAME"] . "</category>" . PHP_EOL;
		}

		$this->Scheme = str_replace('#CATEGORIES#', TrimEx($categories, PHP_EOL, 'right'), $this->Scheme);
	}

	function putProductsToScheme()
	{
		$productsLine = '';
		foreach ($this->result['PRODUCTS'] as $arProduct)
		{
			$productsLine .= "\t\t<offer id=\"" . $arProduct["id"] . "\" available=\""
				. $arProduct["available"] . "\">" . PHP_EOL;
			$productsLine .= "\t\t\t<categoryId>" . $arProduct['categoryId'] . "</categoryId>" . PHP_EOL;
			$productsLine .= "\t\t\t<currencyId>" . $arProduct['currencyId'] . "</currencyId>" . PHP_EOL;
			$productsLine .= "\t\t\t<name>" . htmlspecialchars($arProduct['name']) . "</name>" . PHP_EOL;
			$productsLine .= "\t\t\t<url>" . $this->url . $arProduct['url'] . "</url>" . PHP_EOL;
			$productsLine .= "\t\t\t<price>" . $arProduct['price'] . "</price>" . PHP_EOL;
			$productsLine .= "\t\t\t<picture>" . $this->url . $arProduct['picture'] . "</picture>" . PHP_EOL;
            if(!empty($arProduct['more_pictures'])){
				$productsLine .= "\t\t\t<add_picture>" . implode(',',$arProduct['more_pictures'])."</add_picture>".PHP_EOL;
			}
			$productsLine .= "\t\t\t<description>" . $arProduct['description'] . PHP_EOL . "</description>" . PHP_EOL;
			$productsLine .= "\t\t\t<store>false</store>".PHP_EOL;
			$productsLine .= "\t\t\t<pickup>true</pickup>".PHP_EOL;
			$productsLine .= "\t\t\t<delivery>true</delivery>".PHP_EOL;

			foreach ($arProduct['params'] as $param => $paramValue)
			{
				$productsLine .= "\t\t\t<param name=\"" . $param . "\">" . $paramValue . "</param>" . PHP_EOL;
			}
			$productsLine .= "\t\t</offer>" . PHP_EOL;
		}

		$this->Scheme = str_replace('#PRODUCTS#', TrimEx($productsLine, PHP_EOL, 'right'), $this->Scheme);
	}

	function putSchemeToFile()
	{
		$file = new IO\File(Application::getDocumentRoot() . "/upload/export/tmp.xml");
		$file->putContents($this->Scheme);
		$file->rename(Application::getDocumentRoot() . "/upload/export/ymlCatalog.xml");
		echo 'Ok';
	}

	static function Agent($IBLOCK_ID, $OFFERS_ID)
	{
		$processor = new CYmlCatalogExport($IBLOCK_ID, $OFFERS_ID);
		$processor->process();
		return "\Pai\Tools\CYmlCatalogExport::Agent($IBLOCK_ID,$OFFERS_ID);";

	}
}

Вот так получаем вторую выгрузку на базе первой.

Количество показов: 4443
05.11.2018

Возврат к списку

Если вам была полезна статья можете отблагодарить автора:
Ethereum:

0x16Df809287333C49D3A237296C6248A6c08702Bc

Разработка сайта

Подайте заявку на разработку сайта на базе готового решения от компании 1С-Битрикс или одного из партнеров компании. Максимально подробно опишите, чему будет посвящен сайт, если это интернет-магазин - что он будет продавать, нужна ли мультиязычность, будут ли разные типы цен (розница, опт, крупный опт), будет ли интеграция с 1С, будет ли выгрузка товаров на различные торговые площадки...

Сопровождение сайта

Вы можете подать заявку на сопровождение вашего сайта на базе 1С-Битрикс. Сопровождение включает в себя: проверка актуальности обновлений сайта, проверка актуальности резервной копии, консультации по сайту. Опишите в заявке, какие еще объемы планируются на сопровождении и на какой срок вы планируете заключить договор на сопровождение - мы подберем подходящий вам бюджет на сопровождение

Работы по сайту

Вы можете подать заявку на выполнение определенного объема работ по сайту. Опишите в заявке объем работ. Это может быть разработка какого-то нового функционала, доработки по имеющемуся функционалу, доработки под требования сео-специалистов. На основании заявки вам будет сформирован бюджет работ, а также названы сроки на выполнение тех или иных работ.