Magento 2 Documentation  2.3
Documentation for Magento 2 CMS v2.3 (December 2018)
Product.php
Go to the documentation of this file.
1 <?php
7 
28 
39 class Product extends \Magento\ImportExport\Model\Import\Entity\AbstractEntity
40 {
41  const CONFIG_KEY_PRODUCT_TYPES = 'global/importexport/import_product_types';
42 
46  const BUNCH_SIZE = 20;
47 
51  const ATTRIBUTE_DELETE_BUNCH = 1000;
52 
59 
64 
68  const VALUE_ALL = 'all';
69 
73  const SCOPE_DEFAULT = 1;
74 
75  const SCOPE_WEBSITE = 2;
76 
77  const SCOPE_STORE = 0;
78 
79  const SCOPE_NULL = -1;
80 
91  const COL_STORE = '_store';
92 
96  const COL_STORE_VIEW_CODE = 'store_view_code';
97 
101  const COL_WEBSITE = 'website_code';
102 
106  const COL_ATTR_SET = '_attribute_set';
107 
111  const COL_TYPE = 'product_type';
112 
116  const COL_CATEGORY = 'categories';
117 
121  const COL_VISIBILITY = 'visibility';
122 
126  const COL_SKU = 'sku';
127 
131  const COL_NAME = 'name';
132 
136  const COL_PRODUCT_WEBSITES = '_product_websites';
137 
141  const MEDIA_GALLERY_ATTRIBUTE_CODE = 'media_gallery';
142 
146  const COL_MEDIA_IMAGE = '_media_image';
147 
151  const INVENTORY_USE_CONFIG = 'Use Config';
152 
156  const INVENTORY_USE_CONFIG_PREFIX = 'use_config_';
157 
161  const URL_KEY = 'url_key';
162 
168  protected $_attributeCache = [];
169 
175  protected $_attrSetIdToName = [];
176 
182  protected $_attrSetNameToId = [];
183 
189 
200 
206 
213  'status',
214  'tax_class_id',
215  ];
216 
222  protected $_linkNameToId = [
223  '_related_' => \Magento\Catalog\Model\Product\Link::LINK_TYPE_RELATED,
224  '_crosssell_' => \Magento\Catalog\Model\Product\Link::LINK_TYPE_CROSSSELL,
225  '_upsell_' => \Magento\Catalog\Model\Product\Link::LINK_TYPE_UPSELL,
226  ];
227 
234  protected $dateAttrCodes = [
235  'special_from_date',
236  'special_to_date',
237  'news_from_date',
238  'news_to_date',
239  'custom_design_from',
240  'custom_design_to'
241  ];
242 
248  protected $logInHistory = true;
249 
255  protected $_mediaGalleryAttributeId = null;
256 
263  protected $_messageTemplates = [
264  ValidatorInterface::ERROR_INVALID_SCOPE => 'Invalid value in Scope column',
265  ValidatorInterface::ERROR_INVALID_WEBSITE => 'Invalid value in Website column (website does not exist?)',
266  ValidatorInterface::ERROR_INVALID_STORE => 'Invalid value in Store column (store doesn\'t exist?)',
267  ValidatorInterface::ERROR_INVALID_ATTR_SET => 'Invalid value for Attribute Set column (set doesn\'t exist?)',
268  ValidatorInterface::ERROR_INVALID_TYPE => 'Product Type is invalid or not supported',
269  ValidatorInterface::ERROR_INVALID_CATEGORY => 'Category does not exist',
270  ValidatorInterface::ERROR_VALUE_IS_REQUIRED => 'Please make sure attribute "%s" is not empty.',
271  ValidatorInterface::ERROR_TYPE_CHANGED => 'Trying to change type of existing products',
272  ValidatorInterface::ERROR_SKU_IS_EMPTY => 'SKU is empty',
273  ValidatorInterface::ERROR_NO_DEFAULT_ROW => 'Default values row does not exist',
274  ValidatorInterface::ERROR_CHANGE_TYPE => 'Product type change is not allowed',
275  ValidatorInterface::ERROR_DUPLICATE_SCOPE => 'Duplicate scope',
276  ValidatorInterface::ERROR_DUPLICATE_SKU => 'Duplicate SKU',
277  ValidatorInterface::ERROR_CHANGE_ATTR_SET => 'Attribute set change is not allowed',
278  ValidatorInterface::ERROR_TYPE_UNSUPPORTED => 'Product type is not supported',
279  ValidatorInterface::ERROR_ROW_IS_ORPHAN => 'Orphan rows that will be skipped due default row errors',
280  ValidatorInterface::ERROR_INVALID_TIER_PRICE_QTY => 'Tier Price data price or quantity value is invalid',
281  ValidatorInterface::ERROR_INVALID_TIER_PRICE_SITE => 'Tier Price data website is invalid',
282  ValidatorInterface::ERROR_INVALID_TIER_PRICE_GROUP => 'Tier Price customer group ID is invalid',
283  ValidatorInterface::ERROR_TIER_DATA_INCOMPLETE => 'Tier Price data is incomplete',
284  ValidatorInterface::ERROR_SKU_NOT_FOUND_FOR_DELETE => 'Product with specified SKU not found',
285  ValidatorInterface::ERROR_SUPER_PRODUCTS_SKU_NOT_FOUND => 'Product with specified super products SKU not found',
286  ValidatorInterface::ERROR_MEDIA_DATA_INCOMPLETE => 'Media data is incomplete',
287  ValidatorInterface::ERROR_EXCEEDED_MAX_LENGTH => 'Attribute %s exceeded max length',
288  ValidatorInterface::ERROR_INVALID_ATTRIBUTE_TYPE => 'Value for \'%s\' attribute contains incorrect value',
289  ValidatorInterface::ERROR_ABSENT_REQUIRED_ATTRIBUTE => 'Attribute %s is required',
290  ValidatorInterface::ERROR_INVALID_ATTRIBUTE_OPTION => 'Value for \'%s\' attribute contains incorrect value, see acceptable values on settings specified for Admin',
291  ValidatorInterface::ERROR_DUPLICATE_UNIQUE_ATTRIBUTE => 'Duplicated unique attribute',
292  ValidatorInterface::ERROR_INVALID_VARIATIONS_CUSTOM_OPTIONS => 'Value for \'%s\' sub attribute in \'%s\' attribute contains incorrect value, acceptable values are: \'dropdown\', \'checkbox\', \'radio\', \'text\'',
293  ValidatorInterface::ERROR_INVALID_MEDIA_URL_OR_PATH => 'Wrong URL/path used for attribute %s',
294  ValidatorInterface::ERROR_MEDIA_PATH_NOT_ACCESSIBLE => 'Imported resource (image) does not exist in the local media storage',
295  ValidatorInterface::ERROR_MEDIA_URL_NOT_ACCESSIBLE => 'Imported resource (image) could not be downloaded from external resource due to timeout or access permissions',
296  ValidatorInterface::ERROR_INVALID_WEIGHT => 'Product weight is invalid',
297  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',
298  ValidatorInterface::ERROR_DUPLICATE_MULTISELECT_VALUES => "Value for multiselect attribute %s contains duplicated values",
299  ];
300  //@codingStandardsIgnoreEnd
301 
307  protected $_fieldsMap = [
308  'image' => 'base_image',
309  'image_label' => "base_image_label",
310  'thumbnail' => 'thumbnail_image',
311  'thumbnail_label' => 'thumbnail_image_label',
312  self::COL_MEDIA_IMAGE => 'additional_images',
313  '_media_image_label' => 'additional_image_labels',
314  '_media_is_disabled' => 'hide_from_product_page',
315  Product::COL_STORE => 'store_view_code',
316  Product::COL_ATTR_SET => 'attribute_set_code',
317  Product::COL_TYPE => 'product_type',
318  Product::COL_PRODUCT_WEBSITES => 'product_websites',
319  'status' => 'product_online',
320  'news_from_date' => 'new_from_date',
321  'news_to_date' => 'new_to_date',
322  'options_container' => 'display_product_options_in',
323  'minimal_price' => 'map_price',
324  'msrp' => 'msrp_price',
325  'msrp_enabled' => 'map_enabled',
326  'special_from_date' => 'special_price_from_date',
327  'special_to_date' => 'special_price_to_date',
328  'min_qty' => 'out_of_stock_qty',
329  'backorders' => 'allow_backorders',
330  'min_sale_qty' => 'min_cart_qty',
331  'max_sale_qty' => 'max_cart_qty',
332  'notify_stock_qty' => 'notify_on_stock_below',
333  '_related_sku' => 'related_skus',
334  '_related_position' => 'related_position',
335  '_crosssell_sku' => 'crosssell_skus',
336  '_crosssell_position' => 'crosssell_position',
337  '_upsell_sku' => 'upsell_skus',
338  '_upsell_position' => 'upsell_position',
339  'meta_keyword' => 'meta_keywords',
340  ];
341 
354  protected $_oldSku = [];
355 
361  protected $_specialAttributes = [
366  '_product_websites',
368  '_tier_price_website',
369  '_tier_price_customer_group',
370  '_tier_price_qty',
371  '_tier_price_price',
372  '_related_sku',
373  '_related_position',
374  '_crosssell_sku',
375  '_crosssell_position',
376  '_upsell_sku',
377  '_upsell_position',
378  '_custom_option_store',
379  '_custom_option_type',
380  '_custom_option_title',
381  '_custom_option_is_required',
382  '_custom_option_price',
383  '_custom_option_sku',
384  '_custom_option_max_characters',
385  '_custom_option_sort_order',
386  '_custom_option_file_extension',
387  '_custom_option_image_size_x',
388  '_custom_option_image_size_y',
389  '_custom_option_row_title',
390  '_custom_option_row_price',
391  '_custom_option_row_sku',
392  '_custom_option_row_sort',
393  '_media_attribute_id',
395  '_media_label',
396  '_media_position',
397  '_media_is_disabled',
398  ];
399 
403  protected $defaultStockData = [
404  'manage_stock' => 1,
405  'use_config_manage_stock' => 1,
406  'qty' => 0,
407  'min_qty' => 0,
408  'use_config_min_qty' => 1,
409  'min_sale_qty' => 1,
410  'use_config_min_sale_qty' => 1,
411  'max_sale_qty' => 10000,
412  'use_config_max_sale_qty' => 1,
413  'is_qty_decimal' => 0,
414  'backorders' => 0,
415  'use_config_backorders' => 1,
416  'notify_stock_qty' => 1,
417  'use_config_notify_stock_qty' => 1,
418  'enable_qty_increments' => 0,
419  'use_config_enable_qty_inc' => 1,
420  'qty_increments' => 0,
421  'use_config_qty_increments' => 1,
422  'is_in_stock' => 1,
423  'low_stock_date' => null,
424  'stock_status_changed_auto' => 0,
425  'is_decimal_divided' => 0,
426  ];
427 
436  protected $_imagesArrayKeys = [];
437 
444 
450  protected $_productTypeModels = [];
451 
457  protected $_fileUploader;
458 
464  protected $_optionEntity;
465 
471  protected $_catalogData = null;
472 
476  protected $stockRegistry;
477 
482 
487 
493  protected $_eventManager = null;
494 
498  protected $_importConfig;
499 
503  protected $_resourceFactory;
504 
508  protected $_resource;
509 
513  protected $_setColFactory;
514 
519 
523  protected $_linkFactory;
524 
529 
533  protected $_uploaderFactory;
534 
538  protected $_mediaDirectory;
539 
544  protected $_stockResItemFac;
545 
549  protected $_localeDate;
550 
554  protected $dateTime;
555 
559  protected $indexerRegistry;
560 
564  protected $storeResolver;
565 
569  protected $skuProcessor;
570 
575 
580  protected $scopeConfig;
581 
586  protected $productUrl;
587 
591  protected $websitesCache = [];
592 
596  protected $categoriesCache = [];
597 
602  protected $productUrlSuffix = [];
603 
609  protected $productUrlKeys = [];
610 
617 
621  protected $validator;
622 
628  protected $validatedRows;
629 
633  private $_logger;
634 
638  protected $masterAttributeCode = 'sku';
639 
644 
649 
655  protected $_replaceFlag = null;
656 
662  protected $cachedImages = null;
663 
668  protected $urlKeys = [];
669 
674  protected $rowNumbers = [];
675 
681  private $productEntityLinkField;
682 
688  private $productEntityIdentifierField;
689 
695  private $multiLineSeparatorForRegexp;
696 
702  private $filesystem;
703 
709  private $catalogConfig;
710 
716  private $stockItemImporter;
717 
721  private $imageTypeProcessor;
722 
728  private $mediaProcessor;
729 
733  private $dateTimeFactory;
734 
781  public function __construct(
782  \Magento\Framework\Json\Helper\Data $jsonHelper,
783  \Magento\ImportExport\Helper\Data $importExportData,
784  \Magento\ImportExport\Model\ResourceModel\Import\Data $importData,
785  \Magento\Eav\Model\Config $config,
787  \Magento\ImportExport\Model\ResourceModel\Helper $resourceHelper,
788  \Magento\Framework\Stdlib\StringUtils $string,
790  \Magento\Framework\Event\ManagerInterface $eventManager,
791  \Magento\CatalogInventory\Api\StockRegistryInterface $stockRegistry,
793  \Magento\CatalogInventory\Model\Spi\StockStateProviderInterface $stockStateProvider,
794  \Magento\Catalog\Helper\Data $catalogData,
795  \Magento\ImportExport\Model\Import\Config $importConfig,
796  \Magento\CatalogImportExport\Model\Import\Proxy\Product\ResourceModelFactory $resourceFactory,
797  \Magento\CatalogImportExport\Model\Import\Product\OptionFactory $optionFactory,
798  \Magento\Eav\Model\ResourceModel\Entity\Attribute\Set\CollectionFactory $setColFactory,
799  \Magento\CatalogImportExport\Model\Import\Product\Type\Factory $productTypeFactory,
800  \Magento\Catalog\Model\ResourceModel\Product\LinkFactory $linkFactory,
801  \Magento\CatalogImportExport\Model\Import\Proxy\ProductFactory $proxyProdFactory,
802  \Magento\CatalogImportExport\Model\Import\UploaderFactory $uploaderFactory,
803  \Magento\Framework\Filesystem $filesystem,
804  \Magento\CatalogInventory\Model\ResourceModel\Stock\ItemFactory $stockResItemFac,
805  \Magento\Framework\Stdlib\DateTime\TimezoneInterface $localeDate,
807  \Psr\Log\LoggerInterface $logger,
808  \Magento\Framework\Indexer\IndexerRegistry $indexerRegistry,
809  Product\StoreResolver $storeResolver,
810  Product\SkuProcessor $skuProcessor,
811  Product\CategoryProcessor $categoryProcessor,
815  Product\TaxClassProcessor $taxClassProcessor,
817  \Magento\Catalog\Model\Product\Url $productUrl,
818  array $data = [],
819  array $dateAttrCodes = [],
820  CatalogConfig $catalogConfig = null,
821  ImageTypeProcessor $imageTypeProcessor = null,
822  MediaGalleryProcessor $mediaProcessor = null,
823  StockItemImporterInterface $stockItemImporter = null,
824  DateTimeFactory $dateTimeFactory = null
825  ) {
826  $this->_eventManager = $eventManager;
827  $this->stockRegistry = $stockRegistry;
828  $this->stockConfiguration = $stockConfiguration;
829  $this->stockStateProvider = $stockStateProvider;
830  $this->_catalogData = $catalogData;
831  $this->_importConfig = $importConfig;
832  $this->_resourceFactory = $resourceFactory;
833  $this->_setColFactory = $setColFactory;
834  $this->_productTypeFactory = $productTypeFactory;
835  $this->_linkFactory = $linkFactory;
836  $this->_proxyProdFactory = $proxyProdFactory;
837  $this->_uploaderFactory = $uploaderFactory;
838  $this->filesystem = $filesystem;
839  $this->_mediaDirectory = $filesystem->getDirectoryWrite(DirectoryList::ROOT);
840  $this->_stockResItemFac = $stockResItemFac;
841  $this->_localeDate = $localeDate;
842  $this->dateTime = $dateTime;
843  $this->indexerRegistry = $indexerRegistry;
844  $this->_logger = $logger;
845  $this->storeResolver = $storeResolver;
846  $this->skuProcessor = $skuProcessor;
847  $this->categoryProcessor = $categoryProcessor;
848  $this->validator = $validator;
849  $this->objectRelationProcessor = $objectRelationProcessor;
850  $this->transactionManager = $transactionManager;
851  $this->taxClassProcessor = $taxClassProcessor;
852  $this->scopeConfig = $scopeConfig;
853  $this->productUrl = $productUrl;
854  $this->dateAttrCodes = array_merge($this->dateAttrCodes, $dateAttrCodes);
855  $this->catalogConfig = $catalogConfig ?: ObjectManager::getInstance()->get(CatalogConfig::class);
856  $this->imageTypeProcessor = $imageTypeProcessor ?: ObjectManager::getInstance()->get(ImageTypeProcessor::class);
857  $this->mediaProcessor = $mediaProcessor ?: ObjectManager::getInstance()->get(MediaGalleryProcessor::class);
858  $this->stockItemImporter = $stockItemImporter ?: ObjectManager::getInstance()
859  ->get(StockItemImporterInterface::class);
860  parent::__construct(
861  $jsonHelper,
862  $importExportData,
863  $importData,
864  $config,
865  $resource,
866  $resourceHelper,
867  $string,
869  );
870  $this->_optionEntity = $data['option_entity'] ??
871  $optionFactory->create(['data' => ['product_entity' => $this]]);
872  $this->_initAttributeSets()
873  ->_initTypeModels()
874  ->_initSkus()
875  ->initImagesArrayKeys();
876  $this->validator->init($this);
877  $this->dateTimeFactory = $dateTimeFactory ?? ObjectManager::getInstance()->get(DateTimeFactory::class);
878  }
879 
889  public function isAttributeValid($attrCode, array $attrParams, array $rowData, $rowNum)
890  {
891  if (!$this->validator->isAttributeValid($attrCode, $attrParams, $rowData)) {
892  foreach ($this->validator->getMessages() as $message) {
893  $this->addRowError($message, $rowNum, $attrCode);
894  }
895  return false;
896  }
897  return true;
898  }
899 
905  public function getMultipleValueSeparator()
906  {
907  if (!empty($this->_parameters[Import::FIELD_FIELD_MULTIPLE_VALUE_SEPARATOR])) {
908  return $this->_parameters[Import::FIELD_FIELD_MULTIPLE_VALUE_SEPARATOR];
909  }
911  }
912 
919  {
920  if (!empty($this->_parameters[Import::FIELD_EMPTY_ATTRIBUTE_VALUE_CONSTANT])) {
921  return $this->_parameters[Import::FIELD_EMPTY_ATTRIBUTE_VALUE_CONSTANT];
922  }
924  }
925 
931  public function getOptionEntity()
932  {
933  return $this->_optionEntity;
934  }
935 
941  public function getMediaGalleryAttributeId()
942  {
943  if (!$this->_mediaGalleryAttributeId) {
945  $resource = $this->_resourceFactory->create();
946  $this->_mediaGalleryAttributeId = $resource->getAttribute(self::MEDIA_GALLERY_ATTRIBUTE_CODE)->getId();
947  }
949  }
950 
958  {
959  if (isset($this->_productTypeModels[$name])) {
960  return $this->_productTypeModels[$name];
961  }
962  return null;
963  }
964 
971  public function setParameters(array $params)
972  {
973  parent::setParameters($params);
974  $this->getOptionEntity()->setParameters($params);
975 
976  return $this;
977  }
978 
985  {
986  $this->setParameters(array_merge(
987  $this->getParameters(),
988  ['behavior' => Import::BEHAVIOR_DELETE]
989  ));
990  $this->_deleteProducts();
991 
992  return $this;
993  }
994 
1001  protected function _deleteProducts()
1002  {
1003  $productEntityTable = $this->_resourceFactory->create()->getEntityTable();
1004 
1005  while ($bunch = $this->_dataSourceModel->getNextBunch()) {
1006  $idsToDelete = [];
1007 
1008  foreach ($bunch as $rowNum => $rowData) {
1009  if ($this->validateRow($rowData, $rowNum) && self::SCOPE_DEFAULT == $this->getRowScope($rowData)) {
1010  $idsToDelete[] = $this->getExistingSku($rowData[self::COL_SKU])['entity_id'];
1011  }
1012  }
1013  if ($idsToDelete) {
1014  $this->countItemsDeleted += count($idsToDelete);
1015  $this->transactionManager->start($this->_connection);
1016  try {
1017  $this->objectRelationProcessor->delete(
1018  $this->transactionManager,
1019  $this->_connection,
1020  $productEntityTable,
1021  $this->_connection->quoteInto('entity_id IN (?)', $idsToDelete),
1022  ['entity_id' => $idsToDelete]
1023  );
1024  $this->_eventManager->dispatch(
1025  'catalog_product_import_bunch_delete_commit_before',
1026  [
1027  'adapter' => $this,
1028  'bunch' => $bunch,
1029  'ids_to_delete' => $idsToDelete,
1030  ]
1031  );
1032  $this->transactionManager->commit();
1033  } catch (\Exception $e) {
1034  $this->transactionManager->rollBack();
1035  throw $e;
1036  }
1037  $this->_eventManager->dispatch(
1038  'catalog_product_import_bunch_delete_after',
1039  ['adapter' => $this, 'bunch' => $bunch]
1040  );
1041  }
1042  }
1043  return $this;
1044  }
1045 
1053  protected function _importData()
1054  {
1055  $this->_validatedRows = null;
1056  if (Import::BEHAVIOR_DELETE == $this->getBehavior()) {
1057  $this->_deleteProducts();
1058  } elseif (Import::BEHAVIOR_REPLACE == $this->getBehavior()) {
1059  $this->_replaceFlag = true;
1060  $this->_replaceProducts();
1061  } else {
1062  $this->_saveProductsData();
1063  }
1064  $this->_eventManager->dispatch('catalog_product_import_finish_before', ['adapter' => $this]);
1065  return true;
1066  }
1067 
1073  protected function _replaceProducts()
1074  {
1076  $this->_oldSku = $this->skuProcessor->reloadOldSkus()->getOldSkus();
1077  $this->_validatedRows = null;
1078  $this->setParameters(array_merge(
1079  $this->getParameters(),
1080  ['behavior' => Import::BEHAVIOR_APPEND]
1081  ));
1082  $this->_saveProductsData();
1083 
1084  return $this;
1085  }
1086 
1092  protected function _saveProductsData()
1093  {
1094  $this->_saveProducts();
1095  foreach ($this->_productTypeModels as $productTypeModel) {
1096  $productTypeModel->saveData();
1097  }
1098  $this->_saveLinks();
1099  $this->_saveStockItem();
1100  if ($this->_replaceFlag) {
1101  $this->getOptionEntity()->clearProductsSkuToId();
1102  }
1103  $this->getOptionEntity()->importData();
1104 
1105  return $this;
1106  }
1107 
1113  protected function _initAttributeSets()
1114  {
1115  foreach ($this->_setColFactory->create()->setEntityTypeFilter($this->_entityTypeId) as $attributeSet) {
1116  $this->_attrSetNameToId[$attributeSet->getAttributeSetName()] = $attributeSet->getId();
1117  $this->_attrSetIdToName[$attributeSet->getId()] = $attributeSet->getAttributeSetName();
1118  }
1119  return $this;
1120  }
1121 
1127  protected function _initSkus()
1128  {
1129  $this->skuProcessor->setTypeModels($this->_productTypeModels);
1130  $this->_oldSku = $this->skuProcessor->reloadOldSkus()->getOldSkus();
1131  return $this;
1132  }
1133 
1139  private function initImagesArrayKeys()
1140  {
1141  $this->_imagesArrayKeys = $this->imageTypeProcessor->getImageTypes();
1142  return $this;
1143  }
1144 
1151  protected function _initTypeModels()
1152  {
1153  $productTypes = $this->_importConfig->getEntityTypes($this->getEntityTypeCode());
1154  foreach ($productTypes as $productTypeName => $productTypeConfig) {
1155  $params = [$this, $productTypeName];
1156  if (!($model = $this->_productTypeFactory->create($productTypeConfig['model'], ['params' => $params]))
1157  ) {
1158  throw new LocalizedException(
1159  __('Entity type model \'%1\' is not found', $productTypeConfig['model'])
1160  );
1161  }
1162  if (!$model instanceof \Magento\CatalogImportExport\Model\Import\Product\Type\AbstractType) {
1163  throw new LocalizedException(
1164  __(
1165  'Entity type model must be an instance of '
1166  . \Magento\CatalogImportExport\Model\Import\Product\Type\AbstractType::class
1167  )
1168  );
1169  }
1170  if ($model->isSuitable()) {
1171  $this->_productTypeModels[$productTypeName] = $model;
1172  }
1173  $this->_fieldsMap = array_merge($this->_fieldsMap, $model->getCustomFieldsMapping());
1174  $this->_specialAttributes = array_merge($this->_specialAttributes, $model->getParticularAttributes());
1175  }
1176  $this->_initErrorTemplates();
1177  // remove doubles
1178  $this->_specialAttributes = array_unique($this->_specialAttributes);
1179 
1180  return $this;
1181  }
1182 
1186  protected function _initErrorTemplates()
1187  {
1188  foreach ($this->_messageTemplates as $errorCode => $template) {
1189  $this->addMessageTemplate($errorCode, $template);
1190  }
1191  }
1192 
1202  protected function _prepareRowForDb(array $rowData)
1203  {
1204  $rowData = $this->_customFieldsMapping($rowData);
1205 
1206  $rowData = parent::_prepareRowForDb($rowData);
1207 
1208  static $lastSku = null;
1209 
1210  if (Import::BEHAVIOR_DELETE == $this->getBehavior()) {
1211  return $rowData;
1212  }
1213 
1214  $lastSku = $rowData[self::COL_SKU];
1215 
1216  if ($this->isSkuExist($lastSku)) {
1217  $newSku = $this->skuProcessor->getNewSku($lastSku);
1218  $rowData[self::COL_ATTR_SET] = $newSku['attr_set_code'];
1219  $rowData[self::COL_TYPE] = $newSku['type_id'];
1220  }
1221 
1222  return $rowData;
1223  }
1224 
1235  protected function _saveLinks()
1236  {
1237  $resource = $this->_linkFactory->create();
1238  $mainTable = $resource->getMainTable();
1239  $positionAttrId = [];
1240  $nextLinkId = $this->_resourceHelper->getNextAutoincrement($mainTable);
1241 
1242  // pre-load 'position' attributes ID for each link type once
1243  foreach ($this->_linkNameToId as $linkName => $linkId) {
1244  $select = $this->_connection->select()->from(
1245  $resource->getTable('catalog_product_link_attribute'),
1246  ['id' => 'product_link_attribute_id']
1247  )->where(
1248  'link_type_id = :link_id AND product_link_attribute_code = :position'
1249  );
1250  $bind = [':link_id' => $linkId, ':position' => 'position'];
1251  $positionAttrId[$linkId] = $this->_connection->fetchOne($select, $bind);
1252  }
1253  while ($bunch = $this->_dataSourceModel->getNextBunch()) {
1254  $productIds = [];
1255  $linkRows = [];
1256  $positionRows = [];
1257 
1258  foreach ($bunch as $rowNum => $rowData) {
1259  if (!$this->isRowAllowedToImport($rowData, $rowNum)) {
1260  continue;
1261  }
1262 
1263  $sku = $rowData[self::COL_SKU];
1264 
1265  $productId = $this->skuProcessor->getNewSku($sku)[$this->getProductEntityLinkField()];
1266  $productLinkKeys = [];
1267  $select = $this->_connection->select()->from(
1268  $resource->getTable('catalog_product_link'),
1269  ['id' => 'link_id', 'linked_id' => 'linked_product_id', 'link_type_id' => 'link_type_id']
1270  )->where(
1271  'product_id = :product_id'
1272  );
1273  $bind = [':product_id' => $productId];
1274  foreach ($this->_connection->fetchAll($select, $bind) as $linkData) {
1275  $linkKey = "{$productId}-{$linkData['linked_id']}-{$linkData['link_type_id']}";
1276  $productLinkKeys[$linkKey] = $linkData['id'];
1277  }
1278  foreach ($this->_linkNameToId as $linkName => $linkId) {
1279  $productIds[] = $productId;
1280  if (isset($rowData[$linkName . 'sku'])) {
1281  $linkSkus = explode($this->getMultipleValueSeparator(), $rowData[$linkName . 'sku']);
1282  $linkPositions = !empty($rowData[$linkName . 'position'])
1283  ? explode($this->getMultipleValueSeparator(), $rowData[$linkName . 'position'])
1284  : [];
1285  foreach ($linkSkus as $linkedKey => $linkedSku) {
1286  $linkedSku = trim($linkedSku);
1287  if (($this->skuProcessor->getNewSku($linkedSku) !== null || $this->isSkuExist($linkedSku))
1288  && strcasecmp($linkedSku, $sku) !== 0
1289  ) {
1290  $newSku = $this->skuProcessor->getNewSku($linkedSku);
1291  if (!empty($newSku)) {
1292  $linkedId = $newSku['entity_id'];
1293  } else {
1294  $linkedId = $this->getExistingSku($linkedSku)['entity_id'];
1295  }
1296 
1297  if ($linkedId == null) {
1298  // Import file links to a SKU which is skipped for some reason,
1299  // which leads to a "NULL"
1300  // link causing fatal errors.
1301  $this->_logger->critical(
1302  new \Exception(
1303  sprintf(
1304  'WARNING: Orphaned link skipped: From SKU %s (ID %d) to SKU %s, ' .
1305  'Link type id: %d',
1306  $sku,
1307  $productId,
1308  $linkedSku,
1309  $linkId
1310  )
1311  )
1312  );
1313  continue;
1314  }
1315 
1316  $linkKey = "{$productId}-{$linkedId}-{$linkId}";
1317  if (empty($productLinkKeys[$linkKey])) {
1318  $productLinkKeys[$linkKey] = $nextLinkId;
1319  }
1320  if (!isset($linkRows[$linkKey])) {
1321  $linkRows[$linkKey] = [
1322  'link_id' => $productLinkKeys[$linkKey],
1323  'product_id' => $productId,
1324  'linked_product_id' => $linkedId,
1325  'link_type_id' => $linkId,
1326  ];
1327  }
1328  if (!empty($linkPositions[$linkedKey])) {
1329  $positionRows[] = [
1330  'link_id' => $productLinkKeys[$linkKey],
1331  'product_link_attribute_id' => $positionAttrId[$linkId],
1332  'value' => $linkPositions[$linkedKey],
1333  ];
1334  }
1335  $nextLinkId++;
1336  }
1337  }
1338  }
1339  }
1340  }
1341  if (Import::BEHAVIOR_APPEND != $this->getBehavior() && $productIds) {
1342  $this->_connection->delete(
1343  $mainTable,
1344  $this->_connection->quoteInto('product_id IN (?)', array_unique($productIds))
1345  );
1346  }
1347  if ($linkRows) {
1348  $this->_connection->insertOnDuplicate($mainTable, $linkRows, ['link_id']);
1349  }
1350  if ($positionRows) {
1351  // process linked product positions
1352  $this->_connection->insertOnDuplicate(
1353  $resource->getAttributeTypeTable('int'),
1354  $positionRows,
1355  ['value']
1356  );
1357  }
1358  }
1359  return $this;
1360  }
1361 
1368  protected function _saveProductAttributes(array $attributesData)
1369  {
1370  $linkField = $this->getProductEntityLinkField();
1371  foreach ($attributesData as $tableName => $skuData) {
1372  $tableData = [];
1373  foreach ($skuData as $sku => $attributes) {
1374  $linkId = $this->_oldSku[strtolower($sku)][$linkField];
1375  foreach ($attributes as $attributeId => $storeValues) {
1376  foreach ($storeValues as $storeId => $storeValue) {
1377  $tableData[] = [
1378  $linkField => $linkId,
1379  'attribute_id' => $attributeId,
1380  'store_id' => $storeId,
1381  'value' => $storeValue,
1382  ];
1383  }
1384  }
1385  }
1386  $this->_connection->insertOnDuplicate($tableName, $tableData, ['value']);
1387  }
1388 
1389  return $this;
1390  }
1391 
1398  protected function _saveProductCategories(array $categoriesData)
1399  {
1400  static $tableName = null;
1401 
1402  if (!$tableName) {
1403  $tableName = $this->_resourceFactory->create()->getProductCategoryTable();
1404  }
1405  if ($categoriesData) {
1406  $categoriesIn = [];
1407  $delProductId = [];
1408 
1409  foreach ($categoriesData as $delSku => $categories) {
1410  $productId = $this->skuProcessor->getNewSku($delSku)['entity_id'];
1411  $delProductId[] = $productId;
1412 
1413  foreach (array_keys($categories) as $categoryId) {
1414  $categoriesIn[] = ['product_id' => $productId, 'category_id' => $categoryId, 'position' => 1];
1415  }
1416  }
1417  if (Import::BEHAVIOR_APPEND != $this->getBehavior()) {
1418  $this->_connection->delete(
1419  $tableName,
1420  $this->_connection->quoteInto('product_id IN (?)', $delProductId)
1421  );
1422  }
1423  if ($categoriesIn) {
1424  $this->_connection->insertOnDuplicate($tableName, $categoriesIn, ['product_id', 'category_id']);
1425  }
1426  }
1427  return $this;
1428  }
1429 
1438  public function saveProductEntity(array $entityRowsIn, array $entityRowsUp)
1439  {
1440  static $entityTable = null;
1441  $this->countItemsCreated += count($entityRowsIn);
1442  $this->countItemsUpdated += count($entityRowsUp);
1443 
1444  if (!$entityTable) {
1445  $entityTable = $this->_resourceFactory->create()->getEntityTable();
1446  }
1447  if ($entityRowsUp) {
1448  $this->_connection->insertOnDuplicate($entityTable, $entityRowsUp, ['updated_at', 'attribute_set_id']);
1449  }
1450  if ($entityRowsIn) {
1451  $this->_connection->insertMultiple($entityTable, $entityRowsIn);
1452 
1453  $select = $this->_connection->select()->from(
1454  $entityTable,
1455  array_merge($this->getNewSkuFieldsForSelect(), $this->getOldSkuFieldsForSelect())
1456  )->where(
1457  $this->_connection->quoteInto('sku IN (?)', array_keys($entityRowsIn))
1458  );
1459  $newProducts = $this->_connection->fetchAll($select);
1460  foreach ($newProducts as $data) {
1461  $sku = $data['sku'];
1462  unset($data['sku']);
1463  foreach ($data as $key => $value) {
1464  $this->skuProcessor->setNewSkuData($sku, $key, $value);
1465  }
1466  }
1467 
1468  $this->updateOldSku($newProducts);
1469  }
1470 
1471  return $this;
1472  }
1473 
1479  private function getOldSkuFieldsForSelect()
1480  {
1481  return ['type_id', 'attribute_set_id'];
1482  }
1483 
1490  private function updateOldSku(array $newProducts)
1491  {
1492  $oldSkus = [];
1493  foreach ($newProducts as $info) {
1494  $typeId = $info['type_id'];
1495  $sku = strtolower($info['sku']);
1496  $oldSkus[$sku] = [
1497  'type_id' => $typeId,
1498  'attr_set_id' => $info['attribute_set_id'],
1499  $this->getProductIdentifierField() => $info[$this->getProductIdentifierField()],
1500  'supported_type' => isset($this->_productTypeModels[$typeId]),
1501  $this->getProductEntityLinkField() => $info[$this->getProductEntityLinkField()],
1502  ];
1503  }
1504 
1505  $this->_oldSku = array_replace($this->_oldSku, $oldSkus);
1506  }
1507 
1513  private function getNewSkuFieldsForSelect()
1514  {
1515  $fields = ['sku', $this->getProductEntityLinkField()];
1516  if ($this->getProductEntityLinkField() != $this->getProductIdentifierField()) {
1517  $fields[] = $this->getProductIdentifierField();
1518  }
1519  return $fields;
1520  }
1521 
1529  protected function initMediaGalleryResources()
1530  {
1531  if (null == $this->mediaGalleryTableName) {
1532  $this->productEntityTableName = $this->getResource()->getTable('catalog_product_entity');
1533  $this->mediaGalleryTableName = $this->getResource()->getTable('catalog_product_entity_media_gallery');
1534  $this->mediaGalleryValueTableName = $this->getResource()->getTable(
1535  'catalog_product_entity_media_gallery_value'
1536  );
1537  $this->mediaGalleryEntityToValueTableName = $this->getResource()->getTable(
1538  'catalog_product_entity_media_gallery_value_to_entity'
1539  );
1540  }
1541  }
1542 
1549  protected function getExistingImages($bunch)
1550  {
1551  return $this->mediaProcessor->getExistingImages($bunch);
1552  }
1553 
1560  public function getImagesFromRow(array $rowData)
1561  {
1562  $images = [];
1563  $labels = [];
1564  foreach ($this->_imagesArrayKeys as $column) {
1565  if (!empty($rowData[$column])) {
1566  $images[$column] = array_unique(
1567  array_map(
1568  'trim',
1569  explode($this->getMultipleValueSeparator(), $rowData[$column])
1570  )
1571  );
1572 
1573  if (!empty($rowData[$column . '_label'])) {
1574  $labels[$column] = $this->parseMultipleValues($rowData[$column . '_label']);
1575 
1576  if (count($labels[$column]) > count($images[$column])) {
1577  $labels[$column] = array_slice($labels[$column], 0, count($images[$column]));
1578  }
1579  }
1580  }
1581  }
1582 
1583  return [$images, $labels];
1584  }
1585 
1596  protected function _saveProducts()
1597  {
1598  $priceIsGlobal = $this->_catalogData->isPriceGlobal();
1599  $productLimit = null;
1600  $productsQty = null;
1601  $entityLinkField = $this->getProductEntityLinkField();
1602 
1603  while ($bunch = $this->_dataSourceModel->getNextBunch()) {
1604  $entityRowsIn = [];
1605  $entityRowsUp = [];
1606  $attributes = [];
1607  $this->websitesCache = [];
1608  $this->categoriesCache = [];
1609  $tierPrices = [];
1610  $mediaGallery = [];
1611  $labelsForUpdate = [];
1612  $imagesForChangeVisibility = [];
1613  $uploadedImages = [];
1614  $previousType = null;
1615  $prevAttributeSet = null;
1616  $existingImages = $this->getExistingImages($bunch);
1617 
1618  foreach ($bunch as $rowNum => $rowData) {
1619  // reset category processor's failed categories array
1620  $this->categoryProcessor->clearFailedCategories();
1621 
1622  if (!$this->validateRow($rowData, $rowNum)) {
1623  continue;
1624  }
1625  if ($this->getErrorAggregator()->hasToBeTerminated()) {
1626  $this->getErrorAggregator()->addRowToSkip($rowNum);
1627  continue;
1628  }
1629  $rowScope = $this->getRowScope($rowData);
1630 
1631  $rowData[self::URL_KEY] = $this->getUrlKey($rowData);
1632 
1633  $rowSku = $rowData[self::COL_SKU];
1634 
1635  if (null === $rowSku) {
1636  $this->getErrorAggregator()->addRowToSkip($rowNum);
1637  continue;
1638  } elseif (self::SCOPE_STORE == $rowScope) {
1639  // set necessary data from SCOPE_DEFAULT row
1640  $rowData[self::COL_TYPE] = $this->skuProcessor->getNewSku($rowSku)['type_id'];
1641  $rowData['attribute_set_id'] = $this->skuProcessor->getNewSku($rowSku)['attr_set_id'];
1642  $rowData[self::COL_ATTR_SET] = $this->skuProcessor->getNewSku($rowSku)['attr_set_code'];
1643  }
1644 
1645  // 1. Entity phase
1646  if ($this->isSkuExist($rowSku)) {
1647  // existing row
1648  if (isset($rowData['attribute_set_code'])) {
1649  $attributeSetId = $this->catalogConfig->getAttributeSetId(
1650  $this->getEntityTypeId(),
1651  $rowData['attribute_set_code']
1652  );
1653 
1654  // wrong attribute_set_code was received
1655  if (!$attributeSetId) {
1656  throw new LocalizedException(
1657  __(
1658  'Wrong attribute set code "%1", please correct it and try again.',
1659  $rowData['attribute_set_code']
1660  )
1661  );
1662  }
1663  } else {
1664  $attributeSetId = $this->skuProcessor->getNewSku($rowSku)['attr_set_id'];
1665  }
1666 
1667  $entityRowsUp[] = [
1668  'updated_at' => (new \DateTime())->format(DateTime::DATETIME_PHP_FORMAT),
1669  'attribute_set_id' => $attributeSetId,
1670  $entityLinkField => $this->getExistingSku($rowSku)[$entityLinkField]
1671  ];
1672  } else {
1673  if (!$productLimit || $productsQty < $productLimit) {
1674  $entityRowsIn[strtolower($rowSku)] = [
1675  'attribute_set_id' => $this->skuProcessor->getNewSku($rowSku)['attr_set_id'],
1676  'type_id' => $this->skuProcessor->getNewSku($rowSku)['type_id'],
1677  'sku' => $rowSku,
1678  'has_options' => isset($rowData['has_options']) ? $rowData['has_options'] : 0,
1679  'created_at' => (new \DateTime())->format(DateTime::DATETIME_PHP_FORMAT),
1680  'updated_at' => (new \DateTime())->format(DateTime::DATETIME_PHP_FORMAT),
1681  ];
1682  $productsQty++;
1683  } else {
1684  $rowSku = null;
1685  // sign for child rows to be skipped
1686  $this->getErrorAggregator()->addRowToSkip($rowNum);
1687  continue;
1688  }
1689  }
1690 
1691  if (!array_key_exists($rowSku, $this->websitesCache)) {
1692  $this->websitesCache[$rowSku] = [];
1693  }
1694  // 2. Product-to-Website phase
1695  if (!empty($rowData[self::COL_PRODUCT_WEBSITES])) {
1696  $websiteCodes = explode($this->getMultipleValueSeparator(), $rowData[self::COL_PRODUCT_WEBSITES]);
1697  foreach ($websiteCodes as $websiteCode) {
1698  $websiteId = $this->storeResolver->getWebsiteCodeToId($websiteCode);
1699  $this->websitesCache[$rowSku][$websiteId] = true;
1700  }
1701  }
1702 
1703  // 3. Categories phase
1704  if (!array_key_exists($rowSku, $this->categoriesCache)) {
1705  $this->categoriesCache[$rowSku] = [];
1706  }
1707  $rowData['rowNum'] = $rowNum;
1708  $categoryIds = $this->processRowCategories($rowData);
1709  foreach ($categoryIds as $id) {
1710  $this->categoriesCache[$rowSku][$id] = true;
1711  }
1712  unset($rowData['rowNum']);
1713 
1714  // 4.1. Tier prices phase
1715  if (!empty($rowData['_tier_price_website'])) {
1716  $tierPrices[$rowSku][] = [
1717  'all_groups' => $rowData['_tier_price_customer_group'] == self::VALUE_ALL,
1718  'customer_group_id' => $rowData['_tier_price_customer_group'] ==
1719  self::VALUE_ALL ? 0 : $rowData['_tier_price_customer_group'],
1720  'qty' => $rowData['_tier_price_qty'],
1721  'value' => $rowData['_tier_price_price'],
1722  'website_id' => self::VALUE_ALL == $rowData['_tier_price_website'] ||
1723  $priceIsGlobal ? 0 : $this->storeResolver->getWebsiteCodeToId($rowData['_tier_price_website']),
1724  ];
1725  }
1726 
1727  if (!$this->validateRow($rowData, $rowNum)) {
1728  continue;
1729  }
1730 
1731  // 5. Media gallery phase
1732  list($rowImages, $rowLabels) = $this->getImagesFromRow($rowData);
1733  $storeId = !empty($rowData[self::COL_STORE])
1734  ? $this->getStoreIdByCode($rowData[self::COL_STORE])
1736  $imageHiddenStates = $this->getImagesHiddenStates($rowData);
1737  foreach (array_keys($imageHiddenStates) as $image) {
1738  if (array_key_exists($rowSku, $existingImages)
1739  && array_key_exists($image, $existingImages[$rowSku])
1740  ) {
1741  $rowImages[self::COL_MEDIA_IMAGE][] = $image;
1742  $uploadedImages[$image] = $image;
1743  }
1744 
1745  if (empty($rowImages)) {
1746  $rowImages[self::COL_MEDIA_IMAGE][] = $image;
1747  }
1748  }
1749 
1750  $rowData[self::COL_MEDIA_IMAGE] = [];
1751 
1752  /*
1753  * Note: to avoid problems with undefined sorting, the value of media gallery items positions
1754  * must be unique in scope of one product.
1755  */
1756  $position = 0;
1757  foreach ($rowImages as $column => $columnImages) {
1758  foreach ($columnImages as $columnImageKey => $columnImage) {
1759  if (!isset($uploadedImages[$columnImage])) {
1760  $uploadedFile = $this->uploadMediaFiles($columnImage);
1761  $uploadedFile = $uploadedFile ?: $this->getSystemFile($columnImage);
1762  if ($uploadedFile) {
1763  $uploadedImages[$columnImage] = $uploadedFile;
1764  } else {
1765  $this->addRowError(
1766  ValidatorInterface::ERROR_MEDIA_URL_NOT_ACCESSIBLE,
1767  $rowNum,
1768  null,
1769  null,
1771  );
1772  }
1773  } else {
1774  $uploadedFile = $uploadedImages[$columnImage];
1775  }
1776 
1777  if ($uploadedFile && $column !== self::COL_MEDIA_IMAGE) {
1778  $rowData[$column] = $uploadedFile;
1779  }
1780 
1781  if ($uploadedFile && !isset($mediaGallery[$storeId][$rowSku][$uploadedFile])) {
1782  if (isset($existingImages[$rowSku][$uploadedFile])) {
1783  $currentFileData = $existingImages[$rowSku][$uploadedFile];
1784  if (isset($rowLabels[$column][$columnImageKey])
1785  && $rowLabels[$column][$columnImageKey] !=
1786  $currentFileData['label']
1787  ) {
1788  $labelsForUpdate[] = [
1789  'label' => $rowLabels[$column][$columnImageKey],
1790  'imageData' => $currentFileData
1791  ];
1792  }
1793 
1794  if (array_key_exists($uploadedFile, $imageHiddenStates)
1795  && $currentFileData['disabled'] != $imageHiddenStates[$uploadedFile]
1796  ) {
1797  $imagesForChangeVisibility[] = [
1798  'disabled' => $imageHiddenStates[$uploadedFile],
1799  'imageData' => $currentFileData
1800  ];
1801  }
1802  } else {
1803  if ($column == self::COL_MEDIA_IMAGE) {
1804  $rowData[$column][] = $uploadedFile;
1805  }
1806  $mediaGallery[$storeId][$rowSku][$uploadedFile] = [
1807  'attribute_id' => $this->getMediaGalleryAttributeId(),
1808  'label' => isset($rowLabels[$column][$columnImageKey])
1809  ? $rowLabels[$column][$columnImageKey]
1810  : '',
1811  'position' => ++$position,
1812  'disabled' => isset($imageHiddenStates[$columnImage])
1813  ? $imageHiddenStates[$columnImage] : '0',
1814  'value' => $uploadedFile,
1815  ];
1816  }
1817  }
1818  }
1819  }
1820 
1821  // 6. Attributes phase
1822  $rowStore = (self::SCOPE_STORE == $rowScope)
1823  ? $this->storeResolver->getStoreCodeToId($rowData[self::COL_STORE])
1824  : 0;
1825  $productType = isset($rowData[self::COL_TYPE]) ? $rowData[self::COL_TYPE] : null;
1826  if ($productType !== null) {
1827  $previousType = $productType;
1828  }
1829  if (isset($rowData[self::COL_ATTR_SET])) {
1830  $prevAttributeSet = $rowData[self::COL_ATTR_SET];
1831  }
1832  if (self::SCOPE_NULL == $rowScope) {
1833  // for multiselect attributes only
1834  if ($prevAttributeSet !== null) {
1835  $rowData[self::COL_ATTR_SET] = $prevAttributeSet;
1836  }
1837  if ($productType === null && $previousType !== null) {
1838  $productType = $previousType;
1839  }
1840  if ($productType === null) {
1841  continue;
1842  }
1843  }
1844 
1845  $productTypeModel = $this->_productTypeModels[$productType];
1846  if (!empty($rowData['tax_class_name'])) {
1847  $rowData['tax_class_id'] =
1848  $this->taxClassProcessor->upsertTaxClass($rowData['tax_class_name'], $productTypeModel);
1849  }
1850 
1851  if ($this->getBehavior() == Import::BEHAVIOR_APPEND ||
1852  empty($rowData[self::COL_SKU])
1853  ) {
1854  $rowData = $productTypeModel->clearEmptyData($rowData);
1855  }
1856 
1857  $rowData = $productTypeModel->prepareAttributesWithDefaultValueForSave(
1858  $rowData,
1859  !$this->isSkuExist($rowSku)
1860  );
1861  $product = $this->_proxyProdFactory->create(['data' => $rowData]);
1862 
1863  foreach ($rowData as $attrCode => $attrValue) {
1864  $attribute = $this->retrieveAttributeByCode($attrCode);
1865 
1866  if ('multiselect' != $attribute->getFrontendInput() && self::SCOPE_NULL == $rowScope) {
1867  // skip attribute processing for SCOPE_NULL rows
1868  continue;
1869  }
1870  $attrId = $attribute->getId();
1871  $backModel = $attribute->getBackendModel();
1872  $attrTable = $attribute->getBackend()->getTable();
1873  $storeIds = [0];
1874 
1875  if ('datetime' == $attribute->getBackendType()
1876  && (
1877  in_array($attribute->getAttributeCode(), $this->dateAttrCodes)
1878  || $attribute->getIsUserDefined()
1879  )
1880  ) {
1881  $attrValue = $this->dateTime->formatDate($attrValue, false);
1882  } elseif ('datetime' == $attribute->getBackendType() && strtotime($attrValue)) {
1883  $attrValue = gmdate(
1884  'Y-m-d H:i:s',
1885  $this->_localeDate->date($attrValue)->getTimestamp()
1886  );
1887  } elseif ($backModel) {
1888  $attribute->getBackend()->beforeSave($product);
1889  $attrValue = $product->getData($attribute->getAttributeCode());
1890  }
1891  if (self::SCOPE_STORE == $rowScope) {
1892  if (self::SCOPE_WEBSITE == $attribute->getIsGlobal()) {
1893  // check website defaults already set
1894  if (!isset($attributes[$attrTable][$rowSku][$attrId][$rowStore])) {
1895  $storeIds = $this->storeResolver->getStoreIdToWebsiteStoreIds($rowStore);
1896  }
1897  } elseif (self::SCOPE_STORE == $attribute->getIsGlobal()) {
1898  $storeIds = [$rowStore];
1899  }
1900  if (!$this->isSkuExist($rowSku)) {
1901  $storeIds[] = 0;
1902  }
1903  }
1904  foreach ($storeIds as $storeId) {
1905  if (!isset($attributes[$attrTable][$rowSku][$attrId][$storeId])) {
1906  $attributes[$attrTable][$rowSku][$attrId][$storeId] = $attrValue;
1907  }
1908  }
1909  // restore 'backend_model' to avoid 'default' setting
1910  $attribute->setBackendModel($backModel);
1911  }
1912  }
1913 
1914  foreach ($bunch as $rowNum => $rowData) {
1915  if ($this->getErrorAggregator()->isRowInvalid($rowNum)) {
1916  unset($bunch[$rowNum]);
1917  }
1918  }
1919 
1920  $this->saveProductEntity(
1921  $entityRowsIn,
1922  $entityRowsUp
1923  )->_saveProductWebsites(
1924  $this->websitesCache
1925  )->_saveProductCategories(
1926  $this->categoriesCache
1927  )->_saveProductTierPrices(
1928  $tierPrices
1929  )->_saveMediaGallery(
1930  $mediaGallery
1931  )->_saveProductAttributes(
1932  $attributes
1933  )->updateMediaGalleryVisibility(
1934  $imagesForChangeVisibility
1935  )->updateMediaGalleryLabels(
1936  $labelsForUpdate
1937  );
1938 
1939  $this->_eventManager->dispatch(
1940  'catalog_product_import_bunch_save_after',
1941  ['adapter' => $this, 'bunch' => $bunch]
1942  );
1943  }
1944 
1945  return $this;
1946  }
1947 
1954  private function getImagesHiddenStates($rowData)
1955  {
1956  $statesArray = [];
1957  $mappingArray = [
1958  '_media_is_disabled' => '1'
1959  ];
1960 
1961  foreach ($mappingArray as $key => $value) {
1962  if (isset($rowData[$key]) && strlen(trim($rowData[$key]))) {
1963  $items = explode($this->getMultipleValueSeparator(), $rowData[$key]);
1964 
1965  foreach ($items as $item) {
1966  $statesArray[$item] = $value;
1967  }
1968  }
1969  }
1970 
1971  return $statesArray;
1972  }
1973 
1980  protected function processRowCategories($rowData)
1981  {
1982  $categoriesString = empty($rowData[self::COL_CATEGORY]) ? '' : $rowData[self::COL_CATEGORY];
1983  $categoryIds = [];
1984  if (!empty($categoriesString)) {
1985  $categoryIds = $this->categoryProcessor->upsertCategories(
1986  $categoriesString,
1987  $this->getMultipleValueSeparator()
1988  );
1989  foreach ($this->categoryProcessor->getFailedCategories() as $error) {
1990  $this->errorAggregator->addError(
1991  AbstractEntity::ERROR_CODE_CATEGORY_NOT_VALID,
1993  $rowData['rowNum'],
1994  self::COL_CATEGORY,
1995  __('Category "%1" has not been created.', $error['category'])
1996  . ' ' . $error['exception']->getMessage()
1997  );
1998  }
1999  }
2000  return $categoryIds;
2001  }
2002 
2009  public function getProductWebsites($productSku)
2010  {
2011  return array_keys($this->websitesCache[$productSku]);
2012  }
2013 
2020  public function getProductCategories($productSku)
2021  {
2022  return array_keys($this->categoriesCache[$productSku]);
2023  }
2024 
2031  public function getStoreIdByCode($storeCode)
2032  {
2033  if (empty($storeCode)) {
2034  return self::SCOPE_DEFAULT;
2035  }
2036  return $this->storeResolver->getStoreCodeToId($storeCode);
2037  }
2038 
2045  protected function _saveProductTierPrices(array $tierPriceData)
2046  {
2047  static $tableName = null;
2048 
2049  if (!$tableName) {
2050  $tableName = $this->_resourceFactory->create()->getTable('catalog_product_entity_tier_price');
2051  }
2052  if ($tierPriceData) {
2053  $tierPriceIn = [];
2054  $delProductId = [];
2055 
2056  foreach ($tierPriceData as $delSku => $tierPriceRows) {
2057  $productId = $this->skuProcessor->getNewSku($delSku)[$this->getProductEntityLinkField()];
2058  $delProductId[] = $productId;
2059 
2060  foreach ($tierPriceRows as $row) {
2061  $row[$this->getProductEntityLinkField()] = $productId;
2062  $tierPriceIn[] = $row;
2063  }
2064  }
2065  if (Import::BEHAVIOR_APPEND != $this->getBehavior()) {
2066  $this->_connection->delete(
2067  $tableName,
2068  $this->_connection->quoteInto("{$this->getProductEntityLinkField()} IN (?)", $delProductId)
2069  );
2070  }
2071  if ($tierPriceIn) {
2072  $this->_connection->insertOnDuplicate($tableName, $tierPriceIn, ['value']);
2073  }
2074  }
2075  return $this;
2076  }
2077 
2084  protected function _getUploader()
2085  {
2086  if ($this->_fileUploader === null) {
2087  $this->_fileUploader = $this->_uploaderFactory->create();
2088 
2089  $this->_fileUploader->init();
2090 
2091  $dirConfig = DirectoryList::getDefaultConfig();
2092  $dirAddon = $dirConfig[DirectoryList::MEDIA][DirectoryList::PATH];
2093 
2094  if (!empty($this->_parameters[Import::FIELD_NAME_IMG_FILE_DIR])) {
2095  $tmpPath = $this->_parameters[Import::FIELD_NAME_IMG_FILE_DIR];
2096  } else {
2097  $tmpPath = $dirAddon . '/' . $this->_mediaDirectory->getRelativePath('import');
2098  }
2099 
2100  if (!$this->_fileUploader->setTmpDir($tmpPath)) {
2101  throw new LocalizedException(
2102  __('File directory \'%1\' is not readable.', $tmpPath)
2103  );
2104  }
2105  $destinationDir = "catalog/product";
2106  $destinationPath = $dirAddon . '/' . $this->_mediaDirectory->getRelativePath($destinationDir);
2107 
2108  $this->_mediaDirectory->create($destinationPath);
2109  if (!$this->_fileUploader->setDestDir($destinationPath)) {
2110  throw new LocalizedException(
2111  __('File directory \'%1\' is not writable.', $destinationPath)
2112  );
2113  }
2114  }
2115  return $this->_fileUploader;
2116  }
2117 
2124  public function getUploader()
2125  {
2126  return $this->_getUploader();
2127  }
2128 
2139  protected function uploadMediaFiles($fileName, $renameFileOff = false)
2140  {
2141  try {
2142  $res = $this->_getUploader()->move($fileName, $renameFileOff);
2143  return $res['file'];
2144  } catch (\Exception $e) {
2145  $this->_logger->critical($e);
2146  return '';
2147  }
2148  }
2149 
2156  private function getSystemFile($fileName)
2157  {
2158  $filePath = 'catalog' . DIRECTORY_SEPARATOR . 'product' . DIRECTORY_SEPARATOR . $fileName;
2160  $read = $this->filesystem->getDirectoryRead(DirectoryList::MEDIA);
2161 
2162  return $read->isExist($filePath) && $read->isReadable($filePath) ? $fileName : '';
2163  }
2164 
2171  protected function _saveMediaGallery(array $mediaGalleryData)
2172  {
2173  if (empty($mediaGalleryData)) {
2174  return $this;
2175  }
2176  $this->mediaProcessor->saveMediaGallery($mediaGalleryData);
2177 
2178  return $this;
2179  }
2180 
2187  protected function _saveProductWebsites(array $websiteData)
2188  {
2189  static $tableName = null;
2190 
2191  if (!$tableName) {
2192  $tableName = $this->_resourceFactory->create()->getProductWebsiteTable();
2193  }
2194  if ($websiteData) {
2195  $websitesData = [];
2196  $delProductId = [];
2197 
2198  foreach ($websiteData as $delSku => $websites) {
2199  $productId = $this->skuProcessor->getNewSku($delSku)['entity_id'];
2200  $delProductId[] = $productId;
2201 
2202  foreach (array_keys($websites) as $websiteId) {
2203  $websitesData[] = ['product_id' => $productId, 'website_id' => $websiteId];
2204  }
2205  }
2206  if (Import::BEHAVIOR_APPEND != $this->getBehavior()) {
2207  $this->_connection->delete(
2208  $tableName,
2209  $this->_connection->quoteInto('product_id IN (?)', $delProductId)
2210  );
2211  }
2212  if ($websitesData) {
2213  $this->_connection->insertOnDuplicate($tableName, $websitesData);
2214  }
2215  }
2216  return $this;
2217  }
2218 
2224  protected function _saveStockItem()
2225  {
2226  while ($bunch = $this->_dataSourceModel->getNextBunch()) {
2227  $stockData = [];
2228  $productIdsToReindex = [];
2229  // Format bunch to stock data rows
2230  foreach ($bunch as $rowNum => $rowData) {
2231  if (!$this->isRowAllowedToImport($rowData, $rowNum)) {
2232  continue;
2233  }
2234 
2235  $row = [];
2236  $sku = $rowData[self::COL_SKU];
2237  if ($this->skuProcessor->getNewSku($sku) !== null) {
2238  $row = $this->formatStockDataForRow($rowData);
2239  $productIdsToReindex[] = $row['product_id'];
2240  }
2241 
2242  if (!isset($stockData[$sku])) {
2243  $stockData[$sku] = $row;
2244  }
2245  }
2246 
2247  // Insert rows
2248  if (!empty($stockData)) {
2249  $this->stockItemImporter->import($stockData);
2250  }
2251 
2252  $this->reindexProducts($productIdsToReindex);
2253  }
2254  return $this;
2255  }
2256 
2263  private function reindexProducts($productIdsToReindex = [])
2264  {
2265  $indexer = $this->indexerRegistry->get('catalog_product_category');
2266  if (is_array($productIdsToReindex) && count($productIdsToReindex) > 0 && !$indexer->isScheduled()) {
2267  $indexer->reindexList($productIdsToReindex);
2268  }
2269  }
2270 
2277  public function retrieveAttributeByCode($attrCode)
2278  {
2280  $attrCode = mb_strtolower($attrCode);
2281 
2282  if (!isset($this->_attributeCache[$attrCode])) {
2283  $this->_attributeCache[$attrCode] = $this->getResource()->getAttribute($attrCode);
2284  }
2285 
2286  return $this->_attributeCache[$attrCode];
2287  }
2288 
2294  public function getAttrSetIdToName()
2295  {
2296  return $this->_attrSetIdToName;
2297  }
2298 
2304  public function getConnection()
2305  {
2306  return $this->_connection;
2307  }
2308 
2315  public function getEntityTypeCode()
2316  {
2317  return 'catalog_product';
2318  }
2319 
2329  public function getNewSku($sku = null)
2330  {
2331  return $this->skuProcessor->getNewSku($sku);
2332  }
2333 
2339  public function getNextBunch()
2340  {
2341  return $this->_dataSourceModel->getNextBunch();
2342  }
2343 
2352  public function getOldSku()
2353  {
2354  return $this->_oldSku;
2355  }
2356 
2362  public function getCategoryProcessor()
2363  {
2364  return $this->categoryProcessor;
2365  }
2366 
2373  public function getRowScope(array $rowData)
2374  {
2375  if (empty($rowData[self::COL_STORE])) {
2376  return self::SCOPE_DEFAULT;
2377  }
2378  return self::SCOPE_STORE;
2379  }
2380 
2391  public function validateRow(array $rowData, $rowNum)
2392  {
2393  if (isset($this->_validatedRows[$rowNum])) {
2394  // check that row is already validated
2395  return !$this->getErrorAggregator()->isRowInvalid($rowNum);
2396  }
2397  $this->_validatedRows[$rowNum] = true;
2398 
2399  $rowScope = $this->getRowScope($rowData);
2400  $sku = $rowData[self::COL_SKU];
2401 
2402  // BEHAVIOR_DELETE and BEHAVIOR_REPLACE use specific validation logic
2403  if (Import::BEHAVIOR_REPLACE == $this->getBehavior()) {
2404  if (self::SCOPE_DEFAULT == $rowScope && !$this->isSkuExist($sku)) {
2405  $this->addRowError(ValidatorInterface::ERROR_SKU_NOT_FOUND_FOR_DELETE, $rowNum);
2406  return false;
2407  }
2408  }
2409  if (Import::BEHAVIOR_DELETE == $this->getBehavior()) {
2410  if (self::SCOPE_DEFAULT == $rowScope && !$this->isSkuExist($sku)) {
2411  $this->addRowError(ValidatorInterface::ERROR_SKU_NOT_FOUND_FOR_DELETE, $rowNum);
2412  return false;
2413  }
2414  return true;
2415  }
2416 
2417  if (!$this->validator->isValid($rowData)) {
2418  foreach ($this->validator->getMessages() as $message) {
2419  $this->addRowError($message, $rowNum, $this->validator->getInvalidAttribute());
2420  }
2421  }
2422 
2423  if (null === $sku) {
2424  $this->addRowError(ValidatorInterface::ERROR_SKU_IS_EMPTY, $rowNum);
2425  } elseif (false === $sku) {
2426  $this->addRowError(ValidatorInterface::ERROR_ROW_IS_ORPHAN, $rowNum);
2427  } elseif (self::SCOPE_STORE == $rowScope
2428  && !$this->storeResolver->getStoreCodeToId($rowData[self::COL_STORE])
2429  ) {
2430  $this->addRowError(ValidatorInterface::ERROR_INVALID_STORE, $rowNum);
2431  }
2432 
2433  // SKU is specified, row is SCOPE_DEFAULT, new product block begins
2434  $this->_processedEntitiesCount++;
2435 
2436  if ($this->isSkuExist($sku) && Import::BEHAVIOR_REPLACE !== $this->getBehavior()) {
2437  // can we get all necessary data from existent DB product?
2438  // check for supported type of existing product
2439  if (isset($this->_productTypeModels[$this->getExistingSku($sku)['type_id']])) {
2440  $this->skuProcessor->addNewSku(
2441  $sku,
2442  $this->prepareNewSkuData($sku)
2443  );
2444  } else {
2445  $this->addRowError(ValidatorInterface::ERROR_TYPE_UNSUPPORTED, $rowNum);
2446  }
2447  } else {
2448  // validate new product type and attribute set
2449  if (!isset($rowData[self::COL_TYPE]) || !isset($this->_productTypeModels[$rowData[self::COL_TYPE]])) {
2450  $this->addRowError(ValidatorInterface::ERROR_INVALID_TYPE, $rowNum);
2451  } elseif (!isset($rowData[self::COL_ATTR_SET])
2452  || !isset($this->_attrSetNameToId[$rowData[self::COL_ATTR_SET]])
2453  ) {
2454  $this->addRowError(ValidatorInterface::ERROR_INVALID_ATTR_SET, $rowNum);
2455  } elseif ($this->skuProcessor->getNewSku($sku) === null) {
2456  $this->skuProcessor->addNewSku(
2457  $sku,
2458  [
2459  'row_id' => null,
2460  'entity_id' => null,
2461  'type_id' => $rowData[self::COL_TYPE],
2462  'attr_set_id' => $this->_attrSetNameToId[$rowData[self::COL_ATTR_SET]],
2463  'attr_set_code' => $rowData[self::COL_ATTR_SET],
2464  ]
2465  );
2466  }
2467  }
2468 
2469  if (!$this->getErrorAggregator()->isRowInvalid($rowNum)) {
2470  $newSku = $this->skuProcessor->getNewSku($sku);
2471  // set attribute set code into row data for followed attribute validation in type model
2472  $rowData[self::COL_ATTR_SET] = $newSku['attr_set_code'];
2473 
2475  // isRowValid can add error to general errors pull if row is invalid
2476  $productTypeValidator = $this->_productTypeModels[$newSku['type_id']];
2477  $productTypeValidator->isRowValid(
2478  $rowData,
2479  $rowNum,
2480  !($this->isSkuExist($sku) && Import::BEHAVIOR_REPLACE !== $this->getBehavior())
2481  );
2482  }
2483  // validate custom options
2484  $this->getOptionEntity()->validateRow($rowData, $rowNum);
2485 
2486  if ($this->isNeedToValidateUrlKey($rowData)) {
2487  $urlKey = strtolower($this->getUrlKey($rowData));
2488  $storeCodes = empty($rowData[self::COL_STORE_VIEW_CODE])
2489  ? array_flip($this->storeResolver->getStoreCodeToId())
2490  : explode($this->getMultipleValueSeparator(), $rowData[self::COL_STORE_VIEW_CODE]);
2491  foreach ($storeCodes as $storeCode) {
2492  $storeId = $this->storeResolver->getStoreCodeToId($storeCode);
2494  $urlPath = $urlKey . $productUrlSuffix;
2495  if (empty($this->urlKeys[$storeId][$urlPath])
2496  || ($this->urlKeys[$storeId][$urlPath] == $sku)
2497  ) {
2498  $this->urlKeys[$storeId][$urlPath] = $sku;
2499  $this->rowNumbers[$storeId][$urlPath] = $rowNum;
2500  } else {
2501  $message = sprintf(
2502  $this->retrieveMessageTemplate(ValidatorInterface::ERROR_DUPLICATE_URL_KEY),
2503  $urlKey,
2504  $this->urlKeys[$storeId][$urlPath]
2505  );
2506  $this->addRowError(
2507  ValidatorInterface::ERROR_DUPLICATE_URL_KEY,
2508  $rowNum,
2509  $rowData[self::COL_NAME],
2510  $message
2511  );
2512  }
2513  }
2514  }
2515  return !$this->getErrorAggregator()->isRowInvalid($rowNum);
2516  }
2517 
2524  private function isNeedToValidateUrlKey($rowData)
2525  {
2526  return (!empty($rowData[self::URL_KEY]) || !empty($rowData[self::COL_NAME]))
2527  && (empty($rowData[self::COL_VISIBILITY])
2528  || $rowData[self::COL_VISIBILITY]
2530  }
2531 
2538  private function prepareNewSkuData($sku)
2539  {
2540  $data = [];
2541  foreach ($this->getExistingSku($sku) as $key => $value) {
2542  $data[$key] = $value;
2543  }
2544 
2545  $data['attr_set_code'] = $this->_attrSetIdToName[$this->getExistingSku($sku)['attr_set_id']];
2546 
2547  return $data;
2548  }
2549 
2557  private function _parseAdditionalAttributes($rowData)
2558  {
2559  if (empty($rowData['additional_attributes'])) {
2560  return $rowData;
2561  }
2562  $rowData = array_merge($rowData, $this->getAdditionalAttributes($rowData['additional_attributes']));
2563  return $rowData;
2564  }
2565 
2578  private function getAdditionalAttributes($additionalAttributes)
2579  {
2580  return empty($this->_parameters[Import::FIELDS_ENCLOSURE])
2581  ? $this->parseAttributesWithoutWrappedValues($additionalAttributes)
2582  : $this->parseAttributesWithWrappedValues($additionalAttributes);
2583  }
2584 
2598  private function parseAttributesWithoutWrappedValues($attributesData)
2599  {
2600  $attributeNameValuePairs = explode($this->getMultipleValueSeparator(), $attributesData);
2601  $preparedAttributes = [];
2602  $code = '';
2603  foreach ($attributeNameValuePairs as $attributeData) {
2604  //process case when attribute has ImportModel::DEFAULT_GLOBAL_MULTI_VALUE_SEPARATOR inside its value
2605  if (strpos($attributeData, self::PAIR_NAME_VALUE_SEPARATOR) === false) {
2606  if (!$code) {
2607  continue;
2608  }
2609  $preparedAttributes[$code] .= $this->getMultipleValueSeparator() . $attributeData;
2610  continue;
2611  }
2612  list($code, $value) = explode(self::PAIR_NAME_VALUE_SEPARATOR, $attributeData, 2);
2613  $code = mb_strtolower($code);
2614  $preparedAttributes[$code] = $value;
2615  }
2616  return $preparedAttributes;
2617  }
2618 
2637  private function parseAttributesWithWrappedValues($attributesData)
2638  {
2639  $attributes = [];
2640  preg_match_all(
2641  '~((?:[a-zA-Z0-9_])+)="((?:[^"]|""|"' . $this->getMultiLineSeparatorForRegexp() . '")+)"+~',
2643  $matches
2644  );
2645  foreach ($matches[1] as $i => $attributeCode) {
2646  $attribute = $this->retrieveAttributeByCode($attributeCode);
2647  $value = 'multiselect' != $attribute->getFrontendInput()
2648  ? str_replace('""', '"', $matches[2][$i])
2649  : '"' . $matches[2][$i] . '"';
2650  $attributes[mb_strtolower($attributeCode)] = $value;
2651  }
2652  return $attributes;
2653  }
2654 
2663  public function parseMultiselectValues($values, $delimiter = self::PSEUDO_MULTI_LINE_SEPARATOR)
2664  {
2665  if (empty($this->_parameters[Import::FIELDS_ENCLOSURE])) {
2666  return explode($delimiter, $values);
2667  }
2668  if (preg_match_all('~"((?:[^"]|"")*)"~', $values, $matches)) {
2669  return $values = array_map(function ($value) {
2670  return str_replace('""', '"', $value);
2671  }, $matches[1]);
2672  }
2673  return [$values];
2674  }
2675 
2681  private function getMultiLineSeparatorForRegexp()
2682  {
2683  if (!$this->multiLineSeparatorForRegexp) {
2684  $this->multiLineSeparatorForRegexp = in_array(self::PSEUDO_MULTI_LINE_SEPARATOR, str_split('[\^$.|?*+(){}'))
2687  }
2688  return $this->multiLineSeparatorForRegexp;
2689  }
2690 
2698  private function _setStockUseConfigFieldsValues($rowData)
2699  {
2700  $useConfigFields = [];
2701  foreach ($rowData as $key => $value) {
2702  $useConfigName = $key === StockItemInterface::ENABLE_QTY_INCREMENTS
2704  : self::INVENTORY_USE_CONFIG_PREFIX . $key;
2705 
2706  if (isset($this->defaultStockData[$key])
2707  && isset($this->defaultStockData[$useConfigName])
2708  && !empty($value)
2709  && empty($rowData[$useConfigName])
2710  ) {
2711  $useConfigFields[$useConfigName] = ($value == self::INVENTORY_USE_CONFIG) ? 1 : 0;
2712  }
2713  }
2714  $rowData = array_merge($rowData, $useConfigFields);
2715  return $rowData;
2716  }
2717 
2725  private function _customFieldsMapping($rowData)
2726  {
2727  foreach ($this->_fieldsMap as $systemFieldName => $fileFieldName) {
2728  if (array_key_exists($fileFieldName, $rowData)) {
2729  $rowData[$systemFieldName] = $rowData[$fileFieldName];
2730  }
2731  }
2732 
2733  $rowData = $this->_parseAdditionalAttributes($rowData);
2734 
2735  $rowData = $this->_setStockUseConfigFieldsValues($rowData);
2736  if (array_key_exists('status', $rowData)
2737  && $rowData['status'] != \Magento\Catalog\Model\Product\Attribute\Source\Status::STATUS_ENABLED
2738  ) {
2739  if ($rowData['status'] == 'yes') {
2740  $rowData['status'] = \Magento\Catalog\Model\Product\Attribute\Source\Status::STATUS_ENABLED;
2741  } elseif (!empty($rowData['status']) || $this->getRowScope($rowData) == self::SCOPE_DEFAULT) {
2742  $rowData['status'] = \Magento\Catalog\Model\Product\Attribute\Source\Status::STATUS_DISABLED;
2743  }
2744  }
2745  return $rowData;
2746  }
2747 
2753  protected function _saveValidatedBunches()
2754  {
2755  $source = $this->_getSource();
2756  $source->rewind();
2757 
2758  while ($source->valid()) {
2759  try {
2760  $rowData = $source->current();
2761  } catch (\InvalidArgumentException $e) {
2762  $this->addRowError($e->getMessage(), $this->_processedRowsCount);
2763  $this->_processedRowsCount++;
2764  $source->next();
2765  continue;
2766  }
2767 
2768  $rowData = $this->_customFieldsMapping($rowData);
2769 
2770  $this->validateRow($rowData, $source->key());
2771 
2772  $source->next();
2773  }
2774  $this->checkUrlKeyDuplicates();
2775  $this->getOptionEntity()->validateAmbiguousData();
2776  return parent::_saveValidatedBunches();
2777  }
2778 
2785  protected function checkUrlKeyDuplicates()
2786  {
2787  $resource = $this->getResource();
2788  foreach ($this->urlKeys as $storeId => $urlKeys) {
2789  $urlKeyDuplicates = $this->_connection->fetchAssoc(
2790  $this->_connection->select()->from(
2791  ['url_rewrite' => $resource->getTable('url_rewrite')],
2792  ['request_path', 'store_id']
2793  )->joinLeft(
2794  ['cpe' => $resource->getTable('catalog_product_entity')],
2795  "cpe.entity_id = url_rewrite.entity_id"
2796  )->where('request_path IN (?)', array_keys($urlKeys))
2797  ->where('store_id IN (?)', $storeId)
2798  ->where('cpe.sku not in (?)', array_values($urlKeys))
2799  );
2800  foreach ($urlKeyDuplicates as $entityData) {
2801  $rowNum = $this->rowNumbers[$entityData['store_id']][$entityData['request_path']];
2802  $message = sprintf(
2803  $this->retrieveMessageTemplate(ValidatorInterface::ERROR_DUPLICATE_URL_KEY),
2804  $entityData['request_path'],
2805  $entityData['sku']
2806  );
2807  $this->addRowError(ValidatorInterface::ERROR_DUPLICATE_URL_KEY, $rowNum, 'url_key', $message);
2808  }
2809  }
2810  }
2811 
2819  protected function getProductUrlSuffix($storeId = null)
2820  {
2821  if (!isset($this->productUrlSuffix[$storeId])) {
2822  $this->productUrlSuffix[$storeId] = $this->scopeConfig->getValue(
2823  \Magento\CatalogUrlRewrite\Model\ProductUrlPathGenerator::XML_PATH_PRODUCT_URL_SUFFIX,
2824  \Magento\Store\Model\ScopeInterface::SCOPE_STORE,
2825  $storeId
2826  );
2827  }
2828  return $this->productUrlSuffix[$storeId];
2829  }
2830 
2839  protected function getUrlKey($rowData)
2840  {
2841  if (!empty($rowData[self::URL_KEY])) {
2842  return strtolower($rowData[self::URL_KEY]);
2843  }
2844 
2845  if (!empty($rowData[self::COL_NAME])) {
2846  return $this->productUrl->formatUrlKey($rowData[self::COL_NAME]);
2847  }
2848 
2849  return '';
2850  }
2851 
2859  protected function getResource()
2860  {
2861  if (!$this->_resource) {
2862  $this->_resource = $this->_resourceFactory->create();
2863  }
2864  return $this->_resource;
2865  }
2866 
2872  private function getProductEntityLinkField()
2873  {
2874  if (!$this->productEntityLinkField) {
2875  $this->productEntityLinkField = $this->getMetadataPool()
2876  ->getMetadata(\Magento\Catalog\Api\Data\ProductInterface::class)
2877  ->getLinkField();
2878  }
2879  return $this->productEntityLinkField;
2880  }
2881 
2887  private function getProductIdentifierField()
2888  {
2889  if (!$this->productEntityIdentifierField) {
2890  $this->productEntityIdentifierField = $this->getMetadataPool()
2891  ->getMetadata(\Magento\Catalog\Api\Data\ProductInterface::class)
2892  ->getIdentifierField();
2893  }
2894  return $this->productEntityIdentifierField;
2895  }
2896 
2903  private function updateMediaGalleryLabels(array $labels)
2904  {
2905  if (!empty($labels)) {
2906  $this->mediaProcessor->updateMediaGalleryLabels($labels);
2907  }
2908  }
2909 
2916  private function updateMediaGalleryVisibility(array $images)
2917  {
2918  if (!empty($images)) {
2919  $this->mediaProcessor->updateMediaGalleryVisibility($images);
2920  }
2921 
2922  return $this;
2923  }
2924 
2931  private function parseMultipleValues($labelRow)
2932  {
2933  return $this->parseMultiselectValues(
2934  $labelRow,
2935  $this->getMultipleValueSeparator()
2936  );
2937  }
2938 
2945  private function isSkuExist($sku)
2946  {
2947  $sku = strtolower($sku);
2948  return isset($this->_oldSku[$sku]);
2949  }
2950 
2957  private function getExistingSku($sku)
2958  {
2959  return $this->_oldSku[strtolower($sku)];
2960  }
2961 
2968  private function formatStockDataForRow(array $rowData): array
2969  {
2970  $sku = $rowData[self::COL_SKU];
2971  $row['product_id'] = $this->skuProcessor->getNewSku($sku)['entity_id'];
2972  $row['website_id'] = $this->stockConfiguration->getDefaultScopeId();
2973  $row['stock_id'] = $this->stockRegistry->getStock($row['website_id'])->getStockId();
2974 
2975  $stockItemDo = $this->stockRegistry->getStockItem($row['product_id'], $row['website_id']);
2976  $existStockData = $stockItemDo->getData();
2977 
2978  $row = array_merge(
2979  $this->defaultStockData,
2980  array_intersect_key($existStockData, $this->defaultStockData),
2981  array_intersect_key($rowData, $this->defaultStockData),
2982  $row
2983  );
2984 
2985  if ($this->stockConfiguration->isQty($this->skuProcessor->getNewSku($sku)['type_id'])) {
2986  $stockItemDo->setData($row);
2987  $row['is_in_stock'] = isset($row['is_in_stock']) && $stockItemDo->getBackorders()
2988  ? $row['is_in_stock']
2989  : $this->stockStateProvider->verifyStock($stockItemDo);
2990  if ($this->stockStateProvider->verifyNotification($stockItemDo)) {
2991  $date = $this->dateTimeFactory->create('now', new \DateTimeZone('UTC'));
2992  $row['low_stock_date'] = $date->format(DateTime::DATETIME_PHP_FORMAT);
2993  }
2994  $row['stock_status_changed_auto'] = (int)!$this->stockStateProvider->verifyStock($stockItemDo);
2995  } else {
2996  $row['qty'] = 0;
2997  }
2998 
2999  return $row;
3000  }
3001 }
if($msrpShowOnGesture && $price['price']->getValue()< $product->getMsrp()) if($isSaleable) $tierPriceData
$tableName
Definition: trigger.php:13
elseif(isset( $params[ 'redirect_parent']))
Definition: iframe.phtml:17
$config
Definition: fraud_order.php:17
$id
Definition: fieldset.phtml:14
uploadMediaFiles($fileName, $renameFileOff=false)
Definition: Product.php:2139
$source
Definition: source.php:23
$fields
Definition: details.phtml:14
$values
Definition: options.phtml:88
__()
Definition: __.php:13
$resource
Definition: bulk.php:12
$message
$logger
$storeCode
Definition: indexer.php:15
$fileName
Definition: translate.phtml:15
$attributesData
$value
Definition: gender.phtml:16
$attributeCode
Definition: extend.phtml:12
addRowError( $errorCode, $errorRowNum, $colName=null, $errorMessage=null, $errorLevel=ProcessingError::ERROR_LEVEL_CRITICAL, $errorDescription=null)
if($product->getId()) catch(\Magento\Framework\Exception\NoSuchEntityException $e) $storeCodes
$attributes
Definition: matrix.phtml:13
$categories
__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)
Definition: Product.php:781
saveProductEntity(array $entityRowsIn, array $entityRowsUp)
Definition: Product.php:1438
foreach( $_productCollection as $_product)() ?>" class $info
Definition: listing.phtml:52
$entityTable
Definition: tablerates.php:11
if(!isset($_GET['website_code'])) $websiteCode
Definition: website.php:11
$params[\Magento\Store\Model\StoreManager::PARAM_RUN_CODE]
Definition: website.php:18
$filesystem
$i
Definition: gallery.phtml:31
parseMultiselectValues($values, $delimiter=self::PSEUDO_MULTI_LINE_SEPARATOR)
Definition: Product.php:2663
$template
Definition: export.php:12
$stockData
Definition: products.php:27
$code
Definition: info.phtml:12
$items
if(!isset($_GET['name'])) $name
Definition: log.php:14
isAttributeValid($attrCode, array $attrParams, array $rowData, $rowNum)
Definition: Product.php:889