| Current Path : /home/rtorresani/www/vendor/magento/module-import-export/Model/Import/Entity/ |
| Current File : //home/rtorresani/www/vendor/magento/module-import-export/Model/Import/Entity/AbstractEntity.php |
<?php
/**
* Copyright © Magento, Inc. All rights reserved.
* See COPYING.txt for license details.
*/
namespace Magento\ImportExport\Model\Import\Entity;
use Magento\Framework\App\ObjectManager;
use Magento\Framework\App\ResourceConnection;
use Magento\Framework\Exception\LocalizedException;
use Magento\Framework\Serialize\Serializer\Json;
use Magento\ImportExport\Model\Import as ImportExport;
use Magento\ImportExport\Model\Import\AbstractSource;
use Magento\ImportExport\Model\Import\EntityInterface;
use Magento\ImportExport\Model\Import\ErrorProcessing\ProcessingError;
use Magento\ImportExport\Model\Import\ErrorProcessing\ProcessingErrorAggregatorInterface;
use Magento\ImportExport\Model\ResourceModel\Import\Data as DataSourceModel;
/**
* Import entity abstract model
*
* phpcs:disable Magento2.Classes.AbstractApi
* @api
*
* @SuppressWarnings(PHPMD.TooManyFields)
* @SuppressWarnings(PHPMD.CouplingBetweenObjects)
* @since 100.0.2
*/
abstract class AbstractEntity implements EntityInterface
{
/**
* Database constants
*/
public const DB_MAX_PACKET_COEFFICIENT = 900000;
public const DB_MAX_PACKET_DATA = 1048576;
public const DB_MAX_VARCHAR_LENGTH = 256;
public const DB_MAX_TEXT_LENGTH = 65536;
public const ERROR_CODE_SYSTEM_EXCEPTION = 'systemException';
public const ERROR_CODE_COLUMN_NOT_FOUND = 'columnNotFound';
public const ERROR_CODE_COLUMN_EMPTY_HEADER = 'columnEmptyHeader';
public const ERROR_CODE_COLUMN_NAME_INVALID = 'columnNameInvalid';
public const ERROR_CODE_ATTRIBUTE_NOT_VALID = 'attributeNotInvalid';
public const ERROR_CODE_DUPLICATE_UNIQUE_ATTRIBUTE = 'duplicateUniqueAttribute';
public const ERROR_CODE_ILLEGAL_CHARACTERS = 'illegalCharacters';
public const ERROR_CODE_INVALID_ATTRIBUTE = 'invalidAttributeName';
public const ERROR_CODE_WRONG_QUOTES = 'wrongQuotes';
public const ERROR_CODE_COLUMNS_NUMBER = 'wrongColumnsNumber';
public const ERROR_CODE_CATEGORY_NOT_VALID = 'categoryNotValid';
/**
* @var array
*/
protected $errorMessageTemplates = [
self::ERROR_CODE_SYSTEM_EXCEPTION => 'General system exception happened',
self::ERROR_CODE_COLUMN_NOT_FOUND => 'We can\'t find required columns: %s.',
self::ERROR_CODE_COLUMN_EMPTY_HEADER => 'Columns number: "%s" have empty headers',
self::ERROR_CODE_COLUMN_NAME_INVALID => 'Column names: "%s" are invalid',
self::ERROR_CODE_ATTRIBUTE_NOT_VALID => "Please correct the value for '%s'.",
self::ERROR_CODE_DUPLICATE_UNIQUE_ATTRIBUTE => "Duplicate Unique Attribute for '%s'",
self::ERROR_CODE_ILLEGAL_CHARACTERS => "Illegal character used for attribute %s",
self::ERROR_CODE_INVALID_ATTRIBUTE => 'Header contains invalid attribute(s): "%s"',
self::ERROR_CODE_WRONG_QUOTES => "Curly quotes used instead of straight quotes",
self::ERROR_CODE_COLUMNS_NUMBER => "Number of columns does not correspond to the number of rows in the header",
];
/**
* Validation failure message template definitions
*
* @var array
*/
protected $_messageTemplates = [];
/**
* DB connection.
*
* @var \Magento\Framework\DB\Adapter\AdapterInterface
*/
protected $_connection;
/**
* Has data process validation done?8
*
* @var bool
*/
protected $_dataValidated = false;
/**
* @var array
*/
protected $validColumnNames = [];
/**
* If we should check column names
*
* @var bool
*/
protected $needColumnCheck = false;
/**
* DB data source model.
*
* @var DataSourceModel
*/
protected $_dataSourceModel;
/**
* @var int
*/
protected $_entityTypeId;
/**
* Error codes with arrays of corresponding row numbers.
*
* @var array
*/
protected $_errors = [];
/**
* Flag to disable import.
*
* @var bool
*/
protected $_importAllowed = true;
/**
* Attributes with index (not label) value.
*
* @var array
*/
protected $_indexValueAttributes = [];
/**
* Entity model parameters.
*
* @var array
*/
protected $_parameters = [];
/**
* Column names that holds values with particular meaning.
*
* @var string[]
*/
protected $_specialAttributes = [];
/**
* Permanent entity columns.
*
* @var string[]
*/
protected $_permanentAttributes = [];
/**
* Number of entities processed by validation.
*
* @var int
*/
protected $_processedEntitiesCount = 0;
/**
* Number of rows processed by validation.
*
* @var int
*/
protected $_processedRowsCount = 0;
/**
* Array of numbers of validated rows as keys and boolean TRUE as values.
*
* @var array
*/
protected $_validatedRows = [];
/**
* Source model.
*
* @var AbstractSource
*/
protected $_source;
/**
* Array of unique attributes
*
* @var array
*/
protected $_uniqueAttributes = [];
/**
* @var \Magento\ImportExport\Helper\Data
*/
protected $_importExportData;
/**
* @var \Magento\Framework\Json\Helper\Data
*/
protected $jsonHelper;
/**
* Magento string lib
*
* @var \Magento\Framework\Stdlib\StringUtils
*/
protected $string;
/**
* @var \Magento\ImportExport\Model\ResourceModel\Helper
*/
protected $_resourceHelper;
/**
* Count if created items
*
* @var int
*/
protected $countItemsCreated = 0;
/**
* Count if updated items
*
* @var int
*/
protected $countItemsUpdated = 0;
/**
* Count if deleted items
*
* @var int
*/
protected $countItemsDeleted = 0;
/**
* Need to log in import history
*
* @var bool
*/
protected $logInHistory = false;
/**
* @var ProcessingErrorAggregatorInterface
*/
protected $errorAggregator;
/**
* Product metadata pool
*
* @var \Magento\Framework\EntityManager\MetadataPool
* @since 100.1.0
*/
protected $metadataPool;
/**
* Json Serializer Instance
*
* @var Json
*/
private $serializer;
/**
* Ids of saved data in DB
*
* @var array
*/
private array $ids = [];
/**
* @param \Magento\Framework\Json\Helper\Data $jsonHelper
* @param \Magento\ImportExport\Helper\Data $importExportData
* @param \Magento\ImportExport\Model\ResourceModel\Import\Data $importData
* @param \Magento\Eav\Model\Config $config
* @param ResourceConnection $resource
* @param \Magento\ImportExport\Model\ResourceModel\Helper $resourceHelper
* @param \Magento\Framework\Stdlib\StringUtils $string
* @param ProcessingErrorAggregatorInterface $errorAggregator
* @throws \Magento\Framework\Exception\LocalizedException
*/
public function __construct(
\Magento\Framework\Json\Helper\Data $jsonHelper,
\Magento\ImportExport\Helper\Data $importExportData,
\Magento\ImportExport\Model\ResourceModel\Import\Data $importData,
\Magento\Eav\Model\Config $config,
ResourceConnection $resource,
\Magento\ImportExport\Model\ResourceModel\Helper $resourceHelper,
\Magento\Framework\Stdlib\StringUtils $string,
ProcessingErrorAggregatorInterface $errorAggregator
) {
$this->jsonHelper = $jsonHelper;
$this->_importExportData = $importExportData;
$this->_resourceHelper = $resourceHelper;
$this->string = $string;
$this->errorAggregator = $errorAggregator;
foreach ($this->errorMessageTemplates as $errorCode => $message) {
$this->getErrorAggregator()->addErrorMessageTemplate($errorCode, $message);
}
$entityType = $config->getEntityType($this->getEntityTypeCode());
$this->_entityTypeId = $entityType->getEntityTypeId();
$this->_dataSourceModel = $importData;
$this->_connection = $resource->getConnection();
}
/**
* Inner source object getter.
*
* @return AbstractSource
* @throws \Magento\Framework\Exception\LocalizedException
*/
protected function _getSource()
{
if (!$this->_source) {
throw new LocalizedException(__('Please specify a source.'));
}
return $this->_source;
}
/**
* Import data rows.
*
* @abstract
* @return boolean
*/
abstract protected function _importData();
/**
* Returns boolean TRUE if row scope is default (fundamental) scope.
*
* @param array $rowData
* @return bool
* @SuppressWarnings(PHPMD.UnusedFormalParameter)
*/
protected function _isRowScopeDefault(array $rowData)
{
return true;
}
/**
* Change row data before saving in DB table.
*
* @param array $rowData
* @return array
*/
protected function _prepareRowForDb(array $rowData)
{
/**
* Convert all empty strings to null values, as
* a) we don't use empty string in DB
* b) empty strings instead of numeric values will product errors in Sql Server
*/
foreach ($rowData as $key => $val) {
if ($val === '') {
$rowData[$key] = null;
}
}
return $rowData;
}
/**
* Add errors to error aggregator
*
* @param string $code
* @param array|mixed $errors
* @return void
*/
protected function addErrors($code, $errors)
{
if ($errors) {
$this->getErrorAggregator()->addError(
$code,
ProcessingError::ERROR_LEVEL_CRITICAL,
null,
implode('", "', $errors)
);
}
}
/**
* Validate data rows and save bunches to DB.
*
* @return $this
* @SuppressWarnings(PHPMD.CyclomaticComplexity)
*/
protected function _saveValidatedBunches()
{
$source = $this->_getSource();
$currentDataSize = 0;
$bunchRows = [];
$startNewBunch = false;
$nextRowBackup = [];
$maxDataSize = $this->_resourceHelper->getMaxDataSize();
$bunchSize = $this->_importExportData->getBunchSize();
$skuSet = [];
$source->rewind();
$this->_dataSourceModel->cleanProcessedBunches();
while ($source->valid() || $bunchRows) {
if ($startNewBunch || !$source->valid()) {
$this->ids[] =
$this->_dataSourceModel->saveBunch($this->getEntityTypeCode(), $this->getBehavior(), $bunchRows);
$bunchRows = $nextRowBackup;
$currentDataSize = strlen($this->getSerializer()->serialize($bunchRows));
$startNewBunch = false;
$nextRowBackup = [];
}
if ($source->valid()) {
try {
$rowData = $source->current();
if (array_key_exists('sku', $rowData)) {
$skuSet[$rowData['sku']] = true;
}
} catch (\InvalidArgumentException $e) {
$this->addRowError($e->getMessage(), $this->_processedRowsCount);
$this->_processedRowsCount++;
$source->next();
continue;
}
$this->_processedRowsCount++;
if ($this->validateRow($rowData, $source->key())) {
// add row to bunch for save
$rowData = $this->_prepareRowForDb($rowData);
$rowSize = strlen($this->jsonHelper->jsonEncode($rowData) ?? '');
$isBunchSizeExceeded = $bunchSize > 0 && count($bunchRows) >= $bunchSize;
if ($currentDataSize + $rowSize >= $maxDataSize || $isBunchSizeExceeded) {
$startNewBunch = true;
$nextRowBackup = [$source->key() => $rowData];
} else {
$bunchRows[$source->key()] = $rowData;
$currentDataSize += $rowSize;
}
}
$source->next();
}
}
$this->_processedEntitiesCount = (count($skuSet)) ?: $this->_processedRowsCount;
return $this;
}
/**
* Get Serializer instance
*
* Workaround. Only way to implement dependency and not to break inherited child classes
*
* @return Json
*/
private function getSerializer()
{
if (null === $this->serializer) {
$this->serializer = ObjectManager::getInstance()->get(Json::class);
}
return $this->serializer;
}
/**
* Add error with corresponding current data source row number.
*
* @param string $errorCode Error code or simply column name
* @param int $errorRowNum Row number.
* @param string $colName OPTIONAL Column name.
* @param string $errorMessage OPTIONAL Column name.
* @param string $errorLevel
* @param string $errorDescription
* @return $this
*/
public function addRowError(
$errorCode,
$errorRowNum,
$colName = null,
$errorMessage = null,
$errorLevel = ProcessingError::ERROR_LEVEL_CRITICAL,
$errorDescription = null
) {
$errorCode = (string)$errorCode;
$this->getErrorAggregator()->addError(
$errorCode,
$errorLevel,
$errorRowNum,
$colName,
$errorMessage,
$errorDescription
);
return $this;
}
/**
* Add message template for specific error code from outside.
*
* @param string $errorCode Error code
* @param string $message Message template
* @return $this
*/
public function addMessageTemplate($errorCode, $message)
{
$this->getErrorAggregator()->addErrorMessageTemplate($errorCode, $message);
return $this;
}
/**
* Returns attributes all values in label-value or value-value pairs form. Labels are lower-cased.
*
* @param \Magento\Eav\Model\Entity\Attribute\AbstractAttribute $attribute
* @param array $indexValAttrs OPTIONAL Additional attributes' codes with index values.
* @return array
*/
public function getAttributeOptions(
\Magento\Eav\Model\Entity\Attribute\AbstractAttribute $attribute,
$indexValAttrs = []
) {
$options = [];
if ($attribute->usesSource()) {
// merge global entity index value attributes
$indexValAttrs = array_merge($indexValAttrs, $this->_indexValueAttributes);
// should attribute have index (option value) instead of a label?
$index = in_array($attribute->getAttributeCode(), $indexValAttrs) ? 'value' : 'label';
// only default (admin) store values used
$attribute->setStoreId(\Magento\Store\Model\Store::DEFAULT_STORE_ID);
try {
foreach ($attribute->getSource()->getAllOptions(false) as $option) {
$value = is_array($option['value']) ? $option['value'] : [$option];
foreach ($value as $innerOption) {
if (strlen($innerOption['value'] ?? '')) {
// skip ' -- Please Select -- ' option
$options[strtolower($innerOption[$index] ?? '')] = $innerOption['value'];
}
}
}
// phpcs:disable Magento2.CodeAnalysis.EmptyBlock.DetectedCatch
} catch (\Exception $e) {
// ignore exceptions connected with source models
}
}
return $options;
}
/**
* Import behavior getter.
*
* @return string
*/
public function getBehavior()
{
if (!isset(
$this->_parameters['behavior']
) ||
$this->_parameters['behavior'] != ImportExport::BEHAVIOR_APPEND &&
$this->_parameters['behavior'] != ImportExport::BEHAVIOR_ADD_UPDATE &&
$this->_parameters['behavior'] != ImportExport::BEHAVIOR_REPLACE &&
$this->_parameters['behavior'] != ImportExport::BEHAVIOR_CUSTOM &&
$this->_parameters['behavior'] != ImportExport::BEHAVIOR_DELETE
) {
return ImportExport::getDefaultBehavior();
}
return $this->_parameters['behavior'];
}
/**
* EAV entity type code getter.
*
* @abstract
* @return string
*/
abstract public function getEntityTypeCode();
/**
* Entity type ID getter.
*
* @return int
*/
public function getEntityTypeId()
{
return $this->_entityTypeId;
}
/**
* Returns number of checked entities.
*
* @return int
*/
public function getProcessedEntitiesCount()
{
return $this->_processedEntitiesCount;
}
/**
* Returns number of checked rows.
*
* @return int
*/
public function getProcessedRowsCount()
{
return $this->_processedRowsCount;
}
/**
* Source object getter.
*
* @return AbstractSource
* @throws \Magento\Framework\Exception\LocalizedException
*/
public function getSource()
{
if (!$this->_source) {
throw new LocalizedException(__('The source is not set.'));
}
return $this->_source;
}
/**
* Import process start.
*
* @return bool Result of operation.
*/
public function importData()
{
return $this->_importData();
}
/**
* Is attribute contains particular data (not plain entity attribute).
*
* @param string $attrCode
* @return bool
*/
public function isAttributeParticular($attrCode)
{
return in_array($attrCode, $this->_specialAttributes);
}
/**
* Check one attribute. Can be overridden in child.
*
* @param string $attrCode Attribute code
* @param array $attrParams Attribute params
* @param array $rowData Row data
* @param int $rowNum
* @return boolean
* @SuppressWarnings(PHPMD.CyclomaticComplexity)
*/
public function isAttributeValid($attrCode, array $attrParams, array $rowData, $rowNum)
{
switch ($attrParams['type']) {
case 'varchar':
$val = $this->string->cleanString($rowData[$attrCode]);
$valid = $this->string->strlen($val) < self::DB_MAX_VARCHAR_LENGTH;
break;
case 'decimal':
$val = trim($rowData[$attrCode] ?? '');
$valid = (double)$val == $val;
break;
case 'select':
case 'multiselect':
$valid = isset($attrParams['options'][strtolower($rowData[$attrCode] ?? '')]);
break;
case 'int':
$val = trim($rowData[$attrCode] ?? '');
$valid = (int)$val == $val;
break;
case 'datetime':
$val = trim($rowData[$attrCode] ?? '');
$valid = strtotime($val) !== false;
break;
case 'text':
$val = $this->string->cleanString($rowData[$attrCode]);
$valid = $this->string->strlen($val) < self::DB_MAX_TEXT_LENGTH;
break;
default:
$valid = true;
break;
}
if (!$valid) {
$this->addRowError(self::ERROR_CODE_ATTRIBUTE_NOT_VALID, $rowNum, $attrCode);
} elseif (!empty($attrParams['is_unique'])) {
if (isset($this->_uniqueAttributes[$attrCode][$rowData[$attrCode]])) {
$this->addRowError(self::ERROR_CODE_DUPLICATE_UNIQUE_ATTRIBUTE, $rowNum, $attrCode);
return false;
}
$this->_uniqueAttributes[$attrCode][$rowData[$attrCode]] = true;
}
return (bool)$valid;
}
/**
* Import possibility getter.
*
* @return bool
*/
public function isImportAllowed()
{
return $this->_importAllowed;
}
/**
* Returns TRUE if row is valid and not in skipped rows array.
*
* @param array $rowData
* @param int $rowNum
* @return bool
*/
public function isRowAllowedToImport(array $rowData, $rowNum)
{
$this->validateRow($rowData, $rowNum);
return !$this->getErrorAggregator()->isRowInvalid($rowNum);
}
/**
* Retrieve message template
*
* @param string $errorCode
* @return null|string
*/
public function retrieveMessageTemplate($errorCode)
{
if (isset($this->_messageTemplates[$errorCode])) {
return $this->_messageTemplates[$errorCode];
}
return null;
}
/**
* Is import need to log in history.
*
* @return bool
*/
public function isNeedToLogInHistory()
{
return $this->logInHistory;
}
/**
* Validate data row.
*
* @param array $rowData
* @param int $rowNum
* @return boolean
*/
abstract public function validateRow(array $rowData, $rowNum);
/**
* Set data from outside to change behavior. I.e. for setting some default parameters etc.
*
* @param array $params
* @return $this
*/
public function setParameters(array $params)
{
$this->_parameters = $params;
return $this;
}
/**
* Get data from outside to change behavior. I.e. for setting some default parameters etc.
*
* @return array $params
*/
public function getParameters()
{
return $this->_parameters;
}
/**
* Source model setter.
*
* @param AbstractSource $source
* @return $this
*/
public function setSource(AbstractSource $source)
{
$this->_source = $source;
$this->_dataValidated = false;
return $this;
}
/**
* Validate data.
*
* @return ProcessingErrorAggregatorInterface
* @throws \Magento\Framework\Exception\LocalizedException
* @SuppressWarnings(PHPMD.CyclomaticComplexity)
*/
public function validateData()
{
if (!$this->_dataValidated) {
$this->getErrorAggregator()->clear();
// do all permanent columns exist?
$absentColumns = array_diff($this->_permanentAttributes, $this->getSource()->getColNames());
$this->addErrors(self::ERROR_CODE_COLUMN_NOT_FOUND, $absentColumns);
if (ImportExport::BEHAVIOR_DELETE != $this->getBehavior()) {
// check attribute columns names validity
$columnNumber = 0;
$emptyHeaderColumns = [];
$invalidColumns = [];
$invalidAttributes = [];
foreach ($this->getSource()->getColNames() as $columnName) {
$columnNumber++;
if (!$this->isAttributeParticular($columnName)) {
if (trim($columnName ?? '') == '') {
$emptyHeaderColumns[] = $columnNumber;
} elseif (!$columnName || !preg_match('/^[a-z][a-z0-9_]*$/', $columnName)) {
$invalidColumns[] = $columnName;
} elseif ($this->needColumnCheck && !in_array($columnName, $this->getValidColumnNames())) {
$invalidAttributes[] = $columnName;
}
}
}
$this->addErrors(self::ERROR_CODE_INVALID_ATTRIBUTE, $invalidAttributes);
$this->addErrors(self::ERROR_CODE_COLUMN_EMPTY_HEADER, $emptyHeaderColumns);
$this->addErrors(self::ERROR_CODE_COLUMN_NAME_INVALID, $invalidColumns);
}
if (!$this->getErrorAggregator()->getErrorsCount()) {
$this->_saveValidatedBunches();
$this->_dataValidated = true;
}
}
return $this->getErrorAggregator();
}
/**
* Get error aggregator object
*
* @return ProcessingErrorAggregatorInterface
*/
public function getErrorAggregator()
{
return $this->errorAggregator;
}
/**
* Get count of created items
*
* @return int
*/
public function getCreatedItemsCount()
{
return $this->countItemsCreated;
}
/**
* Get count of updated items
*
* @return int
*/
public function getUpdatedItemsCount()
{
return $this->countItemsUpdated;
}
/**
* Get count of deleted items
*
* @return int
*/
public function getDeletedItemsCount()
{
return $this->countItemsDeleted;
}
/**
* Retrieve valid column names
*
* @return array
*/
public function getValidColumnNames()
{
return $this->validColumnNames;
}
/**
* Get product metadata pool
*
* @return \Magento\Framework\EntityManager\MetadataPool
* @since 100.1.0
*/
protected function getMetadataPool()
{
if (!$this->metadataPool) {
$this->metadataPool = ObjectManager::getInstance()
->get(\Magento\Framework\EntityManager\MetadataPool::class);
}
return $this->metadataPool;
}
/**
* Retrieve Ids of Validated Rows
*
* @return array
*/
public function getIds() : array
{
return $this->ids;
}
/**
* Set Ids of Validated Rows
*
* @param array $ids
* @return void
*/
public function setIds(array $ids)
{
$this->ids = $ids;
}
/**
* Gets the currently used DataSourceModel
*
* @return DataSourceModel
*/
public function getDataSourceModel() : DataSourceModel
{
return $this->_dataSourceModel;
}
}