Address.php 33 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930931932933934935936937938939940941942943944945946947948949950951952953954955956957958959960
  1. <?php
  2. /**
  3. * Copyright © Magento, Inc. All rights reserved.
  4. * See COPYING.txt for license details.
  5. */
  6. namespace Magento\CustomerImportExport\Model\Import;
  7. use Magento\Customer\Model\ResourceModel\Address\Attribute\Source\CountryWithWebsites as CountryWithWebsitesSource;
  8. use Magento\Eav\Model\Entity\Attribute\AbstractAttribute;
  9. use Magento\Framework\App\ObjectManager;
  10. use Magento\ImportExport\Model\Import\ErrorProcessing\ProcessingErrorAggregatorInterface;
  11. use Magento\Store\Model\Store;
  12. use Magento\CustomerImportExport\Model\ResourceModel\Import\Address\Storage as AddressStorage;
  13. use Magento\ImportExport\Model\Import\AbstractSource;
  14. /**
  15. * Customer address import
  16. *
  17. * @SuppressWarnings(PHPMD.TooManyFields)
  18. * @SuppressWarnings(PHPMD.CouplingBetweenObjects)
  19. * @SuppressWarnings(PHPMD.ExcessiveClassComplexity)
  20. */
  21. class Address extends AbstractCustomer
  22. {
  23. /**#@+
  24. * Attribute collection name
  25. */
  26. const ATTRIBUTE_COLLECTION_NAME = \Magento\Customer\Model\ResourceModel\Address\Attribute\Collection::class;
  27. /**#@-*/
  28. /**#@+
  29. * Permanent column names
  30. *
  31. * Names that begins with underscore is not an attribute.
  32. * This name convention is for to avoid interference with same attribute name.
  33. */
  34. const COLUMN_EMAIL = '_email';
  35. const COLUMN_ADDRESS_ID = '_entity_id';
  36. /**#@-*/
  37. /**#@+
  38. * Required column names
  39. */
  40. const COLUMN_REGION = 'region';
  41. const COLUMN_COUNTRY_ID = 'country_id';
  42. const COLUMN_POSTCODE = 'postcode';
  43. /**#@-*/
  44. /**#@+
  45. * Particular columns that contains of customer default addresses
  46. */
  47. const COLUMN_DEFAULT_BILLING = '_address_default_billing_';
  48. const COLUMN_DEFAULT_SHIPPING = '_address_default_shipping_';
  49. /**#@-*/
  50. /**#@+
  51. * Error codes
  52. */
  53. const ERROR_ADDRESS_ID_IS_EMPTY = 'addressIdIsEmpty';
  54. const ERROR_ADDRESS_NOT_FOUND = 'addressNotFound';
  55. const ERROR_INVALID_REGION = 'invalidRegion';
  56. const ERROR_DUPLICATE_PK = 'duplicateAddressId';
  57. /**#@-*/
  58. /**#@-*/
  59. protected static $_defaultAddressAttributeMapping = [
  60. self::COLUMN_DEFAULT_BILLING => 'default_billing',
  61. self::COLUMN_DEFAULT_SHIPPING => 'default_shipping',
  62. ];
  63. /**
  64. * Permanent entity columns
  65. *
  66. * @var string[]
  67. */
  68. protected $_permanentAttributes = [self::COLUMN_WEBSITE, self::COLUMN_EMAIL, self::COLUMN_ADDRESS_ID];
  69. /**
  70. * Attributes with index (not label) value
  71. *
  72. * @var string[]
  73. */
  74. protected $_indexValueAttributes = [self::COLUMN_COUNTRY_ID];
  75. /**
  76. * Customer entity DB table name
  77. *
  78. * @var string
  79. */
  80. protected $_entityTable;
  81. /**
  82. * Region collection instance
  83. *
  84. * @var \Magento\Directory\Model\ResourceModel\Region\Collection
  85. */
  86. private $_regionCollection;
  87. /**
  88. * Countries and regions
  89. *
  90. * Example array: array(
  91. * [country_id_lowercased_1] => array(
  92. * [region_code_lowercased_1] => region_id_1,
  93. * [region_default_name_lowercased_1] => region_id_1,
  94. * ...,
  95. * [region_code_lowercased_n] => region_id_n,
  96. * [region_default_name_lowercased_n] => region_id_n
  97. * ),
  98. * ...
  99. * )
  100. *
  101. * @var array
  102. */
  103. protected $_countryRegions = [];
  104. /**
  105. * Region ID to region default name pairs
  106. *
  107. * @var array
  108. */
  109. protected $_regions = [];
  110. /**
  111. * Column names that holds values with particular meaning
  112. *
  113. * @var string[]
  114. */
  115. protected $_specialAttributes = [
  116. self::COLUMN_ACTION,
  117. self::COLUMN_WEBSITE,
  118. self::COLUMN_EMAIL,
  119. self::COLUMN_ADDRESS_ID,
  120. self::COLUMN_DEFAULT_BILLING,
  121. self::COLUMN_DEFAULT_SHIPPING,
  122. ];
  123. /**
  124. * Customer entity
  125. *
  126. * @var \Magento\Customer\Model\Customer
  127. */
  128. protected $_customerEntity;
  129. /**
  130. * Entity ID incremented value
  131. *
  132. * @var int
  133. */
  134. protected $_nextEntityId;
  135. /**
  136. * Array of region parameters
  137. *
  138. * @var array
  139. */
  140. protected $_regionParameters;
  141. /**
  142. * Address attributes collection
  143. *
  144. * @var \Magento\Customer\Model\ResourceModel\Address\Attribute\Collection
  145. */
  146. protected $_attributeCollection;
  147. /**
  148. * Store imported row primary keys
  149. *
  150. * @var array
  151. */
  152. protected $_importedRowPks = [];
  153. /**
  154. * @var \Magento\ImportExport\Model\ResourceModel\Helper
  155. */
  156. protected $_resourceHelper;
  157. /**
  158. * @var \Magento\Customer\Model\CustomerFactory
  159. */
  160. protected $_customerFactory;
  161. /**
  162. * @var \Magento\Eav\Model\Config
  163. */
  164. protected $_eavConfig;
  165. /**
  166. * @var \Magento\Customer\Model\AddressFactory
  167. */
  168. protected $_addressFactory;
  169. /**
  170. * @var \Magento\Framework\Stdlib\DateTime
  171. */
  172. protected $dateTime;
  173. /**
  174. * Customer attributes
  175. *
  176. * @var string[]
  177. */
  178. protected $_customerAttributes = [];
  179. /**
  180. * Valid column names
  181. *
  182. * @array
  183. */
  184. protected $validColumnNames = [
  185. "region_id", "vat_is_valid", "vat_request_date", "vat_request_id", "vat_request_success"
  186. ];
  187. /**
  188. * @var \Magento\Customer\Model\Address\Validator\Postcode
  189. */
  190. protected $postcodeValidator;
  191. /**
  192. * @var CountryWithWebsitesSource
  193. */
  194. private $countryWithWebsites;
  195. /**
  196. * Options for certain attributes sorted by websites.
  197. *
  198. * @var array[][] With path as <attributeCode> => <websiteID> => options[].
  199. */
  200. private $optionsByWebsite = [];
  201. /**
  202. * @var AddressStorage
  203. */
  204. private $addressStorage;
  205. /**
  206. * @param \Magento\Framework\Stdlib\StringUtils $string
  207. * @param \Magento\Framework\App\Config\ScopeConfigInterface $scopeConfig
  208. * @param \Magento\ImportExport\Model\ImportFactory $importFactory
  209. * @param \Magento\ImportExport\Model\ResourceModel\Helper $resourceHelper
  210. * @param \Magento\Framework\App\ResourceConnection $resource
  211. * @param ProcessingErrorAggregatorInterface $errorAggregator
  212. * @param \Magento\Store\Model\StoreManagerInterface $storeManager
  213. * @param \Magento\ImportExport\Model\Export\Factory $collectionFactory
  214. * @param \Magento\Eav\Model\Config $eavConfig
  215. * @param \Magento\CustomerImportExport\Model\ResourceModel\Import\Customer\StorageFactory $storageFactory
  216. * @param \Magento\Customer\Model\AddressFactory $addressFactory
  217. * @param \Magento\Directory\Model\ResourceModel\Region\CollectionFactory $regionColFactory
  218. * @param \Magento\Customer\Model\CustomerFactory $customerFactory
  219. * @param \Magento\Customer\Model\ResourceModel\Address\Attribute\CollectionFactory $attributesFactory
  220. * @param \Magento\Framework\Stdlib\DateTime $dateTime
  221. * @param \Magento\Customer\Model\Address\Validator\Postcode $postcodeValidator
  222. * @param array $data
  223. * @param CountryWithWebsitesSource|null $countryWithWebsites
  224. * @param AddressStorage|null $addressStorage
  225. *
  226. * @SuppressWarnings(PHPMD.NPathComplexity)
  227. * @SuppressWarnings(PHPMD.ExcessiveParameterList)
  228. */
  229. public function __construct(
  230. \Magento\Framework\Stdlib\StringUtils $string,
  231. \Magento\Framework\App\Config\ScopeConfigInterface $scopeConfig,
  232. \Magento\ImportExport\Model\ImportFactory $importFactory,
  233. \Magento\ImportExport\Model\ResourceModel\Helper $resourceHelper,
  234. \Magento\Framework\App\ResourceConnection $resource,
  235. ProcessingErrorAggregatorInterface $errorAggregator,
  236. \Magento\Store\Model\StoreManagerInterface $storeManager,
  237. \Magento\ImportExport\Model\Export\Factory $collectionFactory,
  238. \Magento\Eav\Model\Config $eavConfig,
  239. \Magento\CustomerImportExport\Model\ResourceModel\Import\Customer\StorageFactory $storageFactory,
  240. \Magento\Customer\Model\AddressFactory $addressFactory,
  241. \Magento\Directory\Model\ResourceModel\Region\CollectionFactory $regionColFactory,
  242. \Magento\Customer\Model\CustomerFactory $customerFactory,
  243. \Magento\Customer\Model\ResourceModel\Address\Attribute\CollectionFactory $attributesFactory,
  244. \Magento\Framework\Stdlib\DateTime $dateTime,
  245. \Magento\Customer\Model\Address\Validator\Postcode $postcodeValidator,
  246. array $data = [],
  247. CountryWithWebsitesSource $countryWithWebsites = null,
  248. AddressStorage $addressStorage = null
  249. ) {
  250. $this->_customerFactory = $customerFactory;
  251. $this->_addressFactory = $addressFactory;
  252. $this->_eavConfig = $eavConfig;
  253. $this->_resourceHelper = $resourceHelper;
  254. $this->dateTime = $dateTime;
  255. $this->postcodeValidator = $postcodeValidator;
  256. $this->countryWithWebsites = $countryWithWebsites ?:
  257. ObjectManager::getInstance()->get(CountryWithWebsitesSource::class);
  258. if (!isset($data['attribute_collection'])) {
  259. /** @var $attributeCollection \Magento\Customer\Model\ResourceModel\Address\Attribute\Collection */
  260. $attributeCollection = $attributesFactory->create();
  261. $attributeCollection->addSystemHiddenFilter()->addExcludeHiddenFrontendFilter();
  262. $data['attribute_collection'] = $attributeCollection;
  263. }
  264. parent::__construct(
  265. $string,
  266. $scopeConfig,
  267. $importFactory,
  268. $resourceHelper,
  269. $resource,
  270. $errorAggregator,
  271. $storeManager,
  272. $collectionFactory,
  273. $eavConfig,
  274. $storageFactory,
  275. $data
  276. );
  277. $this->_entityTable = isset(
  278. $data['entity_table']
  279. ) ? $data['entity_table'] : $addressFactory->create()->getResource()->getEntityTable();
  280. $this->_regionCollection = isset(
  281. $data['region_collection']
  282. ) ? $data['region_collection'] : $regionColFactory->create();
  283. $this->addMessageTemplate(self::ERROR_ADDRESS_ID_IS_EMPTY, __('Customer address id column is not specified'));
  284. $this->addMessageTemplate(
  285. self::ERROR_ADDRESS_NOT_FOUND,
  286. __('We can\'t find that customer address.')
  287. );
  288. $this->addMessageTemplate(self::ERROR_INVALID_REGION, __('Please enter a valid region.'));
  289. $this->addMessageTemplate(
  290. self::ERROR_DUPLICATE_PK,
  291. __('We found another row with this email, website and address ID combination.')
  292. );
  293. $this->addressStorage = $addressStorage
  294. ?: ObjectManager::getInstance()->get(AddressStorage::class);
  295. $this->_initAttributes();
  296. $this->_initCountryRegions();
  297. }
  298. /**
  299. * @inheritDoc
  300. */
  301. public function getAttributeOptions(AbstractAttribute $attribute, array $indexAttributes = [])
  302. {
  303. $standardOptions = parent::getAttributeOptions($attribute, $indexAttributes);
  304. if ($attribute->getAttributeCode() === 'country_id') {
  305. //If we want to get available options for country field then we have to use alternative source
  306. // to get actual data for each website.
  307. $options = $this->countryWithWebsites->getAllOptions();
  308. //Available country options now will be sorted by websites.
  309. $code = $attribute->getAttributeCode();
  310. $websiteOptions = [Store::DEFAULT_STORE_ID => $standardOptions];
  311. //Sorting options by website.
  312. foreach ($options as $option) {
  313. if (array_key_exists('website_ids', $option)) {
  314. foreach ($option['website_ids'] as $websiteId) {
  315. if (!array_key_exists($websiteId, $websiteOptions)) {
  316. $websiteOptions[$websiteId] = [];
  317. }
  318. $optionId = mb_strtolower($option['value']);
  319. $websiteOptions[$websiteId][$optionId] = $option['value'];
  320. }
  321. }
  322. }
  323. //Storing sorted
  324. $this->optionsByWebsite[$code] = $websiteOptions;
  325. }
  326. return $standardOptions;
  327. }
  328. /**
  329. * Attributes' data may vary depending on website settings,
  330. * this method adjusts an attribute's data from $this->_attributes to
  331. * website-specific data.
  332. *
  333. * @param array $attributeData Data from $this->_attributes.
  334. * @param int $websiteId
  335. *
  336. * @return array Adjusted data in the same format.
  337. */
  338. private function adjustAttributeDataForWebsite(array $attributeData, int $websiteId): array
  339. {
  340. if ($attributeData['code'] === 'country_id') {
  341. $attributeOptions = $this->optionsByWebsite[$attributeData['code']];
  342. if (array_key_exists($websiteId, $attributeOptions)) {
  343. $attributeData['options'] = $attributeOptions[$websiteId];
  344. }
  345. }
  346. return $attributeData;
  347. }
  348. /**
  349. * Customer entity getter
  350. *
  351. * @return \Magento\Customer\Model\Customer
  352. */
  353. protected function _getCustomerEntity()
  354. {
  355. if (!$this->_customerEntity) {
  356. $this->_customerEntity = $this->_customerFactory->create();
  357. }
  358. return $this->_customerEntity;
  359. }
  360. /**
  361. * Get next address entity ID
  362. *
  363. * @return int
  364. */
  365. protected function _getNextEntityId()
  366. {
  367. if (!$this->_nextEntityId) {
  368. /** @var $addressResource \Magento\Customer\Model\ResourceModel\Address */
  369. $addressResource = $this->_addressFactory->create()->getResource();
  370. $addressTable = $addressResource->getEntityTable();
  371. $this->_nextEntityId = $this->_resourceHelper->getNextAutoincrement($addressTable);
  372. }
  373. return $this->_nextEntityId++;
  374. }
  375. /**
  376. * Initialize country regions hash for clever recognition
  377. *
  378. * @return $this
  379. */
  380. protected function _initCountryRegions()
  381. {
  382. /** @var $region \Magento\Directory\Model\Region */
  383. foreach ($this->_regionCollection as $region) {
  384. $countryNormalized = strtolower($region->getCountryId());
  385. $regionCode = strtolower($region->getCode());
  386. $regionName = strtolower($region->getDefaultName());
  387. $this->_countryRegions[$countryNormalized][$regionCode] = $region->getId();
  388. $this->_countryRegions[$countryNormalized][$regionName] = $region->getId();
  389. $this->_regions[$region->getId()] = $region->getDefaultName();
  390. }
  391. return $this;
  392. }
  393. /**
  394. * Pre-loading customers for existing customers checks in order
  395. * to perform mass validation/import efficiently.
  396. * Also loading existing addresses for requested customers.
  397. *
  398. * @param array|AbstractSource $rows Each row must contain data from columns email
  399. * and website code.
  400. *
  401. * @return void
  402. */
  403. public function prepareCustomerData($rows): void
  404. {
  405. $customersPresent = [];
  406. foreach ($rows as $rowData) {
  407. $email = $rowData[static::COLUMN_EMAIL] ?? null;
  408. $websiteId = isset($rowData[static::COLUMN_WEBSITE])
  409. ? $this->getWebsiteId($rowData[static::COLUMN_WEBSITE]) : false;
  410. if ($email && $websiteId !== false) {
  411. $customersPresent[] = [
  412. 'email' => $email,
  413. 'website_id' => $websiteId,
  414. ];
  415. }
  416. }
  417. $this->getCustomerStorage()->prepareCustomers($customersPresent);
  418. $ids = [];
  419. foreach ($customersPresent as $customerData) {
  420. $id = $this->getCustomerStorage()->getCustomerId(
  421. $customerData['email'],
  422. $customerData['website_id']
  423. );
  424. if ($id) {
  425. $ids[] = $id;
  426. }
  427. }
  428. $this->addressStorage->prepareAddresses($ids);
  429. }
  430. /**
  431. * @inheritDoc
  432. */
  433. public function validateData()
  434. {
  435. $this->prepareCustomerData($this->getSource());
  436. return parent::validateData();
  437. }
  438. /**
  439. * Import data rows
  440. *
  441. * @abstract
  442. * @return boolean
  443. * @SuppressWarnings(PHPMD.CyclomaticComplexity)
  444. */
  445. protected function _importData()
  446. {
  447. //Preparing data for mass validation/import.
  448. $rows = [];
  449. while ($bunch = $this->_dataSourceModel->getNextBunch()) {
  450. $rows = array_merge($rows, $bunch);
  451. }
  452. $this->prepareCustomerData($rows);
  453. unset($bunch, $rows);
  454. $this->_dataSourceModel->getIterator()->rewind();
  455. //Importing
  456. while ($bunch = $this->_dataSourceModel->getNextBunch()) {
  457. $newRows = [];
  458. $updateRows = [];
  459. $attributes = [];
  460. $defaults = [];
  461. // customer default addresses (billing/shipping) data
  462. $deleteRowIds = [];
  463. foreach ($bunch as $rowNumber => $rowData) {
  464. // check row data
  465. if ($this->_isOptionalAddressEmpty($rowData) || !$this->validateRow($rowData, $rowNumber)) {
  466. continue;
  467. }
  468. if ($this->getErrorAggregator()->hasToBeTerminated()) {
  469. $this->getErrorAggregator()->addRowToSkip($rowNumber);
  470. continue;
  471. }
  472. if ($this->getBehavior($rowData) == \Magento\ImportExport\Model\Import::BEHAVIOR_ADD_UPDATE) {
  473. $addUpdateResult = $this->_prepareDataForUpdate($rowData);
  474. if ($addUpdateResult['entity_row_new']) {
  475. $newRows[] = $addUpdateResult['entity_row_new'];
  476. }
  477. if ($addUpdateResult['entity_row_update']) {
  478. $updateRows[] = $addUpdateResult['entity_row_update'];
  479. }
  480. $attributes = $this->_mergeEntityAttributes($addUpdateResult['attributes'], $attributes);
  481. $defaults = $this->_mergeEntityAttributes($addUpdateResult['defaults'], $defaults);
  482. } elseif ($this->getBehavior($rowData) == \Magento\ImportExport\Model\Import::BEHAVIOR_DELETE) {
  483. $deleteRowIds[] = $rowData[self::COLUMN_ADDRESS_ID];
  484. }
  485. }
  486. $this->updateItemsCounterStats($newRows, $updateRows, $deleteRowIds);
  487. $this->_saveAddressEntities($newRows, $updateRows)
  488. ->_saveAddressAttributes($attributes)
  489. ->_saveCustomerDefaults($defaults);
  490. $this->_deleteAddressEntities($deleteRowIds);
  491. }
  492. return true;
  493. }
  494. /**
  495. * Merge attributes
  496. *
  497. * @param array $newAttributes
  498. * @param array $attributes
  499. * @return array
  500. */
  501. protected function _mergeEntityAttributes(array $newAttributes, array $attributes)
  502. {
  503. foreach ($newAttributes as $tableName => $tableData) {
  504. foreach ($tableData as $entityId => $entityData) {
  505. foreach ($entityData as $attributeId => $attributeValue) {
  506. $attributes[$tableName][$entityId][$attributeId] = $attributeValue;
  507. }
  508. }
  509. }
  510. return $attributes;
  511. }
  512. /**
  513. * Prepare data for add/update action
  514. *
  515. * @param array $rowData
  516. * @return array
  517. * @SuppressWarnings(PHPMD.CyclomaticComplexity)
  518. * @SuppressWarnings(PHPMD.NPathComplexity)
  519. */
  520. protected function _prepareDataForUpdate(array $rowData):array
  521. {
  522. $multiSeparator = $this->getMultipleValueSeparator();
  523. $email = strtolower($rowData[self::COLUMN_EMAIL]);
  524. $customerId = $this->_getCustomerId($email, $rowData[self::COLUMN_WEBSITE]);
  525. // entity table data
  526. $entityRowNew = [];
  527. $entityRowUpdate = [];
  528. // attribute values
  529. $attributes = [];
  530. // customer default addresses
  531. $defaults = [];
  532. $newAddress = true;
  533. // get address id
  534. if ($rowData[self::COLUMN_ADDRESS_ID]
  535. && $this->addressStorage->doesExist(
  536. $rowData[self::COLUMN_ADDRESS_ID],
  537. (string)$customerId
  538. )
  539. ) {
  540. $newAddress = false;
  541. $addressId = $rowData[self::COLUMN_ADDRESS_ID];
  542. } else {
  543. $addressId = $this->_getNextEntityId();
  544. }
  545. $entityRow = [
  546. 'entity_id' => $addressId,
  547. 'parent_id' => $customerId,
  548. 'updated_at' => (new \DateTime())->format(\Magento\Framework\Stdlib\DateTime::DATETIME_PHP_FORMAT),
  549. ];
  550. $websiteId = $this->_websiteCodeToId[$rowData[self::COLUMN_WEBSITE]];
  551. foreach ($this->_attributes as $attributeAlias => $attributeParams) {
  552. if (array_key_exists($attributeAlias, $rowData)) {
  553. $attributeParams = $this->adjustAttributeDataForWebsite($attributeParams, $websiteId);
  554. if (!strlen($rowData[$attributeAlias])) {
  555. if ($newAddress) {
  556. $value = null;
  557. } else {
  558. continue;
  559. }
  560. } elseif ($newAddress && !strlen($rowData[$attributeAlias])) {
  561. } elseif (in_array($attributeParams['type'], ['select', 'boolean'])) {
  562. $value = $this->getSelectAttrIdByValue($attributeParams, mb_strtolower($rowData[$attributeAlias]));
  563. } elseif ('datetime' == $attributeParams['type']) {
  564. $value = (new \DateTime())->setTimestamp(strtotime($rowData[$attributeAlias]));
  565. $value = $value->format(\Magento\Framework\Stdlib\DateTime::DATETIME_PHP_FORMAT);
  566. } elseif ('multiselect' == $attributeParams['type']) {
  567. $ids = [];
  568. foreach (explode($multiSeparator, mb_strtolower($rowData[$attributeAlias])) as $subValue) {
  569. $ids[] = $this->getSelectAttrIdByValue($attributeParams, $subValue);
  570. }
  571. $value = implode(',', $ids);
  572. } else {
  573. $value = $rowData[$attributeAlias];
  574. }
  575. if ($attributeParams['is_static']) {
  576. $entityRow[$attributeAlias] = $value;
  577. } else {
  578. $attributes[$attributeParams['table']][$addressId][$attributeParams['id']]= $value;
  579. }
  580. }
  581. }
  582. foreach (self::getDefaultAddressAttributeMapping() as $columnName => $attributeCode) {
  583. if (!empty($rowData[$columnName])) {
  584. /** @var $attribute \Magento\Eav\Model\Entity\Attribute\AbstractAttribute */
  585. $table = $this->_getCustomerEntity()->getResource()->getTable('customer_entity');
  586. $defaults[$table][$customerId][$attributeCode] = $addressId;
  587. }
  588. }
  589. // let's try to find region ID
  590. $entityRow['region_id'] = null;
  591. if (!empty($rowData[self::COLUMN_REGION])) {
  592. $countryNormalized = strtolower($rowData[self::COLUMN_COUNTRY_ID]);
  593. $regionNormalized = strtolower($rowData[self::COLUMN_REGION]);
  594. if (isset($this->_countryRegions[$countryNormalized][$regionNormalized])) {
  595. $regionId = $this->_countryRegions[$countryNormalized][$regionNormalized];
  596. $entityRow[self::COLUMN_REGION] = $this->_regions[$regionId];
  597. $entityRow['region_id'] = $regionId;
  598. }
  599. }
  600. if ($newAddress) {
  601. $entityRowNew = $entityRow;
  602. $entityRowNew['created_at'] =
  603. (new \DateTime())->format(\Magento\Framework\Stdlib\DateTime::DATETIME_PHP_FORMAT);
  604. } else {
  605. $entityRowUpdate = $entityRow;
  606. }
  607. return [
  608. 'entity_row_new' => $entityRowNew,
  609. 'entity_row_update' => $entityRowUpdate,
  610. 'attributes' => $attributes,
  611. 'defaults' => $defaults
  612. ];
  613. }
  614. /**
  615. * Update and insert data in entity table
  616. *
  617. * @param array $addRows Rows for insert
  618. * @param array $updateRows Rows for update
  619. * @return $this
  620. */
  621. protected function _saveAddressEntities(array $addRows, array $updateRows)
  622. {
  623. if ($addRows) {
  624. $this->_connection->insertMultiple($this->_entityTable, $addRows);
  625. }
  626. if ($updateRows) {
  627. //list of updated fields can be different for addresses. We can not use insertOnDuplicate for whole rows.
  628. foreach ($updateRows as $row) {
  629. $fields = array_diff(array_keys($row), ['entity_id', 'parent_id', 'created_at']);
  630. $this->_connection->insertOnDuplicate($this->_entityTable, $row, $fields);
  631. }
  632. }
  633. return $this;
  634. }
  635. /**
  636. * Save custom customer address attributes
  637. *
  638. * @param array $attributesData
  639. * @return $this
  640. */
  641. protected function _saveAddressAttributes(array $attributesData)
  642. {
  643. foreach ($attributesData as $tableName => $data) {
  644. $tableData = [];
  645. foreach ($data as $addressId => $attributeData) {
  646. foreach ($attributeData as $attributeId => $value) {
  647. $tableData[] = [
  648. 'entity_id' => $addressId,
  649. 'attribute_id' => $attributeId,
  650. 'value' => $value,
  651. ];
  652. }
  653. }
  654. $this->_connection->insertOnDuplicate($tableName, $tableData, ['value']);
  655. }
  656. return $this;
  657. }
  658. /**
  659. * Save customer default addresses
  660. *
  661. * @param array $defaults
  662. * @return $this
  663. * @SuppressWarnings(PHPMD.UnusedLocalVariable)
  664. */
  665. protected function _saveCustomerDefaults(array $defaults)
  666. {
  667. foreach ($defaults as $tableName => $data) {
  668. foreach ($data as $customerId => $defaultsData) {
  669. $data = array_merge(
  670. ['entity_id' => $customerId],
  671. $defaultsData
  672. );
  673. $this->_connection->insertOnDuplicate($tableName, $data, array_keys($defaultsData));
  674. }
  675. }
  676. return $this;
  677. }
  678. /**
  679. * Delete data from entity table
  680. *
  681. * @param array $entityRowIds Row IDs for delete
  682. * @return $this
  683. */
  684. protected function _deleteAddressEntities(array $entityRowIds)
  685. {
  686. if ($entityRowIds) {
  687. $this->_connection->delete($this->_entityTable, ['entity_id IN (?)' => $entityRowIds]);
  688. }
  689. return $this;
  690. }
  691. /**
  692. * EAV entity type code getter
  693. *
  694. * @abstract
  695. * @return string
  696. */
  697. public function getEntityTypeCode()
  698. {
  699. return 'customer_address';
  700. }
  701. /**
  702. * Customer default addresses column name to customer attribute mapping array
  703. *
  704. * @static
  705. * @return array
  706. */
  707. public static function getDefaultAddressAttributeMapping()
  708. {
  709. return self::$_defaultAddressAttributeMapping;
  710. }
  711. /**
  712. * Check if address for import is empty (for customer composite mode)
  713. *
  714. * @param array $rowData
  715. * @return array
  716. */
  717. protected function _isOptionalAddressEmpty(array $rowData)
  718. {
  719. if (empty($this->_customerAttributes)) {
  720. return false;
  721. }
  722. unset(
  723. $rowData[Customer::COLUMN_WEBSITE],
  724. $rowData[Customer::COLUMN_STORE],
  725. $rowData['_email']
  726. );
  727. foreach ($rowData as $key => $value) {
  728. if (!in_array($key, $this->_customerAttributes) && !empty($value)) {
  729. return false;
  730. }
  731. }
  732. return true;
  733. }
  734. /**
  735. * Validate row for add/update action
  736. *
  737. * @param array $rowData
  738. * @param int $rowNumber
  739. * @return void
  740. * @SuppressWarnings(PHPMD.CyclomaticComplexity)
  741. * @SuppressWarnings(PHPMD.NPathComplexity)
  742. */
  743. protected function _validateRowForUpdate(array $rowData, $rowNumber)
  744. {
  745. $multiSeparator = $this->getMultipleValueSeparator();
  746. if ($this->_checkUniqueKey($rowData, $rowNumber)) {
  747. $email = strtolower($rowData[self::COLUMN_EMAIL]);
  748. $website = $rowData[self::COLUMN_WEBSITE];
  749. $addressId = $rowData[self::COLUMN_ADDRESS_ID];
  750. $customerId = $this->_getCustomerId($email, $website);
  751. if ($customerId === false) {
  752. $this->addRowError(self::ERROR_CUSTOMER_NOT_FOUND, $rowNumber);
  753. } else {
  754. if ($this->_checkRowDuplicate($customerId, $addressId)) {
  755. $this->addRowError(self::ERROR_DUPLICATE_PK, $rowNumber);
  756. } else {
  757. // check simple attributes
  758. foreach ($this->_attributes as $attributeCode => $attributeParams) {
  759. $websiteId = $this->_websiteCodeToId[$website];
  760. $attributeParams = $this->adjustAttributeDataForWebsite($attributeParams, $websiteId);
  761. if (in_array($attributeCode, $this->_ignoredAttributes)) {
  762. continue;
  763. }
  764. if (isset($rowData[$attributeCode]) && strlen($rowData[$attributeCode])) {
  765. $this->isAttributeValid(
  766. $attributeCode,
  767. $attributeParams,
  768. $rowData,
  769. $rowNumber,
  770. $multiSeparator
  771. );
  772. } elseif ($attributeParams['is_required']
  773. && !$this->addressStorage->doesExist(
  774. (string)$addressId,
  775. (string)$customerId
  776. )
  777. ) {
  778. $this->addRowError(self::ERROR_VALUE_IS_REQUIRED, $rowNumber, $attributeCode);
  779. }
  780. }
  781. if (isset($rowData[self::COLUMN_POSTCODE])
  782. && isset($rowData[self::COLUMN_COUNTRY_ID])
  783. && !$this->postcodeValidator->isValid(
  784. $rowData[self::COLUMN_COUNTRY_ID],
  785. $rowData[self::COLUMN_POSTCODE]
  786. )
  787. ) {
  788. $this->addRowError(self::ERROR_VALUE_IS_REQUIRED, $rowNumber, self::COLUMN_POSTCODE);
  789. }
  790. if (isset($rowData[self::COLUMN_COUNTRY_ID]) && isset($rowData[self::COLUMN_REGION])) {
  791. $countryRegions = isset(
  792. $this->_countryRegions[strtolower($rowData[self::COLUMN_COUNTRY_ID])]
  793. ) ? $this->_countryRegions[strtolower(
  794. $rowData[self::COLUMN_COUNTRY_ID]
  795. )] : [];
  796. if (!empty($rowData[self::COLUMN_REGION]) && !empty($countryRegions) && !isset(
  797. $countryRegions[strtolower($rowData[self::COLUMN_REGION])]
  798. )
  799. ) {
  800. $this->addRowError(self::ERROR_INVALID_REGION, $rowNumber, self::COLUMN_REGION);
  801. }
  802. }
  803. }
  804. }
  805. }
  806. }
  807. /**
  808. * Validate row for delete action
  809. *
  810. * @param array $rowData
  811. * @param int $rowNumber
  812. * @return void
  813. */
  814. protected function _validateRowForDelete(array $rowData, $rowNumber)
  815. {
  816. if ($this->_checkUniqueKey($rowData, $rowNumber)) {
  817. $email = strtolower($rowData[self::COLUMN_EMAIL]);
  818. $website = $rowData[self::COLUMN_WEBSITE];
  819. $addressId = $rowData[self::COLUMN_ADDRESS_ID];
  820. $customerId = $this->_getCustomerId($email, $website);
  821. if ($customerId === false) {
  822. $this->addRowError(self::ERROR_CUSTOMER_NOT_FOUND, $rowNumber);
  823. } else {
  824. if (!strlen($addressId)) {
  825. $this->addRowError(self::ERROR_ADDRESS_ID_IS_EMPTY, $rowNumber);
  826. } elseif (!$this->addressStorage->doesExist(
  827. (string)$addressId,
  828. (string)$customerId
  829. )) {
  830. $this->addRowError(self::ERROR_ADDRESS_NOT_FOUND, $rowNumber);
  831. }
  832. }
  833. }
  834. }
  835. /**
  836. * Check whether row with such address id was already found in import file
  837. *
  838. * @param int $customerId
  839. * @param int $addressId
  840. * @return bool
  841. */
  842. protected function _checkRowDuplicate($customerId, $addressId)
  843. {
  844. if ($this->addressStorage->doesExist(
  845. (string)$addressId,
  846. (string)$customerId
  847. )) {
  848. if (!isset($this->_importedRowPks[$customerId][$addressId])) {
  849. $this->_importedRowPks[$customerId][$addressId] = true;
  850. return false;
  851. } else {
  852. return true;
  853. }
  854. } else {
  855. return false;
  856. }
  857. }
  858. /**
  859. * Set customer attributes
  860. *
  861. * @param array $customerAttributes
  862. * @return $this
  863. */
  864. public function setCustomerAttributes($customerAttributes)
  865. {
  866. $this->_customerAttributes = $customerAttributes;
  867. return $this;
  868. }
  869. }