Created
September 4, 2019 22:02
-
-
Save kevinvuillemin/802cc5e4b476e73c1c5838b143bd6a51 to your computer and use it in GitHub Desktop.
vendor/magento/module-catalog-import-export/Model/Import/Product.php
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
<?php | |
/** | |
* Copyright © Magento, Inc. All rights reserved. | |
* See COPYING.txt for license details. | |
*/ | |
namespace Magento\CatalogImportExport\Model\Import; | |
use Magento\Catalog\Api\ProductRepositoryInterface; | |
use Magento\Catalog\Model\Config as CatalogConfig; | |
use Magento\Catalog\Model\Product\Visibility; | |
use Magento\CatalogImportExport\Model\Import\Product\ImageTypeProcessor; | |
use Magento\CatalogImportExport\Model\Import\Product\MediaGalleryProcessor; | |
use Magento\CatalogImportExport\Model\Import\Product\RowValidatorInterface as ValidatorInterface; | |
use Magento\CatalogImportExport\Model\StockItemImporterInterface; | |
use Magento\CatalogInventory\Api\Data\StockItemInterface; | |
use Magento\Framework\App\Filesystem\DirectoryList; | |
use Magento\Framework\App\ObjectManager; | |
use Magento\Framework\Exception\LocalizedException; | |
use Magento\Framework\Exception\NoSuchEntityException; | |
use Magento\Framework\Filesystem; | |
use Magento\Framework\Intl\DateTimeFactory; | |
use Magento\Framework\Model\ResourceModel\Db\ObjectRelationProcessor; | |
use Magento\Framework\Model\ResourceModel\Db\TransactionManagerInterface; | |
use Magento\Framework\Stdlib\DateTime; | |
use Magento\ImportExport\Model\Import; | |
use Magento\ImportExport\Model\Import\Entity\AbstractEntity; | |
use Magento\ImportExport\Model\Import\ErrorProcessing\ProcessingError; | |
use Magento\ImportExport\Model\Import\ErrorProcessing\ProcessingErrorAggregatorInterface; | |
use Magento\Store\Model\Store; | |
/** | |
* Import entity product model | |
* | |
* @api | |
* | |
* @SuppressWarnings(PHPMD.TooManyFields) | |
* @SuppressWarnings(PHPMD.ExcessiveClassComplexity) | |
* @SuppressWarnings(PHPMD.CouplingBetweenObjects) | |
* @since 100.0.2 | |
*/ | |
class Product extends \Magento\ImportExport\Model\Import\Entity\AbstractEntity | |
{ | |
const CONFIG_KEY_PRODUCT_TYPES = 'global/importexport/import_product_types'; | |
/** | |
* Size of bunch - part of products to save in one step. | |
*/ | |
const BUNCH_SIZE = 20; | |
/** | |
* Size of bunch to delete attributes of products in one step. | |
*/ | |
const ATTRIBUTE_DELETE_BUNCH = 1000; | |
/** | |
* Pseudo multi line separator in one cell. | |
* | |
* Can be used as custom option value delimiter or in configurable fields cells. | |
*/ | |
const PSEUDO_MULTI_LINE_SEPARATOR = '|'; | |
/** | |
* Symbol between Name and Value between Pairs. | |
*/ | |
const PAIR_NAME_VALUE_SEPARATOR = '='; | |
/** | |
* Value that means all entities (e.g. websites, groups etc.) | |
*/ | |
const VALUE_ALL = 'all'; | |
/** | |
* Data row scopes. | |
*/ | |
const SCOPE_DEFAULT = 1; | |
const SCOPE_WEBSITE = 2; | |
const SCOPE_STORE = 0; | |
const SCOPE_NULL = -1; | |
/** | |
* Permanent column names. | |
* | |
* Names that begins with underscore is not an attribute. This name convention is for | |
* to avoid interference with same attribute name. | |
*/ | |
/** | |
* Column product store. | |
*/ | |
const COL_STORE = '_store'; | |
/** | |
* Column product store view code. | |
*/ | |
const COL_STORE_VIEW_CODE = 'store_view_code'; | |
/** | |
* Column website. | |
*/ | |
const COL_WEBSITE = 'website_code'; | |
/** | |
* Column product attribute set. | |
*/ | |
const COL_ATTR_SET = '_attribute_set'; | |
/** | |
* Column product type. | |
*/ | |
const COL_TYPE = 'product_type'; | |
/** | |
* Column product category. | |
*/ | |
const COL_CATEGORY = 'categories'; | |
/** | |
* Column product visibility. | |
*/ | |
const COL_VISIBILITY = 'visibility'; | |
/** | |
* Column product sku. | |
*/ | |
const COL_SKU = 'sku'; | |
/** | |
* Column product name. | |
*/ | |
const COL_NAME = 'name'; | |
/** | |
* Column product website. | |
*/ | |
const COL_PRODUCT_WEBSITES = '_product_websites'; | |
/** | |
* Media gallery attribute code. | |
*/ | |
const MEDIA_GALLERY_ATTRIBUTE_CODE = 'media_gallery'; | |
/** | |
* Column media image. | |
*/ | |
const COL_MEDIA_IMAGE = '_media_image'; | |
/** | |
* Inventory use config. | |
*/ | |
const INVENTORY_USE_CONFIG = 'Use Config'; | |
/** | |
* Inventory use config prefix. | |
*/ | |
const INVENTORY_USE_CONFIG_PREFIX = 'use_config_'; | |
/** | |
* Url key attribute code | |
*/ | |
const URL_KEY = 'url_key'; | |
/** | |
* Attribute cache | |
* | |
* @var array | |
*/ | |
protected $_attributeCache = []; | |
/** | |
* Pairs of attribute set ID-to-name. | |
* | |
* @var array | |
*/ | |
protected $_attrSetIdToName = []; | |
/** | |
* Pairs of attribute set name-to-ID. | |
* | |
* @var array | |
*/ | |
protected $_attrSetNameToId = []; | |
/** | |
* @var string | |
* @since 100.0.4 | |
*/ | |
protected $mediaGalleryTableName; | |
/** | |
* @var string | |
* @since 100.0.4 | |
*/ | |
protected $mediaGalleryValueTableName; | |
/** | |
* @var string | |
* @since 100.0.4 | |
*/ | |
protected $mediaGalleryEntityToValueTableName; | |
/** | |
* @var string | |
* @since 100.0.4 | |
*/ | |
protected $productEntityTableName; | |
/** | |
* Attributes with index (not label) value. | |
* | |
* @var string[] | |
*/ | |
protected $_indexValueAttributes = [ | |
'status', | |
'tax_class_id', | |
]; | |
/** | |
* Links attribute name-to-link type ID. | |
* | |
* @var array | |
*/ | |
protected $_linkNameToId = [ | |
'_related_' => \Magento\Catalog\Model\Product\Link::LINK_TYPE_RELATED, | |
'_crosssell_' => \Magento\Catalog\Model\Product\Link::LINK_TYPE_CROSSSELL, | |
'_upsell_' => \Magento\Catalog\Model\Product\Link::LINK_TYPE_UPSELL, | |
]; | |
/** | |
* Attributes codes which shows as date | |
* | |
* @var array | |
* @since 100.1.2 | |
*/ | |
protected $dateAttrCodes = [ | |
'special_from_date', | |
'special_to_date', | |
'news_from_date', | |
'news_to_date', | |
'custom_design_from', | |
'custom_design_to' | |
]; | |
/** | |
* Need to log in import history | |
* | |
* @var bool | |
*/ | |
protected $logInHistory = true; | |
/** | |
* Attribute id for product images storage. | |
* | |
* @var array | |
*/ | |
protected $_mediaGalleryAttributeId = null; | |
/** | |
* Validation failure message template definitions | |
* | |
* @var array | |
* @codingStandardsIgnoreStart | |
*/ | |
protected $_messageTemplates = [ | |
ValidatorInterface::ERROR_INVALID_SCOPE => 'Invalid value in Scope column', | |
ValidatorInterface::ERROR_INVALID_WEBSITE => 'Invalid value in Website column (website does not exist?)', | |
ValidatorInterface::ERROR_INVALID_STORE => 'Invalid value in Store column (store doesn\'t exist?)', | |
ValidatorInterface::ERROR_INVALID_ATTR_SET => 'Invalid value for Attribute Set column (set doesn\'t exist?)', | |
ValidatorInterface::ERROR_INVALID_TYPE => 'Product Type is invalid or not supported', | |
ValidatorInterface::ERROR_INVALID_CATEGORY => 'Category does not exist', | |
ValidatorInterface::ERROR_VALUE_IS_REQUIRED => 'Please make sure attribute "%s" is not empty.', | |
ValidatorInterface::ERROR_TYPE_CHANGED => 'Trying to change type of existing products', | |
ValidatorInterface::ERROR_SKU_IS_EMPTY => 'SKU is empty', | |
ValidatorInterface::ERROR_NO_DEFAULT_ROW => 'Default values row does not exist', | |
ValidatorInterface::ERROR_CHANGE_TYPE => 'Product type change is not allowed', | |
ValidatorInterface::ERROR_DUPLICATE_SCOPE => 'Duplicate scope', | |
ValidatorInterface::ERROR_DUPLICATE_SKU => 'Duplicate SKU', | |
ValidatorInterface::ERROR_CHANGE_ATTR_SET => 'Attribute set change is not allowed', | |
ValidatorInterface::ERROR_TYPE_UNSUPPORTED => 'Product type is not supported', | |
ValidatorInterface::ERROR_ROW_IS_ORPHAN => 'Orphan rows that will be skipped due default row errors', | |
ValidatorInterface::ERROR_INVALID_TIER_PRICE_QTY => 'Tier Price data price or quantity value is invalid', | |
ValidatorInterface::ERROR_INVALID_TIER_PRICE_SITE => 'Tier Price data website is invalid', | |
ValidatorInterface::ERROR_INVALID_TIER_PRICE_GROUP => 'Tier Price customer group ID is invalid', | |
ValidatorInterface::ERROR_TIER_DATA_INCOMPLETE => 'Tier Price data is incomplete', | |
ValidatorInterface::ERROR_SKU_NOT_FOUND_FOR_DELETE => 'Product with specified SKU not found', | |
ValidatorInterface::ERROR_SUPER_PRODUCTS_SKU_NOT_FOUND => 'Product with specified super products SKU not found', | |
ValidatorInterface::ERROR_MEDIA_DATA_INCOMPLETE => 'Media data is incomplete', | |
ValidatorInterface::ERROR_EXCEEDED_MAX_LENGTH => 'Attribute %s exceeded max length', | |
ValidatorInterface::ERROR_INVALID_ATTRIBUTE_TYPE => 'Value for \'%s\' attribute contains incorrect value', | |
ValidatorInterface::ERROR_ABSENT_REQUIRED_ATTRIBUTE => 'Attribute %s is required', | |
ValidatorInterface::ERROR_INVALID_ATTRIBUTE_OPTION => 'Value for \'%s\' attribute contains incorrect value, see acceptable values on settings specified for Admin', | |
ValidatorInterface::ERROR_DUPLICATE_UNIQUE_ATTRIBUTE => 'Duplicated unique attribute', | |
ValidatorInterface::ERROR_INVALID_VARIATIONS_CUSTOM_OPTIONS => 'Value for \'%s\' sub attribute in \'%s\' attribute contains incorrect value, acceptable values are: \'dropdown\', \'checkbox\', \'radio\', \'text\'', | |
ValidatorInterface::ERROR_INVALID_MEDIA_URL_OR_PATH => 'Wrong URL/path used for attribute %s', | |
ValidatorInterface::ERROR_MEDIA_PATH_NOT_ACCESSIBLE => 'Imported resource (image) does not exist in the local media storage', | |
ValidatorInterface::ERROR_MEDIA_URL_NOT_ACCESSIBLE => 'Imported resource (image) could not be downloaded from external resource due to timeout or access permissions', | |
ValidatorInterface::ERROR_INVALID_WEIGHT => 'Product weight is invalid', | |
ValidatorInterface::ERROR_DUPLICATE_URL_KEY => 'Url key: \'%s\' was already generated for an item with the SKU: \'%s\'. You need to specify the unique URL key manually', | |
ValidatorInterface::ERROR_DUPLICATE_MULTISELECT_VALUES => 'Value for multiselect attribute %s contains duplicated values', | |
'invalidNewToDateValue' => 'Make sure new_to_date is later than or the same as new_from_date', | |
]; | |
//@codingStandardsIgnoreEnd | |
/** | |
* Map between import file fields and system fields/attributes. | |
* | |
* @var array | |
*/ | |
protected $_fieldsMap = [ | |
'image' => 'base_image', | |
'image_label' => "base_image_label", | |
'thumbnail' => 'thumbnail_image', | |
'thumbnail_label' => 'thumbnail_image_label', | |
self::COL_MEDIA_IMAGE => 'additional_images', | |
'_media_image_label' => 'additional_image_labels', | |
'_media_is_disabled' => 'hide_from_product_page', | |
Product::COL_STORE => 'store_view_code', | |
Product::COL_ATTR_SET => 'attribute_set_code', | |
Product::COL_TYPE => 'product_type', | |
Product::COL_PRODUCT_WEBSITES => 'product_websites', | |
'status' => 'product_online', | |
'news_from_date' => 'new_from_date', | |
'news_to_date' => 'new_to_date', | |
'options_container' => 'display_product_options_in', | |
'minimal_price' => 'map_price', | |
'msrp' => 'msrp_price', | |
'msrp_enabled' => 'map_enabled', | |
'special_from_date' => 'special_price_from_date', | |
'special_to_date' => 'special_price_to_date', | |
'min_qty' => 'out_of_stock_qty', | |
'backorders' => 'allow_backorders', | |
'min_sale_qty' => 'min_cart_qty', | |
'max_sale_qty' => 'max_cart_qty', | |
'notify_stock_qty' => 'notify_on_stock_below', | |
'_related_sku' => 'related_skus', | |
'_related_position' => 'related_position', | |
'_crosssell_sku' => 'crosssell_skus', | |
'_crosssell_position' => 'crosssell_position', | |
'_upsell_sku' => 'upsell_skus', | |
'_upsell_position' => 'upsell_position', | |
'meta_keyword' => 'meta_keywords', | |
]; | |
/** | |
* Existing products SKU-related information in form of array: | |
* | |
* [SKU] => array( | |
* 'type_id' => (string) product type | |
* 'attr_set_id' => (int) product attribute set ID | |
* 'entity_id' => (int) product ID | |
* 'supported_type' => (boolean) is product type supported by current version of import module | |
* ) | |
* | |
* @var array | |
*/ | |
protected $_oldSku = []; | |
/** | |
* Column names that holds values with particular meaning. | |
* | |
* @var string[] | |
*/ | |
protected $_specialAttributes = [ | |
self::COL_STORE, | |
self::COL_ATTR_SET, | |
self::COL_TYPE, | |
self::COL_CATEGORY, | |
'_product_websites', | |
self::COL_PRODUCT_WEBSITES, | |
'_tier_price_website', | |
'_tier_price_customer_group', | |
'_tier_price_qty', | |
'_tier_price_price', | |
'_related_sku', | |
'_related_position', | |
'_crosssell_sku', | |
'_crosssell_position', | |
'_upsell_sku', | |
'_upsell_position', | |
'_custom_option_store', | |
'_custom_option_type', | |
'_custom_option_title', | |
'_custom_option_is_required', | |
'_custom_option_price', | |
'_custom_option_sku', | |
'_custom_option_max_characters', | |
'_custom_option_sort_order', | |
'_custom_option_file_extension', | |
'_custom_option_image_size_x', | |
'_custom_option_image_size_y', | |
'_custom_option_row_title', | |
'_custom_option_row_price', | |
'_custom_option_row_sku', | |
'_custom_option_row_sort', | |
'_media_attribute_id', | |
self::COL_MEDIA_IMAGE, | |
'_media_label', | |
'_media_position', | |
'_media_is_disabled', | |
]; | |
/** | |
* @var array | |
*/ | |
protected $defaultStockData = [ | |
'manage_stock' => 1, | |
'use_config_manage_stock' => 1, | |
'qty' => 0, | |
'min_qty' => 0, | |
'use_config_min_qty' => 1, | |
'min_sale_qty' => 1, | |
'use_config_min_sale_qty' => 1, | |
'max_sale_qty' => 10000, | |
'use_config_max_sale_qty' => 1, | |
'is_qty_decimal' => 0, | |
'backorders' => 0, | |
'use_config_backorders' => 1, | |
'notify_stock_qty' => 1, | |
'use_config_notify_stock_qty' => 1, | |
'enable_qty_increments' => 0, | |
'use_config_enable_qty_inc' => 1, | |
'qty_increments' => 0, | |
'use_config_qty_increments' => 1, | |
'is_in_stock' => 1, | |
'low_stock_date' => null, | |
'stock_status_changed_auto' => 0, | |
'is_decimal_divided' => 0, | |
]; | |
/** | |
* Column names that holds images files names | |
* | |
* Note: the order of array items has a value in order to properly set 'position' value | |
* of media gallery items. | |
* | |
* @var string[] | |
*/ | |
protected $_imagesArrayKeys = []; | |
/** | |
* Permanent entity columns. | |
* | |
* @var string[] | |
*/ | |
protected $_permanentAttributes = [self::COL_SKU]; | |
/** | |
* Array of supported product types as keys with appropriate model object as value. | |
* | |
* @var \Magento\CatalogImportExport\Model\Import\Product\Type\AbstractType[] | |
*/ | |
protected $_productTypeModels = []; | |
/** | |
* Media files uploader | |
* | |
* @var \Magento\CatalogImportExport\Model\Import\Uploader | |
*/ | |
protected $_fileUploader; | |
/** | |
* Import entity which provide import of product custom options | |
* | |
* @var \Magento\CatalogImportExport\Model\Import\Product\Option | |
*/ | |
protected $_optionEntity; | |
/** | |
* Catalog data | |
* | |
* @var \Magento\Catalog\Helper\Data | |
*/ | |
protected $_catalogData = null; | |
/** | |
* @var \Magento\CatalogInventory\Api\StockRegistryInterface | |
*/ | |
protected $stockRegistry; | |
/** | |
* @var \Magento\CatalogInventory\Api\StockConfigurationInterface | |
*/ | |
protected $stockConfiguration; | |
/** | |
* @var \Magento\CatalogInventory\Model\Spi\StockStateProviderInterface | |
*/ | |
protected $stockStateProvider; | |
/** | |
* Core event manager proxy | |
* | |
* @var \Magento\Framework\Event\ManagerInterface | |
*/ | |
protected $_eventManager = null; | |
/** | |
* @var \Magento\ImportExport\Model\Import\Config | |
*/ | |
protected $_importConfig; | |
/** | |
* @var \Magento\CatalogImportExport\Model\Import\Proxy\Product\ResourceModelFactory | |
*/ | |
protected $_resourceFactory; | |
/** | |
* @var \Magento\CatalogImportExport\Model\Import\Proxy\Product\ResourceModel | |
*/ | |
protected $_resource; | |
/** | |
* @var \Magento\Eav\Model\ResourceModel\Entity\Attribute\Set\CollectionFactory | |
*/ | |
protected $_setColFactory; | |
/** | |
* @var \Magento\CatalogImportExport\Model\Import\Product\Type\Factory | |
*/ | |
protected $_productTypeFactory; | |
/** | |
* @var \Magento\Catalog\Model\ResourceModel\Product\LinkFactory | |
*/ | |
protected $_linkFactory; | |
/** | |
* @var \Magento\CatalogImportExport\Model\Import\Proxy\ProductFactory | |
*/ | |
protected $_proxyProdFactory; | |
/** | |
* @var \Magento\CatalogImportExport\Model\Import\UploaderFactory | |
*/ | |
protected $_uploaderFactory; | |
/** | |
* @var \Magento\Framework\Filesystem\Directory\WriteInterface | |
*/ | |
protected $_mediaDirectory; | |
/** | |
* @var \Magento\CatalogInventory\Model\ResourceModel\Stock\ItemFactory | |
* @deprecated this variable isn't used anymore. | |
*/ | |
protected $_stockResItemFac; | |
/** | |
* @var \Magento\Framework\Stdlib\DateTime\TimezoneInterface | |
*/ | |
protected $_localeDate; | |
/** | |
* @var DateTime | |
*/ | |
protected $dateTime; | |
/** | |
* @var \Magento\Framework\Indexer\IndexerRegistry | |
*/ | |
protected $indexerRegistry; | |
/** | |
* @var Product\StoreResolver | |
*/ | |
protected $storeResolver; | |
/** | |
* @var Product\SkuProcessor | |
*/ | |
protected $skuProcessor; | |
/** | |
* @var Product\CategoryProcessor | |
*/ | |
protected $categoryProcessor; | |
/** | |
* @var \Magento\Framework\App\Config\ScopeConfigInterface | |
* @since 100.0.3 | |
*/ | |
protected $scopeConfig; | |
/** | |
* @var \Magento\Catalog\Model\Product\Url | |
* @since 100.0.3 | |
*/ | |
protected $productUrl; | |
/** | |
* @var array | |
*/ | |
protected $websitesCache = []; | |
/** | |
* @var array | |
*/ | |
protected $categoriesCache = []; | |
/** | |
* @var array | |
* @since 100.0.3 | |
*/ | |
protected $productUrlSuffix = []; | |
/** | |
* @var array | |
* @deprecated 100.1.5 | |
* @since 100.0.3 | |
*/ | |
protected $productUrlKeys = []; | |
/** | |
* Instance of product tax class processor. | |
* | |
* @var Product\TaxClassProcessor | |
*/ | |
protected $taxClassProcessor; | |
/** | |
* @var Product\Validator | |
*/ | |
protected $validator; | |
/** | |
* Array of validated rows. | |
* | |
* @var array | |
*/ | |
protected $validatedRows; | |
/** | |
* @var \Psr\Log\LoggerInterface | |
*/ | |
private $_logger; | |
/** | |
* {@inheritdoc} | |
*/ | |
protected $masterAttributeCode = 'sku'; | |
/** | |
* @var ObjectRelationProcessor | |
*/ | |
protected $objectRelationProcessor; | |
/** | |
* @var TransactionManagerInterface | |
*/ | |
protected $transactionManager; | |
/** | |
* Flag for replace operation. | |
* | |
* @var null | |
*/ | |
protected $_replaceFlag = null; | |
/** | |
* Flag for replace operation. | |
* | |
* @var null | |
*/ | |
protected $cachedImages = null; | |
/** | |
* @var array | |
* @since 100.0.3 | |
*/ | |
protected $urlKeys = []; | |
/** | |
* @var array | |
* @since 100.0.3 | |
*/ | |
protected $rowNumbers = []; | |
/** | |
* Product entity link field | |
* | |
* @var string | |
*/ | |
private $productEntityLinkField; | |
/** | |
* Product entity identifier field | |
* | |
* @var string | |
*/ | |
private $productEntityIdentifierField; | |
/** | |
* Escaped separator value for regular expression. | |
* The value is based on PSEUDO_MULTI_LINE_SEPARATOR constant. | |
* @var string | |
*/ | |
private $multiLineSeparatorForRegexp; | |
/** | |
* Container for filesystem object. | |
* | |
* @var Filesystem | |
*/ | |
private $filesystem; | |
/** | |
* Catalog config. | |
* | |
* @var CatalogConfig | |
*/ | |
private $catalogConfig; | |
/** | |
* Stock Item Importer | |
* | |
* @var StockItemImporterInterface | |
*/ | |
private $stockItemImporter; | |
/** | |
* @var ImageTypeProcessor | |
*/ | |
private $imageTypeProcessor; | |
/** | |
* Provide ability to process and save images during import. | |
* | |
* @var MediaGalleryProcessor | |
*/ | |
private $mediaProcessor; | |
/** | |
* @var DateTimeFactory | |
*/ | |
private $dateTimeFactory; | |
/** | |
* @var ProductRepositoryInterface | |
*/ | |
private $productRepository; | |
/** | |
* @param \Magento\Framework\Json\Helper\Data $jsonHelper | |
* @param \Magento\ImportExport\Helper\Data $importExportData | |
* @param \Magento\ImportExport\Model\ResourceModel\Import\Data $importData | |
* @param \Magento\Eav\Model\Config $config | |
* @param \Magento\Framework\App\ResourceConnection $resource | |
* @param \Magento\ImportExport\Model\ResourceModel\Helper $resourceHelper | |
* @param \Magento\Framework\Stdlib\StringUtils $string | |
* @param ProcessingErrorAggregatorInterface $errorAggregator | |
* @param \Magento\Framework\Event\ManagerInterface $eventManager | |
* @param \Magento\CatalogInventory\Api\StockRegistryInterface $stockRegistry | |
* @param \Magento\CatalogInventory\Api\StockConfigurationInterface $stockConfiguration | |
* @param \Magento\CatalogInventory\Model\Spi\StockStateProviderInterface $stockStateProvider | |
* @param \Magento\Catalog\Helper\Data $catalogData | |
* @param \Magento\ImportExport\Model\Import\Config $importConfig | |
* @param Proxy\Product\ResourceModelFactory $resourceFactory | |
* @param Product\OptionFactory $optionFactory | |
* @param \Magento\Eav\Model\ResourceModel\Entity\Attribute\Set\CollectionFactory $setColFactory | |
* @param Product\Type\Factory $productTypeFactory | |
* @param \Magento\Catalog\Model\ResourceModel\Product\LinkFactory $linkFactory | |
* @param Proxy\ProductFactory $proxyProdFactory | |
* @param UploaderFactory $uploaderFactory | |
* @param \Magento\Framework\Filesystem $filesystem | |
* @param \Magento\CatalogInventory\Model\ResourceModel\Stock\ItemFactory $stockResItemFac | |
* @param DateTime\TimezoneInterface $localeDate | |
* @param DateTime $dateTime | |
* @param \Psr\Log\LoggerInterface $logger | |
* @param \Magento\Framework\Indexer\IndexerRegistry $indexerRegistry | |
* @param Product\StoreResolver $storeResolver | |
* @param Product\SkuProcessor $skuProcessor | |
* @param Product\CategoryProcessor $categoryProcessor | |
* @param Product\Validator $validator | |
* @param ObjectRelationProcessor $objectRelationProcessor | |
* @param TransactionManagerInterface $transactionManager | |
* @param Product\TaxClassProcessor $taxClassProcessor | |
* @param \Magento\Framework\App\Config\ScopeConfigInterface $scopeConfig | |
* @param \Magento\Catalog\Model\Product\Url $productUrl | |
* @param array $data | |
* @param array $dateAttrCodes | |
* @param CatalogConfig $catalogConfig | |
* @param ImageTypeProcessor $imageTypeProcessor | |
* @param MediaGalleryProcessor $mediaProcessor | |
* @param StockItemImporterInterface|null $stockItemImporter | |
* @param DateTimeFactory $dateTimeFactory | |
* @param ProductRepositoryInterface|null $productRepository | |
* @throws LocalizedException | |
* @throws \Magento\Framework\Exception\FileSystemException | |
* @SuppressWarnings(PHPMD.ExcessiveParameterList) | |
* @SuppressWarnings(PHPMD.ExcessiveMethodLength) | |
*/ | |
public function __construct( | |
\Magento\Framework\Json\Helper\Data $jsonHelper, | |
\Magento\ImportExport\Helper\Data $importExportData, | |
\Magento\ImportExport\Model\ResourceModel\Import\Data $importData, | |
\Magento\Eav\Model\Config $config, | |
\Magento\Framework\App\ResourceConnection $resource, | |
\Magento\ImportExport\Model\ResourceModel\Helper $resourceHelper, | |
\Magento\Framework\Stdlib\StringUtils $string, | |
ProcessingErrorAggregatorInterface $errorAggregator, | |
\Magento\Framework\Event\ManagerInterface $eventManager, | |
\Magento\CatalogInventory\Api\StockRegistryInterface $stockRegistry, | |
\Magento\CatalogInventory\Api\StockConfigurationInterface $stockConfiguration, | |
\Magento\CatalogInventory\Model\Spi\StockStateProviderInterface $stockStateProvider, | |
\Magento\Catalog\Helper\Data $catalogData, | |
\Magento\ImportExport\Model\Import\Config $importConfig, | |
\Magento\CatalogImportExport\Model\Import\Proxy\Product\ResourceModelFactory $resourceFactory, | |
\Magento\CatalogImportExport\Model\Import\Product\OptionFactory $optionFactory, | |
\Magento\Eav\Model\ResourceModel\Entity\Attribute\Set\CollectionFactory $setColFactory, | |
\Magento\CatalogImportExport\Model\Import\Product\Type\Factory $productTypeFactory, | |
\Magento\Catalog\Model\ResourceModel\Product\LinkFactory $linkFactory, | |
\Magento\CatalogImportExport\Model\Import\Proxy\ProductFactory $proxyProdFactory, | |
\Magento\CatalogImportExport\Model\Import\UploaderFactory $uploaderFactory, | |
\Magento\Framework\Filesystem $filesystem, | |
\Magento\CatalogInventory\Model\ResourceModel\Stock\ItemFactory $stockResItemFac, | |
\Magento\Framework\Stdlib\DateTime\TimezoneInterface $localeDate, | |
DateTime $dateTime, | |
\Psr\Log\LoggerInterface $logger, | |
\Magento\Framework\Indexer\IndexerRegistry $indexerRegistry, | |
Product\StoreResolver $storeResolver, | |
Product\SkuProcessor $skuProcessor, | |
Product\CategoryProcessor $categoryProcessor, | |
Product\Validator $validator, | |
ObjectRelationProcessor $objectRelationProcessor, | |
TransactionManagerInterface $transactionManager, | |
Product\TaxClassProcessor $taxClassProcessor, | |
\Magento\Framework\App\Config\ScopeConfigInterface $scopeConfig, | |
\Magento\Catalog\Model\Product\Url $productUrl, | |
array $data = [], | |
array $dateAttrCodes = [], | |
CatalogConfig $catalogConfig = null, | |
ImageTypeProcessor $imageTypeProcessor = null, | |
MediaGalleryProcessor $mediaProcessor = null, | |
StockItemImporterInterface $stockItemImporter = null, | |
DateTimeFactory $dateTimeFactory = null, | |
ProductRepositoryInterface $productRepository = null | |
) { | |
$this->_eventManager = $eventManager; | |
$this->stockRegistry = $stockRegistry; | |
$this->stockConfiguration = $stockConfiguration; | |
$this->stockStateProvider = $stockStateProvider; | |
$this->_catalogData = $catalogData; | |
$this->_importConfig = $importConfig; | |
$this->_resourceFactory = $resourceFactory; | |
$this->_setColFactory = $setColFactory; | |
$this->_productTypeFactory = $productTypeFactory; | |
$this->_linkFactory = $linkFactory; | |
$this->_proxyProdFactory = $proxyProdFactory; | |
$this->_uploaderFactory = $uploaderFactory; | |
$this->filesystem = $filesystem; | |
$this->_mediaDirectory = $filesystem->getDirectoryWrite(DirectoryList::ROOT); | |
$this->_stockResItemFac = $stockResItemFac; | |
$this->_localeDate = $localeDate; | |
$this->dateTime = $dateTime; | |
$this->indexerRegistry = $indexerRegistry; | |
$this->_logger = $logger; | |
$this->storeResolver = $storeResolver; | |
$this->skuProcessor = $skuProcessor; | |
$this->categoryProcessor = $categoryProcessor; | |
$this->validator = $validator; | |
$this->objectRelationProcessor = $objectRelationProcessor; | |
$this->transactionManager = $transactionManager; | |
$this->taxClassProcessor = $taxClassProcessor; | |
$this->scopeConfig = $scopeConfig; | |
$this->productUrl = $productUrl; | |
$this->dateAttrCodes = array_merge($this->dateAttrCodes, $dateAttrCodes); | |
$this->catalogConfig = $catalogConfig ?: ObjectManager::getInstance()->get(CatalogConfig::class); | |
$this->imageTypeProcessor = $imageTypeProcessor ?: ObjectManager::getInstance()->get(ImageTypeProcessor::class); | |
$this->mediaProcessor = $mediaProcessor ?: ObjectManager::getInstance()->get(MediaGalleryProcessor::class); | |
$this->stockItemImporter = $stockItemImporter ?: ObjectManager::getInstance() | |
->get(StockItemImporterInterface::class); | |
parent::__construct( | |
$jsonHelper, | |
$importExportData, | |
$importData, | |
$config, | |
$resource, | |
$resourceHelper, | |
$string, | |
$errorAggregator | |
); | |
$this->_optionEntity = $data['option_entity'] ?? | |
$optionFactory->create(['data' => ['product_entity' => $this]]); | |
$this->_initAttributeSets() | |
->_initTypeModels() | |
->_initSkus() | |
->initImagesArrayKeys(); | |
$this->validator->init($this); | |
$this->dateTimeFactory = $dateTimeFactory ?? ObjectManager::getInstance()->get(DateTimeFactory::class); | |
$this->productRepository = $productRepository ?? ObjectManager::getInstance() | |
->get(ProductRepositoryInterface::class); | |
} | |
/** | |
* Check one attribute. Can be overridden in child. | |
* | |
* @param string $attrCode Attribute code | |
* @param array $attrParams Attribute params | |
* @param array $rowData Row data | |
* @param int $rowNum | |
* @return bool | |
*/ | |
public function isAttributeValid($attrCode, array $attrParams, array $rowData, $rowNum) | |
{ | |
if (!$this->validator->isAttributeValid($attrCode, $attrParams, $rowData)) { | |
foreach ($this->validator->getMessages() as $message) { | |
$this->skipRow($rowNum, $message, ProcessingError::ERROR_LEVEL_NOT_CRITICAL, $attrCode); | |
} | |
return false; | |
} | |
return true; | |
} | |
/** | |
* Multiple value separator getter. | |
* | |
* @return string | |
*/ | |
public function getMultipleValueSeparator() | |
{ | |
if (!empty($this->_parameters[Import::FIELD_FIELD_MULTIPLE_VALUE_SEPARATOR])) { | |
return $this->_parameters[Import::FIELD_FIELD_MULTIPLE_VALUE_SEPARATOR]; | |
} | |
return Import::DEFAULT_GLOBAL_MULTI_VALUE_SEPARATOR; | |
} | |
/** | |
* Return empty attribute value constant | |
* | |
* @return string | |
*/ | |
public function getEmptyAttributeValueConstant() | |
{ | |
if (!empty($this->_parameters[Import::FIELD_EMPTY_ATTRIBUTE_VALUE_CONSTANT])) { | |
return $this->_parameters[Import::FIELD_EMPTY_ATTRIBUTE_VALUE_CONSTANT]; | |
} | |
return Import::DEFAULT_EMPTY_ATTRIBUTE_VALUE_CONSTANT; | |
} | |
/** | |
* Retrieve instance of product custom options import entity | |
* | |
* @return \Magento\CatalogImportExport\Model\Import\Product\Option | |
*/ | |
public function getOptionEntity() | |
{ | |
return $this->_optionEntity; | |
} | |
/** | |
* Retrieve id of media gallery attribute. | |
* | |
* @return int | |
*/ | |
public function getMediaGalleryAttributeId() | |
{ | |
if (!$this->_mediaGalleryAttributeId) { | |
/** @var $resource \Magento\CatalogImportExport\Model\Import\Proxy\Product\ResourceModel */ | |
$resource = $this->_resourceFactory->create(); | |
$this->_mediaGalleryAttributeId = $resource->getAttribute(self::MEDIA_GALLERY_ATTRIBUTE_CODE)->getId(); | |
} | |
return $this->_mediaGalleryAttributeId; | |
} | |
/** | |
* Retrieve product type by name. | |
* | |
* @param string $name | |
* @return Product\Type\AbstractType | |
*/ | |
public function retrieveProductTypeByName($name) | |
{ | |
if (isset($this->_productTypeModels[$name])) { | |
return $this->_productTypeModels[$name]; | |
} | |
return null; | |
} | |
/** | |
* Set import parameters | |
* | |
* @param array $params | |
* @return $this | |
*/ | |
public function setParameters(array $params) | |
{ | |
parent::setParameters($params); | |
$this->getOptionEntity()->setParameters($params); | |
return $this; | |
} | |
/** | |
* Delete products for replacement. | |
* | |
* @return $this | |
*/ | |
public function deleteProductsForReplacement() | |
{ | |
$this->setParameters(array_merge( | |
$this->getParameters(), | |
['behavior' => Import::BEHAVIOR_DELETE] | |
)); | |
$this->_deleteProducts(); | |
return $this; | |
} | |
/** | |
* Delete products. | |
* | |
* @return $this | |
* @throws \Exception | |
*/ | |
protected function _deleteProducts() | |
{ | |
$productEntityTable = $this->_resourceFactory->create()->getEntityTable(); | |
while ($bunch = $this->_dataSourceModel->getNextBunch()) { | |
$idsToDelete = []; | |
foreach ($bunch as $rowNum => $rowData) { | |
if ($this->validateRow($rowData, $rowNum) && self::SCOPE_DEFAULT == $this->getRowScope($rowData)) { | |
$idsToDelete[] = $this->getExistingSku($rowData[self::COL_SKU])['entity_id']; | |
} | |
} | |
if ($idsToDelete) { | |
$this->countItemsDeleted += count($idsToDelete); | |
$this->transactionManager->start($this->_connection); | |
try { | |
$this->objectRelationProcessor->delete( | |
$this->transactionManager, | |
$this->_connection, | |
$productEntityTable, | |
$this->_connection->quoteInto('entity_id IN (?)', $idsToDelete), | |
['entity_id' => $idsToDelete] | |
); | |
$this->_eventManager->dispatch( | |
'catalog_product_import_bunch_delete_commit_before', | |
[ | |
'adapter' => $this, | |
'bunch' => $bunch, | |
'ids_to_delete' => $idsToDelete, | |
] | |
); | |
$this->transactionManager->commit(); | |
} catch (\Exception $e) { | |
$this->transactionManager->rollBack(); | |
throw $e; | |
} | |
$this->_eventManager->dispatch( | |
'catalog_product_import_bunch_delete_after', | |
['adapter' => $this, 'bunch' => $bunch] | |
); | |
} | |
} | |
return $this; | |
} | |
/** | |
* Create Product entity from raw data. | |
* | |
* @throws \Exception | |
* @return bool Result of operation. | |
* @SuppressWarnings(PHPMD.CyclomaticComplexity) | |
*/ | |
protected function _importData() | |
{ | |
$this->_validatedRows = null; | |
if (Import::BEHAVIOR_DELETE == $this->getBehavior()) { | |
$this->_deleteProducts(); | |
} elseif (Import::BEHAVIOR_REPLACE == $this->getBehavior()) { | |
$this->_replaceFlag = true; | |
$this->_replaceProducts(); | |
} else { | |
$this->_saveProductsData(); | |
} | |
$this->_eventManager->dispatch('catalog_product_import_finish_before', ['adapter' => $this]); | |
return true; | |
} | |
/** | |
* Replace imported products. | |
* | |
* @return $this | |
*/ | |
protected function _replaceProducts() | |
{ | |
$this->deleteProductsForReplacement(); | |
$this->_oldSku = $this->skuProcessor->reloadOldSkus()->getOldSkus(); | |
$this->_validatedRows = null; | |
$this->setParameters(array_merge( | |
$this->getParameters(), | |
['behavior' => Import::BEHAVIOR_APPEND] | |
)); | |
$this->_saveProductsData(); | |
return $this; | |
} | |
/** | |
* Save products data. | |
* | |
* @return $this | |
*/ | |
protected function _saveProductsData() | |
{ | |
$this->_saveProducts(); | |
foreach ($this->_productTypeModels as $productTypeModel) { | |
$productTypeModel->saveData(); | |
} | |
$this->_saveLinks(); | |
$this->_saveStockItem(); | |
if ($this->_replaceFlag) { | |
$this->getOptionEntity()->clearProductsSkuToId(); | |
} | |
$this->getOptionEntity()->importData(); | |
return $this; | |
} | |
/** | |
* Initialize attribute sets code-to-id pairs. | |
* | |
* @return $this | |
*/ | |
protected function _initAttributeSets() | |
{ | |
foreach ($this->_setColFactory->create()->setEntityTypeFilter($this->_entityTypeId) as $attributeSet) { | |
$this->_attrSetNameToId[$attributeSet->getAttributeSetName()] = $attributeSet->getId(); | |
$this->_attrSetIdToName[$attributeSet->getId()] = $attributeSet->getAttributeSetName(); | |
} | |
return $this; | |
} | |
/** | |
* Initialize existent product SKUs. | |
* | |
* @return $this | |
*/ | |
protected function _initSkus() | |
{ | |
$this->skuProcessor->setTypeModels($this->_productTypeModels); | |
$this->_oldSku = $this->skuProcessor->reloadOldSkus()->getOldSkus(); | |
return $this; | |
} | |
/** | |
* Initialize image array keys. | |
* | |
* @return $this | |
*/ | |
private function initImagesArrayKeys() | |
{ | |
$this->_imagesArrayKeys = $this->imageTypeProcessor->getImageTypes(); | |
return $this; | |
} | |
/** | |
* Initialize product type models. | |
* | |
* @return $this | |
* @throws \Magento\Framework\Exception\LocalizedException | |
*/ | |
protected function _initTypeModels() | |
{ | |
$productTypes = $this->_importConfig->getEntityTypes($this->getEntityTypeCode()); | |
foreach ($productTypes as $productTypeName => $productTypeConfig) { | |
$params = [$this, $productTypeName]; | |
if (!($model = $this->_productTypeFactory->create($productTypeConfig['model'], ['params' => $params])) | |
) { | |
throw new LocalizedException( | |
__('Entity type model \'%1\' is not found', $productTypeConfig['model']) | |
); | |
} | |
if (!$model instanceof \Magento\CatalogImportExport\Model\Import\Product\Type\AbstractType) { | |
throw new LocalizedException( | |
__( | |
'Entity type model must be an instance of ' | |
. \Magento\CatalogImportExport\Model\Import\Product\Type\AbstractType::class | |
) | |
); | |
} | |
if ($model->isSuitable()) { | |
$this->_productTypeModels[$productTypeName] = $model; | |
} | |
$this->_fieldsMap = array_merge($this->_fieldsMap, $model->getCustomFieldsMapping()); | |
$this->_specialAttributes = array_merge($this->_specialAttributes, $model->getParticularAttributes()); | |
} | |
$this->_initErrorTemplates(); | |
// remove doubles | |
$this->_specialAttributes = array_unique($this->_specialAttributes); | |
return $this; | |
} | |
/** | |
* Initialize Product error templates | |
*/ | |
protected function _initErrorTemplates() | |
{ | |
foreach ($this->_messageTemplates as $errorCode => $template) { | |
$this->addMessageTemplate($errorCode, $template); | |
} | |
} | |
/** | |
* Set valid attribute set and product type to rows. | |
* | |
* Set valid attribute set and product type to rows with all | |
* scopes to ensure that existing products doesn't changed. | |
* | |
* @param array $rowData | |
* @return array | |
*/ | |
protected function _prepareRowForDb(array $rowData) | |
{ | |
$rowData = $this->_customFieldsMapping($rowData); | |
$rowData = parent::_prepareRowForDb($rowData); | |
static $lastSku = null; | |
if (Import::BEHAVIOR_DELETE == $this->getBehavior()) { | |
return $rowData; | |
} | |
$lastSku = $rowData[self::COL_SKU]; | |
if ($this->isSkuExist($lastSku)) { | |
$newSku = $this->skuProcessor->getNewSku($lastSku); | |
$rowData[self::COL_ATTR_SET] = $newSku['attr_set_code']; | |
$rowData[self::COL_TYPE] = $newSku['type_id']; | |
} | |
return $rowData; | |
} | |
/** | |
* Gather and save information about product links. | |
* | |
* Must be called after ALL products saving done. | |
* | |
* @return $this | |
* @SuppressWarnings(PHPMD.CyclomaticComplexity) | |
* @SuppressWarnings(PHPMD.NPathComplexity) | |
* @SuppressWarnings(PHPMD.ExcessiveMethodLength) | |
*/ | |
protected function _saveLinks() | |
{ | |
$resource = $this->_linkFactory->create(); | |
$mainTable = $resource->getMainTable(); | |
$positionAttrId = []; | |
$nextLinkId = $this->_resourceHelper->getNextAutoincrement($mainTable); | |
// pre-load 'position' attributes ID for each link type once | |
foreach ($this->_linkNameToId as $linkName => $linkId) { | |
$select = $this->_connection->select()->from( | |
$resource->getTable('catalog_product_link_attribute'), | |
['id' => 'product_link_attribute_id'] | |
)->where( | |
'link_type_id = :link_id AND product_link_attribute_code = :position' | |
); | |
$bind = [':link_id' => $linkId, ':position' => 'position']; | |
$positionAttrId[$linkId] = $this->_connection->fetchOne($select, $bind); | |
} | |
while ($bunch = $this->_dataSourceModel->getNextBunch()) { | |
$productIds = []; | |
$linkRows = []; | |
$positionRows = []; | |
foreach ($bunch as $rowNum => $rowData) { | |
if (!$this->isRowAllowedToImport($rowData, $rowNum)) { | |
continue; | |
} | |
$sku = $rowData[self::COL_SKU]; | |
$productId = $this->skuProcessor->getNewSku($sku)[$this->getProductEntityLinkField()]; | |
$productLinkKeys = []; | |
$select = $this->_connection->select()->from( | |
$resource->getTable('catalog_product_link'), | |
['id' => 'link_id', 'linked_id' => 'linked_product_id', 'link_type_id' => 'link_type_id'] | |
)->where( | |
'product_id = :product_id' | |
); | |
$bind = [':product_id' => $productId]; | |
foreach ($this->_connection->fetchAll($select, $bind) as $linkData) { | |
$linkKey = "{$productId}-{$linkData['linked_id']}-{$linkData['link_type_id']}"; | |
$productLinkKeys[$linkKey] = $linkData['id']; | |
} | |
foreach ($this->_linkNameToId as $linkName => $linkId) { | |
$productIds[] = $productId; | |
if (isset($rowData[$linkName . 'sku'])) { | |
$linkSkus = explode($this->getMultipleValueSeparator(), $rowData[$linkName . 'sku']); | |
$linkPositions = !empty($rowData[$linkName . 'position']) | |
? explode($this->getMultipleValueSeparator(), $rowData[$linkName . 'position']) | |
: []; | |
foreach ($linkSkus as $linkedKey => $linkedSku) { | |
$linkedSku = trim($linkedSku); | |
if (($this->skuProcessor->getNewSku($linkedSku) !== null || $this->isSkuExist($linkedSku)) | |
&& strcasecmp($linkedSku, $sku) !== 0 | |
) { | |
$newSku = $this->skuProcessor->getNewSku($linkedSku); | |
if (!empty($newSku)) { | |
$linkedId = $newSku['entity_id']; | |
} else { | |
$linkedId = $this->getExistingSku($linkedSku)['entity_id']; | |
} | |
if ($linkedId == null) { | |
// Import file links to a SKU which is skipped for some reason, | |
// which leads to a "NULL" | |
// link causing fatal errors. | |
$this->_logger->critical( | |
new \Exception( | |
sprintf( | |
'WARNING: Orphaned link skipped: From SKU %s (ID %d) to SKU %s, ' . | |
'Link type id: %d', | |
$sku, | |
$productId, | |
$linkedSku, | |
$linkId | |
) | |
) | |
); | |
continue; | |
} | |
$linkKey = "{$productId}-{$linkedId}-{$linkId}"; | |
if (empty($productLinkKeys[$linkKey])) { | |
$productLinkKeys[$linkKey] = $nextLinkId; | |
} | |
if (!isset($linkRows[$linkKey])) { | |
$linkRows[$linkKey] = [ | |
'link_id' => $productLinkKeys[$linkKey], | |
'product_id' => $productId, | |
'linked_product_id' => $linkedId, | |
'link_type_id' => $linkId, | |
]; | |
} | |
if (!empty($linkPositions[$linkedKey])) { | |
$positionRows[] = [ | |
'link_id' => $productLinkKeys[$linkKey], | |
'product_link_attribute_id' => $positionAttrId[$linkId], | |
'value' => $linkPositions[$linkedKey], | |
]; | |
} | |
$nextLinkId++; | |
} | |
} | |
} | |
} | |
} | |
if (Import::BEHAVIOR_APPEND != $this->getBehavior() && $productIds) { | |
$this->_connection->delete( | |
$mainTable, | |
$this->_connection->quoteInto('product_id IN (?)', array_unique($productIds)) | |
); | |
} | |
if ($linkRows) { | |
$this->_connection->insertOnDuplicate($mainTable, $linkRows, ['link_id']); | |
} | |
if ($positionRows) { | |
// process linked product positions | |
$this->_connection->insertOnDuplicate( | |
$resource->getAttributeTypeTable('int'), | |
$positionRows, | |
['value'] | |
); | |
} | |
} | |
return $this; | |
} | |
/** | |
* Save product attributes. | |
* | |
* @param array $attributesData | |
* @return $this | |
*/ | |
protected function _saveProductAttributes(array $attributesData) | |
{ | |
$linkField = $this->getProductEntityLinkField(); | |
foreach ($attributesData as $tableName => $skuData) { | |
$tableData = []; | |
foreach ($skuData as $sku => $attributes) { | |
$linkId = $this->_oldSku[strtolower($sku)][$linkField]; | |
foreach ($attributes as $attributeId => $storeValues) { | |
foreach ($storeValues as $storeId => $storeValue) { | |
$tableData[] = [ | |
$linkField => $linkId, | |
'attribute_id' => $attributeId, | |
'store_id' => $storeId, | |
'value' => $storeValue, | |
]; | |
} | |
} | |
} | |
$this->_connection->insertOnDuplicate($tableName, $tableData, ['value']); | |
} | |
return $this; | |
} | |
/** | |
* Save product categories. | |
* | |
* @param array $categoriesData | |
* @return $this | |
*/ | |
protected function _saveProductCategories(array $categoriesData) | |
{ | |
static $tableName = null; | |
if (!$tableName) { | |
$tableName = $this->_resourceFactory->create()->getProductCategoryTable(); | |
} | |
if ($categoriesData) { | |
$categoriesIn = []; | |
$delProductId = []; | |
foreach ($categoriesData as $delSku => $categories) { | |
$productId = $this->skuProcessor->getNewSku($delSku)['entity_id']; | |
$delProductId[] = $productId; | |
foreach (array_keys($categories) as $categoryId) { | |
$categoriesIn[] = ['product_id' => $productId, 'category_id' => $categoryId, 'position' => 0]; | |
} | |
} | |
if (Import::BEHAVIOR_APPEND != $this->getBehavior()) { | |
$this->_connection->delete( | |
$tableName, | |
$this->_connection->quoteInto('product_id IN (?)', $delProductId) | |
); | |
} | |
if ($categoriesIn) { | |
$this->_connection->insertOnDuplicate($tableName, $categoriesIn, ['product_id', 'category_id']); | |
} | |
} | |
return $this; | |
} | |
/** | |
* Update and insert data in entity table. | |
* | |
* @param array $entityRowsIn Row for insert | |
* @param array $entityRowsUp Row for update | |
* @return $this | |
* @since 100.1.0 | |
*/ | |
public function saveProductEntity(array $entityRowsIn, array $entityRowsUp) | |
{ | |
static $entityTable = null; | |
$this->countItemsCreated += count($entityRowsIn); | |
$this->countItemsUpdated += count($entityRowsUp); | |
if (!$entityTable) { | |
$entityTable = $this->_resourceFactory->create()->getEntityTable(); | |
} | |
if ($entityRowsUp) { | |
$this->_connection->insertOnDuplicate($entityTable, $entityRowsUp, ['updated_at', 'attribute_set_id']); | |
} | |
if ($entityRowsIn) { | |
$this->_connection->insertMultiple($entityTable, $entityRowsIn); | |
$select = $this->_connection->select()->from( | |
$entityTable, | |
array_merge($this->getNewSkuFieldsForSelect(), $this->getOldSkuFieldsForSelect()) | |
)->where( | |
$this->_connection->quoteInto('sku IN (?)', array_keys($entityRowsIn)) | |
); | |
$newProducts = $this->_connection->fetchAll($select); | |
foreach ($newProducts as $data) { | |
$sku = $data['sku']; | |
unset($data['sku']); | |
foreach ($data as $key => $value) { | |
$this->skuProcessor->setNewSkuData($sku, $key, $value); | |
} | |
} | |
$this->updateOldSku($newProducts); | |
} | |
return $this; | |
} | |
/** | |
* Return additional data, needed to select. | |
* | |
* @return array | |
*/ | |
private function getOldSkuFieldsForSelect() | |
{ | |
return ['type_id', 'attribute_set_id']; | |
} | |
/** | |
* Adds newly created products to _oldSku | |
* | |
* @param array $newProducts | |
* @return void | |
*/ | |
private function updateOldSku(array $newProducts) | |
{ | |
$oldSkus = []; | |
foreach ($newProducts as $info) { | |
$typeId = $info['type_id']; | |
$sku = strtolower($info['sku']); | |
$oldSkus[$sku] = [ | |
'type_id' => $typeId, | |
'attr_set_id' => $info['attribute_set_id'], | |
$this->getProductIdentifierField() => $info[$this->getProductIdentifierField()], | |
'supported_type' => isset($this->_productTypeModels[$typeId]), | |
$this->getProductEntityLinkField() => $info[$this->getProductEntityLinkField()], | |
]; | |
} | |
$this->_oldSku = array_replace($this->_oldSku, $oldSkus); | |
} | |
/** | |
* Get new SKU fields for select | |
* | |
* @return array | |
*/ | |
private function getNewSkuFieldsForSelect() | |
{ | |
$fields = ['sku', $this->getProductEntityLinkField()]; | |
if ($this->getProductEntityLinkField() != $this->getProductIdentifierField()) { | |
$fields[] = $this->getProductIdentifierField(); | |
} | |
return $fields; | |
} | |
/** | |
* Init media gallery resources | |
* | |
* @return void | |
* @since 100.0.4 | |
* @deprecated | |
*/ | |
protected function initMediaGalleryResources() | |
{ | |
if (null == $this->mediaGalleryTableName) { | |
$this->productEntityTableName = $this->getResource()->getTable('catalog_product_entity'); | |
$this->mediaGalleryTableName = $this->getResource()->getTable('catalog_product_entity_media_gallery'); | |
$this->mediaGalleryValueTableName = $this->getResource()->getTable( | |
'catalog_product_entity_media_gallery_value' | |
); | |
$this->mediaGalleryEntityToValueTableName = $this->getResource()->getTable( | |
'catalog_product_entity_media_gallery_value_to_entity' | |
); | |
} | |
} | |
/** | |
* Get existing images for current bunch | |
* | |
* @param array $bunch | |
* @return array | |
*/ | |
protected function getExistingImages($bunch) | |
{ | |
return $this->mediaProcessor->getExistingImages($bunch); | |
} | |
/** | |
* Retrieve image from row. | |
* | |
* @param array $rowData | |
* @return array | |
*/ | |
public function getImagesFromRow(array $rowData) | |
{ | |
$images = []; | |
$labels = []; | |
foreach ($this->_imagesArrayKeys as $column) { | |
if (!empty($rowData[$column])) { | |
$images[$column] = array_unique( | |
array_map( | |
'trim', | |
explode($this->getMultipleValueSeparator(), $rowData[$column]) | |
) | |
); | |
if (!empty($rowData[$column . '_label'])) { | |
$labels[$column] = $this->parseMultipleValues($rowData[$column . '_label']); | |
if (count($labels[$column]) > count($images[$column])) { | |
$labels[$column] = array_slice($labels[$column], 0, count($images[$column])); | |
} | |
} | |
} | |
} | |
return [$images, $labels]; | |
} | |
/** | |
* Gather and save information about product entities. | |
* | |
* @return $this | |
* @SuppressWarnings(PHPMD.CyclomaticComplexity) | |
* @SuppressWarnings(PHPMD.NPathComplexity) | |
* @SuppressWarnings(PHPMD.ExcessiveMethodLength) | |
* @SuppressWarnings(PHPMD.UnusedLocalVariable) | |
* @throws LocalizedException | |
*/ | |
protected function _saveProducts() | |
{ | |
$priceIsGlobal = $this->_catalogData->isPriceGlobal(); | |
$productLimit = null; | |
$productsQty = null; | |
$entityLinkField = $this->getProductEntityLinkField(); | |
while ($bunch = $this->_dataSourceModel->getNextBunch()) { | |
$entityRowsIn = []; | |
$entityRowsUp = []; | |
$attributes = []; | |
$this->websitesCache = []; | |
$this->categoriesCache = []; | |
$tierPrices = []; | |
$mediaGallery = []; | |
$labelsForUpdate = []; | |
$imagesForChangeVisibility = []; | |
$uploadedImages = []; | |
$previousType = null; | |
$prevAttributeSet = null; | |
$importDir = $this->_mediaDirectory->getAbsolutePath($this->getImportDir()); | |
$existingImages = $this->getExistingImages($bunch); | |
$this->addImageHashes($existingImages); | |
foreach ($bunch as $rowNum => $rowData) { | |
// reset category processor's failed categories array | |
$this->categoryProcessor->clearFailedCategories(); | |
if (!$this->validateRow($rowData, $rowNum)) { | |
continue; | |
} | |
if ($this->getErrorAggregator()->hasToBeTerminated()) { | |
$this->getErrorAggregator()->addRowToSkip($rowNum); | |
continue; | |
} | |
$rowScope = $this->getRowScope($rowData); | |
$urlKey = $this->getUrlKey($rowData); | |
if (!empty($rowData[self::URL_KEY])) { | |
// If url_key column and its value were in the CSV file | |
$rowData[self::URL_KEY] = $urlKey; | |
} elseif ($this->isNeedToChangeUrlKey($rowData)) { | |
// If url_key column was empty or even not declared in the CSV file but by the rules it is need to | |
// be setteed. In case when url_key is generating from name column we have to ensure that the bunch | |
// of products will pass for the event with url_key column. | |
$bunch[$rowNum][self::URL_KEY] = $rowData[self::URL_KEY] = $urlKey; | |
} | |
$rowSku = $rowData[self::COL_SKU]; | |
if (null === $rowSku) { | |
$this->getErrorAggregator()->addRowToSkip($rowNum); | |
continue; | |
} | |
if (self::SCOPE_STORE == $rowScope) { | |
// set necessary data from SCOPE_DEFAULT row | |
$rowData[self::COL_TYPE] = $this->skuProcessor->getNewSku($rowSku)['type_id']; | |
$rowData['attribute_set_id'] = $this->skuProcessor->getNewSku($rowSku)['attr_set_id']; | |
$rowData[self::COL_ATTR_SET] = $this->skuProcessor->getNewSku($rowSku)['attr_set_code']; | |
} | |
// 1. Entity phase | |
if ($this->isSkuExist($rowSku)) { | |
// existing row | |
if (isset($rowData['attribute_set_code'])) { | |
$attributeSetId = $this->catalogConfig->getAttributeSetId( | |
$this->getEntityTypeId(), | |
$rowData['attribute_set_code'] | |
); | |
// wrong attribute_set_code was received | |
if (!$attributeSetId) { | |
throw new LocalizedException( | |
__( | |
'Wrong attribute set code "%1", please correct it and try again.', | |
$rowData['attribute_set_code'] | |
) | |
); | |
} | |
} else { | |
$attributeSetId = $this->skuProcessor->getNewSku($rowSku)['attr_set_id']; | |
} | |
$entityRowsUp[] = [ | |
'updated_at' => (new \DateTime())->format(DateTime::DATETIME_PHP_FORMAT), | |
'attribute_set_id' => $attributeSetId, | |
$entityLinkField => $this->getExistingSku($rowSku)[$entityLinkField] | |
]; | |
} else { | |
if (!$productLimit || $productsQty < $productLimit) { | |
$entityRowsIn[strtolower($rowSku)] = [ | |
'attribute_set_id' => $this->skuProcessor->getNewSku($rowSku)['attr_set_id'], | |
'type_id' => $this->skuProcessor->getNewSku($rowSku)['type_id'], | |
'sku' => $rowSku, | |
'has_options' => isset($rowData['has_options']) ? $rowData['has_options'] : 0, | |
'created_at' => (new \DateTime())->format(DateTime::DATETIME_PHP_FORMAT), | |
'updated_at' => (new \DateTime())->format(DateTime::DATETIME_PHP_FORMAT), | |
]; | |
$productsQty++; | |
} else { | |
$rowSku = null; | |
// sign for child rows to be skipped | |
$this->getErrorAggregator()->addRowToSkip($rowNum); | |
continue; | |
} | |
} | |
if (!array_key_exists($rowSku, $this->websitesCache)) { | |
$this->websitesCache[$rowSku] = []; | |
} | |
// 2. Product-to-Website phase | |
if (!empty($rowData[self::COL_PRODUCT_WEBSITES])) { | |
$websiteCodes = explode($this->getMultipleValueSeparator(), $rowData[self::COL_PRODUCT_WEBSITES]); | |
foreach ($websiteCodes as $websiteCode) { | |
$websiteId = $this->storeResolver->getWebsiteCodeToId($websiteCode); | |
$this->websitesCache[$rowSku][$websiteId] = true; | |
} | |
} else { | |
$product = $this->retrieveProductBySku($rowSku); | |
if ($product) { | |
$websiteIds = $product->getWebsiteIds(); | |
foreach ($websiteIds as $websiteId) { | |
$this->websitesCache[$rowSku][$websiteId] = true; | |
} | |
} | |
} | |
// 3. Categories phase | |
if (!array_key_exists($rowSku, $this->categoriesCache)) { | |
$this->categoriesCache[$rowSku] = []; | |
} | |
$rowData['rowNum'] = $rowNum; | |
$categoryIds = $this->processRowCategories($rowData); | |
foreach ($categoryIds as $id) { | |
$this->categoriesCache[$rowSku][$id] = true; | |
} | |
unset($rowData['rowNum']); | |
// 4.1. Tier prices phase | |
if (!empty($rowData['_tier_price_website'])) { | |
$tierPrices[$rowSku][] = [ | |
'all_groups' => $rowData['_tier_price_customer_group'] == self::VALUE_ALL, | |
'customer_group_id' => $rowData['_tier_price_customer_group'] == | |
self::VALUE_ALL ? 0 : $rowData['_tier_price_customer_group'], | |
'qty' => $rowData['_tier_price_qty'], | |
'value' => $rowData['_tier_price_price'], | |
'website_id' => self::VALUE_ALL == $rowData['_tier_price_website'] || | |
$priceIsGlobal ? 0 : $this->storeResolver->getWebsiteCodeToId($rowData['_tier_price_website']), | |
]; | |
} | |
if (!$this->validateRow($rowData, $rowNum)) { | |
continue; | |
} | |
// 5. Media gallery phase | |
list($rowImages, $rowLabels) = $this->getImagesFromRow($rowData); | |
$storeId = !empty($rowData[self::COL_STORE]) | |
? $this->getStoreIdByCode($rowData[self::COL_STORE]) | |
: Store::DEFAULT_STORE_ID; | |
$imageHiddenStates = $this->getImagesHiddenStates($rowData); | |
foreach (array_keys($imageHiddenStates) as $image) { | |
if (array_key_exists($rowSku, $existingImages) | |
&& array_key_exists($image, $existingImages[$rowSku]) | |
) { | |
$rowImages[self::COL_MEDIA_IMAGE][] = $image; | |
$uploadedImages[$image] = $image; | |
} | |
if (empty($rowImages)) { | |
$rowImages[self::COL_MEDIA_IMAGE][] = $image; | |
} | |
} | |
$rowData[self::COL_MEDIA_IMAGE] = []; | |
/* | |
* Note: to avoid problems with undefined sorting, the value of media gallery items positions | |
* must be unique in scope of one product. | |
*/ | |
$position = 0; | |
foreach ($rowImages as $column => $columnImages) { | |
foreach ($columnImages as $columnImageKey => $columnImage) { | |
$filename = $importDir . DIRECTORY_SEPARATOR . $columnImage; | |
$hash = ''; | |
if ($this->_mediaDirectory->isReadable($filename)) { | |
$hash = md5_file($filename); | |
} | |
if (!isset($existingImages[$rowSku])) { | |
$imageAlreadyExists = false; | |
} else { | |
$imageAlreadyExists = array_reduce($existingImages[$rowSku], function ($exists, $file) use ($hash) { | |
if ($exists) { | |
return $exists; | |
} | |
if ($file['hash'] === $hash) { | |
return $file['value']; | |
} | |
return $exists; | |
}, ''); | |
} | |
if ($imageAlreadyExists) { | |
$uploadedFile = $imageAlreadyExists; | |
} else { | |
if (!isset($uploadedImages[$columnImage])) { | |
$uploadedFile = $this->uploadMediaFiles($columnImage); | |
$uploadedFile = $uploadedFile ?: $this->getSystemFile($columnImage); | |
if ($uploadedFile) { | |
$uploadedImages[$columnImage] = $uploadedFile; | |
} else { | |
// unset($rowData[$column]); | |
// $this->skipRow($rowNum, ValidatorInterface::ERROR_MEDIA_URL_NOT_ACCESSIBLE); | |
} | |
} else { | |
$uploadedFile = $uploadedImages[$columnImage]; | |
} | |
} | |
if ($uploadedFile && $column !== self::COL_MEDIA_IMAGE) { | |
$rowData[$column] = $uploadedFile; | |
} | |
if ($uploadedFile && !isset($mediaGallery[$storeId][$rowSku][$uploadedFile])) { | |
if (isset($existingImages[$rowSku][$uploadedFile])) { | |
$currentFileData = $existingImages[$rowSku][$uploadedFile]; | |
if (isset($rowLabels[$column][$columnImageKey]) | |
&& $rowLabels[$column][$columnImageKey] != | |
$currentFileData['label'] | |
) { | |
$labelsForUpdate[] = [ | |
'label' => $rowLabels[$column][$columnImageKey], | |
'imageData' => $currentFileData | |
]; | |
} | |
if (array_key_exists($uploadedFile, $imageHiddenStates) | |
&& $currentFileData['disabled'] != $imageHiddenStates[$uploadedFile] | |
) { | |
$imagesForChangeVisibility[] = [ | |
'disabled' => $imageHiddenStates[$uploadedFile], | |
'imageData' => $currentFileData | |
]; | |
} | |
} else { | |
if ($column == self::COL_MEDIA_IMAGE) { | |
$rowData[$column][] = $uploadedFile; | |
} | |
$mediaGallery[$storeId][$rowSku][$uploadedFile] = [ | |
'attribute_id' => $this->getMediaGalleryAttributeId(), | |
'label' => isset($rowLabels[$column][$columnImageKey]) | |
? $rowLabels[$column][$columnImageKey] | |
: '', | |
'position' => ++$position, | |
'disabled' => isset($imageHiddenStates[$columnImage]) | |
? $imageHiddenStates[$columnImage] : '0', | |
'value' => $uploadedFile, | |
]; | |
} | |
} | |
} | |
} | |
// 6. Attributes phase | |
$rowStore = (self::SCOPE_STORE == $rowScope) | |
? $this->storeResolver->getStoreCodeToId($rowData[self::COL_STORE]) | |
: 0; | |
$productType = isset($rowData[self::COL_TYPE]) ? $rowData[self::COL_TYPE] : null; | |
if ($productType !== null) { | |
$previousType = $productType; | |
} | |
if (isset($rowData[self::COL_ATTR_SET])) { | |
$prevAttributeSet = $rowData[self::COL_ATTR_SET]; | |
} | |
if (self::SCOPE_NULL == $rowScope) { | |
// for multiselect attributes only | |
if ($prevAttributeSet !== null) { | |
$rowData[self::COL_ATTR_SET] = $prevAttributeSet; | |
} | |
if ($productType === null && $previousType !== null) { | |
$productType = $previousType; | |
} | |
if ($productType === null) { | |
continue; | |
} | |
} | |
$productTypeModel = $this->_productTypeModels[$productType]; | |
if (!empty($rowData['tax_class_name'])) { | |
$rowData['tax_class_id'] = | |
$this->taxClassProcessor->upsertTaxClass($rowData['tax_class_name'], $productTypeModel); | |
} | |
if ($this->getBehavior() == Import::BEHAVIOR_APPEND || | |
empty($rowData[self::COL_SKU]) | |
) { | |
$rowData = $productTypeModel->clearEmptyData($rowData); | |
} | |
$rowData = $productTypeModel->prepareAttributesWithDefaultValueForSave( | |
$rowData, | |
!$this->isSkuExist($rowSku) | |
); | |
$product = $this->_proxyProdFactory->create(['data' => $rowData]); | |
foreach ($rowData as $attrCode => $attrValue) { | |
$attribute = $this->retrieveAttributeByCode($attrCode); | |
if ('multiselect' != $attribute->getFrontendInput() && self::SCOPE_NULL == $rowScope) { | |
// skip attribute processing for SCOPE_NULL rows | |
continue; | |
} | |
$attrId = $attribute->getId(); | |
$backModel = $attribute->getBackendModel(); | |
$attrTable = $attribute->getBackend()->getTable(); | |
$storeIds = [0]; | |
if ('datetime' == $attribute->getBackendType() | |
&& ( | |
in_array($attribute->getAttributeCode(), $this->dateAttrCodes) | |
|| $attribute->getIsUserDefined() | |
) | |
) { | |
$attrValue = $this->dateTime->formatDate($attrValue, false); | |
} elseif ('datetime' == $attribute->getBackendType() && strtotime($attrValue)) { | |
$attrValue = gmdate( | |
'Y-m-d H:i:s', | |
$this->_localeDate->date($attrValue)->getTimestamp() | |
); | |
} elseif ($backModel) { | |
$attribute->getBackend()->beforeSave($product); | |
$attrValue = $product->getData($attribute->getAttributeCode()); | |
} | |
if (self::SCOPE_STORE == $rowScope) { | |
if (self::SCOPE_WEBSITE == $attribute->getIsGlobal()) { | |
// check website defaults already set | |
if (!isset($attributes[$attrTable][$rowSku][$attrId][$rowStore])) { | |
$storeIds = $this->storeResolver->getStoreIdToWebsiteStoreIds($rowStore); | |
} | |
} elseif (self::SCOPE_STORE == $attribute->getIsGlobal()) { | |
$storeIds = [$rowStore]; | |
} | |
if (!$this->isSkuExist($rowSku)) { | |
$storeIds[] = 0; | |
} | |
} | |
foreach ($storeIds as $storeId) { | |
if (!isset($attributes[$attrTable][$rowSku][$attrId][$storeId])) { | |
$attributes[$attrTable][$rowSku][$attrId][$storeId] = $attrValue; | |
} | |
} | |
// restore 'backend_model' to avoid 'default' setting | |
$attribute->setBackendModel($backModel); | |
} | |
} | |
foreach ($bunch as $rowNum => $rowData) { | |
if ($this->getErrorAggregator()->isRowInvalid($rowNum)) { | |
unset($bunch[$rowNum]); | |
} | |
} | |
$this->saveProductEntity( | |
$entityRowsIn, | |
$entityRowsUp | |
)->_saveProductWebsites( | |
$this->websitesCache | |
)->_saveProductCategories( | |
$this->categoriesCache | |
)->_saveProductTierPrices( | |
$tierPrices | |
)->_saveMediaGallery( | |
$mediaGallery | |
)->_saveProductAttributes( | |
$attributes | |
)->updateMediaGalleryVisibility( | |
$imagesForChangeVisibility | |
)->updateMediaGalleryLabels( | |
$labelsForUpdate | |
); | |
$this->_eventManager->dispatch( | |
'catalog_product_import_bunch_save_after', | |
['adapter' => $this, 'bunch' => $bunch] | |
); | |
} | |
return $this; | |
} | |
/** | |
* Generate md5 hashes for existing images for comparison with newly uploaded images. | |
* | |
* @param array $images | |
*/ | |
public function addImageHashes(&$images) { | |
$dirConfig = DirectoryList::getDefaultConfig(); | |
$dirAddon = $dirConfig[DirectoryList::MEDIA][DirectoryList::PATH]; | |
$productPath = $this->_mediaDirectory->getAbsolutePath($dirAddon . '/catalog/product'); | |
foreach ($images as $sku => $files) { | |
foreach ($files as $path => $file) { | |
$images[$sku][$path]['hash'] = md5_file($productPath . $file['value']); | |
} | |
} | |
} | |
/** | |
* Prepare array with image states (visible or hidden from product page) | |
* | |
* @param array $rowData | |
* @return array | |
*/ | |
private function getImagesHiddenStates($rowData) | |
{ | |
$statesArray = []; | |
$mappingArray = [ | |
'_media_is_disabled' => '1' | |
]; | |
foreach ($mappingArray as $key => $value) { | |
if (isset($rowData[$key]) && strlen(trim($rowData[$key]))) { | |
$items = explode($this->getMultipleValueSeparator(), $rowData[$key]); | |
foreach ($items as $item) { | |
$statesArray[$item] = $value; | |
} | |
} | |
} | |
return $statesArray; | |
} | |
/** | |
* Resolve valid category ids from provided row data. | |
* | |
* @param array $rowData | |
* @return array | |
*/ | |
protected function processRowCategories($rowData) | |
{ | |
$categoriesString = empty($rowData[self::COL_CATEGORY]) ? '' : $rowData[self::COL_CATEGORY]; | |
$categoryIds = []; | |
if (!empty($categoriesString)) { | |
$categoryIds = $this->categoryProcessor->upsertCategories( | |
$categoriesString, | |
$this->getMultipleValueSeparator() | |
); | |
foreach ($this->categoryProcessor->getFailedCategories() as $error) { | |
$this->errorAggregator->addError( | |
AbstractEntity::ERROR_CODE_CATEGORY_NOT_VALID, | |
ProcessingError::ERROR_LEVEL_NOT_CRITICAL, | |
$rowData['rowNum'], | |
self::COL_CATEGORY, | |
__('Category "%1" has not been created.', $error['category']) | |
. ' ' . $error['exception']->getMessage() | |
); | |
} | |
} else { | |
$product = $this->retrieveProductBySku($rowData['sku']); | |
if ($product) { | |
$categoryIds = $product->getCategoryIds(); | |
} | |
} | |
return $categoryIds; | |
} | |
/** | |
* Get product websites. | |
* | |
* @param string $productSku | |
* @return array | |
*/ | |
public function getProductWebsites($productSku) | |
{ | |
return array_keys($this->websitesCache[$productSku]); | |
} | |
/** | |
* Retrieve product categories. | |
* | |
* @param string $productSku | |
* @return array | |
*/ | |
public function getProductCategories($productSku) | |
{ | |
return array_keys($this->categoriesCache[$productSku]); | |
} | |
/** | |
* Get store id by code. | |
* | |
* @param string $storeCode | |
* @return array|int|null|string | |
*/ | |
public function getStoreIdByCode($storeCode) | |
{ | |
if (empty($storeCode)) { | |
return self::SCOPE_DEFAULT; | |
} | |
return $this->storeResolver->getStoreCodeToId($storeCode); | |
} | |
/** | |
* Save product tier prices. | |
* | |
* @param array $tierPriceData | |
* @return $this | |
*/ | |
protected function _saveProductTierPrices(array $tierPriceData) | |
{ | |
static $tableName = null; | |
if (!$tableName) { | |
$tableName = $this->_resourceFactory->create()->getTable('catalog_product_entity_tier_price'); | |
} | |
if ($tierPriceData) { | |
$tierPriceIn = []; | |
$delProductId = []; | |
foreach ($tierPriceData as $delSku => $tierPriceRows) { | |
$productId = $this->skuProcessor->getNewSku($delSku)[$this->getProductEntityLinkField()]; | |
$delProductId[] = $productId; | |
foreach ($tierPriceRows as $row) { | |
$row[$this->getProductEntityLinkField()] = $productId; | |
$tierPriceIn[] = $row; | |
} | |
} | |
if (Import::BEHAVIOR_APPEND != $this->getBehavior()) { | |
$this->_connection->delete( | |
$tableName, | |
$this->_connection->quoteInto("{$this->getProductEntityLinkField()} IN (?)", $delProductId) | |
); | |
} | |
if ($tierPriceIn) { | |
$this->_connection->insertOnDuplicate($tableName, $tierPriceIn, ['value']); | |
} | |
} | |
return $this; | |
} | |
/** | |
* Returns the import directory if specified or a default import directory (media/import). | |
* | |
* @return string | |
*/ | |
protected function getImportDir() | |
{ | |
$dirConfig = DirectoryList::getDefaultConfig(); | |
$dirAddon = $dirConfig[DirectoryList::MEDIA][DirectoryList::PATH]; | |
if (!empty($this->_parameters[Import::FIELD_NAME_IMG_FILE_DIR])) { | |
$tmpPath = $this->_parameters[Import::FIELD_NAME_IMG_FILE_DIR]; | |
} else { | |
$tmpPath = $dirAddon . '/' . $this->_mediaDirectory->getRelativePath('import'); | |
} | |
return $tmpPath; | |
} | |
/** | |
* Returns an object for upload a media files | |
* | |
* @return \Magento\CatalogImportExport\Model\Import\Uploader | |
* @throws \Magento\Framework\Exception\LocalizedException | |
*/ | |
protected function _getUploader() | |
{ | |
if ($this->_fileUploader === null) { | |
$this->_fileUploader = $this->_uploaderFactory->create(); | |
$this->_fileUploader->init(); | |
$dirConfig = DirectoryList::getDefaultConfig(); | |
$dirAddon = $dirConfig[DirectoryList::MEDIA][DirectoryList::PATH]; | |
$tmpPath = $this->getImportDir(); | |
if (!$this->_fileUploader->setTmpDir($tmpPath)) { | |
throw new LocalizedException( | |
__('File directory \'%1\' is not readable.', $tmpPath) | |
); | |
} | |
$destinationDir = "catalog/product"; | |
$destinationPath = $dirAddon . '/' . $this->_mediaDirectory->getRelativePath($destinationDir); | |
$this->_mediaDirectory->create($destinationPath); | |
if (!$this->_fileUploader->setDestDir($destinationPath)) { | |
throw new LocalizedException( | |
__('File directory \'%1\' is not writable.', $destinationPath) | |
); | |
} | |
} | |
return $this->_fileUploader; | |
} | |
/** | |
* Retrieve uploader. | |
* | |
* @return Uploader | |
* @throws \Magento\Framework\Exception\LocalizedException | |
*/ | |
public function getUploader() | |
{ | |
return $this->_getUploader(); | |
} | |
/** | |
* Uploading files into the "catalog/product" media folder. | |
* | |
* Return a new file name if the same file is already exists. | |
* | |
* @param string $fileName | |
* @param bool $renameFileOff [optional] boolean to pass. | |
* Default is false which will set not to rename the file after import. | |
* @return string | |
*/ | |
protected function uploadMediaFiles($fileName, $renameFileOff = false) | |
{ | |
try { | |
$res = $this->_getUploader()->move($fileName, $renameFileOff); | |
return $res['file']; | |
} catch (\Exception $e) { | |
$this->_logger->critical($e); | |
return ''; | |
} | |
} | |
/** | |
* Try to find file by it's path. | |
* | |
* @param string $fileName | |
* @return string | |
*/ | |
private function getSystemFile($fileName) | |
{ | |
$filePath = 'catalog' . DIRECTORY_SEPARATOR . 'product' . DIRECTORY_SEPARATOR . $fileName; | |
/** @var \Magento\Framework\Filesystem\Directory\ReadInterface $read */ | |
$read = $this->filesystem->getDirectoryRead(DirectoryList::MEDIA); | |
return $read->isExist($filePath) && $read->isReadable($filePath) ? $fileName : ''; | |
} | |
/** | |
* Save product media gallery. | |
* | |
* @param array $mediaGalleryData | |
* @return $this | |
*/ | |
protected function _saveMediaGallery(array $mediaGalleryData) | |
{ | |
if (empty($mediaGalleryData)) { | |
return $this; | |
} | |
$this->mediaProcessor->saveMediaGallery($mediaGalleryData); | |
return $this; | |
} | |
/** | |
* Save product websites. | |
* | |
* @param array $websiteData | |
* @return $this | |
*/ | |
protected function _saveProductWebsites(array $websiteData) | |
{ | |
static $tableName = null; | |
if (!$tableName) { | |
$tableName = $this->_resourceFactory->create()->getProductWebsiteTable(); | |
} | |
if ($websiteData) { | |
$websitesData = []; | |
$delProductId = []; | |
foreach ($websiteData as $delSku => $websites) { | |
$productId = $this->skuProcessor->getNewSku($delSku)['entity_id']; | |
$delProductId[] = $productId; | |
foreach (array_keys($websites) as $websiteId) { | |
$websitesData[] = ['product_id' => $productId, 'website_id' => $websiteId]; | |
} | |
} | |
if (Import::BEHAVIOR_APPEND != $this->getBehavior()) { | |
$this->_connection->delete( | |
$tableName, | |
$this->_connection->quoteInto('product_id IN (?)', $delProductId) | |
); | |
} | |
if ($websitesData) { | |
$this->_connection->insertOnDuplicate($tableName, $websitesData); | |
} | |
} | |
return $this; | |
} | |
/** | |
* Stock item saving. | |
* | |
* @return $this | |
*/ | |
protected function _saveStockItem() | |
{ | |
while ($bunch = $this->_dataSourceModel->getNextBunch()) { | |
$stockData = []; | |
$productIdsToReindex = []; | |
// Format bunch to stock data rows | |
foreach ($bunch as $rowNum => $rowData) { | |
if (!$this->isRowAllowedToImport($rowData, $rowNum)) { | |
continue; | |
} | |
$row = []; | |
$sku = $rowData[self::COL_SKU]; | |
if ($this->skuProcessor->getNewSku($sku) !== null) { | |
$row = $this->formatStockDataForRow($rowData); | |
$productIdsToReindex[] = $row['product_id']; | |
} | |
if (!isset($stockData[$sku])) { | |
$stockData[$sku] = $row; | |
} | |
} | |
// Insert rows | |
if (!empty($stockData)) { | |
$this->stockItemImporter->import($stockData); | |
} | |
$this->reindexProducts($productIdsToReindex); | |
} | |
return $this; | |
} | |
/** | |
* Initiate product reindex by product ids | |
* | |
* @param array $productIdsToReindex | |
* @return void | |
*/ | |
private function reindexProducts($productIdsToReindex = []) | |
{ | |
$indexer = $this->indexerRegistry->get('catalog_product_category'); | |
if (is_array($productIdsToReindex) && count($productIdsToReindex) > 0 && !$indexer->isScheduled()) { | |
$indexer->reindexList($productIdsToReindex); | |
} | |
} | |
/** | |
* Retrieve attribute by code | |
* | |
* @param string $attrCode | |
* @return mixed | |
*/ | |
public function retrieveAttributeByCode($attrCode) | |
{ | |
/** @var string $attrCode */ | |
$attrCode = mb_strtolower($attrCode); | |
if (!isset($this->_attributeCache[$attrCode])) { | |
$this->_attributeCache[$attrCode] = $this->getResource()->getAttribute($attrCode); | |
} | |
return $this->_attributeCache[$attrCode]; | |
} | |
/** | |
* Attribute set ID-to-name pairs getter. | |
* | |
* @return array | |
*/ | |
public function getAttrSetIdToName() | |
{ | |
return $this->_attrSetIdToName; | |
} | |
/** | |
* DB connection getter. | |
* | |
* @return \Magento\Framework\DB\Adapter\AdapterInterface | |
*/ | |
public function getConnection() | |
{ | |
return $this->_connection; | |
} | |
/** | |
* EAV entity type code getter. | |
* | |
* @abstract | |
* @return string | |
*/ | |
public function getEntityTypeCode() | |
{ | |
return 'catalog_product'; | |
} | |
/** | |
* New products SKU data. | |
* | |
* Returns array of new products data with SKU as key. All SKU keys are in lowercase for avoiding creation of | |
* new products with the same SKU in different letter cases. | |
* | |
* @param string $sku | |
* @return array | |
*/ | |
public function getNewSku($sku = null) | |
{ | |
return $this->skuProcessor->getNewSku($sku); | |
} | |
/** | |
* Get next bunch of validated rows. | |
* | |
* @return array|null | |
*/ | |
public function getNextBunch() | |
{ | |
return $this->_dataSourceModel->getNextBunch(); | |
} | |
/** | |
* Existing products SKU getter. | |
* | |
* Returns array of existing products data with SKU as key. All SKU keys are in lowercase for avoiding creation of | |
* new products with the same SKU in different letter cases. | |
* | |
* @return array | |
*/ | |
public function getOldSku() | |
{ | |
return $this->_oldSku; | |
} | |
/** | |
* Retrieve Category Processor | |
* | |
* @return \Magento\CatalogImportExport\Model\Import\Product\CategoryProcessor | |
*/ | |
public function getCategoryProcessor() | |
{ | |
return $this->categoryProcessor; | |
} | |
/** | |
* Obtain scope of the row from row data. | |
* | |
* @param array $rowData | |
* @return int | |
*/ | |
public function getRowScope(array $rowData) | |
{ | |
if (empty($rowData[self::COL_STORE])) { | |
return self::SCOPE_DEFAULT; | |
} | |
return self::SCOPE_STORE; | |
} | |
/** | |
* Validate data row. | |
* | |
* @param array $rowData | |
* @param int $rowNum | |
* @return boolean | |
* @SuppressWarnings(PHPMD.CyclomaticComplexity) | |
* @SuppressWarnings(PHPMD.NPathComplexity) | |
* @SuppressWarnings(PHPMD.ExcessiveMethodLength) | |
* @throws \Zend_Validate_Exception | |
*/ | |
public function validateRow(array $rowData, $rowNum) | |
{ | |
if (isset($this->_validatedRows[$rowNum])) { | |
// check that row is already validated | |
return !$this->getErrorAggregator()->isRowInvalid($rowNum); | |
} | |
$this->_validatedRows[$rowNum] = true; | |
$rowScope = $this->getRowScope($rowData); | |
$sku = $rowData[self::COL_SKU]; | |
// BEHAVIOR_DELETE and BEHAVIOR_REPLACE use specific validation logic | |
if (Import::BEHAVIOR_REPLACE == $this->getBehavior()) { | |
if (self::SCOPE_DEFAULT == $rowScope && !$this->isSkuExist($sku)) { | |
$this->skipRow($rowNum, ValidatorInterface::ERROR_SKU_NOT_FOUND_FOR_DELETE); | |
return false; | |
} | |
} | |
if (Import::BEHAVIOR_DELETE == $this->getBehavior()) { | |
if (self::SCOPE_DEFAULT == $rowScope && !$this->isSkuExist($sku)) { | |
$this->skipRow($rowNum, ValidatorInterface::ERROR_SKU_NOT_FOUND_FOR_DELETE); | |
return false; | |
} | |
return true; | |
} | |
// if product doesn't exist, need to throw critical error else all errors should be not critical. | |
$errorLevel = $this->getValidationErrorLevel($sku); | |
if (!$this->validator->isValid($rowData)) { | |
foreach ($this->validator->getMessages() as $message) { | |
$this->skipRow($rowNum, $message, $errorLevel, $this->validator->getInvalidAttribute()); | |
} | |
} | |
if (null === $sku) { | |
$this->skipRow($rowNum, ValidatorInterface::ERROR_SKU_IS_EMPTY, $errorLevel); | |
} elseif (false === $sku) { | |
$this->skipRow($rowNum, ValidatorInterface::ERROR_ROW_IS_ORPHAN, $errorLevel); | |
} elseif (self::SCOPE_STORE == $rowScope | |
&& !$this->storeResolver->getStoreCodeToId($rowData[self::COL_STORE]) | |
) { | |
$this->skipRow($rowNum, ValidatorInterface::ERROR_INVALID_STORE, $errorLevel); | |
} | |
// SKU is specified, row is SCOPE_DEFAULT, new product block begins | |
$this->_processedEntitiesCount++; | |
if ($this->isSkuExist($sku) && Import::BEHAVIOR_REPLACE !== $this->getBehavior()) { | |
// can we get all necessary data from existent DB product? | |
// check for supported type of existing product | |
if (isset($this->_productTypeModels[$this->getExistingSku($sku)['type_id']])) { | |
$this->skuProcessor->addNewSku( | |
$sku, | |
$this->prepareNewSkuData($sku) | |
); | |
} else { | |
$this->skipRow($rowNum, ValidatorInterface::ERROR_TYPE_UNSUPPORTED, $errorLevel); | |
} | |
} else { | |
// validate new product type and attribute set | |
if (!isset($rowData[self::COL_TYPE], $this->_productTypeModels[$rowData[self::COL_TYPE]])) { | |
$this->skipRow($rowNum, ValidatorInterface::ERROR_INVALID_TYPE, $errorLevel); | |
} elseif (!isset($rowData[self::COL_ATTR_SET], $this->_attrSetNameToId[$rowData[self::COL_ATTR_SET]]) | |
) { | |
$this->skipRow($rowNum, ValidatorInterface::ERROR_INVALID_ATTR_SET, $errorLevel); | |
} elseif ($this->skuProcessor->getNewSku($sku) === null) { | |
$this->skuProcessor->addNewSku( | |
$sku, | |
[ | |
'row_id' => null, | |
'entity_id' => null, | |
'type_id' => $rowData[self::COL_TYPE], | |
'attr_set_id' => $this->_attrSetNameToId[$rowData[self::COL_ATTR_SET]], | |
'attr_set_code' => $rowData[self::COL_ATTR_SET], | |
] | |
); | |
} | |
} | |
if (!$this->getErrorAggregator()->isRowInvalid($rowNum)) { | |
$newSku = $this->skuProcessor->getNewSku($sku); | |
// set attribute set code into row data for followed attribute validation in type model | |
$rowData[self::COL_ATTR_SET] = $newSku['attr_set_code']; | |
/** @var \Magento\CatalogImportExport\Model\Import\Product\Type\AbstractType $productTypeValidator */ | |
// isRowValid can add error to general errors pull if row is invalid | |
$productTypeValidator = $this->_productTypeModels[$newSku['type_id']]; | |
$productTypeValidator->isRowValid( | |
$rowData, | |
$rowNum, | |
!($this->isSkuExist($sku) && Import::BEHAVIOR_REPLACE !== $this->getBehavior()) | |
); | |
} | |
// validate custom options | |
$this->getOptionEntity()->validateRow($rowData, $rowNum); | |
if ($this->isNeedToValidateUrlKey($rowData)) { | |
$urlKey = strtolower($this->getUrlKey($rowData)); | |
$storeCodes = empty($rowData[self::COL_STORE_VIEW_CODE]) | |
? array_flip($this->storeResolver->getStoreCodeToId()) | |
: explode($this->getMultipleValueSeparator(), $rowData[self::COL_STORE_VIEW_CODE]); | |
foreach ($storeCodes as $storeCode) { | |
$storeId = $this->storeResolver->getStoreCodeToId($storeCode); | |
$productUrlSuffix = $this->getProductUrlSuffix($storeId); | |
$urlPath = $urlKey . $productUrlSuffix; | |
if (empty($this->urlKeys[$storeId][$urlPath]) | |
|| ($this->urlKeys[$storeId][$urlPath] == $sku) | |
) { | |
$this->urlKeys[$storeId][$urlPath] = $sku; | |
$this->rowNumbers[$storeId][$urlPath] = $rowNum; | |
} else { | |
$message = sprintf( | |
$this->retrieveMessageTemplate(ValidatorInterface::ERROR_DUPLICATE_URL_KEY), | |
$urlKey, | |
$this->urlKeys[$storeId][$urlPath] | |
); | |
$this->addRowError( | |
ValidatorInterface::ERROR_DUPLICATE_URL_KEY, | |
$rowNum, | |
$rowData[self::COL_NAME], | |
$message, | |
ProcessingError::ERROR_LEVEL_NOT_CRITICAL | |
) | |
->getErrorAggregator() | |
->addRowToSkip($rowNum); | |
} | |
} | |
} | |
if (!empty($rowData['new_from_date']) && !empty($rowData['new_to_date']) | |
) { | |
$newFromTimestamp = strtotime($this->dateTime->formatDate($rowData['new_from_date'], false)); | |
$newToTimestamp = strtotime($this->dateTime->formatDate($rowData['new_to_date'], false)); | |
if ($newFromTimestamp > $newToTimestamp) { | |
$this->skipRow( | |
$rowNum, | |
'invalidNewToDateValue', | |
$errorLevel, | |
$rowData['new_to_date'] | |
); | |
} | |
} | |
return !$this->getErrorAggregator()->isRowInvalid($rowNum); | |
} | |
/** | |
* Check if need to validate url key. | |
* | |
* @param array $rowData | |
* @return bool | |
*/ | |
private function isNeedToValidateUrlKey($rowData) | |
{ | |
return (!empty($rowData[self::URL_KEY]) || !empty($rowData[self::COL_NAME])) | |
&& (empty($rowData[self::COL_VISIBILITY]) | |
|| $rowData[self::COL_VISIBILITY] | |
!== (string)Visibility::getOptionArray()[Visibility::VISIBILITY_NOT_VISIBLE]); | |
} | |
/** | |
* Prepare new SKU data | |
* | |
* @param string $sku | |
* @return array | |
*/ | |
private function prepareNewSkuData($sku) | |
{ | |
$data = []; | |
foreach ($this->getExistingSku($sku) as $key => $value) { | |
$data[$key] = $value; | |
} | |
$data['attr_set_code'] = $this->_attrSetIdToName[$this->getExistingSku($sku)['attr_set_id']]; | |
return $data; | |
} | |
/** | |
* Parse attributes names and values string to array. | |
* | |
* @param array $rowData | |
* | |
* @return array | |
*/ | |
private function _parseAdditionalAttributes($rowData) | |
{ | |
if (empty($rowData['additional_attributes'])) { | |
return $rowData; | |
} | |
$rowData = array_merge($rowData, $this->getAdditionalAttributes($rowData['additional_attributes'])); | |
return $rowData; | |
} | |
/** | |
* Retrieves additional attributes in format: | |
* [ | |
* code1 => value1, | |
* code2 => value2, | |
* ... | |
* codeN => valueN | |
* ] | |
* | |
* @param string $additionalAttributes Attributes data that will be parsed | |
* @return array | |
*/ | |
private function getAdditionalAttributes($additionalAttributes) | |
{ | |
return empty($this->_parameters[Import::FIELDS_ENCLOSURE]) | |
? $this->parseAttributesWithoutWrappedValues($additionalAttributes) | |
: $this->parseAttributesWithWrappedValues($additionalAttributes); | |
} | |
/** | |
* Parses data and returns attributes in format: | |
* [ | |
* code1 => value1, | |
* code2 => value2, | |
* ... | |
* codeN => valueN | |
* ] | |
* | |
* @param string $attributesData Attributes data that will be parsed. It keeps data in format: | |
* code=value,code2=value2...,codeN=valueN | |
* @return array | |
*/ | |
private function parseAttributesWithoutWrappedValues($attributesData) | |
{ | |
$attributeNameValuePairs = explode($this->getMultipleValueSeparator(), $attributesData); | |
$preparedAttributes = []; | |
$code = ''; | |
foreach ($attributeNameValuePairs as $attributeData) { | |
//process case when attribute has ImportModel::DEFAULT_GLOBAL_MULTI_VALUE_SEPARATOR inside its value | |
if (strpos($attributeData, self::PAIR_NAME_VALUE_SEPARATOR) === false) { | |
if (!$code) { | |
continue; | |
} | |
$preparedAttributes[$code] .= $this->getMultipleValueSeparator() . $attributeData; | |
continue; | |
} | |
list($code, $value) = explode(self::PAIR_NAME_VALUE_SEPARATOR, $attributeData, 2); | |
$code = mb_strtolower($code); | |
$preparedAttributes[$code] = $value; | |
} | |
return $preparedAttributes; | |
} | |
/** | |
* Parses data and returns attributes in format: | |
* [ | |
* code1 => value1, | |
* code2 => value2, | |
* ... | |
* codeN => valueN | |
* ] | |
* All values have unescaped data except mupliselect attributes, | |
* they should be parsed in additional method - parseMultiselectValues() | |
* | |
* @param string $attributesData Attributes data that will be parsed. It keeps data in format: | |
* code="value",code2="value2"...,codeN="valueN" | |
* where every value is wrapped in double quotes. Double quotes as part of value should be duplicated. | |
* E.g. attribute with code 'attr_code' has value 'my"value'. This data should be stored as attr_code="my""value" | |
* | |
* @return array | |
*/ | |
private function parseAttributesWithWrappedValues($attributesData) | |
{ | |
$attributes = []; | |
preg_match_all( | |
'~((?:[a-zA-Z0-9_])+)="((?:[^"]|""|"' . $this->getMultiLineSeparatorForRegexp() . '")+)"+~', | |
$attributesData, | |
$matches | |
); | |
foreach ($matches[1] as $i => $attributeCode) { | |
$attribute = $this->retrieveAttributeByCode($attributeCode); | |
$value = 'multiselect' != $attribute->getFrontendInput() | |
? str_replace('""', '"', $matches[2][$i]) | |
: '"' . $matches[2][$i] . '"'; | |
$attributes[mb_strtolower($attributeCode)] = $value; | |
} | |
return $attributes; | |
} | |
/** | |
* Parse values of multiselect attributes depends on "Fields Enclosure" parameter | |
* | |
* @param string $values | |
* @param string $delimiter | |
* @return array | |
* @since 100.1.2 | |
*/ | |
public function parseMultiselectValues($values, $delimiter = self::PSEUDO_MULTI_LINE_SEPARATOR) | |
{ | |
if (empty($this->_parameters[Import::FIELDS_ENCLOSURE])) { | |
return explode($delimiter, $values); | |
} | |
if (preg_match_all('~"((?:[^"]|"")*)"~', $values, $matches)) { | |
return $values = array_map(function ($value) { | |
return str_replace('""', '"', $value); | |
}, $matches[1]); | |
} | |
return [$values]; | |
} | |
/** | |
* Retrieves escaped PSEUDO_MULTI_LINE_SEPARATOR if it is metacharacter for regular expression | |
* | |
* @return string | |
*/ | |
private function getMultiLineSeparatorForRegexp() | |
{ | |
if (!$this->multiLineSeparatorForRegexp) { | |
$this->multiLineSeparatorForRegexp = in_array(self::PSEUDO_MULTI_LINE_SEPARATOR, str_split('[\^$.|?*+(){}')) | |
? '\\' . self::PSEUDO_MULTI_LINE_SEPARATOR | |
: self::PSEUDO_MULTI_LINE_SEPARATOR; | |
} | |
return $this->multiLineSeparatorForRegexp; | |
} | |
/** | |
* Set values in use_config_ fields. | |
* | |
* @param array $rowData | |
* | |
* @return array | |
*/ | |
private function _setStockUseConfigFieldsValues($rowData) | |
{ | |
$useConfigFields = []; | |
foreach ($rowData as $key => $value) { | |
$useConfigName = $key === StockItemInterface::ENABLE_QTY_INCREMENTS | |
? StockItemInterface::USE_CONFIG_ENABLE_QTY_INC | |
: self::INVENTORY_USE_CONFIG_PREFIX . $key; | |
if (isset($this->defaultStockData[$key]) | |
&& isset($this->defaultStockData[$useConfigName]) | |
&& !empty($value) | |
&& empty($rowData[$useConfigName]) | |
) { | |
$useConfigFields[$useConfigName] = ($value == self::INVENTORY_USE_CONFIG) ? 1 : 0; | |
} | |
} | |
$rowData = array_merge($rowData, $useConfigFields); | |
return $rowData; | |
} | |
/** | |
* Custom fields mapping for changed purposes of fields and field names. | |
* | |
* @param array $rowData | |
* | |
* @return array | |
*/ | |
private function _customFieldsMapping($rowData) | |
{ | |
foreach ($this->_fieldsMap as $systemFieldName => $fileFieldName) { | |
if (array_key_exists($fileFieldName, $rowData)) { | |
$rowData[$systemFieldName] = $rowData[$fileFieldName]; | |
} | |
} | |
$rowData = $this->_parseAdditionalAttributes($rowData); | |
$rowData = $this->_setStockUseConfigFieldsValues($rowData); | |
if (array_key_exists('status', $rowData) | |
&& $rowData['status'] != \Magento\Catalog\Model\Product\Attribute\Source\Status::STATUS_ENABLED | |
) { | |
if ($rowData['status'] == 'yes') { | |
$rowData['status'] = \Magento\Catalog\Model\Product\Attribute\Source\Status::STATUS_ENABLED; | |
} elseif (!empty($rowData['status']) || $this->getRowScope($rowData) == self::SCOPE_DEFAULT) { | |
$rowData['status'] = \Magento\Catalog\Model\Product\Attribute\Source\Status::STATUS_DISABLED; | |
} | |
} | |
return $rowData; | |
} | |
/** | |
* Validate data rows and save bunches to DB | |
* | |
* @return $this|AbstractEntity | |
*/ | |
protected function _saveValidatedBunches() | |
{ | |
$source = $this->_getSource(); | |
$source->rewind(); | |
while ($source->valid()) { | |
try { | |
$rowData = $source->current(); | |
} catch (\InvalidArgumentException $e) { | |
$this->addRowError($e->getMessage(), $this->_processedRowsCount); | |
$this->_processedRowsCount++; | |
$source->next(); | |
continue; | |
} | |
$rowData = $this->_customFieldsMapping($rowData); | |
$this->validateRow($rowData, $source->key()); | |
$source->next(); | |
} | |
$this->checkUrlKeyDuplicates(); | |
$this->getOptionEntity()->validateAmbiguousData(); | |
return parent::_saveValidatedBunches(); | |
} | |
/** | |
* Check that url_keys are not assigned to other products in DB | |
* | |
* @return void | |
* @since 100.0.3 | |
*/ | |
protected function checkUrlKeyDuplicates() | |
{ | |
$resource = $this->getResource(); | |
foreach ($this->urlKeys as $storeId => $urlKeys) { | |
$urlKeyDuplicates = $this->_connection->fetchAssoc( | |
$this->_connection->select()->from( | |
['url_rewrite' => $resource->getTable('url_rewrite')], | |
['request_path', 'store_id'] | |
)->joinLeft( | |
['cpe' => $resource->getTable('catalog_product_entity')], | |
"cpe.entity_id = url_rewrite.entity_id" | |
)->where('request_path IN (?)', array_keys($urlKeys)) | |
->where('store_id IN (?)', $storeId) | |
->where('cpe.sku not in (?)', array_values($urlKeys)) | |
); | |
foreach ($urlKeyDuplicates as $entityData) { | |
$rowNum = $this->rowNumbers[$entityData['store_id']][$entityData['request_path']]; | |
$message = sprintf( | |
$this->retrieveMessageTemplate(ValidatorInterface::ERROR_DUPLICATE_URL_KEY), | |
$entityData['request_path'], | |
$entityData['sku'] | |
); | |
$this->addRowError(ValidatorInterface::ERROR_DUPLICATE_URL_KEY, $rowNum, 'url_key', $message); | |
} | |
} | |
} | |
/** | |
* Retrieve product rewrite suffix for store | |
* | |
* @param int $storeId | |
* @return string | |
* @since 100.0.3 | |
*/ | |
protected function getProductUrlSuffix($storeId = null) | |
{ | |
if (!isset($this->productUrlSuffix[$storeId])) { | |
$this->productUrlSuffix[$storeId] = $this->scopeConfig->getValue( | |
\Magento\CatalogUrlRewrite\Model\ProductUrlPathGenerator::XML_PATH_PRODUCT_URL_SUFFIX, | |
\Magento\Store\Model\ScopeInterface::SCOPE_STORE, | |
$storeId | |
); | |
} | |
return $this->productUrlSuffix[$storeId]; | |
} | |
/** | |
* Retrieve url key from provided row data. | |
* | |
* @param array $rowData | |
* @return string | |
* | |
* @since 100.0.3 | |
*/ | |
protected function getUrlKey($rowData) | |
{ | |
if (!empty($rowData[self::URL_KEY])) { | |
return $this->productUrl->formatUrlKey($rowData[self::URL_KEY]); | |
} | |
if (!empty($rowData[self::COL_NAME])) { | |
return $this->productUrl->formatUrlKey($rowData[self::COL_NAME]); | |
} | |
return ''; | |
} | |
/** | |
* Retrieve resource. | |
* | |
* @return Proxy\Product\ResourceModel | |
* | |
* @since 100.0.3 | |
*/ | |
protected function getResource() | |
{ | |
if (!$this->_resource) { | |
$this->_resource = $this->_resourceFactory->create(); | |
} | |
return $this->_resource; | |
} | |
/** | |
* Whether a url key is needed to be change. | |
* | |
* @param array $rowData | |
* @return bool | |
*/ | |
private function isNeedToChangeUrlKey(array $rowData): bool | |
{ | |
$urlKey = $this->getUrlKey($rowData); | |
$productExists = $this->isSkuExist($rowData[self::COL_SKU]); | |
$markedToEraseUrlKey = isset($rowData[self::URL_KEY]); | |
// The product isn't new and the url key index wasn't marked for change. | |
if (!$urlKey && $productExists && !$markedToEraseUrlKey) { | |
// Seems there is no need to change the url key | |
return false; | |
} | |
return true; | |
} | |
/** | |
* Get product entity link field | |
* | |
* @return string | |
*/ | |
private function getProductEntityLinkField() | |
{ | |
if (!$this->productEntityLinkField) { | |
$this->productEntityLinkField = $this->getMetadataPool() | |
->getMetadata(\Magento\Catalog\Api\Data\ProductInterface::class) | |
->getLinkField(); | |
} | |
return $this->productEntityLinkField; | |
} | |
/** | |
* Get product entity identifier field | |
* | |
* @return string | |
*/ | |
private function getProductIdentifierField() | |
{ | |
if (!$this->productEntityIdentifierField) { | |
$this->productEntityIdentifierField = $this->getMetadataPool() | |
->getMetadata(\Magento\Catalog\Api\Data\ProductInterface::class) | |
->getIdentifierField(); | |
} | |
return $this->productEntityIdentifierField; | |
} | |
/** | |
* Update media gallery labels | |
* | |
* @param array $labels | |
* @return void | |
*/ | |
private function updateMediaGalleryLabels(array $labels) | |
{ | |
if (!empty($labels)) { | |
$this->mediaProcessor->updateMediaGalleryLabels($labels); | |
} | |
} | |
/** | |
* Update 'disabled' field for media gallery entity | |
* | |
* @param array $images | |
* @return $this | |
*/ | |
private function updateMediaGalleryVisibility(array $images) | |
{ | |
if (!empty($images)) { | |
$this->mediaProcessor->updateMediaGalleryVisibility($images); | |
} | |
return $this; | |
} | |
/** | |
* Parse values from multiple attributes fields | |
* | |
* @param string $labelRow | |
* @return array | |
*/ | |
private function parseMultipleValues($labelRow) | |
{ | |
return $this->parseMultiselectValues( | |
$labelRow, | |
$this->getMultipleValueSeparator() | |
); | |
} | |
/** | |
* Check if product exists for specified SKU | |
* | |
* @param string $sku | |
* @return bool | |
*/ | |
private function isSkuExist($sku) | |
{ | |
$sku = strtolower($sku); | |
return isset($this->_oldSku[$sku]); | |
} | |
/** | |
* Get existing product data for specified SKU | |
* | |
* @param string $sku | |
* @return array | |
*/ | |
private function getExistingSku($sku) | |
{ | |
return $this->_oldSku[strtolower($sku)]; | |
} | |
/** | |
* Format row data to DB compatible values. | |
* | |
* @param array $rowData | |
* @return array | |
*/ | |
private function formatStockDataForRow(array $rowData): array | |
{ | |
$sku = $rowData[self::COL_SKU]; | |
$row['product_id'] = $this->skuProcessor->getNewSku($sku)['entity_id']; | |
$row['website_id'] = $this->stockConfiguration->getDefaultScopeId(); | |
$row['stock_id'] = $this->stockRegistry->getStock($row['website_id'])->getStockId(); | |
$stockItemDo = $this->stockRegistry->getStockItem($row['product_id'], $row['website_id']); | |
$existStockData = $stockItemDo->getData(); | |
$row = array_merge( | |
$this->defaultStockData, | |
array_intersect_key($existStockData, $this->defaultStockData), | |
array_intersect_key($rowData, $this->defaultStockData), | |
$row | |
); | |
if ($this->stockConfiguration->isQty($this->skuProcessor->getNewSku($sku)['type_id'])) { | |
$stockItemDo->setData($row); | |
$row['is_in_stock'] = $row['is_in_stock'] ?? $this->stockStateProvider->verifyStock($stockItemDo); | |
if ($this->stockStateProvider->verifyNotification($stockItemDo)) { | |
$date = $this->dateTimeFactory->create('now', new \DateTimeZone('UTC')); | |
$row['low_stock_date'] = $date->format(DateTime::DATETIME_PHP_FORMAT); | |
} | |
$row['stock_status_changed_auto'] = (int)!$this->stockStateProvider->verifyStock($stockItemDo); | |
} else { | |
$row['qty'] = 0; | |
} | |
return $row; | |
} | |
/** | |
* Retrieve product by sku. | |
* | |
* @param string $sku | |
* @return \Magento\Catalog\Api\Data\ProductInterface|null | |
*/ | |
private function retrieveProductBySku($sku) | |
{ | |
try { | |
$product = $this->productRepository->get($sku); | |
} catch (NoSuchEntityException $e) { | |
return null; | |
} | |
return $product; | |
} | |
/** | |
* Add row as skipped | |
* | |
* @param int $rowNum | |
* @param string $errorCode Error code or simply column name | |
* @param string $errorLevel error level | |
* @param string|null $colName optional column name | |
* @return $this | |
*/ | |
private function skipRow( | |
$rowNum, | |
string $errorCode, | |
string $errorLevel = ProcessingError::ERROR_LEVEL_NOT_CRITICAL, | |
$colName = null | |
): self { | |
$this->addRowError($errorCode, $rowNum, $colName, null, $errorLevel); | |
$this->getErrorAggregator() | |
->addRowToSkip($rowNum); | |
return $this; | |
} | |
/** | |
* Returns errorLevel for validation | |
* | |
* @param string $sku | |
* @return string | |
*/ | |
private function getValidationErrorLevel($sku): string | |
{ | |
return (!$this->isSkuExist($sku) && Import::BEHAVIOR_REPLACE !== $this->getBehavior()) | |
? ProcessingError::ERROR_LEVEL_CRITICAL | |
: ProcessingError::ERROR_LEVEL_NOT_CRITICAL; | |
} | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
It's 100% perfect.
copy/past and works. You are a chef thank you VERY much !