Magento 2: импорт продуктов из внешних источников

    Magento является e-commerce решением, т.е. больше нацелено на продажу продуктов, чем на сопутствующий продажам складской, логистический или финансовый учёт. Для сопутствующего лучше подходят другие приложения (например, ERP-системы). Поэтому достаточно часто в практике использования Magento возникает задача интеграции магазина с этими другими системами (например, с 1С).


    По большому счёту интеграцию можно свести к репликации данных по:


    • каталогу (продукты, категории);
    • инвентарным данным (остатки продуктов на складах и цены);
    • клиентам;
    • заказам;

    Magento для манипуляции с данными в базе предлагает отдельный класс объектов — репозитории. В силу специфики Magento добавление данных в базу через репозитории легко кодируется, но происходит, скажем так, небыстро. В данной публикации я рассматриваю основные этапы программного добавления в Magento 2 продукта "классическим" способом — с использованием репо-классов.


    Клиенты и заказы реплицируются, обычно, в другую сторону — из Magento во внешние ERP-системы. Поэтому с ними попроще, на стороне Magento нужно просто выбрать соответствующие данные, а дальше — "с нашей стороны пули вылетели".


    Принципы записи данных в базу


    На данный момент создание сохраняемых в базе объектов программным способом в Magento делается через Factory:


    function __construct (\Magento\Cms\Model\BlockFactory $blockFactory) {
        $this->blockFactory = $blockFactory;
    }
    
    /** @var \Magento\Cms\Model\Block $block */
    $block = $this->blockFactory->create();

    а запись в базу — через Repository:


    function __construct (\Magento\Cms\Api\BlockRepositoryInterface $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 \Magento\Catalog\Api\Data\ProductInterfaceFactory $factProd */
    /** @var \Magento\Catalog\Api\ProductRepositoryInterface $repoProd */
    /** @var \Magento\Catalog\Api\Data\ProductInterface $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 \Magento\Store\Model\StoreManagerInterface $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 \Magento\Catalog\Model\Product $prodEntity */
    /** @var \Magento\Catalog\Api\ProductRepositoryInterface $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 \Magento\Catalog\Api\ProductRepositoryInterface $repoProd */
    /** @var \Magento\Catalog\Model\Product\Gallery\CreateHandler $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 \Magento\Catalog\Api\Data\CategoryInterfaceFactory $factCat */
    /** @var \Magento\Catalog\Api\CategoryRepositoryInterface $repoCat */
    $cat = $factCat->create();
    $cat->setName($name);
    $cat->setIsActive(true);
    $repoCat->save($cat);

    Привязка продукта к категории осуществляется по ID категории и SKU продукта:


    /** @var \Magento\Catalog\Model\CategoryProductLinkFactory $factCatProdLink */
    /** @var \Magento\Catalog\Api\CategoryLinkRepositoryInterface $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 — придётся всё "расширенное" делать самому. Тем не менее, оно того стоит. Если получится, то рассмотрю подход с прямой записью в БД в следующей статье.

    Похожие публикации

    AdBlock похитил этот баннер, но баннеры не зубы — отрастут

    Подробнее
    Реклама

    Комментарии 6

      0
      Метода, который вы описали для работы с инвентори
      /** @var \Magento\Catalog\Model\Product $prodEntity */
      /** @var \Magento\Catalog\Api\ProductRepositoryInterface $repoProd */
      $inventory = [
          'is_in_stock' => true,
          'qty' => 1234
      ];
      $prodEntity->setData('quantity_and_stock_status', $inventory);
      $repoProd->save($prodEntity);
      

      Является устаревшим в Magento 2.3.
      C появлением нового механизма инвентори (Magento MSI) данные инвентори отвязаны от сущности продукта и сохранять их предлагается через отдельные API эндпоинты:

      https://github.com/magento-engcom/msi/tree/2.3-develop/app/code/Magento/InventoryApi/Api

      Здесь можно почитать подробней о новом механизме инвентори — https://devdocs.magento.com/guides/v2.3/inventory/
        0

        Спасибо за дополнение. Ваш комментарий как раз и демонстрирует гибкость Magento. Старый подход позволяет просто указать кол-во продукта без необходимости разбираться в sources, stocks & sales channels. А сохранение данных происходит уже в MSI-структуру с использованием отдельных API endpoints (конкретно — \Magento\InventoryApi\Api\SourceItemsSaveInterface / \Magento\Inventory\Model\SourceItem\Command\SourceItemsSaveWithoutLegacySynchronization). Привязка "нового" подхода к "старому" происходит в \Magento\CatalogInventory\Observer\SaveInventoryDataObserver(метод execute). Вернее, в этом месте инвентарные данные отвязываются от продукта (они отвязывались и до версии 2.3), а непосредственно новые API вызовы подключаются в \Magento\InventoryCatalog\Plugin\CatalogInventory\UpdateSourceItemAtLegacyStockItemSavePlugin.


        Здесь использованы сразу два основных механизма обеспечения гибкости в Magento — обсерверы и плагины.

        0
        Привет,
        У использования абстракции для импорта данных есть один минус — скорость, часто этот минус больше плюса по простоте реализации.
        Время импорта 10 продуктов подобным способом составляет порядка 10 секунд на моём ноутбуке.

        Импорт 100k+, 150k+ товаров превращается в кошмар и ужас при каждом обновлении.
        И еще если кеш очистится из-за какого-то чудо-кода модуля и начинает тормозить фронт…

        Самый надежный способ на практике:
        1. Загрузка данных в транзитную transit_flat табличку тупеньким скриптиком.
        2. Создаем товар создав строчки в «catalog_product_entity», «catalog_product_entity_int» для атрибутов visibility, status, tax_class.
        3. catalog_category_product для привязки к категории, catalog_product_website для привязки к магазину.
        4. И для каждого нужного атрибута:
        Insert into catalog_product_entity{_int|_varchar|_text} from… transit.field_x as value select transit_flat transit inner join catalog_product_entity as cpe ON… on duplicate update…
        5. Для склада все просто: все всегда лежит в «cataloginventory_stock_item», можно «cataloginventory_stock_status» обновить сразу и не ждать индексатора.

        Описанный выше метод работает быстро так как все операции происходят в памяти базы данных mysql: search using index — O(log(N)), full scan — O(N), insert — O(1).
        10-20 минут для каталога, который некоторые считают огромным, хватает.

        Прошу прощения, что так сжато.
        Надеюсь найдется время договориться по NDA, подготовить «консерву» быстрого импорта.

          0
          Вы описали не самый идеальный способ импорта, т.к.
          1) Сохраняет по одному продукту, при этом все будет в транзакциях. Фронт при импорте просто упадет.
          2) Оперирует большим колличестовом моделейя, используется $product = $repoProd->get($sku); //загрузит все eav для одной записи
          3) У Вас при сохранении начнет отрабатывать индексатор Eav, stock итд. Первые продукты импортируются быстро, а потом все подвиснит.

          Для быстрого импорта, можно воспользоваться механизмом импорта из csv.

          Пример импорта csv
          $file - название csv
          $path - директория, где лежит csv.
              /**
                   * @var $import  \Magento\ImportExport\Model\Import
                   */
          $import = $this->importFactory->create();
                  $import->setData(
                      [
                          'entity'              => 'catalog_product',
                          'behavior'            => 'append',
                          'validation_strategy' => \Magento\ImportExport\Model\Import\ErrorProcessing\ProcessingErrorAggregatorInterface::VALIDATION_STRATEGY_SKIP_ERRORS,
                          'allowed_error_count' => 100
                      ]
                  );
          
                  $read_file = $this->readFactory->create($path);
                  $csvSource = $this->csvSourceFactory->create(
                      [
                          'file'      => $file,
                          'directory' => $read_file,
                      ]
                  );
          
                  $validate = $import->validateSource($csvSource);
          
                  if ($validate) {
                      try {
                          $result = $import->importSource();
                      } catch (\Throwable $e) {
                          $result = false;
                      }
          ....
          



          Минусы тоже есть: по умолчанию, если поле пустое, то оно не «очищается», а пропускается. Но все лечится напильником.
            0
            Какой конфиг у вашего ноутбука?
              0

              Только полноправные пользователи могут оставлять комментарии. Войдите, пожалуйста.

              Самое читаемое