Magento является e-commerce решением, т.е. больше нацелено на продажу продуктов, чем на сопутствующий продажам складской, логистический или финансовый учёт. Для сопутствующего лучше подходят другие приложения (например, ERP-системы). Поэтому достаточно часто в практике использования Magento возникает задача интеграции магазина с этими другими системами (например, с 1С).
По большому счёту интеграцию можно свести к репликации данных по:
- каталогу (продукты, категории);
- инвентарным данным (остатки продуктов на складах и цены);
- клиентам;
- заказам;
Magento для манипуляции с данными в базе предлагает отдельный класс объектов — репозитории. В силу специфики Magento добавление данных в базу через репозитории легко кодируется, но происходит, скажем так, небыстро. В данной публикации я рассматриваю основные этапы программного добавления в Magento 2 продукта "классическим" способом — с использованием репо-классов.
Клиенты и заказы реплицируются, обычно, в другую сторону — из Magento во внешние ERP-системы. Поэтому с ними попроще, на стороне Magento нужно просто выбрать соответствующие данные, а дальше — "с нашей стороны пули вылетели".
Принципы записи данных в базу
На данный момент создание сохраняемых в базе объектов программным способом в Magento делается через Factory:
function __construct (MagentoCmsModelBlockFactory $blockFactory) {
$this->blockFactory = $blockFactory;
}
/** @var MagentoCmsModelBlock $block */
$block = $this->blockFactory->create();
а запись в базу — через Repository:
function __construct (MagentoCmsApiBlockRepositoryInterface $blockRepo) {
$this->blockRepo = $blockRepo;
}
$this->blockRepo->save($block);
Подход с использованием "Factory" и "Repository" можно использовать для всех основных моделей в предметной области Magento 2.
Базовая информация о продукте
Я рассматриваю структуру данных, соответствующую версии Magento 2.3. Самая основная информация о продукте находится в таблице catalog_product_entity
(реестр продуктов):
entity_id
attribute_set_id
type_id
sku
has_options
required_options
created_at
updated_at
Ограничиваюсь одним типом продукта (type_id='simple'
), набором атрибутов по-умолчанию (attribute_set_id=4
) и игнорирую атрибуты has_options
и required_options
. Так как атрибуты entity_id
, created_at
и updated_at
генерируются автоматически, то, по сути дела, нам для добавления нового продукта достаточно задать sku
. Делаю так:
/** @var MagentoCatalogApiDataProductInterfaceFactory $factProd */
/** @var MagentoCatalogApiProductRepositoryInterface $repoProd */
/** @var MagentoCatalogApiDataProductInterface $prod */
$prod = $factProd->create();
$prod->setAttributeSetId(4);
$prod->setTypeId('simple');
$prod->setSku($sku);
$repoProd->save($prod);
и получаю исключение:
The "Product Name" attribute value is empty. Set the attribute and try again.
Добавляю в запрос имя продукта и получаю сообщение, что не хватает атрибута Price
. После добавления цены продукт ложится в базу:
$prod = $factProd->create();
$prod->setAttributeSetId(4);
$prod->setTypeId('simple');
$prod->setSku($sku);
$prod->setName($name);
$prod->setPrice($price);
$repoProd->save($prod);
Название продукта сохраняется в таблице varchar-атрибутов продукта (catalog_product_entity_varchar
), цена — в таблице catalog_product_entity_decimal
. Перед добавлением продукта желательно в явном виде указать, что мы используем административную витрину для импорта данных:
/** @var MagentoStoreModelStoreManagerInterface $manStore */
$manStore->setCurrentStore(0);
Дополнительные атрибуты
Обработка дополнительных атрибутов продуктов средствами Magento — одно удовольствие. EAV-модель данных для основных сущностей (см. таблицу eav_entity_type
) — одна из ключевых особенностей этой платформы. Просто добавляем соответствующие атрибуты к модели продукта:
$prodEntity->setData('description', $desc);
$prodEntity->setData('short_description', $desc_short);
// или
$prodEntity->setDescription($desc);
$prodEntity->setShortDescription($desc_short);
и при сохранении модели через репо-объект :
$repoProd->save($prod);
дополнительные атрибуты будут также сохранены в соответствующих таблицах БД.
Инвентарные данные
По-простому — количество продукта на складе. В Magento 2.3 структуры в БД, описывающие формат хранения инвентарных данных, значимо отличаются от того, что было ранее. Тем не менее, добавление количества продукта на складе через модель продукта не намного сложнее, чем добавление других атрибутов:
/** @var MagentoCatalogModelProduct $prodEntity */
/** @var MagentoCatalogApiProductRepositoryInterface $repoProd */
$inventory = [
'is_in_stock' => true,
'qty' => 1234
];
$prodEntity->setData('quantity_and_stock_status', $inventory);
$repoProd->save($prodEntity);
Медиа
Как правило, медиа-сопровождение продукта для клиента в магазине (e-commerce) отличается от медиа-сопровождения этого же продукта для сотрудника во внутренней системе учёта (ERP). В первом случае желательно показать "товар лицом", во втором — достаточно дать общее представление о продукте. Тем не менее, перенос хотя бы первичного изображения продукта — достаточно распространённый case
при импорте данных.
При добавлении изображения через админку картинка сначала сохраняется во временном каталоге (./pub/media/tmp/catalog/product
) и только при сохранении продукта перемещается в медиа-каталог (./pub/media/catalog/product
). Также при добавлении через админку изображению выставляются теги image
, small_image
, thumbnail
, swatch_image
.
/** @var MagentoCatalogApiProductRepositoryInterface $repoProd */
/** @var MagentoCatalogModelProductGalleryCreateHandler $hndlGalleryCreate */
/* $imagePath = '/path/to/file.png'; $imagePathRelative = '/f/i/file.png' */
$imagePathRelative = $this->imagePlaceToTmpMedia($imagePath);
/* reload product with gallery data */
$product = $repoProd->get($sku);
/* add image to product's gallery */
$gallery['images'][] = [
'file' => $imagePathRelative,
'media_type' => 'image'
'label' => ''
];
$product->setData('media_gallery', $gallery);
/* set usage areas */
$product->setData('image', $imagePathRelative);
$product->setData('small_image', $imagePathRelative);
$product->setData('thumbnail', $imagePathRelative);
$product->setData('swatch_image', $imagePathRelative);
/* create product's gallery */
$hndlGalleryCreate->execute($product);
Почему-то медиа подвязывается только после предварительного сохранения продукта и получения его из репозитория заново. И нужно указывать атрибут label
при добавлении записи в медиа-галерею продукта (иначе получаем исключение Undefined index: label in .../module-catalog/Model/Product/Gallery/CreateHandler.php on line 516
).
Категории
Зачастую структура категорий магазина и backend-приложения или размещение в них продуктов может значительно различаться. Стратегии переноса данных о категориях и продуктах в них зависят от множества факторов. В данном примере я придерживаюсь следующей:
- категории backend'а и магазина сопоставляются по названию;
- если импортируется категория, которой нет в магазине, то она создаётся под корневой категорией (
Default Category
) и её дальнейшее позиционирование в каталоге магазина предполагается вручную; - привязка продукта к категории происходит только при его создании в магазине (первом импорте);
Основная информация о категории находится в таблице catalog_category_entity
(каталог категорий). Создание категории в Magento:
/** @var MagentoCatalogApiDataCategoryInterfaceFactory $factCat */
/** @var MagentoCatalogApiCategoryRepositoryInterface $repoCat */
$cat = $factCat->create();
$cat->setName($name);
$cat->setIsActive(true);
$repoCat->save($cat);
Привязка продукта к категории осуществляется по ID категории и SKU продукта:
/** @var MagentoCatalogModelCategoryProductLinkFactory $factCatProdLink */
/** @var MagentoCatalogApiCategoryLinkRepositoryInterface $repoCatLink */
$link = $factCatProdLink->create();
$link->setCategoryId($catMageId);
$link->setSku($prodSku);
$repoCatLink->save($link);
Итого
Написать код для добавления в Magento 2 продукта программным путём весьма несложно. Всё изложенное выше я свёл в демо-модуль "flancer32/mage2_ext_demo_import". В модуле всего одна консольная команда fl32:import:prod
, которая импортирует продукты, описанные в JSON-файле "./etc/data/products.json":
[
{
"sku": "...",
"name": "...",
"desc": "...",
"desc_short": "...",
"price": ...,
"qty": ...,
"categories": ["..."],
"image_path": "..."
}
]
Картинки для импорта находятся в каталоге ./etc/data/img
.
Время импорта 10 продуктов подобным способом составляет порядка 10 секунд на моём ноутбуке. Если развивать эту мысль дальше, то несложно прийти к выводу, что в час можно импортировать порядка 3600 продуктов, а на импорт 100К продуктов может уйти порядка 30 часов. Замена ноутбука на сервер позволяет несколько сгладить ситуацию. Может быть даже в разы. Но не на порядки. Возможно эта скорость медленность в какой-то мере является одной из причин появления проекта magento/async-import.
Кардинальным решением для увеличения скорости импорта может стать прямая запись в базу, но в этом случае теряются все "плюшки", касающиеся расширяемости Magento — придётся всё "расширенное" делать самому. Тем не менее, оно того стоит. Если получится, то рассмотрю подход с прямой записью в БД в следующей статье.
Автор: flancer