diff --git a/vendor/magento/module-bundle-staging/Model/Product/BundleProductCopier.php b/vendor/magento/module-bundle-staging/Model/Product/BundleProductCopier.php index 5993c9894bbe..3ab0a6db6659 100644 --- a/vendor/magento/module-bundle-staging/Model/Product/BundleProductCopier.php +++ b/vendor/magento/module-bundle-staging/Model/Product/BundleProductCopier.php @@ -95,6 +95,32 @@ private function copyBundleProductOption(int $fromRowId, int $toRowId): void $connection->query($query); } + /** + * Copy Bundle product option value for staging + * + * @param int $fromRowId + * @param int $toRowId + * @return void + * @throws Exception + */ + private function copyBundleProductOptionValue(int $fromRowId, int $toRowId): void + { + $connection = $this->getConnection(); + $bundleOptionValueTable = $this->resourceConnection->getTableName('catalog_product_bundle_option_value'); + $select = $connection->select() + ->from($bundleOptionValueTable, '') + ->where('parent_product_id = ?', $fromRowId); + $insertColumns = [ + 'option_id' => 'option_id', + 'store_id' => 'store_id', + 'title' => 'title', + 'parent_product_id' => new \Zend_Db_Expr($toRowId), + ]; + $select->columns($insertColumns); + $query = $select->insertFromSelect($bundleOptionValueTable, array_keys($insertColumns), true); + $connection->query($query); + } + /** * Copy Bundle product selection for staging * @@ -168,6 +194,7 @@ public function copy(array $entityData, int $from, int $to): void $toRowId = $this->getLinkId($metadata, $entityId, $to); $this->copyBundleProductOption($fromRowId, $toRowId); + $this->copyBundleProductOptionValue($fromRowId, $toRowId); $this->copyBundleProductSelection($fromRowId, $toRowId); $this->copyBundleProductRelation($fromRowId, $toRowId); } diff --git a/vendor/magento/module-bundle-staging/Model/Product/CopyHandler.php b/vendor/magento/module-bundle-staging/Model/Product/CopyHandler.php new file mode 100644 index 000000000000..4bd951015b33 --- /dev/null +++ b/vendor/magento/module-bundle-staging/Model/Product/CopyHandler.php @@ -0,0 +1,56 @@ +metadataPool->getHydrator(ProductInterface::class); + $entityData = $hydrator->extract($entity); + $this->bundleProductCopier->copy($entityData, $from, $to); + $this->selectionCopier->copy($entityData, $from, $to); + return $entity; + } +} diff --git a/vendor/magento/module-bundle-staging/Model/Product/DeleteHandler.php b/vendor/magento/module-bundle-staging/Model/Product/DeleteHandler.php index 4fc5deb34c4b..c696267638c0 100644 --- a/vendor/magento/module-bundle-staging/Model/Product/DeleteHandler.php +++ b/vendor/magento/module-bundle-staging/Model/Product/DeleteHandler.php @@ -21,16 +21,13 @@ namespace Magento\BundleStaging\Model\Product; -use Magento\Bundle\Api\Data\OptionInterface; -use Magento\Bundle\Api\ProductLinkManagementInterface; -use Magento\Bundle\Api\ProductOptionRepositoryInterface as OptionRepository; -use Magento\Bundle\Model\Product\CheckOptionLinkIfExist; +use Magento\BundleStaging\Model\ResourceModel\Option; +use Magento\BundleStaging\Model\ResourceModel\Selection; use Magento\Catalog\Api\Data\ProductInterface; use Magento\Catalog\Model\Product\Type; use Magento\Framework\EntityManager\MetadataPool; use Magento\Framework\EntityManager\Operation\ExtensionInterface; -use Magento\Framework\Exception\InputException; -use Magento\Framework\Exception\NoSuchEntityException; +use Magento\Staging\Model\VersionManager; /** * Bundle product Delete Handler @@ -38,21 +35,23 @@ class DeleteHandler implements ExtensionInterface { /** - * @param OptionRepository $optionRepository - * @param CheckOptionLinkIfExist $checkOptionLinkIfExist - * @param ProductLinkManagementInterface $productLinkManagement + * @param VersionManager $versionManager + * @param MetadataPool $metadataPool + * @param Option $optionResource + * @param Selection $selectionResource */ public function __construct( - private readonly OptionRepository $optionRepository, - private readonly CheckOptionLinkIfExist $checkOptionLinkIfExist, - private readonly ProductLinkManagementInterface $productLinkManagement, + private readonly VersionManager $versionManager, + private readonly MetadataPool $metadataPool, + private readonly Option $optionResource, + private readonly Selection $selectionResource ) { } /** * Removes Bundle product Links and Options * - * @param object $entity + * @param ProductInterface $entity * @param array $arguments * * @return ProductInterface|object @@ -65,41 +64,16 @@ public function execute($entity, $arguments = []) return $entity; } - $options = $this->optionRepository->getListByProduct($entity); - if (!$options) { - return $entity; - } - - foreach ($options as $option) { - $this->removeOptionLinks($entity->getData('sku'), $option); - $this->optionRepository->delete($option); + if (!$this->versionManager->isPreviewVersion() && !array_key_exists('created_in', $arguments)) { + // The main product is being deleted. Clean up all sequence entries. + $this->selectionResource->deleteByParentProductIdentifierId((int) $entity->getId()); + $this->optionResource->deleteByParentProductIdentifierId((int) $entity->getId()); + } else { + $linkField = $this->metadataPool->getMetadata(ProductInterface::class)->getLinkField(); + $this->selectionResource->deleteByParentProductId((int) $entity->getData($linkField)); + $this->optionResource->deleteByParentProductId((int) $entity->getData($linkField)); } return $entity; } - - /** - * Remove option product links - * - * @param string $entitySku - * @param OptionInterface $option - * - * @return void - * @throws InputException - * @throws NoSuchEntityException - */ - private function removeOptionLinks($entitySku, $option) - { - $links = $option->getProductLinks(); - if (empty($links)) { - return; - } - - foreach ($links as $link) { - $linkCanBeDeleted = $this->checkOptionLinkIfExist->execute($entitySku, $option, $link); - if ($linkCanBeDeleted && $option->getId()) { - $this->productLinkManagement->removeChild($entitySku, $option->getId(), $link->getSku()); - } - } - } } diff --git a/vendor/magento/module-bundle-staging/Model/Product/UpdateHandler.php b/vendor/magento/module-bundle-staging/Model/Product/UpdateHandler.php new file mode 100644 index 000000000000..5ae2743418bf --- /dev/null +++ b/vendor/magento/module-bundle-staging/Model/Product/UpdateHandler.php @@ -0,0 +1,59 @@ +getTypeId() === Type::TYPE_BUNDLE) { + if (!empty($arguments['is_rollback']) && !empty($arguments['copy_origin_in'])) { + // Copy new options and selections from origin version to the rollback version + // This will ensure that option and selection IDs are the same as in the origin version + $this->copyHandler->execute($entity, $arguments); + } + if ($entity->hasData('_cache_instance_options_collection')) { + $entity->unsetData('_cache_instance_options_collection'); + } + } + return $this->saveHandler->execute($entity, $arguments); + } +} diff --git a/vendor/magento/module-bundle-staging/Model/ResourceModel/ObjectRelationProcessor.php b/vendor/magento/module-bundle-staging/Model/ResourceModel/ObjectRelationProcessor.php new file mode 100644 index 000000000000..163afbf6bb5b --- /dev/null +++ b/vendor/magento/module-bundle-staging/Model/ResourceModel/ObjectRelationProcessor.php @@ -0,0 +1,48 @@ +quoteInto('parent_id=?', $involvedData['parent_id']) + ); + } + parent::delete($transactionManager, $connection, $table, $condition, $involvedData); + } +} diff --git a/vendor/magento/module-bundle-staging/Model/ResourceModel/Option.php b/vendor/magento/module-bundle-staging/Model/ResourceModel/Option.php new file mode 100644 index 000000000000..8de626aa2687 --- /dev/null +++ b/vendor/magento/module-bundle-staging/Model/ResourceModel/Option.php @@ -0,0 +1,166 @@ +metadataPool = $metadataPool; + parent::__construct($context, $validator, $metadataPool, $entityManager, $connectionName); + } + + /** + * Get bundle option IDs by parent product ID. + * + * @param int $parenProductId + * @return array + * @throws LocalizedException + */ + public function getIdsByParentProductId(int $parenProductId): array + { + $select = $this->getConnection()->select() + ->from($this->getMainTable(), ['option_id']) + ->where('parent_id = ?', $parenProductId); + + return $this->getConnection()->fetchCol($select); + } + + /** + * Delete bundle option sequence values. + * + * Deletes only those sequence values that are not referenced in the main table. + * + * @param array $ids + * @return void + */ + public function deleteSequence(array $ids): void + { + $select = $this->getConnection()->select() + ->from($this->getTable(self::SEQUENCE_TABLE)) + ->joinLeft( + $this->getMainTable(), + 'sequence_value = option_id' + ) + ->where('sequence_value IN (?)', $ids) + ->where('option_id IS NULL'); + $this->getConnection()->query( + $this->getConnection() + ->deleteFromSelect($select, $this->getTable(self::SEQUENCE_TABLE)) + ); + } + + /** + * Delete bundle options by parent product ID. + * + * @param int $parenProductId Parent product ID + * @return array List of deleted IDs + */ + public function deleteByParentProductId(int $parenProductId): array + { + $idsToDelete = $this->getIdsByParentProductId($parenProductId); + if (!empty($idsToDelete)) { + $this->getConnection()->delete( + $this->getMainTable(), + [ + 'option_id IN (?)' => $idsToDelete, + 'parent_id = ?' => $parenProductId + ] + ); + $this->getConnection()->delete( + $this->getTable(self::VALUE_TABLE), + [ + 'option_id IN (?)' => $idsToDelete, + 'parent_product_id = ?' => $parenProductId + ] + ); + $this->deleteSequence($idsToDelete); + } + + return $idsToDelete; + } + + /** + * Delete bundle option sequence values by parent product identifier (entity) ID. + * + * @param int $id Parent product identifier (entity) ID + * @return array List of deleted IDs + */ + public function deleteByParentProductIdentifierId(int $id): array + { + $linkField = $this->metadataPool->getMetadata(ProductInterface::class)->getLinkField(); + $identifierField = $this->metadataPool->getMetadata(ProductInterface::class)->getIdentifierField(); + $selectBuilder = fn (string $table, string $parentIdField = 'parent_id') => $this->getConnection() + ->select() + ->from($table, ['option_id']) + ->join( + ['e' => $this->getTable('catalog_product_entity')], + $parentIdField . ' = e.' . $linkField + ) + ->where('e.' . $identifierField . ' = ?', $id) + ->setPart('disable_staging_preview', true); + $idsToDelete = $this->getConnection()->fetchCol($selectBuilder($this->getMainTable())); + if (!empty($idsToDelete)) { + $this->getConnection()->query( + $this->getConnection() + ->deleteFromSelect( + $selectBuilder($this->getMainTable()), + $this->getMainTable() + ) + ); + $this->getConnection()->query( + $this->getConnection() + ->deleteFromSelect( + $selectBuilder($this->getTable(self::VALUE_TABLE), 'parent_product_id'), + $this->getTable(self::VALUE_TABLE) + ) + ); + $this->deleteSequence($idsToDelete); + } + return $idsToDelete; + } +} diff --git a/vendor/magento/module-bundle-staging/Model/ResourceModel/Selection.php b/vendor/magento/module-bundle-staging/Model/ResourceModel/Selection.php new file mode 100644 index 000000000000..7119ac95b42c --- /dev/null +++ b/vendor/magento/module-bundle-staging/Model/ResourceModel/Selection.php @@ -0,0 +1,123 @@ +getConnection()->select() + ->from($this->getMainTable(), ['selection_id']) + ->where('parent_product_id = ?', $parenProductId); + + if (!empty($excludeIds)) { + $select->where('selection_id NOT IN (?)', $excludeIds); + } + + return $this->getConnection()->fetchCol($select); + } + + /** + * Delete bundle selection sequence values. + * + * Deletes only those IDs that are not referenced in the main table. + * + * @param array $ids + * @return void + */ + public function deleteSequence(array $ids): void + { + $select = $this->getConnection()->select() + ->from($this->getTable(self::SEQUENCE_TABLE)) + ->joinLeft($this->getMainTable(), 'sequence_value = selection_id') + ->where('sequence_value IN (?)', $ids) + ->where('selection_id IS NULL'); + $this->getConnection()->query( + $this->getConnection() + ->deleteFromSelect($select, $this->getTable(self::SEQUENCE_TABLE)) + ); + } + + /** + * Delete bundle selections by parent product ID. + * + * @param int $parenProductId Parent product ID + * @return array List of deleted IDs + */ + public function deleteByParentProductId(int $parenProductId): array + { + $idsToDelete = $this->getIdsByParentProductId($parenProductId); + if (!empty($idsToDelete)) { + $this->getConnection()->delete( + $this->getMainTable(), + [ + 'selection_id IN (?)' => $idsToDelete, + 'parent_product_id = ?' => $parenProductId + ] + ); + $this->deleteSequence($idsToDelete); + } + + return $idsToDelete; + } + + /** + * Delete bundle selection sequence values parent product identifier (entity) ID. + * + * @param int $id Parent product identifier (entity) ID + * @return array List of deleted IDs + */ + public function deleteByParentProductIdentifierId(int $id): array + { + $linkField = $this->metadataPool->getMetadata(ProductInterface::class)->getLinkField(); + $identifierField = $this->metadataPool->getMetadata(ProductInterface::class)->getIdentifierField(); + $select = $this->getConnection()->select() + ->from($this->getMainTable(), ['selection_id']) + ->join( + ['e' => $this->getTable('catalog_product_entity')], + 'parent_product_id = e.' . $linkField + ) + ->where('e.'. $identifierField .' = ?', $id) + ->setPart('disable_staging_preview', true); + $idsToDelete = $this->getConnection()->fetchCol($select); + if (!empty($idsToDelete)) { + $this->getConnection()->query( + $this->getConnection() + ->deleteFromSelect( + $select, + $this->getMainTable() + ) + ); + $this->deleteSequence($idsToDelete); + } + return $idsToDelete; + } +} diff --git a/vendor/magento/module-bundle-staging/Plugin/Bundle/Model/ResourceModel/BundlePlugin.php b/vendor/magento/module-bundle-staging/Plugin/Bundle/Model/ResourceModel/BundlePlugin.php index 456707dcf413..84526674ba44 100644 --- a/vendor/magento/module-bundle-staging/Plugin/Bundle/Model/ResourceModel/BundlePlugin.php +++ b/vendor/magento/module-bundle-staging/Plugin/Bundle/Model/ResourceModel/BundlePlugin.php @@ -22,8 +22,7 @@ namespace Magento\BundleStaging\Plugin\Bundle\Model\ResourceModel; use Magento\Bundle\Model\ResourceModel\Bundle; -use Magento\Framework\App\ResourceConnection; -use Magento\Framework\App\RequestInterface; +use Magento\BundleStaging\Model\ResourceModel\Selection; /** * Plugin for Bundle product selections @@ -31,27 +30,11 @@ class BundlePlugin { /** - * @var ResourceConnection - */ - private ResourceConnection $resourceConnection; - - /** - * @var RequestInterface - */ - private RequestInterface $request; - - /** - * Constructor - * - * @param ResourceConnection $resourceConnection - * @param RequestInterface $request + * @param Selection $selectionResource */ public function __construct( - ResourceConnection $resourceConnection, - RequestInterface $request + private readonly Selection $selectionResource ) { - $this->resourceConnection = $resourceConnection; - $this->request = $request; } /** @@ -61,6 +44,7 @@ public function __construct( * @param \Closure $proceed * @param int $productId * @param array $ids + * @return mixed * @SuppressWarnings(PHPMD.UnusedFormalParameter) */ public function aroundDropAllUnneededSelections( @@ -68,66 +52,13 @@ public function aroundDropAllUnneededSelections( \Closure $proceed, $productId, $ids - ) { - $activeStagingUpdate = $this->isActiveStagingUpdate(); - $isSaveClicked = $this->isSaveButtonClicked(); - - if ($activeStagingUpdate || $isSaveClicked) { - return $proceed($productId, $ids); - } - - $connection = $this->resourceConnection->getConnection(); - $where = [$connection->quoteInto('parent_product_id = ?', $productId)]; - if (!empty($ids)) { - $where[] = $connection->quoteInto('selection_id NOT IN (?)', $ids); + ): mixed { + $idsToDelete = $this->selectionResource->getIdsByParentProductId((int) $productId, $ids ? (array) $ids : []); + $result = $proceed($productId, $ids); + if (!empty($idsToDelete)) { + $this->selectionResource->deleteSequence($idsToDelete); } - $where = implode(' AND ', $where); - $select = $connection->select() - ->from($subject->getTable('catalog_product_bundle_selection'), ['selection_id']) - ->where($where); - - $selection_id = $connection->fetchOne($select); - - $condition = ['sequence_value = ?' => $selection_id]; - try { - $connection->delete($subject->getTable('sequence_product_bundle_selection'), $condition); - // phpcs:ignore Magento2.CodeAnalysis.EmptyBlock.DetectedCatch - } catch (\Exception $e) { - // nothing to delete - } - } - - /** - * Check if there is an active staging update. - * - * @return bool - */ - private function isActiveStagingUpdate(): bool - { - $connection = $this->resourceConnection->getConnection(); - $stagingTable = $this->resourceConnection->getTableName('staging_update'); - - $currentDate = (new \DateTime())->format('Y-m-d H:i:s'); - - $select = $connection->select() - ->from($stagingTable, ['id']) - ->where('start_time <= ?', $currentDate) - ->where('moved_to >= ?', $currentDate) - ->where('is_rollback = ?', 0) - ->limit(1); - - return (bool)$connection->fetchOne($select); - } - - /** - * Check for the save operation. - * - * @return bool - */ - private function isSaveButtonClicked(): bool - { - return isset($this->request->getParam('staging')['mode']) - && $this->request->getParam('staging')['mode'] == 'save'; + return $result; } } diff --git a/vendor/magento/module-bundle-staging/Plugin/Bundle/Model/ResourceModel/OptionPlugin.php b/vendor/magento/module-bundle-staging/Plugin/Bundle/Model/ResourceModel/OptionPlugin.php index bed0976c9f26..12f5058cc84b 100644 --- a/vendor/magento/module-bundle-staging/Plugin/Bundle/Model/ResourceModel/OptionPlugin.php +++ b/vendor/magento/module-bundle-staging/Plugin/Bundle/Model/ResourceModel/OptionPlugin.php @@ -22,6 +22,7 @@ namespace Magento\BundleStaging\Plugin\Bundle\Model\ResourceModel; use Magento\Bundle\Model\ResourceModel\Option; +use Magento\BundleStaging\Model\ResourceModel\Option as OptionResourceModel; use Magento\Framework\Model\AbstractModel; /** @@ -29,32 +30,31 @@ */ class OptionPlugin { + /** + * @param OptionResourceModel $optionResource + */ + public function __construct( + private readonly OptionResourceModel $optionResource + ) { + } + /** * Delete Bundle Option sequence. * * @param Option $subject - * @param \Closure $proceed + * @param mixed $result * @param AbstractModel $option + * @return mixed + * @SuppressWarnings(PHPMD.UnusedFormalParameter) */ - public function aroundDelete( + public function afterDelete( Option $subject, - \Closure $proceed, + mixed $result, AbstractModel $option - ) { + ): mixed { $optionId = $option->getId(); - $result = $proceed($option); if ($optionId) { - try { - $subject->getConnection()->delete( - $subject->getTable('sequence_product_bundle_option'), - [ - 'sequence_value = ?' => $optionId - ] - ); - // phpcs:ignore Magento2.CodeAnalysis.EmptyBlock.DetectedCatch - } catch (\Exception $e) { - // nothing to delete - } + $this->optionResource->deleteSequence([$optionId]); } return $result; diff --git a/vendor/magento/module-bundle-staging/etc/di.xml b/vendor/magento/module-bundle-staging/etc/di.xml index 2e9b92e0d9aa..3a9f840e226a 100644 --- a/vendor/magento/module-bundle-staging/etc/di.xml +++ b/vendor/magento/module-bundle-staging/etc/di.xml @@ -73,7 +73,7 @@ Magento\BundleStaging\Model\Product\SaveHandler - Magento\BundleStaging\Model\Product\SaveHandler + Magento\BundleStaging\Model\Product\UpdateHandler Magento\BundleStaging\Model\Product\DeleteHandler @@ -83,9 +83,17 @@ + + Magento\BundleStaging\Model\ResourceModel\Db\Context + + + + Magento\BundleStaging\Model\ResourceModel\ObjectRelationProcessor + +