Вводные
1. В Битрикс заведен каталог товаров без привязки к 1С. В 1С УТ 11 аналогичный каталог.
2. Типовая обработка не позволяет состыковать номенклатуру, результат её работы дубли либо со стороны 1С либо со стороны БУС.
3. Реализация обмена со стороны БУС "черный ящик", в который даже продвинутые программисты по битрикс не горят желанием лезть.
4. Реализация обмена со стороны 1С вызывает ряд вопросов (по логичности и по реализации).
5. Установка доп модуля https://1c.1c-bitrix.ru/ecommerce/download.php проблему не решает, усложняет обновление конфигурации.
Все вышесказанное вынес из собственного опыта взаимодействия при реализации обменов различными способами.
Вариант решения
Реализовать
- точку подключения
- аутентификацию
- роутер
- возможность подключения к классам битрикс
- проверки параметров и данных в запросах (тема отдельной статьи)
Настройка проекта
Есть опыт работы с symfony, взял его.
В ходе реализации получил ошибку "Case mismatch between loaded and declared class names: "CCatalogSKU" vs "CCatalogSku". В коде вызывается $catalogInfo = \CCatalogSKU::getInfoByProductIBlock($this->iblockId); хотя внутри class CCatalogSku extends CAllCatalogSku, что отражает не высокое качество кода БУС (как минимум отсутствие проверок).
Так же присутствуют ошибки по проверке доступности реквизитов (PSR-4).
Для обхода ошибок был собран symfony без errorhandler.
В папке /local БУС создал папку api_min, в ней создал проект (архив проекта будет в фйлах для скачивания)
Создал папку public, в ней index.php, это будет точкой входа в API
В админке БУС добавил правило обработки адреса, раздел "Рабочий стол - Настройки - Настройки продукта - Обработка адресов - Правила обработки"
- Условие #^/api_min/#
- Файл /local/api_min/public/index.php
Роутинг реализован через https://symfony.com/doc/current/create_framework/routing.html, пример для ping
Слеш в конце строки сделал для унификации, т.к. в некоторых настройках БУС автодобавление слеша.
$routes->add(
'ping',
(new Route( '/ping/'))
->addDefaults(['_controller' => [DefaultController::class, 'index']])
->setMethods(['GET'])
);
В маршрутах можно использовать regex, что удобно при работе и через id и через xml_id
Пример маршрута получения элемента
function AddRouteByReference($controllerName, $class)
{
$regexRequirements = ['id' => '\d+'];
$regexRequirementsXMLID = ['xml_id' => '[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}'];
$routes->add($controllerName . 'getById', (new Route("/{id}/"))
->addDefaults([
'_controller' => [$class, 'getById']
])
->setMethods(['GET'])
->setRequirements($regexRequirements)
);
$routes->add($controllerName . 'getByXMLId', (new Route("/{xml_id}/"))
->addDefaults([
'_controller' => [$class, 'getByXMLId']
])
->setMethods(['GET'])
->setRequirements($regexRequirementsXMLID)
);
}
Организация точки входа
В файле index.php описываем приложение, подключаем контроллеры с данными.
<?php
use RestApi\Helpers\Routing;
require(__DIR__ . '/../vendor/autoload.php');
require($_SERVER["DOCUMENT_ROOT"] . "/bitrix/modules/main/include/prolog_before.php");
$fileName = basename(__FILE__, '.php');
$routesDir = __DIR__ . '/../routes/';
Routing::buildRoutes($routesDir)->send();
1. Аутентификация по токену
1.1 Заголовок токена, его значения, инфоблоки для работы с товарами заданы в Helpers/ConfigAPI
Работа с товарами
Содержимое \Controllers\References\ProductController
class ProductController extends ReferenceIblockController implements ReferenceInterface
{
static function getIblockId()
{
$config = new ConfigAPI;
return $config->getCatalogBlockId();
}
static function getElementClass()
{
return \CIBlockElement::class;
}
static function getItemFields()
{
return [
'ID',
'XML_ID',
'NAME'
];
}
}
Основная логика описана в "ReferenceIblockController" и "ReferenceController"
Содержимое ReferenceIblockController
class ReferenceIblockController extends ReferenceController {
public static function getItemList(array $parameters , \DateTime $date_update_from)
{
$parameters['filter']['IBLOCK_ID'] = static::getIblockId();
return parent::getItemList($parameters , $date_update_from);
}
public static function updateItem(int $id, $arItem)
{
$arItem['IBLOCK_ID'] = static::getIblockId();
$el_class = static::getElementClass();
$el = new $el_class;
$res = new \Bitrix\Main\Result;
if ($el->update($id, $arItem)) {
$res->setData([]);
} else {
$res->addError([$el->LAST_ERROR]);
}
;
return $res;
}
Содержимое ReferenceController
class ReferenceController implements ReferenceHTTPInterface {
public static function getItemList(array $parameters, \DateTime $date_update_from)
{
return static::getClass()::getList($parameters)->fetchAll();
}
Для чтения списка товаров используется "Iblock\ElementTable", т.к. поддерживает limit/offset, для работы с элементами используется CIBlockElement, потому что в новом движке через ElementTable элемент не поменять.
Наследование использую для уменьшения количества кода, т.к. операции с элементами повторяются.
Для проверки параметров запроса и данных в теле запроса использую https://symfony.com/doc/current/validation.html, существенно удобнее чем вручную все проверять и писать сообщения. При ошибки проверки вызываю исключение BadRequestException в которое передаю массив ошибок.
Работа со справочником товары, реализация со стороны 1С
Процедура загрузки списка
Процедура ЗагрузитьДанныеНаСервере()
ТаблицаГруппыНоменклатуры.Очистить();
ТаблицаНоменклатура.Очистить();
Ответ = GET(АдресСайта, "/product_sections/");
Если Ответ.КодСостояния = 200 Тогда
СтрокаДанные = Ответ.ПолучитьТелоКакСтроку();
МассивОтвет = ИЗ_JSON(СтрокаДанные);
Для каждого СтрМ Из МассивОтвет Цикл
СтрГ = ТаблицаГруппыНоменклатуры.Добавить();
СтрГ.Идентификатор = СтрМ.Получить("id");
СтрГ.Идентификатор1С = СтрМ.Получить("xml_id");
СтрГ.ИдентификаторРодителя = СтрМ.Получить("section");
СтрГ.Наименование = СтрМ.Получить("name");
СтрГ.ВидНоменклатуры = ПолучитьСсылкуПоИдентификатору1С(СтрГ.Идентификатор1С, "ВидыНоменклатуры");
КонецЦикла;
КонецЕсли;
Ответ = GET(АдресСайта, "/products/");
Если Ответ.КодСостояния = 200 Тогда
СтрокаДанные = Ответ.ПолучитьТелоКакСтроку();
МассивОтвет = ИЗ_JSON(СтрокаДанные);
Для каждого СтрМ Из МассивОтвет Цикл
СтрН = ТаблицаНоменклатура.Добавить();
СтрН.Идентификатор = СтрМ.Получить("id");
СтрН.Идентификатор1С = СтрМ.Получить("xml_id");
СтрН.Наименование = СтрМ.Получить("name");
СтрН.Номенклатура = ПолучитьСсылкуПоИдентификатору1С(СтрН.Идентификатор1С, "Номенклатура");
КонецЦикла;
КонецЕсли;
КонецПроцедуры
Гружу без пагинации, т.к. каталог небольшой, текущего limit 1000 хватает.
Для обновления элемента используется код
Функция обновления xml_id
Функция ОбновитьНоменклатуруНаСервере(Номенклатура, Идентификатор)
Если ЗначениеЗаполнено(Номенклатура) Тогда
Идентификатор1С = Строка(Номенклатура.УникальныйИдентификатор());
СоответствиеДанные = Новый Соответствие;
СоответствиеДанные.Вставить("xml_id", Идентификатор1С);
СтрокаДанные = В_JSON(СоответствиеДанные);
Ответ = PUT(АдресСайта, "/products/" + Формат(Идентификатор, "ЧГ=0") + "/", СтрокаДанные);
Если Ответ.КодСостояния = 200 Тогда
Возврат Идентификатор1С;
КонецЕсли;
КонецЕсли;
Возврат Ложь;
КонецФункции
Итог
Реализация обмена через REST не сложна, гибка и структурирована, понятно на каком именно элементе произошла ошибка.
Сборка symfony нестандартная, если получится нормально настроить ErrorHandler, то будет чуть проще.
Благодарю за внимание.