· 6 years ago · Sep 16, 2019, 03:20 AM
1<?php
2/**
3 * Copyright © Magento, Inc. All rights reserved.
4 * See COPYING.txt for license details.
5 */
6
7namespace Magento\CatalogImportExport\Model\Import;
8
9use Magento\Catalog\Api\ProductRepositoryInterface;
10use Magento\Catalog\Model\Config as CatalogConfig;
11use Magento\Catalog\Model\Product\Visibility;
12use Magento\CatalogImportExport\Model\Import\Product\ImageTypeProcessor;
13use Magento\CatalogImportExport\Model\Import\Product\MediaGalleryProcessor;
14use Magento\CatalogImportExport\Model\Import\Product\RowValidatorInterface as ValidatorInterface;
15use Magento\CatalogImportExport\Model\StockItemImporterInterface;
16use Magento\CatalogInventory\Api\Data\StockItemInterface;
17use Magento\Framework\App\Filesystem\DirectoryList;
18use Magento\Framework\App\ObjectManager;
19use Magento\Framework\Exception\LocalizedException;
20use Magento\Framework\Exception\NoSuchEntityException;
21use Magento\Framework\Filesystem;
22use Magento\Framework\Intl\DateTimeFactory;
23use Magento\Framework\Model\ResourceModel\Db\ObjectRelationProcessor;
24use Magento\Framework\Model\ResourceModel\Db\TransactionManagerInterface;
25use Magento\Framework\Stdlib\DateTime;
26use Magento\ImportExport\Model\Import;
27use Magento\ImportExport\Model\Import\Entity\AbstractEntity;
28use Magento\ImportExport\Model\Import\ErrorProcessing\ProcessingError;
29use Magento\ImportExport\Model\Import\ErrorProcessing\ProcessingErrorAggregatorInterface;
30use Magento\Store\Model\Store;
31
32/**
33 * Import entity product model
34 *
35 * @api
36 *
37 * @SuppressWarnings(PHPMD.TooManyFields)
38 * @SuppressWarnings(PHPMD.ExcessiveClassComplexity)
39 * @SuppressWarnings(PHPMD.CouplingBetweenObjects)
40 * @since 100.0.2
41 */
42class Product extends \Magento\ImportExport\Model\Import\Entity\AbstractEntity
43{
44 const CONFIG_KEY_PRODUCT_TYPES = 'global/importexport/import_product_types';
45
46 /**
47 * Size of bunch - part of products to save in one step.
48 */
49 const BUNCH_SIZE = 20;
50
51 /**
52 * Size of bunch to delete attributes of products in one step.
53 */
54 const ATTRIBUTE_DELETE_BUNCH = 1000;
55
56 /**
57 * Pseudo multi line separator in one cell.
58 *
59 * Can be used as custom option value delimiter or in configurable fields cells.
60 */
61 const PSEUDO_MULTI_LINE_SEPARATOR = '|';
62
63 /**
64 * Symbol between Name and Value between Pairs.
65 */
66 const PAIR_NAME_VALUE_SEPARATOR = '=';
67
68 /**
69 * Value that means all entities (e.g. websites, groups etc.)
70 */
71 const VALUE_ALL = 'all';
72
73 /**
74 * Data row scopes.
75 */
76 const SCOPE_DEFAULT = 1;
77
78 const SCOPE_WEBSITE = 2;
79
80 const SCOPE_STORE = 0;
81
82 const SCOPE_NULL = -1;
83
84 /**
85 * Permanent column names.
86 *
87 * Names that begins with underscore is not an attribute. This name convention is for
88 * to avoid interference with same attribute name.
89 */
90
91 /**
92 * Column product store.
93 */
94 const COL_STORE = '_store';
95
96 /**
97 * Column product store view code.
98 */
99 const COL_STORE_VIEW_CODE = 'store_view_code';
100
101 /**
102 * Column website.
103 */
104 const COL_WEBSITE = 'website_code';
105
106 /**
107 * Column product attribute set.
108 */
109 const COL_ATTR_SET = '_attribute_set';
110
111 /**
112 * Column product type.
113 */
114 const COL_TYPE = 'product_type';
115
116 /**
117 * Column product category.
118 */
119 const COL_CATEGORY = 'categories';
120
121 /**
122 * Column product visibility.
123 */
124 const COL_VISIBILITY = 'visibility';
125
126 /**
127 * Column product sku.
128 */
129 const COL_SKU = 'sku';
130
131 /**
132 * Column product name.
133 */
134 const COL_NAME = 'name';
135
136 /**
137 * Column product website.
138 */
139 const COL_PRODUCT_WEBSITES = '_product_websites';
140
141 /**
142 * Media gallery attribute code.
143 */
144 const MEDIA_GALLERY_ATTRIBUTE_CODE = 'media_gallery';
145
146 /**
147 * Column media image.
148 */
149 const COL_MEDIA_IMAGE = '_media_image';
150
151 /**
152 * Inventory use config.
153 */
154 const INVENTORY_USE_CONFIG = 'Use Config';
155
156 /**
157 * Inventory use config prefix.
158 */
159 const INVENTORY_USE_CONFIG_PREFIX = 'use_config_';
160
161 /**
162 * Url key attribute code
163 */
164 const URL_KEY = 'url_key';
165
166 /**
167 * Attribute cache
168 *
169 * @var array
170 */
171 protected $_attributeCache = [];
172
173 /**
174 * Pairs of attribute set ID-to-name.
175 *
176 * @var array
177 */
178 protected $_attrSetIdToName = [];
179
180 /**
181 * Pairs of attribute set name-to-ID.
182 *
183 * @var array
184 */
185 protected $_attrSetNameToId = [];
186
187 /**
188 * @var string
189 * @since 100.0.4
190 */
191 protected $mediaGalleryTableName;
192
193 /**
194 * @var string
195 * @since 100.0.4
196 */
197 protected $mediaGalleryValueTableName;
198 /**
199 * @var string
200 * @since 100.0.4
201 */
202 protected $mediaGalleryEntityToValueTableName;
203
204 /**
205 * @var string
206 * @since 100.0.4
207 */
208 protected $productEntityTableName;
209
210 /**
211 * Attributes with index (not label) value.
212 *
213 * @var string[]
214 */
215 protected $_indexValueAttributes = [
216 'status',
217 'tax_class_id',
218 ];
219
220 /**
221 * Links attribute name-to-link type ID.
222 *
223 * @var array
224 */
225 protected $_linkNameToId = [
226 '_related_' => \Magento\Catalog\Model\Product\Link::LINK_TYPE_RELATED,
227 '_crosssell_' => \Magento\Catalog\Model\Product\Link::LINK_TYPE_CROSSSELL,
228 '_upsell_' => \Magento\Catalog\Model\Product\Link::LINK_TYPE_UPSELL,
229 ];
230
231 /**
232 * Attributes codes which shows as date
233 *
234 * @var array
235 * @since 100.1.2
236 */
237 protected $dateAttrCodes = [
238 'special_from_date',
239 'special_to_date',
240 'news_from_date',
241 'news_to_date',
242 'custom_design_from',
243 'custom_design_to'
244 ];
245
246 /**
247 * Need to log in import history
248 *
249 * @var bool
250 */
251 protected $logInHistory = true;
252
253 /**
254 * Attribute id for product images storage.
255 *
256 * @var array
257 */
258 protected $_mediaGalleryAttributeId = null;
259
260 /**
261 * Validation failure message template definitions
262 *
263 * @var array
264 * @codingStandardsIgnoreStart
265 */
266 protected $_messageTemplates = [
267 ValidatorInterface::ERROR_INVALID_SCOPE => 'Invalid value in Scope column',
268 ValidatorInterface::ERROR_INVALID_WEBSITE => 'Invalid value in Website column (website does not exist?)',
269 ValidatorInterface::ERROR_INVALID_STORE => 'Invalid value in Store column (store doesn\'t exist?)',
270 ValidatorInterface::ERROR_INVALID_ATTR_SET => 'Invalid value for Attribute Set column (set doesn\'t exist?)',
271 ValidatorInterface::ERROR_INVALID_TYPE => 'Product Type is invalid or not supported',
272 ValidatorInterface::ERROR_INVALID_CATEGORY => 'Category does not exist',
273 ValidatorInterface::ERROR_VALUE_IS_REQUIRED => 'Please make sure attribute "%s" is not empty.',
274 ValidatorInterface::ERROR_TYPE_CHANGED => 'Trying to change type of existing products',
275 ValidatorInterface::ERROR_SKU_IS_EMPTY => 'SKU is empty',
276 ValidatorInterface::ERROR_NO_DEFAULT_ROW => 'Default values row does not exist',
277 ValidatorInterface::ERROR_CHANGE_TYPE => 'Product type change is not allowed',
278 ValidatorInterface::ERROR_DUPLICATE_SCOPE => 'Duplicate scope',
279 ValidatorInterface::ERROR_DUPLICATE_SKU => 'Duplicate SKU',
280 ValidatorInterface::ERROR_CHANGE_ATTR_SET => 'Attribute set change is not allowed',
281 ValidatorInterface::ERROR_TYPE_UNSUPPORTED => 'Product type is not supported',
282 ValidatorInterface::ERROR_ROW_IS_ORPHAN => 'Orphan rows that will be skipped due default row errors',
283 ValidatorInterface::ERROR_INVALID_TIER_PRICE_QTY => 'Tier Price data price or quantity value is invalid',
284 ValidatorInterface::ERROR_INVALID_TIER_PRICE_SITE => 'Tier Price data website is invalid',
285 ValidatorInterface::ERROR_INVALID_TIER_PRICE_GROUP => 'Tier Price customer group ID is invalid',
286 ValidatorInterface::ERROR_TIER_DATA_INCOMPLETE => 'Tier Price data is incomplete',
287 ValidatorInterface::ERROR_SKU_NOT_FOUND_FOR_DELETE => 'Product with specified SKU not found',
288 ValidatorInterface::ERROR_SUPER_PRODUCTS_SKU_NOT_FOUND => 'Product with specified super products SKU not found',
289 ValidatorInterface::ERROR_MEDIA_DATA_INCOMPLETE => 'Media data is incomplete',
290 ValidatorInterface::ERROR_EXCEEDED_MAX_LENGTH => 'Attribute %s exceeded max length',
291 ValidatorInterface::ERROR_INVALID_ATTRIBUTE_TYPE => 'Value for \'%s\' attribute contains incorrect value',
292 ValidatorInterface::ERROR_ABSENT_REQUIRED_ATTRIBUTE => 'Attribute %s is required',
293 ValidatorInterface::ERROR_INVALID_ATTRIBUTE_OPTION => 'Value for \'%s\' attribute contains incorrect value, see acceptable values on settings specified for Admin',
294 ValidatorInterface::ERROR_DUPLICATE_UNIQUE_ATTRIBUTE => 'Duplicated unique attribute',
295 ValidatorInterface::ERROR_INVALID_VARIATIONS_CUSTOM_OPTIONS => 'Value for \'%s\' sub attribute in \'%s\' attribute contains incorrect value, acceptable values are: \'dropdown\', \'checkbox\', \'radio\', \'text\'',
296 ValidatorInterface::ERROR_INVALID_MEDIA_URL_OR_PATH => 'Wrong URL/path used for attribute %s',
297 ValidatorInterface::ERROR_MEDIA_PATH_NOT_ACCESSIBLE => 'Imported resource (image) does not exist in the local media storage',
298 ValidatorInterface::ERROR_MEDIA_URL_NOT_ACCESSIBLE => 'Imported resource (image) could not be downloaded from external resource due to timeout or access permissions',
299 ValidatorInterface::ERROR_INVALID_WEIGHT => 'Product weight is invalid',
300 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',
301 ValidatorInterface::ERROR_DUPLICATE_MULTISELECT_VALUES => 'Value for multiselect attribute %s contains duplicated values',
302 'invalidNewToDateValue' => 'Make sure new_to_date is later than or the same as new_from_date',
303 ];
304 //@codingStandardsIgnoreEnd
305
306 /**
307 * Map between import file fields and system fields/attributes.
308 *
309 * @var array
310 */
311 protected $_fieldsMap = [
312 'image' => 'base_image',
313 'image_label' => "base_image_label",
314 'thumbnail' => 'thumbnail_image',
315 'thumbnail_label' => 'thumbnail_image_label',
316 self::COL_MEDIA_IMAGE => 'additional_images',
317 '_media_image_label' => 'additional_image_labels',
318 '_media_is_disabled' => 'hide_from_product_page',
319 Product::COL_STORE => 'store_view_code',
320 Product::COL_ATTR_SET => 'attribute_set_code',
321 Product::COL_TYPE => 'product_type',
322 Product::COL_PRODUCT_WEBSITES => 'product_websites',
323 'status' => 'product_online',
324 'news_from_date' => 'new_from_date',
325 'news_to_date' => 'new_to_date',
326 'options_container' => 'display_product_options_in',
327 'minimal_price' => 'map_price',
328 'msrp' => 'msrp_price',
329 'msrp_enabled' => 'map_enabled',
330 'special_from_date' => 'special_price_from_date',
331 'special_to_date' => 'special_price_to_date',
332 'min_qty' => 'out_of_stock_qty',
333 'backorders' => 'allow_backorders',
334 'min_sale_qty' => 'min_cart_qty',
335 'max_sale_qty' => 'max_cart_qty',
336 'notify_stock_qty' => 'notify_on_stock_below',
337 '_related_sku' => 'related_skus',
338 '_related_position' => 'related_position',
339 '_crosssell_sku' => 'crosssell_skus',
340 '_crosssell_position' => 'crosssell_position',
341 '_upsell_sku' => 'upsell_skus',
342 '_upsell_position' => 'upsell_position',
343 'meta_keyword' => 'meta_keywords',
344 ];
345
346 /**
347 * Existing products SKU-related information in form of array:
348 *
349 * [SKU] => array(
350 * 'type_id' => (string) product type
351 * 'attr_set_id' => (int) product attribute set ID
352 * 'entity_id' => (int) product ID
353 * 'supported_type' => (boolean) is product type supported by current version of import module
354 * )
355 *
356 * @var array
357 */
358 protected $_oldSku = [];
359
360 /**
361 * Column names that holds values with particular meaning.
362 *
363 * @var string[]
364 */
365 protected $_specialAttributes = [
366 self::COL_STORE,
367 self::COL_ATTR_SET,
368 self::COL_TYPE,
369 self::COL_CATEGORY,
370 '_product_websites',
371 self::COL_PRODUCT_WEBSITES,
372 '_tier_price_website',
373 '_tier_price_customer_group',
374 '_tier_price_qty',
375 '_tier_price_price',
376 '_related_sku',
377 '_related_position',
378 '_crosssell_sku',
379 '_crosssell_position',
380 '_upsell_sku',
381 '_upsell_position',
382 '_custom_option_store',
383 '_custom_option_type',
384 '_custom_option_title',
385 '_custom_option_is_required',
386 '_custom_option_price',
387 '_custom_option_sku',
388 '_custom_option_max_characters',
389 '_custom_option_sort_order',
390 '_custom_option_file_extension',
391 '_custom_option_image_size_x',
392 '_custom_option_image_size_y',
393 '_custom_option_row_title',
394 '_custom_option_row_price',
395 '_custom_option_row_sku',
396 '_custom_option_row_sort',
397 '_media_attribute_id',
398 self::COL_MEDIA_IMAGE,
399 '_media_label',
400 '_media_position',
401 '_media_is_disabled',
402 ];
403
404 /**
405 * @var array
406 */
407 protected $defaultStockData = [
408 'manage_stock' => 1,
409 'use_config_manage_stock' => 1,
410 'qty' => 0,
411 'min_qty' => 0,
412 'use_config_min_qty' => 1,
413 'min_sale_qty' => 1,
414 'use_config_min_sale_qty' => 1,
415 'max_sale_qty' => 10000,
416 'use_config_max_sale_qty' => 1,
417 'is_qty_decimal' => 0,
418 'backorders' => 0,
419 'use_config_backorders' => 1,
420 'notify_stock_qty' => 1,
421 'use_config_notify_stock_qty' => 1,
422 'enable_qty_increments' => 0,
423 'use_config_enable_qty_inc' => 1,
424 'qty_increments' => 0,
425 'use_config_qty_increments' => 1,
426 'is_in_stock' => 1,
427 'low_stock_date' => null,
428 'stock_status_changed_auto' => 0,
429 'is_decimal_divided' => 0,
430 ];
431
432 /**
433 * Column names that holds images files names
434 *
435 * Note: the order of array items has a value in order to properly set 'position' value
436 * of media gallery items.
437 *
438 * @var string[]
439 */
440 protected $_imagesArrayKeys = [];
441
442 /**
443 * Permanent entity columns.
444 *
445 * @var string[]
446 */
447 protected $_permanentAttributes = [self::COL_SKU];
448
449 /**
450 * Array of supported product types as keys with appropriate model object as value.
451 *
452 * @var \Magento\CatalogImportExport\Model\Import\Product\Type\AbstractType[]
453 */
454 protected $_productTypeModels = [];
455
456 /**
457 * Media files uploader
458 *
459 * @var \Magento\CatalogImportExport\Model\Import\Uploader
460 */
461 protected $_fileUploader;
462
463 /**
464 * Import entity which provide import of product custom options
465 *
466 * @var \Magento\CatalogImportExport\Model\Import\Product\Option
467 */
468 protected $_optionEntity;
469
470 /**
471 * Catalog data
472 *
473 * @var \Magento\Catalog\Helper\Data
474 */
475 protected $_catalogData = null;
476
477 /**
478 * @var \Magento\CatalogInventory\Api\StockRegistryInterface
479 */
480 protected $stockRegistry;
481
482 /**
483 * @var \Magento\CatalogInventory\Api\StockConfigurationInterface
484 */
485 protected $stockConfiguration;
486
487 /**
488 * @var \Magento\CatalogInventory\Model\Spi\StockStateProviderInterface
489 */
490 protected $stockStateProvider;
491
492 /**
493 * Core event manager proxy
494 *
495 * @var \Magento\Framework\Event\ManagerInterface
496 */
497 protected $_eventManager = null;
498
499 /**
500 * @var \Magento\ImportExport\Model\Import\Config
501 */
502 protected $_importConfig;
503
504 /**
505 * @var \Magento\CatalogImportExport\Model\Import\Proxy\Product\ResourceModelFactory
506 */
507 protected $_resourceFactory;
508
509 /**
510 * @var \Magento\CatalogImportExport\Model\Import\Proxy\Product\ResourceModel
511 */
512 protected $_resource;
513
514 /**
515 * @var \Magento\Eav\Model\ResourceModel\Entity\Attribute\Set\CollectionFactory
516 */
517 protected $_setColFactory;
518
519 /**
520 * @var \Magento\CatalogImportExport\Model\Import\Product\Type\Factory
521 */
522 protected $_productTypeFactory;
523
524 /**
525 * @var \Magento\Catalog\Model\ResourceModel\Product\LinkFactory
526 */
527 protected $_linkFactory;
528
529 /**
530 * @var \Magento\CatalogImportExport\Model\Import\Proxy\ProductFactory
531 */
532 protected $_proxyProdFactory;
533
534 /**
535 * @var \Magento\CatalogImportExport\Model\Import\UploaderFactory
536 */
537 protected $_uploaderFactory;
538
539 /**
540 * @var \Magento\Framework\Filesystem\Directory\WriteInterface
541 */
542 protected $_mediaDirectory;
543
544 /**
545 * @var \Magento\CatalogInventory\Model\ResourceModel\Stock\ItemFactory
546 * @deprecated this variable isn't used anymore.
547 */
548 protected $_stockResItemFac;
549
550 /**
551 * @var \Magento\Framework\Stdlib\DateTime\TimezoneInterface
552 */
553 protected $_localeDate;
554
555 /**
556 * @var DateTime
557 */
558 protected $dateTime;
559
560 /**
561 * @var \Magento\Framework\Indexer\IndexerRegistry
562 */
563 protected $indexerRegistry;
564
565 /**
566 * @var Product\StoreResolver
567 */
568 protected $storeResolver;
569
570 /**
571 * @var Product\SkuProcessor
572 */
573 protected $skuProcessor;
574
575 /**
576 * @var Product\CategoryProcessor
577 */
578 protected $categoryProcessor;
579
580 /**
581 * @var \Magento\Framework\App\Config\ScopeConfigInterface
582 * @since 100.0.3
583 */
584 protected $scopeConfig;
585
586 /**
587 * @var \Magento\Catalog\Model\Product\Url
588 * @since 100.0.3
589 */
590 protected $productUrl;
591
592 /**
593 * @var array
594 */
595 protected $websitesCache = [];
596
597 /**
598 * @var array
599 */
600 protected $categoriesCache = [];
601
602 /**
603 * @var array
604 * @since 100.0.3
605 */
606 protected $productUrlSuffix = [];
607
608 /**
609 * @var array
610 * @deprecated 100.1.5
611 * @since 100.0.3
612 */
613 protected $productUrlKeys = [];
614
615 /**
616 * Instance of product tax class processor.
617 *
618 * @var Product\TaxClassProcessor
619 */
620 protected $taxClassProcessor;
621
622 /**
623 * @var Product\Validator
624 */
625 protected $validator;
626
627 /**
628 * Array of validated rows.
629 *
630 * @var array
631 */
632 protected $validatedRows;
633
634 /**
635 * @var \Psr\Log\LoggerInterface
636 */
637 private $_logger;
638
639 /**
640 * {@inheritdoc}
641 */
642 protected $masterAttributeCode = 'sku';
643
644 /**
645 * @var ObjectRelationProcessor
646 */
647 protected $objectRelationProcessor;
648
649 /**
650 * @var TransactionManagerInterface
651 */
652 protected $transactionManager;
653
654 /**
655 * Flag for replace operation.
656 *
657 * @var null
658 */
659 protected $_replaceFlag = null;
660
661 /**
662 * Flag for replace operation.
663 *
664 * @var null
665 */
666 protected $cachedImages = null;
667
668 /**
669 * @var array
670 * @since 100.0.3
671 */
672 protected $urlKeys = [];
673
674 /**
675 * @var array
676 * @since 100.0.3
677 */
678 protected $rowNumbers = [];
679
680 /**
681 * Product entity link field
682 *
683 * @var string
684 */
685 private $productEntityLinkField;
686
687 /**
688 * Product entity identifier field
689 *
690 * @var string
691 */
692 private $productEntityIdentifierField;
693
694 /**
695 * Escaped separator value for regular expression.
696 * The value is based on PSEUDO_MULTI_LINE_SEPARATOR constant.
697 * @var string
698 */
699 private $multiLineSeparatorForRegexp;
700
701 /**
702 * Container for filesystem object.
703 *
704 * @var Filesystem
705 */
706 private $filesystem;
707
708 /**
709 * Catalog config.
710 *
711 * @var CatalogConfig
712 */
713 private $catalogConfig;
714
715 /**
716 * Stock Item Importer
717 *
718 * @var StockItemImporterInterface
719 */
720 private $stockItemImporter;
721
722 /**
723 * @var ImageTypeProcessor
724 */
725 private $imageTypeProcessor;
726
727 /**
728 * Provide ability to process and save images during import.
729 *
730 * @var MediaGalleryProcessor
731 */
732 private $mediaProcessor;
733
734 /**
735 * @var DateTimeFactory
736 */
737 private $dateTimeFactory;
738
739 /**
740 * @var ProductRepositoryInterface
741 */
742 private $productRepository;
743
744 /**
745 * @param \Magento\Framework\Json\Helper\Data $jsonHelper
746 * @param \Magento\ImportExport\Helper\Data $importExportData
747 * @param \Magento\ImportExport\Model\ResourceModel\Import\Data $importData
748 * @param \Magento\Eav\Model\Config $config
749 * @param \Magento\Framework\App\ResourceConnection $resource
750 * @param \Magento\ImportExport\Model\ResourceModel\Helper $resourceHelper
751 * @param \Magento\Framework\Stdlib\StringUtils $string
752 * @param ProcessingErrorAggregatorInterface $errorAggregator
753 * @param \Magento\Framework\Event\ManagerInterface $eventManager
754 * @param \Magento\CatalogInventory\Api\StockRegistryInterface $stockRegistry
755 * @param \Magento\CatalogInventory\Api\StockConfigurationInterface $stockConfiguration
756 * @param \Magento\CatalogInventory\Model\Spi\StockStateProviderInterface $stockStateProvider
757 * @param \Magento\Catalog\Helper\Data $catalogData
758 * @param \Magento\ImportExport\Model\Import\Config $importConfig
759 * @param Proxy\Product\ResourceModelFactory $resourceFactory
760 * @param Product\OptionFactory $optionFactory
761 * @param \Magento\Eav\Model\ResourceModel\Entity\Attribute\Set\CollectionFactory $setColFactory
762 * @param Product\Type\Factory $productTypeFactory
763 * @param \Magento\Catalog\Model\ResourceModel\Product\LinkFactory $linkFactory
764 * @param Proxy\ProductFactory $proxyProdFactory
765 * @param UploaderFactory $uploaderFactory
766 * @param \Magento\Framework\Filesystem $filesystem
767 * @param \Magento\CatalogInventory\Model\ResourceModel\Stock\ItemFactory $stockResItemFac
768 * @param DateTime\TimezoneInterface $localeDate
769 * @param DateTime $dateTime
770 * @param \Psr\Log\LoggerInterface $logger
771 * @param \Magento\Framework\Indexer\IndexerRegistry $indexerRegistry
772 * @param Product\StoreResolver $storeResolver
773 * @param Product\SkuProcessor $skuProcessor
774 * @param Product\CategoryProcessor $categoryProcessor
775 * @param Product\Validator $validator
776 * @param ObjectRelationProcessor $objectRelationProcessor
777 * @param TransactionManagerInterface $transactionManager
778 * @param Product\TaxClassProcessor $taxClassProcessor
779 * @param \Magento\Framework\App\Config\ScopeConfigInterface $scopeConfig
780 * @param \Magento\Catalog\Model\Product\Url $productUrl
781 * @param array $data
782 * @param array $dateAttrCodes
783 * @param CatalogConfig $catalogConfig
784 * @param ImageTypeProcessor $imageTypeProcessor
785 * @param MediaGalleryProcessor $mediaProcessor
786 * @param StockItemImporterInterface|null $stockItemImporter
787 * @param DateTimeFactory $dateTimeFactory
788 * @param ProductRepositoryInterface|null $productRepository
789 * @throws LocalizedException
790 * @throws \Magento\Framework\Exception\FileSystemException
791 * @SuppressWarnings(PHPMD.ExcessiveParameterList)
792 * @SuppressWarnings(PHPMD.ExcessiveMethodLength)
793 */
794 public function __construct(
795 \Magento\Framework\Json\Helper\Data $jsonHelper,
796 \Magento\ImportExport\Helper\Data $importExportData,
797 \Magento\ImportExport\Model\ResourceModel\Import\Data $importData,
798 \Magento\Eav\Model\Config $config,
799 \Magento\Framework\App\ResourceConnection $resource,
800 \Magento\ImportExport\Model\ResourceModel\Helper $resourceHelper,
801 \Magento\Framework\Stdlib\StringUtils $string,
802 ProcessingErrorAggregatorInterface $errorAggregator,
803 \Magento\Framework\Event\ManagerInterface $eventManager,
804 \Magento\CatalogInventory\Api\StockRegistryInterface $stockRegistry,
805 \Magento\CatalogInventory\Api\StockConfigurationInterface $stockConfiguration,
806 \Magento\CatalogInventory\Model\Spi\StockStateProviderInterface $stockStateProvider,
807 \Magento\Catalog\Helper\Data $catalogData,
808 \Magento\ImportExport\Model\Import\Config $importConfig,
809 \Magento\CatalogImportExport\Model\Import\Proxy\Product\ResourceModelFactory $resourceFactory,
810 \Magento\CatalogImportExport\Model\Import\Product\OptionFactory $optionFactory,
811 \Magento\Eav\Model\ResourceModel\Entity\Attribute\Set\CollectionFactory $setColFactory,
812 \Magento\CatalogImportExport\Model\Import\Product\Type\Factory $productTypeFactory,
813 \Magento\Catalog\Model\ResourceModel\Product\LinkFactory $linkFactory,
814 \Magento\CatalogImportExport\Model\Import\Proxy\ProductFactory $proxyProdFactory,
815 \Magento\CatalogImportExport\Model\Import\UploaderFactory $uploaderFactory,
816 \Magento\Framework\Filesystem $filesystem,
817 \Magento\CatalogInventory\Model\ResourceModel\Stock\ItemFactory $stockResItemFac,
818 \Magento\Framework\Stdlib\DateTime\TimezoneInterface $localeDate,
819 DateTime $dateTime,
820 \Psr\Log\LoggerInterface $logger,
821 \Magento\Framework\Indexer\IndexerRegistry $indexerRegistry,
822 Product\StoreResolver $storeResolver,
823 Product\SkuProcessor $skuProcessor,
824 Product\CategoryProcessor $categoryProcessor,
825 Product\Validator $validator,
826 ObjectRelationProcessor $objectRelationProcessor,
827 TransactionManagerInterface $transactionManager,
828 Product\TaxClassProcessor $taxClassProcessor,
829 \Magento\Framework\App\Config\ScopeConfigInterface $scopeConfig,
830 \Magento\Catalog\Model\Product\Url $productUrl,
831 array $data = [],
832 array $dateAttrCodes = [],
833 CatalogConfig $catalogConfig = null,
834 ImageTypeProcessor $imageTypeProcessor = null,
835 MediaGalleryProcessor $mediaProcessor = null,
836 StockItemImporterInterface $stockItemImporter = null,
837 DateTimeFactory $dateTimeFactory = null,
838 ProductRepositoryInterface $productRepository = null
839 ) {
840 $this->_eventManager = $eventManager;
841 $this->stockRegistry = $stockRegistry;
842 $this->stockConfiguration = $stockConfiguration;
843 $this->stockStateProvider = $stockStateProvider;
844 $this->_catalogData = $catalogData;
845 $this->_importConfig = $importConfig;
846 $this->_resourceFactory = $resourceFactory;
847 $this->_setColFactory = $setColFactory;
848 $this->_productTypeFactory = $productTypeFactory;
849 $this->_linkFactory = $linkFactory;
850 $this->_proxyProdFactory = $proxyProdFactory;
851 $this->_uploaderFactory = $uploaderFactory;
852 $this->filesystem = $filesystem;
853 $this->_mediaDirectory = $filesystem->getDirectoryWrite(DirectoryList::ROOT);
854 $this->_stockResItemFac = $stockResItemFac;
855 $this->_localeDate = $localeDate;
856 $this->dateTime = $dateTime;
857 $this->indexerRegistry = $indexerRegistry;
858 $this->_logger = $logger;
859 $this->storeResolver = $storeResolver;
860 $this->skuProcessor = $skuProcessor;
861 $this->categoryProcessor = $categoryProcessor;
862 $this->validator = $validator;
863 $this->objectRelationProcessor = $objectRelationProcessor;
864 $this->transactionManager = $transactionManager;
865 $this->taxClassProcessor = $taxClassProcessor;
866 $this->scopeConfig = $scopeConfig;
867 $this->productUrl = $productUrl;
868 $this->dateAttrCodes = array_merge($this->dateAttrCodes, $dateAttrCodes);
869 $this->catalogConfig = $catalogConfig ?: ObjectManager::getInstance()->get(CatalogConfig::class);
870 $this->imageTypeProcessor = $imageTypeProcessor ?: ObjectManager::getInstance()->get(ImageTypeProcessor::class);
871 $this->mediaProcessor = $mediaProcessor ?: ObjectManager::getInstance()->get(MediaGalleryProcessor::class);
872 $this->stockItemImporter = $stockItemImporter ?: ObjectManager::getInstance()
873 ->get(StockItemImporterInterface::class);
874 parent::__construct(
875 $jsonHelper,
876 $importExportData,
877 $importData,
878 $config,
879 $resource,
880 $resourceHelper,
881 $string,
882 $errorAggregator
883 );
884 $this->_optionEntity = $data['option_entity'] ??
885 $optionFactory->create(['data' => ['product_entity' => $this]]);
886 $this->_initAttributeSets()
887 ->_initTypeModels()
888 ->_initSkus()
889 ->initImagesArrayKeys();
890 $this->validator->init($this);
891 $this->dateTimeFactory = $dateTimeFactory ?? ObjectManager::getInstance()->get(DateTimeFactory::class);
892 $this->productRepository = $productRepository ?? ObjectManager::getInstance()
893 ->get(ProductRepositoryInterface::class);
894 }
895
896 /**
897 * Check one attribute. Can be overridden in child.
898 *
899 * @param string $attrCode Attribute code
900 * @param array $attrParams Attribute params
901 * @param array $rowData Row data
902 * @param int $rowNum
903 * @return bool
904 */
905 public function isAttributeValid($attrCode, array $attrParams, array $rowData, $rowNum)
906 {
907 if (!$this->validator->isAttributeValid($attrCode, $attrParams, $rowData)) {
908 foreach ($this->validator->getMessages() as $message) {
909 $this->skipRow($rowNum, $message, ProcessingError::ERROR_LEVEL_NOT_CRITICAL, $attrCode);
910 }
911 return false;
912 }
913 return true;
914 }
915
916 /**
917 * Multiple value separator getter.
918 *
919 * @return string
920 */
921 public function getMultipleValueSeparator()
922 {
923 if (!empty($this->_parameters[Import::FIELD_FIELD_MULTIPLE_VALUE_SEPARATOR])) {
924 return $this->_parameters[Import::FIELD_FIELD_MULTIPLE_VALUE_SEPARATOR];
925 }
926 return Import::DEFAULT_GLOBAL_MULTI_VALUE_SEPARATOR;
927 }
928
929 /**
930 * Return empty attribute value constant
931 *
932 * @return string
933 */
934 public function getEmptyAttributeValueConstant()
935 {
936 if (!empty($this->_parameters[Import::FIELD_EMPTY_ATTRIBUTE_VALUE_CONSTANT])) {
937 return $this->_parameters[Import::FIELD_EMPTY_ATTRIBUTE_VALUE_CONSTANT];
938 }
939 return Import::DEFAULT_EMPTY_ATTRIBUTE_VALUE_CONSTANT;
940 }
941
942 /**
943 * Retrieve instance of product custom options import entity
944 *
945 * @return \Magento\CatalogImportExport\Model\Import\Product\Option
946 */
947 public function getOptionEntity()
948 {
949 return $this->_optionEntity;
950 }
951
952 /**
953 * Retrieve id of media gallery attribute.
954 *
955 * @return int
956 */
957 public function getMediaGalleryAttributeId()
958 {
959 if (!$this->_mediaGalleryAttributeId) {
960 /** @var $resource \Magento\CatalogImportExport\Model\Import\Proxy\Product\ResourceModel */
961 $resource = $this->_resourceFactory->create();
962 $this->_mediaGalleryAttributeId = $resource->getAttribute(self::MEDIA_GALLERY_ATTRIBUTE_CODE)->getId();
963 }
964 return $this->_mediaGalleryAttributeId;
965 }
966
967 /**
968 * Retrieve product type by name.
969 *
970 * @param string $name
971 * @return Product\Type\AbstractType
972 */
973 public function retrieveProductTypeByName($name)
974 {
975 if (isset($this->_productTypeModels[$name])) {
976 return $this->_productTypeModels[$name];
977 }
978 return null;
979 }
980
981 /**
982 * Set import parameters
983 *
984 * @param array $params
985 * @return $this
986 */
987 public function setParameters(array $params)
988 {
989 parent::setParameters($params);
990 $this->getOptionEntity()->setParameters($params);
991
992 return $this;
993 }
994
995 /**
996 * Delete products for replacement.
997 *
998 * @return $this
999 */
1000 public function deleteProductsForReplacement()
1001 {
1002 $this->setParameters(array_merge(
1003 $this->getParameters(),
1004 ['behavior' => Import::BEHAVIOR_DELETE]
1005 ));
1006 $this->_deleteProducts();
1007
1008 return $this;
1009 }
1010
1011 /**
1012 * Delete products.
1013 *
1014 * @return $this
1015 * @throws \Exception
1016 */
1017 protected function _deleteProducts()
1018 {
1019 $productEntityTable = $this->_resourceFactory->create()->getEntityTable();
1020
1021 while ($bunch = $this->_dataSourceModel->getNextBunch()) {
1022 $idsToDelete = [];
1023
1024 foreach ($bunch as $rowNum => $rowData) {
1025 if ($this->validateRow($rowData, $rowNum) && self::SCOPE_DEFAULT == $this->getRowScope($rowData)) {
1026 $idsToDelete[] = $this->getExistingSku($rowData[self::COL_SKU])['entity_id'];
1027 }
1028 }
1029 if ($idsToDelete) {
1030 $this->countItemsDeleted += count($idsToDelete);
1031 $this->transactionManager->start($this->_connection);
1032 try {
1033 $this->objectRelationProcessor->delete(
1034 $this->transactionManager,
1035 $this->_connection,
1036 $productEntityTable,
1037 $this->_connection->quoteInto('entity_id IN (?)', $idsToDelete),
1038 ['entity_id' => $idsToDelete]
1039 );
1040 $this->_eventManager->dispatch(
1041 'catalog_product_import_bunch_delete_commit_before',
1042 [
1043 'adapter' => $this,
1044 'bunch' => $bunch,
1045 'ids_to_delete' => $idsToDelete,
1046 ]
1047 );
1048 $this->transactionManager->commit();
1049 } catch (\Exception $e) {
1050 $this->transactionManager->rollBack();
1051 throw $e;
1052 }
1053 $this->_eventManager->dispatch(
1054 'catalog_product_import_bunch_delete_after',
1055 ['adapter' => $this, 'bunch' => $bunch]
1056 );
1057 }
1058 }
1059 return $this;
1060 }
1061
1062 /**
1063 * Create Product entity from raw data.
1064 *
1065 * @throws \Exception
1066 * @return bool Result of operation.
1067 * @SuppressWarnings(PHPMD.CyclomaticComplexity)
1068 */
1069 protected function _importData()
1070 {
1071 $this->_validatedRows = null;
1072 if (Import::BEHAVIOR_DELETE == $this->getBehavior()) {
1073 $this->_deleteProducts();
1074 } elseif (Import::BEHAVIOR_REPLACE == $this->getBehavior()) {
1075 $this->_replaceFlag = true;
1076 $this->_replaceProducts();
1077 } else {
1078 $this->_saveProductsData();
1079 }
1080 $this->_eventManager->dispatch('catalog_product_import_finish_before', ['adapter' => $this]);
1081 return true;
1082 }
1083
1084 /**
1085 * Replace imported products.
1086 *
1087 * @return $this
1088 */
1089 protected function _replaceProducts()
1090 {
1091 $this->deleteProductsForReplacement();
1092 $this->_oldSku = $this->skuProcessor->reloadOldSkus()->getOldSkus();
1093 $this->_validatedRows = null;
1094 $this->setParameters(array_merge(
1095 $this->getParameters(),
1096 ['behavior' => Import::BEHAVIOR_APPEND]
1097 ));
1098 $this->_saveProductsData();
1099
1100 return $this;
1101 }
1102
1103 /**
1104 * Save products data.
1105 *
1106 * @return $this
1107 */
1108 protected function _saveProductsData()
1109 {
1110 $this->_saveProducts();
1111 foreach ($this->_productTypeModels as $productTypeModel) {
1112 $productTypeModel->saveData();
1113 }
1114 $this->_saveLinks();
1115 $this->_saveStockItem();
1116 if ($this->_replaceFlag) {
1117 $this->getOptionEntity()->clearProductsSkuToId();
1118 }
1119 $this->getOptionEntity()->importData();
1120
1121 return $this;
1122 }
1123
1124 /**
1125 * Initialize attribute sets code-to-id pairs.
1126 *
1127 * @return $this
1128 */
1129 protected function _initAttributeSets()
1130 {
1131 foreach ($this->_setColFactory->create()->setEntityTypeFilter($this->_entityTypeId) as $attributeSet) {
1132 $this->_attrSetNameToId[$attributeSet->getAttributeSetName()] = $attributeSet->getId();
1133 $this->_attrSetIdToName[$attributeSet->getId()] = $attributeSet->getAttributeSetName();
1134 }
1135 return $this;
1136 }
1137
1138 /**
1139 * Initialize existent product SKUs.
1140 *
1141 * @return $this
1142 */
1143 protected function _initSkus()
1144 {
1145 $this->skuProcessor->setTypeModels($this->_productTypeModels);
1146 $this->_oldSku = $this->skuProcessor->reloadOldSkus()->getOldSkus();
1147 return $this;
1148 }
1149
1150 /**
1151 * Initialize image array keys.
1152 *
1153 * @return $this
1154 */
1155 private function initImagesArrayKeys()
1156 {
1157 $this->_imagesArrayKeys = $this->imageTypeProcessor->getImageTypes();
1158 return $this;
1159 }
1160
1161 /**
1162 * Initialize product type models.
1163 *
1164 * @return $this
1165 * @throws \Magento\Framework\Exception\LocalizedException
1166 */
1167 protected function _initTypeModels()
1168 {
1169 $productTypes = $this->_importConfig->getEntityTypes($this->getEntityTypeCode());
1170 foreach ($productTypes as $productTypeName => $productTypeConfig) {
1171 $params = [$this, $productTypeName];
1172 if (!($model = $this->_productTypeFactory->create($productTypeConfig['model'], ['params' => $params]))
1173 ) {
1174 throw new LocalizedException(
1175 __('Entity type model \'%1\' is not found', $productTypeConfig['model'])
1176 );
1177 }
1178 if (!$model instanceof \Magento\CatalogImportExport\Model\Import\Product\Type\AbstractType) {
1179 throw new LocalizedException(
1180 __(
1181 'Entity type model must be an instance of '
1182 . \Magento\CatalogImportExport\Model\Import\Product\Type\AbstractType::class
1183 )
1184 );
1185 }
1186 if ($model->isSuitable()) {
1187 $this->_productTypeModels[$productTypeName] = $model;
1188 }
1189 $this->_fieldsMap = array_merge($this->_fieldsMap, $model->getCustomFieldsMapping());
1190 $this->_specialAttributes = array_merge($this->_specialAttributes, $model->getParticularAttributes());
1191 }
1192 $this->_initErrorTemplates();
1193 // remove doubles
1194 $this->_specialAttributes = array_unique($this->_specialAttributes);
1195
1196 return $this;
1197 }
1198
1199 /**
1200 * Initialize Product error templates
1201 */
1202 protected function _initErrorTemplates()
1203 {
1204 foreach ($this->_messageTemplates as $errorCode => $template) {
1205 $this->addMessageTemplate($errorCode, $template);
1206 }
1207 }
1208
1209 /**
1210 * Set valid attribute set and product type to rows.
1211 *
1212 * Set valid attribute set and product type to rows with all
1213 * scopes to ensure that existing products doesn't changed.
1214 *
1215 * @param array $rowData
1216 * @return array
1217 */
1218 protected function _prepareRowForDb(array $rowData)
1219 {
1220 $rowData = $this->_customFieldsMapping($rowData);
1221
1222 $rowData = parent::_prepareRowForDb($rowData);
1223
1224 static $lastSku = null;
1225
1226 if (Import::BEHAVIOR_DELETE == $this->getBehavior()) {
1227 return $rowData;
1228 }
1229
1230 $lastSku = $rowData[self::COL_SKU];
1231
1232 if ($this->isSkuExist($lastSku)) {
1233 $newSku = $this->skuProcessor->getNewSku($lastSku);
1234 $rowData[self::COL_ATTR_SET] = $newSku['attr_set_code'];
1235 $rowData[self::COL_TYPE] = $newSku['type_id'];
1236 }
1237
1238 return $rowData;
1239 }
1240
1241 /**
1242 * Gather and save information about product links.
1243 *
1244 * Must be called after ALL products saving done.
1245 *
1246 * @return $this
1247 * @SuppressWarnings(PHPMD.CyclomaticComplexity)
1248 * @SuppressWarnings(PHPMD.NPathComplexity)
1249 * @SuppressWarnings(PHPMD.ExcessiveMethodLength)
1250 */
1251 protected function _saveLinks()
1252 {
1253 $resource = $this->_linkFactory->create();
1254 $mainTable = $resource->getMainTable();
1255 $positionAttrId = [];
1256 $nextLinkId = $this->_resourceHelper->getNextAutoincrement($mainTable);
1257
1258 // pre-load 'position' attributes ID for each link type once
1259 foreach ($this->_linkNameToId as $linkName => $linkId) {
1260 $select = $this->_connection->select()->from(
1261 $resource->getTable('catalog_product_link_attribute'),
1262 ['id' => 'product_link_attribute_id']
1263 )->where(
1264 'link_type_id = :link_id AND product_link_attribute_code = :position'
1265 );
1266 $bind = [':link_id' => $linkId, ':position' => 'position'];
1267 $positionAttrId[$linkId] = $this->_connection->fetchOne($select, $bind);
1268 }
1269 while ($bunch = $this->_dataSourceModel->getNextBunch()) {
1270 $productIds = [];
1271 $linkRows = [];
1272 $positionRows = [];
1273
1274 foreach ($bunch as $rowNum => $rowData) {
1275 if (!$this->isRowAllowedToImport($rowData, $rowNum)) {
1276 continue;
1277 }
1278
1279 $sku = $rowData[self::COL_SKU];
1280
1281 $productId = $this->skuProcessor->getNewSku($sku)[$this->getProductEntityLinkField()];
1282 $productLinkKeys = [];
1283 $select = $this->_connection->select()->from(
1284 $resource->getTable('catalog_product_link'),
1285 ['id' => 'link_id', 'linked_id' => 'linked_product_id', 'link_type_id' => 'link_type_id']
1286 )->where(
1287 'product_id = :product_id'
1288 );
1289 $bind = [':product_id' => $productId];
1290 foreach ($this->_connection->fetchAll($select, $bind) as $linkData) {
1291 $linkKey = "{$productId}-{$linkData['linked_id']}-{$linkData['link_type_id']}";
1292 $productLinkKeys[$linkKey] = $linkData['id'];
1293 }
1294 foreach ($this->_linkNameToId as $linkName => $linkId) {
1295 $productIds[] = $productId;
1296 if (isset($rowData[$linkName . 'sku'])) {
1297 $linkSkus = explode($this->getMultipleValueSeparator(), $rowData[$linkName . 'sku']);
1298 $linkPositions = !empty($rowData[$linkName . 'position'])
1299 ? explode($this->getMultipleValueSeparator(), $rowData[$linkName . 'position'])
1300 : [];
1301 foreach ($linkSkus as $linkedKey => $linkedSku) {
1302 $linkedSku = trim($linkedSku);
1303 if (($this->skuProcessor->getNewSku($linkedSku) !== null || $this->isSkuExist($linkedSku))
1304 && strcasecmp($linkedSku, $sku) !== 0
1305 ) {
1306 $newSku = $this->skuProcessor->getNewSku($linkedSku);
1307 if (!empty($newSku)) {
1308 $linkedId = $newSku['entity_id'];
1309 } else {
1310 $linkedId = $this->getExistingSku($linkedSku)['entity_id'];
1311 }
1312
1313 if ($linkedId == null) {
1314 // Import file links to a SKU which is skipped for some reason,
1315 // which leads to a "NULL"
1316 // link causing fatal errors.
1317 $this->_logger->critical(
1318 new \Exception(
1319 sprintf(
1320 'WARNING: Orphaned link skipped: From SKU %s (ID %d) to SKU %s, ' .
1321 'Link type id: %d',
1322 $sku,
1323 $productId,
1324 $linkedSku,
1325 $linkId
1326 )
1327 )
1328 );
1329 continue;
1330 }
1331
1332 $linkKey = "{$productId}-{$linkedId}-{$linkId}";
1333 if (empty($productLinkKeys[$linkKey])) {
1334 $productLinkKeys[$linkKey] = $nextLinkId;
1335 }
1336 if (!isset($linkRows[$linkKey])) {
1337 $linkRows[$linkKey] = [
1338 'link_id' => $productLinkKeys[$linkKey],
1339 'product_id' => $productId,
1340 'linked_product_id' => $linkedId,
1341 'link_type_id' => $linkId,
1342 ];
1343 }
1344 if (!empty($linkPositions[$linkedKey])) {
1345 $positionRows[] = [
1346 'link_id' => $productLinkKeys[$linkKey],
1347 'product_link_attribute_id' => $positionAttrId[$linkId],
1348 'value' => $linkPositions[$linkedKey],
1349 ];
1350 }
1351 $nextLinkId++;
1352 }
1353 }
1354 }
1355 }
1356 }
1357 if (Import::BEHAVIOR_APPEND != $this->getBehavior() && $productIds) {
1358 $this->_connection->delete(
1359 $mainTable,
1360 $this->_connection->quoteInto('product_id IN (?)', array_unique($productIds))
1361 );
1362 }
1363 if ($linkRows) {
1364 $this->_connection->insertOnDuplicate($mainTable, $linkRows, ['link_id']);
1365 }
1366 if ($positionRows) {
1367 // process linked product positions
1368 $this->_connection->insertOnDuplicate(
1369 $resource->getAttributeTypeTable('int'),
1370 $positionRows,
1371 ['value']
1372 );
1373 }
1374 }
1375 return $this;
1376 }
1377
1378 /**
1379 * Save product attributes.
1380 *
1381 * @param array $attributesData
1382 * @return $this
1383 */
1384 protected function _saveProductAttributes(array $attributesData)
1385 {
1386 $linkField = $this->getProductEntityLinkField();
1387 foreach ($attributesData as $tableName => $skuData) {
1388 $tableData = [];
1389 foreach ($skuData as $sku => $attributes) {
1390 $linkId = $this->_oldSku[strtolower($sku)][$linkField];
1391 foreach ($attributes as $attributeId => $storeValues) {
1392 foreach ($storeValues as $storeId => $storeValue) {
1393 $tableData[] = [
1394 $linkField => $linkId,
1395 'attribute_id' => $attributeId,
1396 'store_id' => $storeId,
1397 'value' => $storeValue,
1398 ];
1399 }
1400 }
1401 }
1402 $this->_connection->insertOnDuplicate($tableName, $tableData, ['value']);
1403 }
1404
1405 return $this;
1406 }
1407
1408 /**
1409 * Save product categories.
1410 *
1411 * @param array $categoriesData
1412 * @return $this
1413 */
1414 protected function _saveProductCategories(array $categoriesData)
1415 {
1416 static $tableName = null;
1417
1418 if (!$tableName) {
1419 $tableName = $this->_resourceFactory->create()->getProductCategoryTable();
1420 }
1421 if ($categoriesData) {
1422 $categoriesIn = [];
1423 $delProductId = [];
1424
1425 foreach ($categoriesData as $delSku => $categories) {
1426 $productId = $this->skuProcessor->getNewSku($delSku)['entity_id'];
1427 $delProductId[] = $productId;
1428
1429 foreach (array_keys($categories) as $categoryId) {
1430 $categoriesIn[] = ['product_id' => $productId, 'category_id' => $categoryId, 'position' => 0];
1431 }
1432 }
1433 if (Import::BEHAVIOR_APPEND != $this->getBehavior()) {
1434 $this->_connection->delete(
1435 $tableName,
1436 $this->_connection->quoteInto('product_id IN (?)', $delProductId)
1437 );
1438 }
1439 if ($categoriesIn) {
1440 $this->_connection->insertOnDuplicate($tableName, $categoriesIn, ['product_id', 'category_id']);
1441 }
1442 }
1443 return $this;
1444 }
1445
1446 /**
1447 * Update and insert data in entity table.
1448 *
1449 * @param array $entityRowsIn Row for insert
1450 * @param array $entityRowsUp Row for update
1451 * @return $this
1452 * @since 100.1.0
1453 */
1454 public function saveProductEntity(array $entityRowsIn, array $entityRowsUp)
1455 {
1456 static $entityTable = null;
1457 $this->countItemsCreated += count($entityRowsIn);
1458 $this->countItemsUpdated += count($entityRowsUp);
1459
1460 if (!$entityTable) {
1461 $entityTable = $this->_resourceFactory->create()->getEntityTable();
1462 }
1463 if ($entityRowsUp) {
1464 $this->_connection->insertOnDuplicate($entityTable, $entityRowsUp, ['updated_at', 'attribute_set_id']);
1465 }
1466 if ($entityRowsIn) {
1467 $this->_connection->insertMultiple($entityTable, $entityRowsIn);
1468
1469 $select = $this->_connection->select()->from(
1470 $entityTable,
1471 array_merge($this->getNewSkuFieldsForSelect(), $this->getOldSkuFieldsForSelect())
1472 )->where(
1473 $this->_connection->quoteInto('sku IN (?)', array_keys($entityRowsIn))
1474 );
1475 $newProducts = $this->_connection->fetchAll($select);
1476 foreach ($newProducts as $data) {
1477 $sku = $data['sku'];
1478 unset($data['sku']);
1479 foreach ($data as $key => $value) {
1480 $this->skuProcessor->setNewSkuData($sku, $key, $value);
1481 }
1482 }
1483
1484 $this->updateOldSku($newProducts);
1485 }
1486
1487 return $this;
1488 }
1489
1490 /**
1491 * Return additional data, needed to select.
1492 *
1493 * @return array
1494 */
1495 private function getOldSkuFieldsForSelect()
1496 {
1497 return ['type_id', 'attribute_set_id'];
1498 }
1499
1500 /**
1501 * Adds newly created products to _oldSku
1502 *
1503 * @param array $newProducts
1504 * @return void
1505 */
1506 private function updateOldSku(array $newProducts)
1507 {
1508 $oldSkus = [];
1509 foreach ($newProducts as $info) {
1510 $typeId = $info['type_id'];
1511 $sku = strtolower($info['sku']);
1512 $oldSkus[$sku] = [
1513 'type_id' => $typeId,
1514 'attr_set_id' => $info['attribute_set_id'],
1515 $this->getProductIdentifierField() => $info[$this->getProductIdentifierField()],
1516 'supported_type' => isset($this->_productTypeModels[$typeId]),
1517 $this->getProductEntityLinkField() => $info[$this->getProductEntityLinkField()],
1518 ];
1519 }
1520
1521 $this->_oldSku = array_replace($this->_oldSku, $oldSkus);
1522 }
1523
1524 /**
1525 * Get new SKU fields for select
1526 *
1527 * @return array
1528 */
1529 private function getNewSkuFieldsForSelect()
1530 {
1531 $fields = ['sku', $this->getProductEntityLinkField()];
1532 if ($this->getProductEntityLinkField() != $this->getProductIdentifierField()) {
1533 $fields[] = $this->getProductIdentifierField();
1534 }
1535 return $fields;
1536 }
1537
1538 /**
1539 * Init media gallery resources
1540 *
1541 * @return void
1542 * @since 100.0.4
1543 * @deprecated
1544 */
1545 protected function initMediaGalleryResources()
1546 {
1547 if (null == $this->mediaGalleryTableName) {
1548 $this->productEntityTableName = $this->getResource()->getTable('catalog_product_entity');
1549 $this->mediaGalleryTableName = $this->getResource()->getTable('catalog_product_entity_media_gallery');
1550 $this->mediaGalleryValueTableName = $this->getResource()->getTable(
1551 'catalog_product_entity_media_gallery_value'
1552 );
1553 $this->mediaGalleryEntityToValueTableName = $this->getResource()->getTable(
1554 'catalog_product_entity_media_gallery_value_to_entity'
1555 );
1556 }
1557 }
1558
1559 /**
1560 * Get existing images for current bunch
1561 *
1562 * @param array $bunch
1563 * @return array
1564 */
1565 protected function getExistingImages($bunch)
1566 {
1567 return $this->mediaProcessor->getExistingImages($bunch);
1568 }
1569
1570 /**
1571 * Retrieve image from row.
1572 *
1573 * @param array $rowData
1574 * @return array
1575 */
1576 public function getImagesFromRow(array $rowData)
1577 {
1578 $images = [];
1579 $labels = [];
1580 foreach ($this->_imagesArrayKeys as $column) {
1581 if (!empty($rowData[$column])) {
1582 $images[$column] = array_unique(
1583 array_map(
1584 'trim',
1585 explode($this->getMultipleValueSeparator(), $rowData[$column])
1586 )
1587 );
1588
1589 if (!empty($rowData[$column . '_label'])) {
1590 $labels[$column] = $this->parseMultipleValues($rowData[$column . '_label']);
1591
1592 if (count($labels[$column]) > count($images[$column])) {
1593 $labels[$column] = array_slice($labels[$column], 0, count($images[$column]));
1594 }
1595 }
1596 }
1597 }
1598
1599 return [$images, $labels];
1600 }
1601
1602 /**
1603 * Gather and save information about product entities.
1604 *
1605 * @return $this
1606 * @SuppressWarnings(PHPMD.CyclomaticComplexity)
1607 * @SuppressWarnings(PHPMD.NPathComplexity)
1608 * @SuppressWarnings(PHPMD.ExcessiveMethodLength)
1609 * @SuppressWarnings(PHPMD.UnusedLocalVariable)
1610 * @throws LocalizedException
1611 */
1612 protected function _saveProducts()
1613 {
1614 $priceIsGlobal = $this->_catalogData->isPriceGlobal();
1615 $productLimit = null;
1616 $productsQty = null;
1617 $entityLinkField = $this->getProductEntityLinkField();
1618
1619 while ($bunch = $this->_dataSourceModel->getNextBunch()) {
1620 $entityRowsIn = [];
1621 $entityRowsUp = [];
1622 $attributes = [];
1623 $this->websitesCache = [];
1624 $this->categoriesCache = [];
1625 $tierPrices = [];
1626 $mediaGallery = [];
1627 $labelsForUpdate = [];
1628 $imagesForChangeVisibility = [];
1629 $uploadedImages = [];
1630 $previousType = null;
1631 $prevAttributeSet = null;
1632 $importDir = $this->_mediaDirectory->getAbsolutePath($this->getImportDir());
1633
1634 $existingImages = $this->getExistingImages($bunch);
1635
1636 $this->addImageHashes($existingImages);
1637
1638 foreach ($bunch as $rowNum => $rowData) {
1639 // reset category processor's failed categories array
1640 $this->categoryProcessor->clearFailedCategories();
1641
1642 if (!$this->validateRow($rowData, $rowNum)) {
1643 continue;
1644 }
1645 if ($this->getErrorAggregator()->hasToBeTerminated()) {
1646 $this->getErrorAggregator()->addRowToSkip($rowNum);
1647 continue;
1648 }
1649 $rowScope = $this->getRowScope($rowData);
1650
1651 $urlKey = $this->getUrlKey($rowData);
1652 if (!empty($rowData[self::URL_KEY])) {
1653 // If url_key column and its value were in the CSV file
1654 $rowData[self::URL_KEY] = $urlKey;
1655 } elseif ($this->isNeedToChangeUrlKey($rowData)) {
1656 // If url_key column was empty or even not declared in the CSV file but by the rules it is need to
1657 // be setteed. In case when url_key is generating from name column we have to ensure that the bunch
1658 // of products will pass for the event with url_key column.
1659 $bunch[$rowNum][self::URL_KEY] = $rowData[self::URL_KEY] = $urlKey;
1660 }
1661
1662 $rowSku = $rowData[self::COL_SKU];
1663
1664 if (null === $rowSku) {
1665 $this->getErrorAggregator()->addRowToSkip($rowNum);
1666 continue;
1667 }
1668
1669 if (self::SCOPE_STORE == $rowScope) {
1670 // set necessary data from SCOPE_DEFAULT row
1671 $rowData[self::COL_TYPE] = $this->skuProcessor->getNewSku($rowSku)['type_id'];
1672 $rowData['attribute_set_id'] = $this->skuProcessor->getNewSku($rowSku)['attr_set_id'];
1673 $rowData[self::COL_ATTR_SET] = $this->skuProcessor->getNewSku($rowSku)['attr_set_code'];
1674 }
1675
1676 // 1. Entity phase
1677 if ($this->isSkuExist($rowSku)) {
1678 // existing row
1679 if (isset($rowData['attribute_set_code'])) {
1680 $attributeSetId = $this->catalogConfig->getAttributeSetId(
1681 $this->getEntityTypeId(),
1682 $rowData['attribute_set_code']
1683 );
1684
1685 // wrong attribute_set_code was received
1686 if (!$attributeSetId) {
1687 throw new LocalizedException(
1688 __(
1689 'Wrong attribute set code "%1", please correct it and try again.',
1690 $rowData['attribute_set_code']
1691 )
1692 );
1693 }
1694 } else {
1695 $attributeSetId = $this->skuProcessor->getNewSku($rowSku)['attr_set_id'];
1696 }
1697
1698 $entityRowsUp[] = [
1699 'updated_at' => (new \DateTime())->format(DateTime::DATETIME_PHP_FORMAT),
1700 'attribute_set_id' => $attributeSetId,
1701 $entityLinkField => $this->getExistingSku($rowSku)[$entityLinkField]
1702 ];
1703 } else {
1704 if (!$productLimit || $productsQty < $productLimit) {
1705 $entityRowsIn[strtolower($rowSku)] = [
1706 'attribute_set_id' => $this->skuProcessor->getNewSku($rowSku)['attr_set_id'],
1707 'type_id' => $this->skuProcessor->getNewSku($rowSku)['type_id'],
1708 'sku' => $rowSku,
1709 'has_options' => isset($rowData['has_options']) ? $rowData['has_options'] : 0,
1710 'created_at' => (new \DateTime())->format(DateTime::DATETIME_PHP_FORMAT),
1711 'updated_at' => (new \DateTime())->format(DateTime::DATETIME_PHP_FORMAT),
1712 ];
1713 $productsQty++;
1714 } else {
1715 $rowSku = null;
1716 // sign for child rows to be skipped
1717 $this->getErrorAggregator()->addRowToSkip($rowNum);
1718 continue;
1719 }
1720 }
1721
1722 if (!array_key_exists($rowSku, $this->websitesCache)) {
1723 $this->websitesCache[$rowSku] = [];
1724 }
1725 // 2. Product-to-Website phase
1726 if (!empty($rowData[self::COL_PRODUCT_WEBSITES])) {
1727 $websiteCodes = explode($this->getMultipleValueSeparator(), $rowData[self::COL_PRODUCT_WEBSITES]);
1728 foreach ($websiteCodes as $websiteCode) {
1729 $websiteId = $this->storeResolver->getWebsiteCodeToId($websiteCode);
1730 $this->websitesCache[$rowSku][$websiteId] = true;
1731 }
1732 } else {
1733 $product = $this->retrieveProductBySku($rowSku);
1734 if ($product) {
1735 $websiteIds = $product->getWebsiteIds();
1736 foreach ($websiteIds as $websiteId) {
1737 $this->websitesCache[$rowSku][$websiteId] = true;
1738 }
1739 }
1740 }
1741
1742 // 3. Categories phase
1743 if (!array_key_exists($rowSku, $this->categoriesCache)) {
1744 $this->categoriesCache[$rowSku] = [];
1745 }
1746 $rowData['rowNum'] = $rowNum;
1747 $categoryIds = $this->processRowCategories($rowData);
1748 foreach ($categoryIds as $id) {
1749 $this->categoriesCache[$rowSku][$id] = true;
1750 }
1751 unset($rowData['rowNum']);
1752
1753 // 4.1. Tier prices phase
1754 if (!empty($rowData['_tier_price_website'])) {
1755 $tierPrices[$rowSku][] = [
1756 'all_groups' => $rowData['_tier_price_customer_group'] == self::VALUE_ALL,
1757 'customer_group_id' => $rowData['_tier_price_customer_group'] ==
1758 self::VALUE_ALL ? 0 : $rowData['_tier_price_customer_group'],
1759 'qty' => $rowData['_tier_price_qty'],
1760 'value' => $rowData['_tier_price_price'],
1761 'website_id' => self::VALUE_ALL == $rowData['_tier_price_website'] ||
1762 $priceIsGlobal ? 0 : $this->storeResolver->getWebsiteCodeToId($rowData['_tier_price_website']),
1763 ];
1764 }
1765
1766 if (!$this->validateRow($rowData, $rowNum)) {
1767 continue;
1768 }
1769
1770 // 5. Media gallery phase
1771 list($rowImages, $rowLabels) = $this->getImagesFromRow($rowData);
1772 $storeId = !empty($rowData[self::COL_STORE])
1773 ? $this->getStoreIdByCode($rowData[self::COL_STORE])
1774 : Store::DEFAULT_STORE_ID;
1775 $imageHiddenStates = $this->getImagesHiddenStates($rowData);
1776 foreach (array_keys($imageHiddenStates) as $image) {
1777 if (array_key_exists($rowSku, $existingImages)
1778 && array_key_exists($image, $existingImages[$rowSku])
1779 ) {
1780 $rowImages[self::COL_MEDIA_IMAGE][] = $image;
1781 $uploadedImages[$image] = $image;
1782 }
1783
1784 if (empty($rowImages)) {
1785 $rowImages[self::COL_MEDIA_IMAGE][] = $image;
1786 }
1787 }
1788
1789 $rowData[self::COL_MEDIA_IMAGE] = [];
1790
1791 /*
1792 * Note: to avoid problems with undefined sorting, the value of media gallery items positions
1793 * must be unique in scope of one product.
1794 */
1795 $position = 0;
1796 foreach ($rowImages as $column => $columnImages) {
1797 foreach ($columnImages as $columnImageKey => $columnImage) {
1798 $filename = $importDir . DIRECTORY_SEPARATOR . $columnImage;
1799 $hash = '';
1800 if ($this->_mediaDirectory->isReadable($filename)) {
1801 $hash = md5_file($filename);
1802 }
1803
1804 if (!isset($existingImages[$rowSku])) {
1805 $imageAlreadyExists = false;
1806 } else {
1807 $imageAlreadyExists = array_reduce($existingImages[$rowSku], function ($exists, $file) use ($hash) {
1808 if ($exists) {
1809 return $exists;
1810 }
1811 if ($file['hash'] === $hash) {
1812 return $file['value'];
1813 }
1814 return $exists;
1815 }, '');
1816 }
1817
1818 if ($imageAlreadyExists) {
1819 $uploadedFile = $imageAlreadyExists;
1820 } else {
1821 if (!isset($uploadedImages[$columnImage])) {
1822 $uploadedFile = $this->uploadMediaFiles($columnImage);
1823 $uploadedFile = $uploadedFile ?: $this->getSystemFile($columnImage);
1824 if ($uploadedFile) {
1825 $uploadedImages[$columnImage] = $uploadedFile;
1826 } else {
1827 // unset($rowData[$column]);
1828 // $this->skipRow($rowNum, ValidatorInterface::ERROR_MEDIA_URL_NOT_ACCESSIBLE);
1829 }
1830 } else {
1831 $uploadedFile = $uploadedImages[$columnImage];
1832 }
1833 }
1834
1835 if ($uploadedFile && $column !== self::COL_MEDIA_IMAGE) {
1836 $rowData[$column] = $uploadedFile;
1837 }
1838
1839 if ($uploadedFile && !isset($mediaGallery[$storeId][$rowSku][$uploadedFile])) {
1840 if (isset($existingImages[$rowSku][$uploadedFile])) {
1841 $currentFileData = $existingImages[$rowSku][$uploadedFile];
1842 if (isset($rowLabels[$column][$columnImageKey])
1843 && $rowLabels[$column][$columnImageKey] !=
1844 $currentFileData['label']
1845 ) {
1846 $labelsForUpdate[] = [
1847 'label' => $rowLabels[$column][$columnImageKey],
1848 'imageData' => $currentFileData
1849 ];
1850 }
1851
1852 if (array_key_exists($uploadedFile, $imageHiddenStates)
1853 && $currentFileData['disabled'] != $imageHiddenStates[$uploadedFile]
1854 ) {
1855 $imagesForChangeVisibility[] = [
1856 'disabled' => $imageHiddenStates[$uploadedFile],
1857 'imageData' => $currentFileData
1858 ];
1859 }
1860 } else {
1861 if ($column == self::COL_MEDIA_IMAGE) {
1862 $rowData[$column][] = $uploadedFile;
1863 }
1864 $mediaGallery[$storeId][$rowSku][$uploadedFile] = [
1865 'attribute_id' => $this->getMediaGalleryAttributeId(),
1866 'label' => isset($rowLabels[$column][$columnImageKey])
1867 ? $rowLabels[$column][$columnImageKey]
1868 : '',
1869 'position' => ++$position,
1870 'disabled' => isset($imageHiddenStates[$columnImage])
1871 ? $imageHiddenStates[$columnImage] : '0',
1872 'value' => $uploadedFile,
1873 ];
1874 }
1875 }
1876 }
1877 }
1878
1879 // 6. Attributes phase
1880 $rowStore = (self::SCOPE_STORE == $rowScope)
1881 ? $this->storeResolver->getStoreCodeToId($rowData[self::COL_STORE])
1882 : 0;
1883 $productType = isset($rowData[self::COL_TYPE]) ? $rowData[self::COL_TYPE] : null;
1884 if ($productType !== null) {
1885 $previousType = $productType;
1886 }
1887 if (isset($rowData[self::COL_ATTR_SET])) {
1888 $prevAttributeSet = $rowData[self::COL_ATTR_SET];
1889 }
1890 if (self::SCOPE_NULL == $rowScope) {
1891 // for multiselect attributes only
1892 if ($prevAttributeSet !== null) {
1893 $rowData[self::COL_ATTR_SET] = $prevAttributeSet;
1894 }
1895 if ($productType === null && $previousType !== null) {
1896 $productType = $previousType;
1897 }
1898 if ($productType === null) {
1899 continue;
1900 }
1901 }
1902
1903 $productTypeModel = $this->_productTypeModels[$productType];
1904 if (!empty($rowData['tax_class_name'])) {
1905 $rowData['tax_class_id'] =
1906 $this->taxClassProcessor->upsertTaxClass($rowData['tax_class_name'], $productTypeModel);
1907 }
1908
1909 if ($this->getBehavior() == Import::BEHAVIOR_APPEND ||
1910 empty($rowData[self::COL_SKU])
1911 ) {
1912 $rowData = $productTypeModel->clearEmptyData($rowData);
1913 }
1914
1915 $rowData = $productTypeModel->prepareAttributesWithDefaultValueForSave(
1916 $rowData,
1917 !$this->isSkuExist($rowSku)
1918 );
1919 $product = $this->_proxyProdFactory->create(['data' => $rowData]);
1920
1921 foreach ($rowData as $attrCode => $attrValue) {
1922 $attribute = $this->retrieveAttributeByCode($attrCode);
1923
1924 if ('multiselect' != $attribute->getFrontendInput() && self::SCOPE_NULL == $rowScope) {
1925 // skip attribute processing for SCOPE_NULL rows
1926 continue;
1927 }
1928 $attrId = $attribute->getId();
1929 $backModel = $attribute->getBackendModel();
1930 $attrTable = $attribute->getBackend()->getTable();
1931 $storeIds = [0];
1932
1933 if ('datetime' == $attribute->getBackendType()
1934 && (
1935 in_array($attribute->getAttributeCode(), $this->dateAttrCodes)
1936 || $attribute->getIsUserDefined()
1937 )
1938 ) {
1939 $attrValue = $this->dateTime->formatDate($attrValue, false);
1940 } elseif ('datetime' == $attribute->getBackendType() && strtotime($attrValue)) {
1941 $attrValue = gmdate(
1942 'Y-m-d H:i:s',
1943 $this->_localeDate->date($attrValue)->getTimestamp()
1944 );
1945 } elseif ($backModel) {
1946 $attribute->getBackend()->beforeSave($product);
1947 $attrValue = $product->getData($attribute->getAttributeCode());
1948 }
1949 if (self::SCOPE_STORE == $rowScope) {
1950 if (self::SCOPE_WEBSITE == $attribute->getIsGlobal()) {
1951 // check website defaults already set
1952 if (!isset($attributes[$attrTable][$rowSku][$attrId][$rowStore])) {
1953 $storeIds = $this->storeResolver->getStoreIdToWebsiteStoreIds($rowStore);
1954 }
1955 } elseif (self::SCOPE_STORE == $attribute->getIsGlobal()) {
1956 $storeIds = [$rowStore];
1957 }
1958 if (!$this->isSkuExist($rowSku)) {
1959 $storeIds[] = 0;
1960 }
1961 }
1962 foreach ($storeIds as $storeId) {
1963 if (!isset($attributes[$attrTable][$rowSku][$attrId][$storeId])) {
1964 $attributes[$attrTable][$rowSku][$attrId][$storeId] = $attrValue;
1965 }
1966 }
1967 // restore 'backend_model' to avoid 'default' setting
1968 $attribute->setBackendModel($backModel);
1969 }
1970 }
1971
1972 foreach ($bunch as $rowNum => $rowData) {
1973 if ($this->getErrorAggregator()->isRowInvalid($rowNum)) {
1974 unset($bunch[$rowNum]);
1975 }
1976 }
1977
1978 $this->saveProductEntity(
1979 $entityRowsIn,
1980 $entityRowsUp
1981 )->_saveProductWebsites(
1982 $this->websitesCache
1983 )->_saveProductCategories(
1984 $this->categoriesCache
1985 )->_saveProductTierPrices(
1986 $tierPrices
1987 )->_saveMediaGallery(
1988 $mediaGallery
1989 )->_saveProductAttributes(
1990 $attributes
1991 )->updateMediaGalleryVisibility(
1992 $imagesForChangeVisibility
1993 )->updateMediaGalleryLabels(
1994 $labelsForUpdate
1995 );
1996
1997 $this->_eventManager->dispatch(
1998 'catalog_product_import_bunch_save_after',
1999 ['adapter' => $this, 'bunch' => $bunch]
2000 );
2001 }
2002
2003 return $this;
2004 }
2005
2006 /**
2007 * Generate md5 hashes for existing images for comparison with newly uploaded images.
2008 *
2009 * @param array $images
2010 */
2011 public function addImageHashes(&$images) {
2012 $dirConfig = DirectoryList::getDefaultConfig();
2013 $dirAddon = $dirConfig[DirectoryList::MEDIA][DirectoryList::PATH];
2014 $productPath = $this->_mediaDirectory->getAbsolutePath($dirAddon . '/catalog/product');
2015
2016 foreach ($images as $sku => $files) {
2017 foreach ($files as $path => $file) {
2018 $images[$sku][$path]['hash'] = md5_file($productPath . $file['value']);
2019 }
2020 }
2021 }
2022
2023 /**
2024 * Prepare array with image states (visible or hidden from product page)
2025 *
2026 * @param array $rowData
2027 * @return array
2028 */
2029 private function getImagesHiddenStates($rowData)
2030 {
2031 $statesArray = [];
2032 $mappingArray = [
2033 '_media_is_disabled' => '1'
2034 ];
2035
2036 foreach ($mappingArray as $key => $value) {
2037 if (isset($rowData[$key]) && strlen(trim($rowData[$key]))) {
2038 $items = explode($this->getMultipleValueSeparator(), $rowData[$key]);
2039
2040 foreach ($items as $item) {
2041 $statesArray[$item] = $value;
2042 }
2043 }
2044 }
2045
2046 return $statesArray;
2047 }
2048
2049 /**
2050 * Resolve valid category ids from provided row data.
2051 *
2052 * @param array $rowData
2053 * @return array
2054 */
2055 protected function processRowCategories($rowData)
2056 {
2057 $categoriesString = empty($rowData[self::COL_CATEGORY]) ? '' : $rowData[self::COL_CATEGORY];
2058 $categoryIds = [];
2059 if (!empty($categoriesString)) {
2060 $categoryIds = $this->categoryProcessor->upsertCategories(
2061 $categoriesString,
2062 $this->getMultipleValueSeparator()
2063 );
2064 foreach ($this->categoryProcessor->getFailedCategories() as $error) {
2065 $this->errorAggregator->addError(
2066 AbstractEntity::ERROR_CODE_CATEGORY_NOT_VALID,
2067 ProcessingError::ERROR_LEVEL_NOT_CRITICAL,
2068 $rowData['rowNum'],
2069 self::COL_CATEGORY,
2070 __('Category "%1" has not been created.', $error['category'])
2071 . ' ' . $error['exception']->getMessage()
2072 );
2073 }
2074 } else {
2075 $product = $this->retrieveProductBySku($rowData['sku']);
2076 if ($product) {
2077 $categoryIds = $product->getCategoryIds();
2078 }
2079 }
2080 return $categoryIds;
2081 }
2082
2083 /**
2084 * Get product websites.
2085 *
2086 * @param string $productSku
2087 * @return array
2088 */
2089 public function getProductWebsites($productSku)
2090 {
2091 return array_keys($this->websitesCache[$productSku]);
2092 }
2093
2094 /**
2095 * Retrieve product categories.
2096 *
2097 * @param string $productSku
2098 * @return array
2099 */
2100 public function getProductCategories($productSku)
2101 {
2102 return array_keys($this->categoriesCache[$productSku]);
2103 }
2104
2105 /**
2106 * Get store id by code.
2107 *
2108 * @param string $storeCode
2109 * @return array|int|null|string
2110 */
2111 public function getStoreIdByCode($storeCode)
2112 {
2113 if (empty($storeCode)) {
2114 return self::SCOPE_DEFAULT;
2115 }
2116 return $this->storeResolver->getStoreCodeToId($storeCode);
2117 }
2118
2119 /**
2120 * Save product tier prices.
2121 *
2122 * @param array $tierPriceData
2123 * @return $this
2124 */
2125 protected function _saveProductTierPrices(array $tierPriceData)
2126 {
2127 static $tableName = null;
2128
2129 if (!$tableName) {
2130 $tableName = $this->_resourceFactory->create()->getTable('catalog_product_entity_tier_price');
2131 }
2132 if ($tierPriceData) {
2133 $tierPriceIn = [];
2134 $delProductId = [];
2135
2136 foreach ($tierPriceData as $delSku => $tierPriceRows) {
2137 $productId = $this->skuProcessor->getNewSku($delSku)[$this->getProductEntityLinkField()];
2138 $delProductId[] = $productId;
2139
2140 foreach ($tierPriceRows as $row) {
2141 $row[$this->getProductEntityLinkField()] = $productId;
2142 $tierPriceIn[] = $row;
2143 }
2144 }
2145 if (Import::BEHAVIOR_APPEND != $this->getBehavior()) {
2146 $this->_connection->delete(
2147 $tableName,
2148 $this->_connection->quoteInto("{$this->getProductEntityLinkField()} IN (?)", $delProductId)
2149 );
2150 }
2151 if ($tierPriceIn) {
2152 $this->_connection->insertOnDuplicate($tableName, $tierPriceIn, ['value']);
2153 }
2154 }
2155 return $this;
2156 }
2157
2158 /**
2159 * Returns the import directory if specified or a default import directory (media/import).
2160 *
2161 * @return string
2162 */
2163 protected function getImportDir()
2164 {
2165 $dirConfig = DirectoryList::getDefaultConfig();
2166 $dirAddon = $dirConfig[DirectoryList::MEDIA][DirectoryList::PATH];
2167
2168 if (!empty($this->_parameters[Import::FIELD_NAME_IMG_FILE_DIR])) {
2169 $tmpPath = $this->_parameters[Import::FIELD_NAME_IMG_FILE_DIR];
2170 } else {
2171 $tmpPath = $dirAddon . '/' . $this->_mediaDirectory->getRelativePath('import');
2172 }
2173 return $tmpPath;
2174 }
2175
2176 /**
2177 * Returns an object for upload a media files
2178 *
2179 * @return \Magento\CatalogImportExport\Model\Import\Uploader
2180 * @throws \Magento\Framework\Exception\LocalizedException
2181 */
2182 protected function _getUploader()
2183 {
2184 if ($this->_fileUploader === null) {
2185 $this->_fileUploader = $this->_uploaderFactory->create();
2186
2187 $this->_fileUploader->init();
2188
2189 $dirConfig = DirectoryList::getDefaultConfig();
2190 $dirAddon = $dirConfig[DirectoryList::MEDIA][DirectoryList::PATH];
2191
2192 $tmpPath = $this->getImportDir();
2193
2194 if (!$this->_fileUploader->setTmpDir($tmpPath)) {
2195 throw new LocalizedException(
2196 __('File directory \'%1\' is not readable.', $tmpPath)
2197 );
2198 }
2199 $destinationDir = "catalog/product";
2200 $destinationPath = $dirAddon . '/' . $this->_mediaDirectory->getRelativePath($destinationDir);
2201
2202 $this->_mediaDirectory->create($destinationPath);
2203 if (!$this->_fileUploader->setDestDir($destinationPath)) {
2204 throw new LocalizedException(
2205 __('File directory \'%1\' is not writable.', $destinationPath)
2206 );
2207 }
2208 }
2209 return $this->_fileUploader;
2210 }
2211
2212 /**
2213 * Retrieve uploader.
2214 *
2215 * @return Uploader
2216 * @throws \Magento\Framework\Exception\LocalizedException
2217 */
2218 public function getUploader()
2219 {
2220 return $this->_getUploader();
2221 }
2222
2223 /**
2224 * Uploading files into the "catalog/product" media folder.
2225 *
2226 * Return a new file name if the same file is already exists.
2227 *
2228 * @param string $fileName
2229 * @param bool $renameFileOff [optional] boolean to pass.
2230 * Default is false which will set not to rename the file after import.
2231 * @return string
2232 */
2233 protected function uploadMediaFiles($fileName, $renameFileOff = false)
2234 {
2235 try {
2236 $res = $this->_getUploader()->move($fileName, $renameFileOff);
2237 return $res['file'];
2238 } catch (\Exception $e) {
2239 $this->_logger->critical($e);
2240 return '';
2241 }
2242 }
2243
2244 /**
2245 * Try to find file by it's path.
2246 *
2247 * @param string $fileName
2248 * @return string
2249 */
2250 private function getSystemFile($fileName)
2251 {
2252 $filePath = 'catalog' . DIRECTORY_SEPARATOR . 'product' . DIRECTORY_SEPARATOR . $fileName;
2253 /** @var \Magento\Framework\Filesystem\Directory\ReadInterface $read */
2254 $read = $this->filesystem->getDirectoryRead(DirectoryList::MEDIA);
2255
2256 return $read->isExist($filePath) && $read->isReadable($filePath) ? $fileName : '';
2257 }
2258
2259 /**
2260 * Save product media gallery.
2261 *
2262 * @param array $mediaGalleryData
2263 * @return $this
2264 */
2265 protected function _saveMediaGallery(array $mediaGalleryData)
2266 {
2267 if (empty($mediaGalleryData)) {
2268 return $this;
2269 }
2270 $this->mediaProcessor->saveMediaGallery($mediaGalleryData);
2271
2272 return $this;
2273 }
2274
2275 /**
2276 * Save product websites.
2277 *
2278 * @param array $websiteData
2279 * @return $this
2280 */
2281 protected function _saveProductWebsites(array $websiteData)
2282 {
2283 static $tableName = null;
2284
2285 if (!$tableName) {
2286 $tableName = $this->_resourceFactory->create()->getProductWebsiteTable();
2287 }
2288 if ($websiteData) {
2289 $websitesData = [];
2290 $delProductId = [];
2291
2292 foreach ($websiteData as $delSku => $websites) {
2293 $productId = $this->skuProcessor->getNewSku($delSku)['entity_id'];
2294 $delProductId[] = $productId;
2295
2296 foreach (array_keys($websites) as $websiteId) {
2297 $websitesData[] = ['product_id' => $productId, 'website_id' => $websiteId];
2298 }
2299 }
2300 if (Import::BEHAVIOR_APPEND != $this->getBehavior()) {
2301 $this->_connection->delete(
2302 $tableName,
2303 $this->_connection->quoteInto('product_id IN (?)', $delProductId)
2304 );
2305 }
2306 if ($websitesData) {
2307 $this->_connection->insertOnDuplicate($tableName, $websitesData);
2308 }
2309 }
2310 return $this;
2311 }
2312
2313 /**
2314 * Stock item saving.
2315 *
2316 * @return $this
2317 */
2318 protected function _saveStockItem()
2319 {
2320 while ($bunch = $this->_dataSourceModel->getNextBunch()) {
2321 $stockData = [];
2322 $productIdsToReindex = [];
2323 // Format bunch to stock data rows
2324 foreach ($bunch as $rowNum => $rowData) {
2325 if (!$this->isRowAllowedToImport($rowData, $rowNum)) {
2326 continue;
2327 }
2328
2329 $row = [];
2330 $sku = $rowData[self::COL_SKU];
2331 if ($this->skuProcessor->getNewSku($sku) !== null) {
2332 $row = $this->formatStockDataForRow($rowData);
2333 $productIdsToReindex[] = $row['product_id'];
2334 }
2335
2336 if (!isset($stockData[$sku])) {
2337 $stockData[$sku] = $row;
2338 }
2339 }
2340
2341 // Insert rows
2342 if (!empty($stockData)) {
2343 $this->stockItemImporter->import($stockData);
2344 }
2345
2346 $this->reindexProducts($productIdsToReindex);
2347 }
2348 return $this;
2349 }
2350
2351 /**
2352 * Initiate product reindex by product ids
2353 *
2354 * @param array $productIdsToReindex
2355 * @return void
2356 */
2357 private function reindexProducts($productIdsToReindex = [])
2358 {
2359 $indexer = $this->indexerRegistry->get('catalog_product_category');
2360 if (is_array($productIdsToReindex) && count($productIdsToReindex) > 0 && !$indexer->isScheduled()) {
2361 $indexer->reindexList($productIdsToReindex);
2362 }
2363 }
2364
2365 /**
2366 * Retrieve attribute by code
2367 *
2368 * @param string $attrCode
2369 * @return mixed
2370 */
2371 public function retrieveAttributeByCode($attrCode)
2372 {
2373 /** @var string $attrCode */
2374 $attrCode = mb_strtolower($attrCode);
2375
2376 if (!isset($this->_attributeCache[$attrCode])) {
2377 $this->_attributeCache[$attrCode] = $this->getResource()->getAttribute($attrCode);
2378 }
2379
2380 return $this->_attributeCache[$attrCode];
2381 }
2382
2383 /**
2384 * Attribute set ID-to-name pairs getter.
2385 *
2386 * @return array
2387 */
2388 public function getAttrSetIdToName()
2389 {
2390 return $this->_attrSetIdToName;
2391 }
2392
2393 /**
2394 * DB connection getter.
2395 *
2396 * @return \Magento\Framework\DB\Adapter\AdapterInterface
2397 */
2398 public function getConnection()
2399 {
2400 return $this->_connection;
2401 }
2402
2403 /**
2404 * EAV entity type code getter.
2405 *
2406 * @abstract
2407 * @return string
2408 */
2409 public function getEntityTypeCode()
2410 {
2411 return 'catalog_product';
2412 }
2413
2414 /**
2415 * New products SKU data.
2416 *
2417 * Returns array of new products data with SKU as key. All SKU keys are in lowercase for avoiding creation of
2418 * new products with the same SKU in different letter cases.
2419 *
2420 * @param string $sku
2421 * @return array
2422 */
2423 public function getNewSku($sku = null)
2424 {
2425 return $this->skuProcessor->getNewSku($sku);
2426 }
2427
2428 /**
2429 * Get next bunch of validated rows.
2430 *
2431 * @return array|null
2432 */
2433 public function getNextBunch()
2434 {
2435 return $this->_dataSourceModel->getNextBunch();
2436 }
2437
2438 /**
2439 * Existing products SKU getter.
2440 *
2441 * Returns array of existing products data with SKU as key. All SKU keys are in lowercase for avoiding creation of
2442 * new products with the same SKU in different letter cases.
2443 *
2444 * @return array
2445 */
2446 public function getOldSku()
2447 {
2448 return $this->_oldSku;
2449 }
2450
2451 /**
2452 * Retrieve Category Processor
2453 *
2454 * @return \Magento\CatalogImportExport\Model\Import\Product\CategoryProcessor
2455 */
2456 public function getCategoryProcessor()
2457 {
2458 return $this->categoryProcessor;
2459 }
2460
2461 /**
2462 * Obtain scope of the row from row data.
2463 *
2464 * @param array $rowData
2465 * @return int
2466 */
2467 public function getRowScope(array $rowData)
2468 {
2469 if (empty($rowData[self::COL_STORE])) {
2470 return self::SCOPE_DEFAULT;
2471 }
2472 return self::SCOPE_STORE;
2473 }
2474
2475 /**
2476 * Validate data row.
2477 *
2478 * @param array $rowData
2479 * @param int $rowNum
2480 * @return boolean
2481 * @SuppressWarnings(PHPMD.CyclomaticComplexity)
2482 * @SuppressWarnings(PHPMD.NPathComplexity)
2483 * @SuppressWarnings(PHPMD.ExcessiveMethodLength)
2484 * @throws \Zend_Validate_Exception
2485 */
2486 public function validateRow(array $rowData, $rowNum)
2487 {
2488 if (isset($this->_validatedRows[$rowNum])) {
2489 // check that row is already validated
2490 return !$this->getErrorAggregator()->isRowInvalid($rowNum);
2491 }
2492 $this->_validatedRows[$rowNum] = true;
2493
2494 $rowScope = $this->getRowScope($rowData);
2495 $sku = $rowData[self::COL_SKU];
2496
2497 // BEHAVIOR_DELETE and BEHAVIOR_REPLACE use specific validation logic
2498 if (Import::BEHAVIOR_REPLACE == $this->getBehavior()) {
2499 if (self::SCOPE_DEFAULT == $rowScope && !$this->isSkuExist($sku)) {
2500 $this->skipRow($rowNum, ValidatorInterface::ERROR_SKU_NOT_FOUND_FOR_DELETE);
2501 return false;
2502 }
2503 }
2504 if (Import::BEHAVIOR_DELETE == $this->getBehavior()) {
2505 if (self::SCOPE_DEFAULT == $rowScope && !$this->isSkuExist($sku)) {
2506 $this->skipRow($rowNum, ValidatorInterface::ERROR_SKU_NOT_FOUND_FOR_DELETE);
2507 return false;
2508 }
2509 return true;
2510 }
2511
2512 // if product doesn't exist, need to throw critical error else all errors should be not critical.
2513 $errorLevel = $this->getValidationErrorLevel($sku);
2514
2515 if (!$this->validator->isValid($rowData)) {
2516 foreach ($this->validator->getMessages() as $message) {
2517 $this->skipRow($rowNum, $message, $errorLevel, $this->validator->getInvalidAttribute());
2518 }
2519 }
2520
2521 if (null === $sku) {
2522 $this->skipRow($rowNum, ValidatorInterface::ERROR_SKU_IS_EMPTY, $errorLevel);
2523 } elseif (false === $sku) {
2524 $this->skipRow($rowNum, ValidatorInterface::ERROR_ROW_IS_ORPHAN, $errorLevel);
2525 } elseif (self::SCOPE_STORE == $rowScope
2526 && !$this->storeResolver->getStoreCodeToId($rowData[self::COL_STORE])
2527 ) {
2528 $this->skipRow($rowNum, ValidatorInterface::ERROR_INVALID_STORE, $errorLevel);
2529 }
2530
2531 // SKU is specified, row is SCOPE_DEFAULT, new product block begins
2532 $this->_processedEntitiesCount++;
2533
2534 if ($this->isSkuExist($sku) && Import::BEHAVIOR_REPLACE !== $this->getBehavior()) {
2535 // can we get all necessary data from existent DB product?
2536 // check for supported type of existing product
2537 if (isset($this->_productTypeModels[$this->getExistingSku($sku)['type_id']])) {
2538 $this->skuProcessor->addNewSku(
2539 $sku,
2540 $this->prepareNewSkuData($sku)
2541 );
2542 } else {
2543 $this->skipRow($rowNum, ValidatorInterface::ERROR_TYPE_UNSUPPORTED, $errorLevel);
2544 }
2545 } else {
2546 // validate new product type and attribute set
2547 if (!isset($rowData[self::COL_TYPE], $this->_productTypeModels[$rowData[self::COL_TYPE]])) {
2548 $this->skipRow($rowNum, ValidatorInterface::ERROR_INVALID_TYPE, $errorLevel);
2549 } elseif (!isset($rowData[self::COL_ATTR_SET], $this->_attrSetNameToId[$rowData[self::COL_ATTR_SET]])
2550 ) {
2551 $this->skipRow($rowNum, ValidatorInterface::ERROR_INVALID_ATTR_SET, $errorLevel);
2552 } elseif ($this->skuProcessor->getNewSku($sku) === null) {
2553 $this->skuProcessor->addNewSku(
2554 $sku,
2555 [
2556 'row_id' => null,
2557 'entity_id' => null,
2558 'type_id' => $rowData[self::COL_TYPE],
2559 'attr_set_id' => $this->_attrSetNameToId[$rowData[self::COL_ATTR_SET]],
2560 'attr_set_code' => $rowData[self::COL_ATTR_SET],
2561 ]
2562 );
2563 }
2564 }
2565
2566 if (!$this->getErrorAggregator()->isRowInvalid($rowNum)) {
2567 $newSku = $this->skuProcessor->getNewSku($sku);
2568 // set attribute set code into row data for followed attribute validation in type model
2569 $rowData[self::COL_ATTR_SET] = $newSku['attr_set_code'];
2570
2571 /** @var \Magento\CatalogImportExport\Model\Import\Product\Type\AbstractType $productTypeValidator */
2572 // isRowValid can add error to general errors pull if row is invalid
2573 $productTypeValidator = $this->_productTypeModels[$newSku['type_id']];
2574 $productTypeValidator->isRowValid(
2575 $rowData,
2576 $rowNum,
2577 !($this->isSkuExist($sku) && Import::BEHAVIOR_REPLACE !== $this->getBehavior())
2578 );
2579 }
2580 // validate custom options
2581 $this->getOptionEntity()->validateRow($rowData, $rowNum);
2582
2583 if ($this->isNeedToValidateUrlKey($rowData)) {
2584 $urlKey = strtolower($this->getUrlKey($rowData));
2585 $storeCodes = empty($rowData[self::COL_STORE_VIEW_CODE])
2586 ? array_flip($this->storeResolver->getStoreCodeToId())
2587 : explode($this->getMultipleValueSeparator(), $rowData[self::COL_STORE_VIEW_CODE]);
2588 foreach ($storeCodes as $storeCode) {
2589 $storeId = $this->storeResolver->getStoreCodeToId($storeCode);
2590 $productUrlSuffix = $this->getProductUrlSuffix($storeId);
2591 $urlPath = $urlKey . $productUrlSuffix;
2592 if (empty($this->urlKeys[$storeId][$urlPath])
2593 || ($this->urlKeys[$storeId][$urlPath] == $sku)
2594 ) {
2595 $this->urlKeys[$storeId][$urlPath] = $sku;
2596 $this->rowNumbers[$storeId][$urlPath] = $rowNum;
2597 } else {
2598 $message = sprintf(
2599 $this->retrieveMessageTemplate(ValidatorInterface::ERROR_DUPLICATE_URL_KEY),
2600 $urlKey,
2601 $this->urlKeys[$storeId][$urlPath]
2602 );
2603 $this->addRowError(
2604 ValidatorInterface::ERROR_DUPLICATE_URL_KEY,
2605 $rowNum,
2606 $rowData[self::COL_NAME],
2607 $message,
2608 ProcessingError::ERROR_LEVEL_NOT_CRITICAL
2609 )
2610 ->getErrorAggregator()
2611 ->addRowToSkip($rowNum);
2612 }
2613 }
2614 }
2615
2616 if (!empty($rowData['new_from_date']) && !empty($rowData['new_to_date'])
2617 ) {
2618 $newFromTimestamp = strtotime($this->dateTime->formatDate($rowData['new_from_date'], false));
2619 $newToTimestamp = strtotime($this->dateTime->formatDate($rowData['new_to_date'], false));
2620 if ($newFromTimestamp > $newToTimestamp) {
2621 $this->skipRow(
2622 $rowNum,
2623 'invalidNewToDateValue',
2624 $errorLevel,
2625 $rowData['new_to_date']
2626 );
2627 }
2628 }
2629
2630 return !$this->getErrorAggregator()->isRowInvalid($rowNum);
2631 }
2632
2633 /**
2634 * Check if need to validate url key.
2635 *
2636 * @param array $rowData
2637 * @return bool
2638 */
2639 private function isNeedToValidateUrlKey($rowData)
2640 {
2641 return (!empty($rowData[self::URL_KEY]) || !empty($rowData[self::COL_NAME]))
2642 && (empty($rowData[self::COL_VISIBILITY])
2643 || $rowData[self::COL_VISIBILITY]
2644 !== (string)Visibility::getOptionArray()[Visibility::VISIBILITY_NOT_VISIBLE]);
2645 }
2646
2647 /**
2648 * Prepare new SKU data
2649 *
2650 * @param string $sku
2651 * @return array
2652 */
2653 private function prepareNewSkuData($sku)
2654 {
2655 $data = [];
2656 foreach ($this->getExistingSku($sku) as $key => $value) {
2657 $data[$key] = $value;
2658 }
2659
2660 $data['attr_set_code'] = $this->_attrSetIdToName[$this->getExistingSku($sku)['attr_set_id']];
2661
2662 return $data;
2663 }
2664
2665 /**
2666 * Parse attributes names and values string to array.
2667 *
2668 * @param array $rowData
2669 *
2670 * @return array
2671 */
2672 private function _parseAdditionalAttributes($rowData)
2673 {
2674 if (empty($rowData['additional_attributes'])) {
2675 return $rowData;
2676 }
2677 $rowData = array_merge($rowData, $this->getAdditionalAttributes($rowData['additional_attributes']));
2678 return $rowData;
2679 }
2680
2681 /**
2682 * Retrieves additional attributes in format:
2683 * [
2684 * code1 => value1,
2685 * code2 => value2,
2686 * ...
2687 * codeN => valueN
2688 * ]
2689 *
2690 * @param string $additionalAttributes Attributes data that will be parsed
2691 * @return array
2692 */
2693 private function getAdditionalAttributes($additionalAttributes)
2694 {
2695 return empty($this->_parameters[Import::FIELDS_ENCLOSURE])
2696 ? $this->parseAttributesWithoutWrappedValues($additionalAttributes)
2697 : $this->parseAttributesWithWrappedValues($additionalAttributes);
2698 }
2699
2700 /**
2701 * Parses data and returns attributes in format:
2702 * [
2703 * code1 => value1,
2704 * code2 => value2,
2705 * ...
2706 * codeN => valueN
2707 * ]
2708 *
2709 * @param string $attributesData Attributes data that will be parsed. It keeps data in format:
2710 * code=value,code2=value2...,codeN=valueN
2711 * @return array
2712 */
2713 private function parseAttributesWithoutWrappedValues($attributesData)
2714 {
2715 $attributeNameValuePairs = explode($this->getMultipleValueSeparator(), $attributesData);
2716 $preparedAttributes = [];
2717 $code = '';
2718 foreach ($attributeNameValuePairs as $attributeData) {
2719 //process case when attribute has ImportModel::DEFAULT_GLOBAL_MULTI_VALUE_SEPARATOR inside its value
2720 if (strpos($attributeData, self::PAIR_NAME_VALUE_SEPARATOR) === false) {
2721 if (!$code) {
2722 continue;
2723 }
2724 $preparedAttributes[$code] .= $this->getMultipleValueSeparator() . $attributeData;
2725 continue;
2726 }
2727 list($code, $value) = explode(self::PAIR_NAME_VALUE_SEPARATOR, $attributeData, 2);
2728 $code = mb_strtolower($code);
2729 $preparedAttributes[$code] = $value;
2730 }
2731 return $preparedAttributes;
2732 }
2733
2734 /**
2735 * Parses data and returns attributes in format:
2736 * [
2737 * code1 => value1,
2738 * code2 => value2,
2739 * ...
2740 * codeN => valueN
2741 * ]
2742 * All values have unescaped data except mupliselect attributes,
2743 * they should be parsed in additional method - parseMultiselectValues()
2744 *
2745 * @param string $attributesData Attributes data that will be parsed. It keeps data in format:
2746 * code="value",code2="value2"...,codeN="valueN"
2747 * where every value is wrapped in double quotes. Double quotes as part of value should be duplicated.
2748 * E.g. attribute with code 'attr_code' has value 'my"value'. This data should be stored as attr_code="my""value"
2749 *
2750 * @return array
2751 */
2752 private function parseAttributesWithWrappedValues($attributesData)
2753 {
2754 $attributes = [];
2755 preg_match_all(
2756 '~((?:[a-zA-Z0-9_])+)="((?:[^"]|""|"' . $this->getMultiLineSeparatorForRegexp() . '")+)"+~',
2757 $attributesData,
2758 $matches
2759 );
2760 foreach ($matches[1] as $i => $attributeCode) {
2761 $attribute = $this->retrieveAttributeByCode($attributeCode);
2762 $value = 'multiselect' != $attribute->getFrontendInput()
2763 ? str_replace('""', '"', $matches[2][$i])
2764 : '"' . $matches[2][$i] . '"';
2765 $attributes[mb_strtolower($attributeCode)] = $value;
2766 }
2767 return $attributes;
2768 }
2769
2770 /**
2771 * Parse values of multiselect attributes depends on "Fields Enclosure" parameter
2772 *
2773 * @param string $values
2774 * @param string $delimiter
2775 * @return array
2776 * @since 100.1.2
2777 */
2778 public function parseMultiselectValues($values, $delimiter = self::PSEUDO_MULTI_LINE_SEPARATOR)
2779 {
2780 if (empty($this->_parameters[Import::FIELDS_ENCLOSURE])) {
2781 return explode($delimiter, $values);
2782 }
2783 if (preg_match_all('~"((?:[^"]|"")*)"~', $values, $matches)) {
2784 return $values = array_map(function ($value) {
2785 return str_replace('""', '"', $value);
2786 }, $matches[1]);
2787 }
2788 return [$values];
2789 }
2790
2791 /**
2792 * Retrieves escaped PSEUDO_MULTI_LINE_SEPARATOR if it is metacharacter for regular expression
2793 *
2794 * @return string
2795 */
2796 private function getMultiLineSeparatorForRegexp()
2797 {
2798 if (!$this->multiLineSeparatorForRegexp) {
2799 $this->multiLineSeparatorForRegexp = in_array(self::PSEUDO_MULTI_LINE_SEPARATOR, str_split('[\^$.|?*+(){}'))
2800 ? '\\' . self::PSEUDO_MULTI_LINE_SEPARATOR
2801 : self::PSEUDO_MULTI_LINE_SEPARATOR;
2802 }
2803 return $this->multiLineSeparatorForRegexp;
2804 }
2805
2806 /**
2807 * Set values in use_config_ fields.
2808 *
2809 * @param array $rowData
2810 *
2811 * @return array
2812 */
2813 private function _setStockUseConfigFieldsValues($rowData)
2814 {
2815 $useConfigFields = [];
2816 foreach ($rowData as $key => $value) {
2817 $useConfigName = $key === StockItemInterface::ENABLE_QTY_INCREMENTS
2818 ? StockItemInterface::USE_CONFIG_ENABLE_QTY_INC
2819 : self::INVENTORY_USE_CONFIG_PREFIX . $key;
2820
2821 if (isset($this->defaultStockData[$key])
2822 && isset($this->defaultStockData[$useConfigName])
2823 && !empty($value)
2824 && empty($rowData[$useConfigName])
2825 ) {
2826 $useConfigFields[$useConfigName] = ($value == self::INVENTORY_USE_CONFIG) ? 1 : 0;
2827 }
2828 }
2829 $rowData = array_merge($rowData, $useConfigFields);
2830 return $rowData;
2831 }
2832
2833 /**
2834 * Custom fields mapping for changed purposes of fields and field names.
2835 *
2836 * @param array $rowData
2837 *
2838 * @return array
2839 */
2840 private function _customFieldsMapping($rowData)
2841 {
2842 foreach ($this->_fieldsMap as $systemFieldName => $fileFieldName) {
2843 if (array_key_exists($fileFieldName, $rowData)) {
2844 $rowData[$systemFieldName] = $rowData[$fileFieldName];
2845 }
2846 }
2847
2848 $rowData = $this->_parseAdditionalAttributes($rowData);
2849
2850 $rowData = $this->_setStockUseConfigFieldsValues($rowData);
2851 if (array_key_exists('status', $rowData)
2852 && $rowData['status'] != \Magento\Catalog\Model\Product\Attribute\Source\Status::STATUS_ENABLED
2853 ) {
2854 if ($rowData['status'] == 'yes') {
2855 $rowData['status'] = \Magento\Catalog\Model\Product\Attribute\Source\Status::STATUS_ENABLED;
2856 } elseif (!empty($rowData['status']) || $this->getRowScope($rowData) == self::SCOPE_DEFAULT) {
2857 $rowData['status'] = \Magento\Catalog\Model\Product\Attribute\Source\Status::STATUS_DISABLED;
2858 }
2859 }
2860 return $rowData;
2861 }
2862
2863 /**
2864 * Validate data rows and save bunches to DB
2865 *
2866 * @return $this|AbstractEntity
2867 */
2868 protected function _saveValidatedBunches()
2869 {
2870 $source = $this->_getSource();
2871 $source->rewind();
2872
2873 while ($source->valid()) {
2874 try {
2875 $rowData = $source->current();
2876 } catch (\InvalidArgumentException $e) {
2877 $this->addRowError($e->getMessage(), $this->_processedRowsCount);
2878 $this->_processedRowsCount++;
2879 $source->next();
2880 continue;
2881 }
2882
2883 $rowData = $this->_customFieldsMapping($rowData);
2884
2885 $this->validateRow($rowData, $source->key());
2886
2887 $source->next();
2888 }
2889 $this->checkUrlKeyDuplicates();
2890 $this->getOptionEntity()->validateAmbiguousData();
2891 return parent::_saveValidatedBunches();
2892 }
2893
2894 /**
2895 * Check that url_keys are not assigned to other products in DB
2896 *
2897 * @return void
2898 * @since 100.0.3
2899 */
2900 protected function checkUrlKeyDuplicates()
2901 {
2902 $resource = $this->getResource();
2903 foreach ($this->urlKeys as $storeId => $urlKeys) {
2904 $urlKeyDuplicates = $this->_connection->fetchAssoc(
2905 $this->_connection->select()->from(
2906 ['url_rewrite' => $resource->getTable('url_rewrite')],
2907 ['request_path', 'store_id']
2908 )->joinLeft(
2909 ['cpe' => $resource->getTable('catalog_product_entity')],
2910 "cpe.entity_id = url_rewrite.entity_id"
2911 )->where('request_path IN (?)', array_keys($urlKeys))
2912 ->where('store_id IN (?)', $storeId)
2913 ->where('cpe.sku not in (?)', array_values($urlKeys))
2914 );
2915 foreach ($urlKeyDuplicates as $entityData) {
2916 $rowNum = $this->rowNumbers[$entityData['store_id']][$entityData['request_path']];
2917 $message = sprintf(
2918 $this->retrieveMessageTemplate(ValidatorInterface::ERROR_DUPLICATE_URL_KEY),
2919 $entityData['request_path'],
2920 $entityData['sku']
2921 );
2922 $this->addRowError(ValidatorInterface::ERROR_DUPLICATE_URL_KEY, $rowNum, 'url_key', $message);
2923 }
2924 }
2925 }
2926
2927 /**
2928 * Retrieve product rewrite suffix for store
2929 *
2930 * @param int $storeId
2931 * @return string
2932 * @since 100.0.3
2933 */
2934 protected function getProductUrlSuffix($storeId = null)
2935 {
2936 if (!isset($this->productUrlSuffix[$storeId])) {
2937 $this->productUrlSuffix[$storeId] = $this->scopeConfig->getValue(
2938 \Magento\CatalogUrlRewrite\Model\ProductUrlPathGenerator::XML_PATH_PRODUCT_URL_SUFFIX,
2939 \Magento\Store\Model\ScopeInterface::SCOPE_STORE,
2940 $storeId
2941 );
2942 }
2943 return $this->productUrlSuffix[$storeId];
2944 }
2945
2946 /**
2947 * Retrieve url key from provided row data.
2948 *
2949 * @param array $rowData
2950 * @return string
2951 *
2952 * @since 100.0.3
2953 */
2954 protected function getUrlKey($rowData)
2955 {
2956 if (!empty($rowData[self::URL_KEY])) {
2957 return $this->productUrl->formatUrlKey($rowData[self::URL_KEY]);
2958 }
2959
2960 if (!empty($rowData[self::COL_NAME])) {
2961 return $this->productUrl->formatUrlKey($rowData[self::COL_NAME]);
2962 }
2963
2964 return '';
2965 }
2966
2967 /**
2968 * Retrieve resource.
2969 *
2970 * @return Proxy\Product\ResourceModel
2971 *
2972 * @since 100.0.3
2973 */
2974 protected function getResource()
2975 {
2976 if (!$this->_resource) {
2977 $this->_resource = $this->_resourceFactory->create();
2978 }
2979 return $this->_resource;
2980 }
2981
2982 /**
2983 * Whether a url key is needed to be change.
2984 *
2985 * @param array $rowData
2986 * @return bool
2987 */
2988 private function isNeedToChangeUrlKey(array $rowData): bool
2989 {
2990 $urlKey = $this->getUrlKey($rowData);
2991 $productExists = $this->isSkuExist($rowData[self::COL_SKU]);
2992 $markedToEraseUrlKey = isset($rowData[self::URL_KEY]);
2993 // The product isn't new and the url key index wasn't marked for change.
2994 if (!$urlKey && $productExists && !$markedToEraseUrlKey) {
2995 // Seems there is no need to change the url key
2996 return false;
2997 }
2998
2999 return true;
3000 }
3001
3002 /**
3003 * Get product entity link field
3004 *
3005 * @return string
3006 */
3007 private function getProductEntityLinkField()
3008 {
3009 if (!$this->productEntityLinkField) {
3010 $this->productEntityLinkField = $this->getMetadataPool()
3011 ->getMetadata(\Magento\Catalog\Api\Data\ProductInterface::class)
3012 ->getLinkField();
3013 }
3014 return $this->productEntityLinkField;
3015 }
3016
3017 /**
3018 * Get product entity identifier field
3019 *
3020 * @return string
3021 */
3022 private function getProductIdentifierField()
3023 {
3024 if (!$this->productEntityIdentifierField) {
3025 $this->productEntityIdentifierField = $this->getMetadataPool()
3026 ->getMetadata(\Magento\Catalog\Api\Data\ProductInterface::class)
3027 ->getIdentifierField();
3028 }
3029 return $this->productEntityIdentifierField;
3030 }
3031
3032 /**
3033 * Update media gallery labels
3034 *
3035 * @param array $labels
3036 * @return void
3037 */
3038 private function updateMediaGalleryLabels(array $labels)
3039 {
3040 if (!empty($labels)) {
3041 $this->mediaProcessor->updateMediaGalleryLabels($labels);
3042 }
3043 }
3044
3045 /**
3046 * Update 'disabled' field for media gallery entity
3047 *
3048 * @param array $images
3049 * @return $this
3050 */
3051 private function updateMediaGalleryVisibility(array $images)
3052 {
3053 if (!empty($images)) {
3054 $this->mediaProcessor->updateMediaGalleryVisibility($images);
3055 }
3056
3057 return $this;
3058 }
3059
3060 /**
3061 * Parse values from multiple attributes fields
3062 *
3063 * @param string $labelRow
3064 * @return array
3065 */
3066 private function parseMultipleValues($labelRow)
3067 {
3068 return $this->parseMultiselectValues(
3069 $labelRow,
3070 $this->getMultipleValueSeparator()
3071 );
3072 }
3073
3074 /**
3075 * Check if product exists for specified SKU
3076 *
3077 * @param string $sku
3078 * @return bool
3079 */
3080 private function isSkuExist($sku)
3081 {
3082 $sku = strtolower($sku);
3083 return isset($this->_oldSku[$sku]);
3084 }
3085
3086 /**
3087 * Get existing product data for specified SKU
3088 *
3089 * @param string $sku
3090 * @return array
3091 */
3092 private function getExistingSku($sku)
3093 {
3094 return $this->_oldSku[strtolower($sku)];
3095 }
3096
3097 /**
3098 * Format row data to DB compatible values.
3099 *
3100 * @param array $rowData
3101 * @return array
3102 */
3103 private function formatStockDataForRow(array $rowData): array
3104 {
3105 $sku = $rowData[self::COL_SKU];
3106 $row['product_id'] = $this->skuProcessor->getNewSku($sku)['entity_id'];
3107 $row['website_id'] = $this->stockConfiguration->getDefaultScopeId();
3108 $row['stock_id'] = $this->stockRegistry->getStock($row['website_id'])->getStockId();
3109
3110 $stockItemDo = $this->stockRegistry->getStockItem($row['product_id'], $row['website_id']);
3111 $existStockData = $stockItemDo->getData();
3112
3113 $row = array_merge(
3114 $this->defaultStockData,
3115 array_intersect_key($existStockData, $this->defaultStockData),
3116 array_intersect_key($rowData, $this->defaultStockData),
3117 $row
3118 );
3119
3120 if ($this->stockConfiguration->isQty($this->skuProcessor->getNewSku($sku)['type_id'])) {
3121 $stockItemDo->setData($row);
3122 $row['is_in_stock'] = $row['is_in_stock'] ?? $this->stockStateProvider->verifyStock($stockItemDo);
3123 if ($this->stockStateProvider->verifyNotification($stockItemDo)) {
3124 $date = $this->dateTimeFactory->create('now', new \DateTimeZone('UTC'));
3125 $row['low_stock_date'] = $date->format(DateTime::DATETIME_PHP_FORMAT);
3126 }
3127 $row['stock_status_changed_auto'] = (int)!$this->stockStateProvider->verifyStock($stockItemDo);
3128 } else {
3129 $row['qty'] = 0;
3130 }
3131
3132 return $row;
3133 }
3134
3135 /**
3136 * Retrieve product by sku.
3137 *
3138 * @param string $sku
3139 * @return \Magento\Catalog\Api\Data\ProductInterface|null
3140 */
3141 private function retrieveProductBySku($sku)
3142 {
3143 try {
3144 $product = $this->productRepository->get($sku);
3145 } catch (NoSuchEntityException $e) {
3146 return null;
3147 }
3148 return $product;
3149 }
3150
3151 /**
3152 * Add row as skipped
3153 *
3154 * @param int $rowNum
3155 * @param string $errorCode Error code or simply column name
3156 * @param string $errorLevel error level
3157 * @param string|null $colName optional column name
3158 * @return $this
3159 */
3160 private function skipRow(
3161 $rowNum,
3162 string $errorCode,
3163 string $errorLevel = ProcessingError::ERROR_LEVEL_NOT_CRITICAL,
3164 $colName = null
3165 ): self {
3166 $this->addRowError($errorCode, $rowNum, $colName, null, $errorLevel);
3167 $this->getErrorAggregator()
3168 ->addRowToSkip($rowNum);
3169 return $this;
3170 }
3171
3172 /**
3173 * Returns errorLevel for validation
3174 *
3175 * @param string $sku
3176 * @return string
3177 */
3178 private function getValidationErrorLevel($sku): string
3179 {
3180 return (!$this->isSkuExist($sku) && Import::BEHAVIOR_REPLACE !== $this->getBehavior())
3181 ? ProcessingError::ERROR_LEVEL_CRITICAL
3182 : ProcessingError::ERROR_LEVEL_NOT_CRITICAL;
3183 }
3184}