diff --git a/Category/Model/Factory/Import.php b/Category/Model/Factory/Import.php index 5c2fa17..918f6ce 100644 --- a/Category/Model/Factory/Import.php +++ b/Category/Model/Factory/Import.php @@ -5,6 +5,8 @@ use \Pimgento\Import\Model\Factory; use \Pimgento\Entities\Model\Entities; use \Pimgento\Import\Helper\Config as helperConfig; +use \Pimgento\Staging\Helper\Config as StagingConfigHelper; +use \Pimgento\Staging\Helper\Import as StagingHelper; use \Pimgento\Import\Helper\UrlRewrite as urlRewriteHelper; use \Magento\Framework\Event\ManagerInterface; use \Magento\Catalog\Model\Category; @@ -38,6 +40,16 @@ class Import extends Factory */ protected $_urlRewriteHelper; + /** + * @var StagingHelper + */ + protected $stagingHelper; + + /** + * @var StagingConfigHelper + */ + protected $stagingConfigHelper; + /** * @param \Pimgento\Entities\Model\Entities $entities * @param \Pimgento\Import\Helper\Config $helperConfig @@ -47,6 +59,8 @@ class Import extends Factory * @param \Magento\Catalog\Model\Category $category * @param \Magento\Framework\App\Cache\TypeListInterface $cacheTypeList * @param urlRewriteHelper $urlRewriteHelper + * @param StagingConfigHelper $stagingConfigHelper + * @param StagingHelper $stagingHelper * @param array $data */ public function __construct( @@ -58,6 +72,8 @@ public function __construct( Category $category, TypeListInterface $cacheTypeList, urlRewriteHelper $urlRewriteHelper, + StagingConfigHelper $stagingConfigHelper, + StagingHelper $stagingHelper, array $data = [] ) { @@ -66,6 +82,8 @@ public function __construct( $this->_category = $category; $this->_cacheTypeList = $cacheTypeList; $this->_urlRewriteHelper = $urlRewriteHelper; + $this->stagingConfigHelper = $stagingConfigHelper; + $this->stagingHelper = $stagingHelper; } /** @@ -84,6 +102,17 @@ public function createTable() } } + /** + * Add required columns + */ + public function addRequiredData() + { + $connection = $this->_entities->getResource()->getConnection(); + $tmpTable = $this->_entities->getTableName($this->getCode()); + + $this->stagingHelper->addRequiredData($connection, $tmpTable); + } + /** * Insert data into temporary table */ @@ -103,7 +132,26 @@ public function insertData() */ public function matchEntity() { - $this->_entities->matchEntity($this->getCode(), 'code', 'catalog_category_entity', 'entity_id'); + if ($this->stagingConfigHelper->isCatalogStagingModulesEnabled()) { + /** + * When using staging module entity id's are not the primary key of the catalog_product_entity + * table anymore. The new primary keys is row_id. Before we get information on the row_id, we still + * need to get the entiy_id of the products to be imported. We are therefore going to use a different + * table built for this purpose in magento. + */ + $this->_entities->matchEntity($this->getCode(), 'code', 'sequence_catalog_category', 'sequence_value'); + + // Once the entitie id's are matched we can match the row ids. + $this->stagingHelper->matchEntityRows( + $this->_entities, + 'catalog_category_entity', + $this->getCode(), + StagingConfigHelper::STAGING_MODE_LAST + ); + + } else { + $this->_entities->matchEntity($this->getCode(), 'code', 'catalog_category_entity', 'entity_id'); + } } /** @@ -281,29 +329,37 @@ public function createEntities() 'children_count' => new Expr('0'), ); - $columnIdentifier = $this->_entities->getColumnIdentifier($table); - - if ($columnIdentifier == 'row_id') { - $values['row_id'] = '_entity_id'; + if ($this->stagingConfigHelper->isCatalogStagingModulesEnabled()) { + $values['created_in'] = new Expr(1); + $values['updated_in'] = new Expr(VersionManager::MAX_VERSION); + $values['row_id'] = '_row_id'; } + $this->stagingHelper->createEntitiesBefore($connection, 'sequence_catalog_category', $tmpTable); + $parents = $connection->select()->from($tmpTable, $values); $connection->query( $connection->insertFromSelect( - $parents, $table, array_keys($values), 1 + $parents, + $table, + array_keys($values), + 1 ) ); + $this->stagingHelper->createEntitiesAfter( + $connection, + 'catalog_category_entity', + $tmpTable, + StagingConfigHelper::STAGING_MODE_LAST + ); + $values = array( 'created_at' => new Expr('now()') ); $connection->update($table, $values, 'created_at IS NULL'); - if ($columnIdentifier == 'row_id') { - $values = [ - 'created_in' => new Expr(1), - 'updated_in' => new Expr(VersionManager::MAX_VERSION), - ]; + if ($this->stagingConfigHelper->isCatalogStagingModulesEnabled()) { $connection->update($table, $values, 'created_in = 0 AND updated_in = 0'); } } @@ -342,7 +398,8 @@ public function setValues() $resource->getTable('catalog_category_entity'), $values, 3, - $store['store_id'] + $store['store_id'], + 1 ); } } diff --git a/Category/Observer/AddPimgentoImportObserver.php b/Category/Observer/AddPimgentoImportObserver.php index 7a36691..0ecff46 100644 --- a/Category/Observer/AddPimgentoImportObserver.php +++ b/Category/Observer/AddPimgentoImportObserver.php @@ -59,6 +59,10 @@ protected function getStepsDefinition() 'comment' => __('Create temporary table'), 'method' => 'createTable', ), + array( + 'comment' => __('Add product required data'), + 'method' => 'addRequiredData', + ), array( 'comment' => __('Fill temporary table'), 'method' => 'insertData', diff --git a/Category/etc/module.xml b/Category/etc/module.xml index 39ebfa9..ae02075 100644 --- a/Category/etc/module.xml +++ b/Category/etc/module.xml @@ -4,6 +4,7 @@ + \ No newline at end of file diff --git a/Entities/Model/Entities.php b/Entities/Model/Entities.php index c3c84a9..055cf7c 100644 --- a/Entities/Model/Entities.php +++ b/Entities/Model/Entities.php @@ -183,7 +183,14 @@ public function matchEntity($tableSuffix, $pimKey, $entityTable, $entityKey, $pr public function setValues($tableSuffix, $entityTable, $values, $entityTypeId, $storeId, $mode = 1) { $this->_getResource() - ->setValues($this->getTableName($tableSuffix), $entityTable, $values, $entityTypeId, $storeId, $mode); + ->setValues( + $this->getTableName($tableSuffix), + $entityTable, + $values, + $entityTypeId, + $storeId, + $mode + ); return $this; } diff --git a/Entities/Model/ResourceModel/Entities.php b/Entities/Model/ResourceModel/Entities.php index 8afcf6d..85864d1 100644 --- a/Entities/Model/ResourceModel/Entities.php +++ b/Entities/Model/ResourceModel/Entities.php @@ -375,7 +375,7 @@ public function matchEntity($tableName, $pimKey, $entityTable, $entityKey, $impo public function setValues($tableName, $entityTable, $values, $entityTypeId, $storeId, $mode = 1) { $connection = $this->getConnection(); - + foreach ($values as $code => $value) { if (($attribute = $this->getAttribute($code, $entityTypeId))) { if ($attribute['backend_type'] !== 'static') { @@ -396,13 +396,13 @@ public function setValues($tableName, $entityTable, $values, $entityTypeId, $sto array( 'attribute_id' => new Expr($attribute['attribute_id']), 'store_id' => new Expr($storeId), - $identifier => '_entity_id', + $identifier => '_' . $identifier, 'value' => $value, ) ); if ($columnExists) { - $select->where('`' . $columnName . '` <> ?', self::IGNORE_VALUE); + $select->where('TRIM(`' . $columnName . '`) <> ?', self::IGNORE_VALUE); } $insert = $connection->insertFromSelect( @@ -421,7 +421,9 @@ public function setValues($tableName, $entityTable, $values, $entityTypeId, $sto 'value = ?' => '0000-00-00 00:00:00' ); $connection->update( - $this->getTable($entityTable . '_' . $backendType), $values, $where + $this->getTable($entityTable . '_' . $backendType), + $values, + $where ); } } @@ -431,6 +433,53 @@ public function setValues($tableName, $entityTable, $values, $entityTypeId, $sto return $this; } + /** + * Update the values for an entity in all it's stages. + * + * @param $tableName + * @param $entityTable + * @param $entityTypeId + * @param $attributeCode + */ + public function updateAllStageValues($tableName, $entityTable, $entityTypeId, $attributeCode, $joinCondition = 't._row_id != e.row_id') + { + $connection = $this->getConnection(); + + if (($attribute = $this->getAttribute($attributeCode, $entityTypeId))) { + if ($attribute['backend_type'] !== 'static') { + $backendType = $attribute['backend_type']; + $attributeTable = $connection->getTableName($entityTable . '_' . $backendType); + + $select = $connection->select() + ->from( + ['e' =>$entityTable], + [] + )->joinInner( + ['t' => $tableName], + "t._entity_id = e.entity_id AND $joinCondition", + [] + )->joinInner( + ['u' => $attributeTable], + 'u.row_id = t._row_id AND attribute_id = ' . $attribute['attribute_id'], + [] + ); + + $select->columns(['u.attribute_id', 'u.store_id', 'e.row_id', 'u.value']); + + $select->setPart('disable_staging_preview', true); + + $insert = $connection->insertFromSelect( + $select, + $attributeTable, + array('attribute_id', 'store_id', 'row_id', 'value'), + 1 + ); + + $connection->query($insert); + } + } + } + /** * Copy column to an other * diff --git a/Product/Helper/Config.php b/Product/Helper/Config.php index 0625a29..463cd67 100644 --- a/Product/Helper/Config.php +++ b/Product/Helper/Config.php @@ -5,26 +5,37 @@ use \Magento\Framework\App\Helper\AbstractHelper; use \Magento\Framework\App\Helper\Context; use \Magento\Store\Model\StoreManagerInterface; -use \Magento\Framework\Filesystem; +use \Pimgento\Staging\Helper\Config as StagingConfigHelper; class Config extends AbstractHelper { + /** + * Constants to configuration profile. + */ + const CONFIG_PROFILE = 'product'; /** * @var \Magento\Store\Model\StoreManagerInterface */ protected $_storeManager; + /** + * @var \Pimgento\Import\Helper\Config + */ + protected $stagingConfigHelper; + /** * @param \Magento\Framework\App\Helper\Context $context * @param StoreManagerInterface $storeManager */ public function __construct( Context $context, - StoreManagerInterface $storeManager - ) - { + StoreManagerInterface $storeManager, + StagingConfigHelper $statingConfigHelper + ) { $this->_storeManager = $storeManager; + $this->stagingConfigHelper = $statingConfigHelper; + parent::__construct($context); } @@ -72,4 +83,24 @@ public function getDefaultWebsiteId() return $this->_storeManager->getStore()->getWebsiteId(); } + /** + * Get import staging mode to use. + * + * @return mixed|string + */ + public function getImportStagingMode() + { + + return $this->stagingConfigHelper->getImportStagingMode(self::CONFIG_PROFILE); + } + + /** + * Check if current configuration asks import to be in full staging mode or not. + * + * @return bool + */ + public function isImportInFullStagingMode() + { + return $this->getImportStagingMode() == StagingConfigHelper::STAGING_MODE_FULL; + } } \ No newline at end of file diff --git a/Product/Helper/Staging.php b/Product/Helper/Staging.php new file mode 100644 index 0000000..9a42176 --- /dev/null +++ b/Product/Helper/Staging.php @@ -0,0 +1,321 @@ +stagingConfigHelper = $statingConfigHelper; + $this->stagingHelper = $stagingHelper; + + parent::__construct($context); + } + + /** + * Updates the created & updated in dates of configurable products with the ones of the simple products. + * + * @param Entities $entities + * @param $tmpTable + * @param $entityTableCode + * @param $code + * @param $stagingMode + */ + public function updateConfigurableStages(Entities $entities, $tmpTable, $entityTableCode, $code, $stagingMode) + { + $connection = $entities->getResource()->getConnection(); + + $query = " + UPDATE $tmpTable tc, $tmpTable ts + SET tc.created_in = ts.created_in, tc.updated_in = ts.updated_in + WHERE tc._first_children = ts.sku + "; + $connection->query($query); + } + + /** + * Duplicate values into multiple stages. This is necessary for the all mode when all stages are updated with + * the same values & also to duplicate the values properly for the initial stage that might be in multiple pieces. + * + * @param Entities $entities + * @param string $tmpTable + * @param string $condition + * @param string $dataTable + */ + public function updateAllStageValues( + Entities $entities, + $tmpTable, + $condition = 't._row_id != e.row_id', + $dataTable = null + ) { + if (is_null($dataTable)) { + $dataTable = $tmpTable; + } + + $connection = $entities->getResource()->getConnection(); + + $columns = array_keys($connection->describeTable($dataTable)); + $column[] = 'options_container'; + $column[] = 'tax_class_id'; + $column[] = 'visibility'; + + $except = array( + '_entity_id', + '_is_new', + '_status', + '_type_id', + '_options_container', + '_tax_class_id', + '_attribute_set_id', + '_visibility', + '_children', + '_first_children', + '_axis', + 'sku', + 'categories', + 'family', + 'groups', + 'enabled', + 'created_in', + 'updated_in', + ); + + if ($connection->tableColumnExists($tmpTable, 'enabled')) { + $column[] = 'status'; + } + + foreach ($columns as $column) { + if (in_array($column, $except)) { + continue; + } + + if (preg_match('/-unit/', $column)) { + continue; + } + + $columnPrefix = explode('-', $column); + $columnPrefix = reset($columnPrefix); + + $entities + ->getResource() + ->updateAllStageValues ( + $tmpTable, + $connection->getTableName('catalog_product_entity'), + 4, + $columnPrefix, + $condition + ); + } + } + + /** + * Duplicate relations between products for all stages. + * + * @see updateAllStageValues for more information on why it's used. + * + * @param Entities $entities + * @param string $tmpTable + * @param string $joinCondition + */ + public function updateAllStageRelations(Entities $entities, $tmpTable, $joinCondition = 't._row_id != e.row_id') + { + $connection = $entities->getResource()->getConnection(); + + $entityTable = $connection->getTableName('catalog_product_entity'); + $linkTable = $connection->getTableName('catalog_product_link'); + + $select = $this->stagingHelper->getBaseStageDuplicationSelect($connection, $entityTable, $tmpTable, $joinCondition); + $select->joinInner( + ['u' => $linkTable], + 'u.product_id = t._row_id', + [] + ); + + $select->columns(['e.row_id', 'u.linked_product_id', 'u.link_type_id']); + + $select->setPart('disable_staging_preview', true); + + $insert = $connection->insertFromSelect( + $select, + $linkTable, + array('product_id', 'linked_product_id', 'link_type_id'), + 1 + ); + $connection->query($insert); + } + + /** + * Duplicate relations for configurable products for all stages. + * + * @see updateAllStageValues for more information on why it's used. + * + * @param Entities $entities + * @param string $tmpTable + * @param string $joinCondition + */ + public function updateAllStageConfigurables(Entities $entities, $tmpTable, $joinCondition = 't._row_id != e.row_id') + { + $connection = $entities->getResource()->getConnection(); + + $entityTable = $connection->getTableName('catalog_product_entity'); + + $attributeTable = $connection->getTableName('catalog_product_super_attribute'); + $labelTable = $connection->getTableName('catalog_product_super_attribute_label'); + $relationTable = $connection->getTableName('catalog_product_relation'); + $linkTable = $connection->getTableName('catalog_product_super_link'); + + $baseSelect = $this->stagingHelper + ->getBaseStageDuplicationSelect($connection, $entityTable, $tmpTable, $joinCondition); + + /** + * Duplicating Data in catalog_product_super_attribute + */ + $select = clone $baseSelect; + $select->joinInner( + ['u' => $attributeTable], + 'u.product_id = t._row_id', + [] + )->columns(['e.row_id', 'u.attribute_id', 'u.position']); + + $insert = $connection->insertFromSelect( + $select, + $attributeTable, + array('product_id', 'attribute_id', 'position'), + 1 + ); + $connection->query($insert); + + /** + * Duplicating Data in catalog_product_super_attribute_label + */ + $select = clone $baseSelect; + $select->joinInner( + ['u_new' => $attributeTable], + 'u_new.product_id = e.row_id', + [] + )->joinInner( + ['u_source' => $attributeTable], + 'u_source.product_id = t._row_id', + [] + )->joinInner( + ['l_source' => $labelTable], + 'l_source.product_super_attribute_id = u_source.product_super_attribute_id', + [] + )->columns(['u_new.product_super_attribute_id', 'l_source.store_id', 'l_source.use_default', 'l_source.value']); + + $insert = $connection->insertFromSelect( + $select, + $labelTable, + array('product_super_attribute_id', 'store_id', 'use_default', 'value'), + 1 + ); + $connection->query($insert); + + /** + * Duplicating Data in catalog_product_relation + */ + $select = clone $baseSelect; + $select->joinInner( + ['u' => $relationTable], + 'u.parent_id = t._row_id', + [] + )->columns(['e.row_id', 'u.child_id']); + + $insert = $connection->insertFromSelect( + $select, + $relationTable, + array('parent_id', 'child_id'), + 1 + ); + $connection->query($insert); + /** + * Duplicating Data in catalog_product_super_link + */ + $select = clone $baseSelect; + $select->joinInner( + ['u' => $linkTable], + 'u.parent_id = t._row_id', + [] + )->columns(['e.row_id', 'u.product_id']); + + $insert = $connection->insertFromSelect( + $select, + $linkTable, + array('parent_id', 'product_id'), + 1 + ); + $connection->query($insert); + } + + /** + * Duplicate medias between products for all stages. + * + * @see updateAllStageValues for more information on why it's used. + * + * @param Entities $entities + * @param string $tmpTable + * @param string $joinCondition + */ + public function updateAllStageMedias(Entities $entities, $tmpTable, $joinCondition = 't._row_id != e.row_id') + { + $connection = $entities->getResource()->getConnection(); + + $entityTable = $connection->getTableName('catalog_product_entity'); + $mediaTable = $connection->getTableName('catalog_product_entity_media_gallery_value_to_entity'); + + $select = $this->stagingHelper + ->getBaseStageDuplicationSelect($connection, $entityTable, $tmpTable, $joinCondition); + $select->joinInner( + ['u' => $mediaTable], + 'u.row_id = t._row_id', + [] + ); + + $select->columns(['u.value_id', 'e.row_id']); + + $select->setPart('disable_staging_preview', true); + + $insert = $connection->insertFromSelect( + $select, + $mediaTable, + array('value_id', 'row_id'), + 1 + ); + $connection->query($insert); + } +} \ No newline at end of file diff --git a/Product/Model/Factory/Import.php b/Product/Model/Factory/Import.php index d41bb06..cb398ff 100755 --- a/Product/Model/Factory/Import.php +++ b/Product/Model/Factory/Import.php @@ -6,6 +6,9 @@ use \Pimgento\Entities\Model\Entities; use \Pimgento\Import\Helper\Config as helperConfig; use \Pimgento\Import\Helper\UrlRewrite as urlRewriteHelper; +use \Pimgento\Staging\Helper\Config as StagingConfigHelper; +use \Pimgento\Product\Helper\Staging as StagingProductHelper; +use \Pimgento\Staging\Helper\Import as StagingHelper; use \Pimgento\Product\Helper\Config as productHelper; use \Pimgento\Product\Helper\Media as mediaHelper; use \Pimgento\Product\Model\Factory\Import\Related; @@ -23,6 +26,12 @@ class Import extends Factory { + /** + * Column names for the staging support. + */ + const COLUMN_STAGING_FROM = 'from'; + const COLUMN_STAGING_TO = 'to'; + /** * @var Entities */ @@ -59,6 +68,21 @@ class Import extends Factory */ protected $_urlRewriteHelper; + /** + * @var StagingConfigHelper + */ + protected $stagingConfigHelper; + + /** + * @var StagingHelper + */ + protected $stagingHelper; + + /** + * @var StagingProductHelper + */ + protected $stagingProductHelper; + /** * @var Media $_media */ @@ -90,6 +114,9 @@ class Import extends Factory * @param Related $related * @param Media $media * @param Product $product + * @param StagingConfigHelper $stagingConfigHelper + * @param StagingHelper $stagingHelper + * @param StagingProductHelper $stagingProductHelper * @param array $data */ public function __construct( @@ -103,6 +130,9 @@ public function __construct( productHelper $productHelper, mediaHelper $mediaHelper, urlRewriteHelper $urlRewriteHelper, + StagingConfigHelper $stagingConfigHelper, + StagingHelper $stagingHelper, + StagingProductHelper $stagingProductHelper, Related $related, Media $media, Product $product, @@ -116,6 +146,9 @@ public function __construct( $this->_productHelper = $productHelper; $this->_mediaHelper = $mediaHelper; $this->_urlRewriteHelper = $urlRewriteHelper; + $this->stagingConfigHelper = $stagingConfigHelper; + $this->stagingHelper = $stagingHelper; + $this->stagingProductHelper = $stagingProductHelper; $this->_related = $related; $this->_media = $media; $this->_product = $product; @@ -132,9 +165,16 @@ public function createTable() $this->setContinue(false); $this->setStatus(false); $this->setMessage($this->getFileNotFoundErrorMessage()); - } else { - $this->_entities->createTmpTableFromFile($file, $this->getCode(), array('sku')); + $requiredColumns = ['sku']; + + // If full mode is activated we need aditional columns. + if ($this->_productHelper->isImportInFullStagingMode()) { + $requiredColumns[] = self::COLUMN_STAGING_FROM; + $requiredColumns[] = self::COLUMN_STAGING_TO; + } + + $this->_entities->createTmpTableFromFile($file, $this->getCode(), $requiredColumns); } } @@ -180,6 +220,8 @@ public function addRequiredData() $connection->update($tmpTable, array('_visibility' => new Expr('IF(`groups` <> "", 1, 4)'))); } + $this->stagingHelper->addRequiredData($connection, $tmpTable); + if ($connection->tableColumnExists($tmpTable, 'type_id')) { $types = $connection->quote($this->_allowedTypeId); $connection->update( @@ -217,6 +259,40 @@ public function addRequiredData() } } + /** + * When the stage module is enable and we are on full staging mode the csv file may contain the start & end dates + * of versions. Transform those dates to timestamp & check that they are valid. + */ + public function checkStageDates() + { + if ($this->_productHelper->isImportInFullStagingMode()) { + + $connection = $this->_entities->getResource()->getConnection(); + $tmpTable = $this->_entities->getTableName($this->getCode()); + + // First let's transform all the from & to dates to timestamps. + $this->stagingHelper->updateDates( + $connection, + $tmpTable, + [self::COLUMN_STAGING_FROM => 'created_in', self::COLUMN_STAGING_TO => 'updated_in'] + ); + + $errorMsg = $this->stagingHelper->checkStageDates( + $connection, + $tmpTable, + $connection->getTableName('catalog_product_entity'), + 'sku' + ); + if ($errorMsg) { + $this->setContinue(false); + $this->setStatus(false); + $this->setMessage($errorMsg); + + return; + } + } + } + /** * Create Configurable products */ @@ -238,12 +314,14 @@ public function createConfigurable() ); } else { $connection->addColumn($tmpTable, '_children', 'TEXT NULL'); + $connection->addColumn($tmpTable, '_first_children', 'VARCHAR(255) NULL'); $connection->addColumn($tmpTable, '_axis', 'VARCHAR(255) NULL'); $data = array( 'sku' => 'e.groups', 'url_key' => 'e.groups', '_children' => new Expr('GROUP_CONCAT(e.sku SEPARATOR ",")'), + '_first_children' => new Expr('COALESCE(e.sku)'), '_type_id' => new Expr('"configurable"'), '_options_container' => new Expr('"container1"'), '_status' => 'e._status', @@ -327,11 +405,40 @@ public function createConfigurable() } /** - * Match code with entity + * Match code with entity, and if staging then also match with row id and create necessery duplications. */ public function matchEntity() { - $this->_entities->matchEntity($this->getCode(), 'sku', 'catalog_product_entity', 'entity_id'); + if ($this->stagingConfigHelper->isCatalogStagingModulesEnabled()) { + /** + * When using staging module entity id's are not the primary key of the catalog_product_entity + * table anymore. The new primary keys is row_id. Before we get information on the row_id, we still + * need to get the entiy_id of the products to be imported. We are therefore going to use a different + * table built for this purpose in magento. + */ + $this->_entities->matchEntity($this->getCode(), 'sku', 'sequence_product', 'sequence_value'); + $tmpTable = $this->_entities->getTableName($this->getCode()); + + // We need to update the created/updated in values of the configurables. + $this->stagingProductHelper->updateConfigurableStages( + $this->_entities, + $tmpTable, + 'catalog_product_entity', + $this->getCode(), + $this->_productHelper->getImportStagingMode() + ); + + // Once the entiy id's are matched we can match the row ids. + $this->stagingHelper->matchEntityRows( + $this->_entities, + 'catalog_product_entity', + $this->getCode(), + $this->_productHelper->getImportStagingMode() + ); + + } else { + $this->_entities->matchEntity($this->getCode(), 'sku', 'catalog_product_entity', 'entity_id'); + } } /** @@ -403,6 +510,7 @@ public function updateOption() '_attribute_set_id', '_visibility', '_children', + '_first_children', '_axis', 'sku', 'categories', @@ -410,6 +518,9 @@ public function updateOption() 'groups', 'url_key', 'enabled', + // columns to handle staging. + 'created_in', + 'updated_in' ); foreach ($columns as $column) { @@ -454,6 +565,7 @@ public function updateOption() 'entity_id' => 'p._entity_id' ) ) + ->distinct() ->joinInner( array('c1' => new Expr('('.(string) $subSelect.')')), new Expr($conditionJoin), @@ -503,12 +615,14 @@ public function createEntities() $table = $resource->getTable('catalog_product_entity'); - $columnIdentifier = $this->_entities->getColumnIdentifier($table); - - if ($columnIdentifier == 'row_id') { - $values['row_id'] = '_entity_id'; + if ($this->stagingConfigHelper->isCatalogStagingModulesEnabled()) { + $values['created_in'] = 'created_in'; + $values['updated_in'] = 'updated_in'; + $values['row_id'] = '_row_id'; } + $this->stagingHelper->createEntitiesBefore($connection, 'sequence_product', $tmpTable); + $parents = $connection->select()->from($tmpTable, $values); $connection->query( $connection->insertFromSelect( @@ -516,12 +630,19 @@ public function createEntities() ) ); + $this->stagingHelper->createEntitiesAfter( + $connection, + 'catalog_product_entity', + $tmpTable, + $this->_productHelper->getImportStagingMode() + ); + $values = array( 'created_at' => new Expr('now()') ); $connection->update($table, $values, 'created_at IS NULL'); - if ($columnIdentifier == 'row_id') { + if ($this->stagingConfigHelper->isCatalogStagingModulesEnabled()) { $values = [ 'created_in' => new Expr(1), 'updated_in' => new Expr(VersionManager::MAX_VERSION), @@ -560,12 +681,15 @@ public function setValues() '_attribute_set_id', '_visibility', '_children', + '_first_children', '_axis', 'sku', 'categories', 'family', 'groups', 'enabled', + 'created_in', + 'updated_in', ); $values = array( @@ -599,6 +723,8 @@ public function setValues() $columnPrefix = explode('-', $column); $columnPrefix = reset($columnPrefix); + $values[0][$columnPrefix] = $column; + foreach ($stores as $suffix => $affected) { if (preg_match('/^' . $columnPrefix . '-' . $suffix . '$/', $column)) { foreach ($affected as $store) { @@ -631,6 +757,8 @@ public function linkConfigurable() $connection = $resource->getConnection(); $tmpTable = $this->_entities->getTableName($this->getCode()); + $identifierAttribute = $this->stagingConfigHelper->isCatalogStagingModulesEnabled() ? '_row_id' : '_entity_id'; + if (!$this->moduleIsEnabled('Pimgento_Variant')) { $this->setStatus(false); $this->setMessage( @@ -649,7 +777,7 @@ public function linkConfigurable() ->from( $tmpTable, array( - '_entity_id', + $identifierAttribute, '_axis', '_children' ) @@ -686,7 +814,7 @@ public function linkConfigurable() /* catalog_product_super_attribute */ $values = array( - 'product_id' => $row['_entity_id'], + 'product_id' => $row[$identifierAttribute], 'attribute_id' => $id, 'position' => $position++, ); @@ -699,7 +827,7 @@ public function linkConfigurable() $connection->select() ->from($resource->getTable('catalog_product_super_attribute')) ->where('attribute_id = ?', $id) - ->where('product_id = ?', $row['_entity_id']) + ->where('product_id = ?', $row[$identifierAttribute]) ->limit(1) ); @@ -731,14 +859,14 @@ public function linkConfigurable() if ($childId) { /* catalog_product_relation */ $valuesRelations[] = array( - 'parent_id' => $row['_entity_id'], + 'parent_id' => $row[$identifierAttribute], 'child_id' => $childId, ); /* catalog_product_super_link */ $valuesSuperLink[] = array( 'product_id' => $childId, - 'parent_id' => $row['_entity_id'], + 'parent_id' => $row[$identifierAttribute], ); } } @@ -851,7 +979,8 @@ public function setCategories() 'FIND_IN_SET(`c`.`code`, `p`.`categories`) AND `c`.`import` = "category"', array( 'category_id' => 'c.entity_id', - 'product_id' => 'p._entity_id' + 'product_id' => 'p._entity_id', + 'position' => new Expr(1) ) ) ->joinInner( @@ -864,7 +993,7 @@ public function setCategories() $connection->insertFromSelect( $select, $resource->getTable('catalog_category_product'), - array('category_id', 'product_id'), + array('category_id', 'product_id', 'position'), 1 ) ); @@ -1007,12 +1136,57 @@ public function setUrlRewrite() $this->_urlRewriteHelper->dropUrlRewriteTmpTable(); } + /** + * Duplicate imported information in all the stages of the product. + */ + public function updateAllStages() + { + + $tmpTable = $this->_entities->getTableName($this->getCode()); + if ($this->_productHelper->getImportStagingMode() == StagingConfigHelper::STAGING_MODE_ALL) { + /** + * In this mode pimgento don't really cares about the stage it updates because it simply updates all + * stages + */ + $this->stagingProductHelper->updateAllStageValues($this->_entities, $tmpTable); + + $this->stagingProductHelper->updateAllStageRelations($this->_entities, $tmpTable); + + $this->stagingProductHelper->updateAllStageConfigurables($this->_entities, $tmpTable); + + $this->stagingProductHelper->updateAllStageMedias($this->_entities, $tmpTable); + + } elseif ($this->_productHelper->getImportStagingMode() == StagingConfigHelper::STAGING_MODE_FULL) { + + /** + * In this mode pimgento will need to duplicate some information. It needs to do this because a new version + * has been created and the old version was split in 2 parts that needs to have the same data. + */ + $duplicateTmpTable = $this->_entities->getResource()->getConnection()->getTableName('tmp_pimgento_entity_stage_duplicate'); + $condition = 't.created_in = e.created_in'; + + $this->stagingProductHelper + ->updateAllStageValues($this->_entities, $duplicateTmpTable, $condition, $tmpTable); + + $this->stagingProductHelper + ->updateAllStageRelations($this->_entities, $duplicateTmpTable, $condition); + + $this->stagingProductHelper + ->updateAllStageConfigurables($this->_entities, $duplicateTmpTable, $condition); + + $this->stagingProductHelper + ->updateAllStageMedias($this->_entities, $tmpTable, $condition); + } + } + /** * Drop temporary table */ public function dropTable() { $this->_entities->dropTable($this->getCode()); + + $this->stagingHelper->dropTemporaryStageTable($this->_entities->getResource()->getConnection()); } /** @@ -1151,4 +1325,4 @@ public function importMedia() $this->_media->mediaDropTmpTables(); } } -} +} \ No newline at end of file diff --git a/Product/Model/Factory/Import/Media.php b/Product/Model/Factory/Import/Media.php index 1abbedf..9b7c844 100644 --- a/Product/Model/Factory/Import/Media.php +++ b/Product/Model/Factory/Import/Media.php @@ -6,6 +6,7 @@ use \Pimgento\Entities\Model\Entities; use \Pimgento\Import\Helper\Config as helperConfig; use \Pimgento\Product\Helper\Media as mediaHelper; +use \Pimgento\Staging\Helper\Config as StagingConfigHelper; use \Magento\Framework\Event\ManagerInterface; use \Magento\Framework\Module\Manager as moduleManager; use \Magento\Framework\App\Config\ScopeConfigInterface as scopeConfig; @@ -32,6 +33,11 @@ class Media extends Factory */ protected $image; + /** + * @var StagingConfigHelper + */ + protected $stagingConfigHelper; + /** * PHP Constructor * @@ -52,6 +58,7 @@ public function __construct( Entities $entities, mediaHelper $mediaHelper, Image $image, + StagingConfigHelper $stagingConfigHelper, array $data = [] ) { parent::__construct($helperConfig, $eventManager, $moduleManager, $scopeConfig, $data); @@ -59,6 +66,7 @@ public function __construct( $this->_entities = $entities; $this->_mediaHelper = $mediaHelper; $this->image = $image; + $this->stagingConfigHelper = $stagingConfigHelper; } /** @@ -98,6 +106,11 @@ public function mediaCreateTmpTables() $table->addColumn('media_cleaned', Table::TYPE_TEXT, 255, []); $table->addColumn('media_value', Table::TYPE_TEXT, 255, []); $table->addColumn('position', Table::TYPE_INTEGER, 10, ['unsigned' => true]); + + if ($this->stagingConfigHelper->isCatalogStagingModulesEnabled()) { + $table->addColumn('row_id', \Magento\Framework\DB\Ddl\Table::TYPE_INTEGER, 10, ['unsigned' => true]); + } + $table->addIndex( $tableMedia.'_entity_id', ['entity_id'], @@ -164,23 +177,29 @@ public function mediaPrepareValues($column, $attributeId, $position) $attributeId = 'NULL'; } + $cols = [ + 'sku' => 't.sku', + 'entity_id' => 't._entity_id', + 'attribute_id' => new Expr($attributeId), + 'store_id' => new Expr('0'), + 'media_original' => "t.$column", + 'position' => new Expr($position) + ]; + + if ($this->stagingConfigHelper->isCatalogStagingModulesEnabled()) { + $cols['row_id'] = 't._row_id'; + } + $select = $connection->select() ->from( ['t' => $tmpTable], - [ - 'sku' => 't.sku', - 'entity_id' => 't._entity_id', - 'attribute_id' => new Expr($attributeId), - 'store_id' => new Expr('0'), - 'media_original' => "t.$column", - 'position' => new Expr($position) - ] + $cols )->where("`t`.`$column` <> ''"); $query = $connection->insertFromSelect( $select, $tableMedia, - ['sku', 'entity_id', 'attribute_id', 'store_id', 'media_original', 'position'], + array_keys($cols), AdapterInterface::INSERT_ON_DUPLICATE ); @@ -364,27 +383,32 @@ public function mediaUpdateDataBase() $tableMedia = $this->_entities->getTableName('media'); $step = 5000; + // add the media in the varchar product table + $cols = [ + 'attribute_id' => 'attribute_id', + 'store_id' => 'store_id', + 'value' => 'media_value', + ]; + + if ($this->stagingConfigHelper->isCatalogStagingModulesEnabled()) { + $cols['row_id'] = 'row_id'; + $identifier = 'row_id'; + } else { + $cols['entity_id'] = 'entity_id'; + $identifier = 'entity_id'; + } + // add the media in the varchar product table $select = $connection->select() ->from( ['t' => $tableMedia], - [ - 'attribute_id' => 'attribute_id', - 'store_id' => 'store_id', - 'entity_id' => 'entity_id', - 'value' => 'media_value', - ] + $cols ) ->where('t.attribute_id is not null'); - - $identifier = $this->_entities->getColumnIdentifier( - $resource->getTable('catalog_product_entity_varchar') - ); - $query = $connection->insertFromSelect( $select, $resource->getTable('catalog_product_entity_varchar'), - ['attribute_id', 'store_id', $identifier, 'value'], + array_keys($cols), AdapterInterface::INSERT_ON_DUPLICATE ); $connection->query($query); @@ -448,8 +472,6 @@ public function mediaUpdateDataBase() // working on "media gallery value" $tableGallery = $resource->getTable('catalog_product_entity_media_gallery_value'); - $identifier = $this->_entities->getColumnIdentifier($tableGallery); - // get the record id from gallery value (for new medias) $maxId = $this->mediaGetMaxId($tableGallery, 'record_id'); for ($k=1; $k<=$maxId; $k+= $step) { @@ -474,7 +496,7 @@ public function mediaUpdateDataBase() [ 'value_id' => 'value_id', 'store_id' => new Expr('0'), - $identifier => 'entity_id', + $identifier => $identifier, 'label' => new Expr("''"), 'position' => 'position', ] @@ -500,7 +522,7 @@ public function mediaUpdateDataBase() ['t' => $tableMedia], [ 'value_id' => 'value_id', - $identifier => 'entity_id', + $identifier => $identifier, ] ); $query = $connection->insertFromSelect( diff --git a/Product/Model/Factory/Import/Related.php b/Product/Model/Factory/Import/Related.php index a3c9e16..10b8bb5 100644 --- a/Product/Model/Factory/Import/Related.php +++ b/Product/Model/Factory/Import/Related.php @@ -151,7 +151,6 @@ public function relatedImportColumn($type) ); } - // Get the product ids for parents $query = $connection->select() ->from(false, ['parent_id' => 'p.' . $this->_entities->getColumnIdentifier($tableProduct)]) ->joinLeft( @@ -159,7 +158,6 @@ public function relatedImportColumn($type) 'r.parent_sku = p.sku', [] ); - $connection->query( $connection->updateFromSelect($query, ['r' => $tableRelated]) ); diff --git a/Product/Observer/AddPimgentoImportObserver.php b/Product/Observer/AddPimgentoImportObserver.php index 1d01bae..4742547 100644 --- a/Product/Observer/AddPimgentoImportObserver.php +++ b/Product/Observer/AddPimgentoImportObserver.php @@ -67,6 +67,10 @@ protected function getStepsDefinition() 'comment' => __('Add product required data'), 'method' => 'addRequiredData', ), + array( + 'comment' => __('Match code with Magento ID'), + 'method' => 'checkStageDates', + ), array( 'comment' => __('Create configurable product'), 'method' => 'createConfigurable', @@ -122,6 +126,11 @@ protected function getStepsDefinition() 'comment' => __('Import media files'), 'method' => 'importMedia', ), + /* Step that will work only if staging enabled & certain options are selected. */ + array( + 'comment' => __('Update stages'), + 'method' => 'updateAllStages', + ), ); $stepsAfter = array( @@ -147,4 +156,4 @@ protected function getStepsDefinition() $stepsAfter ); } -} +} \ No newline at end of file diff --git a/Product/etc/adminhtml/system.xml b/Product/etc/adminhtml/system.xml index de75ed2..4a49944 100644 --- a/Product/etc/adminhtml/system.xml +++ b/Product/etc/adminhtml/system.xml @@ -21,6 +21,11 @@ Magento\Config\Model\Config\Backend\Serialized\ArraySerialized Fill configurable attributes with default value, leave blank to take simple product value or variant value if exists + + + Pimgento\Staging\Model\Source\StagingMode + In full mode the product import expects version_from, version_to columns + diff --git a/Product/etc/config.xml b/Product/etc/config.xml index 6d20429..84c308b 100644 --- a/Product/etc/config.xml +++ b/Product/etc/config.xml @@ -5,6 +5,7 @@ a:1:{s:18:"_1459928423869_869";a:2:{s:13:"pim_attribute";s:11:"description";s:17:"magento_attribute";s:17:"short_description";}} a:2:{s:18:"_1464857349718_718";a:2:{s:9:"attribute";s:11:"description";s:5:"value";s:0:"";}s:18:"_1464857373248_248";a:2:{s:9:"attribute";s:4:"name";s:5:"value";s:0:"";}} + simple 0 diff --git a/Product/etc/module.xml b/Product/etc/module.xml index bbb28cf..bf15b5c 100644 --- a/Product/etc/module.xml +++ b/Product/etc/module.xml @@ -4,6 +4,7 @@ + \ No newline at end of file diff --git a/Product/i18n/fr_FR.csv b/Product/i18n/fr_FR.csv index 4504dfb..b24ad4d 100644 --- a/Product/i18n/fr_FR.csv +++ b/Product/i18n/fr_FR.csv @@ -43,3 +43,5 @@ "Swatch image column code","Code de la colonne pour l'image Swatch" "Gallery image columns code","Code des colonnes pour l'image Gallery" "Move files","Déplacer les fichiers" +"Staging Mode","Mode de Mise en scène" +"In full mode the product import expects version_from, version_to columns","En mode complet l'import produit s'attend aux colonnes version_from, version_to" \ No newline at end of file diff --git a/Staging/Helper/Config.php b/Staging/Helper/Config.php new file mode 100644 index 0000000..e361217 --- /dev/null +++ b/Staging/Helper/Config.php @@ -0,0 +1,77 @@ + + * @copyright 2017 Smile + * @package Pimgento\Staging\Helper + */ +class Config extends AbstractHelper +{ + /** + * Constants to configuration paths. + */ + const CONFIG_PATH_STAGING_MODE = 'pimgento/%s/staging_mode'; + + /** + * Constants for different staging modes. + */ + const STAGING_MODE_DISABLED = 'disabled'; + const STAGING_MODE_LAST = 'last'; + const STAGING_MODE_CURRENT = 'current'; + const STAGING_MODE_ALL = 'all'; + const STAGING_MODE_FULL = 'full'; + + /** + * @var \Magento\Framework\Module\Manager + */ + protected $moduleManager; + + /** + * Constructor + * + * @param Context $context + * @param \Magento\Framework\Module\Manager $moduleManager + */ + public function __construct( + Context $context, + \Magento\Framework\Module\Manager $moduleManager + ) { + $this->moduleManager = $moduleManager; + + parent::__construct($context); + } + + /** + * Check if staging module is enabled for the catalog. + * + * @return bool + */ + public function isCatalogStagingModulesEnabled() + { + return $this->moduleManager->isEnabled('Magento_Staging') + && $this->moduleManager->isEnabled('Magento_CatalogStaging'); + } + + /** + * Get import staging mode to use. + * + * @return mixed|string + */ + public function getImportStagingMode($profile) + { + + if (!$this->isCatalogStagingModulesEnabled()) { + return self::STAGING_MODE_DISABLED; + } else { + return $this->scopeConfig->getValue(sprintf(self::CONFIG_PATH_STAGING_MODE, $profile)); + } + } +} \ No newline at end of file diff --git a/Staging/Helper/Import.php b/Staging/Helper/Import.php new file mode 100644 index 0000000..1e5962e --- /dev/null +++ b/Staging/Helper/Import.php @@ -0,0 +1,657 @@ + + * @copyright 2017 Smile + * @package Pimgento\Staging\Helper + */ +class Import extends AbstractHelper +{ + /** + * @var Config + */ + protected $configHelper; + + /** + * @param \Magento\Framework\App\Helper\Context $context + * @param Config $configHelper + */ + public function __construct(Context $context, \Pimgento\Staging\Helper\Config $configHelper) + { + $this->configHelper = $configHelper; + + parent::__construct($context); + } + + /** + * Add required data columns on the temporary table. + * + * @param AdapterInterface $connection + * @param string $tmpTable + */ + public function addRequiredData(AdapterInterface $connection, $tmpTable) + { + if ($this->configHelper->isCatalogStagingModulesEnabled()) { + $connection->addColumn($tmpTable, '_row_id', 'INT(11)'); + $connection->addColumn($tmpTable, 'created_in', 'INT(11)'); + $connection->addColumn($tmpTable, 'updated_in', 'INT(11)'); + $connection->addColumn($tmpTable, '_new_entity', 'SMALLINT(1) DEFAULT 0'); + } + } + + /** + * Update dates in the temporary table with unix timestamp. + * + * @param AdapterInterface $connection + * @param $tmpTable + * @param $columns + */ + public function updateDates(AdapterInterface $connection, $tmpTable, $columns) + { + $set = []; + foreach ($columns as $origin => $to) { + $set[] = "$to = UNIX_TIMESTAMP(STR_TO_DATE(t.$origin,'%Y-%m-%d'))"; + } + + $query = "UPDATE $tmpTable t + SET " . implode(', ', $set) . " + "; + $connection->query($query); + + // Drop the original data not to have to bother with it. + foreach ($columns as $origin => $to) { + $connection->dropColumn($tmpTable, $origin); + } + + // Put default values for now for products without version specification. + $connection->query("UPDATE $tmpTable t SET created_in = 1 WHERE created_in = 0"); + $connection->query( + "UPDATE $tmpTable t SET updated_in = " . VersionManager::MAX_VERSION . " WHERE updated_in = 0" + ); + } + + /** + * Check the dates that were given in the csv file to be sure they can be imported. + * + * The function returns the error message if there is one, and null if there isn't one. + * + * @param AdapterInterface $connection + * @param $tmpTable + * @param $identifier + * @return null|string + */ + public function checkStageDates(AdapterInterface $connection, $tmpTable, $entityTable, $identifier) + { + /** + * Check that only one stage of the product is being imported. + */ + $select = $connection + ->select() + ->from($tmpTable, new Expr('count(*) as nb')) + ->group('sku') + ->having('nb > 1'); + $nb = $connection->fetchOne($select); + + if ($nb > 1) { + return "You can't import 2 stages of the same product at the same time!"; + } + + /** + * Check for incoherence in the created in and updated_in values. + */ + $select = $connection + ->select() + ->from($tmpTable, $identifier) + ->where('created_in >= updated_in AND updated_in != 0'); + + $invalidId = $connection->fetchOne($select); + if ($invalidId) { + return "Error with '$invalidId' : 'from' date superior or equal to it's 'to' date !"; + } + + /** + * Check that all version to be imported are for future use as we can't modify older versions. + */ + $select = $connection + ->select() + ->from($tmpTable, $identifier) + ->where('created_in < ' . time() . ' AND created_in > 1') + ->limit(1); + + $invalidId = $connection->fetchOne($select); + if ($invalidId) { + return "Error with '$invalidId' : Trying to update an on going or past stage !"; + } + + /** + * Check that for each from value we have a single to value. + */ + $select = $connection + ->select() + ->from($tmpTable, new Expr('count(DISTINCT updated_in) as nb')) + ->where('created_in != 0') + ->group('created_in') + ->having('nb > 1'); + $nb = $connection->fetchOne($select); + + if ($nb > 1) { + return "Error you can't have 2 stage starting at the same time !"; + } + + /** + * Check If all simple products of configurables have same dates. + */ + if ($connection->tableColumnExists($tmpTable, 'groups')) { + $select = $connection + ->select() + ->from($tmpTable, ['groups']) + ->where('groups != ""') + ->group('groups') + ->having('count(DISTINCT created_in) > 1'); + + $nb = $connection->fetchOne($select); + + if ($nb > 1) { + return "You can't have different stages for the products of the same configurable !"; + } + } + + /* + * Check if created & updated dates are also usable with actual database. In order to do this we check for each + * product that we import if they alread + */ + $select = $connection + ->select() + ->from(['t' => $tmpTable], $identifier) + ->joinInner( + ['e' => $entityTable], + 'e.'. $identifier . ' = t. ' . $identifier, + [] + ) + ->joinInner( + ['u' => $connection->getTableName('staging_update')], + 'u.id = e.created_in', + [] + ) + ->where('u.is_rollback IS NULL') + ->where( + '(u.id < t.created_in AND (u.rollback_id > t.created_in OR u.rollback_id IS NULL)) + OR (u.id < t.updated_in AND (u.rollback_id > t.updated_in OR u.rollback_id IS NULL)) + OR ((u.id BETWEEN t.created_in AND t.updated_in) AND u.id != t.created_in AND u.id != t.updated_in) + OR (u.rollback_id IS NOT NULL + AND (u.rollback_id BETWEEN t.created_in AND t.updated_in) + AND u.rollback_id != t.created_in AND u.rollback_id != t.updated_in + )' + ); + $select->setPart('disable_staging_preview', true); + + $invalidId = $connection->fetchOne($select); + if ($invalidId) { + return "Error with '$invalidId' : 'from' and 'to' dates intersects with existing stages !"; + } + + return null; + } + + /** + * Matching the row id's for all our entities using different rules depending on the staging mode. + * + * @param Entities $entities + * @param string $entityTableCode + * @param string $code + * @param string $stagingMode + */ + public function matchEntityRows(Entities $entities, $entityTableCode, $code, $stagingMode) + { + $connection = $entities->getResource()->getConnection(); + $tmpTable = $entities->getTableName($code); + $entityTable = $entities->getResource()->getTable($entityTableCode); + + $startTime = time(); + + switch ($stagingMode) { + case Config::STAGING_MODE_LAST: + case Config::STAGING_MODE_ALL: + /** + * Update row id column. + * We are going to update the last version that was created if there is multiple versions + * + * In full mode we will have another step that will duplicate the content in all the stages. + */ + $connection->query( + 'UPDATE `' . $tmpTable . '` t + SET `_row_id` = ( + SELECT MAX(row_id) FROM `' . $entityTable . '` c + WHERE c.entity_id = t._entity_id AND updated_in = ' . VersionManager::MAX_VERSION . ' + )' + ); + + break; + + case Config::STAGING_MODE_CURRENT: + /** + * Update row id column. + * We are going to update the current versions. + */ + $connection->query( + 'UPDATE `' . $tmpTable . '` t + SET `_row_id` = ( + SELECT MAX(row_id) FROM `' . $entityTable . '` c + WHERE c.entity_id = t._entity_id + AND ( ' . $startTime . ' BETWEEN created_in AND updated_in) + )' + ); + + break; + + case Config::STAGING_MODE_FULL: + + $this->createTemporaryStageTable($connection); + + /** + * Update row id column with simple rule of getting the rows that can just be updated. + */ + $connection->query( + 'UPDATE `' . $tmpTable . '` t + SET `_row_id` = ( + SELECT MAX(row_id) FROM `' . $entityTable . '` c + WHERE c.entity_id = t._entity_id + AND c.created_in = t.created_in + AND c.updated_in = t.updated_in + )' + ); + + /** + * For all remaining products, either we are importing a new product or we are creating + * a new stage to an existing product. + */ + + // First let's handle new products. Simply change created/updated values to default. There is no need + // to create multiple versions. + $connection->query( + 'UPDATE `' . $tmpTable . '` t + SET `created_in` = 1, + `updated_in` = ' . VersionManager::MAX_VERSION . ', + `_new_entity` = 1 + WHERE t._entity_id NOT IN (SELECT entity_id FROM ' . $entityTable . ')' + ); + + // Let's handle all the products that needs to be updated. + $this->setStageValues($connection, $tmpTable, $entityTable); + } + + + /** + * For existing versions fetch version created_in & updated_in from database. + */ + $connection->query( + 'UPDATE `' . $tmpTable . '` t + INNER JOIN `' . $entityTable . '` c ON c.row_id = t._row_id + SET t.created_in = c.created_in, + t.updated_in = c.updated_in + WHERE t.created_in is NULL' + ); + + /** + * For new entities we need to put default created_in & updated_in values. + */ + $connection->query( + 'UPDATE `' . $tmpTable . '` t + SET `created_in` = 1, + `updated_in` = ' . VersionManager::MAX_VERSION . ' + WHERE t.created_in is NULL' + ); + } + + /** + * When importing a new version for an existing product we will need to update the current stage, + * duplicate it in the future and also update the staging_update table. Prepare the temporary tables to + * handle this. + * + * @param AdapterInterface $connection + * @param $tmpTable + * @param $entityTable + */ + protected function setStageValues(AdapterInterface $connection, $tmpTable, $entityTable) + { + // Let's find the versions that needs modifying. + // TODO need to join with staging_update as well to check that there are no issues. + // TODO We are not handling well stages that finishes in MAX_VERSION. Those just need previous version + // to be updated. + $select = $connection->select() + ->from( + ['t' => $tmpTable], + [ + 'entity_id' => '_entity_id', + 'entity_updated_in' => 'created_in', + 'new_entity_created_in' => 'updated_in' + ] + )->joinInner( + ['e' => $entityTable], + 't._entity_id = e.entity_id', + [ + 'row_id' => 'row_id', + 'entity_created_in' => 'created_in', + 'new_entity_updated_in' => 'updated_in' + ] + )->where('e.created_in <= t.created_in AND e.updated_in >= t.updated_in AND t._row_id IS NULL') + // Prevent staging module preview that adds additional conditions + ->setPart('disable_staging_preview', true); + + $query = $connection->query($select); + + $toUpdate = []; + $toDuplicate= []; + $entityIds = []; + while ($row = $query->fetch()) { + + $toUpdate[] = [ + '_entity_id' => $row['entity_id'], + '_row_id' => $row['row_id'], + 'created_in' => $row['entity_created_in'], + 'updated_in' => $row['entity_updated_in'], + ]; + + if ($row['new_entity_created_in'] != $row['new_entity_updated_in']) { + $toDuplicate[] = [ + '_entity_id' => $row['entity_id'], + '_row_id' => $row['row_id'], + 'created_in' => $row['new_entity_created_in'], + 'updated_in' => $row['new_entity_updated_in'], + ]; + } + + $entityIds[] = $row['entity_id']; + + if (count($toUpdate) > 500) { + $this->insertTemporaryStageValues($connection, $tmpTable, $toUpdate, $toDuplicate, $entityIds); + $toUpdate = []; + $toDuplicate= []; + $entityIds = []; + } + } + + if (count($toUpdate) > 0) { + $this->insertTemporaryStageValues($connection, $tmpTable, $toUpdate, $toDuplicate, $entityIds); + } + } + + /** + * Populate the temporary files to allow update & duplication of existing stage informations. + * + * @param AdapterInterface $connection + * @param $tmpTable + * @param $toUpdate + * @param $toDuplicate + * @param $entityIds + */ + protected function insertTemporaryStageValues( + AdapterInterface $connection, + $tmpTable, + $toUpdate, + $toDuplicate, + $entityIds + ) { + $tableStageUpdate = $connection->getTableName('tmp_pimgento_entity_stage_update'); + $tableStageDuplicate = $connection->getTableName('tmp_pimgento_entity_stage_duplicate'); + + $connection->insertArray($tableStageUpdate, array_keys($toUpdate[0]), $toUpdate); + + if (!empty($toDuplicate)) { + $connection->insertArray($tableStageDuplicate, array_keys($toDuplicate[0]), $toDuplicate); + } + + $connection->update( + $tmpTable, + ['_new_entity' => new Expr('0')], + ['_entity_id IN (?)' => $entityIds] + ); + } + + /** + * Create temporary tables to handle the staging. + * + * @param AdapterInterface $connection + */ + protected function createTemporaryStageTable(AdapterInterface $connection) + { + /** + * Table with row id's whose created_in & updated_in needs to be updated. + */ + $tableStageUpdate = $connection->getTableName('tmp_pimgento_entity_stage_update'); + /** + * Table with new stages that needs to be created with the row to use to get the data from. + */ + $tableStageDuplicate = $connection->getTableName('tmp_pimgento_entity_stage_duplicate'); + + $connection->dropTable($tableStageUpdate); + $connection->dropTable($tableStageDuplicate); + + $table = $connection->newTable($tableStageUpdate); + $table->addColumn('_entity_id', \Magento\Framework\DB\Ddl\Table::TYPE_INTEGER, 11, []); + $table->addColumn('_row_id', \Magento\Framework\DB\Ddl\Table::TYPE_INTEGER, 11, []); + $table->addColumn('created_in', \Magento\Framework\DB\Ddl\Table::TYPE_INTEGER, 11, []); + $table->addColumn('updated_in', \Magento\Framework\DB\Ddl\Table::TYPE_INTEGER, 11, []); + $connection->createTable($table); + + $table = $connection->newTable($tableStageDuplicate); + $table->addColumn('_entity_id', \Magento\Framework\DB\Ddl\Table::TYPE_INTEGER, 11, []); + $table->addColumn('_row_id', \Magento\Framework\DB\Ddl\Table::TYPE_INTEGER, 11, []); + $table->addColumn('created_in', \Magento\Framework\DB\Ddl\Table::TYPE_INTEGER, 11, []); + $table->addColumn('updated_in', \Magento\Framework\DB\Ddl\Table::TYPE_INTEGER, 11, []); + $connection->createTable($table); + } + + public function dropTemporaryStageTable(AdapterInterface $connection) + { + $connection->dropTable($connection->getTableName('tmp_pimgento_entity_stage_update')); + $connection->dropTable($connection->getTableName('tmp_pimgento_entity_stage_duplicate')); + } + + /** + * When staging mode is enabled we also need to fill up the sequence_product table. + * + * @param AdapterInterface $connection + * @param string $sequenceTable + * @param string $tmpTable + */ + public function createEntitiesBefore(AdapterInterface $connection, $sequenceTable, $tmpTable) + { + if ($this->configHelper->isCatalogStagingModulesEnabled()) { + $sequenceValues = ['sequence_value' => '_entity_id']; + $parents = $connection->select()->from($tmpTable, $sequenceValues); + $connection->query( + $connection->insertFromSelect( + $parents, + $connection->getTableName($sequenceTable), + array_keys($sequenceValues), + 1 + ) + ); + } + } + + /** + * Once the catalog_entity table has been filled, we need to get the row id's for all the new + * versions so that we can insert the values after. + * + * @param AdapterInterface $connection + * @param string $entityTableCode + * @param string $tmpTable + * @param string $stagingMode + */ + public function createEntitiesAfter(AdapterInterface $connection, $entityTableCode, $tmpTable, $stagingMode) + { + + if ($this->configHelper->isCatalogStagingModulesEnabled()) { + $entityTable = $connection->getTableName($entityTableCode); + + $connection->query( + 'UPDATE `' . $tmpTable . '` t + SET `_row_id` = ( + SELECT MAX(row_id) FROM `' . $entityTable . '` c + WHERE c.entity_id = t._entity_id + ) + WHERE t._row_id IS NULL' + ); + + if ($stagingMode == Config::STAGING_MODE_FULL) { + $tableStageUpdate = $connection->getTableName('tmp_pimgento_entity_stage_update'); + $tableStageDuplicate = $connection->getTableName('tmp_pimgento_entity_stage_duplicate'); + + // We need to update some of the existing entity rows to update the created_in/updated_in dates. + $select = $connection->select() + ->from( + ['t' => $tableStageUpdate], + [ + 'row_id' => '_row_id', + 'entity_id' => '_entity_id', + 'created_in', + 'updated_in', + 'updated_at' => new Expr('now()'), + ] + ); + + $query = $connection->insertFromSelect( + $select, + $connection->getTableName('catalog_product_entity'), + ['row_id', 'entity_id', 'created_in', 'updated_in', 'updated_at'], + 1 + ); + $connection->query($query); + + // We also need to create some new rows. + $select = $connection->select() + ->from( + ['t' => $tableStageDuplicate], + [ + 'entity_id' => '_entity_id', + 'created_in', + 'updated_in', + 'updated_at' => new Expr('now()'), + 'created_at' => new Expr('now()') + ] + )->joinInner( + ['u' => $entityTable], + 'u.entity_id = t._entity_id', + [ + 'attribute_set_id', 'type_id', 'sku', 'has_options', 'required_options' + ] + ); + + $query = $connection->insertFromSelect( + $select, + $entityTableCode, + [ + 'entity_id', + 'created_in', + 'updated_in', + 'updated_at', + 'created_at', + 'attribute_set_id', + 'type_id', 'sku', + 'has_options', + 'required_options' + ] + ); + $connection->query($query); + + // Need to edit & insert lines in staging_update. + $stagingTable = $connection->getTableName('staging_update'); + $select = $connection->select() + ->from( + ['t' => $tmpTable], + [ + 'id' => 'created_in', + 'start_time' => new Expr('from_unixtime(created_in)'), + 'name' => + new Expr( + 'CONCAT( + "Pimgento : ", + from_unixtime(created_in, GET_FORMAT(DATE,"ISO")), + " to ", + from_unixtime(updated_in, GET_FORMAT(DATE,"ISO")))' + ), + 'rollback_id' + => new Expr('IF(updated_in = ' . VersionManager::MAX_VERSION . ', NULL, updated_in)'), + 'is_rollback' => new Expr('NULL'), + ] + )->where('created_in != 1'); + $query = $connection->insertFromSelect( + $select, + $stagingTable, + ['id', 'start_time', 'name', 'rollback_id', 'is_rollback'], + 1 + ); + $connection->query($query); + + $stagingTable = $connection->getTableName('staging_update'); + $select = $connection->select() + ->from( + ['t' => $tmpTable], + [ + 'id' => 'updated_in', + 'start_time' => new Expr('from_unixtime(updated_in)'), + 'name' => new Expr('"Rollback for Pimgento"'), + 'rollback_id' => new Expr("NULL"), + 'is_rollback' => new Expr('1'), + ] + )->where('created_in != 1 AND updated_in != ' . VersionManager::MAX_VERSION); + $query = $connection->insertFromSelect( + $select, + $stagingTable, + ['id', 'start_time', 'name', 'rollback_id', 'is_rollback'], + 1 + ); + $connection->query($query); + + } + } + } + + /** + * Query to get all entity rows(stages) that has not been updated by the current import. + * + * Used to duplicate the data in all the stages. + * + * @param AdapterInterface $connection + * @param string $entityTable + * @param string $tmpTable + * @param string $joinCondition + * + * @return Select + */ + public function getBaseStageDuplicationSelect( + AdapterInterface $connection, + $entityTable, + $tmpTable, + $joinCondition = 't._row_id != e.row_id' + ) { + return $connection->select() + ->from( + ['e' =>$entityTable], + [] + ) + // For each row we didn't update of the entities we have updated. + ->joinInner( + ['t' => $tmpTable], + 't._entity_id = e.entity_id AND ' . $joinCondition, + [] + ) + // Prevent staging module preview that adds additional conditions + ->setPart('disable_staging_preview', true); + } +} \ No newline at end of file diff --git a/Staging/Model/Source/StagingMode.php b/Staging/Model/Source/StagingMode.php new file mode 100644 index 0000000..52d1576 --- /dev/null +++ b/Staging/Model/Source/StagingMode.php @@ -0,0 +1,71 @@ + + * @copyright 2017 Smile + * @package Pimgento\Staging\Model\Source + */ +class StagingMode implements \Magento\Framework\Option\ArrayInterface +{ + + /** + * @var \Pimgento\Staging\Helper\Config + */ + protected $configHelper; + + /** + * PHP Constructor to get the module manager to be able to display proper options. + * + * @param \Pimgento\Staging\Helper\Config $configHelper + */ + public function __construct( + \Pimgento\Staging\Helper\Config $configHelper + ) { + $this->configHelper = $configHelper; + } + + + /** + * Retrieve Insertion method Option array + * + * @return array + */ + public function toOptionArray() + { + // Allow staging mode only if the catalog staging modules are enabled. + if ($this->configHelper->isCatalogStagingModulesEnabled()) { + $options = [ + [ + 'value' => \Pimgento\Staging\Helper\Config::STAGING_MODE_LAST, + 'label' => __('Update Last Created Stage') + ], + [ + 'value' => \Pimgento\Staging\Helper\Config::STAGING_MODE_CURRENT, + 'label' => __('Update Current Stage') + ], + [ + 'value' => \Pimgento\Staging\Helper\Config::STAGING_MODE_ALL, + 'label' => __('Update All Stages') + ], + [ + 'value' => \Pimgento\Staging\Helper\Config::STAGING_MODE_FULL, + 'label' => __('Full - Require "to" and "from" columns)') + ] + ]; + } else { + $options = [ + [ + 'value' => \Pimgento\Staging\Helper\Config::STAGING_MODE_DISABLED, + 'label' => __("Disabled - Staging isn't activeted") + ], + ]; + } + + return $options; + } +} \ No newline at end of file diff --git a/Staging/etc/module.xml b/Staging/etc/module.xml new file mode 100644 index 0000000..e5f6c49 --- /dev/null +++ b/Staging/etc/module.xml @@ -0,0 +1,9 @@ + + + + + + + + + \ No newline at end of file diff --git a/Staging/registration.php b/Staging/registration.php new file mode 100644 index 0000000..4806c0d --- /dev/null +++ b/Staging/registration.php @@ -0,0 +1,7 @@ +