AbstractEntity.php 26 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908
  1. <?php
  2. /**
  3. * Copyright © Magento, Inc. All rights reserved.
  4. * See COPYING.txt for license details.
  5. */
  6. namespace Magento\ImportExport\Model\Import;
  7. use Magento\Framework\App\ObjectManager;
  8. use Magento\Framework\App\ResourceConnection;
  9. use Magento\Framework\Serialize\Serializer\Json;
  10. use Magento\ImportExport\Model\Import;
  11. use Magento\ImportExport\Model\Import\ErrorProcessing\ProcessingError;
  12. use Magento\ImportExport\Model\Import\ErrorProcessing\ProcessingErrorAggregatorInterface;
  13. /**
  14. * Import entity abstract model
  15. *
  16. * @api
  17. *
  18. * @SuppressWarnings(PHPMD.TooManyFields)
  19. * @SuppressWarnings(PHPMD.CouplingBetweenObjects)
  20. * @since 100.0.2
  21. */
  22. abstract class AbstractEntity
  23. {
  24. /**
  25. * Custom row import behavior column name
  26. */
  27. const COLUMN_ACTION = '_action';
  28. /**
  29. * Value in custom column for delete behaviour
  30. */
  31. const COLUMN_ACTION_VALUE_DELETE = 'delete';
  32. /**#@+
  33. * XML paths to parameters
  34. */
  35. const XML_PATH_BUNCH_SIZE = 'import/format_v2/bunch_size';
  36. const XML_PATH_PAGE_SIZE = 'import/format_v2/page_size';
  37. /**#@-*/
  38. /**#@+
  39. * Database constants
  40. */
  41. const DB_MAX_VARCHAR_LENGTH = 256;
  42. const DB_MAX_TEXT_LENGTH = 65536;
  43. const ERROR_CODE_SYSTEM_EXCEPTION = 'systemException';
  44. const ERROR_CODE_COLUMN_NOT_FOUND = 'columnNotFound';
  45. const ERROR_CODE_COLUMN_EMPTY_HEADER = 'columnEmptyHeader';
  46. const ERROR_CODE_COLUMN_NAME_INVALID = 'columnNameInvalid';
  47. const ERROR_CODE_ATTRIBUTE_NOT_VALID = 'attributeNotInvalid';
  48. const ERROR_CODE_DUPLICATE_UNIQUE_ATTRIBUTE = 'duplicateUniqueAttribute';
  49. const ERROR_CODE_ILLEGAL_CHARACTERS = 'illegalCharacters';
  50. const ERROR_CODE_INVALID_ATTRIBUTE = 'invalidAttributeName';
  51. const ERROR_CODE_WRONG_QUOTES = 'wrongQuotes';
  52. const ERROR_CODE_COLUMNS_NUMBER = 'wrongColumnsNumber';
  53. const ERROR_EXCEEDED_MAX_LENGTH = 'exceededMaxLength';
  54. const ERROR_INVALID_ATTRIBUTE_TYPE = 'invalidAttributeType';
  55. const ERROR_INVALID_ATTRIBUTE_OPTION = 'absentAttributeOption';
  56. /**
  57. * @var array
  58. */
  59. protected $errorMessageTemplates = [
  60. self::ERROR_CODE_SYSTEM_EXCEPTION => 'General system exception happened',
  61. self::ERROR_CODE_COLUMN_NOT_FOUND => 'We can\'t find required columns: %s.',
  62. self::ERROR_CODE_COLUMN_EMPTY_HEADER => 'Columns number: "%s" have empty headers',
  63. self::ERROR_CODE_COLUMN_NAME_INVALID => 'Column names: "%s" are invalid',
  64. self::ERROR_CODE_ATTRIBUTE_NOT_VALID => "Please correct the value for '%s'",
  65. self::ERROR_CODE_DUPLICATE_UNIQUE_ATTRIBUTE => "Duplicate Unique Attribute for '%s'",
  66. self::ERROR_CODE_ILLEGAL_CHARACTERS => "Illegal character used for attribute %s",
  67. self::ERROR_CODE_INVALID_ATTRIBUTE => 'Header contains invalid attribute(s): "%s"',
  68. self::ERROR_CODE_WRONG_QUOTES => "Curly quotes used instead of straight quotes",
  69. self::ERROR_CODE_COLUMNS_NUMBER => "Number of columns does not correspond to the number of rows in the header",
  70. self::ERROR_EXCEEDED_MAX_LENGTH => 'Attribute %s exceeded max length',
  71. self::ERROR_INVALID_ATTRIBUTE_TYPE => 'Value for \'%s\' attribute contains incorrect value',
  72. self::ERROR_INVALID_ATTRIBUTE_OPTION => "Value for %s attribute contains incorrect value"
  73. . ", see acceptable values on settings specified for Admin",
  74. ];
  75. /**#@-*/
  76. /**#@-*/
  77. protected $_connection;
  78. /**
  79. * Has data process validation done?
  80. *
  81. * @var bool
  82. */
  83. protected $_dataValidated = false;
  84. /**
  85. * Valid column names
  86. *
  87. * @array
  88. */
  89. protected $validColumnNames = [];
  90. /**
  91. * If we should check column names
  92. *
  93. * @var bool
  94. */
  95. protected $needColumnCheck = false;
  96. /**
  97. * DB data source model
  98. *
  99. * @var \Magento\ImportExport\Model\ResourceModel\Import\Data
  100. */
  101. protected $_dataSourceModel;
  102. /**
  103. * @var ProcessingErrorAggregatorInterface
  104. */
  105. protected $errorAggregator;
  106. /**
  107. * Flag to disable import
  108. *
  109. * @var bool
  110. */
  111. protected $_importAllowed = true;
  112. /**
  113. * Magento string lib
  114. *
  115. * @var \Magento\Framework\Stdlib\StringUtils
  116. */
  117. protected $string;
  118. /**
  119. * Entity model parameters
  120. *
  121. * @var array
  122. */
  123. protected $_parameters = [];
  124. /**
  125. * Column names that holds values with particular meaning
  126. *
  127. * @var string[]
  128. */
  129. protected $_specialAttributes = [self::COLUMN_ACTION];
  130. /**
  131. * Permanent entity columns
  132. *
  133. * @var string[]
  134. */
  135. protected $_permanentAttributes = [];
  136. /**
  137. * Number of entities processed by validation
  138. *
  139. * @var int
  140. */
  141. protected $_processedEntitiesCount = 0;
  142. /**
  143. * Number of rows processed by validation
  144. *
  145. * @var int
  146. */
  147. protected $_processedRowsCount = 0;
  148. /**
  149. * Need to log in import history
  150. *
  151. * @var bool
  152. */
  153. protected $logInHistory = true;
  154. /**
  155. * Rows which will be skipped during import
  156. *
  157. * [Row number 1] => true,
  158. * ...
  159. * [Row number N] => true
  160. *
  161. * @var array
  162. */
  163. protected $_skippedRows = [];
  164. /**
  165. * Array of numbers of validated rows as keys and boolean TRUE as values
  166. *
  167. * @var array
  168. */
  169. protected $_validatedRows = [];
  170. /**
  171. * Source model
  172. *
  173. * @var AbstractSource
  174. */
  175. protected $_source;
  176. /**
  177. * Array of unique attributes
  178. *
  179. * @var array
  180. */
  181. protected $_uniqueAttributes = [];
  182. /**
  183. * List of available behaviors
  184. *
  185. * @var string[]
  186. */
  187. protected $_availableBehaviors = [
  188. \Magento\ImportExport\Model\Import::BEHAVIOR_ADD_UPDATE,
  189. \Magento\ImportExport\Model\Import::BEHAVIOR_DELETE,
  190. \Magento\ImportExport\Model\Import::BEHAVIOR_CUSTOM,
  191. ];
  192. /**
  193. * Number of items to fetch from db in one query
  194. *
  195. * @var int
  196. */
  197. protected $_pageSize;
  198. /**
  199. * Maximum size of packet, that can be sent to DB
  200. *
  201. * @var int
  202. */
  203. protected $_maxDataSize;
  204. /**
  205. * Number of items to save to the db in one query
  206. *
  207. * @var int
  208. */
  209. protected $_bunchSize;
  210. /**
  211. * Code of a primary attribute which identifies the entity group if import contains of multiple rows
  212. *
  213. * @var string
  214. */
  215. protected $masterAttributeCode;
  216. /**
  217. * Core store config
  218. *
  219. * @var \Magento\Framework\App\Config\ScopeConfigInterface
  220. */
  221. protected $_scopeConfig;
  222. /**
  223. * Count if created items
  224. *
  225. * @var int
  226. */
  227. protected $countItemsCreated = 0;
  228. /**
  229. * Count if updated items
  230. *
  231. * @var int
  232. */
  233. protected $countItemsUpdated = 0;
  234. /**
  235. * Count if deleted items
  236. *
  237. * @var int
  238. */
  239. protected $countItemsDeleted = 0;
  240. /**
  241. * Json Serializer Instance
  242. *
  243. * @var Json
  244. */
  245. private $serializer;
  246. /**
  247. * @param \Magento\Framework\Stdlib\StringUtils $string
  248. * @param \Magento\Framework\App\Config\ScopeConfigInterface $scopeConfig
  249. * @param \Magento\ImportExport\Model\ImportFactory $importFactory
  250. * @param \Magento\ImportExport\Model\ResourceModel\Helper $resourceHelper
  251. * @param \Magento\Framework\App\ResourceConnection $resource
  252. * @param ProcessingErrorAggregatorInterface $errorAggregator
  253. * @param array $data
  254. * @SuppressWarnings(PHPMD.NPathComplexity)
  255. */
  256. public function __construct(
  257. \Magento\Framework\Stdlib\StringUtils $string,
  258. \Magento\Framework\App\Config\ScopeConfigInterface $scopeConfig,
  259. \Magento\ImportExport\Model\ImportFactory $importFactory,
  260. \Magento\ImportExport\Model\ResourceModel\Helper $resourceHelper,
  261. ResourceConnection $resource,
  262. ProcessingErrorAggregatorInterface $errorAggregator,
  263. array $data = []
  264. ) {
  265. $this->_scopeConfig = $scopeConfig;
  266. $this->_dataSourceModel = isset(
  267. $data['data_source_model']
  268. ) ? $data['data_source_model'] : $importFactory->create()->getDataSourceModel();
  269. $this->_connection =
  270. isset($data['connection']) ?
  271. $data['connection'] :
  272. $resource->getConnection();
  273. $this->string = $string;
  274. $this->_pageSize = isset(
  275. $data['page_size']
  276. ) ? $data['page_size'] : (static::XML_PATH_PAGE_SIZE ? (int)$this->_scopeConfig->getValue(
  277. static::XML_PATH_PAGE_SIZE,
  278. \Magento\Store\Model\ScopeInterface::SCOPE_STORE
  279. ) : 0);
  280. $this->_maxDataSize = isset(
  281. $data['max_data_size']
  282. ) ? $data['max_data_size'] : $resourceHelper->getMaxDataSize();
  283. $this->_bunchSize = isset(
  284. $data['bunch_size']
  285. ) ? $data['bunch_size'] : (static::XML_PATH_BUNCH_SIZE ? (int)$this->_scopeConfig->getValue(
  286. static::XML_PATH_BUNCH_SIZE,
  287. \Magento\Store\Model\ScopeInterface::SCOPE_STORE
  288. ) : 0);
  289. $this->errorAggregator = $errorAggregator;
  290. foreach ($this->errorMessageTemplates as $errorCode => $message) {
  291. $this->getErrorAggregator()->addErrorMessageTemplate($errorCode, $message);
  292. }
  293. }
  294. /**
  295. * @return ProcessingErrorAggregatorInterface
  296. */
  297. public function getErrorAggregator()
  298. {
  299. return $this->errorAggregator;
  300. }
  301. /**
  302. * Import data rows
  303. *
  304. * @abstract
  305. * @return boolean
  306. */
  307. abstract protected function _importData();
  308. /**
  309. * Imported entity type code getter
  310. *
  311. * @abstract
  312. * @return string
  313. */
  314. abstract public function getEntityTypeCode();
  315. /**
  316. * Change row data before saving in DB table
  317. *
  318. * @param array $rowData
  319. * @return array
  320. */
  321. protected function _prepareRowForDb(array $rowData)
  322. {
  323. /**
  324. * Convert all empty strings to null values, as
  325. * a) we don't use empty string in DB
  326. * b) empty strings instead of numeric values will product errors in Sql Server
  327. */
  328. foreach ($rowData as $key => $val) {
  329. if ($val === '') {
  330. $rowData[$key] = null;
  331. }
  332. }
  333. return $rowData;
  334. }
  335. /**
  336. * Add errors to error aggregator
  337. *
  338. * @param string $code
  339. * @param array|mixed $errors
  340. * @return void
  341. */
  342. protected function addErrors($code, $errors)
  343. {
  344. if ($errors) {
  345. $this->getErrorAggregator()->addError(
  346. $code,
  347. ProcessingError::ERROR_LEVEL_CRITICAL,
  348. null,
  349. implode('", "', $errors)
  350. );
  351. }
  352. }
  353. /**
  354. * Validate data rows and save bunches to DB
  355. *
  356. * @return $this
  357. * @SuppressWarnings(PHPMD.CyclomaticComplexity)
  358. * @SuppressWarnings(PHPMD.NPathComplexity)
  359. */
  360. protected function _saveValidatedBunches()
  361. {
  362. $source = $this->getSource();
  363. $bunchRows = [];
  364. $startNewBunch = false;
  365. $source->rewind();
  366. $this->_dataSourceModel->cleanBunches();
  367. $masterAttributeCode = $this->getMasterAttributeCode();
  368. while ($source->valid() || count($bunchRows) || isset($entityGroup)) {
  369. if ($startNewBunch || !$source->valid()) {
  370. /* If the end approached add last validated entity group to the bunch */
  371. if (!$source->valid() && isset($entityGroup)) {
  372. foreach ($entityGroup as $key => $value) {
  373. $bunchRows[$key] = $value;
  374. }
  375. unset($entityGroup);
  376. }
  377. $this->_dataSourceModel->saveBunch($this->getEntityTypeCode(), $this->getBehavior(), $bunchRows);
  378. $bunchRows = [];
  379. $startNewBunch = false;
  380. }
  381. if ($source->valid()) {
  382. $valid = true;
  383. try {
  384. $rowData = $source->current();
  385. foreach ($rowData as $attrName => $element) {
  386. if (!mb_check_encoding($element, 'UTF-8')) {
  387. $valid = false;
  388. $this->addRowError(
  389. AbstractEntity::ERROR_CODE_ILLEGAL_CHARACTERS,
  390. $this->_processedRowsCount,
  391. $attrName
  392. );
  393. }
  394. }
  395. } catch (\InvalidArgumentException $e) {
  396. $valid = false;
  397. $this->addRowError($e->getMessage(), $this->_processedRowsCount);
  398. }
  399. if (!$valid) {
  400. $this->_processedRowsCount++;
  401. $source->next();
  402. continue;
  403. }
  404. if (isset($rowData[$masterAttributeCode]) && trim($rowData[$masterAttributeCode])) {
  405. /* Add entity group that passed validation to bunch */
  406. if (isset($entityGroup)) {
  407. foreach ($entityGroup as $key => $value) {
  408. $bunchRows[$key] = $value;
  409. }
  410. $productDataSize = strlen($this->getSerializer()->serialize($bunchRows));
  411. /* Check if the new bunch should be started */
  412. $isBunchSizeExceeded = ($this->_bunchSize > 0 && count($bunchRows) >= $this->_bunchSize);
  413. $startNewBunch = $productDataSize >= $this->_maxDataSize || $isBunchSizeExceeded;
  414. }
  415. /* And start a new one */
  416. $entityGroup = [];
  417. }
  418. if (isset($entityGroup) && $this->validateRow($rowData, $source->key())) {
  419. /* Add row to entity group */
  420. $entityGroup[$source->key()] = $this->_prepareRowForDb($rowData);
  421. } elseif (isset($entityGroup)) {
  422. /* In case validation of one line of the group fails kill the entire group */
  423. unset($entityGroup);
  424. }
  425. $this->_processedRowsCount++;
  426. $source->next();
  427. }
  428. }
  429. return $this;
  430. }
  431. /**
  432. * Get Serializer instance
  433. *
  434. * Workaround. Only way to implement dependency and not to break inherited child classes
  435. *
  436. * @return Json
  437. * @deprecated 100.2.0
  438. */
  439. private function getSerializer()
  440. {
  441. if (null === $this->serializer) {
  442. $this->serializer = ObjectManager::getInstance()->get(Json::class);
  443. }
  444. return $this->serializer;
  445. }
  446. /**
  447. * Add error with corresponding current data source row number.
  448. *
  449. * @param string $errorCode Error code or simply column name
  450. * @param int $errorRowNum Row number.
  451. * @param string $colName OPTIONAL Column name.
  452. * @param string $errorMessage OPTIONAL Column name.
  453. * @param string $errorLevel
  454. * @param string $errorDescription
  455. * @return $this
  456. */
  457. public function addRowError(
  458. $errorCode,
  459. $errorRowNum,
  460. $colName = null,
  461. $errorMessage = null,
  462. $errorLevel = ProcessingError::ERROR_LEVEL_CRITICAL,
  463. $errorDescription = null
  464. ) {
  465. $errorCode = (string)$errorCode;
  466. $this->getErrorAggregator()->addError(
  467. $errorCode,
  468. $errorLevel,
  469. $errorRowNum,
  470. $colName,
  471. $errorMessage,
  472. $errorDescription
  473. );
  474. return $this;
  475. }
  476. /**
  477. * Add message template for specific error code from outside
  478. *
  479. * @param string $errorCode Error code
  480. * @param string $message Message template
  481. * @return $this
  482. */
  483. public function addMessageTemplate($errorCode, $message)
  484. {
  485. $this->getErrorAggregator()->addErrorMessageTemplate($errorCode, $message);
  486. return $this;
  487. }
  488. /**
  489. * Import behavior getter
  490. *
  491. * @param array $rowData
  492. * @return string
  493. */
  494. public function getBehavior(array $rowData = null)
  495. {
  496. if (isset(
  497. $this->_parameters['behavior']
  498. ) && in_array(
  499. $this->_parameters['behavior'],
  500. $this->_availableBehaviors
  501. )
  502. ) {
  503. $behavior = $this->_parameters['behavior'];
  504. if ($rowData !== null && $behavior == \Magento\ImportExport\Model\Import::BEHAVIOR_CUSTOM) {
  505. // try analyze value in self::COLUMN_CUSTOM column and return behavior for given $rowData
  506. if (array_key_exists(self::COLUMN_ACTION, $rowData)) {
  507. if (strtolower($rowData[self::COLUMN_ACTION]) == self::COLUMN_ACTION_VALUE_DELETE) {
  508. $behavior = \Magento\ImportExport\Model\Import::BEHAVIOR_DELETE;
  509. } else {
  510. // as per task description, if column value is different to self::COLUMN_CUSTOM_VALUE_DELETE,
  511. // we should always use default behavior
  512. return self::getDefaultBehavior();
  513. }
  514. if (in_array($behavior, $this->_availableBehaviors)) {
  515. return $behavior;
  516. }
  517. }
  518. } else {
  519. // if method is invoked without $rowData we should just return $this->_parameters['behavior']
  520. return $behavior;
  521. }
  522. }
  523. return self::getDefaultBehavior();
  524. }
  525. /**
  526. * Get default import behavior
  527. *
  528. * @return string
  529. */
  530. public static function getDefaultBehavior()
  531. {
  532. return \Magento\ImportExport\Model\Import::BEHAVIOR_ADD_UPDATE;
  533. }
  534. /**
  535. * Returns number of checked entities
  536. *
  537. * @return int
  538. */
  539. public function getProcessedEntitiesCount()
  540. {
  541. return $this->_processedEntitiesCount;
  542. }
  543. /**
  544. * Returns number of checked rows
  545. *
  546. * @return int
  547. */
  548. public function getProcessedRowsCount()
  549. {
  550. return $this->_processedRowsCount;
  551. }
  552. /**
  553. * Source object getter
  554. *
  555. * @return AbstractSource
  556. * @throws \Magento\Framework\Exception\LocalizedException
  557. */
  558. public function getSource()
  559. {
  560. if (!$this->_source) {
  561. throw new \Magento\Framework\Exception\LocalizedException(__('The source is not set.'));
  562. }
  563. return $this->_source;
  564. }
  565. /**
  566. * Import process start
  567. *
  568. * @return bool Result of operation
  569. */
  570. public function importData()
  571. {
  572. return $this->_importData();
  573. }
  574. /**
  575. * Is attribute contains particular data (not plain entity attribute)
  576. *
  577. * @param string $attributeCode
  578. * @return bool
  579. */
  580. public function isAttributeParticular($attributeCode)
  581. {
  582. return in_array($attributeCode, $this->_specialAttributes);
  583. }
  584. /**
  585. * @return string the master attribute code to use in an import
  586. */
  587. public function getMasterAttributeCode()
  588. {
  589. return $this->masterAttributeCode;
  590. }
  591. /**
  592. * Check one attribute can be overridden in child
  593. *
  594. * @param string $attributeCode Attribute code
  595. * @param array $attributeParams Attribute params
  596. * @param array $rowData Row data
  597. * @param int $rowNumber
  598. * @param string $multiSeparator
  599. * @return bool
  600. * @SuppressWarnings(PHPMD.CyclomaticComplexity)
  601. */
  602. public function isAttributeValid(
  603. $attributeCode,
  604. array $attributeParams,
  605. array $rowData,
  606. $rowNumber,
  607. $multiSeparator = Import::DEFAULT_GLOBAL_MULTI_VALUE_SEPARATOR
  608. ) {
  609. $message = '';
  610. switch ($attributeParams['type']) {
  611. case 'varchar':
  612. $value = $this->string->cleanString($rowData[$attributeCode]);
  613. $valid = $this->string->strlen($value) < self::DB_MAX_VARCHAR_LENGTH;
  614. $message = self::ERROR_EXCEEDED_MAX_LENGTH;
  615. break;
  616. case 'decimal':
  617. $value = trim($rowData[$attributeCode]);
  618. $valid = (double)$value == $value && is_numeric($value);
  619. $message = self::ERROR_INVALID_ATTRIBUTE_TYPE;
  620. break;
  621. case 'select':
  622. case 'multiselect':
  623. case 'boolean':
  624. $valid = true;
  625. foreach (explode($multiSeparator, mb_strtolower($rowData[$attributeCode])) as $value) {
  626. $valid = isset($attributeParams['options'][$value]);
  627. if (!$valid) {
  628. break;
  629. }
  630. }
  631. $message = self::ERROR_INVALID_ATTRIBUTE_OPTION;
  632. break;
  633. case 'int':
  634. $value = trim($rowData[$attributeCode]);
  635. $valid = (int)$value == $value && is_numeric($value);
  636. $message = self::ERROR_INVALID_ATTRIBUTE_TYPE;
  637. break;
  638. case 'datetime':
  639. $value = trim($rowData[$attributeCode]);
  640. $valid = strtotime($value) !== false;
  641. $message = self::ERROR_INVALID_ATTRIBUTE_TYPE;
  642. break;
  643. case 'text':
  644. $value = $this->string->cleanString($rowData[$attributeCode]);
  645. $valid = $this->string->strlen($value) < self::DB_MAX_TEXT_LENGTH;
  646. $message = self::ERROR_EXCEEDED_MAX_LENGTH;
  647. break;
  648. default:
  649. $valid = true;
  650. break;
  651. }
  652. if (!$valid) {
  653. if ($message == self::ERROR_INVALID_ATTRIBUTE_TYPE) {
  654. $message = sprintf(
  655. $this->errorMessageTemplates[$message],
  656. $attributeCode,
  657. $attributeParams['type']
  658. );
  659. }
  660. $this->addRowError($message, $rowNumber, $attributeCode);
  661. } elseif (!empty($attributeParams['is_unique'])) {
  662. if (isset($this->_uniqueAttributes[$attributeCode][$rowData[$attributeCode]])) {
  663. $this->addRowError(self::ERROR_CODE_DUPLICATE_UNIQUE_ATTRIBUTE, $rowNumber, $attributeCode);
  664. return false;
  665. }
  666. $this->_uniqueAttributes[$attributeCode][$rowData[$attributeCode]] = true;
  667. }
  668. return (bool)$valid;
  669. }
  670. /**
  671. * Import possibility getter
  672. *
  673. * @return bool
  674. */
  675. public function isImportAllowed()
  676. {
  677. return $this->_importAllowed;
  678. }
  679. /**
  680. * Returns TRUE if row is valid and not in skipped rows array
  681. *
  682. * @param array $rowData
  683. * @param int $rowNumber
  684. * @return bool
  685. */
  686. public function isRowAllowedToImport(array $rowData, $rowNumber)
  687. {
  688. return $this->validateRow($rowData, $rowNumber) && !isset($this->_skippedRows[$rowNumber]);
  689. }
  690. /**
  691. * Is import need to log in history.
  692. *
  693. * @return bool
  694. */
  695. public function isNeedToLogInHistory()
  696. {
  697. return $this->logInHistory;
  698. }
  699. /**
  700. * Validate data row
  701. *
  702. * @param array $rowData
  703. * @param int $rowNumber
  704. * @return bool
  705. */
  706. abstract public function validateRow(array $rowData, $rowNumber);
  707. /**
  708. * Set data from outside to change behavior
  709. *
  710. * @param array $parameters
  711. * @return $this
  712. */
  713. public function setParameters(array $parameters)
  714. {
  715. $this->_parameters = $parameters;
  716. return $this;
  717. }
  718. /**
  719. * Source model setter
  720. *
  721. * @param AbstractSource $source
  722. * @return $this
  723. */
  724. public function setSource(AbstractSource $source)
  725. {
  726. $this->_source = $source;
  727. $this->_dataValidated = false;
  728. return $this;
  729. }
  730. /**
  731. * Validate data
  732. *
  733. * @return ProcessingErrorAggregatorInterface
  734. * @throws \Magento\Framework\Exception\LocalizedException
  735. */
  736. public function validateData()
  737. {
  738. if (!$this->_dataValidated) {
  739. $this->getErrorAggregator()->clear();
  740. // do all permanent columns exist?
  741. $absentColumns = array_diff($this->_permanentAttributes, $this->getSource()->getColNames());
  742. $this->addErrors(self::ERROR_CODE_COLUMN_NOT_FOUND, $absentColumns);
  743. // check attribute columns names validity
  744. $columnNumber = 0;
  745. $emptyHeaderColumns = [];
  746. $invalidColumns = [];
  747. $invalidAttributes = [];
  748. foreach ($this->getSource()->getColNames() as $columnName) {
  749. $columnNumber++;
  750. if (!$this->isAttributeParticular($columnName)) {
  751. if (trim($columnName) == '') {
  752. $emptyHeaderColumns[] = $columnNumber;
  753. } elseif (!preg_match('/^[a-z][a-z0-9_]*$/', $columnName)) {
  754. $invalidColumns[] = $columnName;
  755. } elseif ($this->needColumnCheck && !in_array($columnName, $this->getValidColumnNames())) {
  756. $invalidAttributes[] = $columnName;
  757. }
  758. }
  759. }
  760. $this->addErrors(self::ERROR_CODE_INVALID_ATTRIBUTE, $invalidAttributes);
  761. $this->addErrors(self::ERROR_CODE_COLUMN_EMPTY_HEADER, $emptyHeaderColumns);
  762. $this->addErrors(self::ERROR_CODE_COLUMN_NAME_INVALID, $invalidColumns);
  763. if (!$this->getErrorAggregator()->getErrorsCount()) {
  764. $this->_saveValidatedBunches();
  765. $this->_dataValidated = true;
  766. }
  767. }
  768. return $this->getErrorAggregator();
  769. }
  770. /**
  771. * Get count of created items
  772. *
  773. * @return int
  774. */
  775. public function getCreatedItemsCount()
  776. {
  777. return $this->countItemsCreated;
  778. }
  779. /**
  780. * Get count of updated items
  781. *
  782. * @return int
  783. */
  784. public function getUpdatedItemsCount()
  785. {
  786. return $this->countItemsUpdated;
  787. }
  788. /**
  789. * Get count of deleted items
  790. *
  791. * @return int
  792. */
  793. public function getDeletedItemsCount()
  794. {
  795. return $this->countItemsDeleted;
  796. }
  797. /**
  798. * Update proceed items counter
  799. *
  800. * @param array $created
  801. * @param array $updated
  802. * @param array $deleted
  803. * @return $this
  804. */
  805. protected function updateItemsCounterStats(array $created = [], array $updated = [], array $deleted = [])
  806. {
  807. $this->countItemsCreated = count($created);
  808. $this->countItemsUpdated = count($updated);
  809. $this->countItemsDeleted = count($deleted);
  810. return $this;
  811. }
  812. /**
  813. * Retrieve valid column names
  814. *
  815. * @return array
  816. */
  817. public function getValidColumnNames()
  818. {
  819. return $this->validColumnNames;
  820. }
  821. }