'default_billing', self::COLUMN_DEFAULT_SHIPPING => 'default_shipping', ]; /** * Permanent entity columns * * @var string[] */ protected $_permanentAttributes = [self::COLUMN_WEBSITE, self::COLUMN_EMAIL, self::COLUMN_ADDRESS_ID]; /** * Attributes with index (not label) value * * @var string[] */ protected $_indexValueAttributes = [self::COLUMN_COUNTRY_ID]; /** * Customer entity DB table name * * @var string */ protected $_entityTable; /** * Region collection instance * * @var \Magento\Directory\Model\ResourceModel\Region\Collection */ private $_regionCollection; /** * Countries and regions * * Example array: array( * [country_id_lowercased_1] => array( * [region_code_lowercased_1] => region_id_1, * [region_default_name_lowercased_1] => region_id_1, * ..., * [region_code_lowercased_n] => region_id_n, * [region_default_name_lowercased_n] => region_id_n * ), * ... * ) * * @var array */ protected $_countryRegions = []; /** * Region ID to region default name pairs * * @var array */ protected $_regions = []; /** * Column names that holds values with particular meaning * * @var string[] */ protected $_specialAttributes = [ self::COLUMN_ACTION, self::COLUMN_WEBSITE, self::COLUMN_EMAIL, self::COLUMN_ADDRESS_ID, self::COLUMN_DEFAULT_BILLING, self::COLUMN_DEFAULT_SHIPPING, ]; /** * Customer entity * * @var \Magento\Customer\Model\Customer */ protected $_customerEntity; /** * Entity ID incremented value * * @var int */ protected $_nextEntityId; /** * Array of region parameters * * @var array */ protected $_regionParameters; /** * Address attributes collection * * @var \Magento\Customer\Model\ResourceModel\Address\Attribute\Collection */ protected $_attributeCollection; /** * Store imported row primary keys * * @var array */ protected $_importedRowPks = []; /** * @var \Magento\ImportExport\Model\ResourceModel\Helper */ protected $_resourceHelper; /** * @var \Magento\Customer\Model\CustomerFactory */ protected $_customerFactory; /** * @var \Magento\Eav\Model\Config */ protected $_eavConfig; /** * @var \Magento\Customer\Model\AddressFactory */ protected $_addressFactory; /** * @var \Magento\Framework\Stdlib\DateTime */ protected $dateTime; /** * Customer attributes * * @var string[] */ protected $_customerAttributes = []; /** * Valid column names * * @array */ protected $validColumnNames = [ "region_id", "vat_is_valid", "vat_request_date", "vat_request_id", "vat_request_success" ]; /** * @var \Magento\Customer\Model\Address\Validator\Postcode */ protected $postcodeValidator; /** * @var CountryWithWebsitesSource */ private $countryWithWebsites; /** * Options for certain attributes sorted by websites. * * @var array[][] With path as => => options[]. */ private $optionsByWebsite = []; /** * @var AddressStorage */ private $addressStorage; /** * @param \Magento\Framework\Stdlib\StringUtils $string * @param \Magento\Framework\App\Config\ScopeConfigInterface $scopeConfig * @param \Magento\ImportExport\Model\ImportFactory $importFactory * @param \Magento\ImportExport\Model\ResourceModel\Helper $resourceHelper * @param \Magento\Framework\App\ResourceConnection $resource * @param ProcessingErrorAggregatorInterface $errorAggregator * @param \Magento\Store\Model\StoreManagerInterface $storeManager * @param \Magento\ImportExport\Model\Export\Factory $collectionFactory * @param \Magento\Eav\Model\Config $eavConfig * @param \Magento\CustomerImportExport\Model\ResourceModel\Import\Customer\StorageFactory $storageFactory * @param \Magento\Customer\Model\AddressFactory $addressFactory * @param \Magento\Directory\Model\ResourceModel\Region\CollectionFactory $regionColFactory * @param \Magento\Customer\Model\CustomerFactory $customerFactory * @param \Magento\Customer\Model\ResourceModel\Address\Attribute\CollectionFactory $attributesFactory * @param \Magento\Framework\Stdlib\DateTime $dateTime * @param \Magento\Customer\Model\Address\Validator\Postcode $postcodeValidator * @param array $data * @param CountryWithWebsitesSource|null $countryWithWebsites * @param AddressStorage|null $addressStorage * * @SuppressWarnings(PHPMD.NPathComplexity) * @SuppressWarnings(PHPMD.ExcessiveParameterList) */ public function __construct( \Magento\Framework\Stdlib\StringUtils $string, \Magento\Framework\App\Config\ScopeConfigInterface $scopeConfig, \Magento\ImportExport\Model\ImportFactory $importFactory, \Magento\ImportExport\Model\ResourceModel\Helper $resourceHelper, \Magento\Framework\App\ResourceConnection $resource, ProcessingErrorAggregatorInterface $errorAggregator, \Magento\Store\Model\StoreManagerInterface $storeManager, \Magento\ImportExport\Model\Export\Factory $collectionFactory, \Magento\Eav\Model\Config $eavConfig, \Magento\CustomerImportExport\Model\ResourceModel\Import\Customer\StorageFactory $storageFactory, \Magento\Customer\Model\AddressFactory $addressFactory, \Magento\Directory\Model\ResourceModel\Region\CollectionFactory $regionColFactory, \Magento\Customer\Model\CustomerFactory $customerFactory, \Magento\Customer\Model\ResourceModel\Address\Attribute\CollectionFactory $attributesFactory, \Magento\Framework\Stdlib\DateTime $dateTime, \Magento\Customer\Model\Address\Validator\Postcode $postcodeValidator, array $data = [], CountryWithWebsitesSource $countryWithWebsites = null, AddressStorage $addressStorage = null ) { $this->_customerFactory = $customerFactory; $this->_addressFactory = $addressFactory; $this->_eavConfig = $eavConfig; $this->_resourceHelper = $resourceHelper; $this->dateTime = $dateTime; $this->postcodeValidator = $postcodeValidator; $this->countryWithWebsites = $countryWithWebsites ?: ObjectManager::getInstance()->get(CountryWithWebsitesSource::class); if (!isset($data['attribute_collection'])) { /** @var $attributeCollection \Magento\Customer\Model\ResourceModel\Address\Attribute\Collection */ $attributeCollection = $attributesFactory->create(); $attributeCollection->addSystemHiddenFilter()->addExcludeHiddenFrontendFilter(); $data['attribute_collection'] = $attributeCollection; } parent::__construct( $string, $scopeConfig, $importFactory, $resourceHelper, $resource, $errorAggregator, $storeManager, $collectionFactory, $eavConfig, $storageFactory, $data ); $this->_entityTable = isset( $data['entity_table'] ) ? $data['entity_table'] : $addressFactory->create()->getResource()->getEntityTable(); $this->_regionCollection = isset( $data['region_collection'] ) ? $data['region_collection'] : $regionColFactory->create(); $this->addMessageTemplate(self::ERROR_ADDRESS_ID_IS_EMPTY, __('Customer address id column is not specified')); $this->addMessageTemplate( self::ERROR_ADDRESS_NOT_FOUND, __('We can\'t find that customer address.') ); $this->addMessageTemplate(self::ERROR_INVALID_REGION, __('Please enter a valid region.')); $this->addMessageTemplate( self::ERROR_DUPLICATE_PK, __('We found another row with this email, website and address ID combination.') ); $this->addressStorage = $addressStorage ?: ObjectManager::getInstance()->get(AddressStorage::class); $this->_initAttributes(); $this->_initCountryRegions(); } /** * @inheritDoc */ public function getAttributeOptions(AbstractAttribute $attribute, array $indexAttributes = []) { $standardOptions = parent::getAttributeOptions($attribute, $indexAttributes); if ($attribute->getAttributeCode() === 'country_id') { //If we want to get available options for country field then we have to use alternative source // to get actual data for each website. $options = $this->countryWithWebsites->getAllOptions(); //Available country options now will be sorted by websites. $code = $attribute->getAttributeCode(); $websiteOptions = [Store::DEFAULT_STORE_ID => $standardOptions]; //Sorting options by website. foreach ($options as $option) { if (array_key_exists('website_ids', $option)) { foreach ($option['website_ids'] as $websiteId) { if (!array_key_exists($websiteId, $websiteOptions)) { $websiteOptions[$websiteId] = []; } $optionId = mb_strtolower($option['value']); $websiteOptions[$websiteId][$optionId] = $option['value']; } } } //Storing sorted $this->optionsByWebsite[$code] = $websiteOptions; } return $standardOptions; } /** * Attributes' data may vary depending on website settings, * this method adjusts an attribute's data from $this->_attributes to * website-specific data. * * @param array $attributeData Data from $this->_attributes. * @param int $websiteId * * @return array Adjusted data in the same format. */ private function adjustAttributeDataForWebsite(array $attributeData, int $websiteId): array { if ($attributeData['code'] === 'country_id') { $attributeOptions = $this->optionsByWebsite[$attributeData['code']]; if (array_key_exists($websiteId, $attributeOptions)) { $attributeData['options'] = $attributeOptions[$websiteId]; } } return $attributeData; } /** * Customer entity getter * * @return \Magento\Customer\Model\Customer */ protected function _getCustomerEntity() { if (!$this->_customerEntity) { $this->_customerEntity = $this->_customerFactory->create(); } return $this->_customerEntity; } /** * Get next address entity ID * * @return int */ protected function _getNextEntityId() { if (!$this->_nextEntityId) { /** @var $addressResource \Magento\Customer\Model\ResourceModel\Address */ $addressResource = $this->_addressFactory->create()->getResource(); $addressTable = $addressResource->getEntityTable(); $this->_nextEntityId = $this->_resourceHelper->getNextAutoincrement($addressTable); } return $this->_nextEntityId++; } /** * Initialize country regions hash for clever recognition * * @return $this */ protected function _initCountryRegions() { /** @var $region \Magento\Directory\Model\Region */ foreach ($this->_regionCollection as $region) { $countryNormalized = strtolower($region->getCountryId()); $regionCode = strtolower($region->getCode()); $regionName = strtolower($region->getDefaultName()); $this->_countryRegions[$countryNormalized][$regionCode] = $region->getId(); $this->_countryRegions[$countryNormalized][$regionName] = $region->getId(); $this->_regions[$region->getId()] = $region->getDefaultName(); } return $this; } /** * Pre-loading customers for existing customers checks in order * to perform mass validation/import efficiently. * Also loading existing addresses for requested customers. * * @param array|AbstractSource $rows Each row must contain data from columns email * and website code. * * @return void */ public function prepareCustomerData($rows): void { $customersPresent = []; foreach ($rows as $rowData) { $email = $rowData[static::COLUMN_EMAIL] ?? null; $websiteId = isset($rowData[static::COLUMN_WEBSITE]) ? $this->getWebsiteId($rowData[static::COLUMN_WEBSITE]) : false; if ($email && $websiteId !== false) { $customersPresent[] = [ 'email' => $email, 'website_id' => $websiteId, ]; } } $this->getCustomerStorage()->prepareCustomers($customersPresent); $ids = []; foreach ($customersPresent as $customerData) { $id = $this->getCustomerStorage()->getCustomerId( $customerData['email'], $customerData['website_id'] ); if ($id) { $ids[] = $id; } } $this->addressStorage->prepareAddresses($ids); } /** * @inheritDoc */ public function validateData() { $this->prepareCustomerData($this->getSource()); return parent::validateData(); } /** * Import data rows * * @abstract * @return boolean * @SuppressWarnings(PHPMD.CyclomaticComplexity) */ protected function _importData() { //Preparing data for mass validation/import. $rows = []; while ($bunch = $this->_dataSourceModel->getNextBunch()) { $rows = array_merge($rows, $bunch); } $this->prepareCustomerData($rows); unset($bunch, $rows); $this->_dataSourceModel->getIterator()->rewind(); //Importing while ($bunch = $this->_dataSourceModel->getNextBunch()) { $newRows = []; $updateRows = []; $attributes = []; $defaults = []; // customer default addresses (billing/shipping) data $deleteRowIds = []; foreach ($bunch as $rowNumber => $rowData) { // check row data if ($this->_isOptionalAddressEmpty($rowData) || !$this->validateRow($rowData, $rowNumber)) { continue; } if ($this->getErrorAggregator()->hasToBeTerminated()) { $this->getErrorAggregator()->addRowToSkip($rowNumber); continue; } if ($this->getBehavior($rowData) == \Magento\ImportExport\Model\Import::BEHAVIOR_ADD_UPDATE) { $addUpdateResult = $this->_prepareDataForUpdate($rowData); if ($addUpdateResult['entity_row_new']) { $newRows[] = $addUpdateResult['entity_row_new']; } if ($addUpdateResult['entity_row_update']) { $updateRows[] = $addUpdateResult['entity_row_update']; } $attributes = $this->_mergeEntityAttributes($addUpdateResult['attributes'], $attributes); $defaults = $this->_mergeEntityAttributes($addUpdateResult['defaults'], $defaults); } elseif ($this->getBehavior($rowData) == \Magento\ImportExport\Model\Import::BEHAVIOR_DELETE) { $deleteRowIds[] = $rowData[self::COLUMN_ADDRESS_ID]; } } $this->updateItemsCounterStats($newRows, $updateRows, $deleteRowIds); $this->_saveAddressEntities($newRows, $updateRows) ->_saveAddressAttributes($attributes) ->_saveCustomerDefaults($defaults); $this->_deleteAddressEntities($deleteRowIds); } return true; } /** * Merge attributes * * @param array $newAttributes * @param array $attributes * @return array */ protected function _mergeEntityAttributes(array $newAttributes, array $attributes) { foreach ($newAttributes as $tableName => $tableData) { foreach ($tableData as $entityId => $entityData) { foreach ($entityData as $attributeId => $attributeValue) { $attributes[$tableName][$entityId][$attributeId] = $attributeValue; } } } return $attributes; } /** * Prepare data for add/update action * * @param array $rowData * @return array * @SuppressWarnings(PHPMD.CyclomaticComplexity) * @SuppressWarnings(PHPMD.NPathComplexity) */ protected function _prepareDataForUpdate(array $rowData):array { $multiSeparator = $this->getMultipleValueSeparator(); $email = strtolower($rowData[self::COLUMN_EMAIL]); $customerId = $this->_getCustomerId($email, $rowData[self::COLUMN_WEBSITE]); // entity table data $entityRowNew = []; $entityRowUpdate = []; // attribute values $attributes = []; // customer default addresses $defaults = []; $newAddress = true; // get address id if ($rowData[self::COLUMN_ADDRESS_ID] && $this->addressStorage->doesExist( $rowData[self::COLUMN_ADDRESS_ID], (string)$customerId ) ) { $newAddress = false; $addressId = $rowData[self::COLUMN_ADDRESS_ID]; } else { $addressId = $this->_getNextEntityId(); } $entityRow = [ 'entity_id' => $addressId, 'parent_id' => $customerId, 'updated_at' => (new \DateTime())->format(\Magento\Framework\Stdlib\DateTime::DATETIME_PHP_FORMAT), ]; $websiteId = $this->_websiteCodeToId[$rowData[self::COLUMN_WEBSITE]]; foreach ($this->_attributes as $attributeAlias => $attributeParams) { if (array_key_exists($attributeAlias, $rowData)) { $attributeParams = $this->adjustAttributeDataForWebsite($attributeParams, $websiteId); if (!strlen($rowData[$attributeAlias])) { if ($newAddress) { $value = null; } else { continue; } } elseif ($newAddress && !strlen($rowData[$attributeAlias])) { } elseif (in_array($attributeParams['type'], ['select', 'boolean'])) { $value = $this->getSelectAttrIdByValue($attributeParams, mb_strtolower($rowData[$attributeAlias])); } elseif ('datetime' == $attributeParams['type']) { $value = (new \DateTime())->setTimestamp(strtotime($rowData[$attributeAlias])); $value = $value->format(\Magento\Framework\Stdlib\DateTime::DATETIME_PHP_FORMAT); } elseif ('multiselect' == $attributeParams['type']) { $ids = []; foreach (explode($multiSeparator, mb_strtolower($rowData[$attributeAlias])) as $subValue) { $ids[] = $this->getSelectAttrIdByValue($attributeParams, $subValue); } $value = implode(',', $ids); } else { $value = $rowData[$attributeAlias]; } if ($attributeParams['is_static']) { $entityRow[$attributeAlias] = $value; } else { $attributes[$attributeParams['table']][$addressId][$attributeParams['id']]= $value; } } } foreach (self::getDefaultAddressAttributeMapping() as $columnName => $attributeCode) { if (!empty($rowData[$columnName])) { /** @var $attribute \Magento\Eav\Model\Entity\Attribute\AbstractAttribute */ $table = $this->_getCustomerEntity()->getResource()->getTable('customer_entity'); $defaults[$table][$customerId][$attributeCode] = $addressId; } } // let's try to find region ID $entityRow['region_id'] = null; if (!empty($rowData[self::COLUMN_REGION])) { $countryNormalized = strtolower($rowData[self::COLUMN_COUNTRY_ID]); $regionNormalized = strtolower($rowData[self::COLUMN_REGION]); if (isset($this->_countryRegions[$countryNormalized][$regionNormalized])) { $regionId = $this->_countryRegions[$countryNormalized][$regionNormalized]; $entityRow[self::COLUMN_REGION] = $this->_regions[$regionId]; $entityRow['region_id'] = $regionId; } } if ($newAddress) { $entityRowNew = $entityRow; $entityRowNew['created_at'] = (new \DateTime())->format(\Magento\Framework\Stdlib\DateTime::DATETIME_PHP_FORMAT); } else { $entityRowUpdate = $entityRow; } return [ 'entity_row_new' => $entityRowNew, 'entity_row_update' => $entityRowUpdate, 'attributes' => $attributes, 'defaults' => $defaults ]; } /** * Update and insert data in entity table * * @param array $addRows Rows for insert * @param array $updateRows Rows for update * @return $this */ protected function _saveAddressEntities(array $addRows, array $updateRows) { if ($addRows) { $this->_connection->insertMultiple($this->_entityTable, $addRows); } if ($updateRows) { //list of updated fields can be different for addresses. We can not use insertOnDuplicate for whole rows. foreach ($updateRows as $row) { $fields = array_diff(array_keys($row), ['entity_id', 'parent_id', 'created_at']); $this->_connection->insertOnDuplicate($this->_entityTable, $row, $fields); } } return $this; } /** * Save custom customer address attributes * * @param array $attributesData * @return $this */ protected function _saveAddressAttributes(array $attributesData) { foreach ($attributesData as $tableName => $data) { $tableData = []; foreach ($data as $addressId => $attributeData) { foreach ($attributeData as $attributeId => $value) { $tableData[] = [ 'entity_id' => $addressId, 'attribute_id' => $attributeId, 'value' => $value, ]; } } $this->_connection->insertOnDuplicate($tableName, $tableData, ['value']); } return $this; } /** * Save customer default addresses * * @param array $defaults * @return $this * @SuppressWarnings(PHPMD.UnusedLocalVariable) */ protected function _saveCustomerDefaults(array $defaults) { foreach ($defaults as $tableName => $data) { foreach ($data as $customerId => $defaultsData) { $data = array_merge( ['entity_id' => $customerId], $defaultsData ); $this->_connection->insertOnDuplicate($tableName, $data, array_keys($defaultsData)); } } return $this; } /** * Delete data from entity table * * @param array $entityRowIds Row IDs for delete * @return $this */ protected function _deleteAddressEntities(array $entityRowIds) { if ($entityRowIds) { $this->_connection->delete($this->_entityTable, ['entity_id IN (?)' => $entityRowIds]); } return $this; } /** * EAV entity type code getter * * @abstract * @return string */ public function getEntityTypeCode() { return 'customer_address'; } /** * Customer default addresses column name to customer attribute mapping array * * @static * @return array */ public static function getDefaultAddressAttributeMapping() { return self::$_defaultAddressAttributeMapping; } /** * Check if address for import is empty (for customer composite mode) * * @param array $rowData * @return array */ protected function _isOptionalAddressEmpty(array $rowData) { if (empty($this->_customerAttributes)) { return false; } unset( $rowData[Customer::COLUMN_WEBSITE], $rowData[Customer::COLUMN_STORE], $rowData['_email'] ); foreach ($rowData as $key => $value) { if (!in_array($key, $this->_customerAttributes) && !empty($value)) { return false; } } return true; } /** * Validate row for add/update action * * @param array $rowData * @param int $rowNumber * @return void * @SuppressWarnings(PHPMD.CyclomaticComplexity) * @SuppressWarnings(PHPMD.NPathComplexity) */ protected function _validateRowForUpdate(array $rowData, $rowNumber) { $multiSeparator = $this->getMultipleValueSeparator(); if ($this->_checkUniqueKey($rowData, $rowNumber)) { $email = strtolower($rowData[self::COLUMN_EMAIL]); $website = $rowData[self::COLUMN_WEBSITE]; $addressId = $rowData[self::COLUMN_ADDRESS_ID]; $customerId = $this->_getCustomerId($email, $website); if ($customerId === false) { $this->addRowError(self::ERROR_CUSTOMER_NOT_FOUND, $rowNumber); } else { if ($this->_checkRowDuplicate($customerId, $addressId)) { $this->addRowError(self::ERROR_DUPLICATE_PK, $rowNumber); } else { // check simple attributes foreach ($this->_attributes as $attributeCode => $attributeParams) { $websiteId = $this->_websiteCodeToId[$website]; $attributeParams = $this->adjustAttributeDataForWebsite($attributeParams, $websiteId); if (in_array($attributeCode, $this->_ignoredAttributes)) { continue; } if (isset($rowData[$attributeCode]) && strlen($rowData[$attributeCode])) { $this->isAttributeValid( $attributeCode, $attributeParams, $rowData, $rowNumber, $multiSeparator ); } elseif ($attributeParams['is_required'] && !$this->addressStorage->doesExist( (string)$addressId, (string)$customerId ) ) { $this->addRowError(self::ERROR_VALUE_IS_REQUIRED, $rowNumber, $attributeCode); } } if (isset($rowData[self::COLUMN_POSTCODE]) && isset($rowData[self::COLUMN_COUNTRY_ID]) && !$this->postcodeValidator->isValid( $rowData[self::COLUMN_COUNTRY_ID], $rowData[self::COLUMN_POSTCODE] ) ) { $this->addRowError(self::ERROR_VALUE_IS_REQUIRED, $rowNumber, self::COLUMN_POSTCODE); } if (isset($rowData[self::COLUMN_COUNTRY_ID]) && isset($rowData[self::COLUMN_REGION])) { $countryRegions = isset( $this->_countryRegions[strtolower($rowData[self::COLUMN_COUNTRY_ID])] ) ? $this->_countryRegions[strtolower( $rowData[self::COLUMN_COUNTRY_ID] )] : []; if (!empty($rowData[self::COLUMN_REGION]) && !empty($countryRegions) && !isset( $countryRegions[strtolower($rowData[self::COLUMN_REGION])] ) ) { $this->addRowError(self::ERROR_INVALID_REGION, $rowNumber, self::COLUMN_REGION); } } } } } } /** * Validate row for delete action * * @param array $rowData * @param int $rowNumber * @return void */ protected function _validateRowForDelete(array $rowData, $rowNumber) { if ($this->_checkUniqueKey($rowData, $rowNumber)) { $email = strtolower($rowData[self::COLUMN_EMAIL]); $website = $rowData[self::COLUMN_WEBSITE]; $addressId = $rowData[self::COLUMN_ADDRESS_ID]; $customerId = $this->_getCustomerId($email, $website); if ($customerId === false) { $this->addRowError(self::ERROR_CUSTOMER_NOT_FOUND, $rowNumber); } else { if (!strlen($addressId)) { $this->addRowError(self::ERROR_ADDRESS_ID_IS_EMPTY, $rowNumber); } elseif (!$this->addressStorage->doesExist( (string)$addressId, (string)$customerId )) { $this->addRowError(self::ERROR_ADDRESS_NOT_FOUND, $rowNumber); } } } } /** * Check whether row with such address id was already found in import file * * @param int $customerId * @param int $addressId * @return bool */ protected function _checkRowDuplicate($customerId, $addressId) { if ($this->addressStorage->doesExist( (string)$addressId, (string)$customerId )) { if (!isset($this->_importedRowPks[$customerId][$addressId])) { $this->_importedRowPks[$customerId][$addressId] = true; return false; } else { return true; } } else { return false; } } /** * Set customer attributes * * @param array $customerAttributes * @return $this */ public function setCustomerAttributes($customerAttributes) { $this->_customerAttributes = $customerAttributes; return $this; } }